diff --git a/docs/data-sources/clickhouse.md b/docs/data-sources/clickhouse.md index 72a5462..f8e7d39 100644 --- a/docs/data-sources/clickhouse.md +++ b/docs/data-sources/clickhouse.md @@ -28,6 +28,7 @@ Clickhouse data source - `cloud_type` (String) Cloud provider (`aws`, `gcp`, or `azure`) - `connection_info` (Attributes) Public connection info (see [below for nested schema](#nestedatt--connection_info)) +- `custom_certificate` (Attributes) Custom TLS certificate (see [below for nested schema](#nestedatt--custom_certificate)) - `description` (String) Cluster description - `private_connection_info` (Attributes) Private connection info (see [below for nested schema](#nestedatt--private_connection_info)) - `region_id` (String) Region where the cluster is located @@ -49,6 +50,16 @@ Read-Only: - `user` (String) ClickHouse user + +### Nested Schema for `custom_certificate` + +Read-Only: + +- `certificate` (String) Public certificate +- `key` (String) Private certificate key +- `root_ca` (String) Root certificate + + ### Nested Schema for `private_connection_info` diff --git a/docs/resources/clickhouse_cluster.md b/docs/resources/clickhouse_cluster.md index 54a147b..d30bb2f 100644 --- a/docs/resources/clickhouse_cluster.md +++ b/docs/resources/clickhouse_cluster.md @@ -60,6 +60,7 @@ resource "doublecloud_clickhouse_cluster" "example-clickhouse" { - `access` (Block, Optional) Access control configuration (see [below for nested schema](#nestedblock--access)) - `config` (Block, Optional) (see [below for nested schema](#nestedblock--config)) +- `custom_certificate` (Block, Optional) Custom TLS certificate (see [below for nested schema](#nestedblock--custom_certificate)) - `description` (String) Cluster description - `id` (String) Cluster ID - `resources` (Block, Optional) Cluster resources (see [below for nested schema](#nestedblock--resources)) @@ -179,6 +180,16 @@ Optional: + +### Nested Schema for `custom_certificate` + +Optional: + +- `certificate` (String) Public certificate +- `key` (String) Private certificate key +- `root_ca` (String) Root certificate + + ### Nested Schema for `resources` diff --git a/go.mod b/go.mod index 8980a16..9100567 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/cloudflare/circl v1.3.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.16.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/cli v1.1.6 // indirect diff --git a/go.sum b/go.sum index ec6cd62..c2e808b 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZt github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -146,6 +148,8 @@ github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgf github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -214,6 +218,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= @@ -224,17 +230,23 @@ github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgr go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw= go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= @@ -242,12 +254,16 @@ golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -273,11 +289,15 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= diff --git a/internal/provider/clickhouse_cluster_resource.go b/internal/provider/clickhouse_cluster_resource.go index 2be3f29..a580ca2 100644 --- a/internal/provider/clickhouse_cluster_resource.go +++ b/internal/provider/clickhouse_cluster_resource.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/golang/protobuf/ptypes/wrappers" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" @@ -49,6 +50,8 @@ type clickhouseClusterModel struct { // TODO: support mw // https://github.com/doublecloud/api/blob/main/doublecloud/v1/maintenance.proto // MaintenanceWindow *maintenanceWindow `tfsdk:"maintenance_window"` + + CustomCertificate types.Object `tfsdk:"custom_certificate"` } type clickhouseClusterResources struct { @@ -350,6 +353,26 @@ func clickhouseConenctionInfoSchema() map[string]schema.Attribute { } } +func clickhouseCustomCertificateSchema() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "certificate": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Public certificate", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "key": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Private certificate key", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "root_ca": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Root certificate", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + } +} + func (r *ClickhouseClusterResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ // This description is used by the documentation generator and the language server. @@ -493,6 +516,12 @@ func (r *ClickhouseClusterResource) Schema(ctx context.Context, req resource.Sch "access": AccessSchemaBlock(), "config": clickhouseConfigSchemaBlock(), // maintenance window + "custom_certificate": schema.SingleNestedBlock{ + Attributes: clickhouseCustomCertificateSchema(), + PlanModifiers: []planmodifier.Object{objectplanmodifier.UseStateForUnknown()}, + MarkdownDescription: "Custom TLS certificate", + Validators: []validator.Object{&clickhouseCustomCertificateValidator{}}, + }, }, } } @@ -643,6 +672,21 @@ func updateClickhouseCluster(m *clickhouseClusterModel) (*clickhouse.UpdateClust rq.Access = access } + cc := m.CustomCertificate.Attributes() + rq.CustomCertificate = &clickhouse.CustomCertificate{ + Enabled: false, + } + certificate, certOk := cc["certificate"] + key, keyOk := cc["key"] + rq.CustomCertificate.Enabled = certOk && keyOk + if rq.CustomCertificate.Enabled { + rq.CustomCertificate.Certificate = &wrappers.BytesValue{Value: []byte(certificate.(types.String).ValueString())} + rq.CustomCertificate.Key = &wrappers.BytesValue{Value: []byte(key.(types.String).ValueString())} + if rootCa, ok := cc["root_ca"]; ok { + rq.CustomCertificate.RootCa = &wrappers.BytesValue{Value: []byte(rootCa.(types.String).ValueString())} + } + } + return rq, diags } @@ -734,6 +778,12 @@ func (m *clickhouseClusterModel) parse(rs *clickhouse.Cluster) diag.Diagnostics diags.Append(m.Access.parse(access)...) } + oldKey := "" + if key, ok := m.CustomCertificate.Attributes()["key"]; ok { + oldKey = key.String() + } + m.CustomCertificate = parseClickhouseCustomCertificate(rs.GetCustomCertificate(), oldKey).convert(diags) + // parse MW return diags } diff --git a/internal/provider/clickhouse_cluster_resource_test.go b/internal/provider/clickhouse_cluster_resource_test.go index 5ef6237..1ea993a 100644 --- a/internal/provider/clickhouse_cluster_resource_test.go +++ b/internal/provider/clickhouse_cluster_resource_test.go @@ -10,6 +10,7 @@ import ( "text/template" "github.com/doublecloud/go-genproto/doublecloud/clickhouse/v1" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) @@ -17,6 +18,40 @@ import ( var ( testAccClickhouseName string = fmt.Sprintf("%v-clickhouse", testPrefix) testAccClickhouseId string = fmt.Sprintf("doublecloud_clickhouse_cluster.%v", testAccClickhouseName) + + testAccClickhouseTLSCert string = ` +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcKT/wmDt+qLwEVOfU0UbJO5f77+0 +nuYermx15MOZh4jg4H/r98b/tD2dNxdLAW/VJ4VTF3vD0AGY2+xN7J8aTA== +-----END PUBLIC KEY----- +` + + testAccClickhouseTLSKey string = ` +-----BEGIN CERTIFICATE----- +MIICoTCCAkegAwIBAgIUWdVSBHIWp+w6Gtmt4Ps+RNgky00wCgYIKoZIzj0EAwIw +gacxCzAJBgNVBAYTAkRFMRIwEAYDVQQIDAlGcmFua2Z1cnQxEjAQBgNVBAcMCUZy +YW5rZnVydDEVMBMGA1UECgwMZG91YmxlLmNsb3VkMSAwHgYDVQQLDBdUZXJyYWZv +cm0gcHJvdmlkZXIgdGVzdDEVMBMGA1UEAwwMZG91YmxlLmNsb3VkMSAwHgYJKoZI +hvcNAQkBFhFpbmZvQGRvdWJsZS5jbG91ZDAeFw0yNDA5MTkxNjE5MDNaFw0yNTA5 +MTkxNjE5MDNaMIG0MQswCQYDVQQGEwJERTESMBAGA1UECAwJRnJhbmtmdXJ0MRIw +EAYDVQQHDAlGcmFua2Z1cnQxFTATBgNVBAoMDGRvdWJsZS5jbG91ZDElMCMGA1UE +CwwcVGVycmFmb3JtIHByb3ZpZGVyIHRlc3QgaW1wbDEdMBsGA1UEAwwUdGVzdC5h +dC5kb3VibGUuY2xvdWQxIDAeBgkqhkiG9w0BCQEWEWluZm9AZG91YmxlLmNsb3Vk +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcKT/wmDt+qLwEVOfU0UbJO5f77+0 +nuYermx15MOZh4jg4H/r98b/tD2dNxdLAW/VJ4VTF3vD0AGY2+xN7J8aTKNCMEAw +HQYDVR0OBBYEFElk8x4Sw1IYKahZDqAKrbPrMQvaMB8GA1UdIwQYMBaAFC/+xZgT +4U3lxhcG2wdT5/NlGB7cMAoGCCqGSM49BAMCA0gAMEUCIBWS0StXMJCfOHU6UqKK +PB+UYxG5mwIw4IP/T7sLa3XlAiEAyS8vLtbgrh8mLXwacAe/SFRS3L/DhOJQa+0e +VQBbsVs= +-----END CERTIFICATE----- +` + + testAccClickhouseTLSRootCA string = ` +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2fZnlTyuGtgATXh0FmgvgsqTI/aB +Wy2sRShP40UqdTQ4pxLkpkskb7RWssyrXZEiieGSIUY33setFOOMV6b4RA== +-----END PUBLIC KEY----- +` ) func TestAccClickhouseClusterResource(t *testing.T) { @@ -27,6 +62,7 @@ func TestAccClickhouseClusterResource(t *testing.T) { RegionId: types.StringValue("eu-central-1"), CloudType: types.StringValue("aws"), NetworkId: types.StringValue(testNetworkId), + Version: types.StringValue("24.8"), Resources: &clickhouseClusterResources{ Clickhouse: &clickhouseClusterResourcesClickhouse{ ResourcePresetId: types.StringValue("g2-c2-m8"), @@ -84,6 +120,20 @@ func TestAccClickhouseClusterResource(t *testing.T) { }, } + m4 := m3 + cc, _ := types.ObjectValue(map[string]attr.Type{ + "certificate": types.StringType, + "key": types.StringType, + "root_ca": types.StringType, + }, + map[string]attr.Value{ + "certificate": types.StringValue(testAccClickhouseTLSCert), + "key": types.StringValue(testAccClickhouseTLSKey), + "root_ca": types.StringValue(testAccClickhouseTLSRootCA), + }, + ) + m4.CustomCertificate = cc + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, @@ -140,6 +190,16 @@ func TestAccClickhouseClusterResource(t *testing.T) { resource.TestCheckResourceAttr(testAccClickhouseId, "resources.clickhouse.max_disk_size", "68719476736"), ), }, + // Check custom TLS certificate + { + Config: convertClickHouseModelToHCL(&m4), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckNoResourceAttr(testAccClickhouseId, "resources.clickhouse.resource_preset_id"), + resource.TestCheckResourceAttr(testAccClickhouseId, "resources.clickhouse.custom_certificate.certificate", testAccClickhouseTLSCert), + resource.TestCheckResourceAttr(testAccClickhouseId, "resources.clickhouse.custom_certificate.key", testAccClickhouseTLSKey), + resource.TestCheckResourceAttr(testAccClickhouseId, "resources.clickhouse.custom_certificate.root_ca", testAccClickhouseTLSRootCA), + ), + }, // Delete testing automatically occurs in TestCase }, }) @@ -213,6 +273,8 @@ resource "doublecloud_clickhouse_cluster" "tf-acc-clickhouse" { region_id = "{{ .RegionId.ValueString }}" cloud_type = "{{ .CloudType.ValueString }}" network_id = "{{ .NetworkId.ValueString }}" + {{- if not .Version.IsNull }} + version = "{{ .Version.ValueString }}"{{end}} resources { clickhouse { @@ -277,6 +339,13 @@ resource "doublecloud_clickhouse_cluster" "tf-acc-clickhouse" { ] {{- end}} } + {{- if not .CustomCertificate.IsNull }} + custom_certificate { + certificate = "{{ .CustomCertificate.Attributes.certificate }}" + key = "{{ .CustomCertificate.Attributes.key }}" + root_ca = "{{ .CustomCertificate.Attributes.root_ca }}" + } + {{- end}} }` var clickhouseHCLTemplate *template.Template diff --git a/internal/provider/clickhouse_data_source.go b/internal/provider/clickhouse_data_source.go index 756d670..b0645ea 100644 --- a/internal/provider/clickhouse_data_source.go +++ b/internal/provider/clickhouse_data_source.go @@ -3,8 +3,11 @@ package provider import ( "context" "fmt" + "strings" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/doublecloud/go-genproto/doublecloud/clickhouse/v1" dcsdk "github.com/doublecloud/go-sdk" @@ -28,15 +31,16 @@ type ClickhouseDataSource struct { } type ClickhouseDataSourceModel struct { - Id types.String `tfsdk:"id"` - ProjectID types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - RegionID types.String `tfsdk:"region_id"` - CloudType types.String `tfsdk:"cloud_type"` - Version types.String `tfsdk:"version"` - ConnectionInfo *ClickhouseConnectionInfo `tfsdk:"connection_info"` - PrivateConnectionInfo *ClickhouseConnectionInfo `tfsdk:"private_connection_info"` + Id types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + RegionID types.String `tfsdk:"region_id"` + CloudType types.String `tfsdk:"cloud_type"` + Version types.String `tfsdk:"version"` + ConnectionInfo *ClickhouseConnectionInfo `tfsdk:"connection_info"` + PrivateConnectionInfo *ClickhouseConnectionInfo `tfsdk:"private_connection_info"` + CustomCertificate *ClickhouseCustomCertificate `tfsdk:"custom_certificate"` } type ClickhouseConnectionInfo struct { @@ -79,6 +83,32 @@ func (ci ClickhouseConnectionInfo) convert(diags diag.Diagnostics) types.Object return res } +type ClickhouseCustomCertificate struct { + Certificate types.String `tfsdk:"certificate"` + Key types.String `tfsdk:"key"` + RootCA types.String `tfsdk:"root_ca"` +} + +func (cc *ClickhouseCustomCertificate) convert(diags diag.Diagnostics) types.Object { + attrTypeMap := map[string]attr.Type{ + "certificate": types.StringType, + "key": types.StringType, + "root_ca": types.StringType, + } + if cc == nil { + return types.ObjectNull(attrTypeMap) + } + res, d := types.ObjectValue(attrTypeMap, + map[string]attr.Value{ + "certificate": cc.Certificate, + "key": cc.Key, + "root_ca": cc.RootCA, + }, + ) + diags.Append(d...) + return res +} + func (d *ClickhouseDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_clickhouse" } @@ -86,6 +116,8 @@ func (d *ClickhouseDataSource) Metadata(ctx context.Context, req datasource.Meta func (d *ClickhouseDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { connInfo := make(map[string]schema.Attribute) resp.Diagnostics.Append(convertSchemaAttributes(clickhouseConenctionInfoSchema(), connInfo)...) + customCertificate := make(map[string]schema.Attribute) + resp.Diagnostics.Append(convertSchemaAttributes(clickhouseCustomCertificateSchema(), customCertificate)...) resp.Schema = schema.Schema{ MarkdownDescription: "Clickhouse data source", Attributes: map[string]schema.Attribute{ @@ -129,6 +161,11 @@ func (d *ClickhouseDataSource) Schema(ctx context.Context, req datasource.Schema Attributes: connInfo, MarkdownDescription: "Private connection info", }, + "custom_certificate": schema.SingleNestedAttribute{ + Computed: true, + Attributes: customCertificate, + MarkdownDescription: "Custom TLS certificate", + }, }, } } @@ -187,6 +224,35 @@ func parseClickhousePrivateConnectionInfo(r *clickhouse.PrivateConnectionInfo) * return c } +func parseClickhouseCustomCertificate(r *clickhouse.CustomCertificate, oldKey string) *ClickhouseCustomCertificate { + if r == nil { + return nil + } + + if !r.GetEnabled() { + return nil + } + + certRaw := string(r.Certificate.GetValue()[:]) + if len(certRaw) == 0 { + return nil + } + + certificate := types.StringValue(certRaw) + key := basetypes.NewStringValue(strings.Replace(strings.Replace(oldKey, "\\n", "\n", -1), "\"", "", -1)) + rootCa := types.StringNull() + rootRaw := string(r.RootCa.GetValue()[:]) + if len(rootRaw) > 0 { + rootCa = types.StringValue(rootRaw) + } + c := &ClickhouseCustomCertificate{ + Certificate: certificate, + Key: key, + RootCA: rootCa, + } + return c +} + func (d *ClickhouseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var data ClickhouseDataSourceModel @@ -235,6 +301,11 @@ func (d *ClickhouseDataSource) Read(ctx context.Context, req datasource.ReadRequ data.Version = types.StringValue(response.Version) data.ConnectionInfo = parseClickhouseConnectionInfo(response.ConnectionInfo) data.PrivateConnectionInfo = parseClickhousePrivateConnectionInfo(response.PrivateConnectionInfo) + oldKey := "" + if data.CustomCertificate != nil { + oldKey = data.CustomCertificate.Key.String() + } + data.CustomCertificate = parseClickhouseCustomCertificate(response.CustomCertificate, oldKey) // Save data into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go index 522e9c2..f4bd77d 100644 --- a/internal/provider/helpers.go +++ b/internal/provider/helpers.go @@ -30,6 +30,8 @@ func convertSchemaAttributes(resAttrs map[string]resourceschema.Attribute, dataA dataAttrs[name] = convertInt64Attribute(attr) case resourceschema.SingleNestedAttribute: dataAttrs[name] = convertSingleNestedAttribute(attr, diags) + case resourceschema.BoolAttribute: + dataAttrs[name] = convertBoolAttribute(attr) default: diags.AddError("can not convert resource attribute to datasource attribute", fmt.Sprintf("unsupported type for attribute %q: %v", name, attr)) } @@ -80,6 +82,16 @@ func convertSingleNestedAttribute(attr resourceschema.SingleNestedAttribute, dia } } +func convertBoolAttribute(attr resourceschema.BoolAttribute) *dataschema.BoolAttribute { + return &dataschema.BoolAttribute{ + Computed: true, + Sensitive: attr.Sensitive, + Description: attr.Description, + MarkdownDescription: attr.MarkdownDescription, + DeprecationMessage: attr.DeprecationMessage, + } +} + type suppressAutoscaledDiskDiff struct{} var _ planmodifier.Int64 = &suppressAutoscaledDiskDiff{} @@ -126,6 +138,42 @@ func (*suppressAutoscaledDiskDiff) PlanModifyInt64(ctx context.Context, req plan } } +type clickhouseCustomCertificateValidator struct{} + +var _ validator.Object = &clickhouseCustomCertificateValidator{} + +func (*clickhouseCustomCertificateValidator) Description(context.Context) string { + return "validate custom TLS certificate" +} + +func (s *clickhouseCustomCertificateValidator) MarkdownDescription(ctx context.Context) string { + return s.Description(ctx) +} + +func (*clickhouseCustomCertificateValidator) ValidateObject(ctx context.Context, req validator.ObjectRequest, rsp *validator.ObjectResponse) { + if req.ConfigValue.IsNull() { + return + } + + certificatePresent := !req.ConfigValue.Attributes()["certificate"].IsNull() + keyPresent := !req.ConfigValue.Attributes()["key"].IsNull() + rootPresent := !req.ConfigValue.Attributes()["root_ca"].IsNull() + + if (certificatePresent && !keyPresent) || (!certificatePresent && keyPresent) { + rsp.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.Path, + `Must be both attributea "certificate" and "key"`, + )) + } + + if !certificatePresent && rootPresent { + rsp.Diagnostics.Append(validatordiag.InvalidAttributeCombinationDiagnostic( + req.Path, + `Attribute "root_ca" can be only with "certificate" and "key"`, + )) + } +} + type clusterResourcesValidator struct{} func (*clusterResourcesValidator) Description(context.Context) string {