Skip to content

Commit 1759e99

Browse files
committed
feat(resource,datasource): restore, read replicas, data sources, timeouts (Phase 5)
Resource: - read_replica_of (RequiresReplace, ConflictsWith restore/password): Create calls CreatePostgresReadReplica. - restore_to_point_in_time {source_id, restore_target} (RequiresReplace, ConflictsWith read_replica_of): Create calls RestorePostgres. Restored instance name = top-level name. - Three-path Create switch (replica / restore / standard), shared wait+sync. - timeouts {} block (create+update) wired into the create + update waits. Data sources (alpha-gated via pkg/datasource/register_{stable,debug}.go; provider.DataSources delegates to GetDataSourceFactories): - clickhouse_postgres_service (by id), clickhouse_postgres_services (list), clickhouse_postgres_service_ca_certificates (PEM). API: GetPostgresCaCertificates now routes through doRequest (gains retry/User-Agent/logging); the separate doRawRequest is unnecessary since doRequest returns bytes verbatim and formatLogBody tolerates non-JSON. Dependent-replica: e2e confirmed the server returns 200 (not 409) when deleting a primary with a replica, so the previously-removed fail-fast heuristic correctly stays removed. Helper-level unit tests for the request builders; live e2e validated read replica (psql), all 3 data sources, CA cert, timeouts, and dependent-replica delete behavior.
1 parent b16d754 commit 1759e99

16 files changed

Lines changed: 904 additions & 53 deletions

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/google/go-cmp v0.7.0
99
github.com/hashicorp/terraform-plugin-docs v0.25.0
1010
github.com/hashicorp/terraform-plugin-framework v1.19.0
11+
github.com/hashicorp/terraform-plugin-framework-timeouts v0.7.0
1112
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0
1213
github.com/hashicorp/terraform-plugin-go v0.31.0
1314
github.com/hashicorp/terraform-plugin-log v0.10.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ github.com/hashicorp/terraform-plugin-docs v0.25.0 h1:qHs1V257NxVe8tv6HS4UQfNqja
101101
github.com/hashicorp/terraform-plugin-docs v0.25.0/go.mod h1:MQggCmY8zgP7R7E/cC0b0cmTvA9hSj3ZKyrrsDjRbLo=
102102
github.com/hashicorp/terraform-plugin-framework v1.19.0 h1:q0bwyhxAOR3vfdgbk9iplv3MlTv/dhBHTXjQOtQDoBA=
103103
github.com/hashicorp/terraform-plugin-framework v1.19.0/go.mod h1:YRXOBu0jvs7xp4AThBbX4mAzYaMJ1JgtFH//oGKxwLc=
104+
github.com/hashicorp/terraform-plugin-framework-timeouts v0.7.0 h1:jblRy1PkLfPm5hb5XeMa3tezusnMRziUGqtT5epSYoI=
105+
github.com/hashicorp/terraform-plugin-framework-timeouts v0.7.0/go.mod h1:5jm2XK8uqrdiSRfD5O47OoxyGMCnwTcl8eoiDgSa+tc=
104106
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0 h1:Zz3iGgzxe/1XBkooZCewS0nJAaCFPFPHdNJd8FgE4Ow=
105107
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0/go.mod h1:GBKTNGbGVJohU03dZ7U8wHqc2zYnMUawgCN+gC0itLc=
106108
github.com/hashicorp/terraform-plugin-go v0.31.0 h1:0Fz2r9DQ+kNNl6bx8HRxFd1TfMKUvnrOtvJPmp3Z0q8=
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
> **Alpha data source.** Exposed only in alpha builds of the provider
2+
> (`-tags alpha`). The backing ClickHouse Cloud Managed Postgres API is
3+
> `beta` server-side. Expect breaking changes between alpha releases.
4+
5+
Fetches a single [ClickHouse Cloud Managed Postgres](https://clickhouse.com/cloud/postgres)
6+
service by ID, including its current `pg_config` / `pgbouncer_config`.
7+
8+
Returns the same attributes as the `clickhouse_postgres_service` resource
9+
**except `password`** (the server never echoes it on read). The
10+
`connection_string` (which embeds the password) is returned and marked
11+
sensitive. `tags`, `pg_config`, and `pgbouncer_config` are exposed as
12+
read-only string maps. Server-reserved `chc_`-prefixed tags are filtered out.
13+
14+
## Example
15+
16+
```hcl
17+
data "clickhouse_postgres_service" "example" {
18+
id = "5f1a3d9d-3d8e-8ed0-8d25-545aec9d5e3b"
19+
}
20+
21+
output "host" {
22+
value = data.clickhouse_postgres_service.example.hostname
23+
}
24+
```
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
> **Alpha data source.** Exposed only in alpha builds of the provider
2+
> (`-tags alpha`). The backing ClickHouse Cloud Managed Postgres API is
3+
> `beta` server-side. Expect breaking changes between alpha releases.
4+
5+
Fetches the PEM-encoded CA certificate chain for a
6+
[ClickHouse Cloud Managed Postgres](https://clickhouse.com/cloud/postgres)
7+
service. Use it to pin the CA when connecting with `sslmode=verify-full`.
8+
9+
Input `service_id`; output `certificate` (the PEM chain).
10+
11+
## Example
12+
13+
```hcl
14+
data "clickhouse_postgres_service_ca_certificates" "example" {
15+
service_id = clickhouse_postgres_service.example.id
16+
}
17+
18+
resource "local_file" "ca" {
19+
content = data.clickhouse_postgres_service_ca_certificates.example.certificate
20+
filename = "${path.module}/pg-ca.pem"
21+
}
22+
```
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
> **Alpha data source.** Exposed only in alpha builds of the provider
2+
> (`-tags alpha`). The backing ClickHouse Cloud Managed Postgres API is
3+
> `beta` server-side. Expect breaking changes between alpha releases.
4+
5+
Lists all [ClickHouse Cloud Managed Postgres](https://clickhouse.com/cloud/postgres)
6+
services in the organization. Returns a `services` list of summary objects
7+
(`id`, `name`, `cloud_provider`, `region`, `postgres_version`, `size`,
8+
`ha_type`, `state`, `created_at`, `is_primary`).
9+
10+
The list endpoint does not return `connection_string`, `password`, or
11+
`pg_config`; use the `clickhouse_postgres_service` data source (by ID) for the
12+
full set of attributes. Useful for `for_each` over existing services.
13+
14+
## Example
15+
16+
```hcl
17+
data "clickhouse_postgres_services" "all" {}
18+
19+
output "service_names" {
20+
value = [for s in data.clickhouse_postgres_services.all.services : s.name]
21+
}
22+
```

pkg/datasource/postgres_service.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
//go:build alpha
2+
3+
package datasource
4+
5+
import (
6+
"context"
7+
_ "embed"
8+
"strings"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/attr"
11+
"github.com/hashicorp/terraform-plugin-framework/datasource"
12+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/diag"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
16+
"github.com/ClickHouse/terraform-provider-clickhouse/pkg/internal/api"
17+
)
18+
19+
//go:embed descriptions/postgres_service.md
20+
var postgresServiceDataSourceDescription string
21+
22+
// postgresReservedTagPrefix mirrors the resource's chc_ reservation; system
23+
// tags are filtered out of data-source output the same way.
24+
const postgresReservedTagPrefix = "chc_"
25+
26+
// postgresDefaultPort mirrors the resource's hardcoded listening port.
27+
const postgresDefaultPort int64 = 5432
28+
29+
var _ datasource.DataSource = &postgresServiceDataSource{}
30+
31+
// NewPostgresServiceDataSource fetches a single Managed Postgres service by ID.
32+
func NewPostgresServiceDataSource() datasource.DataSource {
33+
return &postgresServiceDataSource{}
34+
}
35+
36+
type postgresServiceDataSource struct {
37+
client api.Client
38+
}
39+
40+
type postgresServiceDataSourceModel struct {
41+
ID types.String `tfsdk:"id"`
42+
Name types.String `tfsdk:"name"`
43+
CloudProvider types.String `tfsdk:"cloud_provider"`
44+
Region types.String `tfsdk:"region"`
45+
PostgresVersion types.String `tfsdk:"postgres_version"`
46+
Size types.String `tfsdk:"size"`
47+
HaType types.String `tfsdk:"ha_type"`
48+
State types.String `tfsdk:"state"`
49+
CreatedAt types.String `tfsdk:"created_at"`
50+
IsPrimary types.Bool `tfsdk:"is_primary"`
51+
Hostname types.String `tfsdk:"hostname"`
52+
Port types.Int64 `tfsdk:"port"`
53+
Username types.String `tfsdk:"username"`
54+
ConnectionString types.String `tfsdk:"connection_string"`
55+
Tags types.Map `tfsdk:"tags"`
56+
PgConfig types.Map `tfsdk:"pg_config"`
57+
PgBouncerConfig types.Map `tfsdk:"pgbouncer_config"`
58+
}
59+
60+
func (d *postgresServiceDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
61+
if req.ProviderData == nil {
62+
return
63+
}
64+
client, ok := req.ProviderData.(api.Client)
65+
if !ok {
66+
resp.Diagnostics.AddError(
67+
"Unexpected Data Source Configure Type",
68+
"Expected api.Client, got something else. Please report this issue to the provider developers.",
69+
)
70+
return
71+
}
72+
d.client = client
73+
}
74+
75+
func (d *postgresServiceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
76+
resp.TypeName = req.ProviderTypeName + "_postgres_service"
77+
}
78+
79+
func (d *postgresServiceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
80+
resp.Schema = schema.Schema{
81+
MarkdownDescription: postgresServiceDataSourceDescription,
82+
Attributes: map[string]schema.Attribute{
83+
"id": schema.StringAttribute{Description: "Unique identifier of the Postgres service to look up.", Required: true},
84+
"name": schema.StringAttribute{Description: "Human-readable name.", Computed: true},
85+
"cloud_provider": schema.StringAttribute{Description: "Cloud provider hosting the instance.", Computed: true},
86+
"region": schema.StringAttribute{Description: "Cloud region.", Computed: true},
87+
"postgres_version": schema.StringAttribute{Description: "Major Postgres version.", Computed: true},
88+
"size": schema.StringAttribute{Description: "Instance size (VM SKU).", Computed: true},
89+
"ha_type": schema.StringAttribute{Description: "High-availability mode ('none', 'async', 'sync').", Computed: true},
90+
"state": schema.StringAttribute{Description: "Server-reported state.", Computed: true},
91+
"created_at": schema.StringAttribute{Description: "RFC3339 creation timestamp.", Computed: true},
92+
"is_primary": schema.BoolAttribute{Description: "True for a primary; false for a read replica.", Computed: true},
93+
"hostname": schema.StringAttribute{Description: "Network hostname for client connections.", Computed: true},
94+
"port": schema.Int64Attribute{Description: "TCP port for client connections.", Computed: true},
95+
"username": schema.StringAttribute{Description: "Default superuser name.", Computed: true},
96+
"connection_string": schema.StringAttribute{Description: "Full connection URI (embeds the password). Sensitive.", Computed: true, Sensitive: true},
97+
"tags": schema.MapAttribute{
98+
Description: "User tags (server-reserved 'chc_' tags are filtered out). Read-only; represented as a map.",
99+
Computed: true,
100+
ElementType: types.StringType,
101+
},
102+
"pg_config": schema.MapAttribute{
103+
Description: "Postgres server parameters currently set on the instance. Read-only; represented as a map.",
104+
Computed: true,
105+
ElementType: types.StringType,
106+
},
107+
"pgbouncer_config": schema.MapAttribute{
108+
Description: "PgBouncer parameters currently set on the instance. Read-only; represented as a map.",
109+
Computed: true,
110+
ElementType: types.StringType,
111+
},
112+
},
113+
}
114+
}
115+
116+
func (d *postgresServiceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
117+
var data postgresServiceDataSourceModel
118+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
119+
if resp.Diagnostics.HasError() {
120+
return
121+
}
122+
123+
pg, err := d.client.GetPostgres(ctx, data.ID.ValueString())
124+
if err != nil {
125+
resp.Diagnostics.AddError("Error reading Postgres service", "Could not read Postgres service "+data.ID.ValueString()+": "+err.Error())
126+
return
127+
}
128+
cfg, err := d.client.GetPostgresConfig(ctx, data.ID.ValueString())
129+
if err != nil {
130+
resp.Diagnostics.AddError("Error reading Postgres configuration", "Could not read config for Postgres service "+data.ID.ValueString()+": "+err.Error())
131+
return
132+
}
133+
134+
data.ID = types.StringValue(pg.Id)
135+
data.Name = types.StringValue(pg.Name)
136+
data.CloudProvider = types.StringValue(pg.Provider)
137+
data.Region = types.StringValue(pg.Region)
138+
data.PostgresVersion = types.StringValue(pg.PostgresVersion)
139+
data.Size = types.StringValue(pg.Size)
140+
if pg.HaType != "" {
141+
data.HaType = types.StringValue(pg.HaType)
142+
} else {
143+
data.HaType = types.StringValue("none")
144+
}
145+
data.State = types.StringValue(pg.State)
146+
data.CreatedAt = types.StringValue(pg.CreatedAt)
147+
if pg.IsPrimary != nil {
148+
data.IsPrimary = types.BoolValue(*pg.IsPrimary)
149+
} else {
150+
data.IsPrimary = types.BoolValue(true)
151+
}
152+
data.Hostname = stringOrNull(pg.Hostname)
153+
data.Port = types.Int64Value(postgresDefaultPort)
154+
data.Username = stringOrNull(pg.Username)
155+
data.ConnectionString = stringOrNull(pg.ConnectionString)
156+
157+
tags, diags := apiTagsToMap(pg.Tags)
158+
resp.Diagnostics.Append(diags...)
159+
pgCfg, diags := configToMap(cfg.PgConfig)
160+
resp.Diagnostics.Append(diags...)
161+
pbCfg, diags := configToMap(cfg.PgBouncerConfig)
162+
resp.Diagnostics.Append(diags...)
163+
if resp.Diagnostics.HasError() {
164+
return
165+
}
166+
data.Tags = tags
167+
data.PgConfig = pgCfg
168+
data.PgBouncerConfig = pbCfg
169+
170+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
171+
}
172+
173+
// --- shared helpers (datasource package, alpha) ---------------------------
174+
175+
func stringOrNull(s *string) types.String {
176+
if s == nil {
177+
return types.StringNull()
178+
}
179+
return types.StringValue(*s)
180+
}
181+
182+
// apiTagsToMap converts api tags to a string map, dropping chc_-prefixed
183+
// (server-reserved) tags. An empty value becomes an empty string.
184+
func apiTagsToMap(tags []api.Tag) (types.Map, diag.Diagnostics) {
185+
m := make(map[string]attr.Value, len(tags))
186+
for _, t := range tags {
187+
if strings.HasPrefix(t.Key, postgresReservedTagPrefix) {
188+
continue
189+
}
190+
m[t.Key] = types.StringValue(t.Value)
191+
}
192+
return types.MapValue(types.StringType, m)
193+
}
194+
195+
func configToMap(c api.PgConfigMap) (types.Map, diag.Diagnostics) {
196+
m := make(map[string]attr.Value, len(c))
197+
for k, v := range c {
198+
m[k] = types.StringValue(v)
199+
}
200+
return types.MapValue(types.StringType, m)
201+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//go:build alpha
2+
3+
package datasource
4+
5+
import (
6+
"context"
7+
_ "embed"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/datasource"
10+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
11+
"github.com/hashicorp/terraform-plugin-framework/types"
12+
13+
"github.com/ClickHouse/terraform-provider-clickhouse/pkg/internal/api"
14+
)
15+
16+
//go:embed descriptions/postgres_service_ca_certificates.md
17+
var postgresCaCertsDataSourceDescription string
18+
19+
var _ datasource.DataSource = &postgresCaCertificatesDataSource{}
20+
21+
// NewPostgresServiceCaCertificatesDataSource fetches the PEM-encoded CA chain
22+
// for a Managed Postgres service (for clients that pin the CA).
23+
func NewPostgresServiceCaCertificatesDataSource() datasource.DataSource {
24+
return &postgresCaCertificatesDataSource{}
25+
}
26+
27+
type postgresCaCertificatesDataSource struct {
28+
client api.Client
29+
}
30+
31+
type postgresCaCertificatesDataSourceModel struct {
32+
ServiceID types.String `tfsdk:"service_id"`
33+
Certificate types.String `tfsdk:"certificate"`
34+
}
35+
36+
func (d *postgresCaCertificatesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
37+
if req.ProviderData == nil {
38+
return
39+
}
40+
client, ok := req.ProviderData.(api.Client)
41+
if !ok {
42+
resp.Diagnostics.AddError(
43+
"Unexpected Data Source Configure Type",
44+
"Expected api.Client, got something else. Please report this issue to the provider developers.",
45+
)
46+
return
47+
}
48+
d.client = client
49+
}
50+
51+
func (d *postgresCaCertificatesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
52+
resp.TypeName = req.ProviderTypeName + "_postgres_service_ca_certificates"
53+
}
54+
55+
func (d *postgresCaCertificatesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
56+
resp.Schema = schema.Schema{
57+
MarkdownDescription: postgresCaCertsDataSourceDescription,
58+
Attributes: map[string]schema.Attribute{
59+
"service_id": schema.StringAttribute{
60+
Description: "ID of the Postgres service whose CA certificate chain to fetch.",
61+
Required: true,
62+
},
63+
"certificate": schema.StringAttribute{
64+
Description: "PEM-encoded CA certificate chain.",
65+
Computed: true,
66+
},
67+
},
68+
}
69+
}
70+
71+
func (d *postgresCaCertificatesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
72+
var data postgresCaCertificatesDataSourceModel
73+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
74+
if resp.Diagnostics.HasError() {
75+
return
76+
}
77+
78+
pem, err := d.client.GetPostgresCaCertificates(ctx, data.ServiceID.ValueString())
79+
if err != nil {
80+
resp.Diagnostics.AddError(
81+
"Error reading Postgres CA certificates",
82+
"Could not fetch CA certificates for Postgres service "+data.ServiceID.ValueString()+": "+err.Error(),
83+
)
84+
return
85+
}
86+
87+
data.Certificate = types.StringValue(string(pem))
88+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
89+
}

0 commit comments

Comments
 (0)