From 2dfde31ca46f5b4cd317de00f63c02cee9fb2dd9 Mon Sep 17 00:00:00 2001 From: Tomas Karasek Date: Thu, 7 Dec 2023 16:02:56 +0100 Subject: [PATCH] First draft of changes to separate a resource away from equinix pkg --- equinix/data_source_metal_project_ssh_key.go | 128 ------------------ equinix/equinix_sweeper_test.go | 16 +-- equinix/provider.go | 30 ++-- equinix/provider_test.go | 11 +- equinix/resource_metal_project_ssh_key.go | 25 ---- internal/acceptance/acceptance.go | 83 ++++++++++++ internal/acceptance/device_helpers.go | 119 ++++++++++++++++ internal/acceptance/ssh_key_helpers.go | 36 +++++ internal/config/config.go | 9 ++ .../metal/metal_project_ssh_key/datasource.go | 91 +++++++++++++ .../metal_project_ssh_key/datasource_test.go | 16 ++- .../metal/metal_project_ssh_key/resource.go | 19 +++ .../metal_project_ssh_key/resource_test.go | 21 ++- .../resources/metal/metal_ssh_key/resource.go | 65 ++------- .../metal/metal_ssh_key/resource_test.go | 76 ++++------- .../metal/metal_ssh_key/schema_common.go | 77 +++++++++++ 16 files changed, 519 insertions(+), 303 deletions(-) delete mode 100644 equinix/data_source_metal_project_ssh_key.go delete mode 100644 equinix/resource_metal_project_ssh_key.go create mode 100644 internal/acceptance/acceptance.go create mode 100644 internal/acceptance/device_helpers.go create mode 100644 internal/acceptance/ssh_key_helpers.go create mode 100644 internal/resources/metal/metal_project_ssh_key/datasource.go rename equinix/data_source_metal_project_ssh_key_acc_test.go => internal/resources/metal/metal_project_ssh_key/datasource_test.go (88%) create mode 100644 internal/resources/metal/metal_project_ssh_key/resource.go rename equinix/resource_metal_project_ssh_key_acc_test.go => internal/resources/metal/metal_project_ssh_key/resource_test.go (78%) rename equinix/resource_metal_ssh_key.go => internal/resources/metal/metal_ssh_key/resource.go (60%) rename equinix/resource_metal_ssh_key_acc_test.go => internal/resources/metal/metal_ssh_key/resource_test.go (74%) create mode 100644 internal/resources/metal/metal_ssh_key/schema_common.go diff --git a/equinix/data_source_metal_project_ssh_key.go b/equinix/data_source_metal_project_ssh_key.go deleted file mode 100644 index e20013c4c..000000000 --- a/equinix/data_source_metal_project_ssh_key.go +++ /dev/null @@ -1,128 +0,0 @@ -package equinix - -import ( - "fmt" - "path" - "strings" - - "github.com/equinix/terraform-provider-equinix/internal/config" - equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/packethost/packngo" -) - -func dataSourceMetalProjectSSHKey() *schema.Resource { - return &schema.Resource{ - Read: dataSourceMetalProjectSSHKeyRead, - Schema: map[string]*schema.Schema{ - "search": { - Type: schema.TypeString, - Description: "The name, fingerprint, id, or public_key of the SSH Key to search for in the Equinix Metal project", - Optional: true, - ValidateFunc: validation.NoZeroValues, - }, - "id": { - Type: schema.TypeString, - Description: "The id of the SSH Key", - Optional: true, - ValidateFunc: validation.NoZeroValues, - Computed: true, - }, - "project_id": { - Type: schema.TypeString, - Description: "The Equinix Metal project id of the Equinix Metal SSH Key", - Required: true, - ValidateFunc: validation.NoZeroValues, - }, - "name": { - Type: schema.TypeString, - Description: "The label of the Equinix Metal SSH Key", - Computed: true, - }, - "public_key": { - Type: schema.TypeString, - Description: "The public SSH key that will be authorized for SSH access on Equinix Metal devices provisioned with this key", - Computed: true, - }, - "fingerprint": { - Type: schema.TypeString, - Computed: true, - }, - "created": { - Type: schema.TypeString, - Computed: true, - }, - "updated": { - Type: schema.TypeString, - Computed: true, - }, - "owner_id": { - Type: schema.TypeString, - Computed: true, - }, - }, - } -} - -func dataSourceMetalProjectSSHKeyRead(d *schema.ResourceData, meta interface{}) error { - client := meta.(*config.Config).Metal - - search := d.Get("search").(string) - id := d.Get("id").(string) - projectID := d.Get("project_id").(string) - - if id == "" && search == "" { - return fmt.Errorf("You must supply either search or id") - } - - var ( - key packngo.SSHKey - searchOpts *packngo.SearchOptions - ) - - if search != "" { - searchOpts = &packngo.SearchOptions{Search: search} - } - keys, _, err := client.Projects.ListSSHKeys(projectID, searchOpts) - if err != nil { - err = fmt.Errorf("Error listing project ssh keys: %s", equinix_errors.FriendlyError(err)) - return err - } - - for i := range keys { - // use the first match for searches - if search != "" { - key = keys[i] - break - } - - // otherwise find the matching ID - if keys[i].ID == id { - key = keys[i] - break - } - } - - if key.ID == "" { - // Not Found - return fmt.Errorf("Project %q SSH Key matching %q was not found", projectID, search) - } - - ownerID := path.Base(key.Owner.Href) - - d.SetId(key.ID) - d.Set("name", key.Label) - d.Set("public_key", key.Key) - d.Set("fingerprint", key.FingerPrint) - d.Set("owner_id", ownerID) - d.Set("created", key.Created) - d.Set("updated", key.Updated) - - if strings.Contains(key.Owner.Href, "/projects/") { - d.Set("project_id", ownerID) - } - - return nil -} diff --git a/equinix/equinix_sweeper_test.go b/equinix/equinix_sweeper_test.go index f5e30109d..3cfcd1f32 100644 --- a/equinix/equinix_sweeper_test.go +++ b/equinix/equinix_sweeper_test.go @@ -19,20 +19,20 @@ func TestMain(m *testing.M) { } func sharedConfigForRegion(region string) (*config.Config, error) { - endpoint := getFromEnvDefault(endpointEnvVar, config.DefaultBaseURL) - clientToken := getFromEnvDefault(clientTokenEnvVar, "") - clientID := getFromEnvDefault(clientIDEnvVar, "") - clientSecret := getFromEnvDefault(clientSecretEnvVar, "") - clientTimeout := getFromEnvDefault(clientTimeoutEnvVar, strconv.Itoa(config.DefaultTimeout)) + endpoint := getFromEnvDefault(config.EndpointEnvVar, config.DefaultBaseURL) + clientToken := getFromEnvDefault(config.ClientTokenEnvVar, "") + clientID := getFromEnvDefault(config.ClientIDEnvVar, "") + clientSecret := getFromEnvDefault(config.ClientSecretEnvVar, "") + clientTimeout := getFromEnvDefault(config.ClientTimeoutEnvVar, strconv.Itoa(config.DefaultTimeout)) clientTimeoutInt, err := strconv.Atoi(clientTimeout) if err != nil { - return nil, fmt.Errorf("cannot convert value of '%s' env variable to int", clientTimeoutEnvVar) + return nil, fmt.Errorf("cannot convert value of '%s' env variable to int", config.ClientTimeoutEnvVar) } - metalAuthToken := getFromEnvDefault(metalAuthTokenEnvVar, "") + metalAuthToken := getFromEnvDefault(config.MetalAuthTokenEnvVar, "") if clientToken == "" && (clientID == "" || clientSecret == "") && metalAuthToken == "" { return nil, fmt.Errorf("To run acceptance tests sweeper, one of '%s' or pair '%s' - '%s' must be set for Equinix Fabric and Network Edge, and '%s' for Equinix Metal", - clientTokenEnvVar, clientIDEnvVar, clientSecretEnvVar, metalAuthTokenEnvVar) + config.ClientTokenEnvVar, config.ClientIDEnvVar, config.ClientSecretEnvVar, config.MetalAuthTokenEnvVar) } return &config.Config{ diff --git a/equinix/provider.go b/equinix/provider.go index 2f9ed13a6..03225ac60 100644 --- a/equinix/provider.go +++ b/equinix/provider.go @@ -9,6 +9,9 @@ import ( "strings" "time" + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/metal_project_ssh_key" + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/metal_ssh_key" + v4 "github.com/equinix-labs/fabric-go/fabric/v4" "github.com/equinix/ecx-go/v2" "github.com/equinix/rest-go" @@ -26,15 +29,6 @@ var ( NetworkTypeListHB = strings.Join(DeviceNetworkTypesHB, ", ") ) -const ( - endpointEnvVar = "EQUINIX_API_ENDPOINT" - clientIDEnvVar = "EQUINIX_API_CLIENTID" - clientSecretEnvVar = "EQUINIX_API_CLIENTSECRET" - clientTokenEnvVar = "EQUINIX_API_TOKEN" - clientTimeoutEnvVar = "EQUINIX_API_TIMEOUT" - metalAuthTokenEnvVar = "METAL_AUTH_TOKEN" -) - // resourceDataProvider provies interface to schema.ResourceData // for convenient mocking purposes type resourceDataProvider interface { @@ -51,38 +45,38 @@ func Provider() *schema.Provider { "endpoint": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc(endpointEnvVar, config.DefaultBaseURL), + DefaultFunc: schema.EnvDefaultFunc(config.EndpointEnvVar, config.DefaultBaseURL), ValidateFunc: validation.IsURLWithHTTPorHTTPS, Description: fmt.Sprintf("The Equinix API base URL to point out desired environment. Defaults to %s", config.DefaultBaseURL), }, "client_id": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc(clientIDEnvVar, ""), + DefaultFunc: schema.EnvDefaultFunc(config.ClientIDEnvVar, ""), Description: "API Consumer Key available under My Apps section in developer portal", }, "client_secret": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc(clientSecretEnvVar, ""), + DefaultFunc: schema.EnvDefaultFunc(config.ClientSecretEnvVar, ""), Description: "API Consumer secret available under My Apps section in developer portal", }, "token": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc(clientTokenEnvVar, ""), + DefaultFunc: schema.EnvDefaultFunc(config.ClientTokenEnvVar, ""), Description: "API token from the developer sandbox", }, "auth_token": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc(metalAuthTokenEnvVar, ""), + DefaultFunc: schema.EnvDefaultFunc(config.MetalAuthTokenEnvVar, ""), Description: "The Equinix Metal API auth key for API operations", }, "request_timeout": { Type: schema.TypeInt, Optional: true, - DefaultFunc: schema.EnvDefaultFunc(clientTimeoutEnvVar, config.DefaultTimeout), + DefaultFunc: schema.EnvDefaultFunc(config.ClientTimeoutEnvVar, config.DefaultTimeout), ValidateFunc: validation.IntAtLeast(1), Description: fmt.Sprintf("The duration of time, in seconds, that the Equinix Platform API Client should wait before canceling an API request. Defaults to %d", config.DefaultTimeout), }, @@ -135,7 +129,7 @@ func Provider() *schema.Provider { "equinix_metal_plans": dataSourceMetalPlans(), "equinix_metal_port": dataSourceMetalPort(), "equinix_metal_project": dataSourceMetalProject(), - "equinix_metal_project_ssh_key": dataSourceMetalProjectSSHKey(), + "equinix_metal_project_ssh_key": metal_project_ssh_key.DataSource(), "equinix_metal_reserved_ip_block": dataSourceMetalReservedIPBlock(), "equinix_metal_spot_market_request": dataSourceMetalSpotMarketRequest(), "equinix_metal_virtual_circuit": dataSourceMetalVirtualCircuit(), @@ -162,10 +156,10 @@ func Provider() *schema.Provider { "equinix_metal_connection": resourceMetalConnection(), "equinix_metal_device": resourceMetalDevice(), "equinix_metal_device_network_type": resourceMetalDeviceNetworkType(), - "equinix_metal_ssh_key": resourceMetalSSHKey(), + "equinix_metal_ssh_key": metal_ssh_key.Resource(), "equinix_metal_organization_member": resourceMetalOrganizationMember(), "equinix_metal_port": resourceMetalPort(), - "equinix_metal_project_ssh_key": resourceMetalProjectSSHKey(), + "equinix_metal_project_ssh_key": metal_project_ssh_key.Resource(), "equinix_metal_project": resourceMetalProject(), "equinix_metal_organization": resourceMetalOrganization(), "equinix_metal_reserved_ip_block": resourceMetalReservedIPBlock(), diff --git a/equinix/provider_test.go b/equinix/provider_test.go index 5a6595c7b..f6ee0dc0c 100644 --- a/equinix/provider_test.go +++ b/equinix/provider_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/equinix/terraform-provider-equinix/internal/config" "github.com/equinix/terraform-provider-equinix/internal/hashcode" "github.com/equinix/ecx-go/v2" @@ -403,20 +404,20 @@ func TestProvider_schemaSetToMap(t *testing.T) { func testAccPreCheck(t *testing.T) { var err error - if _, err = getFromEnv(clientTokenEnvVar); err != nil { - _, err = getFromEnv(clientIDEnvVar) + if _, err = getFromEnv(config.ClientTokenEnvVar); err != nil { + _, err = getFromEnv(config.ClientIDEnvVar) if err == nil { - _, err = getFromEnv(clientSecretEnvVar) + _, err = getFromEnv(config.ClientSecretEnvVar) } } if err == nil { - _, err = getFromEnv(metalAuthTokenEnvVar) + _, err = getFromEnv(config.MetalAuthTokenEnvVar) } if err != nil { t.Fatalf("To run acceptance tests, one of '%s' or pair '%s' - '%s' must be set for Equinix Fabric and Network Edge, and '%s' for Equinix Metal", - clientTokenEnvVar, clientIDEnvVar, clientSecretEnvVar, metalAuthTokenEnvVar) + config.ClientTokenEnvVar, config.ClientIDEnvVar, config.ClientSecretEnvVar, config.MetalAuthTokenEnvVar) } } diff --git a/equinix/resource_metal_project_ssh_key.go b/equinix/resource_metal_project_ssh_key.go deleted file mode 100644 index 845d9d0fb..000000000 --- a/equinix/resource_metal_project_ssh_key.go +++ /dev/null @@ -1,25 +0,0 @@ -package equinix - -import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceMetalProjectSSHKey() *schema.Resource { - pkeySchema := metalSSHKeyCommonFields() - pkeySchema["project_id"] = &schema.Schema{ - Type: schema.TypeString, - Description: "The ID of parent project", - ForceNew: true, - Required: true, - } - return &schema.Resource{ - Create: resourceMetalSSHKeyCreate, - Read: resourceMetalSSHKeyRead, - Update: resourceMetalSSHKeyUpdate, - Delete: resourceMetalSSHKeyDelete, - Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, - }, - Schema: pkeySchema, - } -} diff --git a/internal/acceptance/acceptance.go b/internal/acceptance/acceptance.go new file mode 100644 index 000000000..733db94c1 --- /dev/null +++ b/internal/acceptance/acceptance.go @@ -0,0 +1,83 @@ +package acceptance + +import ( + "fmt" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/equinix/terraform-provider-equinix/equinix" + "github.com/equinix/terraform-provider-equinix/internal/config" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + // duplicated from equinix_sweeoer_test.go + tstResourcePrefix = "tfacc" + missingMetalToken = "To run acceptance tests of Equinix Metal Resources, you must set %s" +) + +var ( + TestAccProvider *schema.Provider + TestAccProviders map[string]*schema.Provider + TestAccProviderFactories map[string]func() (*schema.Provider, error) + TestExternalProviders map[string]resource.ExternalProvider +) + +func init() { + TestAccProvider = equinix.Provider() + TestAccProviders = map[string]*schema.Provider{ + "equinix": TestAccProvider, + } + TestAccProviderFactories = map[string]func() (*schema.Provider, error){ + "equinix": func() (*schema.Provider, error) { + return TestAccProvider, nil + }, + } + TestExternalProviders = map[string]resource.ExternalProvider{ + "random": { + Source: "hashicorp/random", + }, + } +} + +func TestAccPreCheckMetal(t *testing.T) { + if os.Getenv(config.MetalAuthTokenEnvVar) == "" { + t.Fatalf(missingMetalToken, config.MetalAuthTokenEnvVar) + } +} + +func IsSweepableTestResource(namePrefix string) bool { + return strings.HasPrefix(namePrefix, tstResourcePrefix) +} + +func getFromEnvDefault(varName string, defaultValue string) string { + if v := os.Getenv(varName); v != "" { + return v + } + return defaultValue +} + +func GetConfigForNonStandardMetalTest() (*config.Config, error) { + endpoint := getFromEnvDefault(config.EndpointEnvVar, config.DefaultBaseURL) + clientTimeout := getFromEnvDefault(config.ClientTimeoutEnvVar, strconv.Itoa(config.DefaultTimeout)) + clientTimeoutInt, err := strconv.Atoi(clientTimeout) + if err != nil { + return nil, fmt.Errorf("cannot convert value of '%s' env variable to int", config.ClientTimeoutEnvVar) + } + metalAuthToken := getFromEnvDefault(config.MetalAuthTokenEnvVar, "") + + if metalAuthToken == "" { + return nil, fmt.Errorf(missingMetalToken, config.MetalAuthTokenEnvVar) + } + + return &config.Config{ + AuthToken: metalAuthToken, + BaseURL: endpoint, + RequestTimeout: time.Duration(clientTimeoutInt) * time.Second, + }, nil +} diff --git a/internal/acceptance/device_helpers.go b/internal/acceptance/device_helpers.go new file mode 100644 index 000000000..43805f207 --- /dev/null +++ b/internal/acceptance/device_helpers.go @@ -0,0 +1,119 @@ +package acceptance + +import ( + "fmt" + "strings" + "time" +) + +// list of plans and metros and os used as filter criteria to find available hardware to run tests +var ( + Preferable_plans = []string{"x1.small.x86", "t1.small.x86", "c2.medium.x86", "c3.small.x86", "c3.medium.x86", "m3.small.x86"} + Preferable_metros = []string{"ch", "ny", "sv", "ty", "am"} + Preferable_os = []string{"ubuntu_20_04"} +) + +func TestDeviceTerminationTime() string { + return time.Now().UTC().Add(60 * time.Minute).Format(time.RFC3339) +} + +// This function should be used to find available plans in all test where a metal_device resource is needed. +// +// TODO consider adding a datasource for equinix_metal_operating_system and making the local.os conditional +// +// https://github.com/equinix/terraform-provider-equinix/pull/220#discussion_r915418418equinix_metal_operating_system +// https://github.com/equinix/terraform-provider-equinix/discussions/221 +func ConfAccMetalDevice_base(plans, metros, os []string) string { + return fmt.Sprintf(` +data "equinix_metal_plans" "test" { + sort { + attribute = "id" + direction = "asc" + } + + filter { + attribute = "name" + values = [%s] + } + filter { + attribute = "available_in_metros" + values = [%s] + } + filter { + attribute = "deployment_types" + values = ["on_demand", "spot_market"] + } +} + +// Select a metal plan randomly and lock it in +// so that we don't pick a different one for +// every subsequent terraform plan +resource "random_integer" "plan_idx" { + min = 0 + max = length(data.equinix_metal_plans.test.plans) - 1 +} + +resource "terraform_data" "plan" { + input = data.equinix_metal_plans.test.plans[random_integer.plan_idx.result] + + lifecycle { + ignore_changes = ["input"] + } +} + +resource "terraform_data" "facilities" { + input = sort(tolist(setsubtract(terraform_data.plan.output.available_in, ["nrt1", "dfw2", "ewr1", "ams1", "sjc1", "ld7", "sy4", "ny6"]))) + + lifecycle { + ignore_changes = ["input"] + } +} + +// Select a metal facility randomly and lock it in +// so that we don't pick a different one for +// every subsequent terraform plan +resource "random_integer" "facility_idx" { + min = 0 + max = length(local.facilities) - 1 +} + +resource "terraform_data" "facility" { + input = local.facilities[random_integer.facility_idx.result] + + lifecycle { + ignore_changes = ["input"] + } +} + +// Select a metal metro randomly and lock it in +// so that we don't pick a different one for +// every subsequent terraform plan +resource "random_integer" "metro_idx" { + min = 0 + max = length(local.metros) - 1 +} + +resource "terraform_data" "metro" { + input = local.metros[random_integer.metro_idx.result] + + lifecycle { + ignore_changes = ["input"] + } +} + +locals { + // Select a random plan + plan = terraform_data.plan.output.slug + + // Select a random facility from the facilities in which the selected plan is available, excluding decommed facilities + facilities = terraform_data.facilities.output + facility = terraform_data.facility.output + + // Select a random metro from the metros in which the selected plan is available + metros = sort(tolist(terraform_data.plan.output.available_in_metros)) + metro = terraform_data.metro.output + + os = [%s][0] +} +`, fmt.Sprintf("\"%s\"", strings.Join(plans[:], `","`)), fmt.Sprintf("\"%s\"", strings.Join(metros[:], `","`)), fmt.Sprintf("\"%s\"", strings.Join(os[:], `","`))) +} diff --git a/internal/acceptance/ssh_key_helpers.go b/internal/acceptance/ssh_key_helpers.go new file mode 100644 index 000000000..75606c896 --- /dev/null +++ b/internal/acceptance/ssh_key_helpers.go @@ -0,0 +1,36 @@ +package acceptance + +import ( + "fmt" + + "github.com/equinix/terraform-provider-equinix/internal/config" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/packethost/packngo" +) + +func TestAccCheckMetalSSHKeyExists(n string, key *packngo.SSHKey) 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 + + foundKey, _, err := client.SSHKeys.Get(rs.Primary.ID, nil) + if err != nil { + return err + } + if foundKey.ID != rs.Primary.ID { + return fmt.Errorf("SSh Key not found: %v - %v", rs.Primary.ID, foundKey) + } + + *key = *foundKey + + return nil + } +} diff --git a/internal/config/config.go b/internal/config/config.go index d5d116b62..65355310b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -28,6 +28,15 @@ import ( xoauth2 "golang.org/x/oauth2" ) +const ( + EndpointEnvVar = "EQUINIX_API_ENDPOINT" + ClientIDEnvVar = "EQUINIX_API_CLIENTID" + ClientSecretEnvVar = "EQUINIX_API_CLIENTSECRET" + ClientTokenEnvVar = "EQUINIX_API_TOKEN" + ClientTimeoutEnvVar = "EQUINIX_API_TIMEOUT" + MetalAuthTokenEnvVar = "METAL_AUTH_TOKEN" +) + type ProviderMeta struct { ModuleName string `cty:"module_name"` } diff --git a/internal/resources/metal/metal_project_ssh_key/datasource.go b/internal/resources/metal/metal_project_ssh_key/datasource.go new file mode 100644 index 000000000..1a6b37da8 --- /dev/null +++ b/internal/resources/metal/metal_project_ssh_key/datasource.go @@ -0,0 +1,91 @@ +package metal_project_ssh_key + +import ( + "github.com/equinix/terraform-provider-equinix/internal/config" + + equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" + + "fmt" + "path" + "strings" + + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/metal_ssh_key" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/packethost/packngo" +) + +func DataSource() *schema.Resource { + dsSchema := metal_ssh_key.CommonFieldsDataSource() + dsSchema["project_id"] = &schema.Schema{ + Type: schema.TypeString, + Description: "The ID of parent project", + ForceNew: true, + Required: true, + } + dataSource := &schema.Resource{ + Read: dataSourceRead, + Schema: dsSchema, + } + return dataSource +} + +func dataSourceRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*config.Config).Metal + + search := d.Get("search").(string) + id := d.Get("id").(string) + projectID := d.Get("project_id").(string) + + if id == "" && search == "" { + return fmt.Errorf("You must supply either search or id") + } + + var ( + key packngo.SSHKey + searchOpts *packngo.SearchOptions + ) + + if search != "" { + searchOpts = &packngo.SearchOptions{Search: search} + } + keys, _, err := client.Projects.ListSSHKeys(projectID, searchOpts) + if err != nil { + err = fmt.Errorf("Error listing project ssh keys: %s", equinix_errors.FriendlyError(err)) + return err + } + + for i := range keys { + // use the first match for searches + if search != "" { + key = keys[i] + break + } + + // otherwise find the matching ID + if keys[i].ID == id { + key = keys[i] + break + } + } + + if key.ID == "" { + // Not Found + return fmt.Errorf("Project %q SSH Key matching %q was not found", projectID, search) + } + + ownerID := path.Base(key.Owner.Href) + + d.SetId(key.ID) + d.Set("name", key.Label) + d.Set("public_key", key.Key) + d.Set("fingerprint", key.FingerPrint) + d.Set("owner_id", ownerID) + d.Set("created", key.Created) + d.Set("updated", key.Updated) + + if strings.Contains(key.Owner.Href, "/projects/") { + d.Set("project_id", ownerID) + } + + return nil +} diff --git a/equinix/data_source_metal_project_ssh_key_acc_test.go b/internal/resources/metal/metal_project_ssh_key/datasource_test.go similarity index 88% rename from equinix/data_source_metal_project_ssh_key_acc_test.go rename to internal/resources/metal/metal_project_ssh_key/datasource_test.go index 45180e237..d950f0708 100644 --- a/equinix/data_source_metal_project_ssh_key_acc_test.go +++ b/internal/resources/metal/metal_project_ssh_key/datasource_test.go @@ -1,4 +1,4 @@ -package equinix +package metal_project_ssh_key_test import ( "fmt" @@ -7,6 +7,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/equinix/terraform-provider-equinix/internal/acceptance" ) func TestAccDataSourceMetalProjectSSHKey_bySearch(t *testing.T) { @@ -19,10 +21,10 @@ func TestAccDataSourceMetalProjectSSHKey_bySearch(t *testing.T) { } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + Providers: acceptance.TestAccProviders, PreventPostDestroyRefresh: true, - CheckDestroy: testAccMetalSSHKeyCheckDestroyed, + CheckDestroy: testAccMetalProjectSSHKeyCheckDestroyed, Steps: []resource.TestStep{ { Config: testAccDataSourceMetalProjectSSHKeyConfig_bySearch(keyName, publicKeyMaterial), @@ -58,10 +60,10 @@ func TestAccDataSourceMetalProjectSSHKeyDataSource_yID(t *testing.T) { } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + Providers: acceptance.TestAccProviders, PreventPostDestroyRefresh: true, - CheckDestroy: testAccMetalSSHKeyCheckDestroyed, + CheckDestroy: testAccMetalProjectSSHKeyCheckDestroyed, Steps: []resource.TestStep{ { Config: testAccDataSourceMetalProjectSSHKeyDataSourceConfig_byID(keyName, publicKeyMaterial), diff --git a/internal/resources/metal/metal_project_ssh_key/resource.go b/internal/resources/metal/metal_project_ssh_key/resource.go new file mode 100644 index 000000000..59a386f46 --- /dev/null +++ b/internal/resources/metal/metal_project_ssh_key/resource.go @@ -0,0 +1,19 @@ +package metal_project_ssh_key + +import ( + "github.com/equinix/terraform-provider-equinix/internal/resources/metal/metal_ssh_key" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func Resource() *schema.Resource { + pkeySchema := metal_ssh_key.CommonFieldsResource() + pkeySchema["project_id"] = &schema.Schema{ + Type: schema.TypeString, + Description: "The ID of parent project", + ForceNew: true, + Required: true, + } + resource := metal_ssh_key.Resource() + resource.Schema = pkeySchema + return resource +} diff --git a/equinix/resource_metal_project_ssh_key_acc_test.go b/internal/resources/metal/metal_project_ssh_key/resource_test.go similarity index 78% rename from equinix/resource_metal_project_ssh_key_acc_test.go rename to internal/resources/metal/metal_project_ssh_key/resource_test.go index 40e2ac9e9..6054d2eb2 100644 --- a/equinix/resource_metal_project_ssh_key_acc_test.go +++ b/internal/resources/metal/metal_project_ssh_key/resource_test.go @@ -1,4 +1,4 @@ -package equinix +package metal_project_ssh_key_test import ( "fmt" @@ -6,6 +6,7 @@ import ( "github.com/equinix/terraform-provider-equinix/internal/config" + "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -37,7 +38,13 @@ resource "equinix_metal_device" "test" { termination_time = "%s" } -`, confAccMetalDevice_base(preferable_plans, preferable_metros, preferable_os), name, publicSshKey, testDeviceTerminationTime()) +`, acceptance.ConfAccMetalDevice_base( + acceptance.Preferable_plans, + acceptance.Preferable_metros, + acceptance.Preferable_os), + name, + publicSshKey, + acceptance.TestDeviceTerminationTime()) } func TestAccMetalProjectSSHKey_basic(t *testing.T) { @@ -50,15 +57,15 @@ func TestAccMetalProjectSSHKey_basic(t *testing.T) { cfg := testAccMetalProjectSSHKeyConfig_basic(rs, publicKeyMaterial) resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - Providers: testAccProviders, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + ExternalProviders: acceptance.TestExternalProviders, + Providers: acceptance.TestAccProviders, CheckDestroy: testAccMetalProjectSSHKeyCheckDestroyed, Steps: []resource.TestStep{ { Config: cfg, Check: resource.ComposeTestCheckFunc( - testAccCheckMetalSSHKeyExists("equinix_metal_project_ssh_key.test", &key), + acceptance.TestAccCheckMetalSSHKeyExists("equinix_metal_project_ssh_key.test", &key), resource.TestCheckResourceAttr( "equinix_metal_project_ssh_key.test", "public_key", publicKeyMaterial), resource.TestCheckResourceAttrPair( @@ -76,7 +83,7 @@ func TestAccMetalProjectSSHKey_basic(t *testing.T) { } func testAccMetalProjectSSHKeyCheckDestroyed(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_project_ssh_key" { diff --git a/equinix/resource_metal_ssh_key.go b/internal/resources/metal/metal_ssh_key/resource.go similarity index 60% rename from equinix/resource_metal_ssh_key.go rename to internal/resources/metal/metal_ssh_key/resource.go index 2faaba737..1fdfc0e51 100644 --- a/equinix/resource_metal_ssh_key.go +++ b/internal/resources/metal/metal_ssh_key/resource.go @@ -1,4 +1,4 @@ -package equinix +package metal_ssh_key import ( "log" @@ -13,60 +13,21 @@ import ( "github.com/packethost/packngo" ) -func metalSSHKeyCommonFields() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Description: "The name of the SSH key for identification", - Required: true, - }, - - "public_key": { - Type: schema.TypeString, - Description: "The public key. If this is a file, it", - Required: true, - ForceNew: true, - }, - "fingerprint": { - Type: schema.TypeString, - Description: "The fingerprint of the SSH key", - Computed: true, - }, - - "created": { - Type: schema.TypeString, - Description: "The timestamp for when the SSH key was created", - Computed: true, - }, - - "updated": { - Type: schema.TypeString, - Description: "The timestamp for the last time the SSH key was updated", - Computed: true, - }, - "owner_id": { - Type: schema.TypeString, - Description: "The UUID of the Equinix Metal API User who owns this key", - Computed: true, - }, - } -} - -func resourceMetalSSHKey() *schema.Resource { +func Resource() *schema.Resource { return &schema.Resource{ - Create: resourceMetalSSHKeyCreate, - Read: resourceMetalSSHKeyRead, - Update: resourceMetalSSHKeyUpdate, - Delete: resourceMetalSSHKeyDelete, + Create: create, + Read: read, + Update: update, + Delete: delete, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, - Schema: metalSSHKeyCommonFields(), + Schema: CommonFieldsResource(), } } -func resourceMetalSSHKeyCreate(d *schema.ResourceData, meta interface{}) error { +func create(d *schema.ResourceData, meta interface{}) error { meta.(*config.Config).AddModuleToMetalUserAgent(d) client := meta.(*config.Config).Metal @@ -87,10 +48,10 @@ func resourceMetalSSHKeyCreate(d *schema.ResourceData, meta interface{}) error { d.SetId(key.ID) - return resourceMetalSSHKeyRead(d, meta) + return read(d, meta) } -func resourceMetalSSHKeyRead(d *schema.ResourceData, meta interface{}) error { +func read(d *schema.ResourceData, meta interface{}) error { meta.(*config.Config).AddModuleToMetalUserAgent(d) client := meta.(*config.Config).Metal @@ -126,7 +87,7 @@ func resourceMetalSSHKeyRead(d *schema.ResourceData, meta interface{}) error { return nil } -func resourceMetalSSHKeyUpdate(d *schema.ResourceData, meta interface{}) error { +func update(d *schema.ResourceData, meta interface{}) error { meta.(*config.Config).AddModuleToMetalUserAgent(d) client := meta.(*config.Config).Metal @@ -147,10 +108,10 @@ func resourceMetalSSHKeyUpdate(d *schema.ResourceData, meta interface{}) error { return equinix_errors.FriendlyError(err) } - return resourceMetalSSHKeyRead(d, meta) + return read(d, meta) } -func resourceMetalSSHKeyDelete(d *schema.ResourceData, meta interface{}) error { +func delete(d *schema.ResourceData, meta interface{}) error { meta.(*config.Config).AddModuleToMetalUserAgent(d) client := meta.(*config.Config).Metal diff --git a/equinix/resource_metal_ssh_key_acc_test.go b/internal/resources/metal/metal_ssh_key/resource_test.go similarity index 74% rename from equinix/resource_metal_ssh_key_acc_test.go rename to internal/resources/metal/metal_ssh_key/resource_test.go index 82601cd12..14659fff0 100644 --- a/equinix/resource_metal_ssh_key_acc_test.go +++ b/internal/resources/metal/metal_ssh_key/resource_test.go @@ -1,4 +1,4 @@ -package equinix +package metal_ssh_key_test import ( "fmt" @@ -6,6 +6,7 @@ import ( "net/http" "testing" + "github.com/equinix/terraform-provider-equinix/internal/acceptance" "github.com/equinix/terraform-provider-equinix/internal/config" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -23,7 +24,7 @@ func init() { func testSweepSSHKeys(region string) error { log.Printf("[DEBUG] Sweeping ssh keys") - config, err := sharedConfigForRegion(region) + config, err := acceptance.GetConfigForNonStandardMetalTest() if err != nil { return fmt.Errorf("[INFO][SWEEPER_LOG] Error getting configuration for sweeping ssh keys: %s", err) } @@ -34,7 +35,7 @@ func testSweepSSHKeys(region string) error { } ids := []string{} for _, k := range sshkeys { - if isSweepableTestResource(k.Label) { + if acceptance.IsSweepableTestResource(k.Label) { ids = append(ids, k.ID) } } @@ -57,15 +58,14 @@ func TestAccMetalSSHKey_basic(t *testing.T) { } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - Providers: testAccProviders, - CheckDestroy: testAccMetalSSHKeyCheckDestroyed, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + Providers: acceptance.TestAccProviders, + CheckDestroy: testAccMetalSSHKeyCheckDestroyed, Steps: []resource.TestStep{ { Config: testAccMetalSSHKeyConfig_basic(rInt, publicKeyMaterial), Check: resource.ComposeTestCheckFunc( - testAccCheckMetalSSHKeyExists("equinix_metal_ssh_key.foobar", &key), + acceptance.TestAccCheckMetalSSHKeyExists("equinix_metal_ssh_key.foobar", &key), resource.TestCheckResourceAttr( "equinix_metal_ssh_key.foobar", "name", fmt.Sprintf("tfacc-user-key-%d", rInt)), resource.TestCheckResourceAttr( @@ -86,10 +86,9 @@ func TestAccMetalSSHKey_projectBasic(t *testing.T) { } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - Providers: testAccProviders, - CheckDestroy: testAccMetalSSHKeyCheckDestroyed, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + Providers: acceptance.TestAccProviders, + CheckDestroy: testAccMetalSSHKeyCheckDestroyed, Steps: []resource.TestStep{ { Config: testAccCheckMetalSSHKeyConfig_projectBasic(rInt, publicKeyMaterial), @@ -113,15 +112,14 @@ func TestAccMetalSSHKey_update(t *testing.T) { } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - Providers: testAccProviders, - CheckDestroy: testAccMetalSSHKeyCheckDestroyed, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + Providers: acceptance.TestAccProviders, + CheckDestroy: testAccMetalSSHKeyCheckDestroyed, Steps: []resource.TestStep{ { Config: testAccMetalSSHKeyConfig_basic(rInt, publicKeyMaterial), Check: resource.ComposeTestCheckFunc( - testAccCheckMetalSSHKeyExists("equinix_metal_ssh_key.foobar", &key), + acceptance.TestAccCheckMetalSSHKeyExists("equinix_metal_ssh_key.foobar", &key), resource.TestCheckResourceAttr( "equinix_metal_ssh_key.foobar", "name", fmt.Sprintf("tfacc-user-key-%d", rInt)), resource.TestCheckResourceAttr( @@ -131,7 +129,7 @@ func TestAccMetalSSHKey_update(t *testing.T) { { Config: testAccMetalSSHKeyConfig_basic(rInt+1, publicKeyMaterial), Check: resource.ComposeTestCheckFunc( - testAccCheckMetalSSHKeyExists("equinix_metal_ssh_key.foobar", &key), + acceptance.TestAccCheckMetalSSHKeyExists("equinix_metal_ssh_key.foobar", &key), resource.TestCheckResourceAttr( "equinix_metal_ssh_key.foobar", "name", fmt.Sprintf("tfacc-user-key-%d", rInt+1)), resource.TestCheckResourceAttr( @@ -148,10 +146,9 @@ func TestAccMetalSSHKey_projectImportBasic(t *testing.T) { t.Fatalf("Cannot generate test SSH key pair: %s", err) } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - Providers: testAccProviders, - CheckDestroy: testAccMetalSSHKeyCheckDestroyed, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + Providers: acceptance.TestAccProviders, + CheckDestroy: testAccMetalSSHKeyCheckDestroyed, Steps: []resource.TestStep{ { Config: testAccCheckMetalSSHKeyConfig_projectBasic(acctest.RandInt(), sshKey), @@ -171,10 +168,9 @@ func TestAccMetalSSHKey_importBasic(t *testing.T) { t.Fatalf("Cannot generate test SSH key pair: %s", err) } resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ExternalProviders: testExternalProviders, - Providers: testAccProviders, - CheckDestroy: testAccMetalSSHKeyCheckDestroyed, + PreCheck: func() { acceptance.TestAccPreCheckMetal(t) }, + Providers: acceptance.TestAccProviders, + CheckDestroy: testAccMetalSSHKeyCheckDestroyed, Steps: []resource.TestStep{ { Config: testAccMetalSSHKeyConfig_basic(acctest.RandInt(), sshKey), @@ -190,7 +186,7 @@ func TestAccMetalSSHKey_importBasic(t *testing.T) { } func testAccMetalSSHKeyCheckDestroyed(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_ssh_key" { @@ -204,32 +200,6 @@ func testAccMetalSSHKeyCheckDestroyed(s *terraform.State) error { return nil } -func testAccCheckMetalSSHKeyExists(n string, key *packngo.SSHKey) 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 - - foundKey, _, err := client.SSHKeys.Get(rs.Primary.ID, nil) - if err != nil { - return err - } - if foundKey.ID != rs.Primary.ID { - return fmt.Errorf("SSh Key not found: %v - %v", rs.Primary.ID, foundKey) - } - - *key = *foundKey - - return nil - } -} - func testAccMetalSSHKeyConfig_basic(rInt int, publicSshKey string) string { return fmt.Sprintf(` resource "equinix_metal_ssh_key" "foobar" { diff --git a/internal/resources/metal/metal_ssh_key/schema_common.go b/internal/resources/metal/metal_ssh_key/schema_common.go new file mode 100644 index 000000000..d5cdef374 --- /dev/null +++ b/internal/resources/metal/metal_ssh_key/schema_common.go @@ -0,0 +1,77 @@ +package metal_ssh_key + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func commonFields() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "fingerprint": { + Type: schema.TypeString, + Description: "The fingerprint of the SSH key", + Computed: true, + }, + + "created": { + Type: schema.TypeString, + Description: "The timestamp for when the SSH key was created", + Computed: true, + }, + + "updated": { + Type: schema.TypeString, + Description: "The timestamp for the last time the SSH key was updated", + Computed: true, + }, + "owner_id": { + Type: schema.TypeString, + Description: "The UUID of the Equinix Metal API User who owns this key", + Computed: true, + }, + } +} + +func CommonFieldsResource() map[string]*schema.Schema { + resourceSchema := commonFields() + resourceSchema["name"] = &schema.Schema{ + Type: schema.TypeString, + Description: "The name of the SSH key for identification", + Required: true, + } + resourceSchema["public_key"] = &schema.Schema{ + Type: schema.TypeString, + Description: "The public key that will be authorized for SSH access on Equinix Metal devices provisioned with this key", + Required: true, + ForceNew: true, + } + return resourceSchema +} + +func CommonFieldsDataSource() map[string]*schema.Schema { + dsSchema := commonFields() + dsSchema["search"] = &schema.Schema{ + Type: schema.TypeString, + Description: "The name, fingerprint, id, or public_key of the SSH Key to search for in the Equinix Metal project", + Optional: true, + ValidateFunc: validation.NoZeroValues, + } + dsSchema["id"] = &schema.Schema{ + Type: schema.TypeString, + Description: "The id of the SSH Key", + Optional: true, + ValidateFunc: validation.NoZeroValues, + Computed: true, + } + dsSchema["name"] = &schema.Schema{ + Type: schema.TypeString, + Description: "The label of the Equinix Metal SSH Key", + Computed: true, + } + dsSchema["public_key"] = &schema.Schema{ + Type: schema.TypeString, + Description: "The public SSH key that is authorized for SSH access on Equinix Metal devices provisioned with this key", + Computed: true, + } + return dsSchema +}