From 0b612bc6c5f3bb94f402870b6e76d7cdbf806c24 Mon Sep 17 00:00:00 2001 From: Jonathan Remy Date: Thu, 4 Dec 2025 06:08:11 +0100 Subject: [PATCH] feat(redis): add support for cluster renew certificate action --- .../redis_cluster_renew_certificate_action.md | 19 ++ ...action_cluster_renew_certificate_action.go | 166 ++++++++++++++++++ ...n_cluster_renew_certificate_action_test.go | 56 ++++++ provider/framework.go | 2 + ...s_cluster_renew_certificate_action.md.tmpl | 10 ++ 5 files changed, 253 insertions(+) create mode 100644 docs/actions/redis_cluster_renew_certificate_action.md create mode 100644 internal/services/redis/action_cluster_renew_certificate_action.go create mode 100644 internal/services/redis/action_cluster_renew_certificate_action_test.go create mode 100644 templates/actions/redis_cluster_renew_certificate_action.md.tmpl diff --git a/docs/actions/redis_cluster_renew_certificate_action.md b/docs/actions/redis_cluster_renew_certificate_action.md new file mode 100644 index 0000000000..c416d9b421 --- /dev/null +++ b/docs/actions/redis_cluster_renew_certificate_action.md @@ -0,0 +1,19 @@ +--- +subcategory: "Redis" +page_title: "Scaleway: scaleway_redis_cluster_renew_certificate_action" +--- + +# scaleway_redis_cluster_renew_certificate_action (Action) + + +## Schema + +### Required + +- `cluster_id` (String) Redis cluster ID to renew certificate for. Can be a plain UUID or a zonal ID. + +### Optional + +- `wait` (Boolean) Wait for the certificate renewal to complete before returning. +- `zone` (String) Zone of the Redis cluster. If not set, the zone is derived from the cluster_id when possible or from the provider configuration. + diff --git a/internal/services/redis/action_cluster_renew_certificate_action.go b/internal/services/redis/action_cluster_renew_certificate_action.go new file mode 100644 index 0000000000..7ee8a6d71c --- /dev/null +++ b/internal/services/redis/action_cluster_renew_certificate_action.go @@ -0,0 +1,166 @@ +package redis + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/scaleway/scaleway-sdk-go/api/redis/v1" + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/locality" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/zonal" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/meta" +) + +var ( + _ action.Action = (*ClusterRenewCertificateAction)(nil) + _ action.ActionWithConfigure = (*ClusterRenewCertificateAction)(nil) +) + +type ClusterRenewCertificateAction struct { + redisAPI *redis.API +} + +func (a *ClusterRenewCertificateAction) Configure(ctx context.Context, req action.ConfigureRequest, resp *action.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + m, ok := req.ProviderData.(*meta.Meta) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Action Configure Type", + fmt.Sprintf("Expected *meta.Meta, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + client := m.ScwClient() + a.redisAPI = redis.NewAPI(client) +} + +func (a *ClusterRenewCertificateAction) Metadata(ctx context.Context, req action.MetadataRequest, resp *action.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_redis_cluster_renew_certificate_action" +} + +type ClusterRenewCertificateActionModel struct { + ClusterID types.String `tfsdk:"cluster_id"` + Zone types.String `tfsdk:"zone"` + Wait types.Bool `tfsdk:"wait"` +} + +func NewClusterRenewCertificateAction() action.Action { + return &ClusterRenewCertificateAction{} +} + +func (a *ClusterRenewCertificateAction) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "cluster_id": schema.StringAttribute{ + Required: true, + Description: "Redis cluster ID to renew certificate for. Can be a plain UUID or a zonal ID.", + }, + "zone": schema.StringAttribute{ + Optional: true, + Description: "Zone of the Redis cluster. If not set, the zone is derived from the cluster_id when possible or from the provider configuration.", + }, + "wait": schema.BoolAttribute{ + Optional: true, + Description: "Wait for the certificate renewal to complete before returning.", + }, + }, + } +} + +func (a *ClusterRenewCertificateAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + var data ClusterRenewCertificateActionModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + if a.redisAPI == nil { + resp.Diagnostics.AddError( + "Unconfigured redisAPI", + "The action was not properly configured. The Scaleway client is missing. "+ + "This is usually a bug in the provider. Please report it to the maintainers.", + ) + + return + } + + if data.ClusterID.IsNull() || data.ClusterID.ValueString() == "" { + resp.Diagnostics.AddError( + "Missing cluster_id", + "The cluster_id attribute is required to renew the Redis cluster certificate.", + ) + + return + } + + clusterID := locality.ExpandID(data.ClusterID.ValueString()) + + var ( + zone scw.Zone + err error + ) + + if !data.Zone.IsNull() && data.Zone.ValueString() != "" { + zone = scw.Zone(data.Zone.ValueString()) + } else { + // Try to derive zone from the cluster_id if it is a zonal ID. + if derivedZone, id, parseErr := zonal.ParseID(data.ClusterID.ValueString()); parseErr == nil { + zone = derivedZone + clusterID = id + } + } + + renewReq := &redis.RenewClusterCertificateRequest{ + ClusterID: clusterID, + } + + if zone != "" { + renewReq.Zone = zone + } + + cluster, err := a.redisAPI.RenewClusterCertificate(renewReq, scw.WithContext(ctx)) + if err != nil { + resp.Diagnostics.AddError( + "Error executing Redis RenewClusterCertificate action", + fmt.Sprintf("Failed to renew certificate for cluster %s: %s", clusterID, err), + ) + + return + } + + if data.Wait.ValueBool() { + waitZone := cluster.Zone + if waitZone == "" && zone != "" { + waitZone = zone + } + + if waitZone == "" { + resp.Diagnostics.AddError( + "Missing zone for wait operation", + "Could not determine zone to wait for Redis cluster certificate renewal completion.", + ) + + return + } + + _, err = waitForCluster(ctx, a.redisAPI, waitZone, clusterID, defaultRedisClusterTimeout) + if err != nil { + resp.Diagnostics.AddError( + "Error waiting for Redis cluster certificate renewal completion", + fmt.Sprintf("Certificate renewal for cluster %s did not complete: %s", clusterID, err), + ) + + return + } + } +} diff --git a/internal/services/redis/action_cluster_renew_certificate_action_test.go b/internal/services/redis/action_cluster_renew_certificate_action_test.go new file mode 100644 index 0000000000..2c60c35e2c --- /dev/null +++ b/internal/services/redis/action_cluster_renew_certificate_action_test.go @@ -0,0 +1,56 @@ +package redis_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" +) + +func TestAccActionRedisClusterRenewCertificate_Basic(t *testing.T) { + if acctest.IsRunningOpenTofu() { + t.Skip("Skipping TestAccActionRedisClusterRenewCertificate_Basic because actions are not yet supported on OpenTofu") + } + + // Note: This test will fail with 501 Not Implemented until Scaleway implements + // the RenewClusterCertificate API endpoint. The SDK method exists but the API + // endpoint is not yet available on the server side. + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + latestRedisVersion := getLatestVersion(tt) + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "scaleway_redis_cluster" "main" { + name = "test-redis-action-renew-certificate" + version = "%s" + node_type = "RED1-XS" + user_name = "my_initial_user" + password = "thiZ_is_v&ry_s3cret" + cluster_size = 1 + tls_enabled = "true" + zone = "fr-par-2" + + lifecycle { + action_trigger { + events = [after_create] + actions = [action.scaleway_redis_cluster_renew_certificate_action.main] + } + } + } + + action "scaleway_redis_cluster_renew_certificate_action" "main" { + config { + cluster_id = scaleway_redis_cluster.main.id + wait = true + } + } + `, latestRedisVersion), + }, + }, + }) +} diff --git a/provider/framework.go b/provider/framework.go index 03dedf8db3..059a3999b7 100644 --- a/provider/framework.go +++ b/provider/framework.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/scaleway/terraform-provider-scaleway/v2/internal/meta" "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/instance" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/redis" ) var ( @@ -138,6 +139,7 @@ func (p *ScalewayProvider) Actions(_ context.Context) []func() action.Action { var res []func() action.Action res = append(res, instance.NewServerAction) + res = append(res, redis.NewClusterRenewCertificateAction) return res } diff --git a/templates/actions/redis_cluster_renew_certificate_action.md.tmpl b/templates/actions/redis_cluster_renew_certificate_action.md.tmpl new file mode 100644 index 0000000000..9bc3ceba15 --- /dev/null +++ b/templates/actions/redis_cluster_renew_certificate_action.md.tmpl @@ -0,0 +1,10 @@ +{{- /*gotype: github.com/hashicorp/terraform-plugin-docs/internal/provider.ActionTemplateType */ -}} +--- +subcategory: "Redis" +page_title: "Scaleway: scaleway_redis_cluster_renew_certificate_action" +--- + +# scaleway_redis_cluster_renew_certificate_action (Action) + +{{ .SchemaMarkdown }} +