diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d6df009..bfc7c4e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -118,6 +118,40 @@ jobs: - name: Run acceptance tests run: make testacc + sweeper: + name: sweeper cleanup + runs-on: ubuntu-latest + needs: acceptance-tests + timeout-minutes: 15 + env: + CLOUDSIGMA_USERNAME: ${{ secrets.CLOUDSIGMA_USERNAME }} + CLOUDSIGMA_PASSWORD: ${{ secrets.CLOUDSIGMA_PASSWORD }} + CLOUDSIGMA_LOCATION: ${{ secrets.CLOUDSIGMA_LOCATION }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Fix permissions for cache directories + run: | + chmod -R 0755 ~/.cache/go-build ~/go/pkg/mod || true + + - name: Set up cache + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('go.sum', 'tools/go.sum') }} + restore-keys: ${{ runner.os }}-go- + + - name: Run sweeper cleanup + run: make sweep + docs: name: documentation runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 188ffe4..017f3c6 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,14 @@ testacc: @mkdir -p $(BUILD_DIR) TF_ACC=1 go test -count=1 -v -cover -coverprofile=$(BUILD_DIR)/coverage-with-acceptance.out -timeout 120m ./... +## sweep: Run sweepers to cleanup leftover infrastructure after acceptance tests. +.PHONY: sweep +sweep: + @echo "==> Running sweepers to cleanup leftover infrastructure..." + @echo " WARNING: This will destroy infrastructure. Use only in development accounts." + @echo "" + @go test -count=1 -v ./internal/provider -sweep=testacc + ## build: Build binary for default local system's operating system and architecture. .PHONY: build build: diff --git a/cloudsigma/provider.go b/cloudsigma/provider.go index 7604c6c..6bdc788 100644 --- a/cloudsigma/provider.go +++ b/cloudsigma/provider.go @@ -69,7 +69,6 @@ func Provider() *schema.Provider { "cloudsigma_server": resourceCloudSigmaServer(), "cloudsigma_snapshot": resourceCloudSigmaSnapshot(), "cloudsigma_ssh_key": resourceCloudSigmaSSHKey(), - "cloudsigma_tag": resourceCloudSigmaTag(), }, } diff --git a/cloudsigma/provider_test.go b/cloudsigma/provider_test.go index b41d8cd..4056c20 100644 --- a/cloudsigma/provider_test.go +++ b/cloudsigma/provider_test.go @@ -1,26 +1,37 @@ package cloudsigma import ( + "context" + "fmt" "os" "testing" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-mux/tf5to6server" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" -var testAccProvider *schema.Provider -var testAccProviders map[string]*schema.Provider -var testAccProviderFactories map[string]func() (*schema.Provider, error) + "github.com/cloudsigma/cloudsigma-sdk-go/cloudsigma" + "github.com/cloudsigma/terraform-provider-cloudsigma/internal/provider" +) -func init() { - testAccProvider = Provider() - testAccProviders = map[string]*schema.Provider{ - "cloudsigma": testAccProvider, - } - testAccProviderFactories = map[string]func() (*schema.Provider, error){ - "cloudsigma": func() (*schema.Provider, error) { - return testAccProvider, nil - }, - } +var testAccProto6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ + "cloudsigma": func() (tfprotov6.ProviderServer, error) { + ctx := context.Background() + upgradedSDKProvider, err := tf5to6server.UpgradeServer(ctx, Provider().GRPCProvider) + if err != nil { + return nil, err + } + providers := []func() tfprotov6.ProviderServer{ + func() tfprotov6.ProviderServer { return upgradedSDKProvider }, + providerserver.NewProtocol6(provider.New("testacc")()), + } + muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) + if err != nil { + return nil, err + } + return muxServer.ProviderServer(), nil + }, } func TestProvider(t *testing.T) { @@ -42,3 +53,25 @@ func testAccPreCheck(t *testing.T) { t.Fatal("CLOUDSIGMA_PASSWORD must be set for acceptance tests") } } + +func sharedClient() (*cloudsigma.Client, error) { + location := os.Getenv("CLOUDSIGMA_LOCATION") + if location == "" { + return nil, fmt.Errorf("empty CLOUDSIGMA_LOCATION") + } + + username := os.Getenv("CLOUDSIGMA_USERNAME") + if username == "" { + return nil, fmt.Errorf("CLOUDSIGMA_USERNAME must be set for acceptance tests") + } + + password := os.Getenv("CLOUDSIGMA_PASSWORD") + if password == "" { + return nil, fmt.Errorf("CLOUDSIGMA_PASSWORD must be set for acceptance tests") + } + + opts := []cloudsigma.ClientOption{cloudsigma.WithUserAgent("terraform-provider-cloudsigma/sweeper")} + opts = append(opts, cloudsigma.WithLocation(location)) + creds := cloudsigma.NewUsernamePasswordCredentialsProvider(username, password) + return cloudsigma.NewClient(creds, opts...), nil +} diff --git a/cloudsigma/resource_cloudsigma_drive_test.go b/cloudsigma/resource_cloudsigma_drive_test.go index 6e6c6b9..daa9a70 100644 --- a/cloudsigma/resource_cloudsigma_drive_test.go +++ b/cloudsigma/resource_cloudsigma_drive_test.go @@ -6,10 +6,11 @@ import ( "regexp" "testing" + "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/cloudsigma/cloudsigma-sdk-go/cloudsigma" - "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" ) func TestAccCloudSigmaDrive_basic(t *testing.T) { @@ -18,9 +19,9 @@ func TestAccCloudSigmaDrive_basic(t *testing.T) { tagName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(10)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaDriveDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaDriveDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaDriveConfig_basic(driveName), @@ -53,9 +54,9 @@ func TestAccCloudSigmaDrive_basic(t *testing.T) { func TestAccCloudSigmaDrive_emptyTag(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaDriveDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaDriveDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaDriveConfig_emptyTag(), @@ -70,13 +71,13 @@ func TestAccCloudSigmaDrive_changeSize(t *testing.T) { driveName := fmt.Sprintf("tf-acc-test--%s", acctest.RandString(10)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaDriveDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaDriveDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaDriveConfig_basic(driveName), - Check: resource.ComposeAggregateTestCheckFunc( + Check: resource.ComposeTestCheckFunc( testAccCheckCloudSigmaDriveExists("cloudsigma_drive.test", &drive), resource.TestCheckResourceAttr("cloudsigma_drive.test", "media", "disk"), resource.TestCheckResourceAttr("cloudsigma_drive.test", "name", driveName), @@ -87,7 +88,7 @@ func TestAccCloudSigmaDrive_changeSize(t *testing.T) { }, { Config: testAccCloudSigmaDriveConfig_changeSize(driveName), - Check: resource.ComposeAggregateTestCheckFunc( + Check: resource.ComposeTestCheckFunc( testAccCheckCloudSigmaDriveExists("cloudsigma_drive.test", &drive), resource.TestCheckResourceAttr("cloudsigma_drive.test", "size", "16106127360"), ), @@ -101,13 +102,13 @@ func TestAccCloudSigmaDrive_changeStorageType(t *testing.T) { driveName := fmt.Sprintf("tf-acc-test--%s", acctest.RandString(10)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaDriveDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaDriveDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaDriveConfig_storageType(driveName), - Check: resource.ComposeAggregateTestCheckFunc( + Check: resource.ComposeTestCheckFunc( testAccCheckCloudSigmaDriveExists("cloudsigma_drive.test", &drive), resource.TestCheckResourceAttr("cloudsigma_drive.test", "storage_type", "dssd"), ), @@ -121,7 +122,10 @@ func TestAccCloudSigmaDrive_changeStorageType(t *testing.T) { } func testAccCheckCloudSigmaDriveDestroy(s *terraform.State) error { - client := testAccProvider.Meta().(*cloudsigma.Client) + client, err := sharedClient() + if err != nil { + return err + } for _, rs := range s.RootModule().Resources { if rs.Type != "cloudsigma_drive" { @@ -148,7 +152,10 @@ func testAccCheckCloudSigmaDriveExists(n string, drive *cloudsigma.Drive) resour return fmt.Errorf("no drive ID is set") } - client := testAccProvider.Meta().(*cloudsigma.Client) + client, err := sharedClient() + if err != nil { + return err + } retrievedDrive, _, err := client.Drives.Get(context.Background(), rs.Primary.ID) if err != nil { return err diff --git a/cloudsigma/resource_cloudsigma_server_test.go b/cloudsigma/resource_cloudsigma_server_test.go index abc6a69..a9838ab 100644 --- a/cloudsigma/resource_cloudsigma_server_test.go +++ b/cloudsigma/resource_cloudsigma_server_test.go @@ -7,9 +7,9 @@ import ( "regexp" "testing" - "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" + "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/cloudsigma/cloudsigma-sdk-go/cloudsigma" ) @@ -20,9 +20,9 @@ func TestAccCloudSigmaServer_basic(t *testing.T) { tagName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(10)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaServerDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaServerDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaServerConfig_basic(serverName), @@ -54,9 +54,9 @@ func TestAccCloudSigmaServer_basic(t *testing.T) { func TestAccCloudSigmaServer_emptySSH(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaServerDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaServerDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaServerConfig_emptySSHKey(), @@ -68,9 +68,9 @@ func TestAccCloudSigmaServer_emptySSH(t *testing.T) { func TestAccCloudSigmaServer_emptyTag(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaServerDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaServerDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaServerConfig_emptyTag(), @@ -85,9 +85,9 @@ func TestAccCloudSigmaServer_smp(t *testing.T) { serverName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(10)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaServerDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaServerDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaServerConfig_basic(serverName), @@ -112,9 +112,9 @@ func TestAccCloudSigmaServer_smp(t *testing.T) { func TestAccCloudSigmaServer_invalidSMP(t *testing.T) { resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaServerDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaServerDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaServerConfig_invalidSMP(), @@ -131,9 +131,9 @@ func TestAccCloudSigmaServer_withDrive(t *testing.T) { driveName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(10)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaServerDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaServerDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaServerConfig_withDrive(serverName, driveName), @@ -167,9 +167,9 @@ func TestAccCloudSigmaServer_withMeta(t *testing.T) { serverName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(10)) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviderFactories, - CheckDestroy: testAccCheckCloudSigmaServerDestroy, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, + CheckDestroy: testAccCheckCloudSigmaServerDestroy, Steps: []resource.TestStep{ { Config: testAccCloudSigmaServerConfig_withMeta(serverName), @@ -192,7 +192,10 @@ func TestAccCloudSigmaServer_withMeta(t *testing.T) { } func testAccCheckCloudSigmaServerDestroy(s *terraform.State) error { - client := testAccProvider.Meta().(*cloudsigma.Client) + client, err := sharedClient() + if err != nil { + return err + } for _, rs := range s.RootModule().Resources { if rs.Type != "cloudsigma_server" { @@ -219,7 +222,10 @@ func testAccCheckCloudSigmaServerExists(n string, server *cloudsigma.Server) res return fmt.Errorf("no server ID is set") } - client := testAccProvider.Meta().(*cloudsigma.Client) + client, err := sharedClient() + if err != nil { + return err + } retrievedServer, _, err := client.Servers.Get(context.Background(), rs.Primary.ID) if err != nil { return err diff --git a/cloudsigma/resource_cloudsigma_ssh_key_test.go b/cloudsigma/resource_cloudsigma_ssh_key_test.go index 7ec1acd..71e7fbb 100644 --- a/cloudsigma/resource_cloudsigma_ssh_key_test.go +++ b/cloudsigma/resource_cloudsigma_ssh_key_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) func TestAccResourceCloudSigmaSSHKey_Basic(t *testing.T) { @@ -14,8 +14,8 @@ func TestAccResourceCloudSigmaSSHKey_Basic(t *testing.T) { config := fmt.Sprintf(testAccResourceCloudSigmaSSHKeyConfig, sshKeyName, sshKeyPublic) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProto6ProviderFactories, Steps: []resource.TestStep{ { Config: config, diff --git a/cloudsigma/resource_cloudsigma_tag.go b/cloudsigma/resource_cloudsigma_tag.go deleted file mode 100644 index 7abb160..0000000 --- a/cloudsigma/resource_cloudsigma_tag.go +++ /dev/null @@ -1,130 +0,0 @@ -package cloudsigma - -import ( - "context" - "log" - - "github.com/cloudsigma/cloudsigma-sdk-go/cloudsigma" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func resourceCloudSigmaTag() *schema.Resource { - return &schema.Resource{ - Description: ` -The tag resource allows you to manage CloudSigma tags. - -A tag is a label that can be applied to a CloudSigma resource in order to better organize or -facilitate the lookups and actions on it. Tags created with this resource can be referenced -in your configurations via their IDs. -`, - - CreateContext: resourceCloudSigmaTagCreate, - ReadContext: resourceCloudSigmaTagRead, - UpdateContext: resourceCloudSigmaTagUpdate, - DeleteContext: resourceCloudSigmaTagDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - - SchemaVersion: 0, - - Schema: map[string]*schema.Schema{ - "name": { - Description: "The tag name.", - Required: true, - Type: schema.TypeString, - }, - - "resource_uri": { - Description: "The unique resource identifier of the tag.", - Computed: true, - Type: schema.TypeString, - }, - }, - } -} - -func resourceCloudSigmaTagCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*cloudsigma.Client) - - // build create configuration - createRequest := &cloudsigma.TagCreateRequest{ - Tags: []cloudsigma.Tag{ - { - Name: d.Get("name").(string), - }, - }, - } - log.Printf("[DEBUG] Tag create configuration: %#v", *createRequest) - tags, _, err := client.Tags.Create(ctx, createRequest) - if err != nil { - return diag.FromErr(err) - } - - d.SetId(tags[0].UUID) - log.Printf("[INFO] Tag ID: %s", d.Id()) - - return resourceCloudSigmaTagRead(ctx, d, meta) -} - -func resourceCloudSigmaTagRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*cloudsigma.Client) - - tag, resp, err := client.Tags.Get(ctx, d.Id()) - if err != nil { - // If the tag is somehow already destroyed, mark as successfully gone - if resp != nil && resp.StatusCode == 404 { - d.SetId("") - return nil - } - return diag.FromErr(err) - } - - _ = d.Set("name", tag.Name) - _ = d.Set("resource_uri", tag.ResourceURI) - - // owner := []map[string]interface{}{ - // { - // "resource_uri": tag.Owner.ResourceURI, - // "uuid": tag.Owner.UUID, - // }, - // } - // _ = d.Set("owner", owner) - - return nil -} - -func resourceCloudSigmaTagUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*cloudsigma.Client) - - if d.HasChange("name") { - _, newName := d.GetChange("name") - updateRequest := &cloudsigma.TagUpdateRequest{ - Tag: &cloudsigma.Tag{ - Name: newName.(string), - }, - } - log.Printf("[DEBUG] Tag update configuration: %#v", *updateRequest) - _, _, err := client.Tags.Update(context.Background(), d.Id(), updateRequest) - if err != nil { - return diag.FromErr(err) - } - - } - - return resourceCloudSigmaTagRead(ctx, d, meta) -} - -func resourceCloudSigmaTagDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*cloudsigma.Client) - - _, err := client.Tags.Delete(ctx, d.Id()) - if err != nil { - return diag.FromErr(err) - } - - d.SetId("") - - return nil -} diff --git a/cloudsigma/resource_cloudsigma_tag_test.go b/cloudsigma/resource_cloudsigma_tag_test.go deleted file mode 100644 index a1e3b97..0000000 --- a/cloudsigma/resource_cloudsigma_tag_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package cloudsigma - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" -) - -func TestAccResourceCloudSigmaTag_Basic(t *testing.T) { - tagName := fmt.Sprintf("tag-%s", acctest.RandString(10)) - config := fmt.Sprintf(testAccResourceCloudSigmaTagConfig, tagName) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - Steps: []resource.TestStep{ - { - Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("cloudsigma_tag.foobar", "name", tagName), - ), - }, - }, - }) -} - -const testAccResourceCloudSigmaTagConfig = ` -resource "cloudsigma_tag" "foobar" { - name = "%s" -} -` diff --git a/docs/resources/tag.md b/docs/resources/tag.md index 02ed9e4..0ef2c15 100644 --- a/docs/resources/tag.md +++ b/docs/resources/tag.md @@ -34,5 +34,5 @@ resource "cloudsigma_tag" "production" { ### Read-Only -- `id` (String) The ID of this resource. +- `id` (String) The ID of the tag. - `resource_uri` (String) The unique resource identifier of the tag. diff --git a/go.mod b/go.mod index 1074535..2cdee4a 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-mux v0.16.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 + github.com/hashicorp/terraform-plugin-testing v1.8.0 + github.com/stretchr/testify v1.9.0 ) require ( @@ -16,6 +18,7 @@ require ( github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.16.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -24,7 +27,7 @@ require ( github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect - github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -45,6 +48,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -59,4 +63,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.34.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 95da202..e89b355 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= @@ -87,6 +87,8 @@ github.com/hashicorp/terraform-plugin-mux v0.16.0 h1:RCzXHGDYwUwwqfYYWJKBFaS3fQs github.com/hashicorp/terraform-plugin-mux v0.16.0/go.mod h1:PF79mAsPc8CpusXPfEVa4X8PtkB+ngWoiUClMrNZlYo= github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= +github.com/hashicorp/terraform-plugin-testing v1.8.0 h1:wdYIgwDk4iO933gC4S8KbKdnMQShu6BXuZQPScmHvpk= +github.com/hashicorp/terraform-plugin-testing v1.8.0/go.mod h1:o2kOgf18ADUaZGhtOl0YCkfIxg01MAiMATT2EtIHlZk= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= diff --git a/internal/provider/cloudsigma_sweeper_test.go b/internal/provider/cloudsigma_sweeper_test.go new file mode 100644 index 0000000..f5dbbc3 --- /dev/null +++ b/internal/provider/cloudsigma_sweeper_test.go @@ -0,0 +1,37 @@ +package provider + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/cloudsigma/cloudsigma-sdk-go/cloudsigma" +) + +func TestMain(m *testing.M) { + resource.TestMain(m) +} + +func sharedClient(_ string) (*cloudsigma.Client, error) { + location := os.Getenv("CLOUDSIGMA_LOCATION") + if location == "" { + return nil, fmt.Errorf("empty CLOUDSIGMA_LOCATION") + } + + username := os.Getenv("CLOUDSIGMA_USERNAME") + if username == "" { + return nil, fmt.Errorf("CLOUDSIGMA_USERNAME must be set for acceptance tests") + } + + password := os.Getenv("CLOUDSIGMA_PASSWORD") + if password == "" { + return nil, fmt.Errorf("CLOUDSIGMA_PASSWORD must be set for acceptance tests") + } + + opts := []cloudsigma.ClientOption{cloudsigma.WithUserAgent("terraform-provider-cloudsigma/sweeper")} + opts = append(opts, cloudsigma.WithLocation(location)) + creds := cloudsigma.NewUsernamePasswordCredentialsProvider(username, password) + return cloudsigma.NewClient(creds, opts...), nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 0e84a37..3760278 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -190,12 +190,14 @@ func (p *cloudSigmaProvider) DataSources(_ context.Context) []func() datasource. } func (p *cloudSigmaProvider) Resources(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{} + return []func() resource.Resource{ + NewTagResource, + } } func (p *cloudSigmaProvider) userAgent() string { name := "terraform-provider-cloudsigma" comment := "https://registry.terraform.io/providers/cloudsigma/cloudsigma" - return fmt.Sprintf("%s/%s (%s)", name, p.version, comment) + return fmt.Sprintf("%s/%s (+%s)", name, p.version, comment) } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go new file mode 100644 index 0000000..cbe068e --- /dev/null +++ b/internal/provider/provider_test.go @@ -0,0 +1,149 @@ +package provider + +import ( + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/stretchr/testify/assert" +) + +const accTestPrefix = "tf-acc-test" + +var testAccProvider = New("testacc")() +var testAccProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ + "cloudsigma": providerserver.NewProtocol6WithError(testAccProvider), +} + +func TestProviderConfigure_invalidCredentials(t *testing.T) { + location := os.Getenv("CLOUDSIGMA_LOCATION") + _ = os.Unsetenv("CLOUDSIGMA_LOCATION") + defer func() { _ = os.Setenv("CLOUDSIGMA_LOCATION", location) }() + + password := os.Getenv("CLOUDSIGMA_PASSWORD") + _ = os.Unsetenv("CLOUDSIGMA_PASSWORD") + defer func() { _ = os.Setenv("CLOUDSIGMA_PASSWORD", password) }() + + token := os.Getenv("CLOUDSIGMA_TOKEN") + _ = os.Unsetenv("CLOUDSIGMA_TOKEN") + defer func() { _ = os.Setenv("CLOUDSIGMA_TOKEN", token) }() + + username := os.Getenv("CLOUDSIGMA_USERNAME") + _ = os.Unsetenv("CLOUDSIGMA_USERNAME") + defer func() { _ = os.Setenv("CLOUDSIGMA_USERNAME", username) }() + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProviderFactories, + + Steps: []resource.TestStep{ + { + Config: providerConfigWithoutCredentials, + ExpectError: regexp.MustCompile("Missing CloudSigma credentials"), + }, + { + Config: providerConfigWithEmptyUsername, + ExpectError: regexp.MustCompile(`"username" must be set`), + }, + { + Config: providerConfigWithEmptyPassword, + ExpectError: regexp.MustCompile(`"password" must be set`), + }, + { + Config: providerConfigWithEmptyUsernameAndPassword, + ExpectError: regexp.MustCompile("Missing CloudSigma credentials"), + }, + { + Config: providerConfigWithUsernamePasswordAndToken, + ExpectError: regexp.MustCompile("Ambiguous CloudSigma credentials"), + }, + }, + }) +} + +func TestProviderUserAgent(t *testing.T) { + t.Parallel() + + type testCase struct { + version string + expectedUserAgent string + } + tests := map[string]testCase{ + "empty_version": { + version: "", + expectedUserAgent: "terraform-provider-cloudsigma/ (+https://registry.terraform.io/providers/cloudsigma/cloudsigma)", + }, + "dev_version": { + version: "dev", + expectedUserAgent: "terraform-provider-cloudsigma/dev (+https://registry.terraform.io/providers/cloudsigma/cloudsigma)", + }, + "release_version": { + version: "1.1.1", + expectedUserAgent: "terraform-provider-cloudsigma/1.1.1 (+https://registry.terraform.io/providers/cloudsigma/cloudsigma)", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + p := &cloudSigmaProvider{version: test.version} + actualUserAgent := p.userAgent() + + assert.Equal(t, test.expectedUserAgent, actualUserAgent) + }) + } +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("CLOUDSIGMA_LOCATION"); v == "" { + t.Fatal("CLOUDSIGMA_LOCATION must be set for acceptance tests") + } + + if v := os.Getenv("CLOUDSIGMA_USERNAME"); v == "" { + t.Fatal("CLOUDSIGMA_USERNAME must be set for acceptance tests") + } + + if v := os.Getenv("CLOUDSIGMA_PASSWORD"); v == "" { + t.Fatal("CLOUDSIGMA_PASSWORD must be set for acceptance tests") + } +} + +const providerConfigWithoutCredentials = ` +provider "cloudsigma" { +} +data "cloudsigma_profile" "me" {} +` + +const providerConfigWithEmptyUsername = ` +provider "cloudsigma" { + username = "" + password = "secret-password" +} +data "cloudsigma_profile" "me" {} +` + +const providerConfigWithEmptyPassword = ` +provider "cloudsigma" { + username = "username@mail" + password = "" +} +data "cloudsigma_profile" "me" {} +` + +const providerConfigWithEmptyUsernameAndPassword = ` +provider "cloudsigma" { + username = "" + password = "" +} +data "cloudsigma_profile" "me" {} +` + +const providerConfigWithUsernamePasswordAndToken = ` +provider "cloudsigma" { + username = "username@mail" + password = "secret-password" + token = "secret-token" +} +data "cloudsigma_profile" "me" {} +` diff --git a/internal/provider/resource_cloudsigma_tag.go b/internal/provider/resource_cloudsigma_tag.go new file mode 100644 index 0000000..1ff9fef --- /dev/null +++ b/internal/provider/resource_cloudsigma_tag.go @@ -0,0 +1,208 @@ +package provider + +import ( + "context" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/cloudsigma/cloudsigma-sdk-go/cloudsigma" +) + +var ( + _ resource.Resource = (*tagResource)(nil) + _ resource.ResourceWithConfigure = (*tagResource)(nil) + _ resource.ResourceWithImportState = (*tagResource)(nil) +) + +// tagResource is the tag resource implementation. +type tagResource struct { + client *cloudsigma.Client +} + +// tagResourceModel maps the tag resource schema data. +type tagResourceModel struct { + Name types.String `tfsdk:"name"` + ID types.String `tfsdk:"id"` + ResourceURI types.String `tfsdk:"resource_uri"` +} + +func NewTagResource() resource.Resource { + return &tagResource{} +} + +func (r *tagResource) Metadata(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "cloudsigma_tag" +} + +func (r *tagResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + response.Schema = schema.Schema{ + MarkdownDescription: ` +The tag resource allows you to manage CloudSigma tags. + +A tag is a label that can be applied to a CloudSigma resource in order to better organize or +facilitate the lookups and actions on it. Tags created with this resource can be referenced +in your configurations via their IDs. +`, + Version: 0, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "The tag name.", + Required: true, + }, + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of the tag.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "resource_uri": schema.StringAttribute{ + MarkdownDescription: "The unique resource identifier of the tag.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *tagResource) Configure(_ context.Context, request resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if request.ProviderData == nil { + return + } + r.client = request.ProviderData.(*cloudsigma.Client) +} + +func (r *tagResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data tagResourceModel + + // read plan data into the model + diags := request.Plan.Get(ctx, &data) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + createRequest := &cloudsigma.TagCreateRequest{ + Tags: []cloudsigma.Tag{{ + Name: data.Name.ValueString(), + }}, + } + tflog.Trace(ctx, "Creating tag", map[string]interface{}{"payload": createRequest}) + tags, _, err := r.client.Tags.Create(ctx, createRequest) + if err != nil { + response.Diagnostics.AddError("Unable to create tag", err.Error()) + return + } + tag := tags[0] + tflog.Trace(ctx, "Created tag", map[string]interface{}{"data": tag}) + + // map response body to attributes + data.Name = types.StringValue(tag.Name) + data.ID = types.StringValue(tag.UUID) + data.ResourceURI = types.StringValue(tag.ResourceURI) + + diags = response.State.Set(ctx, &data) + response.Diagnostics.Append(diags...) +} + +func (r *tagResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data tagResourceModel + + // read state data into the model + diags := request.State.Get(ctx, &data) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + tagUUID := data.ID.ValueString() + tflog.Trace(ctx, "Getting tag", map[string]interface{}{"tag_uuid": tagUUID}) + tag, resp, err := r.client.Tags.Get(ctx, tagUUID) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + // if the tag is somehow already destroyed, mark as successfully gone + response.State.RemoveResource(ctx) + return + } + response.Diagnostics.AddError("Unable to get tag", err.Error()) + return + } + tflog.Trace(ctx, "Got tag", map[string]interface{}{"data": tag}) + + // map response body to attributes + data.Name = types.StringValue(tag.Name) + data.ID = types.StringValue(tag.UUID) + data.ResourceURI = types.StringValue(tag.ResourceURI) + + diags = response.State.Set(ctx, &data) + response.Diagnostics.Append(diags...) +} + +func (r *tagResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var data tagResourceModel + + // read plan data into the model + diags := request.Plan.Get(ctx, &data) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + tagUUID := data.ID.ValueString() + updateRequest := &cloudsigma.TagUpdateRequest{ + Tag: &cloudsigma.Tag{ + Name: data.Name.ValueString(), + }, + } + tflog.Trace(ctx, "Updating tag", map[string]interface{}{ + "payload": updateRequest, + "tag_uuid": tagUUID}, + ) + tag, _, err := r.client.Tags.Update(ctx, tagUUID, updateRequest) + if err != nil { + response.Diagnostics.AddError("Unable to update tag", err.Error()) + return + } + tflog.Trace(ctx, "Updated tag", map[string]interface{}{"data": tag}) + + // map response body to attributes + data.Name = types.StringValue(tag.Name) + data.ID = types.StringValue(tag.UUID) + data.ResourceURI = types.StringValue(tag.ResourceURI) + + diags = response.State.Set(ctx, &data) + response.Diagnostics.Append(diags...) +} + +func (r *tagResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data tagResourceModel + + // read state data into the model + diags := request.State.Get(ctx, &data) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + tagUUID := data.ID.ValueString() + tflog.Trace(ctx, "Deleting tag", map[string]interface{}{"tag_uuid": tagUUID}) + _, err := r.client.Tags.Delete(ctx, tagUUID) + if err != nil { + response.Diagnostics.AddError("Unable to delete tag", err.Error()) + return + } + tflog.Trace(ctx, "Deleted tag", map[string]interface{}{"tag_uuid": tagUUID}) +} + +func (r *tagResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) +} diff --git a/internal/provider/resource_cloudsigma_tag_test.go b/internal/provider/resource_cloudsigma_tag_test.go new file mode 100644 index 0000000..700a02d --- /dev/null +++ b/internal/provider/resource_cloudsigma_tag_test.go @@ -0,0 +1,219 @@ +package provider + +import ( + "context" + "errors" + "fmt" + "log/slog" + "regexp" + "strings" + "testing" + + "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/cloudsigma/cloudsigma-sdk-go/cloudsigma" +) + +func init() { + resource.AddTestSweepers("cloudsigma_tag", &resource.Sweeper{ + Name: "cloudsigma_tag", + F: testSweepTags, + }) +} + +func testSweepTags(region string) error { + ctx := context.Background() + client, err := sharedClient(region) + if err != nil { + return err + } + + tags, _, err := client.Tags.List(ctx) + if err != nil { + return fmt.Errorf("getting tags list: %s", err) + } + + for _, tag := range tags { + if strings.HasPrefix(tag.Name, accTestPrefix) { + slog.Info("Deleting cloudsigma_tag", "name", tag.Name, "uuid", tag.UUID) + _, err := client.Tags.Delete(ctx, tag.UUID) + if err != nil { + slog.Warn("Error deleting tag during sweep", "name", tag.Name, "error", err) + } + } + } + + return nil +} + +func TestAccResourceCloudSigmaTag_basic(t *testing.T) { + var tag cloudsigma.Tag + tagName := fmt.Sprintf("%s-%s", accTestPrefix, acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckTagDestroy, + + Steps: []resource.TestStep{ + { + Config: testAccCloudSigmaTagResource(tagName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTagExists("cloudsigma_tag.foobar", &tag), + resource.TestCheckResourceAttr("cloudsigma_tag.foobar", "name", tagName), + resource.TestCheckResourceAttrSet("cloudsigma_tag.foobar", "id"), + resource.TestCheckResourceAttrSet("cloudsigma_tag.foobar", "resource_uri"), + ), + }, + }, + }) +} + +func TestAccResourceCloudSigmaTag_update(t *testing.T) { + var tag cloudsigma.Tag + tagName := fmt.Sprintf("%s-%s", accTestPrefix, acctest.RandString(10)) + tagNameUpdated := fmt.Sprintf("%s-%s", accTestPrefix, acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckTagDestroy, + + Steps: []resource.TestStep{ + { + Config: testAccCloudSigmaTagResource(tagName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTagExists("cloudsigma_tag.foobar", &tag), + resource.TestCheckResourceAttr("cloudsigma_tag.foobar", "name", tagName), + resource.TestCheckResourceAttrSet("cloudsigma_tag.foobar", "id"), + resource.TestCheckResourceAttrSet("cloudsigma_tag.foobar", "resource_uri"), + ), + }, + { + Config: testAccCloudSigmaTagResource(tagNameUpdated), + Check: resource.ComposeTestCheckFunc( + testAccCheckTagExists("cloudsigma_tag.foobar", &tag), + resource.TestCheckResourceAttr("cloudsigma_tag.foobar", "name", tagNameUpdated), + resource.TestCheckResourceAttrSet("cloudsigma_tag.foobar", "id"), + resource.TestCheckResourceAttrSet("cloudsigma_tag.foobar", "resource_uri"), + ), + }, + }, + }) +} + +func TestAccResourceCloudSigmaTag_expectError(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviderFactories, + CheckDestroy: testAccCheckTagDestroy, + + Steps: []resource.TestStep{ + { + Config: testAccCloudSigmaTagResourceWithoutName(), + ExpectError: regexp.MustCompile(`The argument "name" is required`), + }, + }, + }) +} + +func TestAccResourceCloudSigmaTag_upgradeFromSDK(t *testing.T) { + tagName := fmt.Sprintf("%s-%s", accTestPrefix, acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckTagDestroy, + + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "cloudsigma": { + VersionConstraint: "2.1.0", + Source: "cloudsigma/cloudsigma", + }, + }, + Config: testAccCloudSigmaTagResource(tagName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cloudsigma_tag.foobar", "name", tagName), + ), + }, + { + ProtoV6ProviderFactories: testAccProviderFactories, + Config: testAccCloudSigmaTagResource(tagName), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + }, + }, + }) +} + +func testAccCheckTagDestroy(s *terraform.State) error { + ctx := context.Background() + client, err := sharedClient("testacc") + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudsigma_tag" { + continue + } + + tag, _, err := client.Tags.Get(ctx, rs.Primary.ID) + if err == nil && tag.UUID == rs.Primary.ID { + return fmt.Errorf("tag (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckTagExists(n string, tag *cloudsigma.Tag) 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 errors.New("no tag ID set") + } + + ctx := context.Background() + client, err := sharedClient("testacc") + if err != nil { + return err + } + + retrievedTag, _, err := client.Tags.Get(ctx, rs.Primary.ID) + if err != nil { + return fmt.Errorf("could not get tag: %s", err) + } + + if retrievedTag.UUID != rs.Primary.ID { + return errors.New("tag not found") + } + + tag = retrievedTag + return nil + } +} + +func testAccCloudSigmaTagResource(name string) string { + return fmt.Sprintf(` +resource "cloudsigma_tag" "foobar" { + name = "%s" +}`, name) +} + +func testAccCloudSigmaTagResourceWithoutName() string { + return ` +resource "cloudsigma_tag" "foobar" { +}` +}