From 33bf96356db65bb7960198335a42acea57dbcf6a Mon Sep 17 00:00:00 2001 From: Toni Kangas Date: Thu, 14 Nov 2024 11:32:54 +0200 Subject: [PATCH] feat(dbaas): add Valkey support, deprecate Redis --- CHANGELOG.md | 5 + go.mod | 2 +- go.sum | 4 +- .../service/database/data_source_sessions.go | 173 +++++++++++++++++- internal/service/database/database_test.go | 3 +- .../database/properties/properties_test.go | 3 +- internal/service/database/redis.go | 17 +- internal/service/database/user.go | 122 ++++++++++-- internal/service/database/valkey.go | 70 +++++++ .../service/objectstorage/objectstorage.go | 4 +- internal/utils/deprecation.go | 10 + subcategories.json | 2 + ...d_managed_database_valkey_sessions_test.go | 32 ++++ .../resource_upcloud_managed_database_test.go | 26 +++ ...ce_upcloud_managed_database_valkey_test.go | 64 +++++++ upcloud/sdkv2_provider.go | 2 + .../data_source_valkey_sessions_s1.tf | 10 + .../managed_database_s1.tf | 46 ++++- .../managed_database_s2.tf | 46 +++++ .../valkey_properties_s1.tf | 22 +++ .../valkey_properties_s2.tf | 21 +++ 21 files changed, 651 insertions(+), 33 deletions(-) create mode 100644 internal/service/database/valkey.go create mode 100644 internal/utils/deprecation.go create mode 100644 upcloud/datasource_upcloud_managed_database_valkey_sessions_test.go create mode 100644 upcloud/resource_upcloud_managed_database_valkey_test.go create mode 100644 upcloud/testdata/upcloud_managed_database/data_source_valkey_sessions_s1.tf create mode 100644 upcloud/testdata/upcloud_managed_database/valkey_properties_s1.tf create mode 100644 upcloud/testdata/upcloud_managed_database/valkey_properties_s2.tf diff --git a/CHANGELOG.md b/CHANGELOG.md index ec5921844..9ec30f513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,17 @@ See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/) - upcloud_managed_object_storage_bucket resource for managing buckets in managed object storage services. - upcloud_server: `index` field to `network_interfaces`. +- upcloud_managed_database_valkey: add support for Valkey. ### Changed - upcloud_managed_database_\*: Update available properties to match listing provided by the API, see [#626](https://github.com/UpCloudLtd/terraform-provider-upcloud/pull/626) for details. - upcloud_server: When modifying `network_interfaces`, match configured network interfaces to the server's actual network interfaces by `index` and `ip_address` (in addition to list order). This is to avoid public and utility network interfaces being re-assigned when the interfaces are re-ordered or when interface is removed from middle of the list. This might result to inaccurate diffs in the Terraform plan when interfaces are re-ordered or when interface is removed from middle of the list. We recommend explicitly setting the value for `index` in configuration, when interfaces are re-ordered or when interface is removed from middle of the list. +### Deprecated + +- upcloud_managed_database_redis: Redis is deprecated in favor of Valkey. Please use Valkey for new key value store instances. + ## [5.14.0] - 2024-10-28 ### Changed diff --git a/go.mod b/go.mod index cdee6cef7..c5ac5136d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 toolchain go1.22.5 require ( - github.com/UpCloudLtd/upcloud-go-api/v8 v8.11.0 + github.com/UpCloudLtd/upcloud-go-api/v8 v8.12.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/go-uuid v1.0.3 diff --git a/go.sum b/go.sum index 461dae88e..9823bfc49 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.1.0-alpha.0 h1:nHGfwXmFvJrSR9xu8qL7BkO4DqTHXE9N5vPhgY2I+j0= github.com/ProtonMail/go-crypto v1.1.0-alpha.0/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/UpCloudLtd/upcloud-go-api/v8 v8.11.0 h1:2eQLOlx+s5GlmUcyD9aihuYtWb0eC+dyZl3biu8Lb+8= -github.com/UpCloudLtd/upcloud-go-api/v8 v8.11.0/go.mod h1:bFnrOkfsDDmsb94nnBV5eSQjjsfDnwAzLnCt9+b4t/4= +github.com/UpCloudLtd/upcloud-go-api/v8 v8.12.0 h1:l4pOiBdNvpmZdQukGUqoEBIngFxvFWsNq8WnfHSzPJI= +github.com/UpCloudLtd/upcloud-go-api/v8 v8.12.0/go.mod h1:bFnrOkfsDDmsb94nnBV5eSQjjsfDnwAzLnCt9+b4t/4= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= diff --git a/internal/service/database/data_source_sessions.go b/internal/service/database/data_source_sessions.go index e8d6644fa..72378673a 100644 --- a/internal/service/database/data_source_sessions.go +++ b/internal/service/database/data_source_sessions.go @@ -37,8 +37,9 @@ func DataSourceSessionsPostgreSQL() *schema.Resource { func DataSourceSessionsRedis() *schema.Resource { return &schema.Resource{ - Description: "Current sessions of a Redis managed database", - ReadContext: dataSourceSessionsRedisRead, + Description: utils.DescriptionWithDeprecationWarning(redisDeprecationMessage, "Current sessions of a Redis managed database"), + DeprecationMessage: redisDeprecationMessage, + ReadContext: dataSourceSessionsRedisRead, Schema: utils.JoinSchemas( schemaDataSourceSessionsCommon(), schemaDataSourceSessionsRedis(), @@ -46,6 +47,17 @@ func DataSourceSessionsRedis() *schema.Resource { } } +func DataSourceSessionsValkey() *schema.Resource { + return &schema.Resource{ + Description: "Current sessions of a Valkey managed database", + ReadContext: dataSourceSessionsValkeyRead, + Schema: utils.JoinSchemas( + schemaDataSourceSessionsCommon(), + schemaDataSourceSessionsValkey(), + ), + } +} + func schemaDataSourceSessionsCommon() map[string]*schema.Schema { return map[string]*schema.Schema{ "limit": { @@ -111,6 +123,18 @@ func schemaDataSourceSessionsRedis() map[string]*schema.Schema { } } +func schemaDataSourceSessionsValkey() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "sessions": { + Description: "Current sessions", + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: schemaDatabaseSessionValkey(), + }, + } +} + func schemaDatabaseSessionMySQL() *schema.Resource { return &schema.Resource{ Description: "MySQL session", @@ -372,6 +396,102 @@ func schemaDatabaseSessionRedis() *schema.Resource { } } +func schemaDatabaseSessionValkey() *schema.Resource { + return &schema.Resource{ + Description: "Valkey session", + Schema: map[string]*schema.Schema{ + "active_channel_subscriptions": { + Description: "Number of active channel subscriptions", + Type: schema.TypeInt, + Computed: true, + }, + "active_database": { + Description: "Current database ID", + Type: schema.TypeString, + Computed: true, + }, + "active_pattern_matching_channel_subscriptions": { + Description: "Number of pattern matching subscriptions.", + Type: schema.TypeInt, + Computed: true, + }, + "application_name": { + Description: "Name of the application that is connected to this service.", + Type: schema.TypeString, + Computed: true, + }, + "client_addr": { + Description: "Number of pattern matching subscriptions.", + Type: schema.TypeString, + Computed: true, + }, + "connection_age": { + Description: "Total duration of the connection in nanoseconds.", + Type: schema.TypeInt, + Computed: true, + }, + "connection_idle": { + Description: "Idle time of the connection in nanoseconds.", + Type: schema.TypeInt, + Computed: true, + }, + "flags": { + Description: "A set containing flags' descriptions.", + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + "flags_raw": { + Description: "Client connection flags in raw string format.", + Type: schema.TypeString, + Computed: true, + }, + "id": { + Description: "Process ID of this session.", + Type: schema.TypeString, + Computed: true, + }, + "multi_exec_commands": { + Description: "Number of commands in a MULTI/EXEC context.", + Type: schema.TypeInt, + Computed: true, + }, + "output_buffer": { + Description: "Output buffer length.", + Type: schema.TypeInt, + Computed: true, + }, + "output_buffer_memory": { + Description: "Output buffer memory usage.", + Type: schema.TypeInt, + Computed: true, + }, + "output_list_length": { + Description: "Output list length (replies are queued in this list when the buffer is full).", + Type: schema.TypeInt, + Computed: true, + }, + "query": { + Description: "The last executed command.", + Type: schema.TypeString, + Computed: true, + }, + "query_buffer": { + Description: "Query buffer length (0 means no query pending).", + Type: schema.TypeInt, + Computed: true, + }, + "query_buffer_free": { + Description: "Free space of the query buffer (0 means the buffer is full).", + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + func dataSourceSessionsMySQLRead(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { return dataSourceSessionsRead(ctx, d, meta, upcloud.ManagedDatabaseServiceTypeMySQL) } @@ -381,7 +501,11 @@ func dataSourceSessionsPostgreSQLRead(ctx context.Context, d *schema.ResourceDat } func dataSourceSessionsRedisRead(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { - return dataSourceSessionsRead(ctx, d, meta, upcloud.ManagedDatabaseServiceTypeRedis) + return dataSourceSessionsRead(ctx, d, meta, upcloud.ManagedDatabaseServiceTypeRedis) //nolint:staticcheck // To be removed when Redis support has been removed +} + +func dataSourceSessionsValkeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { + return dataSourceSessionsRead(ctx, d, meta, upcloud.ManagedDatabaseServiceTypeValkey) } func dataSourceSessionsRead(ctx context.Context, d *schema.ResourceData, meta interface{}, serviceType upcloud.ManagedDatabaseServiceType) (diags diag.Diagnostics) { @@ -426,8 +550,13 @@ func dataSourceSessionsRead(ctx context.Context, d *schema.ResourceData, meta in if err := d.Set("sessions", buildSessionsPostgreSQL(sessions.PostgreSQL)); err != nil { return diag.FromErr(err) } - case upcloud.ManagedDatabaseServiceTypeRedis: - err := d.Set("sessions", buildSessionsRedis(sessions.Redis)) + case upcloud.ManagedDatabaseServiceTypeRedis: //nolint:staticcheck // To be removed when Redis support has been removed + err := d.Set("sessions", buildSessionsRedis(sessions.Redis)) //nolint:staticcheck // To be removed when Redis support has been removed + if err != nil { + return diag.FromErr(err) + } + case upcloud.ManagedDatabaseServiceTypeValkey: + err := d.Set("sessions", buildSessionsValkey(sessions.Valkey)) if err != nil { return diag.FromErr(err) } @@ -495,7 +624,39 @@ func buildSessionsPostgreSQL(sessions []upcloud.ManagedDatabaseSessionPostgreSQL return maps } -func buildSessionsRedis(sessions []upcloud.ManagedDatabaseSessionRedis) []map[string]interface{} { +func buildSessionsRedis(sessions []upcloud.ManagedDatabaseSessionRedis) []map[string]interface{} { //nolint:staticcheck // To be removed when Redis support has been removed + maps := make([]map[string]interface{}, 0) + + if len(sessions) == 0 { + return maps + } + + for _, session := range sessions { + maps = append(maps, map[string]interface{}{ + "active_channel_subscriptions": session.ActiveChannelSubscriptions, + "active_database": session.ActiveDatabase, + "active_pattern_matching_channel_subscriptions": session.ActivePatternMatchingChannelSubscriptions, + "application_name": session.ApplicationName, + "client_addr": session.ClientAddr, + "connection_age": session.ConnectionAge.Nanoseconds(), + "connection_idle": session.ConnectionIdle.Nanoseconds(), + "flags": session.Flags, + "flags_raw": session.FlagsRaw, + "id": session.Id, + "multi_exec_commands": session.MultiExecCommands, + "output_buffer": session.OutputBuffer, + "output_buffer_memory": session.OutputBufferMemory, + "output_list_length": session.OutputListLength, + "query": session.Query, + "query_buffer": session.QueryBuffer, + "query_buffer_free": session.QueryBufferFree, + }) + } + + return maps +} + +func buildSessionsValkey(sessions []upcloud.ManagedDatabaseSessionValkey) []map[string]interface{} { maps := make([]map[string]interface{}, 0) if len(sessions) == 0 { diff --git a/internal/service/database/database_test.go b/internal/service/database/database_test.go index 111e9797a..70a75fb9b 100644 --- a/internal/service/database/database_test.go +++ b/internal/service/database/database_test.go @@ -38,7 +38,8 @@ func TestDatabaseProperties(t *testing.T) { upcloud.ManagedDatabaseServiceTypeMySQL, upcloud.ManagedDatabaseServiceTypeOpenSearch, upcloud.ManagedDatabaseServiceTypePostgreSQL, - upcloud.ManagedDatabaseServiceTypeRedis, + upcloud.ManagedDatabaseServiceTypeRedis, //nolint:staticcheck // To be removed when Redis support has been removed + upcloud.ManagedDatabaseServiceTypeValkey, } for _, dbType := range dbTypes { diff --git a/internal/service/database/properties/properties_test.go b/internal/service/database/properties/properties_test.go index b5ad45752..5c47d5d11 100644 --- a/internal/service/database/properties/properties_test.go +++ b/internal/service/database/properties/properties_test.go @@ -12,7 +12,8 @@ func getTypes() []upcloud.ManagedDatabaseServiceType { upcloud.ManagedDatabaseServiceTypeMySQL, upcloud.ManagedDatabaseServiceTypeOpenSearch, upcloud.ManagedDatabaseServiceTypePostgreSQL, - upcloud.ManagedDatabaseServiceTypeRedis, + upcloud.ManagedDatabaseServiceTypeRedis, //nolint:staticcheck // To be removed when Redis support has been removed + upcloud.ManagedDatabaseServiceTypeValkey, } } diff --git a/internal/service/database/redis.go b/internal/service/database/redis.go index b9df84047..b278cf9a1 100644 --- a/internal/service/database/redis.go +++ b/internal/service/database/redis.go @@ -10,13 +10,16 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +const redisDeprecationMessage string = "Redis is deprecated in favor of Valkey. Please use Valkey for new key value store instances." + func ResourceRedis() *schema.Resource { return &schema.Resource{ - Description: serviceDescription("Redis"), - CreateContext: resourceRedisCreate, - ReadContext: resourceRedisRead, - UpdateContext: resourceRedisUpdate, - DeleteContext: resourceDatabaseDelete, + Description: utils.DescriptionWithDeprecationWarning(redisDeprecationMessage, serviceDescription("Redis")), + DeprecationMessage: redisDeprecationMessage, + CreateContext: resourceRedisCreate, + ReadContext: resourceRedisRead, + UpdateContext: resourceRedisUpdate, + DeleteContext: resourceDatabaseDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -28,7 +31,7 @@ func ResourceRedis() *schema.Resource { } func resourceRedisCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - if err := d.Set("type", string(upcloud.ManagedDatabaseServiceTypeRedis)); err != nil { + if err := d.Set("type", string(upcloud.ManagedDatabaseServiceTypeRedis)); err != nil { //nolint:staticcheck // To be removed when Redis support has been removed return diag.FromErr(err) } @@ -63,7 +66,7 @@ func schemaRedisEngine() map[string]*schema.Schema { Computed: true, MaxItems: 1, Elem: &schema.Resource{ - Schema: properties.GetSchemaMap(upcloud.ManagedDatabaseServiceTypeRedis), + Schema: properties.GetSchemaMap(upcloud.ManagedDatabaseServiceTypeRedis), //nolint:staticcheck // To be removed when Redis support has been removed }, }, } diff --git a/internal/service/database/user.go b/internal/service/database/user.go index 1e702bfbc..c65d3ec0b 100644 --- a/internal/service/database/user.go +++ b/internal/service/database/user.go @@ -67,7 +67,7 @@ func schemaUser() map[string]*schema.Schema { }, "pg_access_control": { Description: "PostgreSQL access control object.", - ConflictsWith: []string{"redis_access_control", "opensearch_access_control"}, + ConflictsWith: []string{"redis_access_control", "opensearch_access_control", "valkey_access_control"}, Type: schema.TypeList, Optional: true, MaxItems: 1, @@ -84,6 +84,15 @@ func schemaUser() map[string]*schema.Schema { Schema: schemaRedisUserAccessControl(), }, }, + "valkey_access_control": { + Description: "Valkey access control object.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: schemaValkeyUserAccessControl(), + }, + }, "opensearch_access_control": { Description: "OpenSearch access control object.", Type: schema.TypeList, @@ -148,6 +157,47 @@ func schemaRedisUserAccessControl() map[string]*schema.Schema { } } +func schemaValkeyUserAccessControl() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "categories": { + Description: "Set access control to all commands in specified categories.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "channels": { + Description: "Set access control to Pub/Sub channels.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "commands": { + Description: "Set access control to commands.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "keys": { + Description: "Set access control to keys.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + } +} + func schemaOpenSearchUserAccessControl() map[string]*schema.Schema { return map[string]*schema.Schema{ "rules": { @@ -218,10 +268,12 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, meta interf AllowReplication: &v, } } - case upcloud.ManagedDatabaseServiceTypeRedis: - req.RedisAccessControl = redisAccessControlFromResourceData(d) + case upcloud.ManagedDatabaseServiceTypeRedis: //nolint:staticcheck // To be removed when Redis support has been removed + req.RedisAccessControl = redisAccessControlFromResourceData(d) //nolint:staticcheck // To be removed when Redis support has been removed case upcloud.ManagedDatabaseServiceTypeOpenSearch: req.OpenSearchAccessControl = openSearchAccessControlFromResourceData(d) + case upcloud.ManagedDatabaseServiceTypeValkey: + req.ValkeyAccessControl = valkeyAccessControlFromResourceData(d) } if _, err = client.CreateManagedDatabaseUser(ctx, req); err != nil { @@ -314,7 +366,7 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, meta interf return diag.FromErr(err) } } - case upcloud.ManagedDatabaseServiceTypeRedis: + case upcloud.ManagedDatabaseServiceTypeRedis: //nolint:staticcheck // To be removed when Redis support has been removed if d.HasChange("redis_access_control.0") { if _, err := modifyRedisUserAccessControl(ctx, client, d); err != nil { return diag.FromErr(err) @@ -326,6 +378,12 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, meta interf return diag.FromErr(err) } } + case upcloud.ManagedDatabaseServiceTypeValkey: + if d.HasChange("valkey_access_control.0") { + if _, err := modifyValkeyUserAccessControl(ctx, client, d); err != nil { + return diag.FromErr(err) + } + } } tflog.Info(ctx, "managed database user updated", map[string]interface{}{ "service_name": serviceDetails.Name, "username": username, "service_uuid": serviceID, @@ -403,13 +461,13 @@ func copyUserDetailsToResource(d *schema.ResourceData, details *upcloud.ManagedD } } - if details.RedisAccessControl != nil { + if details.RedisAccessControl != nil { //nolint:staticcheck // To be removed when Redis support has been removed if err := d.Set("redis_access_control", []map[string][]string{ { - "categories": *details.RedisAccessControl.Categories, - "channels": *details.RedisAccessControl.Channels, - "commands": *details.RedisAccessControl.Commands, - "keys": *details.RedisAccessControl.Keys, + "categories": *details.RedisAccessControl.Categories, //nolint:staticcheck // To be removed when Redis support has been removed + "channels": *details.RedisAccessControl.Channels, //nolint:staticcheck // To be removed when Redis support has been removed + "commands": *details.RedisAccessControl.Commands, //nolint:staticcheck // To be removed when Redis support has been removed + "keys": *details.RedisAccessControl.Keys, //nolint:staticcheck // To be removed when Redis support has been removed }, }); err != nil { return diag.FromErr(err) @@ -457,8 +515,17 @@ func modifyRedisUserAccessControl(ctx context.Context, svc *service.Service, d * return svc.ModifyManagedDatabaseUserAccessControl(ctx, req) } -func redisAccessControlFromResourceData(d *schema.ResourceData) *upcloud.ManagedDatabaseUserRedisAccessControl { - acl := &upcloud.ManagedDatabaseUserRedisAccessControl{} +func modifyValkeyUserAccessControl(ctx context.Context, svc *service.Service, d *schema.ResourceData) (*upcloud.ManagedDatabaseUser, error) { + req := &request.ModifyManagedDatabaseUserAccessControlRequest{ + ServiceUUID: d.Get("service").(string), + Username: d.Get("username").(string), + ValkeyAccessControl: valkeyAccessControlFromResourceData(d), + } + return svc.ModifyManagedDatabaseUserAccessControl(ctx, req) +} + +func redisAccessControlFromResourceData(d *schema.ResourceData) *upcloud.ManagedDatabaseUserRedisAccessControl { //nolint:staticcheck // To be removed when Redis support has been removed + acl := &upcloud.ManagedDatabaseUserRedisAccessControl{} //nolint:staticcheck // To be removed when Redis support has been removed if v, ok := d.Get("redis_access_control.0.categories").([]interface{}); ok { categories := make([]string, len(v)) for i := range v { @@ -490,6 +557,39 @@ func redisAccessControlFromResourceData(d *schema.ResourceData) *upcloud.Managed return acl } +func valkeyAccessControlFromResourceData(d *schema.ResourceData) *upcloud.ManagedDatabaseUserValkeyAccessControl { + acl := &upcloud.ManagedDatabaseUserValkeyAccessControl{} + if v, ok := d.Get("valkey_access_control.0.categories").([]interface{}); ok { + categories := make([]string, len(v)) + for i := range v { + categories[i] = v[i].(string) + } + acl.Categories = &categories + } + if v, ok := d.Get("valkey_access_control.0.channels").([]interface{}); ok { + channels := make([]string, len(v)) + for i := range v { + channels[i] = v[i].(string) + } + acl.Channels = &channels + } + if v, ok := d.Get("valkey_access_control.0.commands").([]interface{}); ok { + commands := make([]string, len(v)) + for i := range v { + commands[i] = v[i].(string) + } + acl.Commands = &commands + } + if v, ok := d.Get("valkey_access_control.0.keys").([]interface{}); ok { + keys := make([]string, len(v)) + for i := range v { + keys[i] = v[i].(string) + } + acl.Keys = &keys + } + return acl +} + func openSearchAccessControlFromResourceData(d *schema.ResourceData) *upcloud.ManagedDatabaseUserOpenSearchAccessControl { acl := &upcloud.ManagedDatabaseUserOpenSearchAccessControl{} if v, ok := d.Get("opensearch_access_control.0.rules").([]interface{}); ok { diff --git a/internal/service/database/valkey.go b/internal/service/database/valkey.go new file mode 100644 index 000000000..971610cf3 --- /dev/null +++ b/internal/service/database/valkey.go @@ -0,0 +1,70 @@ +package database + +import ( + "context" + + "github.com/UpCloudLtd/terraform-provider-upcloud/internal/service/database/properties" + "github.com/UpCloudLtd/terraform-provider-upcloud/internal/utils" + "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func ResourceValkey() *schema.Resource { + return &schema.Resource{ + Description: serviceDescription("Valkey"), + CreateContext: resourceValkeyCreate, + ReadContext: resourceValkeyRead, + UpdateContext: resourceValkeyUpdate, + DeleteContext: resourceDatabaseDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: utils.JoinSchemas( + schemaDatabaseCommon(), + schemaValkeyEngine(), + ), + } +} + +func resourceValkeyCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + if err := d.Set("type", string(upcloud.ManagedDatabaseServiceTypeValkey)); err != nil { + return diag.FromErr(err) + } + + diags := resourceDatabaseCreate(ctx, d, meta) + if diags.HasError() { + return diags + } + + return resourceValkeyRead(ctx, d, meta) +} + +func resourceValkeyRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + return resourceDatabaseRead(ctx, d, meta) +} + +func resourceValkeyUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + diags := resourceDatabaseUpdate(ctx, d, meta) + if diags.HasError() { + return diags + } + + diags = append(diags, resourceValkeyRead(ctx, d, meta)...) + return diags +} + +func schemaValkeyEngine() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "properties": { + Description: "Database Engine properties for Valkey", + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: properties.GetSchemaMap(upcloud.ManagedDatabaseServiceTypeValkey), + }, + }, + } +} diff --git a/internal/service/objectstorage/objectstorage.go b/internal/service/objectstorage/objectstorage.go index 31394adb3..3d3993e8a 100644 --- a/internal/service/objectstorage/objectstorage.go +++ b/internal/service/objectstorage/objectstorage.go @@ -34,9 +34,7 @@ type objectStorageKeyType string func ResourceObjectStorage() *schema.Resource { return &schema.Resource{ - Description: fmt.Sprintf(`~> %s - -This resource represents an UpCloud Object Storage instance, which provides S3 compatible storage.`, deprecationMessage), + Description: utils.DescriptionWithDeprecationWarning(deprecationMessage, "This resource represents an UpCloud Object Storage instance, which provides S3 compatible storage."), DeprecationMessage: deprecationMessage, CreateContext: resourceObjectStorageCreate, ReadContext: resourceObjectStorageRead, diff --git a/internal/utils/deprecation.go b/internal/utils/deprecation.go new file mode 100644 index 000000000..bc815a7ef --- /dev/null +++ b/internal/utils/deprecation.go @@ -0,0 +1,10 @@ +package utils + +import "fmt" + +// DescriptionWithDeprecationWarning adds deprecation message to the description in a warning box before the actual description. Remember to also define DeprecationMessage for the resource/data-source. +func DescriptionWithDeprecationWarning(deprecationMessage, description string) string { + return fmt.Sprintf(`~> %s + +%s`, deprecationMessage, description) +} diff --git a/subcategories.json b/subcategories.json index 7295d851b..f8350b24c 100644 --- a/subcategories.json +++ b/subcategories.json @@ -10,6 +10,7 @@ "managed_database_opensearch_indices.md": "Databases", "managed_database_postgresql_sessions.md": "Databases", "managed_database_redis_sessions.md": "Databases", + "managed_database_valkey_sessions.md": "Databases", "kubernetes_cluster.md": "Kubernetes", "hosts.md": "Cloud", "tags.md": "Cloud", @@ -42,6 +43,7 @@ "managed_database_opensearch.md": "Databases", "managed_database_postgresql.md": "Databases", "managed_database_redis.md": "Databases", + "managed_database_valkey.md": "Databases", "managed_database_user.md": "Databases", "loadbalancer.md": "Load Balancer", "loadbalancer_backend.md": "Load Balancer", diff --git a/upcloud/datasource_upcloud_managed_database_valkey_sessions_test.go b/upcloud/datasource_upcloud_managed_database_valkey_sessions_test.go new file mode 100644 index 000000000..b61002f3b --- /dev/null +++ b/upcloud/datasource_upcloud_managed_database_valkey_sessions_test.go @@ -0,0 +1,32 @@ +package upcloud + +import ( + "testing" + + "github.com/UpCloudLtd/terraform-provider-upcloud/internal/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceUpcloudManagedDatabaseValkeySessions(t *testing.T) { + testDataS1 := utils.ReadTestDataFile(t, "testdata/upcloud_managed_database/data_source_valkey_sessions_s1.tf") + + name := "data.upcloud_managed_database_valkey_sessions.valkey_sessions" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testDataS1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(name, "service"), + resource.TestCheckTypeSetElemNestedAttrs(name, "sessions.*", map[string]string{ + "query": "info", + }), + resource.TestCheckTypeSetElemNestedAttrs(name, "sessions.*", map[string]string{ + "query": "ping", + }), + ), + }, + }, + }) +} diff --git a/upcloud/resource_upcloud_managed_database_test.go b/upcloud/resource_upcloud_managed_database_test.go index 9fec11f78..d0f10f244 100644 --- a/upcloud/resource_upcloud_managed_database_test.go +++ b/upcloud/resource_upcloud_managed_database_test.go @@ -25,7 +25,9 @@ func TestAccUpcloudManagedDatabase(t *testing.T) { userName2 := "upcloud_managed_database_user.db_user_2" userName3 := "upcloud_managed_database_user.db_user_3" userName4 := "upcloud_managed_database_user.db_user_4" + userName5 := "upcloud_managed_database_user.db_user_5" redisName := "upcloud_managed_database_redis.r1" + valkeyName := "upcloud_managed_database_valkey.v1" verifyImportStep := func(name string) resource.TestStep { return resource.TestStep{ @@ -87,6 +89,11 @@ func TestAccUpcloudManagedDatabase(t *testing.T) { resource.TestCheckResourceAttr(userName3, "redis_access_control.0.commands.0", "+set"), resource.TestCheckResourceAttr(userName3, "redis_access_control.0.keys.0", "key_*"), + resource.TestCheckResourceAttr(userName5, "valkey_access_control.0.categories.0", "+@set"), + resource.TestCheckResourceAttr(userName5, "valkey_access_control.0.channels.0", "*"), + resource.TestCheckResourceAttr(userName5, "valkey_access_control.0.commands.0", "+set"), + resource.TestCheckResourceAttr(userName5, "valkey_access_control.0.keys.0", "key_*"), + resource.TestCheckResourceAttr(userName4, "opensearch_access_control.0.rules.0.index", ".opensearch-observability"), resource.TestCheckResourceAttr(userName4, "opensearch_access_control.0.rules.0.permission", "admin"), @@ -96,12 +103,20 @@ func TestAccUpcloudManagedDatabase(t *testing.T) { resource.TestCheckResourceAttr(redisName, "zone", "pl-waw1"), resource.TestCheckResourceAttr(redisName, "powered", "true"), resource.TestCheckResourceAttr(redisName, "network.#", "1"), + + resource.TestCheckResourceAttr(valkeyName, "name", withPrefixDB("valkey-1")), + resource.TestCheckResourceAttr(valkeyName, "plan", "1x1xCPU-2GB"), + resource.TestCheckResourceAttr(valkeyName, "title", withPrefixDB("valkey-1")), + resource.TestCheckResourceAttr(valkeyName, "zone", "pl-waw1"), + resource.TestCheckResourceAttr(valkeyName, "powered", "true"), + resource.TestCheckResourceAttr(valkeyName, "network.#", "1"), ), }, verifyImportStep(pg1Name), verifyImportStep(pg2Name), verifyImportStep(msql1Name), verifyImportStep(redisName), + verifyImportStep(valkeyName), verifyImportStep(lgDBName), verifyImportStep(userName1), verifyImportStep(userName2), @@ -143,6 +158,17 @@ func TestAccUpcloudManagedDatabase(t *testing.T) { resource.TestCheckResourceAttr(redisName, "title", withPrefixDB("redis-1-updated")), resource.TestCheckResourceAttr(redisName, "zone", "pl-waw1"), resource.TestCheckResourceAttr(redisName, "powered", "true"), + + resource.TestCheckResourceAttr(userName5, "valkey_access_control.0.categories.#", "0"), + resource.TestCheckResourceAttr(userName5, "valkey_access_control.0.channels.#", "0"), + resource.TestCheckResourceAttr(userName5, "valkey_access_control.0.commands.#", "0"), + resource.TestCheckResourceAttr(userName5, "valkey_access_control.0.keys.0", "key*"), + + resource.TestCheckResourceAttr(valkeyName, "name", withPrefixDB("valkey-1")), + resource.TestCheckResourceAttr(valkeyName, "plan", "1x1xCPU-2GB"), + resource.TestCheckResourceAttr(valkeyName, "title", withPrefixDB("valkey-1-updated")), + resource.TestCheckResourceAttr(valkeyName, "zone", "pl-waw1"), + resource.TestCheckResourceAttr(valkeyName, "powered", "true"), ), }, }, diff --git a/upcloud/resource_upcloud_managed_database_valkey_test.go b/upcloud/resource_upcloud_managed_database_valkey_test.go new file mode 100644 index 000000000..d810f14fe --- /dev/null +++ b/upcloud/resource_upcloud_managed_database_valkey_test.go @@ -0,0 +1,64 @@ +package upcloud + +import ( + "fmt" + "testing" + + "github.com/UpCloudLtd/terraform-provider-upcloud/internal/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccUpcloudManagedDatabaseValkeyProperties(t *testing.T) { + testDataS1 := utils.ReadTestDataFile(t, "testdata/upcloud_managed_database/valkey_properties_s1.tf") + testDataS2 := utils.ReadTestDataFile(t, "testdata/upcloud_managed_database/valkey_properties_s2.tf") + + name := "upcloud_managed_database_valkey.valkey_properties" + prop := func(name string) string { + return fmt.Sprintf("properties.0.%s", name) + } + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testDataS1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "plan", "1x1xCPU-2GB"), + resource.TestCheckResourceAttr(name, "zone", "fi-hel2"), + resource.TestCheckResourceAttr(name, prop("public_access"), "false"), + resource.TestCheckResourceAttr(name, prop("valkey_lfu_decay_time"), "2"), + resource.TestCheckResourceAttr(name, prop("valkey_number_of_databases"), "2"), + resource.TestCheckResourceAttr(name, prop("valkey_notify_keyspace_events"), "KEA"), + resource.TestCheckResourceAttr(name, prop("valkey_pubsub_client_output_buffer_limit"), "128"), + resource.TestCheckResourceAttr(name, prop("valkey_ssl"), "false"), + resource.TestCheckResourceAttr(name, prop("valkey_lfu_log_factor"), "11"), + resource.TestCheckResourceAttr(name, prop("valkey_io_threads"), "1"), + resource.TestCheckResourceAttr(name, prop("valkey_maxmemory_policy"), "allkeys-lru"), + resource.TestCheckResourceAttr(name, prop("valkey_persistence"), "off"), + resource.TestCheckResourceAttr(name, prop("valkey_timeout"), "310"), + resource.TestCheckResourceAttr(name, prop("valkey_acl_channels_default"), "allchannels"), + resource.TestCheckResourceAttr(name, prop("automatic_utility_network_ip_filter"), "false"), + resource.TestCheckResourceAttr(name, prop("service_log"), "true"), + ), + }, + { + Config: testDataS2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, prop("public_access"), "true"), + resource.TestCheckResourceAttr(name, prop("valkey_lfu_decay_time"), "1"), + resource.TestCheckResourceAttr(name, prop("valkey_number_of_databases"), "3"), + resource.TestCheckResourceAttr(name, prop("valkey_notify_keyspace_events"), ""), + resource.TestCheckResourceAttr(name, prop("valkey_pubsub_client_output_buffer_limit"), "256"), + resource.TestCheckResourceAttr(name, prop("valkey_ssl"), "true"), + resource.TestCheckResourceAttr(name, prop("valkey_lfu_log_factor"), "12"), + resource.TestCheckResourceAttr(name, prop("valkey_io_threads"), "1"), + resource.TestCheckResourceAttr(name, prop("valkey_maxmemory_policy"), "volatile-lru"), + resource.TestCheckResourceAttr(name, prop("valkey_persistence"), "rdb"), + resource.TestCheckResourceAttr(name, prop("valkey_timeout"), "320"), + resource.TestCheckResourceAttr(name, prop("valkey_acl_channels_default"), "resetchannels"), + resource.TestCheckResourceAttr(name, prop("automatic_utility_network_ip_filter"), "true"), + ), + }, + }, + }) +} diff --git a/upcloud/sdkv2_provider.go b/upcloud/sdkv2_provider.go index da36a4b4d..3821651cd 100644 --- a/upcloud/sdkv2_provider.go +++ b/upcloud/sdkv2_provider.go @@ -77,6 +77,7 @@ func Provider() *schema.Provider { "upcloud_managed_database_mysql": database.ResourceMySQL(), "upcloud_managed_database_redis": database.ResourceRedis(), "upcloud_managed_database_opensearch": database.ResourceOpenSearch(), + "upcloud_managed_database_valkey": database.ResourceValkey(), "upcloud_managed_database_user": database.ResourceUser(), "upcloud_managed_database_logical_database": database.ResourceLogicalDatabase(), "upcloud_managed_object_storage": managedobjectstorage.ResourceManagedObjectStorage(), @@ -92,6 +93,7 @@ func Provider() *schema.Provider { "upcloud_managed_database_mysql_sessions": database.DataSourceSessionsMySQL(), "upcloud_managed_database_postgresql_sessions": database.DataSourceSessionsPostgreSQL(), "upcloud_managed_database_redis_sessions": database.DataSourceSessionsRedis(), + "upcloud_managed_database_valkey_sessions": database.DataSourceSessionsValkey(), "upcloud_managed_object_storage_policies": managedobjectstorage.DataSourceManagedObjectStoragePolicies(), }, diff --git a/upcloud/testdata/upcloud_managed_database/data_source_valkey_sessions_s1.tf b/upcloud/testdata/upcloud_managed_database/data_source_valkey_sessions_s1.tf new file mode 100644 index 000000000..ac314c638 --- /dev/null +++ b/upcloud/testdata/upcloud_managed_database/data_source_valkey_sessions_s1.tf @@ -0,0 +1,10 @@ +resource "upcloud_managed_database_valkey" "valkey_sessions" { + name = "tf-acc-test-valkey-sessions-1" + title = "tf-acc-test-valkey-sessions-1" + plan = "1x1xCPU-2GB" + zone = "fi-hel2" +} + +data "upcloud_managed_database_valkey_sessions" "valkey_sessions" { + service = upcloud_managed_database_valkey.valkey_sessions.id +} diff --git a/upcloud/testdata/upcloud_managed_database/managed_database_s1.tf b/upcloud/testdata/upcloud_managed_database/managed_database_s1.tf index 9785e80bc..bbef164e4 100644 --- a/upcloud/testdata/upcloud_managed_database/managed_database_s1.tf +++ b/upcloud/testdata/upcloud_managed_database/managed_database_s1.tf @@ -16,6 +16,10 @@ resource "upcloud_router" "r1" { name = "${var.prefix}router-r1" } +resource "upcloud_router" "v1" { + name = "${var.prefix}router-v1" +} + resource "upcloud_network" "pg2" { name = "${var.prefix}net-pg2" zone = var.zone @@ -42,6 +46,19 @@ resource "upcloud_network" "r1" { router = upcloud_router.r1.id } +resource "upcloud_network" "v1" { + name = "${var.prefix}net-v1" + zone = var.zone + + ip_network { + address = "172.18.104.0/24" + dhcp = false + family = "IPv4" + } + + router = upcloud_router.v1.id +} + resource "upcloud_managed_database_postgresql" "pg1" { name = "${var.prefix}pg-1" plan = "1x1xCPU-2GB-25GB" @@ -108,12 +125,27 @@ resource "upcloud_managed_database_redis" "r1" { // Attach network on create network { family = "IPv4" - name = "${var.prefix}net-r1" + name = "${var.prefix}net-1" type = "private" uuid = upcloud_network.r1.id } } +resource "upcloud_managed_database_valkey" "v1" { + name = "${var.prefix}valkey-1" + plan = "1x1xCPU-2GB" + title = "${var.prefix}valkey-1" + zone = var.zone + + // Attach network on create + network { + family = "IPv4" + name = "${var.prefix}net-v1" + type = "private" + uuid = upcloud_network.v1.id + } +} + resource "upcloud_managed_database_opensearch" "o1" { name = "${var.prefix}opensearch-1" plan = "1x2xCPU-4GB-80GB-1D" @@ -164,3 +196,15 @@ resource "upcloud_managed_database_user" "db_user_4" { } } } + +resource "upcloud_managed_database_user" "db_user_5" { + service = upcloud_managed_database_valkey.v1.id + username = "somename" + password = "Superpass123" + valkey_access_control { + categories = ["+@set"] + channels = ["*"] + commands = ["+set"] + keys = ["key_*"] + } +} diff --git a/upcloud/testdata/upcloud_managed_database/managed_database_s2.tf b/upcloud/testdata/upcloud_managed_database/managed_database_s2.tf index 2dcf5c375..026dfcc56 100644 --- a/upcloud/testdata/upcloud_managed_database/managed_database_s2.tf +++ b/upcloud/testdata/upcloud_managed_database/managed_database_s2.tf @@ -16,6 +16,10 @@ resource "upcloud_router" "r1" { name = "${var.prefix}router-r1" } +resource "upcloud_router" "v1" { + name = "${var.prefix}router-v1" +} + resource "upcloud_router" "msql1" { name = "${var.prefix}router-msql1" } @@ -46,6 +50,19 @@ resource "upcloud_network" "r1" { router = upcloud_router.r1.id } +resource "upcloud_network" "v1" { + name = "${var.prefix}net-v1" + zone = var.zone + + ip_network { + address = "172.18.104.0/24" + dhcp = false + family = "IPv4" + } + + router = upcloud_router.v1.id +} + resource "upcloud_network" "msql1" { name = "${var.prefix}net-msql1" zone = var.zone @@ -139,6 +156,26 @@ resource "upcloud_managed_database_redis" "r1" { } } +resource "upcloud_managed_database_valkey" "v1" { + name = "${var.prefix}valkey-1" + plan = "1x1xCPU-2GB" + title = "${var.prefix}valkey-1-updated" + zone = var.zone + + // No change in network + network { + family = "IPv4" + name = "${var.prefix}net-v1" + type = "private" + uuid = upcloud_network.v1.id + } + + labels = { + test = "" + managed-by = "team-devex" + } +} + resource "upcloud_managed_database_user" "db_user_1" { service = upcloud_managed_database_mysql.msql1.id username = "somename" @@ -163,3 +200,12 @@ resource "upcloud_managed_database_user" "db_user_3" { keys = ["key*"] } } + +resource "upcloud_managed_database_user" "db_user_5" { + service = upcloud_managed_database_valkey.v1.id + username = "somename" + password = "Superpass123" + valkey_access_control { + keys = ["key*"] + } +} diff --git a/upcloud/testdata/upcloud_managed_database/valkey_properties_s1.tf b/upcloud/testdata/upcloud_managed_database/valkey_properties_s1.tf new file mode 100644 index 000000000..ad5ce3f96 --- /dev/null +++ b/upcloud/testdata/upcloud_managed_database/valkey_properties_s1.tf @@ -0,0 +1,22 @@ +resource "upcloud_managed_database_valkey" "valkey_properties" { + name = "tf-valkey-properties-test" + title = "tf-valkey-properties-test" + plan = "1x1xCPU-2GB" + zone = "fi-hel2" + properties { + automatic_utility_network_ip_filter = false + public_access = false + valkey_lfu_decay_time = 2 + valkey_number_of_databases = 2 + valkey_notify_keyspace_events = "KEA" + valkey_pubsub_client_output_buffer_limit = 128 + valkey_ssl = false + valkey_lfu_log_factor = 11 + valkey_io_threads = 1 + valkey_maxmemory_policy = "allkeys-lru" + valkey_persistence = "off" + valkey_timeout = 310 + valkey_acl_channels_default = "allchannels" + service_log = true + } +} diff --git a/upcloud/testdata/upcloud_managed_database/valkey_properties_s2.tf b/upcloud/testdata/upcloud_managed_database/valkey_properties_s2.tf new file mode 100644 index 000000000..d3191231e --- /dev/null +++ b/upcloud/testdata/upcloud_managed_database/valkey_properties_s2.tf @@ -0,0 +1,21 @@ +resource "upcloud_managed_database_valkey" "valkey_properties" { + name = "tf-valkey-properties-test" + title = "tf-valkey-properties-test" + plan = "1x1xCPU-2GB" + zone = "fi-hel2" + properties { + automatic_utility_network_ip_filter = true + public_access = true + valkey_lfu_decay_time = 1 + valkey_number_of_databases = 3 + valkey_notify_keyspace_events = "" + valkey_pubsub_client_output_buffer_limit = 256 + valkey_ssl = true + valkey_lfu_log_factor = 12 + valkey_io_threads = 1 + valkey_maxmemory_policy = "volatile-lru" + valkey_persistence = "rdb" + valkey_timeout = 320 + valkey_acl_channels_default = "resetchannels" + } +}