diff --git a/.gitignore b/.gitignore index 6cfac92..479d0f1 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ website/vendor # Keep windows files with windows line endings *.winfile eol=crlf -.env \ No newline at end of file +.env +.envrc +terraform-provider-planetscale \ No newline at end of file diff --git a/docs/data-sources/password.md b/docs/data-sources/password.md index 449dff8..39768ce 100644 --- a/docs/data-sources/password.md +++ b/docs/data-sources/password.md @@ -31,7 +31,7 @@ output "password" { ### Required - `branch` (String) The branch this password belongs to.. -- `database` (String) The datanase this branch password belongs to. +- `database` (String) The database this branch password belongs to. - `id` (String) The ID for the password. - `organization` (String) The organization this database branch password belongs to. diff --git a/docs/data-sources/passwords.md b/docs/data-sources/passwords.md index dd4f0a1..1eeaaf0 100644 --- a/docs/data-sources/passwords.md +++ b/docs/data-sources/passwords.md @@ -50,7 +50,7 @@ Read-Only: - `actor` (Attributes) The actor that created this branch. (see [below for nested schema](#nestedatt--passwords--actor)) - `branch` (String) The branch this password belongs to.. - `created_at` (String) When the password was created. -- `database` (String) The datanase this branch password belongs to. +- `database` (String) The database this branch password belongs to. - `database_branch` (Attributes) The branch this password is allowed to access. (see [below for nested schema](#nestedatt--passwords--database_branch)) - `deleted_at` (String) When the password was deleted. - `expires_at` (String) When the password will expire. diff --git a/docs/resources/password.md b/docs/resources/password.md index 4d4f6b0..3faa079 100644 --- a/docs/resources/password.md +++ b/docs/resources/password.md @@ -32,7 +32,7 @@ output "password" { ### Required - `branch` (String) The branch this password belongs to. -- `database` (String) The datanase this branch password belongs to. +- `database` (String) The database this branch password belongs to. - `organization` (String) The organization this database branch password belongs to. ### Optional diff --git a/go.sum b/go.sum index 0dec57c..7e7bfca 100644 --- a/go.sum +++ b/go.sum @@ -322,6 +322,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/internal/provider/backup_resource.go b/internal/provider/backup_resource.go index 4505f00..d8523ea 100644 --- a/internal/provider/backup_resource.go +++ b/internal/provider/backup_resource.go @@ -21,8 +21,10 @@ import ( ) // Ensure provider defined types fully satisfy framework interfaces. -var _ resource.Resource = &backupResource{} -var _ resource.ResourceWithImportState = &backupResource{} +var ( + _ resource.Resource = &backupResource{} + _ resource.ResourceWithImportState = &backupResource{} +) func newBackupResource() resource.Resource { return &backupResource{} @@ -101,25 +103,29 @@ Known limitations: Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), - }}, + }, + }, "database": schema.StringAttribute{ Description: "The database to which the branch being backed up belongs to.", Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), - }}, + }, + }, "branch": schema.StringAttribute{ Description: "The branch being backed up.", Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), - }}, + }, + }, "name": schema.StringAttribute{ Description: "The name of the backup.", Required: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), - }}, + }, + }, "backup_policy": schema.SingleNestedAttribute{ Description: "The policy used by the backup.", Required: true, @@ -281,6 +287,11 @@ func (r *backupResource) Read(ctx context.Context, req resource.ReadRequest, res res, err := r.client.GetBackup(ctx, org.ValueString(), database.ValueString(), branch.ValueString(), id.ValueString()) if err != nil { + if notFoundErr, ok := err.(*planetscale.GetBackupRes404); ok { + tflog.Warn(ctx, fmt.Sprintf("Backup not found, removing from state: %s", notFoundErr.Message)) + resp.State.RemoveResource(ctx) + return + } resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read backup, got error: %s", err)) return } diff --git a/internal/provider/branch_resource_test.go b/internal/provider/branch_resource_test.go new file mode 100644 index 0000000..6d5afc4 --- /dev/null +++ b/internal/provider/branch_resource_test.go @@ -0,0 +1,64 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccBranchResource(t *testing.T) { + // TODO: This test currently fails because the provider returns immediately + // after DB creation but the DB is still pending and so the branch creation + // will fail. Unblock and finish this test once this issue is resolved. + t.Skip() + + dbName := acctest.RandomWithPrefix("testacc-branch") + branchName := "two" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccBranchResourceConfig(dbName, branchName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("planetscale_branch.two", "name", branchName), + resource.TestCheckResourceAttr("planetscale_branch.two", "parent_branch", "main"), + resource.TestCheckResourceAttr("planetscale_branch.two", "sharded", "false"), + ), + }, + // ImportState testing + { + ResourceName: "planetscale_branch.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update and Read testing + // TODO: Implement an update test. + // Delete testing automatically occurs in TestCase + }, + }) +} + +// TODO: implement an out of bound deletion test like we have in the password and database tests. + +func testAccBranchResourceConfig(dbName, branchName string) string { + return fmt.Sprintf(` +resource "planetscale_database" "test" { + organization = "%s" + name = "%s" + cluster_size = "PS-10" + default_branch = "main" +} + +resource "planetscale_branch" "two" { + organization = "%s" + database = planetscale_database.test.name + name = "%s" + parent_branch = planetscale_database.test.default_branch +} + `, testAccOrg, dbName, testAccOrg, branchName) +} diff --git a/internal/provider/database_resource.go b/internal/provider/database_resource.go index d4cbcd2..4b3ef65 100644 --- a/internal/provider/database_resource.go +++ b/internal/provider/database_resource.go @@ -20,8 +20,10 @@ import ( ) // Ensure provider defined types fully satisfy framework interfaces. -var _ resource.Resource = &databaseResource{} -var _ resource.ResourceWithImportState = &databaseResource{} +var ( + _ resource.Resource = &databaseResource{} + _ resource.ResourceWithImportState = &databaseResource{} +) func newDatabaseResource() resource.Resource { return &databaseResource{} @@ -404,6 +406,11 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r res, err := r.client.GetDatabase(ctx, org.ValueString(), name.ValueString()) if err != nil { + if notFoundErr, ok := err.(*planetscale.GetDatabaseRes404); ok { + tflog.Warn(ctx, fmt.Sprintf("Database not found, removing from state: %s", notFoundErr.Message)) + resp.State.RemoveResource(ctx) + return + } resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read database, got error: %s", err)) return } @@ -489,12 +496,13 @@ func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteReques return } - res, err := r.client.DeleteDatabase(ctx, org.ValueString(), name.ValueString()) + _, err := r.client.DeleteDatabase(ctx, org.ValueString(), name.ValueString()) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete database, got error: %s", err)) return } - _ = res + // Mark dependent resources for recreation + resp.State.RemoveResource(ctx) } func (r *databaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { @@ -510,5 +518,4 @@ func (r *databaseResource) ImportState(ctx context.Context, req resource.ImportS resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization"), idParts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[1])...) - } diff --git a/internal/provider/database_resource_test.go b/internal/provider/database_resource_test.go new file mode 100644 index 0000000..8d53f0e --- /dev/null +++ b/internal/provider/database_resource_test.go @@ -0,0 +1,98 @@ +package provider + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDatabaseResource(t *testing.T) { + dbName := acctest.RandomWithPrefix("testacc-db") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccDatabaseResourceConfig(dbName, "PS-10"), + Check: resource.ComposeAggregateTestCheckFunc( + // resource.TestCheckResourceAttr("planetscale_database.test", "id", "todo"), + resource.TestCheckResourceAttr("planetscale_database.test", "production_branches_count", "1"), + resource.TestCheckResourceAttr("planetscale_database.test", "default_branch", "main"), + resource.TestCheckResourceAttr("planetscale_database.test", "cluster_size", "PS-10"), + ), + }, + // ImportState testing + { + ResourceName: "planetscale_database.test", + ImportStateId: fmt.Sprintf("%s,%s", testAccOrg, dbName), + ImportState: true, + ImportStateVerify: true, + // TODO: API does not return cluster_size which causes a diff on import. When fixed, remove this: + ImportStateVerifyIgnore: []string{"cluster_size"}, + }, + // Update and Read testing + { + Config: testAccDatabaseResourceConfig(dbName, "PS-20"), + Check: resource.ComposeAggregateTestCheckFunc( + // resource.TestCheckResourceAttr("planetscale_database.test", "id", "todo"), + resource.TestCheckResourceAttr("planetscale_database.test", "production_branches_count", "1"), + resource.TestCheckResourceAttr("planetscale_database.test", "default_branch", "main"), + resource.TestCheckResourceAttr("planetscale_database.test", "cluster_size", "PS-20"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +// TestAccDatabaseResource_outOfBandDelete tests the out-of-band deletion of the database. +// In this test we simulate the remote database has been deleted out of band, perhaps by +// a user on the console or using pscale CLI. +// https://github.com/planetscale/terraform-provider-planetscale/issues/53 +func TestAccDatabaseResource_OutOfBandDelete(t *testing.T) { + dbName := acctest.RandomWithPrefix("testacc-db-oob") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccDatabaseResourceConfig(dbName, "PS-10"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("planetscale_database.test", "production_branches_count", "1"), + resource.TestCheckResourceAttr("planetscale_database.test", "default_branch", "main"), + resource.TestCheckResourceAttr("planetscale_database.test", "cluster_size", "PS-10"), + ), + }, + // Test out-of-bands deletion of the database should produce a plan to recreate, not error. + { + ResourceName: "planetscale_database.test", + RefreshState: true, + ExpectNonEmptyPlan: true, + PreConfig: func() { + ctx := context.Background() + _, err := testAccAPIClient.DeleteDatabase(ctx, testAccOrg, dbName) + if err != nil { + t.Fatalf("PreConfig: failed to delete database: %s", err) + } + }, + }, + }, + }) +} + +func testAccDatabaseResourceConfig(dbName string, clusterSize string) string { + return fmt.Sprintf(` +resource "planetscale_database" "test" { + organization = "%s" + name = "%s" + cluster_size = "%s" +} +`, testAccOrg, dbName, clusterSize) +} diff --git a/internal/provider/models_data_source.go b/internal/provider/models_data_source.go index d97c121..e56a8d9 100644 --- a/internal/provider/models_data_source.go +++ b/internal/provider/models_data_source.go @@ -1191,7 +1191,7 @@ func passwordDataSourceSchemaAttribute(computedName bool) map[string]schema.Attr Required: !computedName, Computed: computedName, }, "database": schema.StringAttribute{ - Description: "The datanase this branch password belongs to.", + Description: "The database this branch password belongs to.", Required: !computedName, Computed: computedName, }, "branch": schema.StringAttribute{ diff --git a/internal/provider/organization_regions_data_source_test.go b/internal/provider/organization_regions_data_source_test.go index c73ecf5..3cc5d8e 100644 --- a/internal/provider/organization_regions_data_source_test.go +++ b/internal/provider/organization_regions_data_source_test.go @@ -26,14 +26,13 @@ var regions = []string{ } func TestAccOrganizationRegionsDataSource(t *testing.T) { - orgName := "planetscale-terraform-testing" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Read testing { - Config: testAccOrganizationRegionsDataSourceConfig(orgName), + Config: testAccOrganizationRegionsDataSourceConfig(testAccOrg), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrWith("data.planetscale_organization_regions.test", "regions.#", checkIntegerMin(1)), resource.TestCheckResourceAttrWith("data.planetscale_organization_regions.test", "regions.0.slug", checkOneOf(regions...)), diff --git a/internal/provider/organizations_data_source_test.go b/internal/provider/organizations_data_source_test.go index b37a705..ac4c5bb 100644 --- a/internal/provider/organizations_data_source_test.go +++ b/internal/provider/organizations_data_source_test.go @@ -16,7 +16,7 @@ func TestAccOrganizationsDataSource(t *testing.T) { Config: testAccOrganizationsDataSourceConfig, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("data.planetscale_organizations.test", "organizations.#", "1"), - resource.TestCheckResourceAttr("data.planetscale_organizations.test", "organizations.0.name", "planetscale-terraform-testing"), + resource.TestCheckResourceAttr("data.planetscale_organizations.test", "organizations.0.name", testAccOrg), resource.TestCheckResourceAttrSet("data.planetscale_organizations.test", "organizations.0.admin_only_production_access"), resource.TestCheckResourceAttrSet("data.planetscale_organizations.test", "organizations.0.billing_email"), resource.TestCheckResourceAttrSet("data.planetscale_organizations.test", "organizations.0.can_create_databases"), diff --git a/internal/provider/password_resource.go b/internal/provider/password_resource.go index 850eaa6..d310e22 100644 --- a/internal/provider/password_resource.go +++ b/internal/provider/password_resource.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" "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" @@ -21,8 +22,10 @@ import ( ) // Ensure provider defined types fully satisfy framework interfaces. -var _ resource.Resource = &passwordResource{} -var _ resource.ResourceWithImportState = &passwordResource{} +var ( + _ resource.Resource = &passwordResource{} + _ resource.ResourceWithImportState = &passwordResource{} +) func newPasswordResource() resource.Resource { return &passwordResource{} @@ -140,19 +143,22 @@ func (r *passwordResource) Schema(ctx context.Context, req resource.SchemaReques Attributes: map[string]schema.Attribute{ "organization": schema.StringAttribute{ Description: "The organization this database branch password belongs to.", - Required: true, PlanModifiers: []planmodifier.String{ + Required: true, + PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, }, "database": schema.StringAttribute{ - Description: "The datanase this branch password belongs to.", - Required: true, PlanModifiers: []planmodifier.String{ + Description: "The database this branch password belongs to.", + Required: true, + PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, }, "branch": schema.StringAttribute{ Description: "The branch this password belongs to.", - Required: true, PlanModifiers: []planmodifier.String{ + Required: true, + PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, }, @@ -186,15 +192,21 @@ func (r *passwordResource) Schema(ctx context.Context, req resource.SchemaReques }, "actor": schema.SingleNestedAttribute{ Description: "The actor that created this branch.", - Computed: true, Attributes: actorResourceSchemaAttribute, + Computed: true, + Attributes: actorResourceSchemaAttribute, }, "database_branch": schema.SingleNestedAttribute{ Description: "The branch this password is allowed to access.", - Computed: true, Attributes: databaseBranchResourceAttribute, + Computed: true, + Attributes: databaseBranchResourceAttribute, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplace(), + }, }, "region": schema.SingleNestedAttribute{ Description: "The region in which this password can be used.", - Computed: true, Attributes: regionResourceSchemaAttribute, + Computed: true, + Attributes: regionResourceSchemaAttribute, }, "access_host_url": schema.StringAttribute{ Description: "The host URL for the password.", @@ -224,7 +236,8 @@ func (r *passwordResource) Schema(ctx context.Context, req resource.SchemaReques // read-only, sensitive "plaintext": schema.StringAttribute{ Description: "The plaintext password, only available if the password was created by this provider.", - Sensitive: true, Computed: true}, + Sensitive: true, Computed: true, + }, // manually removed from spec because currently buggy // "integrations": schema.ListAttribute{Computed: true, ElementType: types.StringType}, @@ -346,6 +359,11 @@ func (r *passwordResource) Read(ctx context.Context, req resource.ReadRequest, r nil, // not sure why this would need a region id ) if err != nil { + if notFoundErr, ok := err.(*planetscale.GetPasswordRes404); ok { + tflog.Warn(ctx, fmt.Sprintf("Password not found, removing from state: %s", notFoundErr.Message)) + resp.State.RemoveResource(ctx) + return + } resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read password, got error: %s", err)) return } @@ -477,7 +495,7 @@ func (r *passwordResource) ImportState(ctx context.Context, req resource.ImportS if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { resp.Diagnostics.AddError( "Unexpected Import Identifier", - fmt.Sprintf("Expected import identifier with format: organization,database,name,id. Got: %q", req.ID), + fmt.Sprintf("Expected import identifier with format: organization,database,branch,id. Got: %q", req.ID), ) return } diff --git a/internal/provider/password_resource_test.go b/internal/provider/password_resource_test.go new file mode 100644 index 0000000..f4d2226 --- /dev/null +++ b/internal/provider/password_resource_test.go @@ -0,0 +1,155 @@ +package provider + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccPasswordResource(t *testing.T) { + dbName := acctest.RandomWithPrefix("testacc-passwd-db") + passwdName := acctest.RandomWithPrefix("testacc-passwd") + branchName := "main" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccPasswordResourceConfig(dbName, passwdName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("planetscale_password.test", "role", "admin"), + resource.TestCheckResourceAttr("planetscale_password.test", "branch", branchName), + ), + }, + // ImportState testing + { + ResourceName: "planetscale_password.test", + ImportState: true, + ImportStateVerify: true, + // Import requires: 'organization,database,branch,id' but 'id' of the password + // is only known after creation. Use a func to retrieve the ID from the state: + ImportStateIdFunc: func(s *terraform.State) (string, error) { + id, err := getPasswordIDFromState(s, "planetscale_password.test") + if err != nil { + return "", err + } + // Import requires: 'organization,database,branch,id' + return fmt.Sprintf("%s,%s,%s,%s", testAccOrg, dbName, branchName, id), nil + }, + // The actual password is not returned by the API, so we can't verify it here: + ImportStateVerifyIgnore: []string{"plaintext"}, + }, + // Update and Read testing + // TODO: Implement a test for password Update. Best current idea is to update the + // password's branch to a new branch, but that requires the ability to + // create a database and branch in a single plan which is currently broken + // due to the async nature of planetscale_database. + // Delete testing automatically occurs in TestCase + }, + }) +} + +// TestAccPasswordResource_OutOfBandDelete tests the out-of-band deletion of a branch password. +// In this test we simulate the password has been deleted out of band, perhaps by +// a user on the console or using pscale CLI. +// https://github.com/planetscale/terraform-provider-planetscale/issues/53 +func TestAccPasswordResource_OutOfBandDelete(t *testing.T) { + dbName := acctest.RandomWithPrefix("testacc-passwd-db") + passwdName := acctest.RandomWithPrefix("testacc-passwd") + branchName := "main" + + passId := "" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccPasswordResourceConfig(dbName, passwdName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("planetscale_password.test", "role", "admin"), + resource.TestCheckResourceAttr("planetscale_password.test", "branch", branchName), + ), + }, + // ImportState testing + { + ResourceName: "planetscale_password.test", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + id, err := getPasswordIDFromState(s, "planetscale_password.test") + if err != nil { + return "", err + } + passId = id // save the ID for use in later steps' PreConfig func: + // Import requires: 'organization,database,branch,id' + return fmt.Sprintf("%s,%s,%s,%s", testAccOrg, dbName, branchName, id), nil + }, + // The actual password is not returned by the API, so we can't verify it here: + ImportStateVerifyIgnore: []string{"plaintext"}, + }, + // Test out-of-bands deletion of the database should produce a plan to recreate, not error. + { + ResourceName: "planetscale_password.test", + RefreshState: true, + ExpectNonEmptyPlan: true, + PreConfig: func() { + ctx := context.Background() + if _, err := testAccAPIClient.DeletePassword(ctx, testAccOrg, dbName, branchName, passId); err != nil { + t.Fatalf("PreConfig: failed to delete password: %s", err) + } + }, + }, + }, + }) +} + +func testAccPasswordResourceConfig(dbName, passwdName string) string { + return fmt.Sprintf(` +resource "planetscale_database" "test" { + name = "%s" + organization = "%s" + cluster_size = "PS-10" + default_branch = "main" +} + +# TODO: Uncomment when the issue with branch creation after db creation is solved, then we can +# also expand the test coverage for password to include a change from one branch to another. +# resource "planetscale_branch" "two" { +# name = "TODO" +# organization = "TODO" +# database = planetscale_database.test.name +# parent_branch = planetscale_database.test.default_branch +# } + +resource "planetscale_password" "test" { + name = "%s" + organization = "%s" + database = planetscale_database.test.name + branch = planetscale_database.test.default_branch +} + `, dbName, testAccOrg, passwdName, testAccOrg) +} + +func getPasswordIDFromState(state *terraform.State, resourceName string) (string, error) { + // resourceName := "planetscale_password.test" + var rawState map[string]string + for _, m := range state.Modules { + if len(m.Resources) > 0 { + if v, ok := m.Resources[resourceName]; ok { + rawState = v.Primary.Attributes + } + } + } + if rawState == nil { + return "", fmt.Errorf("resource %s not found in state", resourceName) + } + return rawState["id"], nil +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index d935c31..1342fe6 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -2,6 +2,7 @@ package provider import ( "fmt" + "net/http" "os" "strconv" "strings" @@ -10,12 +11,18 @@ import ( "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/planetscale/terraform-provider-planetscale/internal/client/planetscale" + "golang.org/x/oauth2" ) +const testAccOrg = "planetscale-terraform-testing" + var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ "planetscale": providerserver.NewProtocol6WithError(New("test", false)()), } +var testAccAPIClient *planetscale.Client + func testAccPreCheck(t *testing.T) { var ( accessToken = os.Getenv("PLANETSCALE_ACCESS_TOKEN") @@ -28,16 +35,32 @@ func testAccPreCheck(t *testing.T) { default: t.Fatalf("must have either PLANETSCALE_ACCESS_TOKEN or both of (PLANETSCALE_SERVICE_TOKEN_NAME, PLANETSCALE_SERVICE_TOKEN)") } + + // TODO: factor client creation out of the provider Configure() func so we can + // more easily re-use it here and maintain the logic around access and service-token lookups + if testAccAPIClient == nil { + accessToken := os.Getenv("PLANETSCALE_ACCESS_TOKEN") + if accessToken == "" { + t.Fatal("PLANETSCALE_ACCESS_TOKEN must be set for acceptance tests") + } + + tok := &oauth2.Token{AccessToken: accessToken} + rt := &oauth2.Transport{ + Base: http.DefaultTransport, + Source: oauth2.StaticTokenSource(tok), + } + testAccAPIClient = planetscale.NewClient(&http.Client{Transport: rt}, nil) + } } -func checkIntegerMin(min int) resource.CheckResourceAttrWithFunc { +func checkIntegerMin(minimum int) resource.CheckResourceAttrWithFunc { return func(value string) error { v, err := strconv.Atoi(value) if err != nil { return err } - if v < min { - return fmt.Errorf("value %d is less than %d", v, min) + if v < minimum { + return fmt.Errorf("value %d is less than %d", v, minimum) } return nil } @@ -50,6 +73,6 @@ func checkOneOf(values ...string) resource.CheckResourceAttrWithFunc { return nil } } - return fmt.Errorf("valud %q is not one of %s", value, strings.Join(values, ", ")) + return fmt.Errorf("value %q is not one of %s", value, strings.Join(values, ", ")) } }