Skip to content

Commit

Permalink
Make service password filed configurable (#236)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
minkimipt authored Nov 11, 2024
1 parent eaf998f commit c86156c
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 6 deletions.
2 changes: 1 addition & 1 deletion docs/resources/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions internal/client/queries/change_service_password.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mutation ResetServicePassword($projectId: ID!, $serviceId: ID!, $password: String!, $passwordType: PasswordType!) {
resetServicePassword (data:{
serviceId: $serviceId,
projectId: $projectId,
password: $password,
passwordType: $passwordType
})
}
27 changes: 27 additions & 0 deletions internal/client/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ type ServiceConfig struct {
ReadReplicaSource string
Pooler bool
Environment string
Password string
}

func (c *ServiceConfig) WithName(name string) *ServiceConfig {
Expand Down
36 changes: 33 additions & 3 deletions internal/provider/service_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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))
Expand All @@ -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() {
Expand Down Expand Up @@ -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)...)

Expand Down
12 changes: 10 additions & 2 deletions internal/provider/service_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
),
Expand Down Expand Up @@ -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
Expand All @@ -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)
}

0 comments on commit c86156c

Please sign in to comment.