diff --git a/docs/data-sources/rabbitmq_instance.md b/docs/data-sources/rabbitmq_instance.md index 04dffe39..7881ba9d 100644 --- a/docs/data-sources/rabbitmq_instance.md +++ b/docs/data-sources/rabbitmq_instance.md @@ -46,4 +46,16 @@ data "stackit_rabbitmq_instance" "example" { Read-Only: +- `consumer_timeout` (Number) +- `enable_monitoring` (Boolean) +- `graphite` (String) +- `max_disk_threshold` (Number) +- `metrics_frequency` (Number) +- `metrics_prefix` (String) +- `monitoring_instance_id` (String) +- `plugins` (List of String) +- `roles` (List of String) - `sgw_acl` (String) +- `syslog` (List of String) +- `tls_ciphers` (List of String) +- `tls_protocols` (String) diff --git a/docs/data-sources/redis_instance.md b/docs/data-sources/redis_instance.md index 0f7734eb..2e65f240 100644 --- a/docs/data-sources/redis_instance.md +++ b/docs/data-sources/redis_instance.md @@ -46,4 +46,25 @@ data "stackit_redis_instance" "example" { Read-Only: +- `down_after_milliseconds` (Number) +- `enable_monitoring` (Boolean) +- `failover_timeout` (Number) +- `graphite` (String) +- `lazyfree_lazy_eviction` (String) +- `lazyfree_lazy_expire` (String) +- `lua_time_limit` (Number) +- `max_disk_threshold` (Number) +- `maxclients` (Number) +- `maxmemory_policy` (String) +- `maxmemory_samples` (Number) +- `metrics_frequency` (Number) +- `metrics_prefix` (String) +- `min_replicas_max_lag` (Number) +- `monitoring_instance_id` (String) +- `notify_keyspace_events` (String) - `sgw_acl` (String) +- `snapshot` (String) +- `syslog` (List of String) +- `tls_ciphers` (List of String) +- `tls_ciphersuites` (String) +- `tls_protocols` (String) diff --git a/docs/resources/rabbitmq_instance.md b/docs/resources/rabbitmq_instance.md index b029e1f7..53cfc979 100644 --- a/docs/resources/rabbitmq_instance.md +++ b/docs/resources/rabbitmq_instance.md @@ -19,7 +19,10 @@ resource "stackit_rabbitmq_instance" "example" { version = "10" plan_name = "example-plan-name" parameters = { - sgw_acl = "x.x.x.x/x,y.y.y.y/y" + sgw_acl = "x.x.x.x/x,y.y.y.y/y" + consumer_timeout = 18000000 + enable_monitoring = false + plugins = ["example-plugin1", "example-plugin2"] } } ``` @@ -54,4 +57,16 @@ resource "stackit_rabbitmq_instance" "example" { Optional: -- `sgw_acl` (String) +- `consumer_timeout` (Number) The timeout in milliseconds for the consumer. +- `enable_monitoring` (Boolean) Enable monitoring. +- `graphite` (String) Graphite server URL (host and port). If set, monitoring with Graphite will be enabled. +- `max_disk_threshold` (Number) The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped. +- `metrics_frequency` (Number) The frequency in seconds at which metrics are emitted. +- `metrics_prefix` (String) The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key +- `monitoring_instance_id` (String) The monitoring instance ID. +- `plugins` (List of String) List of plugins to install. Must be a supported plugin name. +- `roles` (List of String) List of roles to assign to the instance. +- `sgw_acl` (String) Comma separated list of IP networks in CIDR notation which are allowed to access this instance. +- `syslog` (List of String) List of syslog servers to send logs to. +- `tls_ciphers` (List of String) List of TLS ciphers to use. +- `tls_protocols` (String) TLS protocol to use. diff --git a/docs/resources/redis_instance.md b/docs/resources/redis_instance.md index 0d635004..aa020b24 100644 --- a/docs/resources/redis_instance.md +++ b/docs/resources/redis_instance.md @@ -19,7 +19,10 @@ resource "stackit_redis_instance" "example" { version = "10" plan_name = "example-plan-name" parameters = { - sgw_acl = "x.x.x.x/x,y.y.y.y/y" + sgw_acl = "x.x.x.x/x,y.y.y.y/y" + enable_monitoring = false + down_after_milliseconds = 30000 + syslog = ["syslog.example.com:514"] } } ``` @@ -54,4 +57,25 @@ resource "stackit_redis_instance" "example" { Optional: -- `sgw_acl` (String) +- `down_after_milliseconds` (Number) The number of milliseconds after which the instance is considered down. +- `enable_monitoring` (Boolean) Enable monitoring. +- `failover_timeout` (Number) The failover timeout in milliseconds. +- `graphite` (String) Graphite server URL (host and port). If set, monitoring with Graphite will be enabled. +- `lazyfree_lazy_eviction` (String) The lazy eviction enablement (yes or no). +- `lazyfree_lazy_expire` (String) The lazy expire enablement (yes or no). +- `lua_time_limit` (Number) The Lua time limit. +- `max_disk_threshold` (Number) The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped. +- `maxclients` (Number) The maximum number of clients. +- `maxmemory_policy` (String) The policy to handle the maximum memory (volatile-lru, noeviction, etc). +- `maxmemory_samples` (Number) The maximum memory samples. +- `metrics_frequency` (Number) The frequency in seconds at which metrics are emitted. +- `metrics_prefix` (String) The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key +- `min_replicas_max_lag` (Number) The minimum replicas maximum lag. +- `monitoring_instance_id` (String) The monitoring instance ID. +- `notify_keyspace_events` (String) The notify keyspace events. +- `sgw_acl` (String) Comma separated list of IP networks in CIDR notation which are allowed to access this instance. +- `snapshot` (String) The snapshot configuration. +- `syslog` (List of String) List of syslog servers to send logs to. +- `tls_ciphers` (List of String) List of TLS ciphers to use. +- `tls_ciphersuites` (String) TLS cipher suites to use. +- `tls_protocols` (String) TLS protocol to use. diff --git a/examples/resources/stackit_rabbitmq_instance/resource.tf b/examples/resources/stackit_rabbitmq_instance/resource.tf index 87b2a1b5..82545a2d 100644 --- a/examples/resources/stackit_rabbitmq_instance/resource.tf +++ b/examples/resources/stackit_rabbitmq_instance/resource.tf @@ -4,6 +4,9 @@ resource "stackit_rabbitmq_instance" "example" { version = "10" plan_name = "example-plan-name" parameters = { - sgw_acl = "x.x.x.x/x,y.y.y.y/y" + sgw_acl = "x.x.x.x/x,y.y.y.y/y" + consumer_timeout = 18000000 + enable_monitoring = false + plugins = ["example-plugin1", "example-plugin2"] } } diff --git a/examples/resources/stackit_redis_instance/resource.tf b/examples/resources/stackit_redis_instance/resource.tf index 4c4d30ae..265e3431 100644 --- a/examples/resources/stackit_redis_instance/resource.tf +++ b/examples/resources/stackit_redis_instance/resource.tf @@ -4,6 +4,9 @@ resource "stackit_redis_instance" "example" { version = "10" plan_name = "example-plan-name" parameters = { - sgw_acl = "x.x.x.x/x,y.y.y.y/y" + sgw_acl = "x.x.x.x/x,y.y.y.y/y" + enable_monitoring = false + down_after_milliseconds = 30000 + syslog = ["syslog.example.com:514"] } } diff --git a/stackit/internal/conversion/conversion.go b/stackit/internal/conversion/conversion.go index 93281eea..47450338 100644 --- a/stackit/internal/conversion/conversion.go +++ b/stackit/internal/conversion/conversion.go @@ -87,3 +87,22 @@ func BoolValueToPointer(s basetypes.BoolValue) *bool { value := s.ValueBool() return &value } + +// StringListToPointer converts basetypes.ListValue to a pointer to a list of strings. +// It returns nil if the value is null or unknown. +func StringListToPointer(list basetypes.ListValue) (*[]string, error) { + if list.IsNull() || list.IsUnknown() { + return nil, nil + } + + listStr := []string{} + for i, el := range list.Elements() { + elStr, ok := el.(types.String) + if !ok { + return nil, fmt.Errorf("element %d is not a string", i) + } + listStr = append(listStr, elStr.ValueString()) + } + + return &listStr, nil +} diff --git a/stackit/internal/services/rabbitmq/instance/datasource.go b/stackit/internal/services/rabbitmq/instance/datasource.go index 125854b4..b6a96012 100644 --- a/stackit/internal/services/rabbitmq/instance/datasource.go +++ b/stackit/internal/services/rabbitmq/instance/datasource.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" @@ -130,6 +131,46 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques "sgw_acl": schema.StringAttribute{ Computed: true, }, + "consumer_timeout": schema.Int64Attribute{ + Computed: true, + }, + "enable_monitoring": schema.BoolAttribute{ + Computed: true, + }, + "graphite": schema.StringAttribute{ + Computed: true, + }, + "max_disk_threshold": schema.Int64Attribute{ + Computed: true, + }, + "metrics_frequency": schema.Int64Attribute{ + Computed: true, + }, + "metrics_prefix": schema.StringAttribute{ + Computed: true, + }, + "monitoring_instance_id": schema.StringAttribute{ + Computed: true, + }, + "plugins": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "roles": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "syslog": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "tls_ciphers": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "tls_protocols": schema.StringAttribute{ + Computed: true, + }, }, Computed: true, }, diff --git a/stackit/internal/services/rabbitmq/instance/resource.go b/stackit/internal/services/rabbitmq/instance/resource.go index e3793492..c58b0621 100644 --- a/stackit/internal/services/rabbitmq/instance/resource.go +++ b/stackit/internal/services/rabbitmq/instance/resource.go @@ -52,12 +52,36 @@ type Model struct { // Struct corresponding to DataSourceModel.Parameters type parametersModel struct { - SgwAcl types.String `tfsdk:"sgw_acl"` + SgwAcl types.String `tfsdk:"sgw_acl"` + ConsumerTimeout types.Int64 `tfsdk:"consumer_timeout"` + EnableMonitoring types.Bool `tfsdk:"enable_monitoring"` + Graphite types.String `tfsdk:"graphite"` + MaxDiskThreshold types.Int64 `tfsdk:"max_disk_threshold"` + MetricsFrequency types.Int64 `tfsdk:"metrics_frequency"` + MetricsPrefix types.String `tfsdk:"metrics_prefix"` + MonitoringInstanceId types.String `tfsdk:"monitoring_instance_id"` + Plugins types.List `tfsdk:"plugins"` + Roles types.List `tfsdk:"roles"` + Syslog types.List `tfsdk:"syslog"` + TlsCiphers types.List `tfsdk:"tls_ciphers"` + TlsProtocols types.String `tfsdk:"tls_protocols"` } // Types corresponding to parametersModel var parametersTypes = map[string]attr.Type{ - "sgw_acl": basetypes.StringType{}, + "sgw_acl": basetypes.StringType{}, + "consumer_timeout": basetypes.Int64Type{}, + "enable_monitoring": basetypes.BoolType{}, + "graphite": basetypes.StringType{}, + "max_disk_threshold": basetypes.Int64Type{}, + "metrics_frequency": basetypes.Int64Type{}, + "metrics_prefix": basetypes.StringType{}, + "monitoring_instance_id": basetypes.StringType{}, + "plugins": basetypes.ListType{ElemType: types.StringType}, + "roles": basetypes.ListType{ElemType: types.StringType}, + "syslog": basetypes.ListType{ElemType: types.StringType}, + "tls_ciphers": basetypes.ListType{ElemType: types.StringType}, + "tls_protocols": basetypes.StringType{}, } // NewInstanceResource is a helper function to simplify the provider implementation. @@ -124,6 +148,22 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "plan_id": "The selected plan ID.", } + parametersDescriptions := map[string]string{ + "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", + "consumer_timeout": "The timeout in milliseconds for the consumer.", + "enable_monitoring": "Enable monitoring.", + "graphite": "Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.", + "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", + "metrics_frequency": "The frequency in seconds at which metrics are emitted.", + "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key", + "monitoring_instance_id": "The monitoring instance ID.", + "plugins": "List of plugins to install. Must be a supported plugin name.", + "roles": "List of roles to assign to the instance.", + "syslog": "List of syslog servers to send logs to.", + "tls_ciphers": "List of TLS ciphers to use.", + "tls_protocols": "TLS protocol to use.", + } + resp.Schema = schema.Schema{ Description: descriptions["main"], Attributes: map[string]schema.Attribute{ @@ -183,8 +223,73 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "parameters": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "sgw_acl": schema.StringAttribute{ - Optional: true, - Computed: true, + Description: parametersDescriptions["sgw_acl"], + Optional: true, + Computed: true, + }, + "consumer_timeout": schema.Int64Attribute{ + Description: parametersDescriptions["consumer_timeout"], + Optional: true, + Computed: true, + }, + "enable_monitoring": schema.BoolAttribute{ + Description: parametersDescriptions["enable_monitoring"], + Optional: true, + Computed: true, + }, + "graphite": schema.StringAttribute{ + Description: parametersDescriptions["graphite"], + Optional: true, + Computed: true, + }, + "max_disk_threshold": schema.Int64Attribute{ + Description: parametersDescriptions["max_disk_threshold"], + Optional: true, + Computed: true, + }, + "metrics_frequency": schema.Int64Attribute{ + Description: parametersDescriptions["metrics_frequency"], + Optional: true, + Computed: true, + }, + "metrics_prefix": schema.StringAttribute{ + Description: parametersDescriptions["metrics_prefix"], + Optional: true, + Computed: true, + }, + "monitoring_instance_id": schema.StringAttribute{ + Description: parametersDescriptions["monitoring_instance_id"], + Optional: true, + Computed: true, + }, + "plugins": schema.ListAttribute{ + Description: parametersDescriptions["plugins"], + ElementType: types.StringType, + Optional: true, + Computed: true, + }, + "roles": schema.ListAttribute{ + Description: parametersDescriptions["roles"], + ElementType: types.StringType, + Optional: true, + Computed: true, + }, + "syslog": schema.ListAttribute{ + Description: parametersDescriptions["syslog"], + ElementType: types.StringType, + Optional: true, + Computed: true, + }, + "tls_ciphers": schema.ListAttribute{ + Description: parametersDescriptions["tls_ciphers"], + ElementType: types.StringType, + Optional: true, + Computed: true, + }, + "tls_protocols": schema.StringAttribute{ + Description: parametersDescriptions["tls_protocols"], + Optional: true, + Computed: true, }, }, Optional: true, @@ -488,7 +593,17 @@ func mapFields(instance *rabbitmq.Instance, model *Model) error { func mapParameters(params map[string]interface{}) (types.Object, error) { attributes := map[string]attr.Value{} for attribute := range parametersTypes { - valueInterface, ok := params[attribute] + var valueInterface interface{} + var ok bool + + // This replacement is necessary because Terraform does not allow hyphens in attribute names + // And the API uses hyphens in the attribute names (tls-ciphers, tls-protocols) + if attribute == "tls_ciphers" || attribute == "tls_protocols" { + alteredAttribute := strings.ReplaceAll(attribute, "_", "-") + valueInterface, ok = params[alteredAttribute] + } else { + valueInterface, ok = params[attribute] + } if !ok { // All fields are optional, so this is ok // Set the value as nil, will be handled accordingly @@ -584,16 +699,12 @@ func toCreatePayload(model *Model, parameters *parametersModel) (*rabbitmq.Creat if model == nil { return nil, fmt.Errorf("nil model") } - if parameters == nil { - return &rabbitmq.CreateInstancePayload{ - InstanceName: conversion.StringValueToPointer(model.Name), - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil - } - payloadParams := &rabbitmq.InstanceParameters{} - if parameters.SgwAcl.ValueString() != "" { - payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) + + payloadParams, err := toInstanceParams(parameters) + if err != nil { + return nil, fmt.Errorf("converting parameters: %w", err) } + return &rabbitmq.CreateInstancePayload{ InstanceName: conversion.StringValueToPointer(model.Name), Parameters: payloadParams, @@ -606,19 +717,57 @@ func toUpdatePayload(model *Model, parameters *parametersModel) (*rabbitmq.Parti return nil, fmt.Errorf("nil model") } - if parameters == nil { - return &rabbitmq.PartialUpdateInstancePayload{ - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil + payloadParams, err := toInstanceParams(parameters) + if err != nil { + return nil, fmt.Errorf("converting parameters: %w", err) } + return &rabbitmq.PartialUpdateInstancePayload{ - Parameters: &rabbitmq.InstanceParameters{ - SgwAcl: conversion.StringValueToPointer(parameters.SgwAcl), - }, - PlanId: conversion.StringValueToPointer(model.PlanId), + Parameters: payloadParams, + PlanId: conversion.StringValueToPointer(model.PlanId), }, nil } +func toInstanceParams(parameters *parametersModel) (*rabbitmq.InstanceParameters, error) { + if parameters == nil { + return nil, nil + } + payloadParams := &rabbitmq.InstanceParameters{} + + payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) + payloadParams.ConsumerTimeout = conversion.Int64ValueToPointer(parameters.ConsumerTimeout) + payloadParams.EnableMonitoring = conversion.BoolValueToPointer(parameters.EnableMonitoring) + payloadParams.Graphite = conversion.StringValueToPointer(parameters.Graphite) + payloadParams.MaxDiskThreshold = conversion.Int64ValueToPointer(parameters.MaxDiskThreshold) + payloadParams.MetricsFrequency = conversion.Int64ValueToPointer(parameters.MetricsFrequency) + payloadParams.MetricsPrefix = conversion.StringValueToPointer(parameters.MetricsPrefix) + payloadParams.MonitoringInstanceId = conversion.StringValueToPointer(parameters.MonitoringInstanceId) + payloadParams.TlsProtocols = conversion.StringValueToPointer(parameters.TlsProtocols) + + var err error + payloadParams.Plugins, err = conversion.StringListToPointer(parameters.Plugins) + if err != nil { + return nil, fmt.Errorf("converting plugins: %w", err) + } + + payloadParams.Roles, err = conversion.StringListToPointer(parameters.Roles) + if err != nil { + return nil, fmt.Errorf("converting roles: %w", err) + } + + payloadParams.Syslog, err = conversion.StringListToPointer(parameters.Syslog) + if err != nil { + return nil, fmt.Errorf("converting syslog: %w", err) + } + + payloadParams.TlsCiphers, err = conversion.StringListToPointer(parameters.TlsCiphers) + if err != nil { + return nil, fmt.Errorf("converting tls_ciphers: %w", err) + } + + return payloadParams, nil +} + func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() res, err := r.client.ListOfferings(ctx, projectId).Execute() diff --git a/stackit/internal/services/rabbitmq/instance/resource_test.go b/stackit/internal/services/rabbitmq/instance/resource_test.go index 6b26199a..2eac739e 100644 --- a/stackit/internal/services/rabbitmq/instance/resource_test.go +++ b/stackit/internal/services/rabbitmq/instance/resource_test.go @@ -1,15 +1,61 @@ package rabbitmq import ( + "context" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) +var fixtureModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ + "sgw_acl": types.StringValue("acl"), + "consumer_timeout": types.Int64Value(10), + "enable_monitoring": types.BoolValue(true), + "graphite": types.StringValue("1.1.1.1:91"), + "max_disk_threshold": types.Int64Value(100), + "metrics_frequency": types.Int64Value(10), + "metrics_prefix": types.StringValue("prefix"), + "monitoring_instance_id": types.StringValue("mid"), + "plugins": types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("plugin1"), + types.StringValue("plugin2"), + }), + "roles": types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("role1"), + types.StringValue("role2"), + }), + "syslog": types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("syslog"), + types.StringValue("syslog2"), + }), + "tls_ciphers": types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ciphers1"), + types.StringValue("ciphers2"), + }), + "tls_protocols": types.StringValue("protocol1"), +}) + +var fixtureInstanceParameters = rabbitmq.InstanceParameters{ + SgwAcl: utils.Ptr("acl"), + ConsumerTimeout: utils.Ptr(int64(10)), + EnableMonitoring: utils.Ptr(true), + Graphite: utils.Ptr("1.1.1.1:91"), + MaxDiskThreshold: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int64(10)), + MetricsPrefix: utils.Ptr("prefix"), + MonitoringInstanceId: utils.Ptr("mid"), + Plugins: &[]string{"plugin1", "plugin2"}, + Roles: &[]string{"role1", "role2"}, + Syslog: &[]string{"syslog", "syslog2"}, + TlsCiphers: &[]string{"ciphers1", "ciphers2"}, + TlsProtocols: utils.Ptr("protocol1"), +} + func TestMapFields(t *testing.T) { tests := []struct { description string @@ -47,7 +93,19 @@ func TestMapFields(t *testing.T) { Name: utils.Ptr("name"), CfOrganizationGuid: utils.Ptr("org"), Parameters: &map[string]interface{}{ - "sgw_acl": "acl", + "sgw_acl": "acl", + "consumer_timeout": 10, + "enable_monitoring": true, + "graphite": "1.1.1.1:91", + "max_disk_threshold": 100, + "metrics_frequency": 10, + "metrics_prefix": "prefix", + "monitoring_instance_id": "mid", + "plugins": []string{"plugin1", "plugin2"}, + "roles": []string{"role1", "role2"}, + "syslog": []string{"syslog", "syslog2"}, + "tls-ciphers": []string{"ciphers1", "ciphers2"}, + "tls-protocols": "protocol1", }, }, Model{ @@ -61,12 +119,11 @@ func TestMapFields(t *testing.T) { DashboardUrl: types.StringValue("dashboard"), ImageUrl: types.StringValue("image"), CfOrganizationGuid: types.StringValue("org"), - Parameters: types.ObjectValueMust(parametersTypes, map[string]attr.Value{ - "sgw_acl": types.StringValue("acl"), - }), + Parameters: fixtureModelParameters, }, true, }, + { "nil_response", nil, @@ -125,16 +182,14 @@ func TestMapFields(t *testing.T) { func TestToCreatePayload(t *testing.T) { tests := []struct { - description string - input *Model - inputParameters *parametersModel - expected *rabbitmq.CreateInstancePayload - isValid bool + description string + input *Model + expected *rabbitmq.CreateInstancePayload + isValid bool }{ { "default_values", &Model{}, - ¶metersModel{}, &rabbitmq.CreateInstancePayload{ Parameters: &rabbitmq.InstanceParameters{}, }, @@ -143,43 +198,34 @@ func TestToCreatePayload(t *testing.T) { { "simple_values", &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - }, - ¶metersModel{ - SgwAcl: types.StringValue("sgw"), + Name: types.StringValue("name"), + PlanId: types.StringValue("plan"), + Parameters: fixtureModelParameters, }, &rabbitmq.CreateInstancePayload{ InstanceName: utils.Ptr("name"), - Parameters: &rabbitmq.InstanceParameters{ - SgwAcl: utils.Ptr("sgw"), - }, - PlanId: utils.Ptr("plan"), + Parameters: &fixtureInstanceParameters, + PlanId: utils.Ptr("plan"), }, true, }, { "null_fields_and_int_conversions", &Model{ - Name: types.StringValue(""), - PlanId: types.StringValue(""), - }, - ¶metersModel{ - SgwAcl: types.StringNull(), + Name: types.StringValue(""), + PlanId: types.StringValue(""), + Parameters: fixtureModelParameters, }, &rabbitmq.CreateInstancePayload{ InstanceName: utils.Ptr(""), - Parameters: &rabbitmq.InstanceParameters{ - SgwAcl: nil, - }, - PlanId: utils.Ptr(""), + PlanId: utils.Ptr(""), + Parameters: &fixtureInstanceParameters, }, true, }, { "nil_model", nil, - ¶metersModel{}, nil, false, }, @@ -189,17 +235,26 @@ func TestToCreatePayload(t *testing.T) { Name: types.StringValue("name"), PlanId: types.StringValue("plan"), }, - nil, &rabbitmq.CreateInstancePayload{ InstanceName: utils.Ptr("name"), PlanId: utils.Ptr("plan"), + Parameters: &rabbitmq.InstanceParameters{}, }, true, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input, tt.inputParameters) + var parameters = ¶metersModel{} + if tt.input != nil { + if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) + if diags.HasError() { + t.Fatalf("Error converting parameters: %v", diags.Errors()) + } + } + } + output, err := toCreatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -218,16 +273,14 @@ func TestToCreatePayload(t *testing.T) { func TestToUpdatePayload(t *testing.T) { tests := []struct { - description string - input *Model - inputParameters *parametersModel - expected *rabbitmq.PartialUpdateInstancePayload - isValid bool + description string + input *Model + expected *rabbitmq.PartialUpdateInstancePayload + isValid bool }{ { "default_values", &Model{}, - ¶metersModel{}, &rabbitmq.PartialUpdateInstancePayload{ Parameters: &rabbitmq.InstanceParameters{}, }, @@ -236,39 +289,31 @@ func TestToUpdatePayload(t *testing.T) { { "simple_values", &Model{ - PlanId: types.StringValue("plan"), - }, - ¶metersModel{ - SgwAcl: types.StringValue("sgw"), + Name: types.StringValue("name"), + PlanId: types.StringValue("plan"), + Parameters: fixtureModelParameters, }, &rabbitmq.PartialUpdateInstancePayload{ - Parameters: &rabbitmq.InstanceParameters{ - SgwAcl: utils.Ptr("sgw"), - }, - PlanId: utils.Ptr("plan"), + Parameters: &fixtureInstanceParameters, + PlanId: utils.Ptr("plan"), }, true, }, { "null_fields_and_int_conversions", &Model{ - PlanId: types.StringValue(""), - }, - ¶metersModel{ - SgwAcl: types.StringNull(), + PlanId: types.StringValue(""), + Parameters: fixtureModelParameters, }, &rabbitmq.PartialUpdateInstancePayload{ - Parameters: &rabbitmq.InstanceParameters{ - SgwAcl: nil, - }, - PlanId: utils.Ptr(""), + Parameters: &fixtureInstanceParameters, + PlanId: utils.Ptr(""), }, true, }, { "nil_model", nil, - ¶metersModel{}, nil, false, }, @@ -277,16 +322,25 @@ func TestToUpdatePayload(t *testing.T) { &Model{ PlanId: types.StringValue("plan"), }, - nil, &rabbitmq.PartialUpdateInstancePayload{ - PlanId: utils.Ptr("plan"), + PlanId: utils.Ptr("plan"), + Parameters: &rabbitmq.InstanceParameters{}, }, true, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input, tt.inputParameters) + var parameters = ¶metersModel{} + if tt.input != nil { + if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) + if diags.HasError() { + t.Fatalf("Error converting parameters: %v", diags.Errors()) + } + } + } + output, err := toUpdatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } diff --git a/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go b/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go index 591ef81e..9d9fd603 100644 --- a/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go +++ b/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go @@ -28,11 +28,30 @@ var instanceResource = map[string]string{ "sgw_acl_valid": "192.168.0.0/16", } -func resourceConfig(acls *string) string { - aclsLine := "" - if acls != nil { - aclsLine = fmt.Sprintf(`sgw_acl = %q`, *acls) +func parametersConfig(params map[string]string) string { + nonStringParams := []string{ + "consumer_timeout", + "enable_monitoring", + "max_disk_threshold", + "metrics_frequency", + "plugins", + "roles", + "syslog", + "tls_ciphers", } + parameters := "parameters = {" + for k, v := range params { + if utils.Contains(nonStringParams, k) { + parameters += fmt.Sprintf("%s = %s\n", k, v) + } else { + parameters += fmt.Sprintf("%s = %q\n", k, v) + } + } + parameters += "\n}" + return parameters +} + +func resourceConfig(params map[string]string) string { return fmt.Sprintf(` %s @@ -41,10 +60,7 @@ func resourceConfig(acls *string) string { name = "%s" plan_name = "%s" version = "%s" - parameters = { - %s - metrics_frequency = "%s" - } + %s } %s @@ -54,34 +70,7 @@ func resourceConfig(acls *string) string { instanceResource["name"], instanceResource["plan_name"], instanceResource["version"], - aclsLine, - instanceResource["metrics_frequency"], - resourceConfigCredential(), - ) -} - -func resourceConfigWithUpdate() string { - return fmt.Sprintf(` - %s - - resource "stackit_rabbitmq_instance" "instance" { - project_id = "%s" - name = "%s" - plan_name = "%s" - version = "%s" - parameters = { - sgw_acl = "%s" - } - } - - %s - `, - testutil.RabbitMQProviderConfig(), - instanceResource["project_id"], - instanceResource["name"], - instanceResource["plan_name"], - instanceResource["version"], - instanceResource["sgw_acl_valid"], + parametersConfig(params), resourceConfigCredential(), ) } @@ -103,12 +92,24 @@ func TestAccRabbitMQResource(t *testing.T) { Steps: []resource.TestStep{ // Creation fail { - Config: resourceConfig(&acls), + Config: resourceConfig(map[string]string{"sgw_acl": acls}), ExpectError: regexp.MustCompile(`.*sgw_acl is invalid.*`), }, // Creation { - Config: resourceConfig(nil), + Config: resourceConfig(map[string]string{ + "sgw_acl": instanceResource["sgw_acl_valid"], + "consumer_timeout": "1800000", + "enable_monitoring": "true", + "graphite": "graphite.example.com:2003", + "max_disk_threshold": "80", + "metrics_frequency": "60", + "metrics_prefix": "rabbitmq", + "plugins": `["rabbitmq_federation"]`, + "roles": `["administrator"]`, + "syslog": `["syslog.example.com:514"]`, + "tls_ciphers": `["TLS_AES_128_GCM_SHA256"]`, + }), Check: resource.ComposeAggregateTestCheckFunc( // Instance data resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "project_id", instanceResource["project_id"]), @@ -117,7 +118,23 @@ func TestAccRabbitMQResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "plan_name", instanceResource["plan_name"]), resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "version", instanceResource["version"]), resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttrSet("stackit_rabbitmq_instance.instance", "parameters.sgw_acl"), + + // Instance params data + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl_valid"]), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.consumer_timeout", "1800000"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.enable_monitoring", "true"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.graphite", "graphite.example.com:2003"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.max_disk_threshold", "80"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.metrics_frequency", "60"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.metrics_prefix", "rabbitmq"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.plugins.#", "1"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.plugins.0", "rabbitmq_federation"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.roles.#", "1"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.roles.0", "administrator"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.syslog.#", "1"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.syslog.0", "syslog.example.com:514"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.tls_ciphers.#", "1"), + resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.tls_ciphers.0", "TLS_AES_128_GCM_SHA256"), // Credential data resource.TestCheckResourceAttrPair( @@ -209,7 +226,7 @@ func TestAccRabbitMQResource(t *testing.T) { }, // Update { - Config: resourceConfigWithUpdate(), + Config: resourceConfig(map[string]string{"sgw_acl": instanceResource["sgw_acl_valid"]}), Check: resource.ComposeAggregateTestCheckFunc( // Instance data resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "project_id", instanceResource["project_id"]), diff --git a/stackit/internal/services/redis/instance/datasource.go b/stackit/internal/services/redis/instance/datasource.go index dfaeacc7..f7f5eb23 100644 --- a/stackit/internal/services/redis/instance/datasource.go +++ b/stackit/internal/services/redis/instance/datasource.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" @@ -130,6 +131,71 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques "sgw_acl": schema.StringAttribute{ Computed: true, }, + "down_after_milliseconds": schema.Int64Attribute{ + Computed: true, + }, + "enable_monitoring": schema.BoolAttribute{ + Computed: true, + }, + "failover_timeout": schema.Int64Attribute{ + Computed: true, + }, + "graphite": schema.StringAttribute{ + Computed: true, + }, + "lazyfree_lazy_eviction": schema.StringAttribute{ + Computed: true, + }, + "lazyfree_lazy_expire": schema.StringAttribute{ + Computed: true, + }, + "lua_time_limit": schema.Int64Attribute{ + Computed: true, + }, + "max_disk_threshold": schema.Int64Attribute{ + Computed: true, + }, + "maxclients": schema.Int64Attribute{ + Computed: true, + }, + "maxmemory_policy": schema.StringAttribute{ + Computed: true, + }, + "maxmemory_samples": schema.Int64Attribute{ + Computed: true, + }, + "metrics_frequency": schema.Int64Attribute{ + Computed: true, + }, + "metrics_prefix": schema.StringAttribute{ + Computed: true, + }, + "min_replicas_max_lag": schema.Int64Attribute{ + Computed: true, + }, + "monitoring_instance_id": schema.StringAttribute{ + Computed: true, + }, + "notify_keyspace_events": schema.StringAttribute{ + Computed: true, + }, + "snapshot": schema.StringAttribute{ + Computed: true, + }, + "syslog": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "tls_ciphers": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + }, + "tls_ciphersuites": schema.StringAttribute{ + Computed: true, + }, + "tls_protocols": schema.StringAttribute{ + Computed: true, + }, }, Computed: true, }, diff --git a/stackit/internal/services/redis/instance/resource.go b/stackit/internal/services/redis/instance/resource.go index 90f5e018..1b290627 100644 --- a/stackit/internal/services/redis/instance/resource.go +++ b/stackit/internal/services/redis/instance/resource.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "slices" "strings" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -52,12 +53,54 @@ type Model struct { // Struct corresponding to DataSourceModel.Parameters type parametersModel struct { - SgwAcl types.String `tfsdk:"sgw_acl"` + SgwAcl types.String `tfsdk:"sgw_acl"` + DownAfterMilliseconds types.Int64 `tfsdk:"down_after_milliseconds"` + EnableMonitoring types.Bool `tfsdk:"enable_monitoring"` + FailoverTimeout types.Int64 `tfsdk:"failover_timeout"` + Graphite types.String `tfsdk:"graphite"` + LazyfreeLazyEviction types.String `tfsdk:"lazyfree_lazy_eviction"` + LazyfreeLazyExpire types.String `tfsdk:"lazyfree_lazy_expire"` + LuaTimeLimit types.Int64 `tfsdk:"lua_time_limit"` + MaxDiskThreshold types.Int64 `tfsdk:"max_disk_threshold"` + Maxclients types.Int64 `tfsdk:"maxclients"` + MaxmemoryPolicy types.String `tfsdk:"maxmemory_policy"` + MaxmemorySamples types.Int64 `tfsdk:"maxmemory_samples"` + MetricsFrequency types.Int64 `tfsdk:"metrics_frequency"` + MetricsPrefix types.String `tfsdk:"metrics_prefix"` + MinReplicasMaxLag types.Int64 `tfsdk:"min_replicas_max_lag"` + MonitoringInstanceId types.String `tfsdk:"monitoring_instance_id"` + NotifyKeyspaceEvents types.String `tfsdk:"notify_keyspace_events"` + Snapshot types.String `tfsdk:"snapshot"` + Syslog types.List `tfsdk:"syslog"` + TlsCiphers types.List `tfsdk:"tls_ciphers"` + TlsCiphersuites types.String `tfsdk:"tls_ciphersuites"` + TlsProtocols types.String `tfsdk:"tls_protocols"` } // Types corresponding to parametersModel var parametersTypes = map[string]attr.Type{ - "sgw_acl": basetypes.StringType{}, + "sgw_acl": basetypes.StringType{}, + "down_after_milliseconds": basetypes.Int64Type{}, + "enable_monitoring": basetypes.BoolType{}, + "failover_timeout": basetypes.Int64Type{}, + "graphite": basetypes.StringType{}, + "lazyfree_lazy_eviction": basetypes.StringType{}, + "lazyfree_lazy_expire": basetypes.StringType{}, + "lua_time_limit": basetypes.Int64Type{}, + "max_disk_threshold": basetypes.Int64Type{}, + "maxclients": basetypes.Int64Type{}, + "maxmemory_policy": basetypes.StringType{}, + "maxmemory_samples": basetypes.Int64Type{}, + "metrics_frequency": basetypes.Int64Type{}, + "metrics_prefix": basetypes.StringType{}, + "min_replicas_max_lag": basetypes.Int64Type{}, + "monitoring_instance_id": basetypes.StringType{}, + "notify_keyspace_events": basetypes.StringType{}, + "snapshot": basetypes.StringType{}, + "syslog": basetypes.ListType{ElemType: types.StringType}, + "tls_ciphers": basetypes.ListType{ElemType: types.StringType}, + "tls_ciphersuites": basetypes.StringType{}, + "tls_protocols": basetypes.StringType{}, } // NewInstanceResource is a helper function to simplify the provider implementation. @@ -124,6 +167,31 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "plan_id": "The selected plan ID.", } + parametersDescriptions := map[string]string{ + "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", + "down_after_milliseconds": "The number of milliseconds after which the instance is considered down.", + "enable_monitoring": "Enable monitoring.", + "failover_timeout": "The failover timeout in milliseconds.", + "graphite": "Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.", + "lazyfree_lazy_eviction": "The lazy eviction enablement (yes or no).", + "lazyfree_lazy_expire": "The lazy expire enablement (yes or no).", + "lua_time_limit": "The Lua time limit.", + "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", + "maxclients": "The maximum number of clients.", + "maxmemory_policy": "The policy to handle the maximum memory (volatile-lru, noeviction, etc).", + "maxmemory_samples": "The maximum memory samples.", + "metrics_frequency": "The frequency in seconds at which metrics are emitted.", + "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key", + "min_replicas_max_lag": "The minimum replicas maximum lag.", + "monitoring_instance_id": "The monitoring instance ID.", + "notify_keyspace_events": "The notify keyspace events.", + "snapshot": "The snapshot configuration.", + "syslog": "List of syslog servers to send logs to.", + "tls_ciphers": "List of TLS ciphers to use.", + "tls_ciphersuites": "TLS cipher suites to use.", + "tls_protocols": "TLS protocol to use.", + } + resp.Schema = schema.Schema{ Description: descriptions["main"], Attributes: map[string]schema.Attribute{ @@ -183,8 +251,116 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r "parameters": schema.SingleNestedAttribute{ Attributes: map[string]schema.Attribute{ "sgw_acl": schema.StringAttribute{ - Optional: true, - Computed: true, + Description: parametersDescriptions["sgw_acl"], + Optional: true, + Computed: true, + }, + "down_after_milliseconds": schema.Int64Attribute{ + Description: parametersDescriptions["down_after_milliseconds"], + Optional: true, + Computed: true, + }, + "enable_monitoring": schema.BoolAttribute{ + Description: parametersDescriptions["enable_monitoring"], + Optional: true, + Computed: true, + }, + "failover_timeout": schema.Int64Attribute{ + Description: parametersDescriptions["failover_timeout"], + Optional: true, + Computed: true, + }, + "graphite": schema.StringAttribute{ + Description: parametersDescriptions["graphite"], + Optional: true, + Computed: true, + }, + "lazyfree_lazy_eviction": schema.StringAttribute{ + Description: parametersDescriptions["lazyfree_lazy_eviction"], + Optional: true, + Computed: true, + }, + "lazyfree_lazy_expire": schema.StringAttribute{ + Description: parametersDescriptions["lazyfree_lazy_expire"], + Optional: true, + Computed: true, + }, + "lua_time_limit": schema.Int64Attribute{ + Description: parametersDescriptions["lua_time_limit"], + Optional: true, + Computed: true, + }, + "max_disk_threshold": schema.Int64Attribute{ + Description: parametersDescriptions["max_disk_threshold"], + Optional: true, + Computed: true, + }, + "maxclients": schema.Int64Attribute{ + Description: parametersDescriptions["maxclients"], + Optional: true, + Computed: true, + }, + "maxmemory_policy": schema.StringAttribute{ + Description: parametersDescriptions["maxmemory_policy"], + Optional: true, + Computed: true, + }, + "maxmemory_samples": schema.Int64Attribute{ + Description: parametersDescriptions["maxmemory_samples"], + Optional: true, + Computed: true, + }, + "metrics_frequency": schema.Int64Attribute{ + Description: parametersDescriptions["metrics_frequency"], + Optional: true, + Computed: true, + }, + "metrics_prefix": schema.StringAttribute{ + Description: parametersDescriptions["metrics_prefix"], + Optional: true, + Computed: true, + }, + "min_replicas_max_lag": schema.Int64Attribute{ + Description: parametersDescriptions["min_replicas_max_lag"], + Optional: true, + Computed: true, + }, + "monitoring_instance_id": schema.StringAttribute{ + Description: parametersDescriptions["monitoring_instance_id"], + Optional: true, + Computed: true, + }, + "notify_keyspace_events": schema.StringAttribute{ + Description: parametersDescriptions["notify_keyspace_events"], + Optional: true, + Computed: true, + }, + "snapshot": schema.StringAttribute{ + Description: parametersDescriptions["snapshot"], + Optional: true, + Computed: true, + }, + "syslog": schema.ListAttribute{ + Description: parametersDescriptions["syslog"], + ElementType: types.StringType, + Optional: true, + Computed: true, + }, + "tls_ciphers": schema.ListAttribute{ + Description: parametersDescriptions["tls_ciphers"], + ElementType: types.StringType, + Optional: true, + Computed: true, + }, + "tls_ciphersuites": schema.StringAttribute{ + Description: parametersDescriptions["tls_ciphersuites"], + Optional: true, + Computed: true, + }, + "tls_protocols": schema.StringAttribute{ + Description: parametersDescriptions["tls_protocols"], + Optional: true, + Computed: true, }, }, Optional: true, @@ -488,7 +664,31 @@ func mapFields(instance *redis.Instance, model *Model) error { func mapParameters(params map[string]interface{}) (types.Object, error) { attributes := map[string]attr.Value{} for attribute := range parametersTypes { - valueInterface, ok := params[attribute] + var valueInterface interface{} + var ok bool + + // This replacement is necessary because Terraform does not allow hyphens in attribute names + // And the API uses hyphens in some of the attribute names, which would cause a mismatch + // The following attributes have hyphens in the API but underscores in the schema + hyphenAttributes := []string{ + "down_after_milliseconds", + "failover_timeout", + "lazyfree_lazy_eviction", + "lazyfree_lazy_expire", + "lua_time_limit", + "maxmemory_policy", + "maxmemory_samples", + "notify_keyspace_events", + "tls_ciphers", + "tls_ciphersuites", + "tls_protocols", + } + if slices.Contains(hyphenAttributes, attribute) { + alteredAttribute := strings.ReplaceAll(attribute, "_", "-") + valueInterface, ok = params[alteredAttribute] + } else { + valueInterface, ok = params[attribute] + } if !ok { // All fields are optional, so this is ok // Set the value as nil, will be handled accordingly @@ -584,16 +784,12 @@ func toCreatePayload(model *Model, parameters *parametersModel) (*redis.CreateIn if model == nil { return nil, fmt.Errorf("nil model") } - if parameters == nil { - return &redis.CreateInstancePayload{ - InstanceName: conversion.StringValueToPointer(model.Name), - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil - } - payloadParams := &redis.InstanceParameters{} - if parameters.SgwAcl.ValueString() != "" { - payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) + + payloadParams, err := toInstanceParams(parameters) + if err != nil { + return nil, fmt.Errorf("converting parameters: %w", err) } + return &redis.CreateInstancePayload{ InstanceName: conversion.StringValueToPointer(model.Name), Parameters: payloadParams, @@ -606,19 +802,58 @@ func toUpdatePayload(model *Model, parameters *parametersModel) (*redis.PartialU return nil, fmt.Errorf("nil model") } - if parameters == nil { - return &redis.PartialUpdateInstancePayload{ - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil + payloadParams, err := toInstanceParams(parameters) + if err != nil { + return nil, fmt.Errorf("converting parameters: %w", err) } + return &redis.PartialUpdateInstancePayload{ - Parameters: &redis.InstanceParameters{ - SgwAcl: conversion.StringValueToPointer(parameters.SgwAcl), - }, - PlanId: conversion.StringValueToPointer(model.PlanId), + Parameters: payloadParams, + PlanId: conversion.StringValueToPointer(model.PlanId), }, nil } +func toInstanceParams(parameters *parametersModel) (*redis.InstanceParameters, error) { + if parameters == nil { + return nil, nil + } + payloadParams := &redis.InstanceParameters{} + + payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) + payloadParams.DownAfterMilliseconds = conversion.Int64ValueToPointer(parameters.DownAfterMilliseconds) + payloadParams.EnableMonitoring = conversion.BoolValueToPointer(parameters.EnableMonitoring) + payloadParams.FailoverTimeout = conversion.Int64ValueToPointer(parameters.FailoverTimeout) + payloadParams.Graphite = conversion.StringValueToPointer(parameters.Graphite) + payloadParams.LazyfreeLazyEviction = conversion.StringValueToPointer(parameters.LazyfreeLazyEviction) + payloadParams.LazyfreeLazyExpire = conversion.StringValueToPointer(parameters.LazyfreeLazyExpire) + payloadParams.LuaTimeLimit = conversion.Int64ValueToPointer(parameters.LuaTimeLimit) + payloadParams.MaxDiskThreshold = conversion.Int64ValueToPointer(parameters.MaxDiskThreshold) + payloadParams.Maxclients = conversion.Int64ValueToPointer(parameters.Maxclients) + payloadParams.MaxmemoryPolicy = conversion.StringValueToPointer(parameters.MaxmemoryPolicy) + payloadParams.MaxmemorySamples = conversion.Int64ValueToPointer(parameters.MaxmemorySamples) + payloadParams.MetricsFrequency = conversion.Int64ValueToPointer(parameters.MetricsFrequency) + payloadParams.MetricsPrefix = conversion.StringValueToPointer(parameters.MetricsPrefix) + payloadParams.MinReplicasMaxLag = conversion.Int64ValueToPointer(parameters.MinReplicasMaxLag) + payloadParams.MonitoringInstanceId = conversion.StringValueToPointer(parameters.MonitoringInstanceId) + payloadParams.NotifyKeyspaceEvents = conversion.StringValueToPointer(parameters.NotifyKeyspaceEvents) + payloadParams.Snapshot = conversion.StringValueToPointer(parameters.Snapshot) + payloadParams.TlsCiphersuites = conversion.StringValueToPointer(parameters.TlsCiphersuites) + payloadParams.TlsProtocols = conversion.StringValueToPointer(parameters.TlsProtocols) + + var err error + payloadParams.Syslog, err = conversion.StringListToPointer(parameters.Syslog) + if err != nil { + return nil, fmt.Errorf("converting syslog: %w", err) + } + + payloadParams.TlsCiphers, err = conversion.StringListToPointer(parameters.TlsCiphers) + if err != nil { + return nil, fmt.Errorf("converting tls_ciphers: %w", err) + } + + return payloadParams, nil +} + func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() res, err := r.client.ListOfferings(ctx, projectId).Execute() diff --git a/stackit/internal/services/redis/instance/resource_test.go b/stackit/internal/services/redis/instance/resource_test.go index 79c800b1..c5c91919 100644 --- a/stackit/internal/services/redis/instance/resource_test.go +++ b/stackit/internal/services/redis/instance/resource_test.go @@ -1,15 +1,73 @@ package redis import ( + "context" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/redis" ) +var fixtureModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ + "sgw_acl": types.StringValue("acl"), + "down_after_milliseconds": types.Int64Value(10), + "enable_monitoring": types.BoolValue(true), + "failover_timeout": types.Int64Value(10), + "graphite": types.StringValue("1.1.1.1:91"), + "lazyfree_lazy_eviction": types.StringValue("lazy_eviction"), + "lazyfree_lazy_expire": types.StringValue("lazy_expire"), + "lua_time_limit": types.Int64Value(10), + "max_disk_threshold": types.Int64Value(100), + "maxclients": types.Int64Value(10), + "maxmemory_policy": types.StringValue("policy"), + "maxmemory_samples": types.Int64Value(10), + "metrics_frequency": types.Int64Value(10), + "metrics_prefix": types.StringValue("prefix"), + "min_replicas_max_lag": types.Int64Value(10), + "monitoring_instance_id": types.StringValue("mid"), + "notify_keyspace_events": types.StringValue("events"), + "snapshot": types.StringValue("snapshot"), + "syslog": types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("syslog"), + types.StringValue("syslog2"), + }), + "tls_ciphers": types.ListValueMust(types.StringType, []attr.Value{ + types.StringValue("ciphers1"), + types.StringValue("ciphers2"), + }), + "tls_ciphersuites": types.StringValue("ciphersuites"), + "tls_protocols": types.StringValue("protocol1"), +}) + +var fixtureInstanceParameters = redis.InstanceParameters{ + SgwAcl: utils.Ptr("acl"), + DownAfterMilliseconds: utils.Ptr(int64(10)), + EnableMonitoring: utils.Ptr(true), + FailoverTimeout: utils.Ptr(int64(10)), + Graphite: utils.Ptr("1.1.1.1:91"), + LazyfreeLazyEviction: utils.Ptr("lazy_eviction"), + LazyfreeLazyExpire: utils.Ptr("lazy_expire"), + LuaTimeLimit: utils.Ptr(int64(10)), + MaxDiskThreshold: utils.Ptr(int64(100)), + Maxclients: utils.Ptr(int64(10)), + MaxmemoryPolicy: utils.Ptr("policy"), + MaxmemorySamples: utils.Ptr(int64(10)), + MetricsFrequency: utils.Ptr(int64(10)), + MetricsPrefix: utils.Ptr("prefix"), + MinReplicasMaxLag: utils.Ptr(int64(10)), + MonitoringInstanceId: utils.Ptr("mid"), + NotifyKeyspaceEvents: utils.Ptr("events"), + Snapshot: utils.Ptr("snapshot"), + Syslog: &[]string{"syslog", "syslog2"}, + TlsCiphers: &[]string{"ciphers1", "ciphers2"}, + TlsCiphersuites: utils.Ptr("ciphersuites"), + TlsProtocols: utils.Ptr("protocol1"), +} + func TestMapFields(t *testing.T) { tests := []struct { description string @@ -47,7 +105,28 @@ func TestMapFields(t *testing.T) { Name: utils.Ptr("name"), CfOrganizationGuid: utils.Ptr("org"), Parameters: &map[string]interface{}{ - "sgw_acl": "acl", + "sgw_acl": "acl", + "down-after-milliseconds": int64(10), + "enable_monitoring": true, + "failover-timeout": int64(10), + "graphite": "1.1.1.1:91", + "lazyfree-lazy-eviction": "lazy_eviction", + "lazyfree-lazy-expire": "lazy_expire", + "lua-time-limit": int64(10), + "max_disk_threshold": int64(100), + "maxclients": int64(10), + "maxmemory-policy": "policy", + "maxmemory-samples": int64(10), + "metrics_frequency": int64(10), + "metrics_prefix": "prefix", + "min_replicas_max_lag": int64(10), + "monitoring_instance_id": "mid", + "notify-keyspace-events": "events", + "snapshot": "snapshot", + "syslog": []string{"syslog", "syslog2"}, + "tls-ciphers": []string{"ciphers1", "ciphers2"}, + "tls-ciphersuites": "ciphersuites", + "tls-protocols": "protocol1", }, }, Model{ @@ -61,9 +140,7 @@ func TestMapFields(t *testing.T) { DashboardUrl: types.StringValue("dashboard"), ImageUrl: types.StringValue("image"), CfOrganizationGuid: types.StringValue("org"), - Parameters: types.ObjectValueMust(parametersTypes, map[string]attr.Value{ - "sgw_acl": types.StringValue("acl"), - }), + Parameters: fixtureModelParameters, }, true, }, @@ -125,16 +202,14 @@ func TestMapFields(t *testing.T) { func TestToCreatePayload(t *testing.T) { tests := []struct { - description string - input *Model - inputParameters *parametersModel - expected *redis.CreateInstancePayload - isValid bool + description string + input *Model + expected *redis.CreateInstancePayload + isValid bool }{ { "default_values", &Model{}, - ¶metersModel{}, &redis.CreateInstancePayload{ Parameters: &redis.InstanceParameters{}, }, @@ -143,43 +218,34 @@ func TestToCreatePayload(t *testing.T) { { "simple_values", &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - }, - ¶metersModel{ - SgwAcl: types.StringValue("sgw"), + Name: types.StringValue("name"), + PlanId: types.StringValue("plan"), + Parameters: fixtureModelParameters, }, &redis.CreateInstancePayload{ InstanceName: utils.Ptr("name"), - Parameters: &redis.InstanceParameters{ - SgwAcl: utils.Ptr("sgw"), - }, - PlanId: utils.Ptr("plan"), + Parameters: &fixtureInstanceParameters, + PlanId: utils.Ptr("plan"), }, true, }, { "null_fields_and_int_conversions", &Model{ - Name: types.StringValue(""), - PlanId: types.StringValue(""), - }, - ¶metersModel{ - SgwAcl: types.StringNull(), + Name: types.StringValue(""), + PlanId: types.StringValue(""), + Parameters: fixtureModelParameters, }, &redis.CreateInstancePayload{ InstanceName: utils.Ptr(""), - Parameters: &redis.InstanceParameters{ - SgwAcl: nil, - }, - PlanId: utils.Ptr(""), + Parameters: &fixtureInstanceParameters, + PlanId: utils.Ptr(""), }, true, }, { "nil_model", nil, - ¶metersModel{}, nil, false, }, @@ -189,17 +255,26 @@ func TestToCreatePayload(t *testing.T) { Name: types.StringValue("name"), PlanId: types.StringValue("plan"), }, - nil, &redis.CreateInstancePayload{ InstanceName: utils.Ptr("name"), PlanId: utils.Ptr("plan"), + Parameters: &redis.InstanceParameters{}, }, true, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input, tt.inputParameters) + var parameters = ¶metersModel{} + if tt.input != nil { + if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) + if diags.HasError() { + t.Fatalf("Error converting parameters: %v", diags.Errors()) + } + } + } + output, err := toCreatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } @@ -218,16 +293,14 @@ func TestToCreatePayload(t *testing.T) { func TestToUpdatePayload(t *testing.T) { tests := []struct { - description string - input *Model - inputParameters *parametersModel - expected *redis.PartialUpdateInstancePayload - isValid bool + description string + input *Model + expected *redis.PartialUpdateInstancePayload + isValid bool }{ { "default_values", &Model{}, - ¶metersModel{}, &redis.PartialUpdateInstancePayload{ Parameters: &redis.InstanceParameters{}, }, @@ -236,39 +309,30 @@ func TestToUpdatePayload(t *testing.T) { { "simple_values", &Model{ - PlanId: types.StringValue("plan"), - }, - ¶metersModel{ - SgwAcl: types.StringValue("sgw"), + PlanId: types.StringValue("plan"), + Parameters: fixtureModelParameters, }, &redis.PartialUpdateInstancePayload{ - Parameters: &redis.InstanceParameters{ - SgwAcl: utils.Ptr("sgw"), - }, - PlanId: utils.Ptr("plan"), + Parameters: &fixtureInstanceParameters, + PlanId: utils.Ptr("plan"), }, true, }, { "null_fields_and_int_conversions", &Model{ - PlanId: types.StringValue(""), - }, - ¶metersModel{ - SgwAcl: types.StringNull(), + PlanId: types.StringValue(""), + Parameters: fixtureModelParameters, }, &redis.PartialUpdateInstancePayload{ - Parameters: &redis.InstanceParameters{ - SgwAcl: nil, - }, - PlanId: utils.Ptr(""), + Parameters: &fixtureInstanceParameters, + PlanId: utils.Ptr(""), }, true, }, { "nil_model", nil, - ¶metersModel{}, nil, false, }, @@ -277,16 +341,25 @@ func TestToUpdatePayload(t *testing.T) { &Model{ PlanId: types.StringValue("plan"), }, - nil, &redis.PartialUpdateInstancePayload{ - PlanId: utils.Ptr("plan"), + PlanId: utils.Ptr("plan"), + Parameters: &redis.InstanceParameters{}, }, true, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input, tt.inputParameters) + var parameters = ¶metersModel{} + if tt.input != nil { + if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) + if diags.HasError() { + t.Fatalf("Error converting parameters: %v", diags.Errors()) + } + } + } + output, err := toUpdatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } diff --git a/stackit/internal/services/redis/redis_acc_test.go b/stackit/internal/services/redis/redis_acc_test.go index 560a546e..53821561 100644 --- a/stackit/internal/services/redis/redis_acc_test.go +++ b/stackit/internal/services/redis/redis_acc_test.go @@ -26,41 +26,36 @@ var instanceResource = map[string]string{ "version": "6", "sgw_acl_invalid": "1.2.3.4/4", "sgw_acl_valid": "192.168.0.0/16", + "sgw_acl_valid2": "10.10.10.0/24", } -func resourceConfig(acls *string) string { - aclsLine := "" - if acls != nil { - aclsLine = fmt.Sprintf(`sgw_acl = %q`, *acls) +func parametersConfig(params map[string]string) string { + nonStringParams := []string{ + "down_after_milliseconds", + "enable_monitoring", + "failover_timeout", + "lua_time_limit", + "max_disk_threshold", + "maxclients", + "maxmemory_samples", + "metrics_frequency", + "min_replicas_max_lag", + "syslog", + "tls_ciphers", } - return fmt.Sprintf(` - %s - - resource "stackit_redis_instance" "instance" { - project_id = "%s" - name = "%s" - plan_name = "%s" - version = "%s" - parameters = { - %s - metrics_frequency = "%s" - } - } - - %s - `, - testutil.RedisProviderConfig(), - instanceResource["project_id"], - instanceResource["name"], - instanceResource["plan_name"], - instanceResource["version"], - aclsLine, - instanceResource["metrics_frequency"], - resourceConfigCredential(), - ) + parameters := "parameters = {" + for k, v := range params { + if utils.Contains(nonStringParams, k) { + parameters += fmt.Sprintf("%s = %s\n", k, v) + } else { + parameters += fmt.Sprintf("%s = %q\n", k, v) + } + } + parameters += "\n}" + return parameters } -func resourceConfigWithUpdate() string { +func resourceConfig(params map[string]string) string { return fmt.Sprintf(` %s @@ -69,9 +64,7 @@ func resourceConfigWithUpdate() string { name = "%s" plan_name = "%s" version = "%s" - parameters = { - sgw_acl = "%s" - } + %s } %s @@ -81,7 +74,7 @@ func resourceConfigWithUpdate() string { instanceResource["name"], instanceResource["plan_name"], instanceResource["version"], - instanceResource["sgw_acl_valid"], + parametersConfig(params), resourceConfigCredential(), ) } @@ -96,19 +89,38 @@ func resourceConfigCredential() string { } func TestAccRedisResource(t *testing.T) { - acls := instanceResource["sgw_acl_invalid"] resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckRedisDestroy, Steps: []resource.TestStep{ // Creation fail { - Config: resourceConfig(&acls), + Config: resourceConfig(map[string]string{"sgw_acl": instanceResource["sgw_acl_invalid"]}), ExpectError: regexp.MustCompile(`.*sgw_acl is invalid.*`), }, // Creation { - Config: resourceConfig(nil), + Config: resourceConfig(map[string]string{ + "sgw_acl": instanceResource["sgw_acl_valid"], + "down_after_milliseconds": "10000", + "enable_monitoring": "false", + "failover_timeout": "30000", + "graphite": "graphite.example.com:2003", + "lazyfree_lazy_eviction": "no", + "lazyfree_lazy_expire": "no", + "lua_time_limit": "5000", + "max_disk_threshold": "80", + "maxclients": "10000", + "maxmemory_policy": "volatile-lru", + "maxmemory_samples": "5", + "metrics_frequency": "10", + "metrics_prefix": "prefix", + "min_replicas_max_lag": "15", + "monitoring_instance_id": "mid", + "notify_keyspace_events": "Ex", + "syslog": `["syslog.example.com:123"]`, + "tls_protocols": "TLSv1.2", + }), Check: resource.ComposeAggregateTestCheckFunc( // Instance data resource.TestCheckResourceAttr("stackit_redis_instance.instance", "project_id", instanceResource["project_id"]), @@ -117,7 +129,28 @@ func TestAccRedisResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_redis_instance.instance", "plan_name", instanceResource["plan_name"]), resource.TestCheckResourceAttr("stackit_redis_instance.instance", "version", instanceResource["version"]), resource.TestCheckResourceAttr("stackit_redis_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttrSet("stackit_redis_instance.instance", "parameters.sgw_acl"), + + // Instance Params data + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl_valid"]), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.down_after_milliseconds", "10000"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.enable_monitoring", "false"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.failover_timeout", "30000"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.graphite", "graphite.example.com:2003"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.lazyfree_lazy_eviction", "no"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.lazyfree_lazy_expire", "no"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.lua_time_limit", "5000"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.max_disk_threshold", "80"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.maxclients", "10000"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.maxmemory_policy", "volatile-lru"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.maxmemory_samples", "5"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.metrics_frequency", "10"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.metrics_prefix", "prefix"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.min_replicas_max_lag", "15"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.monitoring_instance_id", "mid"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.notify_keyspace_events", "Ex"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.syslog.#", "1"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.syslog.0", "syslog.example.com:123"), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.tls_protocols", "TLSv1.2"), // Credential data resource.TestCheckResourceAttrPair( @@ -208,7 +241,7 @@ func TestAccRedisResource(t *testing.T) { }, // Update { - Config: resourceConfigWithUpdate(), + Config: resourceConfig(map[string]string{"sgw_acl": instanceResource["sgw_acl_valid2"]}), Check: resource.ComposeAggregateTestCheckFunc( // Instance data resource.TestCheckResourceAttr("stackit_redis_instance.instance", "project_id", instanceResource["project_id"]), @@ -217,7 +250,7 @@ func TestAccRedisResource(t *testing.T) { resource.TestCheckResourceAttr("stackit_redis_instance.instance", "plan_name", instanceResource["plan_name"]), resource.TestCheckResourceAttr("stackit_redis_instance.instance", "version", instanceResource["version"]), resource.TestCheckResourceAttr("stackit_redis_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl_valid"]), + resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl_valid2"]), ), }, // Deletion is done by the framework implicitly