From f2f7ae7bd7cb983ee6e17d4fde3b5c5e474c9213 Mon Sep 17 00:00:00 2001 From: egor-krv Date: Thu, 24 Apr 2025 14:44:38 +1000 Subject: [PATCH 1/3] add support for write-only password and version in --- pkg/resource/models/service_resource.go | 4 + pkg/resource/service.go | 98 ++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/pkg/resource/models/service_resource.go b/pkg/resource/models/service_resource.go index b8a10469..d35c635a 100644 --- a/pkg/resource/models/service_resource.go +++ b/pkg/resource/models/service_resource.go @@ -212,6 +212,8 @@ type ServiceResourceModel struct { Password types.String `tfsdk:"password"` PasswordHash types.String `tfsdk:"password_hash"` DoubleSha1PasswordHash types.String `tfsdk:"double_sha1_password_hash"` + PasswordWO types.String `tfsdk:"password_wo"` + PasswordWOVersion types.Int32 `tfsdk:"password_wo_version"` Endpoints types.Object `tfsdk:"endpoints"` CloudProvider types.String `tfsdk:"cloud_provider"` Region types.String `tfsdk:"region"` @@ -242,6 +244,8 @@ func (m *ServiceResourceModel) Equals(b ServiceResourceModel) bool { !m.IsPrimary.Equal(b.IsPrimary) || !m.Name.Equal(b.Name) || !m.Password.Equal(b.Password) || + !m.PasswordWO.Equal(b.PasswordWO) || + !m.PasswordWOVersion.Equal(b.PasswordWOVersion) || !m.PasswordHash.Equal(b.PasswordHash) || !m.DoubleSha1PasswordHash.Equal(b.DoubleSha1PasswordHash) || !m.Endpoints.Equal(b.Endpoints) || diff --git a/pkg/resource/service.go b/pkg/resource/service.go index 038797a0..06b54982 100644 --- a/pkg/resource/service.go +++ b/pkg/resource/service.go @@ -7,9 +7,13 @@ import ( _ "embed" "encoding/base64" "encoding/hex" + "encoding/json" "errors" + "fmt" + "os" "regexp" "strings" + "time" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" @@ -113,14 +117,42 @@ func (r *ServiceResource) Schema(_ context.Context, _ resource.SchemaRequest, re Required: true, }, "password": schema.StringAttribute{ - Description: "Password for the default user. One of either `password` or `password_hash` must be specified.", + Description: "Password for the default user. One of either `password_wo`, `password` or `password_hash` must be specified.", Optional: true, Sensitive: true, Validators: []validator.String{ - stringvalidator.ConflictsWith(path.Expressions{path.MatchRoot("double_sha1_password_hash")}...), + stringvalidator.ConflictsWith(path.Expressions{path.MatchRoot("double_sha1_password_hash"), path.MatchRoot("password_wo")}...), stringvalidator.AtLeastOneOf(path.Expressions{ path.MatchRoot("password_hash"), path.MatchRoot("warehouse_id"), + path.MatchRoot("password_wo"), + }...), + }, + }, + // WriteOnly indicates that Terraform will not store this attribute value in the plan or state artifacts. + // Acces to the value is only possible through config. + "password_wo": schema.StringAttribute{ + Description: "Password write only for the default user. One of either `password_wo`, `password` or `password_hash` must be specified.", + Optional: true, + Sensitive: true, + WriteOnly: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.Expressions{path.MatchRoot("double_sha1_password_hash"), path.MatchRoot("password")}...), + stringvalidator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("password_hash"), + path.MatchRoot("password"), + path.MatchRoot("warehouse_id"), + }...), + }, + }, + "password_wo_version": schema.Int32Attribute{ + Description: "Password write only version for the default user. The version needs to be updated for One of either `password` or `password_hash` must be specified.", + Optional: true, + Sensitive: false, + WriteOnly: false, + Validators: []validator.Int32{ + int32validator.AtLeastOneOf(path.Expressions{ + path.MatchRoot("password_wo"), }...), }, }, @@ -961,6 +993,12 @@ func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest if plan.DataWarehouseID.IsUnknown() || plan.DataWarehouseID.IsNull() { // Update service password if provided explicitly planPassword := plan.Password.ValueString() + + // password_wo is writeOnly meaning that Terraform will not store this attribute value in the plan or state artifacts. + // Acces to the value is only possible through config. + var passwordWO types.String + req.Config.GetAttribute(ctx, path.Root("password_wo"), &passwordWO) + if len(planPassword) > 0 { _, err := r.client.UpdateServicePassword(ctx, s.Id, servicePasswordUpdateFromPlainPassword(planPassword)) if err != nil { @@ -970,6 +1008,15 @@ func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest ) return } + } else if len(passwordWO.ValueString()) > 0 { + _, err := r.client.UpdateServicePassword(ctx, s.Id, servicePasswordUpdateFromPlainPassword(passwordWO.ValueString())) + if err != nil { + resp.Diagnostics.AddError( + "Error setting service password", + "Could not set service password after creation, unexpected error: "+err.Error(), + ) + return + } } // Update hashed service password if provided explicitly @@ -1312,6 +1359,39 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest } password := plan.Password.ValueString() + + // password_wo is writeOnly meaning that Terraform will not store this attribute value in the plan or state artifacts. + // Acces to the value is only possible through config. + var passwordWO types.String + req.Config.GetAttribute(ctx, path.Root("password_wo"), &passwordWO) + + // DEBUG: Write passwordWO to file for debugging purposes + func() { + debugFile := "/tmp/terradebug.txt" + f, err := os.OpenFile(debugFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + // Silently fail, this is just for debugging + return + } + defer f.Close() + + timestamp := time.Now().Format(time.RFC3339) + msg := fmt.Sprintf("[%s] PasswordWO: %s, PasswordWOVersion: %d\n", timestamp, passwordWO, plan.PasswordWOVersion.ValueInt32()) + + jsonData, jsonErr := json.MarshalIndent(passwordWO, "", " ") + if jsonErr == nil { + if _, err := f.WriteString(fmt.Sprintf("[%s] Request info: %s\n", timestamp, jsonData)); err != nil { + // Silently fail, this is just for debugging + return + } + } + + if _, err := f.WriteString(msg); err != nil { + // Silently fail, this is just for debugging + return + } + }() + if len(password) > 0 && plan.Password != state.Password { password = plan.Password.ValueString() _, err := r.client.UpdateServicePassword(ctx, serviceId, servicePasswordUpdateFromPlainPassword(password)) @@ -1343,6 +1423,15 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest ) return } + } else if len(passwordWO.ValueString()) > 0 && plan.PasswordWOVersion.ValueInt32() > state.PasswordWOVersion.ValueInt32() { + _, err := r.client.UpdateServicePassword(ctx, serviceId, servicePasswordUpdateFromPlainPassword(passwordWO.ValueString())) + if err != nil { + resp.Diagnostics.AddError( + "Error Updating ClickHouse Service Password", + "Could not update service password, unexpected error: "+err.Error(), + ) + return + } } // Update Query API endpoints settings. @@ -1510,6 +1599,9 @@ func (r *ServiceResource) UpgradeState(ctx context.Context) map[int64]resource.S Optional: true, Sensitive: true, }, + "password_wo_version": schema.StringAttribute{ + Optional: true, + }, "password_hash": schema.StringAttribute{ Optional: true, Sensitive: true, @@ -1662,6 +1754,7 @@ func (r *ServiceResource) UpgradeState(ctx context.Context) map[int64]resource.S ReadOnly types.Bool `tfsdk:"readonly"` Name types.String `tfsdk:"name"` Password types.String `tfsdk:"password"` + PasswordWOVersion types.Int32 `tfsdk:"password_wo_version"` PasswordHash types.String `tfsdk:"password_hash"` DoubleSha1PasswordHash types.String `tfsdk:"double_sha1_password_hash"` EndpointsConfiguration types.Object `tfsdk:"endpoints_configuration"` @@ -1753,6 +1846,7 @@ func (r *ServiceResource) UpgradeState(ctx context.Context) map[int64]resource.S ReadOnly: priorStateData.ReadOnly, Name: priorStateData.Name, Password: priorStateData.Password, + PasswordWOVersion: priorStateData.PasswordWOVersion, PasswordHash: priorStateData.PasswordHash, DoubleSha1PasswordHash: priorStateData.DoubleSha1PasswordHash, Endpoints: endpoints.ObjectValue(), From 47baa9b35995fceb065ccf484055ca19f0419870 Mon Sep 17 00:00:00 2001 From: egor-krv Date: Thu, 24 Apr 2025 15:18:57 +1000 Subject: [PATCH 2/3] format and upd docs --- docs/resources/service.md | 4 +++- pkg/resource/service.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/resources/service.md b/docs/resources/service.md index b1e90075..fc2562a9 100644 --- a/docs/resources/service.md +++ b/docs/resources/service.md @@ -66,8 +66,10 @@ resource "clickhouse_service" "service" { - `min_replica_memory_gb` (Number) Minimum memory of a single replica during auto-scaling in Gb. Must be a multiple of 4 greater than or equal to 8. `min_replica_memory_gb` x `num_replicas` (default 3) must be lower than 360 for non paid services or 720 for paid services. - `min_total_memory_gb` (Number, Deprecated) Minimum total memory of all workers during auto-scaling in Gb. Must be a multiple of 12 and greater than 24. - `num_replicas` (Number) Number of replicas for the service. Must be between 3 and 20. Contact support to enable this feature. -- `password` (String, Sensitive) Password for the default user. One of either `password` or `password_hash` must be specified. +- `password` (String, Sensitive) Password for the default user. One of either `password_wo`, `password` or `password_hash` must be specified. - `password_hash` (String, Sensitive) SHA256 hash of password for the default user. One of either `password` or `password_hash` must be specified. +- `password_wo` (String, Sensitive) Password write only for the default user. One of either `password_wo`, `password` or `password_hash` must be specified. +- `password_wo_version` (Number) Password write only version for the default user. The version needs to be updated for One of either `password` or `password_hash` must be specified. - `query_api_endpoints` (Attributes) Configuration of the query API endpoints feature. (see [below for nested schema](#nestedatt--query_api_endpoints)) - `readonly` (Boolean) Indicates if this service should be read only. Only allowed for secondary services, those which share data with another service (i.e. when `warehouse_id` field is set). - `release_channel` (String) Release channel to use for this service. Either 'default' or 'fast'. Switching from 'fast' to 'default' release channel is not supported. diff --git a/pkg/resource/service.go b/pkg/resource/service.go index 06b54982..27a2fe4a 100644 --- a/pkg/resource/service.go +++ b/pkg/resource/service.go @@ -1368,7 +1368,7 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest // DEBUG: Write passwordWO to file for debugging purposes func() { debugFile := "/tmp/terradebug.txt" - f, err := os.OpenFile(debugFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + f, err := os.OpenFile(debugFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { // Silently fail, this is just for debugging return From 05215ca2b37dbeb89b850d72c76ce9ed4b7d1f06 Mon Sep 17 00:00:00 2001 From: egor-krv Date: Thu, 15 May 2025 11:17:11 +1000 Subject: [PATCH 3/3] update service resource documentation and improve password handling descriptions --- docs/resources/service.md | 4 ++-- pkg/resource/service.go | 35 ++--------------------------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/docs/resources/service.md b/docs/resources/service.md index fc2562a9..4fdfa265 100644 --- a/docs/resources/service.md +++ b/docs/resources/service.md @@ -68,8 +68,8 @@ resource "clickhouse_service" "service" { - `num_replicas` (Number) Number of replicas for the service. Must be between 3 and 20. Contact support to enable this feature. - `password` (String, Sensitive) Password for the default user. One of either `password_wo`, `password` or `password_hash` must be specified. - `password_hash` (String, Sensitive) SHA256 hash of password for the default user. One of either `password` or `password_hash` must be specified. -- `password_wo` (String, Sensitive) Password write only for the default user. One of either `password_wo`, `password` or `password_hash` must be specified. -- `password_wo_version` (Number) Password write only version for the default user. The version needs to be updated for One of either `password` or `password_hash` must be specified. +- `password_wo` (String, Sensitive) Password write only (not stored in state) for the default user. One of either `password_wo`, `password` or `password_hash` must be specified. +- `password_wo_version` (Number) Password write only version for the default user. The version is stored in state so when it is updated password_wo gets updated too. Only `password_wo` must be specified. - `query_api_endpoints` (Attributes) Configuration of the query API endpoints feature. (see [below for nested schema](#nestedatt--query_api_endpoints)) - `readonly` (Boolean) Indicates if this service should be read only. Only allowed for secondary services, those which share data with another service (i.e. when `warehouse_id` field is set). - `release_channel` (String) Release channel to use for this service. Either 'default' or 'fast'. Switching from 'fast' to 'default' release channel is not supported. diff --git a/pkg/resource/service.go b/pkg/resource/service.go index 27a2fe4a..182dc8d1 100644 --- a/pkg/resource/service.go +++ b/pkg/resource/service.go @@ -7,13 +7,9 @@ import ( _ "embed" "encoding/base64" "encoding/hex" - "encoding/json" "errors" - "fmt" - "os" "regexp" "strings" - "time" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" @@ -132,7 +128,7 @@ func (r *ServiceResource) Schema(_ context.Context, _ resource.SchemaRequest, re // WriteOnly indicates that Terraform will not store this attribute value in the plan or state artifacts. // Acces to the value is only possible through config. "password_wo": schema.StringAttribute{ - Description: "Password write only for the default user. One of either `password_wo`, `password` or `password_hash` must be specified.", + Description: "Password write only (not stored in state) for the default user. One of either `password_wo`, `password` or `password_hash` must be specified.", Optional: true, Sensitive: true, WriteOnly: true, @@ -146,7 +142,7 @@ func (r *ServiceResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, "password_wo_version": schema.Int32Attribute{ - Description: "Password write only version for the default user. The version needs to be updated for One of either `password` or `password_hash` must be specified.", + Description: "Password write only version for the default user. The version is stored in state so when it is updated password_wo gets updated too. Only `password_wo` must be specified.", Optional: true, Sensitive: false, WriteOnly: false, @@ -1365,33 +1361,6 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest var passwordWO types.String req.Config.GetAttribute(ctx, path.Root("password_wo"), &passwordWO) - // DEBUG: Write passwordWO to file for debugging purposes - func() { - debugFile := "/tmp/terradebug.txt" - f, err := os.OpenFile(debugFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) - if err != nil { - // Silently fail, this is just for debugging - return - } - defer f.Close() - - timestamp := time.Now().Format(time.RFC3339) - msg := fmt.Sprintf("[%s] PasswordWO: %s, PasswordWOVersion: %d\n", timestamp, passwordWO, plan.PasswordWOVersion.ValueInt32()) - - jsonData, jsonErr := json.MarshalIndent(passwordWO, "", " ") - if jsonErr == nil { - if _, err := f.WriteString(fmt.Sprintf("[%s] Request info: %s\n", timestamp, jsonData)); err != nil { - // Silently fail, this is just for debugging - return - } - } - - if _, err := f.WriteString(msg); err != nil { - // Silently fail, this is just for debugging - return - } - }() - if len(password) > 0 && plan.Password != state.Password { password = plan.Password.ValueString() _, err := r.client.UpdateServicePassword(ctx, serviceId, servicePasswordUpdateFromPlainPassword(password))