diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 0ec24d82e..59e5046fc 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -40,30 +40,7 @@ jobs: fail-fast: false matrix: pkg: [ - kafka, - kafkatopic, - kafkaschema, - account, - cassandra, - clickhouse, - connectionpool, - flink, - pg, - grafana, - influxdb, - m3db, - mysql, - opensearch, - organization, - project, - redis, - servicecomponent, - staticip, - serviceintegration, - vpc, - dragonfly, - thanos, - valkey + alloydbomni ] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 58f11e8db..db309c1eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ nav_order: 1 ## [MAJOR.MINOR.PATCH] - YYYY-MM-DD +- Add `alloydbomni`, `aiven_alloydbomni_user`, `aiven_alloydbomni_database` BETA resources and datasources - Add `aiven_opensearch` resource field `opensearch_user_config.opensearch.search_insights_top_queries` - Add `aiven_thanos` resource field `thanos_user_config.private_access`: Allow access to selected service ports from private networks diff --git a/internal/schemautil/service.go b/internal/schemautil/service.go index db00759b3..3a82791f1 100644 --- a/internal/schemautil/service.go +++ b/internal/schemautil/service.go @@ -37,6 +37,7 @@ func DefaultResourceTimeouts() *schema.ResourceTimeout { } const ( + ServiceTypeAlloyDBOmni = "alloydbomni" ServiceTypePG = "pg" ServiceTypeCassandra = "cassandra" ServiceTypeOpenSearch = "opensearch" @@ -826,7 +827,7 @@ func copyConnectionInfoFromAPIResponseToTerraform( setProp(props, "connect_uri", connectionInfo.KafkaConnectUri) setProp(props, "rest_uri", connectionInfo.KafkaRestUri) setProp(props, "schema_registry_uri", connectionInfo.SchemaRegistryUri) - case ServiceTypePG: + case ServiceTypeAlloyDBOmni, ServiceTypePG: // For compatibility with the old schema, we only set the first URI. // TODO: Remove this block in the next major version. Keep `uris` key only, see below. if len(connectionInfo.Pg) > 0 { diff --git a/internal/schemautil/wait.go b/internal/schemautil/wait.go index 694bf9786..362b637d1 100644 --- a/internal/schemautil/wait.go +++ b/internal/schemautil/wait.go @@ -244,7 +244,7 @@ func grafanaReady(s *service.ServiceGetOut) bool { func backupsReady(s *service.ServiceGetOut) bool { switch s.ServiceType { - case ServiceTypePG, ServiceTypeInfluxDB, ServiceTypeRedis, ServiceTypeDragonfly: + case ServiceTypeAlloyDBOmni, ServiceTypePG, ServiceTypeInfluxDB, ServiceTypeRedis, ServiceTypeDragonfly: // See https://github.com/aiven/terraform-provider-aiven/issues/756 switch "off" { case s.UserConfig["redis_persistence"], s.UserConfig["dragonfly_persistence"]: diff --git a/internal/sdkprovider/provider/provider.go b/internal/sdkprovider/provider/provider.go index ef1634d89..bb67d8e96 100644 --- a/internal/sdkprovider/provider/provider.go +++ b/internal/sdkprovider/provider/provider.go @@ -13,6 +13,7 @@ import ( "github.com/aiven/terraform-provider-aiven/internal/plugin/util" "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/account" + "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/alloydbomni" "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/cassandra" "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/clickhouse" "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/service/connectionpool" @@ -77,6 +78,11 @@ func Provider(version string) (*schema.Provider, error) { "aiven_pg_user": pg.DatasourcePGUser(), "aiven_pg_database": pg.DatasourcePGDatabase(), + // alloydbomni + "aiven_alloydbomni": alloydbomni.DatasourceAlloyDBOmni(), + "aiven_alloydbomni_user": alloydbomni.DatasourceAlloyDBOmniUser(), + "aiven_alloydbomni_database": alloydbomni.DatasourceAlloyDBOmniDatabase(), + // cassandra "aiven_cassandra": cassandra.DatasourceCassandra(), "aiven_cassandra_user": cassandra.DatasourceCassandraUser(), @@ -186,6 +192,11 @@ func Provider(version string) (*schema.Provider, error) { "aiven_pg_user": pg.ResourcePGUser(), "aiven_pg_database": pg.ResourcePGDatabase(), + // alloydbomni + "aiven_alloydbomni": alloydbomni.ResourceAlloyDBOmni(), + "aiven_alloydbomni_user": alloydbomni.ResourceAlloyDBOmniUser(), + "aiven_alloydbomni_database": alloydbomni.ResourceAlloyDBOmniDatabase(), + // cassandra "aiven_cassandra": cassandra.ResourceCassandra(), "aiven_cassandra_user": cassandra.ResourceCassandraUser(), @@ -277,9 +288,16 @@ func Provider(version string) (*schema.Provider, error) { } // Adds "beta" warning to the description - betaResources := []string{} + 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.go b/internal/sdkprovider/service/alloydbomni/alloydbomni.go new file mode 100644 index 000000000..5ceffe904 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni.go @@ -0,0 +1,192 @@ +package alloydbomni + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +func aivenAlloyDBOmniSchema() map[string]*schema.Schema { + s := schemautil.ServiceCommonSchemaWithUserConfig(schemautil.ServiceTypeAlloyDBOmni) + s[schemautil.ServiceTypeAlloyDBOmni] = &schema.Schema{ + Type: schema.TypeList, + MaxItems: 1, + Computed: true, + Description: "Values provided by the AlloyDB Omni server.", + Optional: true, + Sensitive: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // TODO: Remove `uri` in the next major version. + "uri": { + Type: schema.TypeString, + Computed: true, + Description: "AlloyDB Omni primary connection URI.", + Optional: true, + Sensitive: true, + }, + "uris": { + Type: schema.TypeList, + Computed: true, + Description: "AlloyDB Omni primary connection URIs.", + Optional: true, + Sensitive: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + Sensitive: true, + }, + }, + "bouncer": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "PgBouncer connection details for [connection pooling](https://aiven.io/docs/products/postgresql/concepts/pg-connection-pooling).", + Deprecated: "This field was added by mistake and has never worked. It will be removed in future versions.", + }, + // TODO: Remove `host` in the next major version. + "host": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "AlloyDB Omni primary node host IP or name.", + }, + // TODO: Remove `port` in the next major version. + "port": { + Type: schema.TypeInt, + Computed: true, + Sensitive: true, + Description: "AlloyDB Omni port.", + }, + // TODO: Remove `sslmode` in the next major version. + "sslmode": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "AlloyDB Omni SSL mode setting.", + }, + // TODO: Remove `user` in the next major version. + "user": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "AlloyDB Omni admin user name.", + }, + // TODO: Remove `password` in the next major version. + "password": { + Type: schema.TypeString, + Computed: true, + Description: "AlloyDB Omni admin user password.", + Sensitive: true, + }, + // TODO: Remove `dbname` in the next major version. + "dbname": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "Primary AlloyDB Omni database name.", + }, + "params": { + Type: schema.TypeList, + Computed: true, + Description: "AlloyDB Omni connection parameters.", + Optional: true, + Sensitive: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "host": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "AlloyDB Omni host IP or name.", + }, + "port": { + Type: schema.TypeInt, + Computed: true, + Sensitive: true, + Description: "AlloyDB Omni port.", + }, + "sslmode": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "AlloyDB Omni SSL mode setting.", + }, + "user": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "AlloyDB Omni admin user name.", + }, + "password": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "AlloyDB Omni admin user password.", + }, + "database_name": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "Primary AlloyDB Omni database name.", + }, + }, + }, + }, + "replica_uri": { + Type: schema.TypeString, + Computed: true, + Description: "AlloyDB Omni replica URI for services with a replica.", + Sensitive: true, + }, + "standby_uris": { + Type: schema.TypeList, + Computed: true, + Description: "AlloyDB Omni standby connection URIs.", + Optional: true, + Sensitive: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + Sensitive: true, + }, + }, + "syncing_uris": { + Type: schema.TypeList, + Computed: true, + Description: "AlloyDB Omni syncing connection URIs.", + Optional: true, + Sensitive: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + Sensitive: true, + }, + }, + // TODO: This isn't in the connection info, but it's in the metadata. + // We should move this to the other part of the schema in the next major version. + "max_connections": { + Type: schema.TypeInt, + Computed: true, + Sensitive: true, + Description: "The [number of allowed connections](https://aiven.io/docs/products/postgresql/reference/pg-connection-limits). Varies based on the service plan.", + }, + }, + }, + } + return s +} + +func ResourceAlloyDBOmni() *schema.Resource { + return &schema.Resource{ + Description: "Creates and manages an Aiven for AlloyDB Omni service.", + CreateContext: schemautil.ResourceServiceCreateWrapper(schemautil.ServiceTypeAlloyDBOmni), + ReadContext: schemautil.ResourceServiceRead, + UpdateContext: schemautil.ResourceServiceUpdate, + DeleteContext: schemautil.ResourceServiceDelete, + CustomizeDiff: schemautil.CustomizeDiffGenericService(schemautil.ServiceTypeAlloyDBOmni), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: schemautil.DefaultResourceTimeouts(), + Schema: aivenAlloyDBOmniSchema(), + SchemaVersion: 1, + } +} diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_data_source.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_data_source.go new file mode 100644 index 000000000..9fac7bdd5 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_data_source.go @@ -0,0 +1,15 @@ +package alloydbomni + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +func DatasourceAlloyDBOmni() *schema.Resource { + return &schema.Resource{ + ReadContext: schemautil.DatasourceServiceRead, + Description: "Gets information about an Aiven for AlloyDB Omni service.", + Schema: schemautil.ResourceSchemaAsDatasourceSchema(aivenAlloyDBOmniSchema(), "project", "service_name"), + } +} diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_database.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_database.go new file mode 100644 index 000000000..dc29b1ac6 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_database.go @@ -0,0 +1,162 @@ +package alloydbomni + +import ( + "context" + + "github.com/aiven/aiven-go-client/v2" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/schemautil" + "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" +) + +const defaultLC = "en_US.UTF-8" + +// handleLcDefaults checks if the lc values have actually changed +func handleLcDefaults(_, old, new string, _ *schema.ResourceData) bool { + // NOTE! not all database resources return lc_* values even if + // they are set when the database is created; best we can do is + // to assume it was created using the default value. + return new == "" || (old == "" && new == defaultLC) || old == new +} + +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_collate": { + Type: schema.TypeString, + Optional: true, + Default: defaultLC, + ForceNew: true, + DiffSuppressFunc: handleLcDefaults, + Description: userconfig.Desc("Default string sort order (`LC_COLLATE`) of the database.").DefaultValue(defaultLC).ForceNew().Build(), + }, + "lc_ctype": { + Type: schema.TypeString, + Optional: true, + Default: defaultLC, + ForceNew: true, + DiffSuppressFunc: handleLcDefaults, + Description: userconfig.Desc("Default character classification (`LC_CTYPE`) of the database.").DefaultValue(defaultLC).ForceNew().Build(), + }, + "termination_protection": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: userconfig.Desc(`Terraform client-side deletion protection, which prevents the database from being deleted by Terraform. It's recommended to enable this for any production databases containing critical data.`).DefaultValue(false).Build(), + }, +} + +func ResourceAlloyDBOmniDatabase() *schema.Resource { + return &schema.Resource{ + Description: "Creates and manages a database in an Aiven for AlloyDB Omni service.", + CreateContext: resourceAlloyDBOmniDatabaseCreate, + ReadContext: resourceAlloyDBOmniDatabaseRead, + DeleteContext: resourceAlloyDBOmniDatabaseDelete, + UpdateContext: resourceAlloyDBOmniDatabaseUpdate, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: schemautil.DefaultResourceTimeouts(), + + Schema: aivenAlloyDBOmniDatabaseSchema, + } +} + +func resourceAlloyDBOmniDatabaseCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*aiven.Client) + + projectName := d.Get("project").(string) + serviceName := d.Get("service_name").(string) + databaseName := d.Get("database_name").(string) + _, err := client.Databases.Create( + ctx, + projectName, + serviceName, + aiven.CreateDatabaseRequest{ + Database: databaseName, + LcCollate: d.Get("lc_collate").(string), + LcType: d.Get("lc_ctype").(string), + }, + ) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(schemautil.BuildResourceID(projectName, serviceName, databaseName)) + + return resourceAlloyDBOmniDatabaseRead(ctx, d, m) +} + +func resourceAlloyDBOmniDatabaseUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + return resourceAlloyDBOmniDatabaseRead(ctx, d, m) +} + +func resourceAlloyDBOmniDatabaseRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*aiven.Client) + + projectName, serviceName, databaseName, err := schemautil.SplitResourceID3(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + database, err := client.Databases.Get(ctx, projectName, serviceName, databaseName) + if err != nil { + return diag.FromErr(schemautil.ResourceReadHandleNotFound(err, d)) + } + + if err := d.Set("database_name", database.DatabaseName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("project", projectName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("service_name", serviceName); err != nil { + return diag.FromErr(err) + } + if err := d.Set("lc_collate", database.LcCollate); err != nil { + return diag.FromErr(err) + } + if err := d.Set("lc_ctype", database.LcType); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceAlloyDBOmniDatabaseDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client := m.(*aiven.Client) + + projectName, serviceName, databaseName, err := schemautil.SplitResourceID3(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + if d.Get("termination_protection").(bool) { + return diag.Errorf("cannot delete a database termination_protection is enabled") + } + + waiter := schemautil.DatabaseDeleteWaiter{ + Context: ctx, + Client: client, + ProjectName: projectName, + ServiceName: serviceName, + Database: databaseName, + } + + timeout := d.Timeout(schema.TimeoutDelete) + + _, err = waiter.Conf(timeout).WaitForStateContext(ctx) + if err != nil { + return diag.Errorf("error waiting for Aiven Database to be DELETED: %s", err) + } + + return nil +} 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..685fed416 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_database_data_source.go @@ -0,0 +1,43 @@ +package alloydbomni + +import ( + "context" + + "github.com/aiven/aiven-go-client/v2" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +func DatasourceAlloyDBOmniDatabase() *schema.Resource { + return &schema.Resource{ + ReadContext: 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, m interface{}) diag.Diagnostics { + client := m.(*aiven.Client) + + projectName := d.Get("project").(string) + serviceName := d.Get("service_name").(string) + databaseName := d.Get("database_name").(string) + + databases, err := client.Databases.List(ctx, projectName, serviceName) + if err != nil { + return diag.FromErr(err) + } + + for _, db := range databases { + if db.DatabaseName == databaseName { + d.SetId(schemautil.BuildResourceID(projectName, serviceName, databaseName)) + return resourceAlloyDBOmniDatabaseRead(ctx, d, m) + } + } + + return diag.Errorf("database %s/%s/%s not found", + projectName, serviceName, databaseName) +} 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..e52fcf81d --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_database_test.go @@ -0,0 +1,212 @@ +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) + rName2 := 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"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + ), + }, + { + Config: testAccAlloyDBOmniDatabaseTerminationProtectionResource(projectName, rName2), + PreventPostDestroyRefresh: true, + ExpectNonEmptyPlan: true, + PlanOnly: true, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "project", projectName), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName2)), + resource.TestCheckResourceAttr(resourceName, "database_name", fmt.Sprintf("test-acc-db-%s", rName2)), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "true"), + ), + }, + { + 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) +} + +func testAccAlloyDBOmniDatabaseTerminationProtectionResource(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" + termination_protection = true +} + +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) +} diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_import_test.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_import_test.go new file mode 100644 index 000000000..c19791e14 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_import_test.go @@ -0,0 +1,108 @@ +package alloydbomni_test + +import ( + "fmt" + "os" + "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/stretchr/testify/assert" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" +) + +func TestAccAivenAlloyDBOmni_import(t *testing.T) { + resourceName := "aiven_alloydbomni.main" + 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: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniImportResource(projectName, rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "project", projectName), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s-main", rName)), + ), + }, + { + Config: testAccAlloyDBOmniImportResource(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) + } + return rs.Primary.ID, nil + }, + ImportStateCheck: func(s []*terraform.InstanceState) error { + assert := assert.New(t) + if !assert.Len(s, 1, "expected only one instance to be imported") { + return fmt.Errorf("state: %#v", s) + } + attributes := s[0].Attributes + assert.Equal(projectName, attributes["project"]) + assert.Equal("google-europe-west1", attributes["cloud_name"]) + assert.Equal("startup-4", attributes["plan"]) + assert.Equal(fmt.Sprintf("test-acc-sr-%s-main", rName), attributes["service_name"]) + assert.Equal("monday", attributes["maintenance_window_dow"]) + assert.Equal("10:00:00", attributes["maintenance_window_time"]) + assert.Equal("30GiB", attributes["additional_disk_space"]) + assert.Equal("alloydbomniimporttest@aiven.io", attributes["tech_emails.0.email"]) + assert.Equal("test-key", attributes["tag.0.key"]) + assert.Equal("test-value", attributes["tag.0.value"]) + assert.Equal(fmt.Sprintf("%s/test-acc-sr-%s-main", projectName, rName), s[0].ID) + return nil + }, + }, + }, + }) +} + +func testAccAlloyDBOmniImportResource(project string, name string) string { + return fmt.Sprintf(` +data "aiven_project" "foo" { + project = "%s" +} + +resource "aiven_alloydbomni" "main" { + project = data.aiven_project.foo.project + cloud_name = "google-europe-west1" + plan = "startup-4" + service_name = "test-acc-sr-%s-main" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" + additional_disk_space = "30GiB" + + tech_emails { + email = "alloydbomniimporttest@aiven.io" + } + + tag { + key = "test-key" + value = "test-value" + } +} + +resource "aiven_alloydbomni" "read_replica" { + project = data.aiven_project.foo.project + cloud_name = "google-europe-west1" + plan = "startup-4" + service_name = "test-acc-sr-%s-read-replica" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" + additional_disk_space = "30GiB" + + service_integrations { + source_service_name = aiven_alloydbomni.main.service_name + integration_type = "read_replica" + } +} +`, project, name, name) +} diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_test.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_test.go new file mode 100644 index 000000000..da58f40f8 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_test.go @@ -0,0 +1,1062 @@ +package alloydbomni_test + +import ( + "context" + "fmt" + "os" + "regexp" + "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 TestAccAivenAlloyDBOmni_invalid_disk_size(t *testing.T) { + expectErrorRegexBadString := regexp.MustCompile(regexp.QuoteMeta("configured string must match ^[1-9][0-9]*(G|GiB)")) + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // bad strings + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "abc"), + PlanOnly: true, + ExpectError: expectErrorRegexBadString, + }, + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "01MiB"), + PlanOnly: true, + ExpectError: expectErrorRegexBadString, + }, + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "1234"), + PlanOnly: true, + ExpectError: expectErrorRegexBadString, + }, + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "5TiB"), + PlanOnly: true, + ExpectError: expectErrorRegexBadString, + }, + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, " 1Gib "), + PlanOnly: true, + ExpectError: expectErrorRegexBadString, + }, + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "1 GiB"), + PlanOnly: true, + ExpectError: expectErrorRegexBadString, + }, + // bad disk sizes + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "1GiB"), + PlanOnly: true, + ExpectError: regexp.MustCompile("requested disk size is too small"), + }, + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "100000GiB"), + PlanOnly: true, + ExpectError: regexp.MustCompile("requested disk size is too large"), + }, + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "127GiB"), + PlanOnly: true, + ExpectError: regexp.MustCompile("requested disk size has to increase from: '.*' in increments of '.*'"), + }, + { + Config: testAccAlloyDBOmniResourceWithAdditionalDiskSize(rName, "127GiB"), + PlanOnly: true, + ExpectError: regexp.MustCompile("requested disk size has to increase from: '.*' in increments of '.*'"), + }, + { + Config: testAccAlloyDBOmniResourceWithAdditionalDiskSize(rName, "100000GiB"), + PlanOnly: true, + ExpectError: regexp.MustCompile("requested disk size is too large"), + }, + { + Config: testAccAlloyDBOmniResourceWithAdditionalDiskSize(rName, "abc"), + PlanOnly: true, + ExpectError: expectErrorRegexBadString, + }, + { + Config: testAccAlloyDBOmniResourceWithAdditionalDiskSize(rName, "01MiB"), + PlanOnly: true, + ExpectError: expectErrorRegexBadString, + }, + { + Config: testAccAlloyDBOmniResourceWithAdditionalDiskSize(rName, "1234"), + PlanOnly: true, + ExpectError: expectErrorRegexBadString, + }, + { + Config: testAccAlloyDBOmniDoubleTagResource(rName), + PlanOnly: true, + ExpectNonEmptyPlan: true, + ExpectError: regexp.MustCompile("tag keys should be unique"), + }, + }, + }) +} + +func TestAccAivenAlloyDBOmni_static_ips(t *testing.T) { + resourceName := "aiven_alloydbomni.bar" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniWithStaticIps(rName, 2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "service_type", "alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + resource.TestCheckResourceAttrSet(resourceName, "service_username"), + resource.TestCheckResourceAttrSet(resourceName, "service_password"), + resource.TestCheckResourceAttrSet(resourceName, "service_host"), + resource.TestCheckResourceAttrSet(resourceName, "service_port"), + resource.TestCheckResourceAttrSet(resourceName, "service_uri"), + resource.TestCheckResourceAttr(resourceName, "static_ips.#", "2"), + ), + }, + { + Config: testAccAlloyDBOmniWithStaticIps(rName, 3), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "static_ips.#", "3"), + ), + }, + { + Config: testAccAlloyDBOmniWithStaticIps(rName, 4), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "static_ips.#", "4"), + ), + }, + { + Config: testAccAlloyDBOmniWithStaticIps(rName, 3), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "static_ips.#", "3"), + ), + }, + { + Config: testAccAlloyDBOmniWithStaticIps(rName, 4), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "static_ips.#", "4"), + ), + }, + { + Config: testAccAlloyDBOmniWithStaticIps(rName, 2), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "static_ips.#", "2"), + ), + }, + }, + }) +} + +func TestAccAivenAlloyDBOmni_changing_plan(t *testing.T) { + resourceName := "aiven_alloydbomni.bar" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniResourcePlanChange(rName, "business-8"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "service_type", "alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + resource.TestCheckResourceAttrSet(resourceName, "disk_space_used"), + ), + }, + { + Config: testAccAlloyDBOmniResourcePlanChange(rName, "business-4"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "service_type", "alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + resource.TestCheckResourceAttrSet(resourceName, "disk_space_used"), + ), + }, + }, + }) +} + +func TestAccAivenAlloyDBOmni_deleting_additional_disk_size(t *testing.T) { + resourceName := "aiven_alloydbomni.bar" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniResourceWithAdditionalDiskSize(rName, "20GiB"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "service_type", "alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "disk_space_used", "100GiB"), + resource.TestCheckResourceAttr(resourceName, "disk_space_default", "80GiB"), + resource.TestCheckResourceAttr(resourceName, "additional_disk_space", "20GiB"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + ), + }, + { + Config: testAccAlloyDBOmniResourceWithoutDiskSize(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "service_type", "alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "disk_space_default", "80GiB"), + resource.TestCheckResourceAttr(resourceName, "disk_space_used", "80GiB"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + ), + }, + }, + }) +} + +func TestAccAivenAlloyDBOmni_deleting_disk_size(t *testing.T) { + resourceName := "aiven_alloydbomni.bar" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "90GiB"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "service_type", "alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "disk_space", "90GiB"), + resource.TestCheckResourceAttr(resourceName, "disk_space_used", "90GiB"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + ), + }, + { + Config: testAccAlloyDBOmniResourceWithoutDiskSize(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "service_type", "alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "disk_space", ""), + resource.TestCheckResourceAttrSet(resourceName, "disk_space_used"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + ), + }, + }, + }) +} + +func TestAccAivenAlloyDBOmni_changing_disk_size(t *testing.T) { + resourceName := "aiven_alloydbomni.bar" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "90GiB"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "service_type", "alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "disk_space", "90GiB"), + resource.TestCheckResourceAttr(resourceName, "disk_space_used", "90GiB"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + ), + }, + { + Config: testAccAlloyDBOmniResourceWithDiskSize(rName, "100GiB"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "service_type", "alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "disk_space", "100GiB"), + resource.TestCheckResourceAttr(resourceName, "disk_space_used", "100GiB"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + ), + }, + }, + }) +} + +func testAccAlloyDBOmniWithStaticIps(name string, count int) string { + return fmt.Sprintf(` +data "aiven_project" "foo" { + project = "%s" +} + +resource "aiven_static_ip" "ips" { + count = %d + project = data.aiven_project.foo.project + cloud_name = "google-europe-west1" +} + +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" + static_ips = toset(aiven_static_ip.ips[*].static_ip_address_id) + + alloydbomni_user_config { + static_ips = true + } +} + +data "aiven_alloydbomni" "common" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + + depends_on = [aiven_alloydbomni.bar] +}`, os.Getenv("AIVEN_PROJECT_NAME"), count, name) +} + +func testAccAlloyDBOmniResourceWithDiskSize(name, diskSize 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" + disk_space = "%s" + + alloydbomni_user_config { + public_access { + pg = true + prometheus = false + } + + pg { + idle_in_transaction_session_timeout = 900 + log_min_duration_statement = -1 + } + } +} + +data "aiven_alloydbomni" "common" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + + depends_on = [aiven_alloydbomni.bar] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name, diskSize) +} + +func testAccAlloyDBOmniResourceWithAdditionalDiskSize(name, diskSize 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" + additional_disk_space = "%s" + + alloydbomni_user_config { + public_access { + pg = true + prometheus = false + } + + pg { + idle_in_transaction_session_timeout = 900 + log_min_duration_statement = -1 + } + } +} + +data "aiven_alloydbomni" "common" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + + depends_on = [aiven_alloydbomni.bar] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name, diskSize) +} + +func testAccAlloyDBOmniResourceWithoutDiskSize(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" + + tag { + key = "test" + value = "val" + } + + alloydbomni_user_config { + public_access { + pg = true + prometheus = false + } + + pg { + idle_in_transaction_session_timeout = 900 + log_min_duration_statement = -1 + } + } +} + +data "aiven_alloydbomni" "common" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + + depends_on = [aiven_alloydbomni.bar] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name) +} + +func testAccAlloyDBOmniResourcePlanChange(name, plan 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 = "%s" + service_name = "test-acc-sr-%s" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" + + tag { + key = "test" + value = "val" + } + + alloydbomni_user_config { + public_access { + pg = true + prometheus = false + } + + pg { + idle_in_transaction_session_timeout = 900 + log_min_duration_statement = -1 + } + } +} + +data "aiven_alloydbomni" "common" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + + depends_on = [aiven_alloydbomni.bar] +}`, os.Getenv("AIVEN_PROJECT_NAME"), plan, name) +} + +func testAccAlloyDBOmniDoubleTagResource(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" + + tag { + key = "test" + value = "val" + } + tag { + key = "test" + value = "val2" + } + + alloydbomni_user_config { + public_access { + pg = true + prometheus = false + } + + pg { + idle_in_transaction_session_timeout = 900 + log_min_duration_statement = -1 + } + } +} + +data "aiven_alloydbomni" "common" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + + depends_on = [aiven_alloydbomni.bar] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name) +} + +// TestAccAivenAlloyDBOmni_admin_creds tests admin creds in user_config +func TestAccAivenAlloyDBOmni_admin_creds(t *testing.T) { + resourceName := "aiven_alloydbomni.alloydbomni" + prefix := "test-tf-acc-" + acctest.RandString(7) + project := os.Getenv("AIVEN_PROJECT_NAME") + expectedURLPrefix := fmt.Sprintf("postgres://root:%s-password", prefix) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniResourceAdminCreds(prefix, project), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith(resourceName, "service_uri", func(value string) error { + if !strings.HasPrefix(value, expectedURLPrefix) { + return fmt.Errorf("invalid service_uri, doesn't contain admin_username: %q", value) + } + return nil + }), + resource.TestCheckResourceAttr(resourceName, "alloydbomni_user_config.0.admin_username", "root"), + resource.TestCheckResourceAttr(resourceName, "alloydbomni_user_config.0.admin_password", prefix+"-password"), + ), + }, + }, + }) +} + +// testAccAlloyDBOmniResourceAdminCreds returns config TestAccAivenAlloyDBOmni_admin_creds +func testAccAlloyDBOmniResourceAdminCreds(prefix, project string) string { + return fmt.Sprintf(` +resource "aiven_alloydbomni" "alloydbomni" { + project = %[2]q + cloud_name = "google-europe-west1" + plan = "startup-4" + service_name = "%[1]s-alloydbomni" + + alloydbomni_user_config { + admin_username = "root" + admin_password = "%[1]s-password" + } +} + `, prefix, project) +} + +// AlloyDBOmni service tests +func TestAccAivenServiceAlloyDBOmni_basic(t *testing.T) { + resourceName := "aiven_alloydbomni.bar-alloydbomni" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniServiceResource(rName), + Check: resource.ComposeTestCheckFunc( + acc.TestAccCheckAivenServiceCommonAttributes("data.aiven_alloydbomni.common-alloydbomni"), + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common-alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "state", "RUNNING"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + ), + }, + }, + }) +} + +func TestAccAivenServiceAlloyDBOmni_termination_protection(t *testing.T) { + resourceName := "aiven_alloydbomni.bar-alloydbomni" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniTerminationProtectionServiceResource(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAivenServiceTerminationProtection("data.aiven_alloydbomni.common-alloydbomni"), + acc.TestAccCheckAivenServiceCommonAttributes("data.aiven_alloydbomni.common-alloydbomni"), + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common-alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "state", "RUNNING"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "true"), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAivenServiceAlloyDBOmni_read_replica(t *testing.T) { + resourceName := "aiven_alloydbomni.bar-alloydbomni" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniReadReplicaServiceResource(rName), + PreventPostDestroyRefresh: true, + Check: resource.ComposeTestCheckFunc( + acc.TestAccCheckAivenServiceCommonAttributes("data.aiven_alloydbomni.common-alloydbomni"), + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common-alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "state", "RUNNING"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + ), + }, + }, + }) +} + +func TestAccAivenServiceAlloyDBOmni_custom_timeouts(t *testing.T) { + resourceName := "aiven_alloydbomni.bar-alloydbomni" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniServiceCustomTimeoutsResource(rName), + Check: resource.ComposeTestCheckFunc( + acc.TestAccCheckAivenServiceCommonAttributes("data.aiven_alloydbomni.common-alloydbomni"), + testAccCheckAivenServiceAlloyDBOmniAttributes("data.aiven_alloydbomni.common-alloydbomni"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "cloud_name", "google-europe-west1"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_dow", "monday"), + resource.TestCheckResourceAttr(resourceName, "maintenance_window_time", "10:00:00"), + resource.TestCheckResourceAttr(resourceName, "state", "RUNNING"), + resource.TestCheckResourceAttr(resourceName, "termination_protection", "false"), + ), + }, + }, + }) +} + +func testAccAlloyDBOmniServiceResource(name string) string { + return fmt.Sprintf(` +data "aiven_project" "foo-alloydbomni" { + project = "%s" +} + +resource "aiven_alloydbomni" "bar-alloydbomni" { + project = data.aiven_project.foo-alloydbomni.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 + } + } +} + +data "aiven_alloydbomni" "common-alloydbomni" { + service_name = aiven_alloydbomni.bar-alloydbomni.service_name + project = aiven_alloydbomni.bar-alloydbomni.project + + depends_on = [aiven_alloydbomni.bar-alloydbomni] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name) +} + +func testAccAlloyDBOmniServiceCustomTimeoutsResource(name string) string { + return fmt.Sprintf(` +data "aiven_project" "foo-alloydbomni" { + project = "%s" +} + +resource "aiven_alloydbomni" "bar-alloydbomni" { + project = data.aiven_project.foo-alloydbomni.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" + + timeouts { + create = "25m" + update = "30m" + } + + alloydbomni_user_config { + public_access { + pg = true + prometheus = false + } + + pg { + idle_in_transaction_session_timeout = 900 + } + } +} + +data "aiven_alloydbomni" "common-alloydbomni" { + service_name = aiven_alloydbomni.bar-alloydbomni.service_name + project = aiven_alloydbomni.bar-alloydbomni.project + + depends_on = [aiven_alloydbomni.bar-alloydbomni] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name) +} + +func testAccAlloyDBOmniTerminationProtectionServiceResource(name string) string { + return fmt.Sprintf(` +data "aiven_project" "foo-alloydbomni" { + project = "%s" +} + +resource "aiven_alloydbomni" "bar-alloydbomni" { + project = data.aiven_project.foo-alloydbomni.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" + termination_protection = true + + alloydbomni_user_config { + public_access { + pg = true + prometheus = false + } + + pg { + idle_in_transaction_session_timeout = 900 + } + } +} + +data "aiven_alloydbomni" "common-alloydbomni" { + service_name = aiven_alloydbomni.bar-alloydbomni.service_name + project = aiven_alloydbomni.bar-alloydbomni.project + + depends_on = [aiven_alloydbomni.bar-alloydbomni] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name) +} + +func testAccAlloyDBOmniReadReplicaServiceResource(name string) string { + return fmt.Sprintf(` +data "aiven_project" "foo-alloydbomni" { + project = "%s" +} + +resource "aiven_alloydbomni" "bar-alloydbomni" { + project = data.aiven_project.foo-alloydbomni.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" "bar-replica" { + project = data.aiven_project.foo-alloydbomni.project + cloud_name = "google-europe-west1" + plan = "startup-4" + service_name = "test-acc-sr-repica-%s" + maintenance_window_dow = "monday" + maintenance_window_time = "10:00:00" + + alloydbomni_user_config { + backup_hour = 19 + backup_minute = 30 + public_access { + pg = true + prometheus = false + } + + pg { + idle_in_transaction_session_timeout = 900 + } + } + + service_integrations { + integration_type = "read_replica" + source_service_name = aiven_alloydbomni.bar-alloydbomni.service_name + } + + depends_on = [aiven_alloydbomni.bar-alloydbomni] +} + +resource "aiven_service_integration" "alloydbomni-readreplica" { + project = data.aiven_project.foo-alloydbomni.project + integration_type = "read_replica" + source_service_name = aiven_alloydbomni.bar-alloydbomni.service_name + destination_service_name = aiven_alloydbomni.bar-replica.service_name + + depends_on = [aiven_alloydbomni.bar-replica] +} + +data "aiven_alloydbomni" "common-alloydbomni" { + service_name = aiven_alloydbomni.bar-alloydbomni.service_name + project = aiven_alloydbomni.bar-alloydbomni.project + + depends_on = [aiven_alloydbomni.bar-alloydbomni] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name, name) +} + +func testAccCheckAivenServiceTerminationProtection(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + r := s.RootModule().Resources[n] + a := r.Primary.Attributes + + projectName, serviceName, err := schemautil.SplitResourceID2(a["id"]) + if err != nil { + return err + } + + c := acc.GetTestAivenClient() + + ctx := context.Background() + + service, err := c.Services.Get(ctx, projectName, serviceName) + if err != nil { + return fmt.Errorf("cannot get service %s err: %w", serviceName, err) + } + + if service.TerminationProtection == false { + return fmt.Errorf("expected to get a termination_protection=true from Aiven") + } + + // try to delete Aiven service with termination_protection enabled + // should be an error from Aiven API + err = c.Services.Delete(ctx, projectName, serviceName) + if err == nil { + return fmt.Errorf("termination_protection enabled should prevent from deletion of a service, deletion went OK") + } + + // set service termination_protection to false to make Terraform Destroy plan work + _, err = c.Services.Update( + ctx, + projectName, + service.Name, + aiven.UpdateServiceRequest{ + Cloud: service.CloudName, + MaintenanceWindow: &service.MaintenanceWindow, + Plan: service.Plan, + ProjectVPCID: service.ProjectVPCID, + Powered: true, + TerminationProtection: false, + UserConfig: service.UserConfig, + }, + ) + + if err != nil { + return fmt.Errorf("unable to update Aiven service to set termination_protection=false err: %w", err) + } + + return nil + } +} + +func testAccCheckAivenServiceAlloyDBOmniAttributes(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + r := s.RootModule().Resources[n] + a := r.Primary.Attributes + + if !strings.Contains(a["service_type"], "alloydbomni") { + return fmt.Errorf("expected to get a correct service_type from Aiven, got :%s", a["service_type"]) + } + + if a["alloydbomni_user_config.0.pg.0.idle_in_transaction_session_timeout"] != "900" { + return fmt.Errorf("expected to get a correct idle_in_transaction_session_timeout from Aiven") + } + + if a["alloydbomni_user_config.0.public_access.0.pg"] != "true" { + return fmt.Errorf("expected to get a correct public_access.alloydbomni from Aiven") + } + + if a["alloydbomni_user_config.0.public_access.0.pgbouncer"] != "false" { + return fmt.Errorf("expected to get a correct public_access.alloydbomnibouncer from Aiven") + } + + if a["alloydbomni_user_config.0.public_access.0.prometheus"] != "false" { + return fmt.Errorf("expected to get a correct public_access.prometheus from Aiven") + } + + if a["alloydbomni_user_config.0.service_to_fork_from"] != "" { + return fmt.Errorf("expected to get a service_to_fork_from not set to any value") + } + + if a["alloydbomni.0.uri"] == "" { + return fmt.Errorf("expected to get a correct uri from Aiven") + } + + if a["alloydbomni.0.uris.#"] == "" { + return fmt.Errorf("expected to get uris from Aiven") + } + + if a["alloydbomni.0.host"] == "" { + return fmt.Errorf("expected to get a correct host from Aiven") + } + + if a["alloydbomni.0.port"] == "" { + return fmt.Errorf("expected to get a correct port from Aiven") + } + + if a["alloydbomni.0.sslmode"] != "require" { + return fmt.Errorf("expected to get a correct sslmode from Aiven") + } + + if a["alloydbomni.0.user"] != "avnadmin" { + return fmt.Errorf("expected to get a correct user from Aiven") + } + + if a["alloydbomni.0.password"] == "" { + return fmt.Errorf("expected to get a correct password from Aiven") + } + + if a["alloydbomni.0.dbname"] != "defaultdb" { + return fmt.Errorf("expected to get a correct dbname from Aiven") + } + + if a["alloydbomni.0.params.#"] == "" { + return fmt.Errorf("expected to get params from Aiven") + } + + if a["alloydbomni.0.params.0.host"] == "" { + return fmt.Errorf("expected to get a correct host from Aiven") + } + + if a["alloydbomni.0.params.0.port"] == "" { + return fmt.Errorf("expected to get a correct port from Aiven") + } + + if a["alloydbomni.0.params.0.sslmode"] != "require" { + return fmt.Errorf("expected to get a correct sslmode from Aiven") + } + + if a["alloydbomni.0.params.0.user"] != "avnadmin" { + return fmt.Errorf("expected to get a correct user from Aiven") + } + + if a["alloydbomni.0.params.0.password"] == "" { + return fmt.Errorf("expected to get a correct password from Aiven") + } + + if a["alloydbomni.0.params.0.database_name"] != "defaultdb" { + return fmt.Errorf("expected to get a correct database_name from Aiven") + } + + if a["alloydbomni.0.max_connections"] != "100" && a["alloydbomni.0.max_connections"] != "200" { + return fmt.Errorf("expected to get a correct max_connections from Aiven") + } + + return nil + } +} diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_user.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_user.go new file mode 100644 index 000000000..d967e5e25 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_user.go @@ -0,0 +1,186 @@ +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" +) + +var aivenAlloyDBOmniUserSchema = map[string]*schema.Schema{ + "project": schemautil.CommonSchemaProjectReference, + "service_name": schemautil.CommonSchemaServiceNameReference, + "username": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: schemautil.GetServiceUserValidateFunc(), + Description: userconfig.Desc("The name of the service user for this service.").ForceNew().Referenced().Build(), + }, + "password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Computed: true, + DiffSuppressFunc: schemautil.EmptyObjectDiffSuppressFunc, + Description: "The password of the service user.", + }, + "alloydbomni_allow_replication": { + Type: schema.TypeBool, + Optional: true, + Description: "Allows replication. For the default avnadmin user this attribute is required and is always `true`.", + }, + + // computed fields + "type": { + Type: schema.TypeString, + Computed: true, + Description: "The service user account type, either primary or regular.", + }, + "access_cert": { + Type: schema.TypeString, + Sensitive: true, + Computed: true, + Description: "The access certificate for the servie user.", + }, + "access_key": { + Type: schema.TypeString, + Sensitive: true, + Computed: true, + Description: "The access certificate key for the service user.", + }, +} + +func ResourceAlloyDBOmniUser() *schema.Resource { + return &schema.Resource{ + Description: "Creates and manages an Aiven for AlloyDB Omni service user.", + CreateContext: common.WithGenClient(resourceAlloyDBOmniUserCreate), + UpdateContext: common.WithGenClient(resourceAlloyDBOmniUserUpdate), + ReadContext: common.WithGenClient(resourceAlloyDBOmniUserRead), + DeleteContext: common.WithGenClient(resourceServiceUserDelete), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: schemautil.DefaultResourceTimeouts(), + + Schema: aivenAlloyDBOmniUserSchema, + } +} + +func resourceAlloyDBOmniUserCreate(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + projectName := d.Get("project").(string) + serviceName := d.Get("service_name").(string) + username := d.Get("username").(string) + allowReplication := d.Get("alloydbomni_allow_replication").(bool) + _, err := client.ServiceUserCreate( + ctx, + projectName, + serviceName, + &service.ServiceUserCreateIn{ + Username: username, + AccessControl: &service.AccessControlIn{ + PgAllowReplication: &allowReplication, + }, + }, + ) + if err != nil { + return err + } + + if _, ok := d.GetOk("password"); ok { + _, err = client.ServiceUserCredentialsModify( + ctx, projectName, serviceName, username, + &service.ServiceUserCredentialsModifyIn{ + NewPassword: schemautil.OptionalStringPointer(d, "password"), + }, + ) + if err != nil { + return err + } + } + + d.SetId(schemautil.BuildResourceID(projectName, serviceName, username)) + return resourceAlloyDBOmniUserRead(ctx, d, client) +} + +func resourceAlloyDBOmniUserUpdate(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + projectName, serviceName, username, err := schemautil.SplitResourceID3(d.Id()) + if err != nil { + return err + } + + _, err = client.ServiceUserCredentialsModify( + ctx, projectName, serviceName, username, + &service.ServiceUserCredentialsModifyIn{ + Operation: service.OperationTypeResetCredentials, + NewPassword: schemautil.OptionalStringPointer(d, "password"), + }, + ) + + if err != nil { + return err + } + + if d.HasChange("alloydbomni_allow_replication") { + allowReplication := d.Get("alloydbomni_allow_replication").(bool) + _, err = client.ServiceUserCredentialsModify( + ctx, projectName, serviceName, username, + &service.ServiceUserCredentialsModifyIn{ + Operation: service.OperationTypeSetAccessControl, + AccessControl: &service.AccessControlIn{ + PgAllowReplication: &allowReplication, + }, + }, + ) + if err != nil { + return err + } + } + + return resourceAlloyDBOmniUserRead(ctx, d, client) +} + +func resourceAlloyDBOmniUserRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + projectName, serviceName, username, err := schemautil.SplitResourceID3(d.Id()) + if err != nil { + return err + } + + user, err := client.ServiceUserGet(ctx, projectName, serviceName, username) + if err != nil { + return schemautil.ResourceReadHandleNotFound(err, d) + } + + err = schemautil.ResourceDataSet(aivenAlloyDBOmniUserSchema, d, user) + if err != nil { + return err + } + + if user.AccessControl.PgAllowReplication != nil { + err = d.Set("alloydbomni_allow_replication", *user.AccessControl.PgAllowReplication) + if err != nil { + return err + } + } + + return nil +} + +func resourceServiceUserDelete(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + projectName, serviceName, username, err := schemautil.SplitResourceID3(d.Id()) + if err != nil { + return err + } + + err = client.ServiceUserDelete(ctx, projectName, serviceName, username) + if common.IsCritical(err) { + return err + } + + return nil +} diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_user_data_source.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_user_data_source.go new file mode 100644 index 000000000..638e3abe1 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_user_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 DatasourceAlloyDBOmniUser() *schema.Resource { + return &schema.Resource{ + ReadContext: common.WithGenClient(datasourceAlloyDBOmniUserRead), + Description: "Gets information about an Aiven for AlloyDB Omni service user.", + Schema: schemautil.ResourceSchemaAsDatasourceSchema(aivenAlloyDBOmniUserSchema, + "project", "service_name", "username"), + } +} + +func datasourceAlloyDBOmniUserRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + projectName := d.Get("project").(string) + serviceName := d.Get("service_name").(string) + userName := d.Get("username").(string) + + d.SetId(schemautil.BuildResourceID(projectName, serviceName, userName)) + return resourceAlloyDBOmniUserRead(ctx, d, client) +} diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_user_test.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_user_test.go new file mode 100644 index 000000000..91f076b10 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_user_test.go @@ -0,0 +1,307 @@ +package alloydbomni_test + +import ( + "context" + "errors" + "fmt" + "os" + "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 TestAccAivenAlloyDBOmniUser_basic(t *testing.T) { + resourceName := "aiven_alloydbomni_user.foo" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: testAccCheckAivenAlloyDBOmniUserResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniUserNewPasswordResource(rName), + Check: resource.ComposeTestCheckFunc( + schemautil.TestAccCheckAivenServiceUserAttributes("data.aiven_alloydbomni_user.user"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "username", fmt.Sprintf("user-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "password", "Test$1234"), + ), + }, + }, + }) +} + +func TestAccAivenAlloyDBOmniUser_alloydbomni_no_password(t *testing.T) { + resourceName := "aiven_alloydbomni_user.foo" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: testAccCheckAivenAlloyDBOmniUserResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniUserNoPasswordResource(rName), + Check: resource.ComposeTestCheckFunc( + schemautil.TestAccCheckAivenServiceUserAttributes("data.aiven_alloydbomni_user.user"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "project", os.Getenv("AIVEN_PROJECT_NAME")), + resource.TestCheckResourceAttr(resourceName, "username", fmt.Sprintf("user-%s", rName)), + ), + }, + }, + }) +} + +func TestAccAivenAlloyDBOmniUser_alloydbomni_replica(t *testing.T) { + resourceName := "aiven_alloydbomni_user.foo" + rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + project := os.Getenv("AIVEN_PROJECT_NAME") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: testAccCheckAivenAlloyDBOmniUserResourceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAlloyDBOmniUserPgReplicationResource(rName), + Check: resource.ComposeTestCheckFunc( + schemautil.TestAccCheckAivenServiceUserAttributes("data.aiven_alloydbomni_user.user"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "project", project), + resource.TestCheckResourceAttr(resourceName, "username", fmt.Sprintf("user-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "password", "Test$1234"), + resource.TestCheckResourceAttr(resourceName, "alloydbomni_allow_replication", "true"), + ), + }, + { + Config: testAccAlloyDBOmniUserPgReplicationDisableResource(rName), + Check: resource.ComposeTestCheckFunc( + schemautil.TestAccCheckAivenServiceUserAttributes("data.aiven_alloydbomni_user.user"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "project", project), + resource.TestCheckResourceAttr(resourceName, "username", fmt.Sprintf("user-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "password", "Test$1234"), + resource.TestCheckResourceAttr(resourceName, "alloydbomni_allow_replication", "false"), + ), + }, + { + Config: testAccAlloyDBOmniUserPgReplicationEnableResource(rName), + Check: resource.ComposeTestCheckFunc( + schemautil.TestAccCheckAivenServiceUserAttributes("data.aiven_alloydbomni_user.user"), + resource.TestCheckResourceAttr(resourceName, "service_name", fmt.Sprintf("test-acc-sr-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "project", project), + resource.TestCheckResourceAttr(resourceName, "username", fmt.Sprintf("user-%s", rName)), + resource.TestCheckResourceAttr(resourceName, "password", "Test$1234"), + resource.TestCheckResourceAttr(resourceName, "alloydbomni_allow_replication", "true"), + ), + }, + }, + }) +} + +func testAccCheckAivenAlloyDBOmniUserResourceDestroy(s *terraform.State) error { + c := acc.GetTestAivenClient() + + // loop through the resources in state, verifying each aiven_alloydbomni_user is destroyed + for _, rs := range s.RootModule().Resources { + if rs.Type != "aiven_alloydbomni_user" { + continue + } + + projectName, serviceName, username, err := schemautil.SplitResourceID3(rs.Primary.ID) + if err != nil { + return err + } + + ctx := context.Background() + + p, err := c.ServiceUsers.Get(ctx, projectName, serviceName, username) + if err != nil { + var e aiven.Error + if errors.As(err, &e) && e.Status != 404 { + return err + } + } + + if p != nil { + return fmt.Errorf("common user (%s) still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccAlloyDBOmniUserPgReplicationResource(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" +} + +resource "aiven_alloydbomni_user" "foo" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + username = "user-%s" + password = "Test$1234" + alloydbomni_allow_replication = true + + depends_on = [aiven_alloydbomni.bar] +} + +data "aiven_alloydbomni_user" "user" { + service_name = aiven_alloydbomni_user.foo.service_name + project = aiven_alloydbomni_user.foo.project + username = aiven_alloydbomni_user.foo.username + + depends_on = [aiven_alloydbomni_user.foo] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name, name) +} + +func testAccAlloyDBOmniUserPgReplicationDisableResource(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" +} + +resource "aiven_alloydbomni_user" "foo" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + username = "user-%s" + password = "Test$1234" + alloydbomni_allow_replication = false + + depends_on = [aiven_alloydbomni.bar] +} + +data "aiven_alloydbomni_user" "user" { + service_name = aiven_alloydbomni_user.foo.service_name + project = aiven_alloydbomni_user.foo.project + username = aiven_alloydbomni_user.foo.username + + depends_on = [aiven_alloydbomni_user.foo] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name, name) +} + +func testAccAlloyDBOmniUserPgReplicationEnableResource(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" +} + +resource "aiven_alloydbomni_user" "foo" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + username = "user-%s" + password = "Test$1234" + alloydbomni_allow_replication = true + + depends_on = [aiven_alloydbomni.bar] +} + +data "aiven_alloydbomni_user" "user" { + service_name = aiven_alloydbomni_user.foo.service_name + project = aiven_alloydbomni_user.foo.project + username = aiven_alloydbomni_user.foo.username + + depends_on = [aiven_alloydbomni_user.foo] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name, name) +} + +func testAccAlloyDBOmniUserNewPasswordResource(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" +} + +resource "aiven_alloydbomni_user" "foo" { + service_name = aiven_alloydbomni.bar.service_name + project = data.aiven_project.foo.project + username = "user-%s" + password = "Test$1234" + + depends_on = [aiven_alloydbomni.bar] +} + +data "aiven_alloydbomni_user" "user" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + username = aiven_alloydbomni_user.foo.username + + depends_on = [aiven_alloydbomni_user.foo] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name, name) +} + +func testAccAlloyDBOmniUserNoPasswordResource(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" +} + +resource "aiven_alloydbomni_user" "foo" { + service_name = aiven_alloydbomni.bar.service_name + project = data.aiven_project.foo.project + username = "user-%s" + + depends_on = [aiven_alloydbomni.bar] +} + +// check that we can use the password in template interpolations +output "use-template-interpolation" { + sensitive = true + value = "${aiven_alloydbomni_user.foo.password}/testing" +} + +data "aiven_alloydbomni_user" "user" { + service_name = aiven_alloydbomni.bar.service_name + project = aiven_alloydbomni.bar.project + username = aiven_alloydbomni_user.foo.username + + depends_on = [aiven_alloydbomni_user.foo] +}`, os.Getenv("AIVEN_PROJECT_NAME"), name, name) +} diff --git a/internal/sdkprovider/service/alloydbomni/sweep.go b/internal/sdkprovider/service/alloydbomni/sweep.go new file mode 100644 index 000000000..05cc5e099 --- /dev/null +++ b/internal/sdkprovider/service/alloydbomni/sweep.go @@ -0,0 +1,9 @@ +package alloydbomni + +import ( + "github.com/aiven/terraform-provider-aiven/internal/sweep" +) + +func init() { + sweep.AddServiceSweeper("alloydbomni") +} diff --git a/internal/sdkprovider/service/connectionpool/sweep.go b/internal/sdkprovider/service/connectionpool/sweep.go index 35f66160c..70ca6b5f8 100644 --- a/internal/sdkprovider/service/connectionpool/sweep.go +++ b/internal/sdkprovider/service/connectionpool/sweep.go @@ -37,7 +37,9 @@ func sweepConnectionPoll(ctx context.Context) func(string) error { } for _, s := range services { - if s.Type != schemautil.ServiceTypePG { + switch s.Type { + case schemautil.ServiceTypeAlloyDBOmni, schemautil.ServiceTypePG: + default: continue } diff --git a/internal/sdkprovider/userconfig/service/alloydbomni.go b/internal/sdkprovider/userconfig/service/alloydbomni.go new file mode 100644 index 000000000..1b7a40018 --- /dev/null +++ b/internal/sdkprovider/userconfig/service/alloydbomni.go @@ -0,0 +1,575 @@ +// Code generated by user config generator. DO NOT EDIT. + +package service + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/aiven/terraform-provider-aiven/internal/sdkprovider/userconfig/diff" +) + +func alloydbomniUserConfig() *schema.Schema { + return &schema.Schema{ + Description: "Alloydbomni user configurable settings. **Warning:** There's no way to reset advanced configuration options to default. Options that you add cannot be removed later", + DiffSuppressFunc: diff.SuppressUnchanged, + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "additional_backup_regions": { + Description: "Additional Cloud Regions for Backup Replication.", + Elem: &schema.Schema{ + Description: "Target cloud. Example: `aws-eu-central-1`.", + Type: schema.TypeString, + }, + MaxItems: 1, + Optional: true, + Type: schema.TypeList, + }, + "admin_password": { + Description: "Custom password for admin user. Defaults to random string. This must be set only when a new service is being created.", + ForceNew: true, + Optional: true, + Sensitive: true, + Type: schema.TypeString, + }, + "admin_username": { + Description: "Custom username for admin user. This must be set only when a new service is being created. Example: `avnadmin`.", + ForceNew: true, + Optional: true, + Type: schema.TypeString, + }, + "alloydbomni_version": { + Description: "Enum: `15`, and newer. PostgreSQL major version.", + Optional: true, + Type: schema.TypeString, + }, + "backup_hour": { + Description: "The hour of day (in UTC) when backup for the service is started. New backup is only started if previous backup has already completed. Example: `3`.", + Optional: true, + Type: schema.TypeInt, + }, + "backup_minute": { + Description: "The minute of an hour when backup for the service is started. New backup is only started if previous backup has already completed. Example: `30`.", + Optional: true, + Type: schema.TypeInt, + }, + "enable_ipv6": { + Description: "Register AAAA DNS records for the service, and allow IPv6 packets to service ports.", + Optional: true, + Type: schema.TypeBool, + }, + "google_columnar_engine_enabled": { + Description: "Enables or disables the columnar engine. When enabled, it accelerates SQL query processing. Default: `true`.", + Optional: true, + Type: schema.TypeBool, + }, + "google_columnar_engine_memory_size_percentage": { + Description: "Allocate the amount of RAM to store columnar data. Default: `10`.", + Optional: true, + Type: schema.TypeInt, + }, + "ip_filter": { + Deprecated: "Deprecated. Use `ip_filter_string` instead.", + Description: "Allow incoming connections from CIDR address block, e.g. `10.20.0.0/16`.", + Elem: &schema.Schema{ + Description: "CIDR address block, either as a string, or in a dict with an optional description field. Example: `10.20.0.0/16`.", + Type: schema.TypeString, + }, + MaxItems: 1024, + Optional: true, + Type: schema.TypeSet, + }, + "ip_filter_object": { + Description: "Allow incoming connections from CIDR address block, e.g. `10.20.0.0/16`", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "description": { + Description: "Description for IP filter list entry. Example: `Production service IP range`.", + Optional: true, + Type: schema.TypeString, + }, + "network": { + Description: "CIDR address block. Example: `10.20.0.0/16`.", + Required: true, + Type: schema.TypeString, + }, + }}, + MaxItems: 1024, + Optional: true, + Type: schema.TypeSet, + }, + "ip_filter_string": { + Description: "Allow incoming connections from CIDR address block, e.g. `10.20.0.0/16`.", + Elem: &schema.Schema{ + Description: "CIDR address block, either as a string, or in a dict with an optional description field. Example: `10.20.0.0/16`.", + Type: schema.TypeString, + }, + MaxItems: 1024, + Optional: true, + Type: schema.TypeSet, + }, + "pg": { + Description: "postgresql.conf configuration values", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "autovacuum_analyze_scale_factor": { + Description: "Specifies a fraction of the table size to add to autovacuum_analyze_threshold when deciding whether to trigger an ANALYZE. The default is 0.2 (20% of table size).", + Optional: true, + Type: schema.TypeFloat, + }, + "autovacuum_analyze_threshold": { + Description: "Specifies the minimum number of inserted, updated or deleted tuples needed to trigger an ANALYZE in any one table. The default is 50 tuples.", + Optional: true, + Type: schema.TypeInt, + }, + "autovacuum_freeze_max_age": { + Description: "Specifies the maximum age (in transactions) that a table's pg_class.relfrozenxid field can attain before a VACUUM operation is forced to prevent transaction ID wraparound within the table. Note that the system will launch autovacuum processes to prevent wraparound even when autovacuum is otherwise disabled. This parameter will cause the server to be restarted. Example: `200000000`.", + Optional: true, + Type: schema.TypeInt, + }, + "autovacuum_max_workers": { + Description: "Specifies the maximum number of autovacuum processes (other than the autovacuum launcher) that may be running at any one time. The default is three. This parameter can only be set at server start.", + Optional: true, + Type: schema.TypeInt, + }, + "autovacuum_naptime": { + Description: "Specifies the minimum delay between autovacuum runs on any given database. The delay is measured in seconds, and the default is one minute.", + Optional: true, + Type: schema.TypeInt, + }, + "autovacuum_vacuum_cost_delay": { + Description: "Specifies the cost delay value that will be used in automatic VACUUM operations. If -1 is specified, the regular vacuum_cost_delay value will be used. The default value is 20 milliseconds.", + Optional: true, + Type: schema.TypeInt, + }, + "autovacuum_vacuum_cost_limit": { + Description: "Specifies the cost limit value that will be used in automatic VACUUM operations. If -1 is specified (which is the default), the regular vacuum_cost_limit value will be used.", + Optional: true, + Type: schema.TypeInt, + }, + "autovacuum_vacuum_scale_factor": { + Description: "Specifies a fraction of the table size to add to autovacuum_vacuum_threshold when deciding whether to trigger a VACUUM. The default is 0.2 (20% of table size).", + Optional: true, + Type: schema.TypeFloat, + }, + "autovacuum_vacuum_threshold": { + Description: "Specifies the minimum number of updated or deleted tuples needed to trigger a VACUUM in any one table. The default is 50 tuples.", + Optional: true, + Type: schema.TypeInt, + }, + "bgwriter_delay": { + Description: "Specifies the delay between activity rounds for the background writer in milliseconds. Default is 200. Example: `200`.", + Optional: true, + Type: schema.TypeInt, + }, + "bgwriter_flush_after": { + Description: "Whenever more than bgwriter_flush_after bytes have been written by the background writer, attempt to force the OS to issue these writes to the underlying storage. Specified in kilobytes, default is 512. Setting of 0 disables forced writeback. Example: `512`.", + Optional: true, + Type: schema.TypeInt, + }, + "bgwriter_lru_maxpages": { + Description: "In each round, no more than this many buffers will be written by the background writer. Setting this to zero disables background writing. Default is 100. Example: `100`.", + Optional: true, + Type: schema.TypeInt, + }, + "bgwriter_lru_multiplier": { + Description: "The average recent need for new buffers is multiplied by bgwriter_lru_multiplier to arrive at an estimate of the number that will be needed during the next round, (up to bgwriter_lru_maxpages). 1.0 represents a “just in time” policy of writing exactly the number of buffers predicted to be needed. Larger values provide some cushion against spikes in demand, while smaller values intentionally leave writes to be done by server processes. The default is 2.0. Example: `2.0`.", + Optional: true, + Type: schema.TypeFloat, + }, + "deadlock_timeout": { + Description: "This is the amount of time, in milliseconds, to wait on a lock before checking to see if there is a deadlock condition. Example: `1000`.", + Optional: true, + Type: schema.TypeInt, + }, + "default_toast_compression": { + Description: "Enum: `lz4`, `pglz`. Specifies the default TOAST compression method for values of compressible columns (the default is lz4).", + Optional: true, + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"lz4", "pglz"}, false), + }, + "idle_in_transaction_session_timeout": { + Description: "Time out sessions with open transactions after this number of milliseconds.", + Optional: true, + Type: schema.TypeInt, + }, + "jit": { + Description: "Controls system-wide use of Just-in-Time Compilation (JIT).", + Optional: true, + Type: schema.TypeBool, + }, + "log_autovacuum_min_duration": { + Description: "Causes each action executed by autovacuum to be logged if it ran for at least the specified number of milliseconds. Setting this to zero logs all autovacuum actions. Minus-one (the default) disables logging autovacuum actions.", + Optional: true, + Type: schema.TypeInt, + }, + "log_error_verbosity": { + Description: "Enum: `DEFAULT`, `TERSE`, `VERBOSE`. Controls the amount of detail written in the server log for each message that is logged.", + Optional: true, + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"DEFAULT", "TERSE", "VERBOSE"}, false), + }, + "log_line_prefix": { + Description: "Enum: `'%m [%p] %q[user=%u,db=%d,app=%a] '`, `'%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '`, `'pid=%p,user=%u,db=%d,app=%a,client=%h '`, `'pid=%p,user=%u,db=%d,app=%a,client=%h,txid=%x,qid=%Q '`. Choose from one of the available log formats.", + Optional: true, + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"'%m [%p] %q[user=%u,db=%d,app=%a] '", "'%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '", "'pid=%p,user=%u,db=%d,app=%a,client=%h '", "'pid=%p,user=%u,db=%d,app=%a,client=%h,txid=%x,qid=%Q '"}, false), + }, + "log_min_duration_statement": { + Description: "Log statements that take more than this number of milliseconds to run, -1 disables.", + Optional: true, + Type: schema.TypeInt, + }, + "log_temp_files": { + Description: "Log statements for each temporary file created larger than this number of kilobytes, -1 disables.", + Optional: true, + Type: schema.TypeInt, + }, + "max_files_per_process": { + Description: "PostgreSQL maximum number of files that can be open per process.", + Optional: true, + Type: schema.TypeInt, + }, + "max_locks_per_transaction": { + Description: "PostgreSQL maximum locks per transaction.", + Optional: true, + Type: schema.TypeInt, + }, + "max_logical_replication_workers": { + Description: "PostgreSQL maximum logical replication workers (taken from the pool of max_parallel_workers).", + Optional: true, + Type: schema.TypeInt, + }, + "max_parallel_workers": { + Description: "Sets the maximum number of workers that the system can support for parallel queries.", + Optional: true, + Type: schema.TypeInt, + }, + "max_parallel_workers_per_gather": { + Description: "Sets the maximum number of workers that can be started by a single Gather or Gather Merge node.", + Optional: true, + Type: schema.TypeInt, + }, + "max_pred_locks_per_transaction": { + Description: "PostgreSQL maximum predicate locks per transaction.", + Optional: true, + Type: schema.TypeInt, + }, + "max_prepared_transactions": { + Description: "PostgreSQL maximum prepared transactions.", + Optional: true, + Type: schema.TypeInt, + }, + "max_replication_slots": { + Description: "PostgreSQL maximum replication slots.", + Optional: true, + Type: schema.TypeInt, + }, + "max_slot_wal_keep_size": { + Description: "PostgreSQL maximum WAL size (MB) reserved for replication slots. Default is -1 (unlimited). wal_keep_size minimum WAL size setting takes precedence over this.", + Optional: true, + Type: schema.TypeInt, + }, + "max_stack_depth": { + Description: "Maximum depth of the stack in bytes.", + Optional: true, + Type: schema.TypeInt, + }, + "max_standby_archive_delay": { + Description: "Max standby archive delay in milliseconds.", + Optional: true, + Type: schema.TypeInt, + }, + "max_standby_streaming_delay": { + Description: "Max standby streaming delay in milliseconds.", + Optional: true, + Type: schema.TypeInt, + }, + "max_wal_senders": { + Description: "PostgreSQL maximum WAL senders.", + Optional: true, + Type: schema.TypeInt, + }, + "max_worker_processes": { + Description: "Sets the maximum number of background processes that the system can support.", + Optional: true, + Type: schema.TypeInt, + }, + "pg_partman_bgw__dot__interval": { + Description: "Sets the time interval to run pg_partman's scheduled tasks. Example: `3600`.", + Optional: true, + Type: schema.TypeInt, + }, + "pg_partman_bgw__dot__role": { + Description: "Controls which role to use for pg_partman's scheduled background tasks. Example: `myrolename`.", + Optional: true, + Type: schema.TypeString, + }, + "pg_stat_statements__dot__track": { + Description: "Enum: `all`, `none`, `top`. Controls which statements are counted. Specify top to track top-level statements (those issued directly by clients), all to also track nested statements (such as statements invoked within functions), or none to disable statement statistics collection. The default value is top.", + Optional: true, + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"all", "none", "top"}, false), + }, + "temp_file_limit": { + Description: "PostgreSQL temporary file limit in KiB, -1 for unlimited. Example: `5000000`.", + Optional: true, + Type: schema.TypeInt, + }, + "timezone": { + Description: "PostgreSQL service timezone. Example: `Europe/Helsinki`.", + Optional: true, + Type: schema.TypeString, + }, + "track_activity_query_size": { + Description: "Specifies the number of bytes reserved to track the currently executing command for each active session. Example: `1024`.", + Optional: true, + Type: schema.TypeInt, + }, + "track_commit_timestamp": { + Description: "Enum: `off`, `on`. Record commit time of transactions.", + Optional: true, + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"off", "on"}, false), + }, + "track_functions": { + Description: "Enum: `all`, `none`, `pl`. Enables tracking of function call counts and time used.", + Optional: true, + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"all", "none", "pl"}, false), + }, + "track_io_timing": { + Description: "Enum: `off`, `on`. Enables timing of database I/O calls. This parameter is off by default, because it will repeatedly query the operating system for the current time, which may cause significant overhead on some platforms.", + Optional: true, + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"off", "on"}, false), + }, + "wal_sender_timeout": { + Description: "Terminate replication connections that are inactive for longer than this amount of time, in milliseconds. Setting this value to zero disables the timeout. Example: `60000`.", + Optional: true, + Type: schema.TypeInt, + }, + "wal_writer_delay": { + Description: "WAL flush interval in milliseconds. Note that setting this value to lower than the default 200ms may negatively impact performance. Example: `50`.", + Optional: true, + Type: schema.TypeInt, + }, + }}, + MaxItems: 1, + Optional: true, + Type: schema.TypeList, + }, + "pg_read_replica": { + Description: "Should the service which is being forked be a read replica (deprecated, use read_replica service integration instead).", + Optional: true, + Type: schema.TypeBool, + }, + "pg_service_to_fork_from": { + Description: "Name of the PG Service from which to fork (deprecated, use service_to_fork_from). This has effect only when a new service is being created. Example: `anotherservicename`.", + ForceNew: true, + Optional: true, + Type: schema.TypeString, + }, + "pg_version": { + Description: "Enum: `15`, and newer. PostgreSQL major version.", + Optional: true, + Type: schema.TypeString, + }, + "pgbouncer": { + Description: "PGBouncer connection pooling settings", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "autodb_idle_timeout": { + Description: "If the automatically created database pools have been unused this many seconds, they are freed. If 0 then timeout is disabled. (seconds). Default: `3600`.", + Optional: true, + Type: schema.TypeInt, + }, + "autodb_max_db_connections": { + Description: "Do not allow more than this many server connections per database (regardless of user). Setting it to 0 means unlimited. Example: `0`.", + Optional: true, + Type: schema.TypeInt, + }, + "autodb_pool_mode": { + Description: "Enum: `session`, `statement`, `transaction`. PGBouncer pool mode. Default: `transaction`.", + Optional: true, + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"session", "statement", "transaction"}, false), + }, + "autodb_pool_size": { + Description: "If non-zero then create automatically a pool of that size per user when a pool doesn't exist. Default: `0`.", + Optional: true, + Type: schema.TypeInt, + }, + "ignore_startup_parameters": { + Description: "List of parameters to ignore when given in startup packet.", + Elem: &schema.Schema{ + Description: "Enum: `extra_float_digits`, `search_path`.", + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"extra_float_digits", "search_path"}, false), + }, + MaxItems: 32, + Optional: true, + Type: schema.TypeList, + }, + "max_prepared_statements": { + Description: "PgBouncer tracks protocol-level named prepared statements related commands sent by the client in transaction and statement pooling modes when max_prepared_statements is set to a non-zero value. Setting it to 0 disables prepared statements. max_prepared_statements defaults to 100, and its maximum is 3000. Default: `100`.", + Optional: true, + Type: schema.TypeInt, + }, + "min_pool_size": { + Description: "Add more server connections to pool if below this number. Improves behavior when usual load comes suddenly back after period of total inactivity. The value is effectively capped at the pool size. Default: `0`.", + Optional: true, + Type: schema.TypeInt, + }, + "server_idle_timeout": { + Description: "If a server connection has been idle more than this many seconds it will be dropped. If 0 then timeout is disabled. (seconds). Default: `600`.", + Optional: true, + Type: schema.TypeInt, + }, + "server_lifetime": { + Description: "The pooler will close an unused server connection that has been connected longer than this. (seconds). Default: `3600`.", + Optional: true, + Type: schema.TypeInt, + }, + "server_reset_query_always": { + Description: "Run server_reset_query (DISCARD ALL) in all pooling modes. Default: `false`.", + Optional: true, + Type: schema.TypeBool, + }, + }}, + MaxItems: 1, + Optional: true, + Type: schema.TypeList, + }, + "pglookout": { + Description: "System-wide settings for pglookout", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{"max_failover_replication_time_lag": { + Description: "Number of seconds of master unavailability before triggering database failover to standby. Default: `60`.", + Optional: true, + Type: schema.TypeInt, + }}}, + MaxItems: 1, + Optional: true, + Type: schema.TypeList, + }, + "private_access": { + Description: "Allow access to selected service ports from private networks", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "pg": { + Description: "Allow clients to connect to pg with a DNS name that always resolves to the service's private IP addresses. Only available in certain network locations.", + Optional: true, + Type: schema.TypeBool, + }, + "pgbouncer": { + Description: "Allow clients to connect to pgbouncer with a DNS name that always resolves to the service's private IP addresses. Only available in certain network locations.", + Optional: true, + Type: schema.TypeBool, + }, + "prometheus": { + Description: "Allow clients to connect to prometheus with a DNS name that always resolves to the service's private IP addresses. Only available in certain network locations.", + Optional: true, + Type: schema.TypeBool, + }, + }}, + MaxItems: 1, + Optional: true, + Type: schema.TypeList, + }, + "privatelink_access": { + Description: "Allow access to selected service components through Privatelink", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "pg": { + Description: "Enable pg.", + Optional: true, + Type: schema.TypeBool, + }, + "pgbouncer": { + Description: "Enable pgbouncer.", + Optional: true, + Type: schema.TypeBool, + }, + "prometheus": { + Description: "Enable prometheus.", + Optional: true, + Type: schema.TypeBool, + }, + }}, + MaxItems: 1, + Optional: true, + Type: schema.TypeList, + }, + "project_to_fork_from": { + Description: "Name of another project to fork a service from. This has effect only when a new service is being created. Example: `anotherprojectname`.", + ForceNew: true, + Optional: true, + Type: schema.TypeString, + }, + "public_access": { + Description: "Allow access to selected service ports from the public Internet", + Elem: &schema.Resource{Schema: map[string]*schema.Schema{ + "pg": { + Description: "Allow clients to connect to pg from the public internet for service nodes that are in a project VPC or another type of private network.", + Optional: true, + Type: schema.TypeBool, + }, + "pgbouncer": { + Description: "Allow clients to connect to pgbouncer from the public internet for service nodes that are in a project VPC or another type of private network.", + Optional: true, + Type: schema.TypeBool, + }, + "prometheus": { + Description: "Allow clients to connect to prometheus from the public internet for service nodes that are in a project VPC or another type of private network.", + Optional: true, + Type: schema.TypeBool, + }, + }}, + MaxItems: 1, + Optional: true, + Type: schema.TypeList, + }, + "recovery_target_time": { + Description: "Recovery target time when forking a service. This has effect only when a new service is being created. Example: `2019-01-01 23:34:45`.", + ForceNew: true, + Optional: true, + Type: schema.TypeString, + }, + "service_log": { + Description: "Store logs for the service so that they are available in the HTTP API and console.", + Optional: true, + Type: schema.TypeBool, + }, + "service_to_fork_from": { + Description: "Name of another service to fork from. This has effect only when a new service is being created. Example: `anotherservicename`.", + ForceNew: true, + Optional: true, + Type: schema.TypeString, + }, + "shared_buffers_percentage": { + Description: "Percentage of total RAM that the database server uses for shared memory buffers. Valid range is 20-60 (float), which corresponds to 20% - 60%. This setting adjusts the shared_buffers configuration value. Example: `41.5`.", + Optional: true, + Type: schema.TypeFloat, + }, + "static_ips": { + Description: "Use static public IP addresses.", + Optional: true, + Type: schema.TypeBool, + }, + "synchronous_replication": { + Description: "Enum: `off`, `quorum`. Synchronous replication type. Note that the service plan also needs to support synchronous replication.", + Optional: true, + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"off", "quorum"}, false), + }, + "variant": { + Description: "Enum: `aiven`, `timescale`. Variant of the PostgreSQL service, may affect the features that are exposed by default.", + Optional: true, + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice([]string{"aiven", "timescale"}, false), + }, + "work_mem": { + Description: "Sets the maximum amount of memory to be used by a query operation (such as a sort or hash table) before writing to temporary disk files, in MB. Default is 1MB + 0.075% of total RAM (up to 32MB). Example: `4`.", + Optional: true, + Type: schema.TypeInt, + }, + }}, + MaxItems: 1, + Optional: true, + Type: schema.TypeList, + } +} diff --git a/internal/sdkprovider/userconfig/service/service.go b/internal/sdkprovider/userconfig/service/service.go index ab12ae029..201e538a6 100644 --- a/internal/sdkprovider/userconfig/service/service.go +++ b/internal/sdkprovider/userconfig/service/service.go @@ -6,6 +6,8 @@ import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" func GetUserConfig(kind string) *schema.Schema { switch kind { + case "alloydbomni": + return alloydbomniUserConfig() case "cassandra": return cassandraUserConfig() case "clickhouse": @@ -48,6 +50,13 @@ func GetUserConfig(kind string) *schema.Schema { // GetFieldMapping returns TF fields to Json fields mapping (in unix-path way) func GetFieldMapping(kind string) map[string]string { return map[string]map[string]string{ + "alloydbomni": { + "ip_filter_object": "ip_filter", + "ip_filter_string": "ip_filter", + "pg/pg_partman_bgw__dot__interval": "pg/pg_partman_bgw.interval", + "pg/pg_partman_bgw__dot__role": "pg/pg_partman_bgw.role", + "pg/pg_stat_statements__dot__track": "pg/pg_stat_statements.track", + }, "cassandra": { "ip_filter_object": "ip_filter", "ip_filter_string": "ip_filter", @@ -138,5 +147,5 @@ func GetFieldMapping(kind string) map[string]string { }[kind] } func UserConfigTypes() []string { - return []string{"cassandra", "clickhouse", "dragonfly", "flink", "grafana", "influxdb", "kafka", "kafka_connect", "kafka_mirrormaker", "m3aggregator", "m3db", "mysql", "opensearch", "pg", "redis", "thanos", "valkey"} + return []string{"alloydbomni", "cassandra", "clickhouse", "dragonfly", "flink", "grafana", "influxdb", "kafka", "kafka_connect", "kafka_mirrormaker", "m3aggregator", "m3db", "mysql", "opensearch", "pg", "redis", "thanos", "valkey"} }