From c86156cd0d3a976ac55e5c67e8e41e093c63637d Mon Sep 17 00:00:00 2001 From: Danil Zhigalin Date: Mon, 11 Nov 2024 12:24:55 +0100 Subject: [PATCH] Make service password filed configurable (#236) Allows to assign a password via terraform during service creation as well as to change passwords for already running services. That solves the issue of state inconsistency when already existing services are imported and allows customers to control the password of their services. When no password is specified during service creation, it is populated from initial password, that gets assigned by service create call. When a password is specified in resource configuration, services still get assigned an initial password, but it gets immediately overwritten by user specified password and it gets reflected in terraform state. When a service is imported into terraform and no password is specified in terraform configuration, it is set to null in terraform state. In that case password stays unmanaged by terraform. If a service is imported and a password exists in terraform configuration during the first import a password is set to null because we have no way of finding out that password from a running service. If terraform apply is done after such import, a password is applied to a service according to terraform configuration and it is either a no-op because the password matches, or it is overwritten to conform with terraform configuration. --- docs/resources/service.md | 2 +- internal/client/client.go | 2 ++ .../queries/change_service_password.graphql | 8 +++++ internal/client/service.go | 27 ++++++++++++++ internal/provider/provider_test.go | 1 + internal/provider/service_resource.go | 36 +++++++++++++++++-- internal/provider/service_resource_test.go | 12 +++++-- 7 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 internal/client/queries/change_service_password.graphql diff --git a/docs/resources/service.md b/docs/resources/service.md index d2af1a8..9458643 100644 --- a/docs/resources/service.md +++ b/docs/resources/service.md @@ -42,6 +42,7 @@ resource "timescale_service" "read_replica" { - `memory_gb` (Number) Memory GB - `milli_cpu` (Number) Milli CPU - `name` (String) Service Name is the configurable name assigned to this resource. If none is provided, a default will be generated by the provider. +- `password` (String, Sensitive) The Postgres password for this service - `paused` (Boolean) Paused status of the service. - `read_replica_source` (String) If set, this database will be a read replica of the provided source database. The region must be the same as the source, or if omitted will be handled by the provider - `region_code` (String) The region for this service. @@ -53,7 +54,6 @@ resource "timescale_service" "read_replica" { - `hostname` (String) The hostname for this service - `id` (String) Service ID is the unique identifier for this service. -- `password` (String, Sensitive) The Postgres password for this service. The password is provided once during service creation - `pooler_hostname` (String) Hostname of the pooler of this service. - `pooler_port` (Number) Port of the pooler of this service. - `port` (Number) The port for this service diff --git a/internal/client/client.go b/internal/client/client.go index f57c74f..3a6b120 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -40,6 +40,8 @@ var ( JWTFromCCQuery string //go:embed queries/set_replica_count.graphql SetReplicaCountMutation string + //go:embed queries/change_service_password.graphql + ResetServicePassword string // VCPs /////////////////////////////// //go:embed queries/vpcs.graphql diff --git a/internal/client/queries/change_service_password.graphql b/internal/client/queries/change_service_password.graphql new file mode 100644 index 0000000..720cda0 --- /dev/null +++ b/internal/client/queries/change_service_password.graphql @@ -0,0 +1,8 @@ +mutation ResetServicePassword($projectId: ID!, $serviceId: ID!, $password: String!, $passwordType: PasswordType!) { + resetServicePassword (data:{ + serviceId: $serviceId, + projectId: $projectId, + password: $password, + passwordType: $passwordType + }) +} diff --git a/internal/client/service.go b/internal/client/service.go index fea4f6f..5be6ffa 100644 --- a/internal/client/service.go +++ b/internal/client/service.go @@ -228,6 +228,33 @@ func (c *Client) SetReplicaCount(ctx context.Context, serviceID string, replicaC return nil } +func (c *Client) ResetServicePassword(ctx context.Context, serviceID string, password string) error { + tflog.Trace(ctx, "Client.ResetServicePassword") + + req := map[string]interface{}{ + "operationName": "ResetServicePassword", + "query": ResetServicePassword, + "variables": map[string]any{ + "projectId": c.projectID, + "serviceId": serviceID, + "password": password, + // we only support SCRAM password type, MD5 is deprecated in the backend + "passwordType": "SCRAM", + }, + } + var resp Response[any] + if err := c.do(ctx, req, &resp); err != nil { + return err + } + if len(resp.Errors) > 0 { + return resp.Errors[0] + } + if resp.Data == nil { + return errors.New("no response found") + } + return nil +} + type ResourceConfig struct { MilliCPU string MemoryGB string diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index e34abdc..dae5503 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -166,6 +166,7 @@ type ServiceConfig struct { ReadReplicaSource string Pooler bool Environment string + Password string } func (c *ServiceConfig) WithName(name string) *ServiceConfig { diff --git a/internal/provider/service_resource.go b/internal/provider/service_resource.go index d808101..8ea0363 100644 --- a/internal/provider/service_resource.go +++ b/internal/provider/service_resource.go @@ -160,8 +160,9 @@ The change has been taken into account but must still be propagated. You can run Create: true, }), "password": schema.StringAttribute{ - Description: "The Postgres password for this service. The password is provided once during service creation", - MarkdownDescription: "The Postgres password for this service. The password is provided once during service creation", + Description: "The Postgres password for this service", + MarkdownDescription: "The Postgres password for this service", + Optional: true, Computed: true, Sensitive: true, PlanModifiers: []planmodifier.String{ @@ -342,7 +343,10 @@ func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest return } - plan.Password = types.StringValue(response.InitialPassword) + // Set the password to the initial password if not provided by the user + if plan.Password.IsNull() || plan.Password.IsUnknown() { + plan.Password = types.StringValue(response.InitialPassword) + } service, err := r.waitForServiceReadiness(ctx, response.Service.ID, plan.Timeouts) if err != nil { resp.Diagnostics.AddError(ErrCreateTimeout, fmt.Sprintf("error occurred while waiting for service deployment, got error: %s", err)) @@ -353,6 +357,22 @@ func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest } return } + + // Check if a user-specified password was provided and update it if so, but only if the service is not a read replica + if !plan.Password.IsNull() && plan.Password.ValueString() != response.InitialPassword && readReplicaSource == "" { + err = r.client.ResetServicePassword(ctx, service.ID, plan.Password.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Setting the password failed", fmt.Sprintf("Unable to set user configured password, got error: %s", err)) + + // Attempt to delete the service to avoid leaving an instance in an inconsistent state + _, deleteErr := r.client.DeleteService(context.Background(), service.ID) + if deleteErr != nil { + resp.Diagnostics.AddWarning("Error Deleting Resource", fmt.Sprintf("Failed to delete service after password setting error; Remove orphaned resources from your account manually. Error: %s", deleteErr)) + } + return + } + } + resourceModel := serviceToResource(resp.Diagnostics, service, plan) resp.Diagnostics.Append(resp.State.Set(ctx, resourceModel)...) if resp.Diagnostics.HasError() { @@ -577,6 +597,16 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest resp.Diagnostics.AddError(ErrCreateTimeout, fmt.Sprintf("error occurred while waiting for service reconfiguration, got error: %s", err)) return } + + // Update Password if it has changed and if it's not a read replica + if !plan.Password.Equal(state.Password) && !plan.Password.IsNull() && readReplicaSource == "" { + err := r.client.ResetServicePassword(ctx, serviceID, plan.Password.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Failed to update password", fmt.Sprintf("Unable to update password, got error: %s", err)) + return + } + } + resources := serviceToResource(resp.Diagnostics, service, plan) resp.Diagnostics.Append(resp.State.Set(ctx, resources)...) diff --git a/internal/provider/service_resource_test.go b/internal/provider/service_resource_test.go index 1776850..0011e0b 100644 --- a/internal/provider/service_resource_test.go +++ b/internal/provider/service_resource_test.go @@ -269,9 +269,11 @@ func TestServiceResource_CustomConf(t *testing.T) { RegionCode: "eu-central-1", MilliCPU: 1000, MemoryGB: 4, + Password: "test123456789", }), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("timescale_service.custom", "name", "service resource test conf"), + resource.TestCheckResourceAttr("timescale_service.custom", "password", "test123456789"), resource.TestCheckResourceAttr("timescale_service.custom", "region_code", "eu-central-1"), resource.TestCheckNoResourceAttr("timescale_service.custom", "vpc_id"), ), @@ -369,6 +371,12 @@ func newServiceCustomConfig(resourceName string, config ServiceConfig) string { if config.Timeouts.Create == "" { config.Timeouts.Create = "30m" } + + passwordLine := "" + if config.Password != "" { + passwordLine = fmt.Sprintf("\n\t\tpassword = %q", config.Password) + } + return providerConfig + fmt.Sprintf(` resource "timescale_service" "%s" { name = %q @@ -378,6 +386,6 @@ func newServiceCustomConfig(resourceName string, config ServiceConfig) string { milli_cpu = %d memory_gb = %d region_code = %q - enable_ha_replica = %t - }`, resourceName, config.Name, config.Timeouts.Create, config.MilliCPU, config.MemoryGB, config.RegionCode, config.EnableHAReplica) + enable_ha_replica = %t%s + }`, resourceName, config.Name, config.Timeouts.Create, config.MilliCPU, config.MemoryGB, config.RegionCode, config.EnableHAReplica, passwordLine) }