diff --git a/client/bridge_vlan.go b/client/bridge_vlan.go index 3474c97..f4fa5fa 100644 --- a/client/bridge_vlan.go +++ b/client/bridge_vlan.go @@ -7,11 +7,11 @@ import ( // BridgeVlan defines vlan filtering in bridge resource type BridgeVlan struct { - Id string `mikrotik:".id"` - Bridge string `mikrotik:"bridge"` - Tagged types.MikrotikList `mikrotik:"tagged"` - Untagged types.MikrotikList `mikrotik:"untagged"` - VlanIds types.MikrotikIntList `mikrotik:"vlan-ids"` + Id string `mikrotik:".id" codegen:"id,mikrotikID"` + Bridge string `mikrotik:"bridge" codegen:"bridge,required"` + Tagged types.MikrotikList `mikrotik:"tagged" codegen:"tagged,elemType=String"` + Untagged types.MikrotikList `mikrotik:"untagged" codegen:"untagged,elemType=String"` + VlanIds types.MikrotikIntList `mikrotik:"vlan-ids" codegen:"vlan_ids,elemType=Int64"` } var _ Resource = (*BridgeVlan)(nil) diff --git a/docs/resources/bridge_vlan.md b/docs/resources/bridge_vlan.md index 9a54296..1e6d3cf 100644 --- a/docs/resources/bridge_vlan.md +++ b/docs/resources/bridge_vlan.md @@ -1,5 +1,5 @@ # mikrotik_bridge_vlan (Resource) -Adds VLAN aware Layer2 forwarding and VLAN tag modifications within the bridge. +Creates a MikroTik BridgeVlan. ## Example Usage ```terraform @@ -24,13 +24,13 @@ resource "mikrotik_bridge_vlan" "testacc" { ### Optional -- `tagged` (List of String) Interface list with a VLAN tag adding action in egress. -- `untagged` (List of String) Interface list with a VLAN tag removing action in egress. -- `vlan_ids` (List of Number) The list of VLAN IDs for certain port configuration. Ranges are not supported yet. +- `tagged` (Set of String) Interface list with a VLAN tag adding action in egress. +- `untagged` (Set of String) Interface list with a VLAN tag removing action in egress. +- `vlan_ids` (Set of Number) The list of VLAN IDs for certain port configuration. Ranges are not supported yet. ### Read-Only -- `id` (String) The ID of this resource. +- `id` (String) A unique ID for this resource. ## Import Import is supported using the following syntax: diff --git a/mikrotik/internal/utils/struct_copy.go b/mikrotik/internal/utils/struct_copy.go index c2bbe5a..b59a415 100644 --- a/mikrotik/internal/utils/struct_copy.go +++ b/mikrotik/internal/utils/struct_copy.go @@ -100,24 +100,41 @@ func coreTypeToTerraformType(src, dest reflect.Value) error { case reflect.Float32, reflect.Float64: tfValue = tftypes.Float64Value(src.Float()) case reflect.Slice: - var diag diag.Diagnostics + var diags diag.Diagnostics var elements []interface{} for i := 0; i < src.Len(); i++ { elements = append(elements, src.Index(i).Interface()) } + var tfType attr.Type switch kind := src.Type().Elem().Kind(); kind { case reflect.Bool: - tfValue, diag = tftypes.ListValueFrom(context.TODO(), tftypes.BoolType, elements) + tfType = tftypes.BoolType case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - tfValue, diag = tftypes.ListValueFrom(context.TODO(), tftypes.Int64Type, elements) + tfType = tftypes.Int64Type case reflect.String: - tfValue, diag = tftypes.ListValueFrom(context.TODO(), tftypes.StringType, elements) + tfType = tftypes.StringType default: return fmt.Errorf("unsupported slice element type %q", kind) } + var valueFromFunc func(t attr.Type, elements []interface{}) (attr.Value, diag.Diagnostics) - if diag.HasError() { - return fmt.Errorf("error creating Terraform type: %v", diag.Errors()) + switch dest.Interface().(type) { + case tftypes.List: + valueFromFunc = func(t attr.Type, elements []interface{}) (attr.Value, diag.Diagnostics) { + return tftypes.ListValueFrom(context.TODO(), t, elements) + } + case tftypes.Set: + valueFromFunc = func(t attr.Type, elements []interface{}) (attr.Value, diag.Diagnostics) { + return tftypes.SetValueFrom(context.TODO(), t, elements) + } + default: + return fmt.Errorf("unsupported destination Terraform type %v", reflect.TypeOf(dest).Name()) + } + + tfValue, diags = valueFromFunc(tfType, elements) + + if diags.HasError() { + return fmt.Errorf("error creating Terraform type: %v", diags.Errors()) } } @@ -165,6 +182,40 @@ func terraformTypeToCoreType(src, dest reflect.Value) error { dest.Set(targetPtr.Elem()) + return nil + case tftypes.Set: + var diag diag.Diagnostics + var sliceType reflect.Type + + switch dest.Type().Elem().Kind() { + case reflect.Bool: + sliceType = reflect.TypeOf(true) + case reflect.Int: + sliceType = reflect.TypeOf(int(0)) + case reflect.Int8: + sliceType = reflect.TypeOf(int8(0)) + case reflect.Int16: + sliceType = reflect.TypeOf(int16(0)) + case reflect.Int32: + sliceType = reflect.TypeOf(int32(0)) + case reflect.Int64: + sliceType = reflect.TypeOf(int64(0)) + case reflect.String: + sliceType = reflect.TypeOf("") + default: + return fmt.Errorf("unsupported list element types: %s -> []%s", src.Type().Name(), dest.Type().Elem().Kind()) + } + targetPtr := reflect.New(reflect.SliceOf(sliceType)) + if len(f.Elements()) > 0 { + diag = f.ElementsAs(context.TODO(), targetPtr.Interface(), false) + } + + if diag.HasError() { + return fmt.Errorf("%s", diag.Errors()) + } + + dest.Set(targetPtr.Elem()) + return nil default: return fmt.Errorf("unsupported field type assignment: %s -> %s", src.Type().Name(), dest.Kind()) diff --git a/mikrotik/internal/utils/struct_copy_test.go b/mikrotik/internal/utils/struct_copy_test.go index 5aa7513..54e3eda 100644 --- a/mikrotik/internal/utils/struct_copy_test.go +++ b/mikrotik/internal/utils/struct_copy_test.go @@ -328,13 +328,13 @@ func TestCopyTerraformToMikrotik(t *testing.T) { src: struct { Id tftypes.String Bridge tftypes.String - Tagged tftypes.List + Tagged tftypes.Set Untagged tftypes.List VlanIds tftypes.List }{ Id: tftypes.StringValue("new id field"), Bridge: tftypes.StringValue("new bridge"), - Tagged: tftypes.ListValueMust(tftypes.StringType, []attr.Value{ + Tagged: tftypes.SetValueMust(tftypes.StringType, []attr.Value{ tftypes.StringValue("new tagged 3"), }), Untagged: tftypes.ListValueMust(tftypes.StringType, []attr.Value{ diff --git a/mikrotik/provider.go b/mikrotik/provider.go index 3935a81..de1333a 100644 --- a/mikrotik/provider.go +++ b/mikrotik/provider.go @@ -65,7 +65,6 @@ func Provider(client *mt.Mikrotik) *schema.Provider { }, ResourcesMap: map[string]*schema.Resource{ "mikrotik_bgp_instance": resourceBgpInstance(), - "mikrotik_bridge_vlan": resourceBridgeVlan(), "mikrotik_firewall_filter_rule": resourceFirewallFilterRule(), }, } diff --git a/mikrotik/provider_framework.go b/mikrotik/provider_framework.go index e0065d9..cb5727c 100644 --- a/mikrotik/provider_framework.go +++ b/mikrotik/provider_framework.go @@ -185,6 +185,7 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res NewBgpPeerResource, NewBridgePortResource, NewBridgeResource, + NewBridgeVlanResource, NewDhcpLeaseResource, NewDhcpServerNetworkResource, NewDhcpServerResource, diff --git a/mikrotik/resource_bridge_vlan.go b/mikrotik/resource_bridge_vlan.go index 6539d99..666b416 100644 --- a/mikrotik/resource_bridge_vlan.go +++ b/mikrotik/resource_bridge_vlan.go @@ -5,150 +5,123 @@ 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 resourceBridgeVlan() *schema.Resource { - return &schema.Resource{ - Description: "Adds VLAN aware Layer2 forwarding and VLAN tag modifications within the bridge.", +type bridgeVlan struct { + client *client.Mikrotik +} - CreateContext: resourceBridgeVlanCreate, - ReadContext: resourceBridgeVlanRead, - UpdateContext: resourceBridgeVlanUpdate, - DeleteContext: resourceBridgeVlanDelete, +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &bridgeVlan{} + _ resource.ResourceWithConfigure = &bridgeVlan{} + _ resource.ResourceWithImportState = &bridgeVlan{} +) - Importer: &schema.ResourceImporter{ - StateContext: utils.ImportStateContextUppercaseWrapper(schema.ImportStatePassthroughContext), - }, +// NewBridgeVlanResource is a helper function to simplify the provider implementation. +func NewBridgeVlanResource() resource.Resource { + return &bridgeVlan{} +} - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, +func (r *bridgeVlan) 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 *bridgeVlan) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_bridge_vlan" +} + +// Schema defines the schema for the resource. +func (s *bridgeVlan) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Creates a MikroTik BridgeVlan.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Description: "A unique ID for this resource.", }, - "bridge": { - Type: schema.TypeString, + "bridge": schema.StringAttribute{ Required: true, Description: "The bridge interface which the respective VLAN entry is intended for.", }, - "tagged": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, + "tagged": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: tftypes.StringType, Description: "Interface list with a VLAN tag adding action in egress.", }, - "untagged": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, + "untagged": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: tftypes.StringType, Description: "Interface list with a VLAN tag removing action in egress. ", }, - "vlan_ids": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{ - Type: schema.TypeInt, - }, + "vlan_ids": schema.SetAttribute{ + Optional: true, + Computed: true, + ElementType: tftypes.Int64Type, Description: "The list of VLAN IDs for certain port configuration. Ranges are not supported yet.", }, }, } } -func resourceBridgeVlanCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(*client.Mikrotik) - r, err := c.AddBridgeVlan(dataToBridgeVlan(d)) - if err != nil { - return diag.FromErr(err) - } - d.SetId(r.Id) - - return resourceBridgeVlanRead(ctx, d, m) +// Create creates the resource and sets the initial Terraform state. +func (r *bridgeVlan) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var terraformModel bridgeVlanModel + var mikrotikModel client.BridgeVlan + GenericCreateResource(&terraformModel, &mikrotikModel, r.client)(ctx, req, resp) } -func resourceBridgeVlanRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(*client.Mikrotik) - r, err := c.FindBridgeVlan(d.Id()) - if client.IsNotFoundError(err) { - d.SetId("") - return nil - } - - if err != nil { - return diag.FromErr(err) - } - - return recordBridgeVlanToData(r, d) +// Read refreshes the Terraform state with the latest data. +func (r *bridgeVlan) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var terraformModel bridgeVlanModel + var mikrotikModel client.BridgeVlan + GenericReadResource(&terraformModel, &mikrotikModel, r.client)(ctx, req, resp) } -func resourceBridgeVlanUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(*client.Mikrotik) - r, err := c.UpdateBridgeVlan(dataToBridgeVlan(d)) - if err != nil { - return diag.FromErr(err) - } - return recordBridgeVlanToData(r, d) +// Update updates the resource and sets the updated Terraform state on success. +func (r *bridgeVlan) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var terraformModel bridgeVlanModel + var mikrotikModel client.BridgeVlan + GenericUpdateResource(&terraformModel, &mikrotikModel, r.client)(ctx, req, resp) } -func resourceBridgeVlanDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - c := m.(*client.Mikrotik) - if err := c.DeleteBridgeVlan(d.Id()); err != nil { - return diag.FromErr(err) - } - - return nil +// Delete deletes the resource and removes the Terraform state on success. +func (r *bridgeVlan) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var terraformModel bridgeVlanModel + var mikrotikModel client.BridgeVlan + GenericDeleteResource(&terraformModel, &mikrotikModel, r.client)(ctx, req, resp) } -func dataToBridgeVlan(d *schema.ResourceData) *client.BridgeVlan { - taggedInterface := d.Get("tagged").([]interface{}) - tagged := make([]string, len(taggedInterface)) - for i, v := range taggedInterface { - tagged[i] = v.(string) - } - - untaggedInterface := d.Get("untagged").([]interface{}) - untagged := make([]string, len(untaggedInterface)) - for i, v := range untaggedInterface { - untagged[i] = v.(string) - } - - vlanIDsInterface := d.Get("vlan_ids").([]interface{}) - vlanIDs := make([]int, len(vlanIDsInterface)) - for i, v := range vlanIDsInterface { - vlanIDs[i] = v.(int) - } - - return &client.BridgeVlan{ - Id: d.Id(), - Bridge: d.Get("bridge").(string), - Tagged: tagged, - Untagged: untagged, - VlanIds: vlanIDs, - } +func (r *bridgeVlan) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + // Retrieve import ID and save to id attribute + utils.ImportUppercaseWrapper(resource.ImportStatePassthroughID)(ctx, + path.Root("id"), + req, + resp, + ) } -func recordBridgeVlanToData(r *client.BridgeVlan, d *schema.ResourceData) diag.Diagnostics { - var diags diag.Diagnostics - - if err := d.Set("bridge", r.Bridge); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - if err := d.Set("tagged", r.Tagged); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - if err := d.Set("untagged", r.Untagged); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - if err := d.Set("vlan_ids", r.VlanIds); err != nil { - diags = append(diags, diag.FromErr(err)...) - } - - d.SetId(r.Id) - - return diags +type bridgeVlanModel struct { + Id tftypes.String `tfsdk:"id"` + Bridge tftypes.String `tfsdk:"bridge"` + Tagged tftypes.Set `tfsdk:"tagged"` + Untagged tftypes.Set `tfsdk:"untagged"` + VlanIds tftypes.Set `tfsdk:"vlan_ids"` }