diff --git a/docs/data-sources/database_uri.md b/docs/data-sources/database_uri.md index 71960a89b..e90ca4a13 100644 --- a/docs/data-sources/database_uri.md +++ b/docs/data-sources/database_uri.md @@ -5,7 +5,7 @@ description: |- Fetch Exoscale Database https://community.exoscale.com/documentation/dbaas/ connection URI data. This data source returns database conection details of the default (admin) user only. URI parts are also available individually for convenience. - Corresponding resource: exoscale_database ../resources/database.md. + Corresponding resource: exoscale_dbaas ../resources/database.md. --- # exoscale_database_uri (Data Source) diff --git a/docs/resources/database.md b/docs/resources/database.md index e115d23b2..f8d6477c6 100644 --- a/docs/resources/database.md +++ b/docs/resources/database.md @@ -3,11 +3,13 @@ page_title: "exoscale_database Resource - terraform-provider-exoscale" subcategory: "" description: |- + ❗This resource is deprecated and renamed to exoscale_dbaas, do not use it to create new resources❗ Manage Exoscale Database Services (DBaaS) https://community.exoscale.com/documentation/dbaas/. --- # exoscale_database (Resource) +❗This resource is deprecated and renamed to exoscale_dbaas, do not use it to create new resources❗ Manage Exoscale [Database Services (DBaaS)](https://community.exoscale.com/documentation/dbaas/). ## Example Usage diff --git a/docs/resources/dbaas.md b/docs/resources/dbaas.md new file mode 100644 index 000000000..1c29debbb --- /dev/null +++ b/docs/resources/dbaas.md @@ -0,0 +1,173 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "exoscale_dbaas Resource - terraform-provider-exoscale" +subcategory: "" +description: |- + Manage Exoscale Database Services (DBaaS) https://community.exoscale.com/documentation/dbaas/. +--- + +# exoscale_dbaas (Resource) + +Manage Exoscale [Database Services (DBaaS)](https://community.exoscale.com/documentation/dbaas/). + + + + +## Schema + +### Required + +- `name` (String) ❗ The name of the database service. +- `plan` (String) The plan of the database service (use the [Exoscale CLI](https://github.com/exoscale/cli/) - `exo dbaas type show --plans` - for reference). +- `type` (String) ❗ The type of the database service (`kafka`, `mysql`, `opensearch`, `pg`, `redis`, `grafana`). +- `zone` (String) ❗ The Exoscale [Zone](https://www.exoscale.com/datacenters/) name. + +### Optional + +- `grafana` (Block, Optional) *grafana* database service type specific arguments. Structure is documented below. (see [below for nested schema](#nestedblock--grafana)) +- `kafka` (Block, Optional) *kafka* database service type specific arguments. Structure is documented below. (see [below for nested schema](#nestedblock--kafka)) +- `maintenance_dow` (String) The day of week to perform the automated database service maintenance (`never`, `monday`, `tuesday`, `wednesday`, `thursday`, `friday`, `saturday`, `sunday`). +- `maintenance_time` (String) The time of day to perform the automated database service maintenance (`HH:MM:SS`) +- `mysql` (Block, Optional) *mysql* database service type specific arguments. Structure is documented below. (see [below for nested schema](#nestedblock--mysql)) +- `opensearch` (Block, Optional) *opensearch* database service type specific arguments. Structure is documented below. (see [below for nested schema](#nestedblock--opensearch)) +- `pg` (Block, Optional) *pg* database service type specific arguments. Structure is documented below. (see [below for nested schema](#nestedblock--pg)) +- `redis` (Block, Optional) *redis* database service type specific arguments. Structure is documented below. (see [below for nested schema](#nestedblock--redis)) +- `termination_protection` (Boolean) The database service protection boolean flag against termination/power-off. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `ca_certificate` (String) CA Certificate required to reach a DBaaS service through a TLS-protected connection. +- `created_at` (String) The creation date of the database service. +- `disk_size` (Number) The disk size of the database service. +- `id` (String) The ID of this resource. +- `node_cpus` (Number) The number of CPUs of the database service. +- `node_memory` (Number) The amount of memory of the database service. +- `nodes` (Number) The number of nodes of the database service. +- `state` (String) The current state of the database service. +- `updated_at` (String) The date of the latest database service update. + + +### Nested Schema for `grafana` + +Optional: + +- `grafana_settings` (String) Grafana configuration settings in JSON format (`exo dbaas type show grafana --settings=grafana` for reference). +- `ip_filter` (Set of String) A list of CIDR blocks to allow incoming connections from. + + + +### Nested Schema for `kafka` + +Optional: + +- `enable_cert_auth` (Boolean) Enable certificate-based authentication method. +- `enable_kafka_connect` (Boolean) Enable Kafka Connect. +- `enable_kafka_rest` (Boolean) Enable Kafka REST. +- `enable_sasl_auth` (Boolean) Enable SASL-based authentication method. +- `enable_schema_registry` (Boolean) Enable Schema Registry. +- `ip_filter` (Set of String) A list of CIDR blocks to allow incoming connections from. +- `kafka_connect_settings` (String) Kafka Connect configuration settings in JSON format (`exo dbaas type show kafka --settings=kafka-connect` for reference). +- `kafka_rest_settings` (String) Kafka REST configuration settings in JSON format (`exo dbaas type show kafka --settings=kafka-rest` for reference). +- `kafka_settings` (String) Kafka configuration settings in JSON format (`exo dbaas type show kafka --settings=kafka` for reference). +- `schema_registry_settings` (String) Schema Registry configuration settings in JSON format (`exo dbaas type show kafka --settings=schema-registry` for reference) +- `version` (String) Kafka major version (`exo dbaas type show kafka` for reference; may only be set at creation time). + + + +### Nested Schema for `mysql` + +Optional: + +- `admin_password` (String, Sensitive) A custom administrator account password (may only be set at creation time). +- `admin_username` (String) A custom administrator account username (may only be set at creation time). +- `backup_schedule` (String) The automated backup schedule (`HH:MM`). +- `ip_filter` (Set of String) A list of CIDR blocks to allow incoming connections from. +- `mysql_settings` (String) MySQL configuration settings in JSON format (`exo dbaas type show mysql --settings=mysql` for reference). +- `version` (String) MySQL major version (`exo dbaas type show mysql` for reference; may only be set at creation time). + + + +### Nested Schema for `opensearch` + +Optional: + +- `dashboards` (Block, Optional) OpenSearch Dashboards settings (see [below for nested schema](#nestedblock--opensearch--dashboards)) +- `fork_from_service` (String) ❗ Service name +- `index_pattern` (Block List) (can be used multiple times) Allows you to create glob style patterns and set a max number of indexes matching this pattern you want to keep. Creating indexes exceeding this value will cause the oldest one to get deleted. You could for example create a pattern looking like 'logs.?' and then create index logs.1, logs.2 etc, it will delete logs.1 once you create logs.6. Do note 'logs.?' does not apply to logs.10. Note: Setting max_index_count to 0 will do nothing and the pattern gets ignored. (see [below for nested schema](#nestedblock--opensearch--index_pattern)) +- `index_template` (Block, Optional) Template settings for all new indexes (see [below for nested schema](#nestedblock--opensearch--index_template)) +- `ip_filter` (Set of String) Allow incoming connections from this list of CIDR address block, e.g. `["10.20.0.0/16"] +- `keep_index_refresh_interval` (Boolean) Aiven automation resets index.refresh_interval to default value for every index to be sure that indices are always visible to search. If it doesn't fit your case, you can disable this by setting up this flag to true. +- `max_index_count` (Number) Maximum number of indexes to keep (Minimum value is `0`) +- `recovery_backup_name` (String) ❗ Name of a backup to recover from +- `settings` (String) OpenSearch-specific settings, in json. e.g.`jsonencode({thread_pool_search_size: 64})`. Use `exo x get-dbaas-settings-opensearch` to get a list of available settings. +- `version` (String) ❗ OpenSearch major version (`exo dbaas type show opensearch` for reference) + + +### Nested Schema for `opensearch.dashboards` + +Optional: + +- `enabled` (Boolean) Enable or disable OpenSearch Dashboards (default: true). +- `max_old_space_size` (Number) Limits the maximum amount of memory (in MiB) the OpenSearch Dashboards process can use. This sets the max_old_space_size option of the nodejs running the OpenSearch Dashboards. Note: the memory reserved by OpenSearch Dashboards is not available for OpenSearch. (default: 128). +- `request_timeout` (Number) Timeout in milliseconds for requests made by OpenSearch Dashboards towards OpenSearch (default: 30000) + + + +### Nested Schema for `opensearch.index_pattern` + +Optional: + +- `max_index_count` (Number) Maximum number of indexes to keep before deleting the oldest one (Minimum value is `0`) +- `pattern` (String) fnmatch pattern +- `sorting_algorithm` (String) `alphabetical` or `creation_date`. + + + +### Nested Schema for `opensearch.index_template` + +Optional: + +- `mapping_nested_objects_limit` (Number) The maximum number of nested JSON objects that a single document can contain across all nested types. This limit helps to prevent out of memory errors when a document contains too many nested objects. (Default is 10000. Minimum value is `0`, maximum value is `100000`.) +- `number_of_replicas` (Number) The number of replicas each primary shard has. (Minimum value is `0`, maximum value is `29`) +- `number_of_shards` (Number) The number of primary shards that an index should have. (Minimum value is `1`, maximum value is `1024`.) + + + + +### Nested Schema for `pg` + +Optional: + +- `admin_password` (String, Sensitive) A custom administrator account password (may only be set at creation time). +- `admin_username` (String) A custom administrator account username (may only be set at creation time). +- `backup_schedule` (String) The automated backup schedule (`HH:MM`). +- `ip_filter` (Set of String) A list of CIDR blocks to allow incoming connections from. +- `pg_settings` (String) PostgreSQL configuration settings in JSON format (`exo dbaas type show pg --settings=pg` for reference). +- `pgbouncer_settings` (String) PgBouncer configuration settings in JSON format (`exo dbaas type show pg --settings=pgbouncer` for reference). +- `pglookout_settings` (String) pglookout configuration settings in JSON format (`exo dbaas type show pg --settings=pglookout` for reference). +- `version` (String) PostgreSQL major version (`exo dbaas type show pg` for reference; may only be set at creation time). + + + +### Nested Schema for `redis` + +Optional: + +- `ip_filter` (Set of String) A list of CIDR blocks to allow incoming connections from. +- `redis_settings` (String) Redis configuration settings in JSON format (`exo dbaas type show redis --settings=redis` for reference). + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + +-> The symbol ❗ in an attribute indicates that modifying it, will force the creation of a new resource. + + diff --git a/docs/resources/dbaas_mysql_database.md b/docs/resources/dbaas_mysql_database.md new file mode 100644 index 000000000..592da15ff --- /dev/null +++ b/docs/resources/dbaas_mysql_database.md @@ -0,0 +1,44 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "exoscale_dbaas_mysql_database Resource - terraform-provider-exoscale" +subcategory: "" +description: |- + ❗ Manage service database for a PostgreSQL Exoscale Database Services (DBaaS) https://community.exoscale.com/documentation/dbaas/. +--- + +# exoscale_dbaas_mysql_database (Resource) + +❗ Manage service database for a PostgreSQL Exoscale [Database Services (DBaaS)](https://community.exoscale.com/documentation/dbaas/). + + + + +## Schema + +### Required + +- `database_name` (String) ❗ The name of the database for this service. +- `service` (String) ❗ The name of the database service. +- `zone` (String) ❗ The Exoscale [Zone](https://www.exoscale.com/datacenters/) name. + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The ID of this resource, computed as service/database_name + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + +-> The symbol ❗ in an attribute indicates that modifying it, will force the creation of a new resource. + + diff --git a/docs/resources/dbaas_pg_database.md b/docs/resources/dbaas_pg_database.md new file mode 100644 index 000000000..920f73e32 --- /dev/null +++ b/docs/resources/dbaas_pg_database.md @@ -0,0 +1,46 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "exoscale_dbaas_pg_database Resource - terraform-provider-exoscale" +subcategory: "" +description: |- + ❗ Manage service database for a PostgreSQL Exoscale Database Services (DBaaS) https://community.exoscale.com/documentation/dbaas/. +--- + +# exoscale_dbaas_pg_database (Resource) + +❗ Manage service database for a PostgreSQL Exoscale [Database Services (DBaaS)](https://community.exoscale.com/documentation/dbaas/). + + + + +## Schema + +### Required + +- `database_name` (String) ❗ The name of the database for this service. +- `service` (String) ❗ The name of the database service. +- `zone` (String) ❗ The Exoscale [Zone](https://www.exoscale.com/datacenters/) name. + +### Optional + +- `lc_collate` (String) Default string sort order (LC_COLLATE) for PostgreSQL database +- `lc_ctype` (String) Default character classification (LC_CTYPE) for PostgreSQL database +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The ID of this resource, computed as service/database_name + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + +-> The symbol ❗ in an attribute indicates that modifying it, will force the creation of a new resource. + + diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index b6051818a..5ff949ebc 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -217,11 +217,14 @@ func (p *ExoscaleProvider) DataSources(ctx context.Context) []func() datasource. func (p *ExoscaleProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ - database.NewResource, + database.DeprecatedNewResource, + database.NewServiceResource, database.NewMysqlUserResource, database.NewKafkaUserResource, database.NewOpensearchUserResource, database.NewPGUserResource, + database.NewPGDatabaseResource, + database.NewMysqlDatabaseResource, iam.NewResourceOrgPolicy, iam.NewResourceRole, iam.NewResourceAPIKey, diff --git a/pkg/resources/database/datasource_uri.go b/pkg/resources/database/datasource_uri.go index 0eaf6a166..dbb6b6e2f 100644 --- a/pkg/resources/database/datasource_uri.go +++ b/pkg/resources/database/datasource_uri.go @@ -27,7 +27,7 @@ This data source returns database conection details of the default (admin) user URI parts are also available individually for convenience. -Corresponding resource: [exoscale_database](../resources/database.md).` +Corresponding resource: [exoscale_dbaas](../resources/database.md).` // Ensure provider defined types fully satisfy framework interfaces. var _ datasource.DataSourceWithConfigure = &DataSourceURI{} @@ -214,12 +214,12 @@ polling: return service, nil } -// waitForDBAASServiceReadyForUsers polls the database service until it is ready to accept user creation -func waitForDBAASServiceReadyForUsers[T any]( +// waitForDBAASServiceReadyForFn polls the database service until dbReady evaluates to true +func waitForDBAASServiceReadyForFn[T any]( ctx context.Context, getService func(context.Context, string) (*T, error), serviceName string, - usersReady func(*T) bool, + dbReadyFn func(*T) bool, ) (*T, error) { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() @@ -233,8 +233,8 @@ polling: return nil, fmt.Errorf("error polling service status: %w", err) } - usersReady := usersReady(service) - if usersReady { + dbReady := dbReadyFn(service) + if dbReady { break polling } diff --git a/pkg/resources/database/datasource_uri_test.go b/pkg/resources/database/datasource_uri_test.go index fe9798f3f..8a6a3c437 100644 --- a/pkg/resources/database/datasource_uri_test.go +++ b/pkg/resources/database/datasource_uri_test.go @@ -30,7 +30,7 @@ func testDataSourceURI(t *testing.T) { pgUsername := acctest.RandomWithPrefix(testutils.TestUsername) data := DataSourceURIModel{ ResourceName: "test", - Name: "exoscale_database.test.name", + Name: "exoscale_dbaas.test.name", Zone: testutils.TestZoneName, } diff --git a/pkg/resources/database/resource_db.go b/pkg/resources/database/resource_db.go new file mode 100644 index 000000000..5c9b08213 --- /dev/null +++ b/pkg/resources/database/resource_db.go @@ -0,0 +1,21 @@ +package database + +import ( + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/types" + + v3 "github.com/exoscale/egoscale/v3" +) + +type DBResource struct { + client *v3.Client +} + +type DBResourceModel struct { + Id types.String `tfsdk:"id"` + Service types.String `tfsdk:"service"` + DatabaseName types.String `tfsdk:"database_name"` + Zone types.String `tfsdk:"zone"` + + Timeouts timeouts.Value `tfsdk:"timeouts"` +} diff --git a/pkg/resources/database/resource_db_mysql.go b/pkg/resources/database/resource_db_mysql.go new file mode 100644 index 000000000..7b9331d66 --- /dev/null +++ b/pkg/resources/database/resource_db_mysql.go @@ -0,0 +1,286 @@ +package database + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + + v3 "github.com/exoscale/egoscale/v3" + "github.com/exoscale/terraform-provider-exoscale/pkg/config" + providerConfig "github.com/exoscale/terraform-provider-exoscale/pkg/provider/config" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +type MysqlDatabaseResource struct { + DBResource +} + +type MysqlDatabaseResourceModel struct { + DBResourceModel +} + +var _ resource.Resource = &MysqlDatabaseResource{} +var _ resource.ResourceWithImportState = &MysqlDatabaseResource{} + +func NewMysqlDatabaseResource() resource.Resource { + return &MysqlDatabaseResource{} +} + +func (r *MysqlDatabaseResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + r.client = req.ProviderData.(*providerConfig.ExoscaleProviderConfig).ClientV3 +} + +// ImportState implements resource.ResourceWithImportState. +func (p *MysqlDatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + + idParts := strings.Split(req.ID, "@") + + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: service/database_name@zone. Got: %q", req.ID), + ) + + return + } + + databaseID := idParts[0] + zone := idParts[1] + + id := strings.Split(databaseID, "/") + + if len(id) != 2 || id[0] == "" || id[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: service/database_name@zone. Got: %q", req.ID), + ) + } + + serviceName := id[0] + databaseName := id[1] + + var data MysqlDatabaseResourceModel + + // Set timeouts (quirk https://github.com/hashicorp/terraform-plugin-framework-timeouts/issues/46) + var timeouts timeouts.Value + resp.Diagnostics.Append(resp.State.GetAttribute(ctx, path.Root("timeouts"), &timeouts)...) + if resp.Diagnostics.HasError() { + return + } + data.Timeouts = timeouts + + data.Id = types.StringValue(databaseID) + data.DatabaseName = types.StringValue(databaseName) + data.Service = types.StringValue(serviceName) + data.Zone = types.StringValue(zone) + + ReadResourceForImport(ctx, req, resp, &data, p.client) +} + +// Create implements resource.Resource. +func (p *MysqlDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data MysqlDatabaseResourceModel + CreateResource(ctx, req, resp, &data, p.client) +} + +// Delete implements resource.Resource. +func (p *MysqlDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data MysqlDatabaseResourceModel + DeleteResource(ctx, req, resp, &data, p.client) +} + +// Metadata implements resource.Resource. +func (p *MysqlDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dbaas_mysql_database" +} + +// Read implements resource.Resource. +func (p *MysqlDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data MysqlDatabaseResourceModel + ReadResource(ctx, req, resp, &data, p.client) + +} + +// Schema implements resource.Resource. +func (p *MysqlDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + + MarkdownDescription: "❗ Manage service database for a PostgreSQL Exoscale [Database Services (DBaaS)](https://community.exoscale.com/documentation/dbaas/).", + Attributes: map[string]schema.Attribute{ + // Computed attributes + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of this resource, computed as service/database_name", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + // Attributes referencing the service + "service": schema.StringAttribute{ + Required: true, + MarkdownDescription: "❗ The name of the database service.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "zone": schema.StringAttribute{ + MarkdownDescription: "❗ The Exoscale [Zone](https://www.exoscale.com/datacenters/) name.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(config.Zones...), + }, + }, + // Variables + "database_name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "❗ The name of the database for this service.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.BlockAll(ctx), + }, + } +} + +// Update implements resource.Resource. +func (p *MysqlDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var stateData, planData MysqlDatabaseResourceModel + UpdateResource(ctx, req, resp, &stateData, &planData, p.client) +} + +// ReadResource reads resource from remote and populate the model accordingly +func (data MysqlDatabaseResourceModel) ReadResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + svc, err := client.GetDBAASServiceMysql(ctx, data.Service.ValueString()) + if err != nil { + diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read service mysql, got error: %s", err)) + return + } + + for _, db := range svc.Databases { + if string(db) == data.DatabaseName.ValueString() { + return + } + } + diagnostics.AddError("Client Error", "Unable to find database for the service") +} + +// CreateResource creates the resource according to the model, and then +// update computed fields if applicable +func (data MysqlDatabaseResourceModel) CreateResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + + createRequest := v3.CreateDBAASMysqlDatabaseRequest{ + DatabaseName: v3.DBAASDatabaseName(data.DatabaseName.ValueString()), + } + + op, err := client.CreateDBAASMysqlDatabase(ctx, data.Service.ValueString(), createRequest) + if err != nil { + diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to create service database, got error %s", err.Error()), + ) + return + } + + if _, err := client.Wait(ctx, op, v3.OperationStateSuccess); err != nil { + diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to create service database, got error %s", err.Error()), + ) + return + } + + svc, err := client.GetDBAASServiceMysql(ctx, data.Service.ValueString()) + if err != nil { + diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read database service pg, got error: %s", err)) + return + } + + for _, db := range svc.Databases { + if string(db) == data.DatabaseName.ValueString() { + return + } + } + diagnostics.AddError("Client Error", "Unable to find newly created database for the service") +} + +// DeleteResource deletes the resource +func (data MysqlDatabaseResourceModel) DeleteResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + + op, err := client.DeleteDBAASMysqlDatabase(ctx, data.Service.ValueString(), data.DatabaseName.ValueString()) + if err != nil { + diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to delete service database, got error %s", err.Error()), + ) + return + } + _, err = client.Wait(ctx, op, v3.OperationStateSuccess) + if err != nil { + diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to delete service database, got error %s", err.Error()), + ) + return + } + +} + +// UpdateResource updates the remote resource w/ the new model +func (MysqlDatabaseResourceModel) UpdateResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + // Nothing to do as these resources are systematically recreated +} + +func (data MysqlDatabaseResourceModel) WaitForService(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + _, err := waitForDBAASServiceReadyForFn(ctx, client.GetDBAASServiceMysql, data.Service.ValueString(), func(t *v3.DBAASServiceMysql) bool { return t.State == v3.EnumServiceStateRunning }) + // DbaaS API is unstable when a service goes from rebuilding from running, + // this wait time helps avoid that + time.Sleep(time.Second * 10) + if err != nil { + diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Database service Mysql %s", err.Error())) + } +} + +// Accessing and setting attributes +func (data *MysqlDatabaseResourceModel) GetTimeouts() timeouts.Value { + return data.Timeouts +} + +func (data *MysqlDatabaseResourceModel) SetTimeouts(t timeouts.Value) { + data.Timeouts = t +} + +func (data *MysqlDatabaseResourceModel) GetID() basetypes.StringValue { + return data.Id +} + +func (data *MysqlDatabaseResourceModel) GetZone() basetypes.StringValue { + return data.Zone +} + +// Should set the return value of .GetID() to service/database_name +func (data *MysqlDatabaseResourceModel) GenerateID() { + data.Id = basetypes.NewStringValue(fmt.Sprintf("%s/%s", data.Service.ValueString(), data.DatabaseName.ValueString())) +} diff --git a/pkg/resources/database/resource_db_pg.go b/pkg/resources/database/resource_db_pg.go new file mode 100644 index 000000000..660088ad8 --- /dev/null +++ b/pkg/resources/database/resource_db_pg.go @@ -0,0 +1,310 @@ +package database + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + + v3 "github.com/exoscale/egoscale/v3" + "github.com/exoscale/terraform-provider-exoscale/pkg/config" + providerConfig "github.com/exoscale/terraform-provider-exoscale/pkg/provider/config" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +type PGDatabaseResource struct { + DBResource +} + +type PGDatabaseResourceModel struct { + DBResourceModel + LcCtype types.String `tfsdk:"lc_ctype"` + LcCollate types.String `tfsdk:"lc_collate"` +} + +var _ resource.Resource = &PGDatabaseResource{} +var _ resource.ResourceWithImportState = &PGDatabaseResource{} + +func NewPGDatabaseResource() resource.Resource { + return &PGDatabaseResource{} +} + +func (r *PGDatabaseResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + r.client = req.ProviderData.(*providerConfig.ExoscaleProviderConfig).ClientV3 +} + +// ImportState implements resource.ResourceWithImportState. +func (p *PGDatabaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + + idParts := strings.Split(req.ID, "@") + + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: service/database_name@zone. Got: %q", req.ID), + ) + + return + } + + databaseID := idParts[0] + zone := idParts[1] + + id := strings.Split(databaseID, "/") + + if len(id) != 2 || id[0] == "" || id[1] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: service/database_name@zone. Got: %q", req.ID), + ) + } + + serviceName := id[0] + databaseName := id[1] + + var data PGDatabaseResourceModel + + // Set timeouts (quirk https://github.com/hashicorp/terraform-plugin-framework-timeouts/issues/46) + var timeouts timeouts.Value + resp.Diagnostics.Append(resp.State.GetAttribute(ctx, path.Root("timeouts"), &timeouts)...) + if resp.Diagnostics.HasError() { + return + } + data.Timeouts = timeouts + + data.Id = types.StringValue(databaseID) + data.DatabaseName = types.StringValue(databaseName) + data.Service = types.StringValue(serviceName) + data.Zone = types.StringValue(zone) + + ReadResourceForImport(ctx, req, resp, &data, p.client) +} + +// Create implements resource.Resource. +func (p *PGDatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data PGDatabaseResourceModel + CreateResource(ctx, req, resp, &data, p.client) +} + +// Delete implements resource.Resource. +func (p *PGDatabaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data PGDatabaseResourceModel + DeleteResource(ctx, req, resp, &data, p.client) +} + +// Metadata implements resource.Resource. +func (p *PGDatabaseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dbaas_pg_database" +} + +// Read implements resource.Resource. +func (p *PGDatabaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data PGDatabaseResourceModel + ReadResource(ctx, req, resp, &data, p.client) + +} + +// Schema implements resource.Resource. +func (p *PGDatabaseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + + MarkdownDescription: "❗ Manage service database for a PostgreSQL Exoscale [Database Services (DBaaS)](https://community.exoscale.com/documentation/dbaas/).", + Attributes: map[string]schema.Attribute{ + // Computed attributes + "id": schema.StringAttribute{ + MarkdownDescription: "The ID of this resource, computed as service/database_name", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + // Attributes referencing the service + "service": schema.StringAttribute{ + Required: true, + MarkdownDescription: "❗ The name of the database service.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "zone": schema.StringAttribute{ + MarkdownDescription: "❗ The Exoscale [Zone](https://www.exoscale.com/datacenters/) name.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(config.Zones...), + }, + }, + // Variables + "database_name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "❗ The name of the database for this service.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "lc_collate": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Default string sort order (LC_COLLATE) for PostgreSQL database", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "lc_ctype": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Default character classification (LC_CTYPE) for PostgreSQL database", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.BlockAll(ctx), + }, + } +} + +// Update implements resource.Resource. +func (p *PGDatabaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var stateData, planData PGDatabaseResourceModel + UpdateResource(ctx, req, resp, &stateData, &planData, p.client) +} + +// ReadResource reads resource from remote and populate the model accordingly +func (data PGDatabaseResourceModel) ReadResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + svc, err := client.GetDBAASServicePG(ctx, data.Service.ValueString()) + if err != nil { + diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read service pg, got error: %s", err)) + return + } + + for _, db := range svc.Databases { + if string(db) == data.DatabaseName.ValueString() { + return + } + } + diagnostics.AddError("Client Error", "Unable to find database for the service") +} + +// CreateResource creates the resource according to the model, and then +// update computed fields if applicable +func (data PGDatabaseResourceModel) CreateResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + + createRequest := v3.CreateDBAASPGDatabaseRequest{ + DatabaseName: v3.DBAASDatabaseName(data.DatabaseName.ValueString()), + } + + if !data.LcCollate.IsNull() { + createRequest.LCCollate = data.LcCtype.ValueString() + } + if !data.LcCtype.IsNull() { + createRequest.LCCollate = data.LcCollate.ValueString() + } + + op, err := client.CreateDBAASPGDatabase(ctx, data.Service.ValueString(), createRequest) + if err != nil { + diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to create service database, got error %s", err.Error()), + ) + return + } + + if _, err := client.Wait(ctx, op, v3.OperationStateSuccess); err != nil { + diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to create service database, got error %s", err.Error()), + ) + return + } + + svc, err := client.GetDBAASServicePG(ctx, data.Service.ValueString()) + if err != nil { + diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read database service pg, got error: %s", err)) + return + } + + for _, db := range svc.Databases { + if string(db) == data.DatabaseName.ValueString() { + return + } + } + diagnostics.AddError("Client Error", "Unable to find newly created database for the service") +} + +// DeleteResource deletes the resource +func (data PGDatabaseResourceModel) DeleteResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + + op, err := client.DeleteDBAASPGDatabase(ctx, data.Service.ValueString(), data.DatabaseName.ValueString()) + if err != nil { + diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to delete service database, got error %s", err.Error()), + ) + return + } + _, err = client.Wait(ctx, op, v3.OperationStateSuccess) + if err != nil { + diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to delete service database, got error %s", err.Error()), + ) + return + } + +} + +// UpdateResource updates the remote resource w/ the new model +func (PGDatabaseResourceModel) UpdateResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + // Nothing to do as these resources are systematically recreated +} + +func (data PGDatabaseResourceModel) WaitForService(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + _, err := waitForDBAASServiceReadyForFn(ctx, client.GetDBAASServicePG, data.Service.ValueString(), func(t *v3.DBAASServicePG) bool { return t.State == v3.EnumServiceStateRunning }) + // DbaaS API is unstable when a service goes from rebuilding from running, + // this wait time helps avoid that + time.Sleep(time.Second * 10) + + if err != nil { + diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Database service PG %s", err.Error())) + } +} + +// Accessing and setting attributes +func (data *PGDatabaseResourceModel) GetTimeouts() timeouts.Value { + return data.Timeouts +} + +func (data *PGDatabaseResourceModel) SetTimeouts(t timeouts.Value) { + data.Timeouts = t +} + +func (data *PGDatabaseResourceModel) GetID() basetypes.StringValue { + return data.Id +} + +func (data *PGDatabaseResourceModel) GetZone() basetypes.StringValue { + return data.Zone +} + +// Should set the return value of .GetID() to service/database_name +func (data *PGDatabaseResourceModel) GenerateID() { + data.Id = basetypes.NewStringValue(fmt.Sprintf("%s/%s", data.Service.ValueString(), data.DatabaseName.ValueString())) +} diff --git a/pkg/resources/database/resource.go b/pkg/resources/database/resource_service.go similarity index 83% rename from pkg/resources/database/resource.go rename to pkg/resources/database/resource_service.go index cdb636cb2..b9c26cdfa 100644 --- a/pkg/resources/database/resource.go +++ b/pkg/resources/database/resource_service.go @@ -26,21 +26,21 @@ import ( ) // Ensure provider defined types fully satisfy framework interfaces. -var _ resource.Resource = &Resource{} -var _ resource.ResourceWithImportState = &Resource{} +var _ resource.Resource = &ServiceResource{} +var _ resource.ResourceWithImportState = &ServiceResource{} -func NewResource() resource.Resource { - return &Resource{} +func NewServiceResource() resource.Resource { + return &ServiceResource{} } -// Resource defines the DBaaS Service resource implementation. -type Resource struct { +// ServiceResource defines the DBaaS Service resource implementation. +type ServiceResource struct { client *exoscale.Client env string } -// ResourceModel describes the generic DBaaS Service resource data model. -type ResourceModel struct { +// ServiceResourceModel describes the generic DBaaS Service resource data model. +type ServiceResourceModel struct { Id types.String `tfsdk:"id"` CreatedAt types.String `tfsdk:"created_at"` DiskSize types.Int64 `tfsdk:"disk_size"` @@ -68,11 +68,11 @@ type ResourceModel struct { Timeouts timeouts.Value `tfsdk:"timeouts"` } -func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_database" +func (r *ServiceResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_dbaas" } -func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *ServiceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Manage Exoscale [Database Services (DBaaS)](https://community.exoscale.com/documentation/dbaas/).", Attributes: map[string]schema.Attribute{ @@ -223,7 +223,7 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp } } -func (r *Resource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *ServiceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { if req.ProviderData == nil { return } @@ -232,7 +232,7 @@ func (r *Resource) Configure(ctx context.Context, req resource.ConfigureRequest, r.env = req.ProviderData.(*providerConfig.ExoscaleProviderConfig).Environment } -func (r *Resource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { +func (r *ServiceResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { return map[int64]resource.StateUpgrader{ // SDKv2 to Framework migration requires state upgrade in database blocks // to remove array from database blocks. @@ -326,7 +326,7 @@ func (r *Resource) UpgradeState(ctx context.Context) map[int64]resource.StateUpg return } - upgradedStateData := ResourceModel{ + upgradedStateData := ServiceResourceModel{ Id: priorState.Id, CreatedAt: priorState.CreatedAt, DiskSize: priorState.DiskSize, @@ -370,7 +370,7 @@ func (r *Resource) UpgradeState(ctx context.Context) map[int64]resource.StateUpg } } -func (r *Resource) ValidateConfig( +func (r *ServiceResource) ValidateConfig( ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse, @@ -402,8 +402,8 @@ func (r *Resource) ValidateConfig( } } -func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data ResourceModel +func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ServiceResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) @@ -449,8 +449,8 @@ func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp }) } -func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data ResourceModel +func (r *ServiceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data ServiceResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -496,8 +496,8 @@ func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *res }) } -func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var stateData, planData ResourceModel +func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var stateData, planData ServiceResourceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...) @@ -544,8 +544,8 @@ func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp }) } -func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data ResourceModel +func (r *ServiceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data ServiceResourceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -575,7 +575,7 @@ func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp }) } -func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { +func (r *ServiceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, "@") if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { @@ -586,7 +586,7 @@ func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequ return } - var data ResourceModel + var data ServiceResourceModel // Set timeouts (quirk https://github.com/hashicorp/terraform-plugin-framework-timeouts/issues/46) var timeouts timeouts.Value @@ -623,3 +623,61 @@ func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequ "id": data.Id, }) } + +// We're renaming `exoscale_dbaas` to `exoscale_dbaas“ +type DeprecatedServiceResource struct { + Resource *ServiceResource +} + +func (d *DeprecatedServiceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + d.Resource.Create(ctx, req, resp) +} + +func (d *DeprecatedServiceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + d.Resource.Delete(ctx, req, resp) +} + +func (d *DeprecatedServiceResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + d.Resource.Metadata(ctx, req, resp) + resp.TypeName = req.ProviderTypeName + "_database" +} + +func (d *DeprecatedServiceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + d.Resource.Read(ctx, req, resp) +} + +func (d *DeprecatedServiceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + d.Resource.Schema(ctx, req, resp) + resp.Schema.DeprecationMessage = "This resource is renamed to exoscale_dbaas, reimport it after renaming it" + resp.Schema.MarkdownDescription = "❗This resource is deprecated and renamed to exoscale_dbaas, do not use it to create new resources❗\n" + resp.Schema.MarkdownDescription +} + +func (d *DeprecatedServiceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + d.Resource.Update(ctx, req, resp) +} + +func (r *DeprecatedServiceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.Resource.Configure(ctx, req, resp) +} + +func (r *DeprecatedServiceResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader { + return r.Resource.UpgradeState(ctx) +} + +func (r *DeprecatedServiceResource) ValidateConfig( + ctx context.Context, + req resource.ValidateConfigRequest, + resp *resource.ValidateConfigResponse, +) { + r.Resource.ValidateConfig(ctx, req, resp) +} + +func (r *DeprecatedServiceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.Resource.ImportState(ctx, req, resp) +} + +func DeprecatedNewResource() resource.Resource { + return &DeprecatedServiceResource{ + Resource: &ServiceResource{}, + } +} diff --git a/pkg/resources/database/resource_grafana.go b/pkg/resources/database/resource_service_grafana.go similarity index 95% rename from pkg/resources/database/resource_grafana.go rename to pkg/resources/database/resource_service_grafana.go index f125cc852..7e0defde2 100644 --- a/pkg/resources/database/resource_grafana.go +++ b/pkg/resources/database/resource_service_grafana.go @@ -44,7 +44,7 @@ var ResourceGrafanaSchema = schema.SingleNestedBlock{ } // createGrafana function handles Grafana specific part of database resource creation logic. -func (r *Resource) createGrafana(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) createGrafana(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { service := oapi.CreateDbaasServiceGrafanaJSONRequestBody{ Plan: data.Plan.ValueString(), TerminationProtection: data.TerminationProtection.ValueBoolPointer(), @@ -112,7 +112,7 @@ func (r *Resource) createGrafana(ctx context.Context, data *ResourceModel, diagn // readGrafana function handles Grafana specific part of database resource Read logic. // It is used in the dedicated Read action but also as a finishing step of Create, Update and Import. -func (r *Resource) readGrafana(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) readGrafana(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { caCert, err := r.client.GetDatabaseCACertificate(ctx, data.Zone.ValueString()) if err != nil { diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get CA Certificate: %s", err)) @@ -179,7 +179,7 @@ func (r *Resource) readGrafana(ctx context.Context, data *ResourceModel, diagnos } // updateGrafana function handles Grafana specific part of database resource Update logic. -func (r *Resource) updateGrafana(ctx context.Context, stateData *ResourceModel, planData *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) updateGrafana(ctx context.Context, stateData *ServiceResourceModel, planData *ServiceResourceModel, diagnostics *diag.Diagnostics) { var updated bool service := oapi.UpdateDbaasServiceGrafanaJSONRequestBody{} diff --git a/pkg/resources/database/resource_grafana_test.go b/pkg/resources/database/resource_service_grafana_test.go similarity index 99% rename from pkg/resources/database/resource_grafana_test.go rename to pkg/resources/database/resource_service_grafana_test.go index 21b0d8f94..4f00b010c 100644 --- a/pkg/resources/database/resource_grafana_test.go +++ b/pkg/resources/database/resource_service_grafana_test.go @@ -44,7 +44,7 @@ func testResourceGrafana(t *testing.T) { t.Fatal(err) } - fullResourceName := "exoscale_database.test" + fullResourceName := "exoscale_dbaas.test" dataBase := TemplateModelGrafana{ ResourceName: "test", Name: acctest.RandomWithPrefix(testutils.Prefix), diff --git a/pkg/resources/database/resource_kafka.go b/pkg/resources/database/resource_service_kafka.go similarity index 97% rename from pkg/resources/database/resource_kafka.go rename to pkg/resources/database/resource_service_kafka.go index c115ef850..f22af50d7 100644 --- a/pkg/resources/database/resource_kafka.go +++ b/pkg/resources/database/resource_service_kafka.go @@ -99,7 +99,7 @@ var ResourceKafkaSchema = schema.SingleNestedBlock{ } // createKafka function handles Kafka specific part of database resource creation logic. -func (r *Resource) createKafka(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) createKafka(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { service := oapi.CreateDbaasServiceKafkaJSONRequestBody{ Plan: data.Plan.ValueString(), TerminationProtection: data.TerminationProtection.ValueBoolPointer(), @@ -210,7 +210,7 @@ func (r *Resource) createKafka(ctx context.Context, data *ResourceModel, diagnos // readKafka function handles Kafka specific part of database resource Read logic. // It is used in the dedicated Read action but also as a finishing step of Create, Update and Import. -func (r *Resource) readKafka(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) readKafka(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { caCert, err := r.client.GetDatabaseCACertificate(ctx, data.Zone.ValueString()) if err != nil { diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get CA Certificate: %s", err)) @@ -323,7 +323,7 @@ func (r *Resource) readKafka(ctx context.Context, data *ResourceModel, diagnosti } // updateKafka function handles Kafka specific part of database resource Update logic. -func (r *Resource) updateKafka(ctx context.Context, stateData *ResourceModel, planData *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) updateKafka(ctx context.Context, stateData *ServiceResourceModel, planData *ServiceResourceModel, diagnostics *diag.Diagnostics) { var updated bool service := oapi.UpdateDbaasServiceKafkaJSONRequestBody{} diff --git a/pkg/resources/database/resource_kafka_test.go b/pkg/resources/database/resource_service_kafka_test.go similarity index 99% rename from pkg/resources/database/resource_kafka_test.go rename to pkg/resources/database/resource_service_kafka_test.go index 1252d3f1f..f3f5c3b9f 100644 --- a/pkg/resources/database/resource_kafka_test.go +++ b/pkg/resources/database/resource_service_kafka_test.go @@ -72,7 +72,7 @@ func testResourceKafka(t *testing.T) { t.Fatal(err) } - serviceFullResourceName := "exoscale_database.test" + serviceFullResourceName := "exoscale_dbaas.test" serviceDataBase := TemplateModelKafka{ ResourceName: "test", Name: acctest.RandomWithPrefix(testutils.Prefix), diff --git a/pkg/resources/database/resource_mysql.go b/pkg/resources/database/resource_service_mysql.go similarity index 96% rename from pkg/resources/database/resource_mysql.go rename to pkg/resources/database/resource_service_mysql.go index 70a8915de..8cc585352 100644 --- a/pkg/resources/database/resource_mysql.go +++ b/pkg/resources/database/resource_service_mysql.go @@ -68,7 +68,7 @@ var ResourceMysqlSchema = schema.SingleNestedBlock{ } // createMysql function handles MySQL specific part of database resource creation logic. -func (r *Resource) createMysql(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) createMysql(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { mysqlData := &ResourceMysqlModel{} if data.Mysql != nil { mysqlData = data.Mysql @@ -168,7 +168,7 @@ func (r *Resource) createMysql(ctx context.Context, data *ResourceModel, diagnos // readMysql function handles MySQL specific part of database resource Read logic. // It is used in the dedicated Read action but also as a finishing step of Create, Update and Import. -func (r *Resource) readMysql(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) readMysql(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { caCert, err := r.client.GetDatabaseCACertificate(ctx, data.Zone.ValueString()) if err != nil { diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get CA Certificate: %s", err)) @@ -247,7 +247,7 @@ func (r *Resource) readMysql(ctx context.Context, data *ResourceModel, diagnosti } // updateMysql function handles MySQL specific part of database resource Update logic. -func (r *Resource) updateMysql(ctx context.Context, stateData *ResourceModel, planData *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) updateMysql(ctx context.Context, stateData *ServiceResourceModel, planData *ServiceResourceModel, diagnostics *diag.Diagnostics) { var updated bool service := oapi.UpdateDbaasServiceMysqlJSONRequestBody{} diff --git a/pkg/resources/database/resource_mysql_test.go b/pkg/resources/database/resource_service_mysql_test.go similarity index 76% rename from pkg/resources/database/resource_mysql_test.go rename to pkg/resources/database/resource_service_mysql_test.go index 8a2891064..3b8a8dd24 100644 --- a/pkg/resources/database/resource_mysql_test.go +++ b/pkg/resources/database/resource_service_mysql_test.go @@ -55,6 +55,14 @@ type TemplateModelMysqlUser struct { Authentication string } +type TemplateModelMysqlDb struct { + ResourceName string + + DatabaseName string + Service string + Zone string +} + func testResourceMysql(t *testing.T) { serviceTpl, err := template.ParseFiles("testdata/resource_mysql.tmpl") if err != nil { @@ -64,8 +72,12 @@ func testResourceMysql(t *testing.T) { if err != nil { t.Fatal(err) } + dbTpl, err := template.ParseFiles("testdata/resource_database_mysql.tmpl") + if err != nil { + t.Fatal(err) + } - serviceFullResourceName := "exoscale_database.test" + serviceFullResourceName := "exoscale_dbaas.test" serviceDataBase := TemplateModelMysql{ ResourceName: "test", Name: acctest.RandomWithPrefix(testutils.Prefix), @@ -83,6 +95,13 @@ func testResourceMysql(t *testing.T) { Service: fmt.Sprintf("%s.name", serviceFullResourceName), } + dbFullResourceName := "exoscale_dbaas_mysql_database.test_database" + dbDataBase := TemplateModelMysqlDb{ + ResourceName: "test_database", + DatabaseName: "foo_db", + Zone: serviceDataBase.Zone, + Service: fmt.Sprintf("%s.name", serviceFullResourceName), + } serviceDataCreate := serviceDataBase serviceDataCreate.MaintenanceDow = "monday" serviceDataCreate.MaintenanceTime = "01:23:00" @@ -93,6 +112,8 @@ func testResourceMysql(t *testing.T) { userDataCreate := userDataBase userDataCreate.Authentication = "caching_sha2_password" + dbDataCreate := dbDataBase + buf := &bytes.Buffer{} err = serviceTpl.Execute(buf, &serviceDataCreate) if err != nil { @@ -102,6 +123,10 @@ func testResourceMysql(t *testing.T) { if err != nil { t.Fatal(err) } + err = dbTpl.Execute(buf, &dbDataCreate) + if err != nil { + t.Fatal(err) + } configCreate := buf.String() serviceDataUpdate := serviceDataBase @@ -114,6 +139,9 @@ func testResourceMysql(t *testing.T) { userDataUpdate := userDataBase userDataUpdate.Authentication = "mysql_native_password" + dbDataUpdate := dbDataBase + dbDataUpdate.DatabaseName = "bar_db" + buf = &bytes.Buffer{} err = serviceTpl.Execute(buf, &serviceDataUpdate) if err != nil { @@ -123,6 +151,10 @@ func testResourceMysql(t *testing.T) { if err != nil { t.Fatal(err) } + err = dbTpl.Execute(buf, &dbDataUpdate) + if err != nil { + t.Fatal(err) + } configUpdate := buf.String() resource.Test(t, resource.TestCase{ @@ -163,6 +195,16 @@ func testResourceMysql(t *testing.T) { return nil }, + + // Database + func(s *terraform.State) error { + err := CheckExistsMysqlDatabase(serviceDataBase.Name, dbDataCreate.DatabaseName, &dbDataCreate) + if err != nil { + return err + } + + return nil + }, ), }, { @@ -190,6 +232,22 @@ func testResourceMysql(t *testing.T) { return nil }, + + // Database + func(s *terraform.State) error { + // Check the old database was deleted + err := CheckExistsMysqlDatabase(serviceDataBase.Name, dbDataBase.DatabaseName, &dbDataUpdate) + if err == nil { + return fmt.Errorf("expected to not find database %s", dbDataBase.DatabaseName) + } + + // Check the new user exists + err = CheckExistsMysqlDatabase(serviceDataBase.Name, dbDataUpdate.DatabaseName, &dbDataUpdate) + if err != nil { + return err + } + return nil + }, ), }, { @@ -214,6 +272,16 @@ func testResourceMysql(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + { + ResourceName: dbFullResourceName, + ImportStateIdFunc: func() resource.ImportStateIdFunc { + return func(*terraform.State) (string, error) { + return fmt.Sprintf("%s/%s@%s", serviceDataBase.Name, dbDataUpdate.DatabaseName, dbDataBase.Zone), nil + } + }(), + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -320,3 +388,35 @@ func CheckExistsMysqlUser(service, username string, data *TemplateModelMysqlUser return fmt.Errorf("could not find user %s for service %s, found %v", username, service, serviceUsernames) } + +func CheckExistsMysqlDatabase(service, databaseName string, data *TemplateModelMysqlDb) error { + + client, err := testutils.APIClient() + if err != nil { + return err + } + + ctx := exoapi.WithEndpoint(context.Background(), exoapi.NewReqEndpoint(testutils.TestEnvironment(), testutils.TestZoneName)) + + res, err := client.GetDbaasServiceMysqlWithResponse(ctx, oapi.DbaasServiceName(service)) + if err != nil { + return err + } + if res.StatusCode() != http.StatusOK { + return fmt.Errorf("API request error: unexpected status %s", res.Status()) + } + svc := res.JSON200 + + serviceDbs := make([]string, 0) + if svc.Databases != nil { + + for _, db := range *svc.Databases { + serviceDbs = append(serviceDbs, string(db)) + if string(db) == databaseName { + return nil + } + } + } + + return fmt.Errorf("could not find database %s for service %s, found %v", databaseName, service, serviceDbs) +} diff --git a/pkg/resources/database/resource_opensearch.go b/pkg/resources/database/resource_service_opensearch.go similarity index 98% rename from pkg/resources/database/resource_opensearch.go rename to pkg/resources/database/resource_service_opensearch.go index 718ab35d8..1b6bac695 100644 --- a/pkg/resources/database/resource_opensearch.go +++ b/pkg/resources/database/resource_service_opensearch.go @@ -159,7 +159,7 @@ var ResourceOpensearchSchema = schema.SingleNestedBlock{ } // createOpensearch function handles OpenSearch specific part of database resource creation logic. -func (r *Resource) createOpensearch(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) createOpensearch(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { if data.Opensearch == nil { data.Opensearch = &ResourceOpensearchModel{} } @@ -303,7 +303,7 @@ func (r *Resource) createOpensearch(ctx context.Context, data *ResourceModel, di // readOpensearch function handles OpenSearch specific part of database resource Read logic. // It is used in the dedicated Read action but also as a finishing step of Create, Update and Import. // NOTE: For optional but not computed attributes we only read remote value if they are defined in the plan. -func (r *Resource) readOpensearch(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) readOpensearch(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { caCert, err := r.client.GetDatabaseCACertificate(ctx, data.Zone.ValueString()) if err != nil { diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get CA Certificate: %s", err)) @@ -430,7 +430,7 @@ func (r *Resource) readOpensearch(ctx context.Context, data *ResourceModel, diag } // updateOpensearch function handles OpenSearch specific part of database resource Update logic. -func (r *Resource) updateOpensearch(ctx context.Context, stateData *ResourceModel, planData *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) updateOpensearch(ctx context.Context, stateData *ServiceResourceModel, planData *ServiceResourceModel, diagnostics *diag.Diagnostics) { var updated bool service := oapi.UpdateDbaasServiceOpensearchJSONRequestBody{} diff --git a/pkg/resources/database/resource_opensearch_test.go b/pkg/resources/database/resource_service_opensearch_test.go similarity index 99% rename from pkg/resources/database/resource_opensearch_test.go rename to pkg/resources/database/resource_service_opensearch_test.go index 789473bb4..cf0b17526 100644 --- a/pkg/resources/database/resource_opensearch_test.go +++ b/pkg/resources/database/resource_service_opensearch_test.go @@ -82,7 +82,7 @@ func testResourceOpensearch(t *testing.T) { t.Fatal(err) } - serviceFullResourceName := "exoscale_database.test" + serviceFullResourceName := "exoscale_dbaas.test" serviceDataBase := TemplateModelOpensearch{ ResourceName: "test", Name: acctest.RandomWithPrefix(testutils.Prefix), diff --git a/pkg/resources/database/resource_pg.go b/pkg/resources/database/resource_service_pg.go similarity index 98% rename from pkg/resources/database/resource_pg.go rename to pkg/resources/database/resource_service_pg.go index d22f59c6f..7acc57d56 100644 --- a/pkg/resources/database/resource_pg.go +++ b/pkg/resources/database/resource_service_pg.go @@ -80,7 +80,7 @@ var ResourcePgSchema = schema.SingleNestedBlock{ } // createPg function handles PostgreSQL specific part of database resource creation logic. -func (r *Resource) createPg(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) createPg(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { service := oapi.CreateDbaasServicePgJSONRequestBody{ Plan: data.Plan.ValueString(), TerminationProtection: data.TerminationProtection.ValueBoolPointer(), @@ -302,7 +302,7 @@ func (r *Resource) createPg(ctx context.Context, data *ResourceModel, diagnostic // readPg function handles PostgreSQL specific part of database resource Read logic. // It is used in the dedicated Read action but also as a finishing step of Create, Update and Import. -func (r *Resource) readPg(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) readPg(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { caCert, err := r.client.GetDatabaseCACertificate(ctx, data.Zone.ValueString()) if err != nil { diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get CA Certificate: %s", err)) @@ -461,7 +461,7 @@ func (r *Resource) readPg(ctx context.Context, data *ResourceModel, diagnostics } // updatePg function handles PostgreSQL specific part of database resource Update logic. -func (r *Resource) updatePg(ctx context.Context, stateData *ResourceModel, planData *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) updatePg(ctx context.Context, stateData *ServiceResourceModel, planData *ServiceResourceModel, diagnostics *diag.Diagnostics) { var updated bool service := oapi.UpdateDbaasServicePgJSONRequestBody{} diff --git a/pkg/resources/database/resource_pg_test.go b/pkg/resources/database/resource_service_pg_test.go similarity index 76% rename from pkg/resources/database/resource_pg_test.go rename to pkg/resources/database/resource_service_pg_test.go index 304085f06..3dded9de8 100644 --- a/pkg/resources/database/resource_pg_test.go +++ b/pkg/resources/database/resource_service_pg_test.go @@ -51,6 +51,14 @@ type TemplateModelPgUser struct { Zone string } +type TemplateModelPgDb struct { + ResourceName string + + DatabaseName string + Service string + Zone string +} + func testResourcePg(t *testing.T) { serviceTpl, err := template.ParseFiles("testdata/resource_pg.tmpl") if err != nil { @@ -60,8 +68,12 @@ func testResourcePg(t *testing.T) { if err != nil { t.Fatal(err) } + dbTpl, err := template.ParseFiles("testdata/resource_database_pg.tmpl") + if err != nil { + t.Fatal(err) + } - serviceFullResourceName := "exoscale_database.test" + serviceFullResourceName := "exoscale_dbaas.test" serviceDataBase := TemplateModelPg{ ResourceName: "test", Name: acctest.RandomWithPrefix(testutils.Prefix), @@ -79,6 +91,14 @@ func testResourcePg(t *testing.T) { Service: fmt.Sprintf("%s.name", serviceFullResourceName), } + dbFullResourceName := "exoscale_dbaas_pg_database.test_database" + dbDataBase := TemplateModelPgDb{ + ResourceName: "test_database", + DatabaseName: "foo_db", + Zone: serviceDataBase.Zone, + Service: fmt.Sprintf("%s.name", serviceFullResourceName), + } + serviceDataCreate := serviceDataBase serviceDataCreate.MaintenanceDow = "monday" serviceDataCreate.MaintenanceTime = "01:23:00" @@ -88,6 +108,7 @@ func testResourcePg(t *testing.T) { serviceDataCreate.PgbouncerSettings = strconv.Quote(`{"min_pool_size":10}`) userDataCreate := userDataBase + dbDataCreate := dbDataBase buf := &bytes.Buffer{} err = serviceTpl.Execute(buf, &serviceDataCreate) @@ -98,6 +119,10 @@ func testResourcePg(t *testing.T) { if err != nil { t.Fatal(err) } + err = dbTpl.Execute(buf, &dbDataCreate) + if err != nil { + t.Fatal(err) + } configCreate := buf.String() serviceDataUpdate := serviceDataBase @@ -112,6 +137,9 @@ func testResourcePg(t *testing.T) { userDataUpdate := userDataBase userDataUpdate.Username = "bar" + dbDataUpdate := dbDataBase + dbDataUpdate.DatabaseName = "bar_db" + buf = &bytes.Buffer{} err = serviceTpl.Execute(buf, &serviceDataUpdate) if err != nil { @@ -121,6 +149,10 @@ func testResourcePg(t *testing.T) { if err != nil { t.Fatal(err) } + err = dbTpl.Execute(buf, &dbDataUpdate) + if err != nil { + t.Fatal(err) + } configUpdate := buf.String() resource.Test(t, resource.TestCase{ @@ -159,6 +191,16 @@ func testResourcePg(t *testing.T) { return nil }, + + // Database + func(s *terraform.State) error { + err := CheckExistsPgDatabase(serviceDataBase.Name, dbDataCreate.DatabaseName, &dbDataCreate) + if err != nil { + return err + } + + return nil + }, ), }, { @@ -191,6 +233,22 @@ func testResourcePg(t *testing.T) { return nil }, + + // Database + func(s *terraform.State) error { + // Check the old database was deleted + err := CheckExistsPgDatabase(serviceDataBase.Name, dbDataBase.DatabaseName, &dbDataUpdate) + if err == nil { + return fmt.Errorf("expected to not find database %s", dbDataBase.DatabaseName) + } + + // Check the new user exists + err = CheckExistsPgDatabase(serviceDataBase.Name, dbDataUpdate.DatabaseName, &dbDataUpdate) + if err != nil { + return err + } + return nil + }, ), }, { @@ -215,6 +273,16 @@ func testResourcePg(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + { + ResourceName: dbFullResourceName, + ImportStateIdFunc: func() resource.ImportStateIdFunc { + return func(*terraform.State) (string, error) { + return fmt.Sprintf("%s/%s@%s", serviceDataBase.Name, dbDataUpdate.DatabaseName, dbDataBase.Zone), nil + } + }(), + ImportState: true, + ImportStateVerify: true, + }, }, }) } @@ -301,3 +369,34 @@ func CheckExistsPgUser(service, username string, data *TemplateModelPgUser) erro return fmt.Errorf("could not find user %s for service %s, found %v", username, service, serviceUsernames) } + +func CheckExistsPgDatabase(service, databaseName string, data *TemplateModelPgDb) error { + + client, err := testutils.APIClient() + if err != nil { + return err + } + + ctx := exoapi.WithEndpoint(context.Background(), exoapi.NewReqEndpoint(testutils.TestEnvironment(), testutils.TestZoneName)) + + res, err := client.GetDbaasServicePgWithResponse(ctx, oapi.DbaasServiceName(service)) + if err != nil { + return err + } + if res.StatusCode() != http.StatusOK { + return fmt.Errorf("API request error: unexpected status %s", res.Status()) + } + svc := res.JSON200 + + serviceDbs := make([]string, 0) + if svc.Databases != nil { + for _, db := range *svc.Databases { + serviceDbs = append(serviceDbs, string(db)) + if string(db) == databaseName { + return nil + } + } + } + + return fmt.Errorf("could not find database %s for service %s, found %v", databaseName, service, serviceDbs) +} diff --git a/pkg/resources/database/resource_redis.go b/pkg/resources/database/resource_service_redis.go similarity index 95% rename from pkg/resources/database/resource_redis.go rename to pkg/resources/database/resource_service_redis.go index 7925e245b..83c9876ad 100644 --- a/pkg/resources/database/resource_redis.go +++ b/pkg/resources/database/resource_service_redis.go @@ -44,7 +44,7 @@ var ResourceRedisSchema = schema.SingleNestedBlock{ } // createRedis function handles Redis specific part of database resource creation logic. -func (r *Resource) createRedis(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) createRedis(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { service := oapi.CreateDbaasServiceRedisJSONRequestBody{ Plan: data.Plan.ValueString(), TerminationProtection: data.TerminationProtection.ValueBoolPointer(), @@ -113,7 +113,7 @@ func (r *Resource) createRedis(ctx context.Context, data *ResourceModel, diagnos // readRedis function handles Redis specific part of database resource Read logic. // It is used in the dedicated Read action but also as a finishing step of Create, Update and Import. -func (r *Resource) readRedis(ctx context.Context, data *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) readRedis(ctx context.Context, data *ServiceResourceModel, diagnostics *diag.Diagnostics) { caCert, err := r.client.GetDatabaseCACertificate(ctx, data.Zone.ValueString()) if err != nil { diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get CA Certificate: %s", err)) @@ -177,7 +177,7 @@ func (r *Resource) readRedis(ctx context.Context, data *ResourceModel, diagnosti } // updateRedis function handles Redis specific part of database resource Update logic. -func (r *Resource) updateRedis(ctx context.Context, stateData *ResourceModel, planData *ResourceModel, diagnostics *diag.Diagnostics) { +func (r *ServiceResource) updateRedis(ctx context.Context, stateData *ServiceResourceModel, planData *ServiceResourceModel, diagnostics *diag.Diagnostics) { var updated bool service := oapi.UpdateDbaasServiceRedisJSONRequestBody{} diff --git a/pkg/resources/database/resource_redis_test.go b/pkg/resources/database/resource_service_redis_test.go similarity index 99% rename from pkg/resources/database/resource_redis_test.go rename to pkg/resources/database/resource_service_redis_test.go index 8d6219708..b7ae9251c 100644 --- a/pkg/resources/database/resource_redis_test.go +++ b/pkg/resources/database/resource_service_redis_test.go @@ -44,7 +44,7 @@ func testResourceRedis(t *testing.T) { t.Fatal(err) } - fullResourceName := "exoscale_database.test" + fullResourceName := "exoscale_dbaas.test" dataBase := TemplateModelRedis{ ResourceName: "test", Name: acctest.RandomWithPrefix(testutils.Prefix), diff --git a/pkg/resources/database/resource_user.go b/pkg/resources/database/resource_user.go index f3ab33e3e..b178b7a20 100644 --- a/pkg/resources/database/resource_user.go +++ b/pkg/resources/database/resource_user.go @@ -1,23 +1,16 @@ package database import ( - "context" - "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" exoscale "github.com/exoscale/egoscale/v3" "github.com/exoscale/terraform-provider-exoscale/pkg/config" - "github.com/exoscale/terraform-provider-exoscale/pkg/utils" ) // UserResource defines the resource implementation. @@ -98,257 +91,3 @@ func buildUserAttributes(newAttributes map[string]schema.Attribute) map[string]s return newSchemas } - -// ResourceModelInterface defines necessary functions for interacting with resources -type ResourceModelInterface interface { - // ReadResource reads resource from remote and populate the model accordingly - ReadResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) - // CreateResource creates the resource according to the model, and then - // update computed fields if applicable - CreateResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) - // DeleteResource deletes the resource - Delete(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) - // UpdateResource updates the remote resource w/ the new model - UpdateResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) - - // WaitForService waits for the service to be RUNNING. - WaitForService(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) - - // Accessing and setting attributes - GetTimeouts() timeouts.Value - SetTimeouts(timeouts.Value) - GetID() basetypes.StringValue - GetZone() basetypes.StringValue - - // Should set the return value of .GetID() to service/username - GenerateID() -} - -func UserRead[T ResourceModelInterface](ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, data T, client *exoscale.Client) { - - // Read Terraform prior state data into the model - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { - return - } - - // Set timeout - t, diags := data.GetTimeouts().Read(ctx, config.DefaultTimeout) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - ctx, cancel := context.WithTimeout(ctx, t) - defer cancel() - - data.GenerateID() - - client, err := utils.SwitchClientZone( - ctx, - client, - exoscale.ZoneName(data.GetZone().ValueString()), - ) - if err != nil { - resp.Diagnostics.AddError( - "unable to change exoscale client zone", - err.Error(), - ) - return - } - - data.ReadResource(ctx, client, &resp.Diagnostics) - - if resp.Diagnostics.HasError() { - return - } - - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) - - tflog.Trace(ctx, "resource read done", map[string]interface{}{ - "id": data.GetID(), - }) - -} - -func UserReadForImport[T ResourceModelInterface](ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse, data T, client *exoscale.Client) { - - // Set timeout - t, diags := data.GetTimeouts().Read(ctx, config.DefaultTimeout) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - ctx, cancel := context.WithTimeout(ctx, t) - defer cancel() - - data.GenerateID() - - client, err := utils.SwitchClientZone( - ctx, - client, - exoscale.ZoneName(data.GetZone().ValueString()), - ) - if err != nil { - resp.Diagnostics.AddError( - "unable to change exoscale client zone", - err.Error(), - ) - return - } - - data.ReadResource(ctx, client, &resp.Diagnostics) - - if resp.Diagnostics.HasError() { - return - } - - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) - - tflog.Trace(ctx, "resource read done", map[string]interface{}{ - "id": data.GetID(), - }) - -} - -func UserCreate[T ResourceModelInterface](ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, data T, client *exoscale.Client) { - - // Read Terraform prior state data into the model - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - if resp.Diagnostics.HasError() { - return - } - - // Set timeout - t, diags := data.GetTimeouts().Create(ctx, config.DefaultTimeout) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - ctx, cancel := context.WithTimeout(ctx, t) - defer cancel() - - data.GenerateID() - - client, err := utils.SwitchClientZone( - ctx, - client, - exoscale.ZoneName(data.GetZone().ValueString()), - ) - if err != nil { - resp.Diagnostics.AddError( - "unable to change exoscale client zone", - err.Error(), - ) - return - } - - data.WaitForService(ctx, client, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - data.CreateResource(ctx, client, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) - - tflog.Trace(ctx, "resource created", map[string]interface{}{ - "id": data.GetID(), - }) - -} - -func UserUpdate[T ResourceModelInterface](ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, stateData, planData T, client *exoscale.Client) { - // Read Terraform plan data into the model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...) - // Read Terraform state data (for comparison) into the model - resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...) - if resp.Diagnostics.HasError() { - return - } - - // Set timeout - t, diags := stateData.GetTimeouts().Update(ctx, config.DefaultTimeout) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - ctx, cancel := context.WithTimeout(ctx, t) - defer cancel() - - client, err := utils.SwitchClientZone( - ctx, - client, - exoscale.ZoneName(planData.GetZone().ValueString()), - ) - if err != nil { - resp.Diagnostics.AddError( - "unable to change exoscale client zone", - err.Error(), - ) - return - } - - planData.WaitForService(ctx, client, &resp.Diagnostics) - planData.UpdateResource(ctx, client, &diags) - - if resp.Diagnostics.HasError() { - return - } - - // Save updated data into Terraform state - resp.Diagnostics.Append(resp.State.Set(ctx, &planData)...) - - tflog.Trace(ctx, "resource updated", map[string]interface{}{ - "id": planData.GetID(), - }) -} - -func UserDelete[T ResourceModelInterface](ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, data T, client *exoscale.Client) { - // Read Terraform prior state data into the model - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - - if resp.Diagnostics.HasError() { - return - } - - // Set timeout - t, diags := data.GetTimeouts().Delete(ctx, config.DefaultTimeout) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - ctx, cancel := context.WithTimeout(ctx, t) - defer cancel() - - data.GenerateID() - - client, err := utils.SwitchClientZone( - ctx, - client, - exoscale.ZoneName(data.GetZone().ValueString()), - ) - if err != nil { - resp.Diagnostics.AddError( - "unable to change exoscale client zone", - err.Error(), - ) - return - } - - data.Delete(ctx, client, &diags) - - if resp.Diagnostics.HasError() { - return - } - - tflog.Trace(ctx, "resource deleted", map[string]interface{}{ - "id": data.GetID(), - }) - -} diff --git a/pkg/resources/database/resource_user_kafka.go b/pkg/resources/database/resource_user_kafka.go index 1a35b6fab..c792d6b35 100644 --- a/pkg/resources/database/resource_user_kafka.go +++ b/pkg/resources/database/resource_user_kafka.go @@ -73,23 +73,23 @@ func (r *KafkaUserResource) Schema(ctx context.Context, req resource.SchemaReque func (r *KafkaUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data KafkaUserResourceModel - UserRead(ctx, req, resp, &data, r.client) + ReadResource(ctx, req, resp, &data, r.client) } func (r *KafkaUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data KafkaUserResourceModel - UserCreate(ctx, req, resp, &data, r.client) + CreateResource(ctx, req, resp, &data, r.client) } func (r *KafkaUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var stateData, planData KafkaUserResourceModel - UserUpdate(ctx, req, resp, &stateData, &planData, r.client) + UpdateResource(ctx, req, resp, &stateData, &planData, r.client) } func (r *KafkaUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data KafkaUserResourceModel - UserDelete(ctx, req, resp, &data, r.client) + DeleteResource(ctx, req, resp, &data, r.client) } func (r *KafkaUserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { @@ -137,7 +137,7 @@ func (r *KafkaUserResource) ImportState(ctx context.Context, req resource.Import data.Service = types.StringValue(serviceName) data.Zone = types.StringValue(zone) - UserReadForImport(ctx, req, resp, &data, r.client) + ReadResourceForImport(ctx, req, resp, &data, r.client) } @@ -184,7 +184,7 @@ func (data *KafkaUserResourceModel) CreateResource(ctx context.Context, client * diagnostics.AddError("Client Error", "Unable to find newly created user for the service") } -func (data *KafkaUserResourceModel) Delete(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { +func (data *KafkaUserResourceModel) DeleteResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { op, err := client.DeleteDBAASKafkaUser(ctx, data.Service.ValueString(), data.Username.ValueString()) if err != nil { @@ -233,7 +233,7 @@ func (data *KafkaUserResourceModel) UpdateResource(ctx context.Context, client * } func (data *KafkaUserResourceModel) WaitForService(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { - _, err := waitForDBAASServiceReadyForUsers(ctx, client.GetDBAASServiceKafka, data.Service.ValueString(), func(t *exoscale.DBAASServiceKafka) bool { return len(t.Users) > 0 }) + _, err := waitForDBAASServiceReadyForFn(ctx, client.GetDBAASServiceKafka, data.Service.ValueString(), func(t *exoscale.DBAASServiceKafka) bool { return len(t.Users) > 0 }) if err != nil { diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Database service Kafka %s", err.Error())) } diff --git a/pkg/resources/database/resource_user_mysql.go b/pkg/resources/database/resource_user_mysql.go index 883347533..33fc6d948 100644 --- a/pkg/resources/database/resource_user_mysql.go +++ b/pkg/resources/database/resource_user_mysql.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" exoscale "github.com/exoscale/egoscale/v3" providerConfig "github.com/exoscale/terraform-provider-exoscale/pkg/provider/config" @@ -73,22 +74,22 @@ func (r *MysqlUserResource) Schema(ctx context.Context, req resource.SchemaReque func (r *MysqlUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data MysqlUserResourceModel - UserRead(ctx, req, resp, &data, r.client) + ReadResource(ctx, req, resp, &data, r.client) } func (r *MysqlUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data MysqlUserResourceModel - UserCreate(ctx, req, resp, &data, r.client) + CreateResource(ctx, req, resp, &data, r.client) } func (r *MysqlUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var stateData, planData MysqlUserResourceModel - UserUpdate(ctx, req, resp, &stateData, &planData, r.client) + UpdateResource(ctx, req, resp, &stateData, &planData, r.client) } func (r *MysqlUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data MysqlUserResourceModel - UserDelete(ctx, req, resp, &data, r.client) + DeleteResource(ctx, req, resp, &data, r.client) } func (r *MysqlUserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { @@ -136,7 +137,7 @@ func (r *MysqlUserResource) ImportState(ctx context.Context, req resource.Import data.Service = types.StringValue(serviceName) data.Zone = types.StringValue(zone) - UserReadForImport(ctx, req, resp, &data, r.client) + ReadResourceForImport(ctx, req, resp, &data, r.client) } @@ -186,7 +187,7 @@ func (data *MysqlUserResourceModel) CreateResource(ctx context.Context, client * diagnostics.AddError("Client Error", "Unable to find newly created user for the service") } -func (data *MysqlUserResourceModel) Delete(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { +func (data *MysqlUserResourceModel) DeleteResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { op, err := client.DeleteDBAASMysqlUser(ctx, data.Service.ValueString(), data.Username.ValueString()) if err != nil { @@ -233,7 +234,10 @@ func (data *MysqlUserResourceModel) UpdateResource(ctx context.Context, client * } func (data *MysqlUserResourceModel) WaitForService(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { - _, err := waitForDBAASServiceReadyForUsers(ctx, client.GetDBAASServiceMysql, data.Service.ValueString(), func(t *exoscale.DBAASServiceMysql) bool { return t.State == exoscale.EnumServiceStateRunning }) + _, err := waitForDBAASServiceReadyForFn(ctx, client.GetDBAASServiceMysql, data.Service.ValueString(), func(t *exoscale.DBAASServiceMysql) bool { return t.State == exoscale.EnumServiceStateRunning }) + // DbaaS API is unstable when a service goes from rebuilding from running, + // this wait time helps avoid that + time.Sleep(time.Second * 10) if err != nil { diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Database service MySQL %s", err.Error())) } diff --git a/pkg/resources/database/resource_user_opensearch.go b/pkg/resources/database/resource_user_opensearch.go index e218c3993..37658bd15 100644 --- a/pkg/resources/database/resource_user_opensearch.go +++ b/pkg/resources/database/resource_user_opensearch.go @@ -55,22 +55,22 @@ func (r *OpensearchUserResource) Schema(ctx context.Context, req resource.Schema func (r *OpensearchUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data OpensearchUserResourceModel - UserRead(ctx, req, resp, &data, r.client) + ReadResource(ctx, req, resp, &data, r.client) } func (r *OpensearchUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data OpensearchUserResourceModel - UserCreate(ctx, req, resp, &data, r.client) + CreateResource(ctx, req, resp, &data, r.client) } func (r *OpensearchUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var stateData, planData OpensearchUserResourceModel - UserUpdate(ctx, req, resp, &stateData, &planData, r.client) + UpdateResource(ctx, req, resp, &stateData, &planData, r.client) } func (r *OpensearchUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data OpensearchUserResourceModel - UserDelete(ctx, req, resp, &data, r.client) + DeleteResource(ctx, req, resp, &data, r.client) } func (r *OpensearchUserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { @@ -118,7 +118,7 @@ func (r *OpensearchUserResource) ImportState(ctx context.Context, req resource.I data.Service = types.StringValue(serviceName) data.Zone = types.StringValue(zone) - UserReadForImport(ctx, req, resp, &data, r.client) + ReadResourceForImport(ctx, req, resp, &data, r.client) } @@ -162,7 +162,7 @@ func (data *OpensearchUserResourceModel) CreateResource(ctx context.Context, cli diagnostics.AddError("Client Error", "Unable to find newly created user for the service") } -func (data *OpensearchUserResourceModel) Delete(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { +func (data *OpensearchUserResourceModel) DeleteResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { op, err := client.DeleteDBAASOpensearchUser(ctx, data.Service.ValueString(), data.Username.ValueString()) if err != nil { @@ -208,7 +208,7 @@ func (data *OpensearchUserResourceModel) UpdateResource(ctx context.Context, cli } func (data *OpensearchUserResourceModel) WaitForService(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { - _, err := waitForDBAASServiceReadyForUsers(ctx, client.GetDBAASServiceOpensearch, data.Service.ValueString(), func(t *exoscale.DBAASServiceOpensearch) bool { return len(t.Users) > 0 }) + _, err := waitForDBAASServiceReadyForFn(ctx, client.GetDBAASServiceOpensearch, data.Service.ValueString(), func(t *exoscale.DBAASServiceOpensearch) bool { return len(t.Users) > 0 }) if err != nil { diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Database service Opensearch %s", err.Error())) } diff --git a/pkg/resources/database/resource_user_pg.go b/pkg/resources/database/resource_user_pg.go index 9a781d1ac..8b3cdae62 100644 --- a/pkg/resources/database/resource_user_pg.go +++ b/pkg/resources/database/resource_user_pg.go @@ -4,8 +4,9 @@ import ( "context" "fmt" "strings" + "time" - exoscale "github.com/exoscale/egoscale/v3" + v3 "github.com/exoscale/egoscale/v3" providerConfig "github.com/exoscale/terraform-provider-exoscale/pkg/provider/config" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -67,23 +68,23 @@ func (r *PGUserResource) Schema(ctx context.Context, req resource.SchemaRequest, func (r *PGUserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data PGUserResourceModel - UserRead(ctx, req, resp, &data, r.client) + ReadResource(ctx, req, resp, &data, r.client) } func (r *PGUserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data PGUserResourceModel - UserCreate(ctx, req, resp, &data, r.client) + CreateResource(ctx, req, resp, &data, r.client) } func (r *PGUserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var stateData, planData PGUserResourceModel - UserUpdate(ctx, req, resp, &stateData, &planData, r.client) + UpdateResource(ctx, req, resp, &stateData, &planData, r.client) } func (r *PGUserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var data PGUserResourceModel - UserDelete(ctx, req, resp, &data, r.client) + DeleteResource(ctx, req, resp, &data, r.client) } func (r *PGUserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { @@ -130,16 +131,15 @@ func (r *PGUserResource) ImportState(ctx context.Context, req resource.ImportSta data.Username = types.StringValue(username) data.Service = types.StringValue(serviceName) data.Zone = types.StringValue(zone) - data.Zone = types.StringValue(zone) - UserReadForImport(ctx, req, resp, &data, r.client) + ReadResourceForImport(ctx, req, resp, &data, r.client) } -func (data *PGUserResourceModel) CreateResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { +func (data *PGUserResourceModel) CreateResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { - createRequest := exoscale.CreateDBAASPostgresUserRequest{ - Username: exoscale.DBAASUserUsername(data.Username.ValueString()), + createRequest := v3.CreateDBAASPostgresUserRequest{ + Username: v3.DBAASUserUsername(data.Username.ValueString()), } if !data.AllowReplication.IsNull() { @@ -155,7 +155,7 @@ func (data *PGUserResourceModel) CreateResource(ctx context.Context, client *exo return } - _, err = client.Wait(ctx, op, exoscale.OperationStateSuccess) + _, err = client.Wait(ctx, op, v3.OperationStateSuccess) if err != nil { diagnostics.AddError( "Client Error", @@ -183,7 +183,7 @@ func (data *PGUserResourceModel) CreateResource(ctx context.Context, client *exo diagnostics.AddError("Client Error", "Unable to find newly created user for the service") } -func (data *PGUserResourceModel) Delete(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { +func (data *PGUserResourceModel) DeleteResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { op, err := client.DeleteDBAASPostgresUser(ctx, data.Service.ValueString(), data.Username.ValueString()) if err != nil { @@ -194,7 +194,7 @@ func (data *PGUserResourceModel) Delete(ctx context.Context, client *exoscale.Cl return } - _, err = client.Wait(ctx, op, exoscale.OperationStateSuccess) + _, err = client.Wait(ctx, op, v3.OperationStateSuccess) if err != nil { diagnostics.AddError( "Client Error", @@ -205,7 +205,7 @@ func (data *PGUserResourceModel) Delete(ctx context.Context, client *exoscale.Cl } -func (data *PGUserResourceModel) ReadResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { +func (data *PGUserResourceModel) ReadResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { svc, err := client.GetDBAASServicePG(ctx, data.Service.ValueString()) if err != nil { @@ -226,15 +226,18 @@ func (data *PGUserResourceModel) ReadResource(ctx context.Context, client *exosc diagnostics.AddError("Client Error", "Unable to read user for the service") } -func (data *PGUserResourceModel) UpdateResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { +func (data *PGUserResourceModel) UpdateResource(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { // Nothing to do here as all fields of this resource are immutable; replaces will be required // automatically } -func (data *PGUserResourceModel) WaitForService(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) { - _, err := waitForDBAASServiceReadyForUsers(ctx, client.GetDBAASServicePG, data.Service.ValueString(), func(t *exoscale.DBAASServicePG) bool { return len(t.Users) > 0 }) +func (data *PGUserResourceModel) WaitForService(ctx context.Context, client *v3.Client, diagnostics *diag.Diagnostics) { + _, err := waitForDBAASServiceReadyForFn(ctx, client.GetDBAASServicePG, data.Service.ValueString(), func(t *v3.DBAASServicePG) bool { return t.State == v3.EnumServiceStateRunning }) + // DbaaS API is unstable when a service goes from rebuilding from running, + // this wait time helps avoid that + time.Sleep(time.Second * 10) if err != nil { - diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Database service Opensearch %s", err.Error())) + diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Database service PG %s", err.Error())) } } diff --git a/pkg/resources/database/testdata/resource_database_mysql.tmpl b/pkg/resources/database/testdata/resource_database_mysql.tmpl new file mode 100644 index 000000000..d0d0db493 --- /dev/null +++ b/pkg/resources/database/testdata/resource_database_mysql.tmpl @@ -0,0 +1,5 @@ +resource "exoscale_dbaas_mysql_database" {{ .ResourceName }} { + database_name = "{{ .DatabaseName }}" + service = {{ .Service }} + zone = "{{ .Zone }}" +} diff --git a/pkg/resources/database/testdata/resource_database_pg.tmpl b/pkg/resources/database/testdata/resource_database_pg.tmpl new file mode 100644 index 000000000..fc8e9beea --- /dev/null +++ b/pkg/resources/database/testdata/resource_database_pg.tmpl @@ -0,0 +1,5 @@ +resource "exoscale_dbaas_pg_database" {{ .ResourceName }} { + database_name = "{{ .DatabaseName }}" + service = {{ .Service }} + zone = "{{ .Zone }}" +} diff --git a/pkg/resources/database/testdata/resource_grafana.tmpl b/pkg/resources/database/testdata/resource_grafana.tmpl index 2bf32689e..72a6cea6b 100644 --- a/pkg/resources/database/testdata/resource_grafana.tmpl +++ b/pkg/resources/database/testdata/resource_grafana.tmpl @@ -1,4 +1,4 @@ -resource "exoscale_database" {{ .ResourceName }} { +resource "exoscale_dbaas" {{ .ResourceName }} { name = "{{ .Name }}" type = "grafana" plan = "{{ .Plan }}" diff --git a/pkg/resources/database/testdata/resource_kafka.tmpl b/pkg/resources/database/testdata/resource_kafka.tmpl index 03bd5a2a0..b0209b0e6 100644 --- a/pkg/resources/database/testdata/resource_kafka.tmpl +++ b/pkg/resources/database/testdata/resource_kafka.tmpl @@ -1,4 +1,4 @@ -resource "exoscale_database" {{ .ResourceName }} { +resource "exoscale_dbaas" {{ .ResourceName }} { name = "{{ .Name }}" type = "kafka" plan = "{{ .Plan }}" diff --git a/pkg/resources/database/testdata/resource_mysql.tmpl b/pkg/resources/database/testdata/resource_mysql.tmpl index c486912c0..0d3e12aaa 100644 --- a/pkg/resources/database/testdata/resource_mysql.tmpl +++ b/pkg/resources/database/testdata/resource_mysql.tmpl @@ -1,4 +1,4 @@ -resource "exoscale_database" {{ .ResourceName }} { +resource "exoscale_dbaas" {{ .ResourceName }} { name = "{{ .Name }}" type = "mysql" plan = "{{ .Plan }}" diff --git a/pkg/resources/database/testdata/resource_opensearch.tmpl b/pkg/resources/database/testdata/resource_opensearch.tmpl index 4c98c5ce1..a43398eca 100644 --- a/pkg/resources/database/testdata/resource_opensearch.tmpl +++ b/pkg/resources/database/testdata/resource_opensearch.tmpl @@ -1,4 +1,4 @@ -resource "exoscale_database" {{ .ResourceName }} { +resource "exoscale_dbaas" {{ .ResourceName }} { name = "{{ .Name }}" type = "opensearch" plan = "{{ .Plan }}" diff --git a/pkg/resources/database/testdata/resource_pg.tmpl b/pkg/resources/database/testdata/resource_pg.tmpl index 8cb72dba8..d6bcfeae9 100644 --- a/pkg/resources/database/testdata/resource_pg.tmpl +++ b/pkg/resources/database/testdata/resource_pg.tmpl @@ -1,4 +1,4 @@ -resource "exoscale_database" {{ .ResourceName }} { +resource "exoscale_dbaas" {{ .ResourceName }} { name = "{{ .Name }}" type = "pg" plan = "{{ .Plan }}" diff --git a/pkg/resources/database/testdata/resource_redis.tmpl b/pkg/resources/database/testdata/resource_redis.tmpl index 52cbf6233..ad27c1e4f 100644 --- a/pkg/resources/database/testdata/resource_redis.tmpl +++ b/pkg/resources/database/testdata/resource_redis.tmpl @@ -1,4 +1,4 @@ -resource "exoscale_database" {{ .ResourceName }} { +resource "exoscale_dbaas" {{ .ResourceName }} { name = "{{ .Name }}" type = "redis" plan = "{{ .Plan }}" diff --git a/pkg/resources/database/utils.go b/pkg/resources/database/utils.go index 4d9a06f6d..c2cbefac4 100644 --- a/pkg/resources/database/utils.go +++ b/pkg/resources/database/utils.go @@ -1,12 +1,21 @@ package database import ( + "context" "encoding/json" "errors" "fmt" "strconv" "strings" + exoscale "github.com/exoscale/egoscale/v3" + "github.com/exoscale/terraform-provider-exoscale/pkg/config" + "github.com/exoscale/terraform-provider-exoscale/pkg/utils" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/xeipuuv/gojsonschema" ) @@ -80,3 +89,257 @@ func PartialSettingsPatch(data, patch map[string]interface{}) { } } } + +// ResourceModelInterface defines necessary functions for interacting with resources through abstraction +type ResourceModelInterface interface { + // ReadResource reads resource from remote and populate the model accordingly + ReadResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) + // CreateResource creates the resource according to the model, and then + // update computed fields if applicable + CreateResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) + // DeleteResource deletes the resource + DeleteResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) + // UpdateResource updates the remote resource w/ the new model + UpdateResource(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) + + // WaitForService waits for the service to be be available for resource updates. + WaitForService(ctx context.Context, client *exoscale.Client, diagnostics *diag.Diagnostics) + + // Accessing and setting attributes + GetTimeouts() timeouts.Value + SetTimeouts(timeouts.Value) + GetID() basetypes.StringValue + GetZone() basetypes.StringValue + + // Should set the return value of .GetID() to service/username + GenerateID() +} + +func ReadResource[T ResourceModelInterface](ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, data T, client *exoscale.Client) { + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Set timeout + t, diags := data.GetTimeouts().Read(ctx, config.DefaultTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, t) + defer cancel() + + data.GenerateID() + + client, err := utils.SwitchClientZone( + ctx, + client, + exoscale.ZoneName(data.GetZone().ValueString()), + ) + if err != nil { + resp.Diagnostics.AddError( + "unable to change exoscale client zone", + err.Error(), + ) + return + } + + data.ReadResource(ctx, client, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + tflog.Trace(ctx, "resource read done", map[string]interface{}{ + "id": data.GetID(), + }) + +} + +func ReadResourceForImport[T ResourceModelInterface](ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse, data T, client *exoscale.Client) { + + // Set timeout + t, diags := data.GetTimeouts().Read(ctx, config.DefaultTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, t) + defer cancel() + + data.GenerateID() + + client, err := utils.SwitchClientZone( + ctx, + client, + exoscale.ZoneName(data.GetZone().ValueString()), + ) + if err != nil { + resp.Diagnostics.AddError( + "unable to change exoscale client zone", + err.Error(), + ) + return + } + + data.ReadResource(ctx, client, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + tflog.Trace(ctx, "resource read done", map[string]interface{}{ + "id": data.GetID(), + }) + +} + +func CreateResource[T ResourceModelInterface](ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, data T, client *exoscale.Client) { + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Set timeout + t, diags := data.GetTimeouts().Create(ctx, config.DefaultTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, t) + defer cancel() + + data.GenerateID() + + client, err := utils.SwitchClientZone( + ctx, + client, + exoscale.ZoneName(data.GetZone().ValueString()), + ) + if err != nil { + resp.Diagnostics.AddError( + "unable to change exoscale client zone", + err.Error(), + ) + return + } + + data.WaitForService(ctx, client, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + data.CreateResource(ctx, client, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + + tflog.Trace(ctx, "resource created", map[string]interface{}{ + "id": data.GetID(), + }) + +} + +func UpdateResource[T ResourceModelInterface](ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, stateData, planData T, client *exoscale.Client) { + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...) + // Read Terraform state data (for comparison) into the model + resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...) + if resp.Diagnostics.HasError() { + return + } + + // Set timeout + t, diags := stateData.GetTimeouts().Update(ctx, config.DefaultTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, t) + defer cancel() + + client, err := utils.SwitchClientZone( + ctx, + client, + exoscale.ZoneName(planData.GetZone().ValueString()), + ) + if err != nil { + resp.Diagnostics.AddError( + "unable to change exoscale client zone", + err.Error(), + ) + return + } + + planData.WaitForService(ctx, client, &resp.Diagnostics) + planData.UpdateResource(ctx, client, &diags) + + if resp.Diagnostics.HasError() { + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &planData)...) + + tflog.Trace(ctx, "resource updated", map[string]interface{}{ + "id": planData.GetID(), + }) +} + +func DeleteResource[T ResourceModelInterface](ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, data T, client *exoscale.Client) { + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Set timeout + t, diags := data.GetTimeouts().Delete(ctx, config.DefaultTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, t) + defer cancel() + + data.GenerateID() + + client, err := utils.SwitchClientZone( + ctx, + client, + exoscale.ZoneName(data.GetZone().ValueString()), + ) + if err != nil { + resp.Diagnostics.AddError( + "unable to change exoscale client zone", + err.Error(), + ) + return + } + + data.DeleteResource(ctx, client, &diags) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, "resource deleted", map[string]interface{}{ + "id": data.GetID(), + }) + +}