From 4fa2624bfed6687c53f68b9aa32642d8143d5a38 Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Mon, 26 Feb 2024 11:37:15 -0600 Subject: [PATCH 1/7] chore(ci): skip sweeping devices that are not in a deletable state (#589) removed import cycle Signed-off-by: Ayush Rangwala --- equinix/provider.go | 5 +- equinix/resource_metal_vlan.go | 176 ---------------- .../resources/metal/vlans/datasource.go | 32 +-- .../resources/metal/vlans/datasource_test.go | 32 +-- internal/resources/metal/vlans/resource.go | 192 ++++++++++++++++++ .../resources/metal/vlans/resource_test.go | 74 +++++-- 6 files changed, 290 insertions(+), 221 deletions(-) delete mode 100644 equinix/resource_metal_vlan.go rename equinix/data_source_metal_vlan.go => internal/resources/metal/vlans/datasource.go (83%) rename equinix/data_source_metal_vlan_acc_test.go => internal/resources/metal/vlans/datasource_test.go (87%) create mode 100644 internal/resources/metal/vlans/resource.go rename equinix/resource_metal_vlan_acc_test.go => internal/resources/metal/vlans/resource_test.go (61%) diff --git a/equinix/provider.go b/equinix/provider.go index 45dc2f0b9..01814a6e3 100644 --- a/equinix/provider.go +++ b/equinix/provider.go @@ -8,6 +8,7 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/config" metal_project "github.com/equinix/terraform-provider-equinix/internal/resources/metal/project" + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlans" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vrf" "github.com/equinix/ecx-go/v2" @@ -110,7 +111,7 @@ func Provider() *schema.Provider { "equinix_metal_reserved_ip_block": dataSourceMetalReservedIPBlock(), "equinix_metal_spot_market_request": dataSourceMetalSpotMarketRequest(), "equinix_metal_virtual_circuit": dataSourceMetalVirtualCircuit(), - "equinix_metal_vlan": dataSourceMetalVlan(), + "equinix_metal_vlan": vlans.DataSource(), "equinix_metal_vrf": vrf.DataSource(), }, ResourcesMap: map[string]*schema.Resource{ @@ -139,7 +140,7 @@ func Provider() *schema.Provider { "equinix_metal_reserved_ip_block": resourceMetalReservedIPBlock(), "equinix_metal_ip_attachment": resourceMetalIPAttachment(), "equinix_metal_spot_market_request": resourceMetalSpotMarketRequest(), - "equinix_metal_vlan": resourceMetalVlan(), + "equinix_metal_vlan": vlans.Resource(), "equinix_metal_virtual_circuit": resourceMetalVirtualCircuit(), "equinix_metal_vrf": vrf.Resource(), "equinix_metal_bgp_session": resourceMetalBGPSession(), diff --git a/equinix/resource_metal_vlan.go b/equinix/resource_metal_vlan.go deleted file mode 100644 index 86873da18..000000000 --- a/equinix/resource_metal_vlan.go +++ /dev/null @@ -1,176 +0,0 @@ -package equinix - -import ( - "errors" - "path" - - "github.com/equinix/terraform-provider-equinix/internal/converters" - - equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" - - "github.com/equinix/terraform-provider-equinix/internal/config" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/packethost/packngo" -) - -func resourceMetalVlan() *schema.Resource { - return &schema.Resource{ - Create: resourceMetalVlanCreate, - Read: resourceMetalVlanRead, - Delete: resourceMetalVlanDelete, - Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, - }, - Schema: map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeString, - Description: "ID of parent project", - Required: true, - ForceNew: true, - }, - "description": { - Type: schema.TypeString, - Description: "Description string", - Optional: true, - ForceNew: true, - }, - "facility": { - Type: schema.TypeString, - Description: "Facility where to create the VLAN", - Deprecated: "Use metro instead of facility. For more information, read the migration guide: https://registry.terraform.io/providers/equinix/equinix/latest/docs/guides/migration_guide_facilities_to_metros_devices", - Optional: true, - ForceNew: true, - ConflictsWith: []string{"metro"}, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - // suppress diff when unsetting facility - if len(old) > 0 && new == "" { - return true - } - return old == new - }, - }, - "metro": { - Type: schema.TypeString, - Description: "Metro in which to create the VLAN", - Optional: true, - ForceNew: true, - ConflictsWith: []string{"facility"}, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - _, facOk := d.GetOk("facility") - // new - new val from template - // old - old val from state - // - // suppress diff if metro is manually set for first time, and - // facility is already set - if len(new) > 0 && old == "" && facOk { - return facOk - } - return old == new - }, - StateFunc: converters.ToLowerIf, - }, - "vxlan": { - Type: schema.TypeInt, - Description: "VLAN ID, must be unique in metro", - ForceNew: true, - Optional: true, - Computed: true, - }, - }, - } -} - -func resourceMetalVlanCreate(d *schema.ResourceData, meta interface{}) error { - meta.(*config.Config).AddModuleToMetalUserAgent(d) - client := meta.(*config.Config).Metal - - facRaw, facOk := d.GetOk("facility") - metroRaw, metroOk := d.GetOk("metro") - vxlanRaw, vxlanOk := d.GetOk("vxlan") - - if !facOk && !metroOk { - return equinix_errors.FriendlyError(errors.New("one of facility or metro must be configured")) - } - if facOk && vxlanOk { - return equinix_errors.FriendlyError(errors.New("you can set vxlan only for metro vlans")) - } - - createRequest := &packngo.VirtualNetworkCreateRequest{ - ProjectID: d.Get("project_id").(string), - Description: d.Get("description").(string), - } - if metroOk { - createRequest.Metro = metroRaw.(string) - createRequest.VXLAN = vxlanRaw.(int) - } - if facOk { - createRequest.Facility = facRaw.(string) - } - vlan, _, err := client.ProjectVirtualNetworks.Create(createRequest) - if err != nil { - return equinix_errors.FriendlyError(err) - } - d.SetId(vlan.ID) - return resourceMetalVlanRead(d, meta) -} - -func resourceMetalVlanRead(d *schema.ResourceData, meta interface{}) error { - meta.(*config.Config).AddModuleToMetalUserAgent(d) - client := meta.(*config.Config).Metal - - vlan, _, err := client.ProjectVirtualNetworks.Get(d.Id(), - &packngo.GetOptions{Includes: []string{"assigned_to"}}) - if err != nil { - err = equinix_errors.FriendlyError(err) - if equinix_errors.IsNotFound(err) { - d.SetId("") - return nil - } - return err - - } - d.Set("description", vlan.Description) - d.Set("project_id", vlan.Project.ID) - d.Set("vxlan", vlan.VXLAN) - d.Set("facility", vlan.FacilityCode) - d.Set("metro", vlan.MetroCode) - return nil -} - -func resourceMetalVlanDelete(d *schema.ResourceData, meta interface{}) error { - meta.(*config.Config).AddModuleToMetalUserAgent(d) - client := meta.(*config.Config).Metal - - id := d.Id() - vlan, resp, err := client.ProjectVirtualNetworks.Get(id, &packngo.GetOptions{Includes: []string{"instances", "instances.network_ports.virtual_networks", "internet_gateway"}}) - if equinix_errors.IgnoreResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(resp, err) != nil { - return equinix_errors.FriendlyError(err) - } else if err != nil { - // missing vlans are deleted - return nil - } - - // all device ports must be unassigned before delete - for _, i := range vlan.Instances { - for _, p := range i.NetworkPorts { - for _, a := range p.AttachedVirtualNetworks { - // a.ID is not set despite including instaces.network_ports.virtual_networks - // TODO(displague) packngo should offer GetID() that uses ID or Href - aID := path.Base(a.Href) - - if aID == id { - _, resp, err := client.Ports.Unassign(p.ID, id) - - if equinix_errors.IgnoreResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(resp, err) != nil { - return equinix_errors.FriendlyError(err) - } - } - } - } - } - - // TODO(displague) do we need to unassign gateway connections before delete? - - return equinix_errors.FriendlyError(equinix_errors.IgnoreResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(client.ProjectVirtualNetworks.Delete(id))) -} diff --git a/equinix/data_source_metal_vlan.go b/internal/resources/metal/vlans/datasource.go similarity index 83% rename from equinix/data_source_metal_vlan.go rename to internal/resources/metal/vlans/datasource.go index 11a952e81..94d7ef9db 100644 --- a/equinix/data_source_metal_vlan.go +++ b/internal/resources/metal/vlans/datasource.go @@ -1,24 +1,24 @@ -package equinix +package vlans import ( + "context" "fmt" + "github.com/equinix/terraform-provider-equinix/internal/config" "github.com/equinix/terraform-provider-equinix/internal/converters" - equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" equinix_schema "github.com/equinix/terraform-provider-equinix/internal/schema" - "github.com/equinix/terraform-provider-equinix/internal/config" - + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/packethost/packngo" ) -func dataSourceMetalVlan() *schema.Resource { +func DataSource() *schema.Resource { return &schema.Resource{ - Read: dataSourceMetalVlanRead, + ReadWithoutTimeout: dataSourceMetalVlanRead, Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + StateContext: schema.ImportStatePassthroughContext, }, Schema: map[string]*schema.Schema{ "project_id": { @@ -73,7 +73,7 @@ func dataSourceMetalVlan() *schema.Resource { } } -func dataSourceMetalVlanRead(d *schema.ResourceData, meta interface{}) error { +func dataSourceMetalVlanRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*config.Config).Metal projectRaw, projectOk := d.GetOk("project_id") @@ -83,7 +83,7 @@ func dataSourceMetalVlanRead(d *schema.ResourceData, meta interface{}) error { facilityRaw, facilityOk := d.GetOk("facility") if !(vlanIdOk || (vxlanOk || projectOk || metroOk || facilityOk)) { - return equinix_errors.FriendlyError(fmt.Errorf("You must set either vlan_id or a combination of vxlan, project_id, and, metro or facility")) + return diag.FromErr(equinix_errors.FriendlyError(fmt.Errorf("You must set either vlan_id or a combination of vxlan, project_id, and, metro or facility"))) } var vlan *packngo.VirtualNetwork @@ -95,7 +95,7 @@ func dataSourceMetalVlanRead(d *schema.ResourceData, meta interface{}) error { &packngo.GetOptions{Includes: []string{"assigned_to"}}, ) if err != nil { - return equinix_errors.FriendlyError(err) + return diag.FromErr(equinix_errors.FriendlyError(err)) } } else { @@ -108,12 +108,12 @@ func dataSourceMetalVlanRead(d *schema.ResourceData, meta interface{}) error { &packngo.GetOptions{Includes: []string{"assigned_to"}}, ) if err != nil { - return equinix_errors.FriendlyError(err) + return diag.FromErr(equinix_errors.FriendlyError(err)) } - vlan, err = matchingVlan(vlans.VirtualNetworks, vxlan, projectID, facility, metro) + vlan, err = MatchingVlan(vlans.VirtualNetworks, vxlan, projectID, facility, metro) if err != nil { - return equinix_errors.FriendlyError(err) + return diag.FromErr(equinix_errors.FriendlyError(err)) } } @@ -124,17 +124,17 @@ func dataSourceMetalVlanRead(d *schema.ResourceData, meta interface{}) error { d.SetId(vlan.ID) - return equinix_schema.SetMap(d, map[string]interface{}{ + return diag.FromErr(equinix_schema.SetMap(d, map[string]interface{}{ "vlan_id": vlan.ID, "project_id": vlan.Project.ID, "vxlan": vlan.VXLAN, "facility": vlan.FacilityCode, "metro": vlan.MetroCode, "description": vlan.Description, - }) + })) } -func matchingVlan(vlans []packngo.VirtualNetwork, vxlan int, projectID, facility, metro string) (*packngo.VirtualNetwork, error) { +func MatchingVlan(vlans []packngo.VirtualNetwork, vxlan int, projectID, facility, metro string) (*packngo.VirtualNetwork, error) { matches := []packngo.VirtualNetwork{} for _, v := range vlans { if vxlan != 0 && v.VXLAN != vxlan { diff --git a/equinix/data_source_metal_vlan_acc_test.go b/internal/resources/metal/vlans/datasource_test.go similarity index 87% rename from equinix/data_source_metal_vlan_acc_test.go rename to internal/resources/metal/vlans/datasource_test.go index f77d1e416..98a7a7ef0 100644 --- a/equinix/data_source_metal_vlan_acc_test.go +++ b/internal/resources/metal/vlans/datasource_test.go @@ -1,11 +1,13 @@ -package equinix +package vlans_test import ( "fmt" "reflect" "testing" + "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/equinix/terraform-provider-equinix/internal/config" + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlans" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -18,9 +20,9 @@ func TestAccDataSourceMetalVlan_byVxlanFacility(t *testing.T) { fac := "sv15" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDatasourceVlanCheckDestroyed, Steps: []resource.TestStep{ { @@ -65,9 +67,9 @@ func TestAccDataSourceMetalVlan_byVxlanMetro(t *testing.T) { metro := "sv" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDatasourceVlanCheckDestroyed, Steps: []resource.TestStep{ { @@ -136,9 +138,9 @@ func TestAccDataSourceMetalVlan_byVlanId(t *testing.T) { metro := "sv" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDatasourceVlanCheckDestroyed, Steps: []resource.TestStep{ { @@ -182,9 +184,9 @@ func TestAccDataSourceMetalVlan_byProjectId(t *testing.T) { metro := "sv" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalDatasourceVlanCheckDestroyed, Steps: []resource.TestStep{ { @@ -309,7 +311,7 @@ func TestMetalVlan_matchingVlan(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := matchingVlan(tt.args.vlans, tt.args.vxlan, tt.args.projectID, tt.args.facility, tt.args.metro) + got, err := vlans.MatchingVlan(tt.args.vlans, tt.args.vxlan, tt.args.projectID, tt.args.facility, tt.args.metro) if (err != nil) != tt.wantErr { t.Errorf("matchingVlan() error = %v, wantErr %v", err, tt.wantErr) return @@ -322,7 +324,7 @@ func TestMetalVlan_matchingVlan(t *testing.T) { } func testAccMetalDatasourceVlanCheckDestroyed(s *terraform.State) error { - client := testAccProvider.Meta().(*config.Config).Metal + client := acceptance.TestAccProvider.Meta().(*config.Config).Metal for _, rs := range s.RootModule().Resources { if rs.Type != "equinix_metal_vlan" { diff --git a/internal/resources/metal/vlans/resource.go b/internal/resources/metal/vlans/resource.go new file mode 100644 index 000000000..e3aaffd8a --- /dev/null +++ b/internal/resources/metal/vlans/resource.go @@ -0,0 +1,192 @@ +package vlans + +import ( + "context" + "errors" + "net/http" + "path" + + "github.com/equinix/terraform-provider-equinix/internal/config" + "github.com/equinix/terraform-provider-equinix/internal/converters" + equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" + + "github.com/equinix/equinix-sdk-go/services/metalv1" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func Resource() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceMetalVlanCreate, + ReadWithoutTimeout: resourceMetalVlanRead, + DeleteWithoutTimeout: resourceMetalVlanDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeString, + Description: "ID of parent project", + Required: true, + ForceNew: true, + }, + "description": { + Type: schema.TypeString, + Description: "Description string", + Optional: true, + ForceNew: true, + }, + "facility": { + Type: schema.TypeString, + Description: "Facility where to create the VLAN", + Deprecated: "Use metro instead of facility. For more information, read the migration guide: https://registry.terraform.io/providers/equinix/equinix/latest/docs/guides/migration_guide_facilities_to_metros_devices", + Optional: true, + ForceNew: true, + ConflictsWith: []string{"metro"}, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // suppress diff when unsetting facility + if len(old) > 0 && new == "" { + return true + } + return old == new + }, + }, + "metro": { + Type: schema.TypeString, + Description: "Metro in which to create the VLAN", + Optional: true, + ForceNew: true, + ConflictsWith: []string{"facility"}, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + _, facOk := d.GetOk("facility") + // new - new val from template + // old - old val from state + // + // suppress diff if metro is manually set for first time, and + // facility is already set + if len(new) > 0 && old == "" && facOk { + return facOk + } + return old == new + }, + StateFunc: converters.ToLowerIf, + }, + "vxlan": { + Type: schema.TypeInt, + Description: "VLAN ID, must be unique in metro", + ForceNew: true, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceMetalVlanCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + meta.(*config.Config).AddModuleToMetalUserAgent(d) + client := meta.(*config.Config).Metalgo + + facRaw, facOk := d.GetOk("facility") + metroRaw, metroOk := d.GetOk("metro") + vxlanRaw, vxlanOk := d.GetOk("vxlan") + + if !facOk && !metroOk { + return diag.FromErr(equinix_errors.FriendlyError(errors.New("one of facility or metro must be configured"))) + } + if facOk && vxlanOk { + return diag.FromErr(equinix_errors.FriendlyError(errors.New("you can set vxlan only for metro vlans"))) + } + + createRequest := metalv1.VirtualNetworkCreateInput{ + Description: metalv1.PtrString(d.Get("description").(string)), + } + if metroOk { + createRequest.Metro = metalv1.PtrString(metroRaw.(string)) + createRequest.Vxlan = metalv1.PtrInt32(int32(vxlanRaw.(int))) + } + if facOk { + createRequest.Facility = metalv1.PtrString(facRaw.(string)) + } + projectId := d.Get("project_id").(string) + vlan, _, err := client.VLANsApi. + CreateVirtualNetwork(context.Background(), projectId). + VirtualNetworkCreateInput(createRequest). + Execute() + if err != nil { + return diag.FromErr(equinix_errors.FriendlyError(err)) + } + d.SetId(vlan.GetId()) + return resourceMetalVlanRead(ctx, d, meta) +} + +func resourceMetalVlanRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + meta.(*config.Config).AddModuleToMetalUserAgent(d) + client := meta.(*config.Config).Metalgo + + vlan, _, err := client.VLANsApi. + GetVirtualNetwork(context.Background(), d.Id()). + Include([]string{"assigned_to"}). + Execute() + if err != nil { + err = equinix_errors.FriendlyError(err) + if equinix_errors.IsNotFound(err) { + d.SetId("") + return nil + } + return diag.FromErr(err) + + } + d.Set("description", vlan.GetDescription()) + //d.Set("project_id", vlan) + d.Set("vxlan", vlan.Vxlan) + d.Set("facility", vlan.GetFacility()) + d.Set("metro", vlan.GetMetro()) + return nil +} + +func resourceMetalVlanDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + meta.(*config.Config).AddModuleToMetalUserAgent(d) + client := meta.(*config.Config).Metalgo + + vlan, resp, err := client.VLANsApi. + GetVirtualNetwork(ctx, d.Id()). + Include([]string{"instances", "meta_gateway"}). + Execute() + if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusNotFound { + return diag.FromErr(equinix_errors.FriendlyError(err)) + } else if err != nil { + // missing vlans are deleted + return nil + } + + // all device ports must be unassigned before delete + for _, i := range vlan.GetInstances() { + devideId := path.Base(i.GetHref()) + device, resp, _ := client.DevicesApi.FindDeviceById(ctx, devideId).Execute() + if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusNotFound { + break + } + for _, p := range device.GetNetworkPorts() { + for _, vlanHref := range p.GetVirtualNetworks() { + vlanId := path.Base(vlanHref.GetHref()) + + if vlanId == vlan.GetId() { + _, resp, err := client.PortsApi. + UnassignPort(ctx, p.GetId()). + PortAssignInput(metalv1.PortAssignInput{Vnid: &vlanId}). + Execute() + if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusNotFound { + return diag.FromErr(equinix_errors.FriendlyError(err)) + } + } + } + } + } + + _, resp, err = client.VLANsApi.DeleteVirtualNetwork(ctx, vlan.GetId()).Execute() + if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusNotFound { + return diag.FromErr(equinix_errors.FriendlyError(err)) + } + + return nil +} diff --git a/equinix/resource_metal_vlan_acc_test.go b/internal/resources/metal/vlans/resource_test.go similarity index 61% rename from equinix/resource_metal_vlan_acc_test.go rename to internal/resources/metal/vlans/resource_test.go index 7fd8e21da..ce0dd5ccf 100644 --- a/equinix/resource_metal_vlan_acc_test.go +++ b/internal/resources/metal/vlans/resource_test.go @@ -1,9 +1,10 @@ -package equinix +package vlans_test import ( "fmt" "testing" + "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/equinix/terraform-provider-equinix/internal/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -12,6 +13,55 @@ import ( "github.com/packethost/packngo" ) +func init() { + resource.AddTestSweepers("equinix_metal_vlan", &resource.Sweeper{ + Name: "equinix_metal_vlan", + Dependencies: []string{"equinix_metal_virtual_circuit", "equinix_metal_vrf", "equinix_metal_device"}, + F: testSweepVlans, + }) +} + +func testSweepVlans(region string) error { + log.Printf("[DEBUG] Sweeping vlans") + config, err := acceptance.GetConfigForNonStandardMetalTest() + if err != nil { + return fmt.Errorf("[INFO][SWEEPER_LOG] Error getting configuration for sweeping vlans: %s", err) + } + metal := config.NewMetalClient() + ps, _, err := metal.Projects.List(nil) + if err != nil { + return fmt.Errorf("[INFO][SWEEPER_LOG] Error getting project list for sweeping vlans: %s", err) + } + pids := []string{} + for _, p := range ps { + if acceptance.IsSweepableTestResource(p.Name) { + pids = append(pids, p.ID) + } + } + dids := []string{} + for _, pid := range pids { + ds, _, err := metal.ProjectVirtualNetworks.List(pid, nil) + if err != nil { + log.Printf("Error listing vlans to sweep: %s", err) + continue + } + for _, d := range ds.VirtualNetworks { + if acceptance.IsSweepableTestResource(d.Description) { + dids = append(dids, d.ID) + } + } + } + + for _, did := range dids { + log.Printf("Removing vlan %s", did) + _, err := metal.ProjectVirtualNetworks.Delete(did) + if err != nil { + return fmt.Errorf("Error deleting vlan %s", err) + } + } + return nil +} + func testAccCheckMetalVlanConfig_metro(projSuffix, metro, desc string) string { return fmt.Sprintf(` resource "equinix_metal_project" "foobar" { @@ -32,9 +82,9 @@ func TestAccMetalVlan_metro(t *testing.T) { metro := "sv" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalVlanCheckDestroyed, Steps: []resource.TestStep{ { @@ -56,9 +106,9 @@ func TestAccMetalVlan_basic(t *testing.T) { fac := "ny5" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalVlanCheckDestroyed, Steps: []resource.TestStep{ { @@ -85,7 +135,7 @@ func testAccCheckMetalVlanExists(n string, vlan *packngo.VirtualNetwork) resourc return fmt.Errorf("No Record ID is set") } - client := testAccProvider.Meta().(*config.Config).Metal + client := acceptance.TestAccProvider.Meta().(*config.Config).Metal foundVlan, _, err := client.ProjectVirtualNetworks.Get(rs.Primary.ID, nil) if err != nil { @@ -102,7 +152,7 @@ func testAccCheckMetalVlanExists(n string, vlan *packngo.VirtualNetwork) resourc } func testAccMetalVlanCheckDestroyed(s *terraform.State) error { - client := testAccProvider.Meta().(*config.Config).Metal + client := acceptance.TestAccProvider.Meta().(*config.Config).Metal for _, rs := range s.RootModule().Resources { if rs.Type != "equinix_metal_vlan" { @@ -135,9 +185,9 @@ func TestAccMetalVlan_importBasic(t *testing.T) { fac := "ny5" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, CheckDestroy: testAccMetalVlanCheckDestroyed, Steps: []resource.TestStep{ { From 0efac486d31f18a483031d3e9e5d9ac9d49071c6 Mon Sep 17 00:00:00 2001 From: Ayush Rangwala Date: Fri, 16 Feb 2024 23:29:44 +0530 Subject: [PATCH 2/7] Plugin sdk to Framework migration for Vlan Signed-off-by: Ayush Rangwala --- equinix/provider.go | 3 - internal/provider/provider.go | 18 +- internal/resources/metal/vlans/datasource.go | 153 ++++------ .../metal/vlans/datasource_schema.go | 86 ++++++ .../resources/metal/vlans/datasource_test.go | 98 +++--- internal/resources/metal/vlans/models.go | 85 ++++++ internal/resources/metal/vlans/resource.go | 283 +++++++++--------- .../resources/metal/vlans/resource_schema.go | 98 ++++++ .../resources/metal/vlans/resource_test.go | 133 +++----- .../metal/{vlan => vlans}/sweeper.go | 2 +- internal/sweep/sweep_test.go | 4 +- 11 files changed, 576 insertions(+), 387 deletions(-) create mode 100644 internal/resources/metal/vlans/datasource_schema.go create mode 100644 internal/resources/metal/vlans/models.go create mode 100644 internal/resources/metal/vlans/resource_schema.go rename internal/resources/metal/{vlan => vlans}/sweeper.go (99%) diff --git a/equinix/provider.go b/equinix/provider.go index 01814a6e3..43d37f4fc 100644 --- a/equinix/provider.go +++ b/equinix/provider.go @@ -8,7 +8,6 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/config" metal_project "github.com/equinix/terraform-provider-equinix/internal/resources/metal/project" - "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlans" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vrf" "github.com/equinix/ecx-go/v2" @@ -111,7 +110,6 @@ func Provider() *schema.Provider { "equinix_metal_reserved_ip_block": dataSourceMetalReservedIPBlock(), "equinix_metal_spot_market_request": dataSourceMetalSpotMarketRequest(), "equinix_metal_virtual_circuit": dataSourceMetalVirtualCircuit(), - "equinix_metal_vlan": vlans.DataSource(), "equinix_metal_vrf": vrf.DataSource(), }, ResourcesMap: map[string]*schema.Resource{ @@ -140,7 +138,6 @@ func Provider() *schema.Provider { "equinix_metal_reserved_ip_block": resourceMetalReservedIPBlock(), "equinix_metal_ip_attachment": resourceMetalIPAttachment(), "equinix_metal_spot_market_request": resourceMetalSpotMarketRequest(), - "equinix_metal_vlan": vlans.Resource(), "equinix_metal_virtual_circuit": resourceMetalVirtualCircuit(), "equinix_metal_vrf": vrf.Resource(), "equinix_metal_bgp_session": resourceMetalBGPSession(), diff --git a/internal/provider/provider.go b/internal/provider/provider.go index e682f63c3..14ed66083 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -4,13 +4,6 @@ import ( "context" "fmt" - "github.com/equinix/terraform-provider-equinix/internal/config" - metalconnection "github.com/equinix/terraform-provider-equinix/internal/resources/metal/connection" - metalgateway "github.com/equinix/terraform-provider-equinix/internal/resources/metal/gateway" - metalorganization "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization" - metalprojectsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/project_ssh_key" - metalsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/ssh_key" - equinix_validation "github.com/equinix/terraform-provider-equinix/internal/validation" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -18,6 +11,15 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/equinix/terraform-provider-equinix/internal/config" + metalconnection "github.com/equinix/terraform-provider-equinix/internal/resources/metal/connection" + metalgateway "github.com/equinix/terraform-provider-equinix/internal/resources/metal/gateway" + metalorganization "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization" + metalprojectsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/project_ssh_key" + metalsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/ssh_key" + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlans" + equinix_validation "github.com/equinix/terraform-provider-equinix/internal/validation" ) type FrameworkProvider struct { @@ -116,6 +118,7 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res metalsshkey.NewResource, metalconnection.NewResource, metalorganization.NewResource, + vlans.NewResource, } } @@ -125,5 +128,6 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource metalprojectsshkey.NewDataSource, metalconnection.NewDataSource, metalorganization.NewDataSource, + vlans.NewDataSource, } } diff --git a/internal/resources/metal/vlans/datasource.go b/internal/resources/metal/vlans/datasource.go index 94d7ef9db..d9c7aadc9 100644 --- a/internal/resources/metal/vlans/datasource.go +++ b/internal/resources/metal/vlans/datasource.go @@ -4,116 +4,93 @@ import ( "context" "fmt" - "github.com/equinix/terraform-provider-equinix/internal/config" - "github.com/equinix/terraform-provider-equinix/internal/converters" equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" - equinix_schema "github.com/equinix/terraform-provider-equinix/internal/schema" + "github.com/equinix/terraform-provider-equinix/internal/framework" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/packethost/packngo" ) -func DataSource() *schema.Resource { - return &schema.Resource{ - ReadWithoutTimeout: dataSourceMetalVlanRead, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - Schema: map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"vlan_id"}, - Description: "ID of parent project of the VLAN. Use together with vxlan and metro or facility", +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: framework.NewBaseDataSource( + framework.BaseDataSourceConfig{ + Name: "equinix_metal_vlan", }, - "vxlan": { - Type: schema.TypeInt, - Optional: true, - Computed: true, - ConflictsWith: []string{"vlan_id"}, - Description: "VXLAN numner of the VLAN. Unique in a project and facility or metro. Use with project_id", - }, - "facility": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"vlan_id", "metro"}, - Description: "Facility where the VLAN is deployed", - Deprecated: "Use metro instead of facility. For more information, read the migration guide: https://registry.terraform.io/providers/equinix/equinix/latest/docs/guides/migration_guide_facilities_to_metros_devices", - }, - "metro": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"vlan_id", "facility"}, - Description: "Metro where the VLAN is deployed", - StateFunc: converters.ToLowerIf, - }, - "vlan_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ConflictsWith: []string{"project_id", "vxlan", "metro", "facility"}, - Description: "Metal UUID of the VLAN resource", - }, - "description": { - Type: schema.TypeString, - Computed: true, - Description: "VLAN description text", - }, - "assigned_devices_ids": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Description: "List of device IDs to which this VLAN is assigned", - }, - }, + ), } } -func dataSourceMetalVlanRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.Config).Metal +func (r *DataSource) Schema( + ctx context.Context, + req datasource.SchemaRequest, + resp *datasource.SchemaResponse, +) { + s := dataSourceSchema() + if s.Blocks == nil { + s.Blocks = make(map[string]schema.Block) + } + resp.Schema = s +} + +type DataSource struct { + framework.BaseDataSource + framework.WithTimeouts +} - projectRaw, projectOk := d.GetOk("project_id") - vxlanRaw, vxlanOk := d.GetOk("vxlan") - vlanIdRaw, vlanIdOk := d.GetOk("vlan_id") - metroRaw, metroOk := d.GetOk("metro") - facilityRaw, facilityOk := d.GetOk("facility") +func (r *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + r.Meta.AddFwModuleToMetalUserAgent(ctx, req.ProviderMeta) + client := r.Meta.Metal + + var data DataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } - if !(vlanIdOk || (vxlanOk || projectOk || metroOk || facilityOk)) { - return diag.FromErr(equinix_errors.FriendlyError(fmt.Errorf("You must set either vlan_id or a combination of vxlan, project_id, and, metro or facility"))) + if data.VlanID.IsNull() && + (data.Vxlan.IsNull() && data.ProjectID.IsNull() && data.Metro.IsNull() && data.Facility.IsNull()) { + resp.Diagnostics.AddError("Error fetching Vlan datasource", + equinix_errors. + FriendlyError(fmt.Errorf("You must set either vlan_id or a combination of vxlan, project_id, and, metro or facility")). + Error()) + return } var vlan *packngo.VirtualNetwork - if vlanIdOk { + if !data.VlanID.IsNull() { var err error vlan, _, err = client.ProjectVirtualNetworks.Get( - vlanIdRaw.(string), + data.VlanID.ValueString(), &packngo.GetOptions{Includes: []string{"assigned_to"}}, ) if err != nil { - return diag.FromErr(equinix_errors.FriendlyError(err)) + resp.Diagnostics.AddError("Error fetching Vlan using vlanId", equinix_errors.FriendlyError(err).Error()) + return } } else { - projectID := projectRaw.(string) - vxlan := vxlanRaw.(int) - metro := metroRaw.(string) - facility := facilityRaw.(string) vlans, _, err := client.ProjectVirtualNetworks.List( - projectRaw.(string), + data.ProjectID.ValueString(), &packngo.GetOptions{Includes: []string{"assigned_to"}}, ) if err != nil { - return diag.FromErr(equinix_errors.FriendlyError(err)) + resp.Diagnostics.AddError("Error fetching vlans list for projectId", + equinix_errors.FriendlyError(err).Error()) + return } - vlan, err = MatchingVlan(vlans.VirtualNetworks, vxlan, projectID, facility, metro) + vlan, err = MatchingVlan(vlans.VirtualNetworks, int(data.Vxlan.ValueInt64()), data.ProjectID.ValueString(), + data.Facility.ValueString(), data.Metro.ValueString()) if err != nil { - return diag.FromErr(equinix_errors.FriendlyError(err)) + resp.Diagnostics.AddError("Error expected vlan not found", equinix_errors.FriendlyError(err).Error()) + return } } @@ -122,16 +99,14 @@ func dataSourceMetalVlanRead(ctx context.Context, d *schema.ResourceData, meta i assignedDevices = append(assignedDevices, d.ID) } - d.SetId(vlan.ID) + // Set state to fully populated data + resp.Diagnostics.Append(data.parse(vlan)...) + if resp.Diagnostics.HasError() { + return + } - return diag.FromErr(equinix_schema.SetMap(d, map[string]interface{}{ - "vlan_id": vlan.ID, - "project_id": vlan.Project.ID, - "vxlan": vlan.VXLAN, - "facility": vlan.FacilityCode, - "metro": vlan.MetroCode, - "description": vlan.Description, - })) + // Update the Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func MatchingVlan(vlans []packngo.VirtualNetwork, vxlan int, projectID, facility, metro string) (*packngo.VirtualNetwork, error) { diff --git a/internal/resources/metal/vlans/datasource_schema.go b/internal/resources/metal/vlans/datasource_schema.go new file mode 100644 index 000000000..32498e840 --- /dev/null +++ b/internal/resources/metal/vlans/datasource_schema.go @@ -0,0 +1,86 @@ +package vlans + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func dataSourceSchema() schema.Schema { + return schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier for this Metal Vlan", + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "ID of parent project of the VLAN. Use together with vxlan and metro or facility", + Computed: true, + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("vlan_id"), + }...), + }, + }, + "vxlan": schema.Int64Attribute{ + Optional: true, + Computed: true, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.Expressions{ + path.MatchRoot("vlan_id"), + }...), + }, + Description: "VXLAN numner of the VLAN. Unique in a project and facility or metro. Use with project_id", + }, + "facility": schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("vlan_id"), + path.MatchRoot("metro"), + }...), + }, + Description: "Facility where the VLAN is deployed", + DeprecationMessage: "Use metro instead of facility. For more information, read the migration guide: https://registry.terraform.io/providers/equinix/equinix/latest/docs/guides/migration_guide_facilities_to_metros_devices", + }, + "metro": schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("vlan_id"), + path.MatchRoot("facility"), + }...), + }, + Description: "Metro where the VLAN is deployed", + }, + "vlan_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("project_id"), + path.MatchRoot("vxlan"), + path.MatchRoot("metro"), + path.MatchRoot("facility"), + }...), + }, + Description: "Metal UUID of the VLAN resource", + }, + "description": schema.StringAttribute{ + Computed: true, + Description: "VLAN description text", + }, + "assigned_devices_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + Description: "List of device IDs to which this VLAN is assigned", + }, + }, + } +} diff --git a/internal/resources/metal/vlans/datasource_test.go b/internal/resources/metal/vlans/datasource_test.go index 98a7a7ef0..27628cb0f 100644 --- a/internal/resources/metal/vlans/datasource_test.go +++ b/internal/resources/metal/vlans/datasource_test.go @@ -8,6 +8,7 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/equinix/terraform-provider-equinix/internal/config" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlans" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -15,53 +16,6 @@ import ( "github.com/packethost/packngo" ) -func TestAccDataSourceMetalVlan_byVxlanFacility(t *testing.T) { - rs := acctest.RandString(10) - fac := "sv15" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, - ExternalProviders: acceptance.TestExternalProviders, - ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, - CheckDestroy: testAccMetalDatasourceVlanCheckDestroyed, - Steps: []resource.TestStep{ - { - Config: testAccDataSourceMetalVlanConfig_byVxlanFacility(rs, fac, "tfacc-vlan"), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrPair( - "equinix_metal_vlan.foovlan", "vxlan", - "data.equinix_metal_vlan.dsvlan", "vxlan", - ), - resource.TestCheckResourceAttrPair( - "equinix_metal_vlan.foovlan", "id", - "data.equinix_metal_vlan.dsvlan", "id", - ), - ), - }, - }, - }) -} - -func testAccDataSourceMetalVlanConfig_byVxlanFacility(projSuffix, fac, desc string) string { - return fmt.Sprintf(` -resource "equinix_metal_project" "foobar" { - name = "tfacc-vlan-%s" -} - -resource "equinix_metal_vlan" "foovlan" { - project_id = equinix_metal_project.foobar.id - facility = "%s" - description = "%s" -} - -data "equinix_metal_vlan" "dsvlan" { - facility = equinix_metal_vlan.foovlan.facility - project_id = equinix_metal_vlan.foovlan.project_id - vxlan = equinix_metal_vlan.foovlan.vxlan -} -`, projSuffix, fac, desc) -} - func TestAccDataSourceMetalVlan_byVxlanMetro(t *testing.T) { rs := acctest.RandString(10) metro := "sv" @@ -337,3 +291,53 @@ func testAccMetalDatasourceVlanCheckDestroyed(s *terraform.State) error { return nil } + +func TestAccDataSourceMetalVlan_byVxlanMetro_upgradeFromVersion(t *testing.T) { + rs := acctest.RandString(10) + metro := "sv" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + CheckDestroy: testAccMetalDatasourceVlanCheckDestroyed, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "equinix": { + VersionConstraint: "1.29.0", // latest version with resource defined on SDKv2 + Source: "equinix/equinix", + }, + }, + Config: testAccDataSourceMetalVlanConfig_byVxlanMetro(rs, metro, "tfacc-vlan"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair( + "equinix_metal_vlan.foovlan", "vxlan", + "data.equinix_metal_vlan.dsvlan", "vxlan", + ), + resource.TestCheckResourceAttrPair( + "equinix_metal_vlan.foovlan", "id", + "data.equinix_metal_vlan.dsvlan", "id", + ), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.barvlan", "vxlan", "6", + ), + resource.TestCheckResourceAttr( + "data.equinix_metal_vlan.bardsvlan", "vxlan", "6", + ), + resource.TestCheckResourceAttrPair( + "equinix_metal_vlan.barvlan", "id", + "data.equinix_metal_vlan.bardsvlan", "id", + ), + ), + }, + { + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + Config: testAccDataSourceMetalVlanConfig_byVxlanMetro(rs, metro, "tfacc-vlan"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} diff --git a/internal/resources/metal/vlans/models.go b/internal/resources/metal/vlans/models.go new file mode 100644 index 000000000..5c8077e82 --- /dev/null +++ b/internal/resources/metal/vlans/models.go @@ -0,0 +1,85 @@ +package vlans + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/packethost/packngo" + "strings" +) + +type DataSourceModel struct { + ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` + VlanID types.String `tfsdk:"vlan_id"` + Vxlan types.Int64 `tfsdk:"vxlan"` + Facility types.String `tfsdk:"facility"` + Metro types.String `tfsdk:"metro"` + Description types.String `tfsdk:"description"` + AssignedDevicesIds types.List `tfsdk:"assigned_devices_ids"` +} + +func (m *DataSourceModel) parse(vlan *packngo.VirtualNetwork) diag.Diagnostics { + m.ID = types.StringValue(vlan.ID) + + if vlan.Project.ID != "" { + m.ProjectID = types.StringValue(vlan.Project.ID) + } + + m.VlanID = types.StringValue(vlan.ID) + + m.Facility = types.StringNull() + if vlan.FacilityCode != "" { + m.Facility = types.StringValue(vlan.FacilityCode) + } + + m.Description = types.StringValue(vlan.Description) + m.Vxlan = types.Int64Value(int64(vlan.VXLAN)) + + // version of this resource. StateFunc doesn't exist in terraform and it requires implementation + // of bespoke logic before storing state. To ensure backward compatibility we ignore lower/upper + // case diff for now, but we may want to require input upper case + if !strings.EqualFold(m.Metro.ValueString(), vlan.MetroCode) { + m.Metro = types.StringValue(vlan.MetroCode) + } + + deviceIds := make([]types.String, 0, len(vlan.Instances)) + for _, device := range vlan.Instances { + deviceIds = append(deviceIds, types.StringValue(device.ID)) + } + + return m.AssignedDevicesIds.ElementsAs(context.Background(), &deviceIds, false) +} + +type ResourceModel struct { + ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` + Vxlan types.Int64 `tfsdk:"vxlan"` + Facility types.String `tfsdk:"facility"` + Metro types.String `tfsdk:"metro"` + Description types.String `tfsdk:"description"` +} + +func (m *ResourceModel) parse(vlan *packngo.VirtualNetwork) diag.Diagnostics { + m.ID = types.StringValue(vlan.ID) + + if vlan.Project.ID != "" { + m.ProjectID = types.StringValue(vlan.Project.ID) + } + + m.Facility = types.StringNull() + if vlan.FacilityCode != "" { + m.Facility = types.StringValue(vlan.FacilityCode) + } + + m.Description = types.StringValue(vlan.Description) + m.Vxlan = types.Int64Value(int64(vlan.VXLAN)) + + // version of this resource. StateFunc doesn't exist in terraform and it requires implementation + // of bespoke logic before storing state. To ensure backward compatibility we ignore lower/upper + // case diff for now, but we may want to require input upper case + if !strings.EqualFold(m.Metro.ValueString(), vlan.MetroCode) { + m.Metro = types.StringValue(vlan.MetroCode) + } + return nil +} diff --git a/internal/resources/metal/vlans/resource.go b/internal/resources/metal/vlans/resource.go index e3aaffd8a..c875d6453 100644 --- a/internal/resources/metal/vlans/resource.go +++ b/internal/resources/metal/vlans/resource.go @@ -3,190 +3,177 @@ package vlans import ( "context" "errors" - "net/http" - "path" - - "github.com/equinix/terraform-provider-equinix/internal/config" - "github.com/equinix/terraform-provider-equinix/internal/converters" + "fmt" equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" - - "github.com/equinix/equinix-sdk-go/services/metalv1" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/equinix/terraform-provider-equinix/internal/framework" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/packethost/packngo" ) -func Resource() *schema.Resource { - return &schema.Resource{ - CreateWithoutTimeout: resourceMetalVlanCreate, - ReadWithoutTimeout: resourceMetalVlanRead, - DeleteWithoutTimeout: resourceMetalVlanDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - Schema: map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeString, - Description: "ID of parent project", - Required: true, - ForceNew: true, - }, - "description": { - Type: schema.TypeString, - Description: "Description string", - Optional: true, - ForceNew: true, - }, - "facility": { - Type: schema.TypeString, - Description: "Facility where to create the VLAN", - Deprecated: "Use metro instead of facility. For more information, read the migration guide: https://registry.terraform.io/providers/equinix/equinix/latest/docs/guides/migration_guide_facilities_to_metros_devices", - Optional: true, - ForceNew: true, - ConflictsWith: []string{"metro"}, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - // suppress diff when unsetting facility - if len(old) > 0 && new == "" { - return true - } - return old == new - }, - }, - "metro": { - Type: schema.TypeString, - Description: "Metro in which to create the VLAN", - Optional: true, - ForceNew: true, - ConflictsWith: []string{"facility"}, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - _, facOk := d.GetOk("facility") - // new - new val from template - // old - old val from state - // - // suppress diff if metro is manually set for first time, and - // facility is already set - if len(new) > 0 && old == "" && facOk { - return facOk - } - return old == new - }, - StateFunc: converters.ToLowerIf, - }, - "vxlan": { - Type: schema.TypeInt, - Description: "VLAN ID, must be unique in metro", - ForceNew: true, - Optional: true, - Computed: true, +type Resource struct { + framework.BaseResource + framework.WithTimeouts +} + +func NewResource() resource.Resource { + r := Resource{ + BaseResource: framework.NewBaseResource( + framework.BaseResourceConfig{ + Name: "equinix_metal_vlan", }, - }, + ), } + + return &r +} + +func (r *Resource) Schema( + ctx context.Context, + req resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + s := resourceSchema(ctx) + if s.Blocks == nil { + s.Blocks = make(map[string]schema.Block) + } + + resp.Schema = s } -func resourceMetalVlanCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - meta.(*config.Config).AddModuleToMetalUserAgent(d) - client := meta.(*config.Config).Metalgo +func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + r.Meta.AddFwModuleToMetalUserAgent(ctx, request.ProviderMeta) + client := r.Meta.Metal - facRaw, facOk := d.GetOk("facility") - metroRaw, metroOk := d.GetOk("metro") - vxlanRaw, vxlanOk := d.GetOk("vxlan") + var data ResourceModel + response.Diagnostics.Append(request.Config.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } - if !facOk && !metroOk { - return diag.FromErr(equinix_errors.FriendlyError(errors.New("one of facility or metro must be configured"))) + if data.Facility.IsNull() && data.Metro.IsNull() { + response.Diagnostics.AddError("Invalid input params", + equinix_errors.FriendlyError(errors.New("one of facility or metro must be configured")).Error()) + return } - if facOk && vxlanOk { - return diag.FromErr(equinix_errors.FriendlyError(errors.New("you can set vxlan only for metro vlans"))) + if !data.Facility.IsNull() && !data.Vxlan.IsNull() { + response.Diagnostics.AddError("Invalid input params", + equinix_errors.FriendlyError(errors.New("you can set vxlan only for metro vlans")).Error()) + return } - createRequest := metalv1.VirtualNetworkCreateInput{ - Description: metalv1.PtrString(d.Get("description").(string)), + createRequest := &packngo.VirtualNetworkCreateRequest{ + ProjectID: data.ProjectID.ValueString(), + Description: data.Description.ValueString(), } - if metroOk { - createRequest.Metro = metalv1.PtrString(metroRaw.(string)) - createRequest.Vxlan = metalv1.PtrInt32(int32(vxlanRaw.(int))) + if !data.Metro.IsNull() { + createRequest.Metro = data.Metro.ValueString() + createRequest.VXLAN = int(data.Vxlan.ValueInt64()) } - if facOk { - createRequest.Facility = metalv1.PtrString(facRaw.(string)) + if !data.Facility.IsNull() { + createRequest.Facility = data.Facility.ValueString() } - projectId := d.Get("project_id").(string) - vlan, _, err := client.VLANsApi. - CreateVirtualNetwork(context.Background(), projectId). - VirtualNetworkCreateInput(createRequest). - Execute() + vlan, _, err := client.ProjectVirtualNetworks.Create(createRequest) if err != nil { - return diag.FromErr(equinix_errors.FriendlyError(err)) + response.Diagnostics.AddError("Error creating Vlan", equinix_errors.FriendlyError(err).Error()) + return + } + + // Parse API response into the Terraform state + response.Diagnostics.Append(data.parse(vlan)...) + if response.Diagnostics.HasError() { + return } - d.SetId(vlan.GetId()) - return resourceMetalVlanRead(ctx, d, meta) + + // Set state to fully populated data + response.Diagnostics.Append(response.State.Set(ctx, &data)...) + return } -func resourceMetalVlanRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - meta.(*config.Config).AddModuleToMetalUserAgent(d) - client := meta.(*config.Config).Metalgo +func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + r.Meta.AddFwModuleToMetalUserAgent(ctx, request.ProviderMeta) + client := r.Meta.Metal - vlan, _, err := client.VLANsApi. - GetVirtualNetwork(context.Background(), d.Id()). - Include([]string{"assigned_to"}). - Execute() + var data ResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + vlan, _, err := client.ProjectVirtualNetworks.Get( + data.ID.ValueString(), + &packngo.GetOptions{Includes: []string{"assigned_to"}}, + ) if err != nil { - err = equinix_errors.FriendlyError(err) if equinix_errors.IsNotFound(err) { - d.SetId("") - return nil + response.Diagnostics.AddWarning( + "Equinix Metal Vlan not found during refresh", + fmt.Sprintf("[WARN] Vlan (%s) not found, removing from state", data.ID.ValueString()), + ) + response.State.RemoveResource(ctx) + return } - return diag.FromErr(err) + response.Diagnostics.AddError("Error fetching Vlan using vlanId", + equinix_errors.FriendlyError(err).Error()) + return + } + response.Diagnostics.Append(data.parse(vlan)...) + if response.Diagnostics.HasError() { + return } - d.Set("description", vlan.GetDescription()) - //d.Set("project_id", vlan) - d.Set("vxlan", vlan.Vxlan) - d.Set("facility", vlan.GetFacility()) - d.Set("metro", vlan.GetMetro()) - return nil + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) } -func resourceMetalVlanDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - meta.(*config.Config).AddModuleToMetalUserAgent(d) - client := meta.(*config.Config).Metalgo +func (r *Resource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {} - vlan, resp, err := client.VLANsApi. - GetVirtualNetwork(ctx, d.Id()). - Include([]string{"instances", "meta_gateway"}). - Execute() - if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusNotFound { - return diag.FromErr(equinix_errors.FriendlyError(err)) - } else if err != nil { - // missing vlans are deleted - return nil +func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + r.Meta.AddFwModuleToMetalUserAgent(ctx, request.ProviderMeta) + client := r.Meta.Metal + + var data ResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return } - // all device ports must be unassigned before delete - for _, i := range vlan.GetInstances() { - devideId := path.Base(i.GetHref()) - device, resp, _ := client.DevicesApi.FindDeviceById(ctx, devideId).Execute() - if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusNotFound { - break + vlan, resp, err := client.ProjectVirtualNetworks.Get( + data.ID.ValueString(), + &packngo.GetOptions{Includes: []string{"instances", "meta_gateway"}}, + ) + if err != nil { + if equinix_errors.IgnoreResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(resp, err) != nil { + response.Diagnostics.AddWarning( + "Equinix Metal Vlan not found during delete", + equinix_errors.FriendlyError(err).Error(), + ) + return } - for _, p := range device.GetNetworkPorts() { - for _, vlanHref := range p.GetVirtualNetworks() { - vlanId := path.Base(vlanHref.GetHref()) - - if vlanId == vlan.GetId() { - _, resp, err := client.PortsApi. - UnassignPort(ctx, p.GetId()). - PortAssignInput(metalv1.PortAssignInput{Vnid: &vlanId}). - Execute() - if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusNotFound { - return diag.FromErr(equinix_errors.FriendlyError(err)) + response.Diagnostics.AddError("Error fetching Vlan using vlanId", + equinix_errors.FriendlyError(err).Error()) + return + } + + // all device ports must be unassigned before delete + for _, instance := range vlan.Instances { + for _, port := range instance.NetworkPorts { + for _, v := range port.AttachedVirtualNetworks { + if v.ID == vlan.ID { + _, resp, err = client.Ports.Unassign(port.ID, vlan.ID) + if equinix_errors.IgnoreResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(resp, err) != nil { + response.Diagnostics.AddError("Error unassign port with Vlan", + equinix_errors.FriendlyError(err).Error()) + return } } } } } - _, resp, err = client.VLANsApi.DeleteVirtualNetwork(ctx, vlan.GetId()).Execute() - if resp.StatusCode != http.StatusForbidden && resp.StatusCode != http.StatusNotFound { - return diag.FromErr(equinix_errors.FriendlyError(err)) + if err := equinix_errors.IgnoreResponseErrors(equinix_errors.HttpForbidden, equinix_errors.HttpNotFound)(client.ProjectVirtualNetworks.Delete(vlan.ID)); err != nil { + response.Diagnostics.AddError("Error deleting Vlan", + equinix_errors.FriendlyError(err).Error()) + return } - - return nil } diff --git a/internal/resources/metal/vlans/resource_schema.go b/internal/resources/metal/vlans/resource_schema.go new file mode 100644 index 000000000..62edd3217 --- /dev/null +++ b/internal/resources/metal/vlans/resource_schema.go @@ -0,0 +1,98 @@ +package vlans + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +func resourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier for this Metal Vlan", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "project_id": schema.StringAttribute{ + Description: "ID of parent project", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "description": schema.StringAttribute{ + Description: "Description string", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "facility": schema.StringAttribute{ + Description: "Facility where to create the VLAN", + DeprecationMessage: "Use metro instead of facility. For more information, read the migration guide: https://registry.terraform.io/providers/equinix/equinix/latest/docs/guides/migration_guide_facilities_to_metros_devices", + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("metro"), + }...), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + // TODO: aayushrangwala to check if this is needed with the framework changes + //DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // suppress diff when unsetting facility + //if len(old) > 0 && new == "" { + // return true + //} + //return old == new + //}, + }, + "metro": schema.StringAttribute{ + Description: "Metro in which to create the VLAN", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{ + path.MatchRoot("facility"), + }...), + }, + // TODO: aayushrangwala to check if this is needed with the framework changes + //DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // _, facOk := d.GetOk("facility") + // new - new val from template + // old - old val from state + // + // suppress diff if metro is manually set for first time, and + // facility is already set + //if len(new) > 0 && old == "" && facOk { + // return facOk + //} + //return old == new + //}, + // TODO: add statefunc in framework + //StateFunc: converters.ToLowerIf, + }, + "vxlan": schema.Int64Attribute{ + Description: "VLAN ID, must be unique in metro", + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Optional: true, + Computed: true, + }, + }, + } +} diff --git a/internal/resources/metal/vlans/resource_test.go b/internal/resources/metal/vlans/resource_test.go index ce0dd5ccf..fdc417a75 100644 --- a/internal/resources/metal/vlans/resource_test.go +++ b/internal/resources/metal/vlans/resource_test.go @@ -6,6 +6,7 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/equinix/terraform-provider-equinix/internal/config" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -13,55 +14,6 @@ import ( "github.com/packethost/packngo" ) -func init() { - resource.AddTestSweepers("equinix_metal_vlan", &resource.Sweeper{ - Name: "equinix_metal_vlan", - Dependencies: []string{"equinix_metal_virtual_circuit", "equinix_metal_vrf", "equinix_metal_device"}, - F: testSweepVlans, - }) -} - -func testSweepVlans(region string) error { - log.Printf("[DEBUG] Sweeping vlans") - config, err := acceptance.GetConfigForNonStandardMetalTest() - if err != nil { - return fmt.Errorf("[INFO][SWEEPER_LOG] Error getting configuration for sweeping vlans: %s", err) - } - metal := config.NewMetalClient() - ps, _, err := metal.Projects.List(nil) - if err != nil { - return fmt.Errorf("[INFO][SWEEPER_LOG] Error getting project list for sweeping vlans: %s", err) - } - pids := []string{} - for _, p := range ps { - if acceptance.IsSweepableTestResource(p.Name) { - pids = append(pids, p.ID) - } - } - dids := []string{} - for _, pid := range pids { - ds, _, err := metal.ProjectVirtualNetworks.List(pid, nil) - if err != nil { - log.Printf("Error listing vlans to sweep: %s", err) - continue - } - for _, d := range ds.VirtualNetworks { - if acceptance.IsSweepableTestResource(d.Description) { - dids = append(dids, d.ID) - } - } - } - - for _, did := range dids { - log.Printf("Removing vlan %s", did) - _, err := metal.ProjectVirtualNetworks.Delete(did) - if err != nil { - return fmt.Errorf("Error deleting vlan %s", err) - } - } - return nil -} - func testAccCheckMetalVlanConfig_metro(projSuffix, metro, desc string) string { return fmt.Sprintf(` resource "equinix_metal_project" "foobar" { @@ -78,6 +30,7 @@ resource "equinix_metal_vlan" "foovlan" { } func TestAccMetalVlan_metro(t *testing.T) { + var vlan packngo.VirtualNetwork rs := acctest.RandString(10) metro := "sv" @@ -89,36 +42,12 @@ func TestAccMetalVlan_metro(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccCheckMetalVlanConfig_metro(rs, metro, "tfacc-vlan"), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "equinix_metal_vlan.foovlan", "metro", metro), - resource.TestCheckResourceAttr( - "equinix_metal_vlan.foovlan", "facility", ""), - ), - }, - }, - }) -} - -func TestAccMetalVlan_basic(t *testing.T) { - var vlan packngo.VirtualNetwork - rs := acctest.RandString(10) - fac := "ny5" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, - ExternalProviders: acceptance.TestExternalProviders, - ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, - CheckDestroy: testAccMetalVlanCheckDestroyed, - Steps: []resource.TestStep{ - { - Config: testAccMetalVlanConfig_var(rs, fac, "tfacc-vlan"), Check: resource.ComposeTestCheckFunc( testAccCheckMetalVlanExists("equinix_metal_vlan.foovlan", &vlan), resource.TestCheckResourceAttr( "equinix_metal_vlan.foovlan", "description", "tfacc-vlan"), resource.TestCheckResourceAttr( - "equinix_metal_vlan.foovlan", "facility", fac), + "equinix_metal_vlan.foovlan", "metro", metro), ), }, }, @@ -166,23 +95,9 @@ func testAccMetalVlanCheckDestroyed(s *terraform.State) error { return nil } -func testAccMetalVlanConfig_var(projSuffix, facility, desc string) string { - return fmt.Sprintf(` -resource "equinix_metal_project" "foobar" { - name = "tfacc-vlan-%s" -} - -resource "equinix_metal_vlan" "foovlan" { - project_id = "${equinix_metal_project.foobar.id}" - facility = "%s" - description = "%s" -} -`, projSuffix, facility, desc) -} - func TestAccMetalVlan_importBasic(t *testing.T) { rs := acctest.RandString(10) - fac := "ny5" + metro := "sv" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, @@ -191,7 +106,7 @@ func TestAccMetalVlan_importBasic(t *testing.T) { CheckDestroy: testAccMetalVlanCheckDestroyed, Steps: []resource.TestStep{ { - Config: testAccMetalVlanConfig_var(rs, fac, "tfacc-vlan"), + Config: testAccCheckMetalVlanConfig_metro(rs, metro, "tfacc-vlan"), }, { ResourceName: "equinix_metal_vlan.foovlan", @@ -201,3 +116,41 @@ func TestAccMetalVlan_importBasic(t *testing.T) { }, }) } + +func TestAccMetalVlan_metro_upgradeFromVersion(t *testing.T) { + var vlan packngo.VirtualNetwork + rs := acctest.RandString(10) + metro := "sv" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + CheckDestroy: testAccMetalDatasourceVlanCheckDestroyed, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "equinix": { + VersionConstraint: "1.29.0", // latest version with resource defined on SDKv2 + Source: "equinix/equinix", + }, + }, + Config: testAccCheckMetalVlanConfig_metro(rs, metro, "tfacc-vlan"), + Check: resource.ComposeTestCheckFunc( + testAccCheckMetalVlanExists("equinix_metal_vlan.foovlan", &vlan), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.foovlan", "description", "tfacc-vlan"), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.foovlan", "metro", metro), + ), + }, + { + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + Config: testAccCheckMetalVlanConfig_metro(rs, metro, "tfacc-vlan"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} diff --git a/internal/resources/metal/vlan/sweeper.go b/internal/resources/metal/vlans/sweeper.go similarity index 99% rename from internal/resources/metal/vlan/sweeper.go rename to internal/resources/metal/vlans/sweeper.go index 5ca3d60aa..7bb00ff69 100644 --- a/internal/resources/metal/vlan/sweeper.go +++ b/internal/resources/metal/vlans/sweeper.go @@ -1,4 +1,4 @@ -package vlan +package vlans import ( "fmt" diff --git a/internal/sweep/sweep_test.go b/internal/sweep/sweep_test.go index 87da9b28b..778330de6 100644 --- a/internal/sweep/sweep_test.go +++ b/internal/sweep/sweep_test.go @@ -1,6 +1,7 @@ package sweep_test import ( + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlans" "testing" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/device" @@ -9,7 +10,6 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/resources/metal/ssh_key" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/user_api_key" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/virtual_circuit" - "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlan" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vrf" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) @@ -28,6 +28,6 @@ func addTestSweepers() { ssh_key.AddTestSweeper() user_api_key.AddTestSweeper() virtual_circuit.AddTestSweeper() - vlan.AddTestSweeper() + vlans.AddTestSweeper() vrf.AddTestSweeper() } From 7bf7d5c5d904eae16f3fe1b19c6438b0991ce3e2 Mon Sep 17 00:00:00 2001 From: Ayush Rangwala Date: Thu, 29 Feb 2024 20:57:51 +0530 Subject: [PATCH 3/7] Fix support for diffSuppressFunc Signed-off-by: Ayush Rangwala --- internal/resources/metal/vlans/models.go | 18 +++--- internal/resources/metal/vlans/resource.go | 13 ++++- .../resources/metal/vlans/resource_schema.go | 34 ++---------- .../resources/metal/vlans/resource_test.go | 55 ++++++++++++++++++- 4 files changed, 78 insertions(+), 42 deletions(-) diff --git a/internal/resources/metal/vlans/models.go b/internal/resources/metal/vlans/models.go index 5c8077e82..a60b3da14 100644 --- a/internal/resources/metal/vlans/models.go +++ b/internal/resources/metal/vlans/models.go @@ -62,24 +62,20 @@ type ResourceModel struct { func (m *ResourceModel) parse(vlan *packngo.VirtualNetwork) diag.Diagnostics { m.ID = types.StringValue(vlan.ID) + m.Description = types.StringValue(vlan.Description) + m.Vxlan = types.Int64Value(int64(vlan.VXLAN)) if vlan.Project.ID != "" { m.ProjectID = types.StringValue(vlan.Project.ID) } - m.Facility = types.StringNull() - if vlan.FacilityCode != "" { - m.Facility = types.StringValue(vlan.FacilityCode) + if vlan.Facility != nil { + m.Facility = types.StringValue(vlan.Facility.Code) + m.Metro = types.StringValue(strings.ToLower(vlan.Facility.Metro.Code)) } - m.Description = types.StringValue(vlan.Description) - m.Vxlan = types.Int64Value(int64(vlan.VXLAN)) - - // version of this resource. StateFunc doesn't exist in terraform and it requires implementation - // of bespoke logic before storing state. To ensure backward compatibility we ignore lower/upper - // case diff for now, but we may want to require input upper case - if !strings.EqualFold(m.Metro.ValueString(), vlan.MetroCode) { - m.Metro = types.StringValue(vlan.MetroCode) + if vlan.Metro != nil { + m.Metro = types.StringValue(strings.ToLower(vlan.Metro.Code)) } return nil } diff --git a/internal/resources/metal/vlans/resource.go b/internal/resources/metal/vlans/resource.go index c875d6453..143e23e04 100644 --- a/internal/resources/metal/vlans/resource.go +++ b/internal/resources/metal/vlans/resource.go @@ -11,6 +11,10 @@ import ( "github.com/packethost/packngo" ) +var ( + vlanDefaultIncludes = []string{"assigned_to", "facility", "metro"} +) + type Resource struct { framework.BaseResource framework.WithTimeouts @@ -79,6 +83,13 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r return } + // get the current state of newly created vlan with default include fields + vlan, _, err = client.ProjectVirtualNetworks.Get(vlan.ID, &packngo.GetOptions{Includes: vlanDefaultIncludes}) + if err != nil { + response.Diagnostics.AddError("Error reading Vlan after create", equinix_errors.FriendlyError(err).Error()) + return + } + // Parse API response into the Terraform state response.Diagnostics.Append(data.parse(vlan)...) if response.Diagnostics.HasError() { @@ -102,7 +113,7 @@ func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, respo vlan, _, err := client.ProjectVirtualNetworks.Get( data.ID.ValueString(), - &packngo.GetOptions{Includes: []string{"assigned_to"}}, + &packngo.GetOptions{Includes: vlanDefaultIncludes}, ) if err != nil { if equinix_errors.IsNotFound(err) { diff --git a/internal/resources/metal/vlans/resource_schema.go b/internal/resources/metal/vlans/resource_schema.go index 62edd3217..b1fcee622 100644 --- a/internal/resources/metal/vlans/resource_schema.go +++ b/internal/resources/metal/vlans/resource_schema.go @@ -40,50 +40,26 @@ func resourceSchema(ctx context.Context) schema.Schema { Description: "Facility where to create the VLAN", DeprecationMessage: "Use metro instead of facility. For more information, read the migration guide: https://registry.terraform.io/providers/equinix/equinix/latest/docs/guides/migration_guide_facilities_to_metros_devices", Optional: true, + Computed: true, Validators: []validator.String{ - stringvalidator.ConflictsWith(path.Expressions{ - path.MatchRoot("metro"), - }...), + stringvalidator.ConflictsWith(path.MatchRoot("metro")), }, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), stringplanmodifier.UseStateForUnknown(), }, - // TODO: aayushrangwala to check if this is needed with the framework changes - //DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - // suppress diff when unsetting facility - //if len(old) > 0 && new == "" { - // return true - //} - //return old == new - //}, }, "metro": schema.StringAttribute{ Description: "Metro in which to create the VLAN", Optional: true, + Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, Validators: []validator.String{ - stringvalidator.ConflictsWith(path.Expressions{ - path.MatchRoot("facility"), - }...), + stringvalidator.ConflictsWith(path.MatchRoot("facility")), + stringvalidator.AtLeastOneOf(path.MatchRoot("facility"), path.MatchRoot("metro")), }, - // TODO: aayushrangwala to check if this is needed with the framework changes - //DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - // _, facOk := d.GetOk("facility") - // new - new val from template - // old - old val from state - // - // suppress diff if metro is manually set for first time, and - // facility is already set - //if len(new) > 0 && old == "" && facOk { - // return facOk - //} - //return old == new - //}, - // TODO: add statefunc in framework - //StateFunc: converters.ToLowerIf, }, "vxlan": schema.Int64Attribute{ Description: "VLAN ID, must be unique in metro", diff --git a/internal/resources/metal/vlans/resource_test.go b/internal/resources/metal/vlans/resource_test.go index fdc417a75..61cf64b82 100644 --- a/internal/resources/metal/vlans/resource_test.go +++ b/internal/resources/metal/vlans/resource_test.go @@ -24,11 +24,23 @@ resource "equinix_metal_vlan" "foovlan" { project_id = equinix_metal_project.foobar.id metro = "%s" description = "%s" - vxlan = 5 } `, projSuffix, metro, desc) } +func testAccCheckMetalVlanConfig_facility(projSuffix, facility, desc string) string { + return fmt.Sprintf(` +resource "equinix_metal_project" "foobar" { + name = "tfacc-vlan-%s" +} +resource "equinix_metal_vlan" "foovlan" { + project_id = equinix_metal_project.foobar.id + facility = "%s" + description = "%s" +} +`, projSuffix, facility, desc) +} + func TestAccMetalVlan_metro(t *testing.T) { var vlan packngo.VirtualNetwork rs := acctest.RandString(10) @@ -117,6 +129,47 @@ func TestAccMetalVlan_importBasic(t *testing.T) { }) } +func TestAccMetalVlan_facility_to_metro(t *testing.T) { + var vlan packngo.VirtualNetwork + rs := acctest.RandString(10) + metro := "sv" + facility := "sv15" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + CheckDestroy: testAccMetalVlanCheckDestroyed, + Steps: []resource.TestStep{ + { + Config: testAccCheckMetalVlanConfig_facility(rs, facility, "tfacc-vlan"), + Check: resource.ComposeTestCheckFunc( + testAccCheckMetalVlanExists("equinix_metal_vlan.foovlan", &vlan), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.foovlan", "description", "tfacc-vlan"), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.foovlan", "metro", metro), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.foovlan", "facility", facility), + ), + }, + { + Config: testAccCheckMetalVlanConfig_metro(rs, metro, "tfacc-vlan"), + ExpectNonEmptyPlan: false, + Check: resource.ComposeTestCheckFunc( + testAccCheckMetalVlanExists("equinix_metal_vlan.foovlan", &vlan), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.foovlan", "description", "tfacc-vlan"), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.foovlan", "metro", metro), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.foovlan", "facility", facility), + ), + }, + }, + }) +} + func TestAccMetalVlan_metro_upgradeFromVersion(t *testing.T) { var vlan packngo.VirtualNetwork rs := acctest.RandString(10) From 50a2923322aa4a53c7597ba2467efc223e60dd59 Mon Sep 17 00:00:00 2001 From: Ayush Rangwala Date: Thu, 7 Mar 2024 22:13:53 +0530 Subject: [PATCH 4/7] Add caseinsensitive plan modifier for metro attribute --- internal/planmodifiers/caseinsensitive.go | 35 ++++++++++++ .../planmodifiers/caseinsensitive_test.go | 56 +++++++++++++++++++ internal/provider/provider.go | 6 +- .../metal/{vlans => vlan}/datasource.go | 4 +- .../{vlans => vlan}/datasource_schema.go | 2 +- .../metal/{vlans => vlan}/datasource_test.go | 6 +- .../resources/metal/{vlans => vlan}/models.go | 18 ++++-- .../metal/{vlans => vlan}/resource.go | 18 +++++- .../metal/{vlans => vlan}/resource_schema.go | 10 ++-- .../metal/{vlans => vlan}/resource_test.go | 36 +++++++++++- .../metal/{vlans => vlan}/sweeper.go | 10 ++-- internal/sweep/sweep_test.go | 4 +- 12 files changed, 177 insertions(+), 28 deletions(-) create mode 100644 internal/planmodifiers/caseinsensitive.go create mode 100644 internal/planmodifiers/caseinsensitive_test.go rename internal/resources/metal/{vlans => vlan}/datasource.go (97%) rename internal/resources/metal/{vlans => vlan}/datasource_schema.go (99%) rename internal/resources/metal/{vlans => vlan}/datasource_test.go (98%) rename internal/resources/metal/{vlans => vlan}/models.go (76%) rename internal/resources/metal/{vlans => vlan}/resource.go (93%) rename internal/resources/metal/{vlans => vlan}/resource_schema.go (89%) rename internal/resources/metal/{vlans => vlan}/resource_test.go (85%) rename internal/resources/metal/{vlans => vlan}/sweeper.go (88%) diff --git a/internal/planmodifiers/caseinsensitive.go b/internal/planmodifiers/caseinsensitive.go new file mode 100644 index 000000000..88f3be6bb --- /dev/null +++ b/internal/planmodifiers/caseinsensitive.go @@ -0,0 +1,35 @@ +package planmodifiers + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func CaseInsensitiveString() planmodifier.String { + return &caseInsensitivePlanModifier{} +} + +type caseInsensitivePlanModifier struct{} + +func (d *caseInsensitivePlanModifier) PlanModifyString(ctx context.Context, request planmodifier.StringRequest, response *planmodifier.StringResponse) { + oldValue := request.StateValue.ValueString() + newValue := request.PlanValue.ValueString() + + result := oldValue + if !strings.EqualFold(strings.ToLower(newValue), strings.ToLower(oldValue)) { + result = newValue + } + + response.PlanValue = types.StringValue(result) +} + +func (d *caseInsensitivePlanModifier) Description(ctx context.Context) string { + return "For same string but different cases, does not trigger diffs in the plan" +} + +func (d *caseInsensitivePlanModifier) MarkdownDescription(ctx context.Context) string { + return d.Description(ctx) +} diff --git a/internal/planmodifiers/caseinsensitive_test.go b/internal/planmodifiers/caseinsensitive_test.go new file mode 100644 index 000000000..a061718d8 --- /dev/null +++ b/internal/planmodifiers/caseinsensitive_test.go @@ -0,0 +1,56 @@ +package planmodifiers + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestCaseInsensitiveSet(t *testing.T) { + testCases := []struct { + Old, New, Expected string + }{ + { + Old: "foo", + New: "foo", + Expected: "foo", + }, + { + Old: "Bar", + New: "bar", + Expected: "Bar", + }, + { + Old: "foo", + New: "fOO", + Expected: "foo", + }, + } + + testPlanModifier := CaseInsensitiveString() + + for i, testCase := range testCases { + stateValue := types.StringValue(testCase.Old) + planValue := types.StringValue(testCase.New) + expectedValue := types.StringValue(testCase.Expected) + + req := planmodifier.StringRequest{ + StateValue: stateValue, + PlanValue: planValue, + } + + var resp planmodifier.StringResponse + + testPlanModifier.PlanModifyString(context.Background(), req, &resp) + + if resp.Diagnostics.HasError() { + t.Fatalf("%d: got error modifying plan: %v", i, resp.Diagnostics.Errors()) + } + + if !resp.PlanValue.Equal(expectedValue) { + t.Fatalf("%d: output plan value does not equal expected plan value", i) + } + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 14ed66083..ef9565b72 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -18,7 +18,7 @@ import ( metalorganization "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization" metalprojectsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/project_ssh_key" metalsshkey "github.com/equinix/terraform-provider-equinix/internal/resources/metal/ssh_key" - "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlans" + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlan" equinix_validation "github.com/equinix/terraform-provider-equinix/internal/validation" ) @@ -118,7 +118,7 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res metalsshkey.NewResource, metalconnection.NewResource, metalorganization.NewResource, - vlans.NewResource, + vlan.NewResource, } } @@ -128,6 +128,6 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource metalprojectsshkey.NewDataSource, metalconnection.NewDataSource, metalorganization.NewDataSource, - vlans.NewDataSource, + vlan.NewDataSource, } } diff --git a/internal/resources/metal/vlans/datasource.go b/internal/resources/metal/vlan/datasource.go similarity index 97% rename from internal/resources/metal/vlans/datasource.go rename to internal/resources/metal/vlan/datasource.go index d9c7aadc9..1fa0353d2 100644 --- a/internal/resources/metal/vlans/datasource.go +++ b/internal/resources/metal/vlan/datasource.go @@ -1,4 +1,4 @@ -package vlans +package vlan import ( "context" @@ -81,7 +81,7 @@ func (r *DataSource) Read( &packngo.GetOptions{Includes: []string{"assigned_to"}}, ) if err != nil { - resp.Diagnostics.AddError("Error fetching vlans list for projectId", + resp.Diagnostics.AddError("Error fetching vlan list for projectId", equinix_errors.FriendlyError(err).Error()) return } diff --git a/internal/resources/metal/vlans/datasource_schema.go b/internal/resources/metal/vlan/datasource_schema.go similarity index 99% rename from internal/resources/metal/vlans/datasource_schema.go rename to internal/resources/metal/vlan/datasource_schema.go index 32498e840..4f0fb525c 100644 --- a/internal/resources/metal/vlans/datasource_schema.go +++ b/internal/resources/metal/vlan/datasource_schema.go @@ -1,4 +1,4 @@ -package vlans +package vlan import ( "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" diff --git a/internal/resources/metal/vlans/datasource_test.go b/internal/resources/metal/vlan/datasource_test.go similarity index 98% rename from internal/resources/metal/vlans/datasource_test.go rename to internal/resources/metal/vlan/datasource_test.go index 27628cb0f..6688f46e0 100644 --- a/internal/resources/metal/vlans/datasource_test.go +++ b/internal/resources/metal/vlan/datasource_test.go @@ -1,4 +1,4 @@ -package vlans_test +package vlan_test import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/equinix/terraform-provider-equinix/internal/config" - "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlans" + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlan" "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -265,7 +265,7 @@ func TestMetalVlan_matchingVlan(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := vlans.MatchingVlan(tt.args.vlans, tt.args.vxlan, tt.args.projectID, tt.args.facility, tt.args.metro) + got, err := vlan.MatchingVlan(tt.args.vlans, tt.args.vxlan, tt.args.projectID, tt.args.facility, tt.args.metro) if (err != nil) != tt.wantErr { t.Errorf("matchingVlan() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/internal/resources/metal/vlans/models.go b/internal/resources/metal/vlan/models.go similarity index 76% rename from internal/resources/metal/vlans/models.go rename to internal/resources/metal/vlan/models.go index a60b3da14..bb4e61286 100644 --- a/internal/resources/metal/vlans/models.go +++ b/internal/resources/metal/vlan/models.go @@ -1,4 +1,4 @@ -package vlans +package vlan import ( "context" @@ -69,13 +69,23 @@ func (m *ResourceModel) parse(vlan *packngo.VirtualNetwork) diag.Diagnostics { m.ProjectID = types.StringValue(vlan.Project.ID) } + var metroCode, facilityCode types.String if vlan.Facility != nil { - m.Facility = types.StringValue(vlan.Facility.Code) - m.Metro = types.StringValue(strings.ToLower(vlan.Facility.Metro.Code)) + facilityCode = types.StringValue(vlan.Facility.Code) + metroCode = types.StringValue(strings.ToLower(vlan.Facility.Metro.Code)) + } + // version of this resource. StateFunc doesn't exist in terraform and it requires implementation + // of bespoke logic before storing state. To ensure backward compatibility we ignore lower/upper + // case diff for now, but we may want to require input upper case + if !strings.EqualFold(m.Facility.ValueString(), facilityCode.ValueString()) { + m.Facility = facilityCode } if vlan.Metro != nil { - m.Metro = types.StringValue(strings.ToLower(vlan.Metro.Code)) + metroCode = types.StringValue(strings.ToLower(vlan.Metro.Code)) + } + if !strings.EqualFold(m.Metro.ValueString(), metroCode.ValueString()) { + m.Metro = metroCode } return nil } diff --git a/internal/resources/metal/vlans/resource.go b/internal/resources/metal/vlan/resource.go similarity index 93% rename from internal/resources/metal/vlans/resource.go rename to internal/resources/metal/vlan/resource.go index 143e23e04..23028f6a2 100644 --- a/internal/resources/metal/vlans/resource.go +++ b/internal/resources/metal/vlan/resource.go @@ -1,4 +1,4 @@ -package vlans +package vlan import ( "context" @@ -6,6 +6,7 @@ import ( "fmt" equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" "github.com/equinix/terraform-provider-equinix/internal/framework" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/packethost/packngo" @@ -62,7 +63,7 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r } if !data.Facility.IsNull() && !data.Vxlan.IsNull() { response.Diagnostics.AddError("Invalid input params", - equinix_errors.FriendlyError(errors.New("you can set vxlan only for metro vlans")).Error()) + equinix_errors.FriendlyError(errors.New("you can set vxlan only for metro vlan")).Error()) return } @@ -137,7 +138,18 @@ func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, respo response.Diagnostics.Append(response.State.Set(ctx, &data)...) } -func (r *Resource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) {} +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data ResourceModel + if diag := req.Plan.Get(ctx, &data); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } + + if diag := resp.State.Set(ctx, &data); diag.HasError() { + resp.Diagnostics.Append(diag...) + return + } +} func (r *Resource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { r.Meta.AddFwModuleToMetalUserAgent(ctx, request.ProviderMeta) diff --git a/internal/resources/metal/vlans/resource_schema.go b/internal/resources/metal/vlan/resource_schema.go similarity index 89% rename from internal/resources/metal/vlans/resource_schema.go rename to internal/resources/metal/vlan/resource_schema.go index b1fcee622..3ad6f5261 100644 --- a/internal/resources/metal/vlans/resource_schema.go +++ b/internal/resources/metal/vlan/resource_schema.go @@ -1,8 +1,8 @@ -package vlans +package vlan import ( "context" - + equinixplanmodifiers "github.com/equinix/terraform-provider-equinix/internal/planmodifiers" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -54,11 +54,13 @@ func resourceSchema(ctx context.Context) schema.Schema { Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), + //stringplanmodifier.RequiresReplace(), + equinixplanmodifiers.CaseInsensitiveString(), }, Validators: []validator.String{ stringvalidator.ConflictsWith(path.MatchRoot("facility")), - stringvalidator.AtLeastOneOf(path.MatchRoot("facility"), path.MatchRoot("metro")), + stringvalidator.ExactlyOneOf(path.MatchRoot("facility"), + path.MatchRoot("metro")), }, }, "vxlan": schema.Int64Attribute{ diff --git a/internal/resources/metal/vlans/resource_test.go b/internal/resources/metal/vlan/resource_test.go similarity index 85% rename from internal/resources/metal/vlans/resource_test.go rename to internal/resources/metal/vlan/resource_test.go index 61cf64b82..17b5a858c 100644 --- a/internal/resources/metal/vlans/resource_test.go +++ b/internal/resources/metal/vlan/resource_test.go @@ -1,7 +1,8 @@ -package vlans_test +package vlan_test import ( "fmt" + "strings" "testing" "github.com/equinix/terraform-provider-equinix/internal/acceptance" @@ -207,3 +208,36 @@ func TestAccMetalVlan_metro_upgradeFromVersion(t *testing.T) { }, }) } + +func TestAccMetalVlan_metro_suppress_diff(t *testing.T) { + var vlan packngo.VirtualNetwork + rs := acctest.RandString(10) + metro := "sv" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + CheckDestroy: testAccMetalVlanCheckDestroyed, + Steps: []resource.TestStep{ + { + Config: testAccCheckMetalVlanConfig_metro(rs, metro, "tfacc-vlan"), + Check: resource.ComposeTestCheckFunc( + testAccCheckMetalVlanExists("equinix_metal_vlan.foovlan", &vlan), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.foovlan", "description", "tfacc-vlan"), + resource.TestCheckResourceAttr( + "equinix_metal_vlan.foovlan", "metro", metro), + ), + }, + { + Config: testAccCheckMetalVlanConfig_metro(rs, strings.ToUpper(metro), "tfacc-vlan"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} diff --git a/internal/resources/metal/vlans/sweeper.go b/internal/resources/metal/vlan/sweeper.go similarity index 88% rename from internal/resources/metal/vlans/sweeper.go rename to internal/resources/metal/vlan/sweeper.go index 7bb00ff69..b55788bbd 100644 --- a/internal/resources/metal/vlans/sweeper.go +++ b/internal/resources/metal/vlan/sweeper.go @@ -1,4 +1,4 @@ -package vlans +package vlan import ( "fmt" @@ -17,15 +17,15 @@ func AddTestSweeper() { } func testSweepVlans(region string) error { - log.Printf("[DEBUG] Sweeping vlans") + log.Printf("[DEBUG] Sweeping vlan") config, err := sweep.GetConfigForMetal() if err != nil { - return fmt.Errorf("[INFO][SWEEPER_LOG] Error getting configuration for sweeping vlans: %s", err) + return fmt.Errorf("[INFO][SWEEPER_LOG] Error getting configuration for sweeping vlan: %s", err) } metal := config.NewMetalClient() ps, _, err := metal.Projects.List(nil) if err != nil { - return fmt.Errorf("[INFO][SWEEPER_LOG] Error getting project list for sweeping vlans: %s", err) + return fmt.Errorf("[INFO][SWEEPER_LOG] Error getting project list for sweeping vlan: %s", err) } pids := []string{} for _, p := range ps { @@ -37,7 +37,7 @@ func testSweepVlans(region string) error { for _, pid := range pids { ds, _, err := metal.ProjectVirtualNetworks.List(pid, nil) if err != nil { - log.Printf("Error listing vlans to sweep: %s", err) + log.Printf("Error listing vlan to sweep: %s", err) continue } for _, d := range ds.VirtualNetworks { diff --git a/internal/sweep/sweep_test.go b/internal/sweep/sweep_test.go index 778330de6..57f9fbb64 100644 --- a/internal/sweep/sweep_test.go +++ b/internal/sweep/sweep_test.go @@ -1,7 +1,7 @@ package sweep_test import ( - "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlans" + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlan" "testing" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/device" @@ -28,6 +28,6 @@ func addTestSweepers() { ssh_key.AddTestSweeper() user_api_key.AddTestSweeper() virtual_circuit.AddTestSweeper() - vlans.AddTestSweeper() + vlan.AddTestSweeper() vrf.AddTestSweeper() } From b6c792ee401b1b18fccb53c7132c0fe6e18c031d Mon Sep 17 00:00:00 2001 From: Ayush Rangwala Date: Fri, 8 Mar 2024 21:43:06 +0530 Subject: [PATCH 5/7] fix resource schema --- internal/planmodifiers/caseinsensitive.go | 7 +++++- internal/resources/metal/vlan/datasource.go | 7 +++--- .../resources/metal/vlan/datasource_test.go | 2 +- internal/resources/metal/vlan/models.go | 22 ++++++++++++------- internal/resources/metal/vlan/resource.go | 6 +++-- .../resources/metal/vlan/resource_schema.go | 5 ++--- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/internal/planmodifiers/caseinsensitive.go b/internal/planmodifiers/caseinsensitive.go index 88f3be6bb..7a9d8df1f 100644 --- a/internal/planmodifiers/caseinsensitive.go +++ b/internal/planmodifiers/caseinsensitive.go @@ -15,12 +15,17 @@ func CaseInsensitiveString() planmodifier.String { type caseInsensitivePlanModifier struct{} func (d *caseInsensitivePlanModifier) PlanModifyString(ctx context.Context, request planmodifier.StringRequest, response *planmodifier.StringResponse) { + if request.StateValue.IsNull() && request.PlanValue.IsUnknown() { + return + } + oldValue := request.StateValue.ValueString() newValue := request.PlanValue.ValueString() result := oldValue - if !strings.EqualFold(strings.ToLower(newValue), strings.ToLower(oldValue)) { + if !strings.EqualFold(newValue, oldValue) { result = newValue + response.RequiresReplace = true } response.PlanValue = types.StringValue(result) diff --git a/internal/resources/metal/vlan/datasource.go b/internal/resources/metal/vlan/datasource.go index 1fa0353d2..9a751d450 100644 --- a/internal/resources/metal/vlan/datasource.go +++ b/internal/resources/metal/vlan/datasource.go @@ -3,6 +3,7 @@ package vlan import ( "context" "fmt" + "strings" equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" "github.com/equinix/terraform-provider-equinix/internal/framework" @@ -115,10 +116,10 @@ func MatchingVlan(vlans []packngo.VirtualNetwork, vxlan int, projectID, facility if vxlan != 0 && v.VXLAN != vxlan { continue } - if facility != "" && v.FacilityCode != facility { + if facility != "" && !strings.EqualFold(v.FacilityCode, facility) { continue } - if metro != "" && v.MetroCode != metro { + if metro != "" && !strings.EqualFold(v.MetroCode, metro) { continue } matches = append(matches, v) @@ -128,7 +129,7 @@ func MatchingVlan(vlans []packngo.VirtualNetwork, vxlan int, projectID, facility } if len(matches) == 0 { - return nil, equinix_errors.FriendlyError(fmt.Errorf("Project %s does not have matching VLANs", projectID)) + return nil, equinix_errors.FriendlyError(fmt.Errorf("Project %s does not have matching VLANs for vlan [%d] and metro [%s]", projectID, vxlan, metro)) } return &matches[0], nil } diff --git a/internal/resources/metal/vlan/datasource_test.go b/internal/resources/metal/vlan/datasource_test.go index 6688f46e0..cae8a7deb 100644 --- a/internal/resources/metal/vlan/datasource_test.go +++ b/internal/resources/metal/vlan/datasource_test.go @@ -8,10 +8,10 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/equinix/terraform-provider-equinix/internal/config" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/vlan" - "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/packethost/packngo" ) diff --git a/internal/resources/metal/vlan/models.go b/internal/resources/metal/vlan/models.go index bb4e61286..92da521d6 100644 --- a/internal/resources/metal/vlan/models.go +++ b/internal/resources/metal/vlan/models.go @@ -27,20 +27,26 @@ func (m *DataSourceModel) parse(vlan *packngo.VirtualNetwork) diag.Diagnostics { } m.VlanID = types.StringValue(vlan.ID) - - m.Facility = types.StringNull() - if vlan.FacilityCode != "" { - m.Facility = types.StringValue(vlan.FacilityCode) - } - m.Description = types.StringValue(vlan.Description) m.Vxlan = types.Int64Value(int64(vlan.VXLAN)) + var metroCode, facilityCode types.String + if vlan.Facility != nil { + facilityCode = types.StringValue(vlan.Facility.Code) + metroCode = types.StringValue(strings.ToLower(vlan.Facility.Metro.Code)) + } // version of this resource. StateFunc doesn't exist in terraform and it requires implementation // of bespoke logic before storing state. To ensure backward compatibility we ignore lower/upper // case diff for now, but we may want to require input upper case - if !strings.EqualFold(m.Metro.ValueString(), vlan.MetroCode) { - m.Metro = types.StringValue(vlan.MetroCode) + if !strings.EqualFold(m.Facility.ValueString(), facilityCode.ValueString()) { + m.Facility = facilityCode + } + + if vlan.Metro != nil { + metroCode = types.StringValue(strings.ToLower(vlan.Metro.Code)) + } + if !strings.EqualFold(m.Metro.ValueString(), metroCode.ValueString()) { + m.Metro = metroCode } deviceIds := make([]types.String, 0, len(vlan.Instances)) diff --git a/internal/resources/metal/vlan/resource.go b/internal/resources/metal/vlan/resource.go index 23028f6a2..6adf064bf 100644 --- a/internal/resources/metal/vlan/resource.go +++ b/internal/resources/metal/vlan/resource.go @@ -6,6 +6,8 @@ import ( "fmt" equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" "github.com/equinix/terraform-provider-equinix/internal/framework" + "github.com/hashicorp/terraform-plugin-framework/types" + "strings" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -72,7 +74,7 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r Description: data.Description.ValueString(), } if !data.Metro.IsNull() { - createRequest.Metro = data.Metro.ValueString() + createRequest.Metro = strings.ToLower(data.Metro.ValueString()) createRequest.VXLAN = int(data.Vxlan.ValueInt64()) } if !data.Facility.IsNull() { @@ -99,7 +101,6 @@ func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, r // Set state to fully populated data response.Diagnostics.Append(response.State.Set(ctx, &data)...) - return } func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { @@ -145,6 +146,7 @@ func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp return } + data.Metro = types.StringValue(strings.ToLower(data.Metro.ValueString())) if diag := resp.State.Set(ctx, &data); diag.HasError() { resp.Diagnostics.Append(diag...) return diff --git a/internal/resources/metal/vlan/resource_schema.go b/internal/resources/metal/vlan/resource_schema.go index 3ad6f5261..11405c340 100644 --- a/internal/resources/metal/vlan/resource_schema.go +++ b/internal/resources/metal/vlan/resource_schema.go @@ -40,7 +40,6 @@ func resourceSchema(ctx context.Context) schema.Schema { Description: "Facility where to create the VLAN", DeprecationMessage: "Use metro instead of facility. For more information, read the migration guide: https://registry.terraform.io/providers/equinix/equinix/latest/docs/guides/migration_guide_facilities_to_metros_devices", Optional: true, - Computed: true, Validators: []validator.String{ stringvalidator.ConflictsWith(path.MatchRoot("metro")), }, @@ -54,11 +53,10 @@ func resourceSchema(ctx context.Context) schema.Schema { Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ - //stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), equinixplanmodifiers.CaseInsensitiveString(), }, Validators: []validator.String{ - stringvalidator.ConflictsWith(path.MatchRoot("facility")), stringvalidator.ExactlyOneOf(path.MatchRoot("facility"), path.MatchRoot("metro")), }, @@ -66,6 +64,7 @@ func resourceSchema(ctx context.Context) schema.Schema { "vxlan": schema.Int64Attribute{ Description: "VLAN ID, must be unique in metro", PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), int64planmodifier.RequiresReplace(), }, Optional: true, From e8ff49c295c56b8c422d18ebe41dc4d2b9f20e34 Mon Sep 17 00:00:00 2001 From: Ayush Rangwala Date: Thu, 14 Mar 2024 20:58:21 +0530 Subject: [PATCH 6/7] add default value to schema --- internal/resources/metal/vlan/models.go | 42 ++++++------------- .../resources/metal/vlan/resource_schema.go | 7 +++- 2 files changed, 18 insertions(+), 31 deletions(-) diff --git a/internal/resources/metal/vlan/models.go b/internal/resources/metal/vlan/models.go index 92da521d6..bab880ef1 100644 --- a/internal/resources/metal/vlan/models.go +++ b/internal/resources/metal/vlan/models.go @@ -21,32 +21,22 @@ type DataSourceModel struct { func (m *DataSourceModel) parse(vlan *packngo.VirtualNetwork) diag.Diagnostics { m.ID = types.StringValue(vlan.ID) + m.VlanID = types.StringValue(vlan.ID) + m.Description = types.StringValue(vlan.Description) + m.Vxlan = types.Int64Value(int64(vlan.VXLAN)) + m.Facility = types.StringValue("") if vlan.Project.ID != "" { m.ProjectID = types.StringValue(vlan.Project.ID) } - m.VlanID = types.StringValue(vlan.ID) - m.Description = types.StringValue(vlan.Description) - m.Vxlan = types.Int64Value(int64(vlan.VXLAN)) - - var metroCode, facilityCode types.String if vlan.Facility != nil { - facilityCode = types.StringValue(vlan.Facility.Code) - metroCode = types.StringValue(strings.ToLower(vlan.Facility.Metro.Code)) - } - // version of this resource. StateFunc doesn't exist in terraform and it requires implementation - // of bespoke logic before storing state. To ensure backward compatibility we ignore lower/upper - // case diff for now, but we may want to require input upper case - if !strings.EqualFold(m.Facility.ValueString(), facilityCode.ValueString()) { - m.Facility = facilityCode + m.Facility = types.StringValue(strings.ToLower(vlan.Facility.Code)) + m.Metro = types.StringValue(strings.ToLower(vlan.Facility.Metro.Code)) } if vlan.Metro != nil { - metroCode = types.StringValue(strings.ToLower(vlan.Metro.Code)) - } - if !strings.EqualFold(m.Metro.ValueString(), metroCode.ValueString()) { - m.Metro = metroCode + m.Metro = types.StringValue(strings.ToLower(vlan.Metro.Code)) } deviceIds := make([]types.String, 0, len(vlan.Instances)) @@ -70,28 +60,20 @@ func (m *ResourceModel) parse(vlan *packngo.VirtualNetwork) diag.Diagnostics { m.ID = types.StringValue(vlan.ID) m.Description = types.StringValue(vlan.Description) m.Vxlan = types.Int64Value(int64(vlan.VXLAN)) + m.Facility = types.StringValue("") if vlan.Project.ID != "" { m.ProjectID = types.StringValue(vlan.Project.ID) } - var metroCode, facilityCode types.String if vlan.Facility != nil { - facilityCode = types.StringValue(vlan.Facility.Code) - metroCode = types.StringValue(strings.ToLower(vlan.Facility.Metro.Code)) - } - // version of this resource. StateFunc doesn't exist in terraform and it requires implementation - // of bespoke logic before storing state. To ensure backward compatibility we ignore lower/upper - // case diff for now, but we may want to require input upper case - if !strings.EqualFold(m.Facility.ValueString(), facilityCode.ValueString()) { - m.Facility = facilityCode + m.Facility = types.StringValue(strings.ToLower(vlan.Facility.Code)) + m.Metro = types.StringValue(strings.ToLower(vlan.Facility.Metro.Code)) } if vlan.Metro != nil { - metroCode = types.StringValue(strings.ToLower(vlan.Metro.Code)) - } - if !strings.EqualFold(m.Metro.ValueString(), metroCode.ValueString()) { - m.Metro = metroCode + m.Metro = types.StringValue(strings.ToLower(vlan.Metro.Code)) } + return nil } diff --git a/internal/resources/metal/vlan/resource_schema.go b/internal/resources/metal/vlan/resource_schema.go index 11405c340..b1e2cde5d 100644 --- a/internal/resources/metal/vlan/resource_schema.go +++ b/internal/resources/metal/vlan/resource_schema.go @@ -2,14 +2,17 @@ package vlan import ( "context" - equinixplanmodifiers "github.com/equinix/terraform-provider-equinix/internal/planmodifiers" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + equinixplanmodifiers "github.com/equinix/terraform-provider-equinix/internal/planmodifiers" ) func resourceSchema(ctx context.Context) schema.Schema { @@ -40,6 +43,7 @@ func resourceSchema(ctx context.Context) schema.Schema { Description: "Facility where to create the VLAN", DeprecationMessage: "Use metro instead of facility. For more information, read the migration guide: https://registry.terraform.io/providers/equinix/equinix/latest/docs/guides/migration_guide_facilities_to_metros_devices", Optional: true, + Computed: true, Validators: []validator.String{ stringvalidator.ConflictsWith(path.MatchRoot("metro")), }, @@ -47,6 +51,7 @@ func resourceSchema(ctx context.Context) schema.Schema { stringplanmodifier.RequiresReplace(), stringplanmodifier.UseStateForUnknown(), }, + Default: stringdefault.StaticString(""), }, "metro": schema.StringAttribute{ Description: "Metro in which to create the VLAN", From 5fc741715fedba4dbb5e82e990dea703f25e377b Mon Sep 17 00:00:00 2001 From: Charles Treatman Date: Tue, 19 Mar 2024 14:16:53 -0500 Subject: [PATCH 7/7] update schema to allow removing facility from configuration --- internal/resources/metal/vlan/resource_schema.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/resources/metal/vlan/resource_schema.go b/internal/resources/metal/vlan/resource_schema.go index b1e2cde5d..0913362e7 100644 --- a/internal/resources/metal/vlan/resource_schema.go +++ b/internal/resources/metal/vlan/resource_schema.go @@ -8,7 +8,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -48,16 +47,16 @@ func resourceSchema(ctx context.Context) schema.Schema { stringvalidator.ConflictsWith(path.MatchRoot("metro")), }, PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), + stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown(), }, - Default: stringdefault.StaticString(""), }, "metro": schema.StringAttribute{ Description: "Metro in which to create the VLAN", Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), stringplanmodifier.UseStateForUnknown(), equinixplanmodifiers.CaseInsensitiveString(), },