From e86dd8ac0570528d1dc3bf5ad178570192c6bdae Mon Sep 17 00:00:00 2001 From: Aayush Rangwala Date: Wed, 20 Mar 2024 02:57:33 +0530 Subject: [PATCH] chore: equinix_metal_vlan plugin sdk v2 to framework migration (#578) Part of https://github.com/equinix/terraform-provider-equinix/issues/612 --------- Signed-off-by: Ayush Rangwala Signed-off-by: Charles Treatman Co-authored-by: Charles Treatman --- equinix/data_source_metal_vlan.go | 159 ------------ equinix/provider.go | 2 - equinix/resource_metal_vlan.go | 176 ------------- equinix/resource_metal_vlan_acc_test.go | 153 ----------- internal/planmodifiers/caseinsensitive.go | 40 +++ .../planmodifiers/caseinsensitive_test.go | 56 ++++ internal/provider/provider.go | 3 + internal/resources/metal/vlan/datasource.go | 135 ++++++++++ .../resources/metal/vlan/datasource_schema.go | 86 +++++++ .../resources/metal/vlan/datasource_test.go | 124 ++++----- internal/resources/metal/vlan/models.go | 79 ++++++ internal/resources/metal/vlan/resource.go | 204 +++++++++++++++ .../resources/metal/vlan/resource_schema.go | 79 ++++++ .../resources/metal/vlan/resource_test.go | 243 ++++++++++++++++++ internal/resources/metal/vlan/sweeper.go | 8 +- internal/sweep/sweep_test.go | 2 +- 16 files changed, 995 insertions(+), 554 deletions(-) delete mode 100644 equinix/data_source_metal_vlan.go delete mode 100644 equinix/resource_metal_vlan.go delete mode 100644 equinix/resource_metal_vlan_acc_test.go create mode 100644 internal/planmodifiers/caseinsensitive.go create mode 100644 internal/planmodifiers/caseinsensitive_test.go create mode 100644 internal/resources/metal/vlan/datasource.go create mode 100644 internal/resources/metal/vlan/datasource_schema.go rename equinix/data_source_metal_vlan_acc_test.go => internal/resources/metal/vlan/datasource_test.go (77%) create mode 100644 internal/resources/metal/vlan/models.go create mode 100644 internal/resources/metal/vlan/resource.go create mode 100644 internal/resources/metal/vlan/resource_schema.go create mode 100644 internal/resources/metal/vlan/resource_test.go diff --git a/equinix/data_source_metal_vlan.go b/equinix/data_source_metal_vlan.go deleted file mode 100644 index 11a952e81..000000000 --- a/equinix/data_source_metal_vlan.go +++ /dev/null @@ -1,159 +0,0 @@ -package equinix - -import ( - "fmt" - - "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/helper/schema" - "github.com/packethost/packngo" -) - -func dataSourceMetalVlan() *schema.Resource { - return &schema.Resource{ - Read: dataSourceMetalVlanRead, - Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, - }, - 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", - }, - "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(d *schema.ResourceData, meta interface{}) error { - client := meta.(*config.Config).Metal - - 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") - - 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")) - } - - var vlan *packngo.VirtualNetwork - - if vlanIdOk { - var err error - vlan, _, err = client.ProjectVirtualNetworks.Get( - vlanIdRaw.(string), - &packngo.GetOptions{Includes: []string{"assigned_to"}}, - ) - if err != nil { - return equinix_errors.FriendlyError(err) - } - - } else { - projectID := projectRaw.(string) - vxlan := vxlanRaw.(int) - metro := metroRaw.(string) - facility := facilityRaw.(string) - vlans, _, err := client.ProjectVirtualNetworks.List( - projectRaw.(string), - &packngo.GetOptions{Includes: []string{"assigned_to"}}, - ) - if err != nil { - return equinix_errors.FriendlyError(err) - } - - vlan, err = matchingVlan(vlans.VirtualNetworks, vxlan, projectID, facility, metro) - if err != nil { - return equinix_errors.FriendlyError(err) - } - } - - assignedDevices := []string{} - for _, d := range vlan.Instances { - assignedDevices = append(assignedDevices, d.ID) - } - - d.SetId(vlan.ID) - - return 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) { - matches := []packngo.VirtualNetwork{} - for _, v := range vlans { - if vxlan != 0 && v.VXLAN != vxlan { - continue - } - if facility != "" && v.FacilityCode != facility { - continue - } - if metro != "" && v.MetroCode != metro { - continue - } - matches = append(matches, v) - } - if len(matches) > 1 { - return nil, equinix_errors.FriendlyError(fmt.Errorf("Project %s has more than one matching VLAN", projectID)) - } - - if len(matches) == 0 { - return nil, equinix_errors.FriendlyError(fmt.Errorf("Project %s does not have matching VLANs", projectID)) - } - return &matches[0], nil -} diff --git a/equinix/provider.go b/equinix/provider.go index e1af82f75..ea0d0c04d 100644 --- a/equinix/provider.go +++ b/equinix/provider.go @@ -110,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": dataSourceMetalVlan(), "equinix_metal_vrf": vrf.DataSource(), }, ResourcesMap: map[string]*schema.Resource{ @@ -138,7 +137,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": resourceMetalVlan(), "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/resource_metal_vlan_acc_test.go b/equinix/resource_metal_vlan_acc_test.go deleted file mode 100644 index 7fd8e21da..000000000 --- a/equinix/resource_metal_vlan_acc_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package equinix - -import ( - "fmt" - "testing" - - "github.com/equinix/terraform-provider-equinix/internal/config" - - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/packethost/packngo" -) - -func testAccCheckMetalVlanConfig_metro(projSuffix, metro, 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 - metro = "%s" - description = "%s" - vxlan = 5 -} -`, projSuffix, metro, desc) -} - -func TestAccMetalVlan_metro(t *testing.T) { - rs := acctest.RandString(10) - metro := "sv" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, - CheckDestroy: testAccMetalVlanCheckDestroyed, - 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() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, - 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), - ), - }, - }, - }) -} - -func testAccCheckMetalVlanExists(n string, vlan *packngo.VirtualNetwork) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not found: %s", n) - } - if rs.Primary.ID == "" { - return fmt.Errorf("No Record ID is set") - } - - client := testAccProvider.Meta().(*config.Config).Metal - - foundVlan, _, err := client.ProjectVirtualNetworks.Get(rs.Primary.ID, nil) - if err != nil { - return err - } - if foundVlan.ID != rs.Primary.ID { - return fmt.Errorf("Record not found: %v - %v", rs.Primary.ID, foundVlan) - } - - *vlan = *foundVlan - - return nil - } -} - -func testAccMetalVlanCheckDestroyed(s *terraform.State) error { - client := testAccProvider.Meta().(*config.Config).Metal - - for _, rs := range s.RootModule().Resources { - if rs.Type != "equinix_metal_vlan" { - continue - } - if _, _, err := client.ProjectVirtualNetworks.Get(rs.Primary.ID, nil); err == nil { - return fmt.Errorf("Metal Vlan still exists") - } - } - - 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" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, - CheckDestroy: testAccMetalVlanCheckDestroyed, - Steps: []resource.TestStep{ - { - Config: testAccMetalVlanConfig_var(rs, fac, "tfacc-vlan"), - }, - { - ResourceName: "equinix_metal_vlan.foovlan", - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} diff --git a/internal/planmodifiers/caseinsensitive.go b/internal/planmodifiers/caseinsensitive.go new file mode 100644 index 000000000..7a9d8df1f --- /dev/null +++ b/internal/planmodifiers/caseinsensitive.go @@ -0,0 +1,40 @@ +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) { + if request.StateValue.IsNull() && request.PlanValue.IsUnknown() { + return + } + + oldValue := request.StateValue.ValueString() + newValue := request.PlanValue.ValueString() + + result := oldValue + if !strings.EqualFold(newValue, oldValue) { + result = newValue + response.RequiresReplace = true + } + + 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 f5677c607..fab6a8e46 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -11,6 +11,7 @@ import ( metalorganizationmember "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization_member" 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/vlan" 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" @@ -118,6 +119,7 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res metalconnection.NewResource, metalorganization.NewResource, metalorganizationmember.NewResource, + vlan.NewResource, } } @@ -127,5 +129,6 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource metalprojectsshkey.NewDataSource, metalconnection.NewDataSource, metalorganization.NewDataSource, + vlan.NewDataSource, } } diff --git a/internal/resources/metal/vlan/datasource.go b/internal/resources/metal/vlan/datasource.go new file mode 100644 index 000000000..9a751d450 --- /dev/null +++ b/internal/resources/metal/vlan/datasource.go @@ -0,0 +1,135 @@ +package vlan + +import ( + "context" + "fmt" + "strings" + + equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" + "github.com/equinix/terraform-provider-equinix/internal/framework" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/packethost/packngo" +) + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: framework.NewBaseDataSource( + framework.BaseDataSourceConfig{ + Name: "equinix_metal_vlan", + }, + ), + } +} + +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 +} + +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 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 !data.VlanID.IsNull() { + var err error + vlan, _, err = client.ProjectVirtualNetworks.Get( + data.VlanID.ValueString(), + &packngo.GetOptions{Includes: []string{"assigned_to"}}, + ) + if err != nil { + resp.Diagnostics.AddError("Error fetching Vlan using vlanId", equinix_errors.FriendlyError(err).Error()) + return + } + + } else { + vlans, _, err := client.ProjectVirtualNetworks.List( + data.ProjectID.ValueString(), + &packngo.GetOptions{Includes: []string{"assigned_to"}}, + ) + if err != nil { + resp.Diagnostics.AddError("Error fetching vlan list for projectId", + equinix_errors.FriendlyError(err).Error()) + return + } + + vlan, err = MatchingVlan(vlans.VirtualNetworks, int(data.Vxlan.ValueInt64()), data.ProjectID.ValueString(), + data.Facility.ValueString(), data.Metro.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Error expected vlan not found", equinix_errors.FriendlyError(err).Error()) + return + } + } + + assignedDevices := []string{} + for _, d := range vlan.Instances { + assignedDevices = append(assignedDevices, d.ID) + } + + // Set state to fully populated data + resp.Diagnostics.Append(data.parse(vlan)...) + if resp.Diagnostics.HasError() { + return + } + + // 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) { + matches := []packngo.VirtualNetwork{} + for _, v := range vlans { + if vxlan != 0 && v.VXLAN != vxlan { + continue + } + if facility != "" && !strings.EqualFold(v.FacilityCode, facility) { + continue + } + if metro != "" && !strings.EqualFold(v.MetroCode, metro) { + continue + } + matches = append(matches, v) + } + if len(matches) > 1 { + return nil, equinix_errors.FriendlyError(fmt.Errorf("Project %s has more than one matching VLAN", projectID)) + } + + if len(matches) == 0 { + 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_schema.go b/internal/resources/metal/vlan/datasource_schema.go new file mode 100644 index 000000000..4f0fb525c --- /dev/null +++ b/internal/resources/metal/vlan/datasource_schema.go @@ -0,0 +1,86 @@ +package vlan + +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/equinix/data_source_metal_vlan_acc_test.go b/internal/resources/metal/vlan/datasource_test.go similarity index 77% rename from equinix/data_source_metal_vlan_acc_test.go rename to internal/resources/metal/vlan/datasource_test.go index f77d1e416..cae8a7deb 100644 --- a/equinix/data_source_metal_vlan_acc_test.go +++ b/internal/resources/metal/vlan/datasource_test.go @@ -1,73 +1,29 @@ -package equinix +package vlan_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/vlan" "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" ) -func TestAccDataSourceMetalVlan_byVxlanFacility(t *testing.T) { - rs := acctest.RandString(10) - fac := "sv15" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - ProtoV5ProviderFactories: testAccProtoV5ProviderFactories, - 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" 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 +92,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 +138,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 +265,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 := 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 @@ -322,7 +278,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" { @@ -335,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/vlan/models.go b/internal/resources/metal/vlan/models.go new file mode 100644 index 000000000..bab880ef1 --- /dev/null +++ b/internal/resources/metal/vlan/models.go @@ -0,0 +1,79 @@ +package vlan + +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) + 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) + } + + if vlan.Facility != nil { + m.Facility = types.StringValue(strings.ToLower(vlan.Facility.Code)) + m.Metro = types.StringValue(strings.ToLower(vlan.Facility.Metro.Code)) + } + + if vlan.Metro != nil { + m.Metro = types.StringValue(strings.ToLower(vlan.Metro.Code)) + } + + 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) + 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) + } + + if vlan.Facility != nil { + m.Facility = types.StringValue(strings.ToLower(vlan.Facility.Code)) + m.Metro = types.StringValue(strings.ToLower(vlan.Facility.Metro.Code)) + } + + if vlan.Metro != nil { + m.Metro = types.StringValue(strings.ToLower(vlan.Metro.Code)) + } + + return nil +} diff --git a/internal/resources/metal/vlan/resource.go b/internal/resources/metal/vlan/resource.go new file mode 100644 index 000000000..6adf064bf --- /dev/null +++ b/internal/resources/metal/vlan/resource.go @@ -0,0 +1,204 @@ +package vlan + +import ( + "context" + "errors" + "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" + "github.com/packethost/packngo" +) + +var ( + vlanDefaultIncludes = []string{"assigned_to", "facility", "metro"} +) + +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 (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + r.Meta.AddFwModuleToMetalUserAgent(ctx, request.ProviderMeta) + client := r.Meta.Metal + + var data ResourceModel + response.Diagnostics.Append(request.Config.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + 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 !data.Facility.IsNull() && !data.Vxlan.IsNull() { + response.Diagnostics.AddError("Invalid input params", + equinix_errors.FriendlyError(errors.New("you can set vxlan only for metro vlan")).Error()) + return + } + + createRequest := &packngo.VirtualNetworkCreateRequest{ + ProjectID: data.ProjectID.ValueString(), + Description: data.Description.ValueString(), + } + if !data.Metro.IsNull() { + createRequest.Metro = strings.ToLower(data.Metro.ValueString()) + createRequest.VXLAN = int(data.Vxlan.ValueInt64()) + } + if !data.Facility.IsNull() { + createRequest.Facility = data.Facility.ValueString() + } + vlan, _, err := client.ProjectVirtualNetworks.Create(createRequest) + if err != nil { + response.Diagnostics.AddError("Error creating Vlan", equinix_errors.FriendlyError(err).Error()) + 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() { + return + } + + // Set state to fully populated data + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *Resource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + 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 + } + + vlan, _, err := client.ProjectVirtualNetworks.Get( + data.ID.ValueString(), + &packngo.GetOptions{Includes: vlanDefaultIncludes}, + ) + if err != nil { + if equinix_errors.IsNotFound(err) { + 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 + } + 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 + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +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 + } + + data.Metro = types.StringValue(strings.ToLower(data.Metro.ValueString())) + 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) + client := r.Meta.Metal + + var data ResourceModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + 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 + } + 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 + } + } + } + } + } + + 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 + } +} diff --git a/internal/resources/metal/vlan/resource_schema.go b/internal/resources/metal/vlan/resource_schema.go new file mode 100644 index 000000000..0913362e7 --- /dev/null +++ b/internal/resources/metal/vlan/resource_schema.go @@ -0,0 +1,79 @@ +package vlan + +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" + + equinixplanmodifiers "github.com/equinix/terraform-provider-equinix/internal/planmodifiers" +) + +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, + Computed: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("metro")), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplaceIfConfigured(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + "metro": schema.StringAttribute{ + Description: "Metro in which to create the VLAN", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + equinixplanmodifiers.CaseInsensitiveString(), + }, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRoot("facility"), + path.MatchRoot("metro")), + }, + }, + "vxlan": schema.Int64Attribute{ + Description: "VLAN ID, must be unique in metro", + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + int64planmodifier.RequiresReplace(), + }, + Optional: true, + Computed: true, + }, + }, + } +} diff --git a/internal/resources/metal/vlan/resource_test.go b/internal/resources/metal/vlan/resource_test.go new file mode 100644 index 000000000..17b5a858c --- /dev/null +++ b/internal/resources/metal/vlan/resource_test.go @@ -0,0 +1,243 @@ +package vlan_test + +import ( + "fmt" + "strings" + "testing" + + "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" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/packethost/packngo" +) + +func testAccCheckMetalVlanConfig_metro(projSuffix, metro, 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 + metro = "%s" + description = "%s" +} +`, 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) + 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), + ), + }, + }, + }) +} + +func testAccCheckMetalVlanExists(n string, vlan *packngo.VirtualNetwork) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + if rs.Primary.ID == "" { + return fmt.Errorf("No Record ID is set") + } + + client := acceptance.TestAccProvider.Meta().(*config.Config).Metal + + foundVlan, _, err := client.ProjectVirtualNetworks.Get(rs.Primary.ID, nil) + if err != nil { + return err + } + if foundVlan.ID != rs.Primary.ID { + return fmt.Errorf("Record not found: %v - %v", rs.Primary.ID, foundVlan) + } + + *vlan = *foundVlan + + return nil + } +} + +func testAccMetalVlanCheckDestroyed(s *terraform.State) error { + client := acceptance.TestAccProvider.Meta().(*config.Config).Metal + + for _, rs := range s.RootModule().Resources { + if rs.Type != "equinix_metal_vlan" { + continue + } + if _, _, err := client.ProjectVirtualNetworks.Get(rs.Primary.ID, nil); err == nil { + return fmt.Errorf("Metal Vlan still exists") + } + } + + return nil +} + +func TestAccMetalVlan_importBasic(t *testing.T) { + 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"), + }, + { + ResourceName: "equinix_metal_vlan.foovlan", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +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) + 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(), + }, + }, + }, + }, + }) +} + +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/vlan/sweeper.go b/internal/resources/metal/vlan/sweeper.go index 5f674346f..ba41b3578 100644 --- a/internal/resources/metal/vlan/sweeper.go +++ b/internal/resources/metal/vlan/sweeper.go @@ -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 eeb0b668b..8a725898f 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/vlan" "testing" "github.com/equinix/terraform-provider-equinix/internal/resources/metal/connection" @@ -10,7 +11,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" )