From f117259cf4157f53b0fb38d6d1c0a9cb04dab482 Mon Sep 17 00:00:00 2001 From: Murad Biashimov Date: Fri, 29 Nov 2024 15:48:01 +0100 Subject: [PATCH] feat(alloydbomni_database): add resource and datasource --- CHANGELOG.md | 1 + docs/data-sources/alloydbomni_database.md | 33 ++++ docs/resources/alloydbomni_database.md | 48 ++++++ internal/schemautil/service.go | 15 ++ internal/schemautil/wait.go | 13 ++ internal/sdkprovider/provider/provider.go | 12 +- .../alloydbomni/alloydbomni_database.go | 128 ++++++++++++++ .../alloydbomni_database_data_source.go | 29 ++++ .../alloydbomni/alloydbomni_database_test.go | 156 ++++++++++++++++++ 9 files changed, 431 insertions(+), 4 deletions(-) create mode 100644 docs/data-sources/alloydbomni_database.md create mode 100644 docs/resources/alloydbomni_database.md create mode 100644 internal/sdkprovider/service/alloydbomni/alloydbomni_database.go create mode 100644 internal/sdkprovider/service/alloydbomni/alloydbomni_database_data_source.go create mode 100644 internal/sdkprovider/service/alloydbomni/alloydbomni_database_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ecfeb152e..50018cda8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ nav_order: 1 - Add `alloydbomni` BETA resource and datasource - Add `aiven_alloydbomni_user` BETA resource and datasource +- Add `aiven_alloydbomni_database` BETA resource and datasource ## [4.30.0] - 2024-12-05 diff --git a/docs/data-sources/alloydbomni_database.md b/docs/data-sources/alloydbomni_database.md new file mode 100644 index 000000000..b462da890 --- /dev/null +++ b/docs/data-sources/alloydbomni_database.md @@ -0,0 +1,33 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_alloydbomni_database Data Source - terraform-provider-aiven" +subcategory: "" +description: |- + Gets information about a database in an Aiven for AlloyDB Omni service. + This resource is in the beta stage and may change without notice. Set + the PROVIDER_AIVEN_ENABLE_BETA environment variable to use the resource. +--- + +# aiven_alloydbomni_database (Data Source) + +Gets information about a database in an Aiven for AlloyDB Omni service. + +**This resource is in the beta stage and may change without notice.** Set +the `PROVIDER_AIVEN_ENABLE_BETA` environment variable to use the resource. + + + + +## Schema + +### Required + +- `database_name` (String) The name of the service database. Changing this property forces recreation of the resource. +- `project` (String) The name of the project this resource belongs to. To set up proper dependencies please refer to this variable as a reference. Changing this property forces recreation of the resource. +- `service_name` (String) The name of the service that this resource belongs to. To set up proper dependencies please refer to this variable as a reference. Changing this property forces recreation of the resource. + +### Read-Only + +- `id` (String) The ID of this resource. +- `lc_collate` (String) Default string sort order (`LC_COLLATE`) of the database. The default value is `en_US.UTF-8`. Changing this property forces recreation of the resource. +- `lc_ctype` (String) Default character classification (`LC_CTYPE`) of the database. The default value is `en_US.UTF-8`. Changing this property forces recreation of the resource. diff --git a/docs/resources/alloydbomni_database.md b/docs/resources/alloydbomni_database.md new file mode 100644 index 000000000..c10f6e9e6 --- /dev/null +++ b/docs/resources/alloydbomni_database.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_alloydbomni_database Resource - terraform-provider-aiven" +subcategory: "" +description: |- + Creates and manages a database in an Aiven for AlloyDB Omni service. + This resource is in the beta stage and may change without notice. Set + the PROVIDER_AIVEN_ENABLE_BETA environment variable to use the resource. +--- + +# aiven_alloydbomni_database (Resource) + +Creates and manages a database in an Aiven for AlloyDB Omni service. + +**This resource is in the beta stage and may change without notice.** Set +the `PROVIDER_AIVEN_ENABLE_BETA` environment variable to use the resource. + + + + +## Schema + +### Required + +- `database_name` (String) The name of the service database. Changing this property forces recreation of the resource. +- `project` (String) The name of the project this resource belongs to. To set up proper dependencies please refer to this variable as a reference. Changing this property forces recreation of the resource. +- `service_name` (String) The name of the service that this resource belongs to. To set up proper dependencies please refer to this variable as a reference. Changing this property forces recreation of the resource. + +### Optional + +- `lc_collate` (String) Default string sort order (`LC_COLLATE`) of the database. The default value is `en_US.UTF-8`. Changing this property forces recreation of the resource. +- `lc_ctype` (String) Default character classification (`LC_CTYPE`) of the database. The default value is `en_US.UTF-8`. Changing this property forces recreation of the resource. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `default` (String) +- `delete` (String) +- `read` (String) +- `update` (String) diff --git a/internal/schemautil/service.go b/internal/schemautil/service.go index f6081a715..45609cbee 100644 --- a/internal/schemautil/service.go +++ b/internal/schemautil/service.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "strconv" "strings" "time" @@ -957,10 +958,24 @@ func setProp[T comparable](m map[string]any, k string, v *T) { } } +// NewNotFound creates a new not found error +// There are lots of endpoints that return a list of objects which might not contain the object we are looking for. +// In this case, we should still return 404. +func NewNotFound(msg string, args ...any) error { + return aiven.Error{Status: http.StatusNotFound, Message: fmt.Sprintf(msg, args...)} +} + func IsNotFound(err error) bool { return aiven.IsNotFound(err) || avngen.IsNotFound(err) } +func OmitNotFound(err error) error { + if IsNotFound(err) { + return nil + } + return err +} + // IsUnknownRole checks if the database returned an error because of an unknown role // to make deletions idempotent func IsUnknownRole(err error) bool { diff --git a/internal/schemautil/wait.go b/internal/schemautil/wait.go index 362b637d1..9b4884358 100644 --- a/internal/schemautil/wait.go +++ b/internal/schemautil/wait.go @@ -12,6 +12,7 @@ import ( avngen "github.com/aiven/go-client-codegen" "github.com/aiven/go-client-codegen/handler/service" "github.com/aiven/go-client-codegen/handler/staticip" + retryGo "github.com/avast/retry-go" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -357,3 +358,15 @@ L: func staticIpsForServiceFromSchema(d *schema.ResourceData) []string { return FlattenToString(d.Get("static_ips").(*schema.Set).List()) } + +// WaitUntilNotFound retries the given retryableFunc until it returns 404 +// To stop the retrying, the function should return retryGo.Unrecoverable +func WaitUntilNotFound(ctx context.Context, retryableFunc retryGo.RetryableFunc) error { + return retryGo.Do( + func() error { + return OmitNotFound(retryableFunc()) + }, + retryGo.Context(ctx), + retryGo.Delay(common.DefaultStateChangeDelay), + ) +} diff --git a/internal/sdkprovider/provider/provider.go b/internal/sdkprovider/provider/provider.go index 99666c8a0..5f35201be 100644 --- a/internal/sdkprovider/provider/provider.go +++ b/internal/sdkprovider/provider/provider.go @@ -79,8 +79,9 @@ func Provider(version string) (*schema.Provider, error) { "aiven_pg_database": pg.DatasourcePGDatabase(), // alloydbomni - "aiven_alloydbomni": alloydbomni.DatasourceAlloyDBOmni(), - "aiven_alloydbomni_user": alloydbomni.DatasourceAlloyDBOmniUser(), + "aiven_alloydbomni": alloydbomni.DatasourceAlloyDBOmni(), + "aiven_alloydbomni_user": alloydbomni.DatasourceAlloyDBOmniUser(), + "aiven_alloydbomni_database": alloydbomni.DatasourceAlloyDBOmniDatabase(), // cassandra "aiven_cassandra": cassandra.DatasourceCassandra(), @@ -192,8 +193,9 @@ func Provider(version string) (*schema.Provider, error) { "aiven_pg_database": pg.ResourcePGDatabase(), // alloydbomni - "aiven_alloydbomni": alloydbomni.ResourceAlloyDBOmni(), - "aiven_alloydbomni_user": alloydbomni.ResourceAlloyDBOmniUser(), + "aiven_alloydbomni": alloydbomni.ResourceAlloyDBOmni(), + "aiven_alloydbomni_user": alloydbomni.ResourceAlloyDBOmniUser(), + "aiven_alloydbomni_database": alloydbomni.ResourceAlloyDBOmniDatabase(), // cassandra "aiven_cassandra": cassandra.ResourceCassandra(), @@ -290,11 +292,13 @@ func Provider(version string) (*schema.Provider, error) { betaResources := []string{ "aiven_alloydbomni", "aiven_alloydbomni_user", + "aiven_alloydbomni_database", } betaDataSources := []string{ "aiven_alloydbomni", "aiven_alloydbomni_user", + "aiven_alloydbomni_database", "aiven_organization_user_list", } diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_database.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_database.go new file mode 100644 index 000000000..83fbc9311 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_database.go @@ -0,0 +1,128 @@ +package alloydbomni + +import ( + "context" + + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/service" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" + "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" +) + +const defaultLC = "en_US.UTF-8" + +var aivenAlloyDBOmniDatabaseSchema = map[string]*schema.Schema{ + "project": schemautil.CommonSchemaProjectReference, + "service_name": schemautil.CommonSchemaServiceNameReference, + "database_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: userconfig.Desc("The name of the service database.").ForceNew().Build(), + }, + "lc_ctype": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: defaultLC, + Description: userconfig.Desc("Default character classification (`LC_CTYPE`) of the database.").DefaultValue(defaultLC).ForceNew().Build(), + }, + "lc_collate": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: defaultLC, + Description: userconfig.Desc("Default string sort order (`LC_COLLATE`) of the database.").DefaultValue(defaultLC).ForceNew().Build(), + }, +} + +func ResourceAlloyDBOmniDatabase() *schema.Resource { + return &schema.Resource{ + Description: "Creates and manages a database in an Aiven for AlloyDB Omni service.", + CreateContext: common.WithGenClient(resourceAlloyDBOmniDatabaseCreate), + ReadContext: common.WithGenClient(resourceAlloyDBOmniDatabaseRead), + DeleteContext: common.WithGenClient(resourceAlloyDBOmniDatabaseDelete), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: schemautil.DefaultResourceTimeouts(), + Schema: aivenAlloyDBOmniDatabaseSchema, + } +} + +func resourceAlloyDBOmniDatabaseCreate(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + projectName := d.Get("project").(string) + serviceName := d.Get("service_name").(string) + + req := new(service.ServiceDatabaseCreateIn) + err := schemautil.ResourceDataGet(d, req, schemautil.RenameAlias("database_name", "database")) + if err != nil { + return err + } + + err = client.ServiceDatabaseCreate(ctx, projectName, serviceName, req) + if err != nil { + return err + } + + d.SetId(schemautil.BuildResourceID(projectName, serviceName, req.Database)) + return resourceAlloyDBOmniDatabaseRead(ctx, d, client) +} + +func resourceAlloyDBOmniDatabaseRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + projectName, serviceName, dbName, err := schemautil.SplitResourceID3(d.Id()) + if err != nil { + return err + } + + db, err := getDatabase(ctx, client, projectName, serviceName, dbName) + if err != nil { + return err + } + + if err := d.Set("project", projectName); err != nil { + return err + } + + if err := d.Set("service_name", serviceName); err != nil { + return err + } + + return schemautil.ResourceDataSet(aivenAlloyDBOmniDatabaseSchema, d, db) +} + +func resourceAlloyDBOmniDatabaseDelete(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + projectName, serviceName, dbName, err := schemautil.SplitResourceID3(d.Id()) + if err != nil { + return err + } + + err = client.ServiceDatabaseDelete(ctx, projectName, serviceName, dbName) + if err != nil { + return err + } + + // Waits until database is deleted + return schemautil.WaitUntilNotFound(ctx, func() error { + _, err = getDatabase(ctx, client, projectName, serviceName, dbName) + return err + }) +} + +func getDatabase(ctx context.Context, client avngen.Client, projectName, serviceName, dbName string) (*service.DatabaseOut, error) { + list, err := client.ServiceDatabaseList(ctx, projectName, serviceName) + if err != nil { + return nil, err + } + + for _, db := range list { + if db.DatabaseName == dbName { + return &db, nil + } + } + + return nil, schemautil.NewNotFound("service database %q not found", dbName) +} diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_database_data_source.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_database_data_source.go new file mode 100644 index 000000000..6c5c75125 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_database_data_source.go @@ -0,0 +1,29 @@ +package alloydbomni + +import ( + "context" + + avngen "github.com/aiven/go-client-codegen" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +func DatasourceAlloyDBOmniDatabase() *schema.Resource { + return &schema.Resource{ + ReadContext: common.WithGenClient(datasourceDatabaseRead), + Description: "Gets information about a database in an Aiven for AlloyDB Omni service.", + Schema: schemautil.ResourceSchemaAsDatasourceSchema(aivenAlloyDBOmniDatabaseSchema, + "project", "service_name", "database_name"), + } +} + +func datasourceDatabaseRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + projectName := d.Get("project").(string) + serviceName := d.Get("service_name").(string) + databaseName := d.Get("database_name").(string) + + d.SetId(schemautil.BuildResourceID(projectName, serviceName, databaseName)) + return resourceAlloyDBOmniDatabaseRead(ctx, d, client) +} diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_database_test.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_database_test.go new file mode 100644 index 000000000..e87efde30 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_database_test.go @@ -0,0 +1,156 @@ +package alloydbomni_test + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "testing" + + "github.com/aiven/aiven-go-client/v2" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +func TestAccAivenAlloyDBOmniDatabase_basic(t *testing.T) { + resourceName := "aiven_alloydbomni_database.foo" + projectName := os.Getenv("AIVEN_PROJECT_NAME") + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: testAccCheckAivenAlloyDBOmniDatabaseResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniDatabaseResource(projectName, rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "project", projectName), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "database_name", fmt.Sprintf("test-acc-db-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "lc_ctype", "en_US.UTF-8"), + resource.TestCheckResourceAttr(resourceName, "lc_collate", "en_US.UTF-8"), + ), + }, + { + Config: testAccAlloyDBOmniDatabaseResource(projectName, rName), + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("expected resource '%s' to be present in the state", resourceName) + } + if _, ok := rs.Primary.Attributes["database_name"]; !ok { + return "", fmt.Errorf("expected resource '%s' to have 'database_name' attribute", resourceName) + } + return rs.Primary.ID, nil + }, + ImportStateCheck: func(s []*terraform.InstanceState) error { + if len(s) != 1 { + return fmt.Errorf("expected only one instance to be imported, state: %#v", s) + } + attributes := s[0].Attributes + if !strings.EqualFold(attributes["project"], projectName) { + return fmt.Errorf("expected project to match '%s', got: '%s'", projectName, attributes["project_name"]) + } + databaseName, ok := attributes["database_name"] + if !ok { + return errors.New("expected 'database_name' field to be set") + } + if _, ok := attributes["lc_ctype"]; !ok { + return errors.New("expected 'lc_ctype' field to be set") + } + if _, ok := attributes["lc_collate"]; !ok { + return errors.New("expected 'lc_collate' field to be set") + } + expectedID := fmt.Sprintf("%s/test-acc-sr-%s/%s", projectName, rName, databaseName) + if !strings.EqualFold(s[0].ID, expectedID) { + return fmt.Errorf("expected ID to match '%s', but got: %s", expectedID, s[0].ID) + } + return nil + }, + }, + }, + }) +} + +func testAccCheckAivenAlloyDBOmniDatabaseResourceDestroy(s *terraform.State) error { + c := acc.GetTestAivenClient() + + ctx := context.Background() + + // loop through the resources in state, verifying each database is destroyed + for _, rs := range s.RootModule().Resources { + if rs.Type != "aiven_alloydbomni_database" { + continue + } + + projectName, serviceName, databaseName, err := schemautil.SplitResourceID3(rs.Primary.ID) + if err != nil { + return err + } + + db, err := c.Databases.Get(ctx, projectName, serviceName, databaseName) + if err != nil { + var e aiven.Error + if errors.As(err, &e) && e.Status != 404 { + return err + } + } + + if db != nil { + return fmt.Errorf("database (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccAlloyDBOmniDatabaseResource(project string, name string) string { + return fmt.Sprintf(` +data "aiven_project" "foo" { + project = "%s" +} + +resource "aiven_alloydbomni" "bar" { + project = data.aiven_project.foo.project + cloud_name = "google-europe-west1" + plan = "startup-4" + service_name = "test-acc-sr-%s" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" + + alloydbomni_user_config { + public_access { + pg = true + prometheus = false + } + + pg { + idle_in_transaction_session_timeout = 900 + } + } +} + +resource "aiven_alloydbomni_database" "foo" { + project = aiven_alloydbomni.bar.project + service_name = aiven_alloydbomni.bar.service_name + database_name = "test-acc-db-%s" + lc_ctype = "en_US.UTF-8" + lc_collate = "en_US.UTF-8" +} + +data "aiven_alloydbomni_database" "database" { + project = aiven_alloydbomni_database.foo.project + service_name = aiven_alloydbomni_database.foo.service_name + database_name = aiven_alloydbomni_database.foo.database_name + + depends_on = [aiven_alloydbomni_database.foo] +}`, project, name, name) +}