diff --git a/client/pool.go b/client/pool.go index fd8491fb..4b0e205a 100644 --- a/client/pool.go +++ b/client/pool.go @@ -1,152 +1,80 @@ package client import ( - "fmt" - "log" + "github.com/go-routeros/routeros" ) type Pool struct { - Id string `mikrotik:".id"` - Name string `mikrotik:"name"` - Ranges string `mikrotik:"ranges"` - NextPool string `mikrotik:"next-pool"` - Comment string `mikrotik:"comment"` + Id string `mikrotik:".id" codegen:"id,mikrotikID,terraformID"` + Name string `mikrotik:"name" codegen:"name,required"` + Ranges string `mikrotik:"ranges" codegen:"ranges,required"` + NextPool string `mikrotik:"next-pool" codegen:"next_pool,optiona,computed"` + Comment string `mikrotik:"comment" codegen:"comment,optional,computed"` } -func (client Mikrotik) AddPool(p *Pool) (*Pool, error) { - c, err := client.getMikrotikClient() +var _ Resource = (*Pool)(nil) - if err != nil { - return nil, err - } - cmd := Marshal("/ip/pool/add", p) - log.Printf("[INFO] Running the mikrotik command: `%s`", cmd) - r, err := c.RunArgs(cmd) - - log.Printf("[DEBUG] Pool creation response: `%v`", r) - - if err != nil { - return nil, err - } - - id := r.Done.Map["ret"] - - return client.FindPool(id) +func (b *Pool) ActionToCommand(a Action) string { + return map[Action]string{ + Add: "/ip/pool/add", + Find: "/ip/pool/print", + Update: "/ip/pool/set", + Delete: "/ip/pool/remove", + }[a] } -func (client Mikrotik) ListPools() ([]Pool, error) { - c, err := client.getMikrotikClient() - - if err != nil { - return nil, err - } - cmd := []string{"/ip/pool/print"} - log.Printf("[INFO] Running the mikrotik command: `%s`", cmd) - r, err := c.RunArgs(cmd) - - if err != nil { - return nil, err - } - log.Printf("[DEBUG] Found pools: %v", r) - - pools := []Pool{} - - err = Unmarshal(*r, &pools) - - if err != nil { - return nil, err - } - - return pools, nil +func (b *Pool) IDField() string { + return ".id" } -func (client Mikrotik) FindPool(id string) (*Pool, error) { - c, err := client.getMikrotikClient() - - if err != nil { - return nil, err - } - cmd := []string{"/ip/pool/print", "?.id=" + id} - - log.Printf("[INFO] Running the mikrotik command: `%s`", cmd) - r, err := c.RunArgs(cmd) - - log.Printf("[DEBUG] Pool response: %v", r) - - if err != nil { - return nil, err - } - - pool := Pool{} - err = Unmarshal(*r, &pool) - - if err != nil { - return nil, err - } - if pool.Id == "" { - return nil, NewNotFound(fmt.Sprintf("pool `%s` not found", id)) - } - - return &pool, nil +func (b *Pool) ID() string { + return b.Id } -func (client Mikrotik) FindPoolByName(name string) (*Pool, error) { - c, err := client.getMikrotikClient() - - if err != nil { - return nil, err - } - cmd := []string{"/ip/pool/print", "?name=" + name} - log.Printf("[INFO] Running the mikrotik command: `%s`", cmd) - r, err := c.RunArgs(cmd) - - log.Printf("[DEBUG] Pool response: %v", r) +func (b *Pool) SetID(id string) { + b.Id = id +} - if err != nil { - return nil, err - } +func (b *Pool) AfterAddHook(r *routeros.Reply) { + b.Id = r.Done.Map["ret"] +} - pool := Pool{} - err = Unmarshal(*r, &pool) +// Typed wrappers +func (c Mikrotik) AddPool(r *Pool) (*Pool, error) { + return r.processResourceErrorTuplePtr(c.Add(r)) +} - if err != nil { - return nil, err - } +func (c Mikrotik) UpdatePool(r *Pool) (*Pool, error) { + return r.processResourceErrorTuplePtr(c.Update(r)) +} - if pool.Name == "" { - return nil, NewNotFound(fmt.Sprintf("pool `%s` not found", name)) - } +func (c Mikrotik) FindPool(id string) (*Pool, error) { + return Pool{}.processResourceErrorTuplePtr(c.Find(&Pool{Id: id})) +} - return &pool, nil +func (c Mikrotik) FindPoolByName(name string) (*Pool, error) { + return Pool{}.processResourceErrorTuplePtr(c.findByField(&Pool{}, "name", name)) } -func (client Mikrotik) UpdatePool(p *Pool) (*Pool, error) { - c, err := client.getMikrotikClient() +func (c Mikrotik) DeletePool(id string) error { + return c.Delete(&Pool{Id: id}) +} +func (c Mikrotik) ListPools() ([]Pool, error) { + res, err := c.List(&Pool{}) if err != nil { return nil, err } - - cmd := Marshal("/ip/pool/set", p) - log.Printf("[INFO] Running the mikrotik command: `%s`", cmd) - _, err = c.RunArgs(cmd) - - if err != nil { - return nil, err + returnSlice := make([]Pool, len(res)) + for i, v := range res { + returnSlice[i] = *(v.(*Pool)) } - - return client.FindPool(p.Id) + return returnSlice, nil } -func (client Mikrotik) DeletePool(id string) error { - c, err := client.getMikrotikClient() - +func (b Pool) processResourceErrorTuplePtr(r Resource, err error) (*Pool, error) { if err != nil { - return err + return nil, err } - - cmd := []string{"/ip/pool/remove", "=.id=" + id} - log.Printf("[INFO] Running the mikrotik command: `%s`", cmd) - _, err = c.RunArgs(cmd) - return err + return r.(*Pool), nil } diff --git a/docs/resources/pool.md b/docs/resources/pool.md index fab0248c..f3490467 100644 --- a/docs/resources/pool.md +++ b/docs/resources/pool.md @@ -25,7 +25,7 @@ resource "mikrotik_pool" "pool" { ### Read-Only -- `id` (String) The ID of this resource. +- `id` (String) ID of this resource. ## Import Import is supported using the following syntax: diff --git a/go.work.sum b/go.work.sum index 56f9d847..6872dabb 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1 +1,2 @@ github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= diff --git a/mikrotik/provider.go b/mikrotik/provider.go index 8cf22dd6..925e5b3b 100644 --- a/mikrotik/provider.go +++ b/mikrotik/provider.go @@ -75,7 +75,6 @@ func Provider(client *mt.Mikrotik) *schema.Provider { "mikrotik_interface_list": resourceInterfaceList(), "mikrotik_ip_address": resourceIpAddress(), "mikrotik_firewall_filter_rule": resourceFirewallFilterRule(), - "mikrotik_pool": resourcePool(), }, } diff --git a/mikrotik/provider_framework.go b/mikrotik/provider_framework.go index d11e0fb2..ed850528 100644 --- a/mikrotik/provider_framework.go +++ b/mikrotik/provider_framework.go @@ -187,6 +187,7 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res NewInterfaceWireguardPeerResource, NewInterfaceWireguardResource, NewIpv6AddressResource, + NewPoolResource, NewSchedulerResource, NewScriptResource, NewVlanInterfaceResource, diff --git a/mikrotik/resource_pool.go b/mikrotik/resource_pool.go index 9c0c3526..f563c3f9 100644 --- a/mikrotik/resource_pool.go +++ b/mikrotik/resource_pool.go @@ -5,143 +5,156 @@ import ( "github.com/ddelnano/terraform-provider-mikrotik/client" "github.com/ddelnano/terraform-provider-mikrotik/mikrotik/internal/utils" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + tftypes "github.com/hashicorp/terraform-plugin-framework/types" ) -func resourcePool() *schema.Resource { - return &schema.Resource{ - Description: "Creates a Mikrotik IP Pool.", +type pool struct { + client *client.Mikrotik +} - CreateContext: resourcePoolCreate, - ReadContext: resourcePoolRead, - UpdateContext: resourcePoolUpdate, - DeleteContext: resourcePoolDelete, - Importer: &schema.ResourceImporter{ - StateContext: utils.ImportStateContextUppercaseWrapper(schema.ImportStatePassthroughContext), - }, +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &pool{} + _ resource.ResourceWithConfigure = &pool{} + _ resource.ResourceWithImportState = &pool{} +) + +// NewPoolResource is a helper function to simplify the provider implementation. +func NewPoolResource() resource.Resource { + return &pool{} +} - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, +func (r *pool) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + r.client = req.ProviderData.(*client.Mikrotik) +} + +// Metadata returns the resource type name. +func (r *pool) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_pool" +} + +// Schema defines the schema for the resource. +func (s *pool) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Creates a Mikrotik IP Pool.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Description: "ID of this resource.", + }, + "name": schema.StringAttribute{ Required: true, Description: "The name of IP pool.", }, - "ranges": { - Type: schema.TypeString, + "ranges": schema.StringAttribute{ Required: true, Description: "The IP range(s) of the pool. Multiple ranges can be specified, separated by commas: `172.16.0.6-172.16.0.12,172.16.0.50-172.16.0.60`.", }, - "next_pool": { - Type: schema.TypeString, + "next_pool": schema.StringAttribute{ Optional: true, - StateFunc: func(i interface{}) string { - v := i.(string) - // handle special case for 'none' string: - // it behaves the same as an empty string - unsets the value - // and MikroTik API will return an empty string, but we don't wont diff on '' != 'none' - if v == "none" { - return "" - } - - return v + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), }, Description: "The IP pool to pick next address from if current is exhausted.", }, - "comment": { - Type: schema.TypeString, + "comment": schema.StringAttribute{ Optional: true, + Computed: true, Description: "The comment of the IP Pool to be created.", }, }, } } -func resourcePoolCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - p := preparePool(d) +// Create creates the resource and sets the initial Terraform state. +func (r *pool) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var terraformModel poolModel + var mikrotikModel client.Pool - c := m.(*client.Mikrotik) - - pool, err := c.AddPool(p) - if err != nil { - return diag.FromErr(err) - } - - return poolToData(pool, d) + GenericCreateResource(&terraformModel, &mikrotikModel, r.client)(ctx, req, resp) } -func resourcePoolRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(*client.Mikrotik) +// Read refreshes the Terraform state with the latest data. +func (r *pool) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var terraformModel poolModel + var mikrotikModel client.Pool - pool, err := c.FindPool(d.Id()) + GenericReadResource(&terraformModel, &mikrotikModel, r.client)(ctx, req, resp) +} - if client.IsNotFoundError(err) { - d.SetId("") - return nil +// Update updates the resource and sets the updated Terraform state on success. +// +// The body is a copy-paste code from `GenericUpdateResource()` functions. +// It's done to support special case of 'unsetting' the 'next_pool' field. +// Since RouterOS API does not support empty value `""` for this field, +// a 'magic' value of 'none' is used. +// In that case, Terraform argues that planned value was `none` but actual (after Read() method) is `""` +// The only difference from `GenericUpdateResource()` in this implementation is checking of +// transition from some value to `""` for `next_pool` field. In that case, we simply change value to `none`, +// so API client can unset this value and subsequent `Read()` method will see `""` which is the same as config value. +// +// Be aware, that this hack prevents using `none` value explicitly in the configuration. +func (r *pool) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var terraformModel, state poolModel + var mikrotikModel client.Pool + resp.Diagnostics.Append(req.Plan.Get(ctx, &terraformModel)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return } - if err != nil { - return diag.FromErr(err) + // if practitioner sets this value to `""` to unset field in remote system, + // implicitly send `"none"` via API + if !terraformModel.NextPool.Equal(state.NextPool) && terraformModel.NextPool.ValueString() == "" { + terraformModel.NextPool = tftypes.StringValue("none") } - return poolToData(pool, d) -} - -func resourcePoolUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(*client.Mikrotik) - - p := preparePool(d) - p.Id = d.Id() - - pool, err := c.UpdatePool(p) - - if err != nil { - return diag.FromErr(err) + if err := utils.TerraformModelToMikrotikStruct(&terraformModel, &mikrotikModel); err != nil { + resp.Diagnostics.AddError("Cannot copy model: Terraform -> MikroTik", err.Error()) + return } - - return poolToData(pool, d) -} - -func resourcePoolDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(*client.Mikrotik) - - err := c.DeletePool(d.Id()) - + updated, err := r.client.Update(&mikrotikModel) if err != nil { - return diag.FromErr(err) - } - - d.SetId("") - return nil -} - -func poolToData(pool *client.Pool, d *schema.ResourceData) diag.Diagnostics { - values := map[string]interface{}{ - "name": pool.Name, - "ranges": pool.Ranges, - "next_pool": pool.NextPool, - "comment": pool.Comment, + resp.Diagnostics.AddError("Update failed", err.Error()) + return } - - d.SetId(pool.Id) - - var diags diag.Diagnostics - - for key, value := range values { - if err := d.Set(key, value); err != nil { - diags = append(diags, diag.Errorf("failed to set %s: %v", key, err)...) - } + if err := utils.MikrotikStructToTerraformModel(updated, &terraformModel); err != nil { + resp.Diagnostics.AddError("Cannot copy model: MikroTik -> Terraform", err.Error()) + return } - return diags + resp.Diagnostics.Append(resp.State.Set(ctx, terraformModel)...) } -func preparePool(d *schema.ResourceData) *client.Pool { - pool := new(client.Pool) +// Delete deletes the resource and removes the Terraform state on success. +func (r *pool) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var terraformModel poolModel + var mikrotikModel client.Pool + GenericDeleteResource(&terraformModel, &mikrotikModel, r.client)(ctx, req, resp) +} - pool.Name = d.Get("name").(string) - pool.NextPool = d.Get("next_pool").(string) - pool.Ranges = d.Get("ranges").(string) - pool.Comment = d.Get("comment").(string) +func (r *pool) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to id attribute + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} - return pool +type poolModel struct { + Id tftypes.String `tfsdk:"id"` + Name tftypes.String `tfsdk:"name"` + Ranges tftypes.String `tfsdk:"ranges"` + NextPool tftypes.String `tfsdk:"next_pool"` + Comment tftypes.String `tfsdk:"comment"` } diff --git a/mikrotik/resource_pool_test.go b/mikrotik/resource_pool_test.go index 751857e3..89d671e3 100644 --- a/mikrotik/resource_pool_test.go +++ b/mikrotik/resource_pool_test.go @@ -36,6 +36,7 @@ func TestAccMikrotikPool_create(t *testing.T) { func TestAccMikrotikPool_createNextPool(t *testing.T) { name := acctest.RandomWithPrefix("pool-create") + nextPoolName := acctest.RandomWithPrefix("next_ip_pool") ranges := fmt.Sprintf("%s,%s", internal.GetNewIpAddrRange(10), internal.GetNewIpAddr()) resourceName := "mikrotik_pool.bar" @@ -45,17 +46,26 @@ func TestAccMikrotikPool_createNextPool(t *testing.T) { CheckDestroy: testAccCheckMikrotikPoolDestroy, Steps: []resource.TestStep{ { - Config: testAccPoolWithNextPool(name, ranges, "next_ip_pool", "next_ip_pool"), + Config: testAccPoolWithNextPool(name, ranges, "", nextPoolName), Check: resource.ComposeAggregateTestCheckFunc( testAccPoolExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "id"), resource.TestCheckResourceAttr(resourceName, "name", name), resource.TestCheckResourceAttr(resourceName, "ranges", ranges), - resource.TestCheckResourceAttr(resourceName, "next_pool", "next_ip_pool"), + resource.TestCheckResourceAttr(resourceName, "next_pool", ""), + ), + }, { + Config: testAccPoolWithNextPool(name, ranges, nextPoolName, nextPoolName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccPoolExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "name", name), + resource.TestCheckResourceAttr(resourceName, "ranges", ranges), + resource.TestCheckResourceAttr(resourceName, "next_pool", nextPoolName), ), }, { - Config: testAccPoolWithNextPool(name, ranges, "none", "next_ip_pool"), + Config: testAccPoolWithNextPool(name, ranges, "", "next_ip_pool"), Check: resource.ComposeAggregateTestCheckFunc( testAccPoolExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "id"),