From 26286831b8016d8e760a0988a2266e0763238181 Mon Sep 17 00:00:00 2001 From: KallyDev Date: Wed, 18 Oct 2023 01:18:45 +0800 Subject: [PATCH] Support MySQL database advanced configuration (#1051) * Support MySQL database advanced configuration Resolved #1042. * Fix invalid interface conversion for MySQL long query time parameter Co-authored-by: Andrew Starr-Bochicchio * Add more test cases for MySQL config Co-authored-by: Andrew Starr-Bochicchio --------- Co-authored-by: Andrew Starr-Bochicchio --- .../resource_database_cluster_test.go | 11 + .../resource_database_mysql_config.go | 392 ++++++++++++++++++ .../resource_database_mysql_config_test.go | 45 ++ digitalocean/provider.go | 1 + docs/resources/database_mysql_config.md | 76 ++++ 5 files changed, 525 insertions(+) create mode 100644 digitalocean/database/resource_database_mysql_config.go create mode 100644 digitalocean/database/resource_database_mysql_config_test.go create mode 100644 docs/resources/database_mysql_config.md diff --git a/digitalocean/database/resource_database_cluster_test.go b/digitalocean/database/resource_database_cluster_test.go index c5b85abf0..1b6d00d41 100644 --- a/digitalocean/database/resource_database_cluster_test.go +++ b/digitalocean/database/resource_database_cluster_test.go @@ -784,6 +784,17 @@ resource "digitalocean_database_cluster" "foobar" { tags = ["production"] }` +const testAccCheckDigitalOceanDatabaseClusterMySQL = ` +resource "digitalocean_database_cluster" "foobar" { + name = "%s" + engine = "mysql" + version = "%s" + size = "db-s-1vcpu-1gb" + region = "nyc1" + node_count = 1 + tags = ["production"] +}` + const testAccCheckDigitalOceanDatabaseClusterConfigWithEvictionPolicy = ` resource "digitalocean_database_cluster" "foobar" { name = "%s" diff --git a/digitalocean/database/resource_database_mysql_config.go b/digitalocean/database/resource_database_mysql_config.go new file mode 100644 index 000000000..4e3391600 --- /dev/null +++ b/digitalocean/database/resource_database_mysql_config.go @@ -0,0 +1,392 @@ +package database + +import ( + "context" + "fmt" + "log" + + "github.com/digitalocean/godo" + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/config" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func ResourceDigitalOceanDatabaseMySQLConfig() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDigitalOceanDatabaseMySQLConfigCreate, + ReadContext: resourceDigitalOceanDatabaseMySQLConfigRead, + UpdateContext: resourceDigitalOceanDatabaseMySQLConfigUpdate, + DeleteContext: resourceDigitalOceanDatabaseMySQLConfigDelete, + Importer: &schema.ResourceImporter{ + State: resourceDigitalOceanDatabaseMySQLConfigImport, + }, + Schema: map[string]*schema.Schema{ + "cluster_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + "connect_timeout": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "default_time_zone": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "innodb_log_buffer_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "innodb_online_alter_log_max_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "innodb_lock_wait_timeout": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "interactive_timeout": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "max_allowed_packet": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "net_read_timeout": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "sort_buffer_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "sql_mode": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "sql_require_primary_key": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "wait_timeout": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "net_write_timeout": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "group_concat_max_len": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "information_schema_stats_expiry": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "innodb_ft_min_token_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "innodb_ft_server_stopword_table": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "innodb_print_all_deadlocks": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "innodb_rollback_on_timeout": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "internal_tmp_mem_storage_engine": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice( + []string{ + "TempTable", + "MEMORY", + }, + false, + ), + }, + "max_heap_table_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "tmp_table_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "slow_query_log": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "long_query_time": { + Type: schema.TypeFloat, + Optional: true, + Computed: true, + }, + "backup_hour": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "backup_minute": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "binlog_retention_period": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceDigitalOceanDatabaseMySQLConfigCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).GodoClient() + clusterID := d.Get("cluster_id").(string) + + if err := updateMySQLConfig(ctx, d, client); err != nil { + return diag.Errorf("Error updating MySQL configuration: %s", err) + } + + d.SetId(makeDatabaseMySQLConfigID(clusterID)) + + return resourceDigitalOceanDatabaseMySQLConfigRead(ctx, d, meta) +} + +func resourceDigitalOceanDatabaseMySQLConfigUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).GodoClient() + + if err := updateMySQLConfig(ctx, d, client); err != nil { + return diag.Errorf("Error updating MySQL configuration: %s", err) + } + + return resourceDigitalOceanDatabaseMySQLConfigRead(ctx, d, meta) +} + +func updateMySQLConfig(ctx context.Context, d *schema.ResourceData, client *godo.Client) error { + clusterID := d.Get("cluster_id").(string) + + opts := &godo.MySQLConfig{} + + if v, ok := d.GetOk("connect_timeout"); ok { + opts.ConnectTimeout = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("default_time_zone"); ok { + opts.DefaultTimeZone = godo.PtrTo(v.(string)) + } + + if v, ok := d.GetOk("innodb_log_buffer_size"); ok { + opts.InnodbLogBufferSize = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("innodb_online_alter_log_max_size"); ok { + opts.InnodbOnlineAlterLogMaxSize = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("innodb_lock_wait_timeout"); ok { + opts.InnodbLockWaitTimeout = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("interactive_timeout"); ok { + opts.InteractiveTimeout = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("max_allowed_packet"); ok { + opts.MaxAllowedPacket = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("net_read_timeout"); ok { + opts.NetReadTimeout = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("sort_buffer_size"); ok { + opts.SortBufferSize = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("sql_mode"); ok { + opts.SQLMode = godo.PtrTo(v.(string)) + } + + if v, ok := d.GetOk("sql_require_primary_key"); ok { + opts.SQLRequirePrimaryKey = godo.PtrTo(v.(bool)) + } + + if v, ok := d.GetOk("wait_timeout"); ok { + opts.WaitTimeout = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("net_write_timeout"); ok { + opts.NetWriteTimeout = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("group_concat_max_len"); ok { + opts.GroupConcatMaxLen = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("information_schema_stats_expiry"); ok { + opts.InformationSchemaStatsExpiry = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("innodb_ft_min_token_size"); ok { + opts.InnodbFtMinTokenSize = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("innodb_ft_server_stopword_table"); ok { + opts.InnodbFtServerStopwordTable = godo.PtrTo(v.(string)) + } + + if v, ok := d.GetOk("innodb_print_all_deadlocks"); ok { + opts.InnodbPrintAllDeadlocks = godo.PtrTo(v.(bool)) + } + + if v, ok := d.GetOk("innodb_rollback_on_timeout"); ok { + opts.InnodbRollbackOnTimeout = godo.PtrTo(v.(bool)) + } + + if v, ok := d.GetOk("internal_tmp_mem_storage_engine"); ok { + opts.InternalTmpMemStorageEngine = godo.PtrTo(v.(string)) + } + + if v, ok := d.GetOk("max_heap_table_size"); ok { + opts.MaxHeapTableSize = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("tmp_table_size"); ok { + opts.TmpTableSize = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("slow_query_log"); ok { + opts.SlowQueryLog = godo.PtrTo(v.(bool)) + } + + if v, ok := d.GetOk("long_query_time"); ok { + opts.LongQueryTime = godo.PtrTo(float32(v.(float64))) + } + + if v, ok := d.GetOk("backup_hour"); ok { + opts.BackupHour = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("backup_minute"); ok { + opts.BackupMinute = godo.PtrTo(v.(int)) + } + + if v, ok := d.GetOk("binlog_retention_period"); ok { + opts.BinlogRetentionPeriod = godo.PtrTo(v.(int)) + } + + log.Printf("[DEBUG] MySQL configuration: %s", godo.Stringify(opts)) + + if _, err := client.Databases.UpdateMySQLConfig(ctx, clusterID, opts); err != nil { + return err + } + + return nil +} + +func resourceDigitalOceanDatabaseMySQLConfigRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*config.CombinedConfig).GodoClient() + clusterID := d.Get("cluster_id").(string) + + config, resp, err := client.Databases.GetMySQLConfig(ctx, clusterID) + if err != nil { + if resp != nil && resp.StatusCode == 404 { + d.SetId("") + return nil + } + + return diag.Errorf("Error retrieving MySQL configuration: %s", err) + } + + d.Set("connect_timeout", config.ConnectTimeout) + d.Set("default_time_zone", config.DefaultTimeZone) + d.Set("innodb_log_buffer_size", config.InnodbLogBufferSize) + d.Set("innodb_online_alter_log_max_size", config.InnodbOnlineAlterLogMaxSize) + d.Set("innodb_lock_wait_timeout", config.InnodbLockWaitTimeout) + d.Set("interactive_timeout", config.InteractiveTimeout) + d.Set("max_allowed_packet", config.MaxAllowedPacket) + d.Set("net_read_timeout", config.NetReadTimeout) + d.Set("sort_buffer_size", config.SortBufferSize) + d.Set("sql_mode", config.SQLMode) + d.Set("sql_require_primary_key", config.SQLRequirePrimaryKey) + d.Set("wait_timeout", config.WaitTimeout) + d.Set("net_write_timeout", config.NetWriteTimeout) + d.Set("group_concat_max_len", config.GroupConcatMaxLen) + d.Set("information_schema_stats_expiry", config.InformationSchemaStatsExpiry) + d.Set("innodb_ft_min_token_size", config.InnodbFtMinTokenSize) + d.Set("innodb_ft_server_stopword_table", config.InnodbFtServerStopwordTable) + d.Set("innodb_print_all_deadlocks", config.InnodbPrintAllDeadlocks) + d.Set("innodb_rollback_on_timeout", config.InnodbRollbackOnTimeout) + d.Set("internal_tmp_mem_storage_engine", config.InternalTmpMemStorageEngine) + d.Set("max_heap_table_size", config.MaxHeapTableSize) + d.Set("tmp_table_size", config.TmpTableSize) + d.Set("slow_query_log", config.SlowQueryLog) + d.Set("long_query_time", config.LongQueryTime) + d.Set("backup_hour", config.BackupHour) + d.Set("backup_minute", config.BackupMinute) + d.Set("binlog_retention_period", config.BinlogRetentionPeriod) + + return nil +} + +func resourceDigitalOceanDatabaseMySQLConfigDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + d.SetId("") + + warn := []diag.Diagnostic{ + { + Severity: diag.Warning, + Summary: "digitalocean_database_mysql_config removed from state", + Detail: "Database configurations are only removed from state when destroyed. The remote configuration is not unset.", + }, + } + + return warn +} + +func resourceDigitalOceanDatabaseMySQLConfigImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + clusterID := d.Id() + + d.SetId(makeDatabaseMySQLConfigID(clusterID)) + d.Set("cluster_id", clusterID) + + return []*schema.ResourceData{d}, nil +} + +func makeDatabaseMySQLConfigID(clusterID string) string { + return fmt.Sprintf("%s/mysql-config", clusterID) +} diff --git a/digitalocean/database/resource_database_mysql_config_test.go b/digitalocean/database/resource_database_mysql_config_test.go new file mode 100644 index 000000000..4fda6773e --- /dev/null +++ b/digitalocean/database/resource_database_mysql_config_test.go @@ -0,0 +1,45 @@ +package database_test + +import ( + "fmt" + "testing" + + "github.com/digitalocean/terraform-provider-digitalocean/digitalocean/acceptance" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDigitalOceanDatabaseMySQLConfig_Basic(t *testing.T) { + name := acceptance.RandomTestName() + dbConfig := fmt.Sprintf(testAccCheckDigitalOceanDatabaseClusterMySQL, name, "8") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t) }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: testAccCheckDigitalOceanDatabaseClusterDestroy, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testAccCheckDigitalOceanDatabaseMySQLConfigConfigBasic, dbConfig, 10, "UTC"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("digitalocean_database_mysql_config.foobar", "connect_timeout", "10"), + resource.TestCheckResourceAttr("digitalocean_database_mysql_config.foobar", "default_time_zone", "UTC"), + ), + }, + { + Config: fmt.Sprintf(testAccCheckDigitalOceanDatabaseMySQLConfigConfigBasic, dbConfig, 15, "SYSTEM"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("digitalocean_database_mysql_config.foobar", "connect_timeout", "15"), + resource.TestCheckResourceAttr("digitalocean_database_mysql_config.foobar", "default_time_zone", "SYSTEM"), + ), + }, + }, + }) +} + +const testAccCheckDigitalOceanDatabaseMySQLConfigConfigBasic = ` +%s + +resource "digitalocean_database_mysql_config" "foobar" { + cluster_id = digitalocean_database_cluster.foobar.id + connect_timeout = %d + default_time_zone = "%s" +}` diff --git a/digitalocean/provider.go b/digitalocean/provider.go index 2f86c2f05..a7d9fe5fb 100644 --- a/digitalocean/provider.go +++ b/digitalocean/provider.go @@ -147,6 +147,7 @@ func Provider() *schema.Provider { "digitalocean_database_replica": database.ResourceDigitalOceanDatabaseReplica(), "digitalocean_database_user": database.ResourceDigitalOceanDatabaseUser(), "digitalocean_database_redis_config": database.ResourceDigitalOceanDatabaseRedisConfig(), + "digitalocean_database_mysql_config": database.ResourceDigitalOceanDatabaseMySQLConfig(), "digitalocean_domain": domain.ResourceDigitalOceanDomain(), "digitalocean_droplet": droplet.ResourceDigitalOceanDroplet(), "digitalocean_droplet_snapshot": snapshot.ResourceDigitalOceanDropletSnapshot(), diff --git a/docs/resources/database_mysql_config.md b/docs/resources/database_mysql_config.md new file mode 100644 index 000000000..dff886d96 --- /dev/null +++ b/docs/resources/database_mysql_config.md @@ -0,0 +1,76 @@ +--- +page_title: "DigitalOcean: digitalocean_database_mysql_config" +--- + +# digitalocean\_database\_mysql\_config + +Provides a virtual resource that can be used to change advanced configuration +options for a DigitalOcean managed MySQL database cluster. + +-> **Note** MySQL configurations are only removed from state when destroyed. The remote configuration is not unset. + +## Example Usage + +```hcl +resource "digitalocean_database_mysql_config" "example" { + cluster_id = digitalocean_database_cluster.example.id + connect_timeout = 10 + default_time_zone = "UTC" +} + +resource "digitalocean_database_cluster" "example" { + name = "example-mysql-cluster" + engine = "mysql" + version = "8" + size = "db-s-1vcpu-1gb" + region = "nyc1" + node_count = 1 +} +``` + + +## Argument Reference + +The following arguments are supported. See the [DigitalOcean API documentation](https://docs.digitalocean.com/reference/api/api-reference/#operation/databases_patch_config) +for additional details on each option. + +* `cluster_id` - (Required) The ID of the target MySQL cluster. +* `connect_timeout` - (Optional) The number of seconds that the mysqld server waits for a connect packet before responding with bad handshake. +* `default_time_zone` - (Optional) Default server time zone, in the form of an offset from UTC (from -12:00 to +12:00), a time zone name (EST), or `SYSTEM` to use the MySQL server default. +* `innodb_log_buffer_size` - (Optional) The size of the buffer, in bytes, that InnoDB uses to write to the log files. on disk. +* `innodb_online_alter_log_max_size` - (Optional) The upper limit, in bytes, of the size of the temporary log files used during online DDL operations for InnoDB tables. +* `innodb_lock_wait_timeout` - (Optional) The time, in seconds, that an InnoDB transaction waits for a row lock. before giving up. +* `interactive_timeout` - (Optional) The time, in seconds, the server waits for activity on an interactive. connection before closing it. +* `max_allowed_packet` - (Optional) The size of the largest message, in bytes, that can be received by the server. Default is `67108864` (64M). +* `net_read_timeout` - (Optional) The time, in seconds, to wait for more data from an existing connection. aborting the read. +* `sort_buffer_size` - (Optional) The sort buffer size, in bytes, for `ORDER BY` optimization. Default is `262144`. (256K). +* `sql_mode` - (Optional) Global SQL mode. If empty, uses MySQL server defaults. Must only include uppercase alphabetic characters, underscores, and commas. +* `sql_require_primary_key` - (Optional) Require primary key to be defined for new tables or old tables modified with ALTER TABLE and fail if missing. It is recommended to always have primary keys because various functionality may break if any large table is missing them. +* `wait_timeout` - (Optional) The number of seconds the server waits for activity on a noninteractive connection before closing it. +* `net_write_timeout` - (Optional) The number of seconds to wait for a block to be written to a connection before aborting the write. +* `group_concat_max_len` - (Optional) The maximum permitted result length, in bytes, for the `GROUP_CONCAT()` function. +* `information_schema_stats_expiry` - (Optional) The time, in seconds, before cached statistics expire. +* `innodb_ft_min_token_size` - (Optional) The minimum length of words that an InnoDB FULLTEXT index stores. +* `innodb_ft_server_stopword_table` - (Optional) The InnoDB FULLTEXT index stopword list for all InnoDB tables. +* `innodb_print_all_deadlocks` - (Optional) When enabled, records information about all deadlocks in InnoDB user transactions in the error log. Disabled by default. +* `innodb_rollback_on_timeout` - (Optional) When enabled, transaction timeouts cause InnoDB to abort and roll back the entire transaction. +* `internal_tmp_mem_storage_engine` - (Optional) The storage engine for in-memory internal temporary tables. Supported values are: `TempTable`, `MEMORY`. +* `max_heap_table_size` - (Optional) The maximum size, in bytes, of internal in-memory tables. Also set `tmp_table_size`. Default is `16777216` (16M) +* `tmp_table_size` - (Optional) The maximum size, in bytes, of internal in-memory tables. Also set `max_heap_table_size`. Default is `16777216` (16M). +* `slow_query_log` - (Optional) When enabled, captures slow queries. When disabled, also truncates the mysql.slow_log table. Default is false. +* `long_query_time` - (Optional) The time, in seconds, for a query to take to execute before being captured by `slow_query_logs`. Default is `10` seconds. +* `backup_hour` - (Optional) The hour of day (in UTC) when backup for the service starts. New backup only starts if previous backup has already completed. +* `backup_minute` - (Optional) The minute of the backup hour when backup for the service starts. New backup only starts if previous backup has already completed. +* `binlog_retention_period` - (Optional) The minimum amount of time, in seconds, to keep binlog entries before deletion. This may be extended for services that require binlog entries for longer than the default, for example if using the MySQL Debezium Kafka connector. + +## Attributes Reference + +All above attributes are exported. If an attribute was set outside of Terraform, it will be computed. + +## Import + +A MySQL database cluster's configuration can be imported using the `id` the parent cluster, e.g. + +``` +terraform import digitalocean_database_mysql_config.example 4b62829a-9c42-465b-aaa3-84051048e712 +```