From 033b7f7843cd7a3bd1fe152f46831c9902f6d820 Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Thu, 8 Oct 2026 14:56:51 +0300 Subject: [PATCH 01/12] feat(httpcache)!: remove the cozystack_httpcache resource and data source Cozystack dropped the HTTPCache application together with its ResourceDefinition, and the api/apps/v1alpha1 module no longer ships the httpcache values type. A cluster on that API does not serve the kind, so the resource could only fail there, and its schema-guard test would keep the module pin from moving past the removal. Signed-off-by: Alexey Artamonov --- CHANGELOG.md | 1 + README.md | 1 - docs/data-sources/httpcache.md | 39 -------- docs/resources/httpcache.md | 58 ------------ .../cozystack_httpcache/data-source.tf | 4 - .../resources/cozystack_httpcache/import.sh | 2 - .../resources/cozystack_httpcache/resource.tf | 7 -- internal/client/httpcache.go | 6 -- internal/provider/acc_test.go | 32 ------- internal/provider/httpcache_model.go | 85 ----------------- internal/provider/httpcache_model_test.go | 92 ------------------- internal/provider/httpcache_schema.go | 54 ----------- internal/provider/provider.go | 2 - 13 files changed, 1 insertion(+), 382 deletions(-) delete mode 100644 docs/data-sources/httpcache.md delete mode 100644 docs/resources/httpcache.md delete mode 100644 examples/data-sources/cozystack_httpcache/data-source.tf delete mode 100644 examples/resources/cozystack_httpcache/import.sh delete mode 100644 examples/resources/cozystack_httpcache/resource.tf delete mode 100644 internal/client/httpcache.go delete mode 100644 internal/provider/httpcache_model.go delete mode 100644 internal/provider/httpcache_model_test.go delete mode 100644 internal/provider/httpcache_schema.go diff --git a/CHANGELOG.md b/CHANGELOG.md index afd7b67..5e383f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ To ship with the first Cozystack API pin that contains cozystack/cozystack#3936. ### Breaking changes +- `cozystack_httpcache` (resource and data source) is removed, following Cozystack, which dropped the HTTPCache application and its ResourceDefinition: a cluster on this API no longer serves the kind. Remove the resource from your configuration and run `terraform state rm` for each instance before upgrading the provider. - `rabbitmq`: the default `resources_preset` moves from `t1.nano` to `s1.nano`. The attribute is optional-with-default, so the old value sits in existing state: after upgrading the provider, a `cozystack_rabbitmq` that never set it explicitly plans `t1.nano` → `s1.nano`, which updates the cluster and rolls its pods. Set `resources_preset = "t1.nano"` explicitly to keep the previous sizing. ## v1.6.3 diff --git a/README.md b/README.md index 05e7a19..e2ed879 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ Every kind is served by the same aggregated API, so the provider is built to gro | Managed NATS | [`cozystack_nats`](docs/resources/nats.md) | [`cozystack_nats`](docs/data-sources/nats.md) | | Managed OpenSearch | [`cozystack_opensearch`](docs/resources/opensearch.md) | [`cozystack_opensearch`](docs/data-sources/opensearch.md) | | Managed PostgreSQL | [`cozystack_postgres`](docs/resources/postgres.md) | [`cozystack_postgres`](docs/data-sources/postgres.md) | -| HTTP cache | [`cozystack_httpcache`](docs/resources/httpcache.md) | [`cozystack_httpcache`](docs/data-sources/httpcache.md) | | TCP load balancer | [`cozystack_tcpbalancer`](docs/resources/tcpbalancer.md) | [`cozystack_tcpbalancer`](docs/data-sources/tcpbalancer.md) | | Harbor registry | [`cozystack_harbor`](docs/resources/harbor.md) | [`cozystack_harbor`](docs/data-sources/harbor.md) | | Virtual private cloud | [`cozystack_vpc`](docs/resources/vpc.md) | [`cozystack_vpc`](docs/data-sources/vpc.md) | diff --git a/docs/data-sources/httpcache.md b/docs/data-sources/httpcache.md deleted file mode 100644 index 5b84e37..0000000 --- a/docs/data-sources/httpcache.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "cozystack_httpcache Data Source - cozystack" -subcategory: "" -description: |- - Read an existing Cozystack HTTP cache by name and namespace. ---- - -# cozystack_httpcache (Data Source) - -Read an existing Cozystack HTTP cache by name and namespace. - -## Example Usage - -```terraform -data "cozystack_httpcache" "cdn" { - name = "cdn" - namespace = "tenant-root" -} -``` - - -## Schema - -### Required - -- `name` (String) HTTPCache instance name. -- `namespace` (String) Tenant namespace. - -### Read-Only - -- `chart_version` (String) Deployed chart version. -- `endpoints` (List of String) Upstream endpoints. -- `external` (Boolean) Whether external access is enabled. -- `id` (String) Synthetic identifier `namespace/name`. -- `ready` (Boolean) Whether the application's `Ready` condition is true. -- `size` (String) Persistent volume size. -- `storage_class` (String) StorageClass used to store the data. -- `uid` (String) Server-assigned object UID (`metadata.uid`). diff --git a/docs/resources/httpcache.md b/docs/resources/httpcache.md deleted file mode 100644 index 986e6b7..0000000 --- a/docs/resources/httpcache.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "cozystack_httpcache Resource - cozystack" -subcategory: "" -description: |- - A Cozystack managed HTTP cache, deployed inside a tenant namespace. The haproxy and nginx tuning blocks use server defaults. ---- - -# cozystack_httpcache (Resource) - -A Cozystack managed HTTP cache, deployed inside a tenant namespace. The haproxy and nginx tuning blocks use server defaults. - -## Example Usage - -```terraform -resource "cozystack_httpcache" "cdn" { - name = "cdn" - namespace = "tenant-root" - - size = "10Gi" - endpoints = ["192.0.2.10:80", "192.0.2.11:80"] -} -``` - - -## Schema - -### Required - -- `name` (String) HTTPCache instance name (`metadata.name`). Immutable. -- `namespace` (String) Tenant namespace the application is created in. Immutable. - -### Optional - -- `endpoints` (List of String) Upstream endpoints as a list of `ip:port`. -- `external` (Boolean) Enable external access from outside the cluster. -- `size` (String) Persistent volume size for cache data (quantity, e.g. `10Gi`). -- `storage_class` (String) StorageClass used to store the data. Changing a value set here replaces the object, because an existing volume is never migrated to another class. Removing the attribute from the configuration does not: the object keeps its volumes and the recorded class reverts to the default. -- `wait_for_ready` (Boolean) Block on create/update until the `Ready` condition is true and any server-generated outputs the resource exposes are readable. -- `wait_timeout` (String) Maximum time to wait when `wait_for_ready` is set (Go duration, e.g. `10m`). Readiness and outputs share this budget; outputs that never appear leave a warning and their attributes stay null until the next refresh. - -### Read-Only - -- `chart_version` (String) Deployed chart version (`status.version`). -- `id` (String) Synthetic identifier in the form `namespace/name`. -- `ready` (Boolean) Whether the application's `Ready` condition is true. -- `uid` (String) Server-assigned object UID (`metadata.uid`). Stable across updates; changes on recreate. - -## Import - -Import is supported using the following syntax: - -The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: - -```shell -# HTTPCache instances are imported as "/". -terraform import cozystack_httpcache.cdn tenant-root/cdn -``` diff --git a/examples/data-sources/cozystack_httpcache/data-source.tf b/examples/data-sources/cozystack_httpcache/data-source.tf deleted file mode 100644 index 4567a23..0000000 --- a/examples/data-sources/cozystack_httpcache/data-source.tf +++ /dev/null @@ -1,4 +0,0 @@ -data "cozystack_httpcache" "cdn" { - name = "cdn" - namespace = "tenant-root" -} diff --git a/examples/resources/cozystack_httpcache/import.sh b/examples/resources/cozystack_httpcache/import.sh deleted file mode 100644 index 924af6d..0000000 --- a/examples/resources/cozystack_httpcache/import.sh +++ /dev/null @@ -1,2 +0,0 @@ -# HTTPCache instances are imported as "/". -terraform import cozystack_httpcache.cdn tenant-root/cdn diff --git a/examples/resources/cozystack_httpcache/resource.tf b/examples/resources/cozystack_httpcache/resource.tf deleted file mode 100644 index 890d203..0000000 --- a/examples/resources/cozystack_httpcache/resource.tf +++ /dev/null @@ -1,7 +0,0 @@ -resource "cozystack_httpcache" "cdn" { - name = "cdn" - namespace = "tenant-root" - - size = "10Gi" - endpoints = ["192.0.2.10:80", "192.0.2.11:80"] -} diff --git a/internal/client/httpcache.go b/internal/client/httpcache.go deleted file mode 100644 index 08cab2b..0000000 --- a/internal/client/httpcache.go +++ /dev/null @@ -1,6 +0,0 @@ -package client - -// HTTPCacheResource identifies the HTTPCache application kind. -func HTTPCacheResource() Resource { - return Resource{Resource: "httpcaches", Kind: "HTTPCache"} -} diff --git a/internal/provider/acc_test.go b/internal/provider/acc_test.go index 8d8eaa7..92ae012 100644 --- a/internal/provider/acc_test.go +++ b/internal/provider/acc_test.go @@ -720,38 +720,6 @@ resource "cozystack_postgres" "test" { }) } -func TestAccHTTPCacheResource(t *testing.T) { - config := ` -resource "cozystack_httpcache" "test" { - name = "tfacchc" - namespace = "tenant-root" - size = "1Gi" - endpoints = ["192.0.2.10:80"] -} -` - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - CheckDestroy: checkApplicationDestroy(client.HTTPCacheResource(), "cozystack_httpcache"), - Steps: []resource.TestStep{ - { - Config: config, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("cozystack_httpcache.test", "id", "tenant-root/tfacchc"), - resource.TestCheckResourceAttr("cozystack_httpcache.test", "endpoints.0", "192.0.2.10:80"), - ), - }, - { - ResourceName: "cozystack_httpcache.test", - ImportState: true, - ImportStateId: "tenant-root/tfacchc", - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"wait_for_ready", "wait_timeout", "ready", "chart_version"}, - }, - }, - }) -} - func TestAccTCPBalancerResource(t *testing.T) { config := ` resource "cozystack_tcpbalancer" "test" { diff --git a/internal/provider/httpcache_model.go b/internal/provider/httpcache_model.go deleted file mode 100644 index 0872786..0000000 --- a/internal/provider/httpcache_model.go +++ /dev/null @@ -1,85 +0,0 @@ -package provider - -import ( - "context" - - "github.com/cozystack/terraform-provider-cozystack/internal/client" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -// httpcacheModel maps the cozystack_httpcache schema to Go types. The haproxy and -// nginx tuning blocks are not managed (they use server defaults). -type httpcacheModel struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Namespace types.String `tfsdk:"namespace"` - Size types.String `tfsdk:"size"` - StorageClass types.String `tfsdk:"storage_class"` - External types.Bool `tfsdk:"external"` - Endpoints types.List `tfsdk:"endpoints"` - Ready types.Bool `tfsdk:"ready"` - ChartVersion types.String `tfsdk:"chart_version"` - UID types.String `tfsdk:"uid"` -} - -type httpcacheResourceModel struct { - httpcacheModel - - WaitForReady types.Bool `tfsdk:"wait_for_ready"` - WaitTimeout types.String `tfsdk:"wait_timeout"` -} - -func (m *httpcacheResourceModel) waitConfig() (types.Bool, types.String) { - return m.WaitForReady, m.WaitTimeout -} - -func (m *httpcacheModel) identity() (string, string) { - return m.Namespace.ValueString(), m.Name.ValueString() -} - -func (m *httpcacheModel) expand(ctx context.Context) (*client.Application, diag.Diagnostics) { - var diags diag.Diagnostics - - endpoints, eDiags := expandStringList(ctx, m.Endpoints) - diags.Append(eDiags...) - - if diags.HasError() { - return nil, diags - } - - spec := map[string]any{ - attrSize: m.Size.ValueString(), - specStorageClass: m.StorageClass.ValueString(), - attrExternal: m.External.ValueBool(), - "endpoints": endpoints, - } - - return &client.Application{ - Name: m.Name.ValueString(), - Namespace: m.Namespace.ValueString(), - Spec: spec, - }, diags -} - -func (m *httpcacheModel) flatten(app *client.Application) diag.Diagnostics { - var diags diag.Diagnostics - - m.ID = types.StringValue(app.Namespace + "/" + app.Name) - m.Name = types.StringValue(app.Name) - m.Namespace = types.StringValue(app.Namespace) - m.Size = types.StringValue(specString(app.Spec, attrSize)) - m.StorageClass = types.StringValue(specString(app.Spec, specStorageClass)) - m.External = types.BoolValue(specBool(app.Spec, attrExternal)) - - endpoints, eDiags := flattenStringList(app.Spec["endpoints"]) - diags.Append(eDiags...) - - m.Endpoints = endpoints - - m.Ready = types.BoolValue(app.Status.Ready) - m.ChartVersion = types.StringValue(app.Status.Version) - m.UID = types.StringValue(app.UID) - - return diags -} diff --git a/internal/provider/httpcache_model_test.go b/internal/provider/httpcache_model_test.go deleted file mode 100644 index 1fe7f90..0000000 --- a/internal/provider/httpcache_model_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package provider - -import ( - "context" - "testing" - - "github.com/cozystack/cozystack/api/apps/v1alpha1/httpcache" - "github.com/cozystack/terraform-provider-cozystack/internal/client" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -func fullHTTPCacheModel() httpcacheModel { - return httpcacheModel{ - Name: types.StringValue("cache"), - Namespace: types.StringValue("tenant-root"), - Size: types.StringValue("10Gi"), - StorageClass: types.StringValue(""), - External: types.BoolValue(false), - Endpoints: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.0.2.10:80"), - }), - } -} - -func TestHTTPCacheExpand_Endpoints(t *testing.T) { - t.Parallel() - - model := fullHTTPCacheModel() - - got, diags := model.expand(context.Background()) - if diags.HasError() { - t.Fatalf("expand diagnostics: %v", diags) - } - - endpoints, _ := got.Spec["endpoints"].([]any) - if len(endpoints) != 1 || endpoints[0] != "192.0.2.10:80" { - t.Errorf("endpoints = %v, want [192.0.2.10:80]", endpoints) - } -} - -func TestHTTPCacheFlatten_RoundTrip(t *testing.T) { - t.Parallel() - - app := &client.Application{ - Name: "cache", - Namespace: "tenant-root", - Spec: map[string]any{ - "size": "20Gi", - "storageClass": "fast", - "external": true, - "endpoints": []any{"192.0.2.11:8080"}, - }, - Status: client.ApplicationStatus{Version: "1.0.0", Ready: true}, - } - - var model httpcacheModel - - if diags := model.flatten(app); diags.HasError() { - t.Fatalf("flatten diagnostics: %v", diags) - } - - if model.Size.ValueString() != "20Gi" { - t.Errorf("size = %q, want 20Gi", model.Size.ValueString()) - } - - if !model.External.ValueBool() { - t.Errorf("external = false, want true") - } - - if model.Endpoints.Elements()[0].(types.String).ValueString() != "192.0.2.11:8080" { - t.Errorf("endpoints = %v, want [192.0.2.11:8080]", model.Endpoints.Elements()) - } -} - -func TestHTTPCacheExpandKeysMatchConfigSpec(t *testing.T) { - t.Parallel() - - model := fullHTTPCacheModel() - - got, diags := model.expand(context.Background()) - if diags.HasError() { - t.Fatalf("expand diagnostics: %v", diags) - } - - emitted := make(map[string]bool, len(got.Spec)) - for key := range got.Spec { - emitted[key] = true - } - - assertSpecCoverage(t, emitted, httpcache.ConfigSpec{}, "haproxy", "nginx") -} diff --git a/internal/provider/httpcache_schema.go b/internal/provider/httpcache_schema.go deleted file mode 100644 index 7df1938..0000000 --- a/internal/provider/httpcache_schema.go +++ /dev/null @@ -1,54 +0,0 @@ -package provider - -import ( - "maps" - - dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" - "github.com/hashicorp/terraform-plugin-framework/types" -) - -func httpcacheSchema() rschema.Schema { - attributes := identityResourceAttributes("HTTPCache instance name (`metadata.name`). Immutable.") - - maps.Copy(attributes, map[string]rschema.Attribute{ - attrSize: rschema.StringAttribute{ - Optional: true, Computed: true, - Default: stringdefault.StaticString("10Gi"), - MarkdownDescription: "Persistent volume size for cache data (quantity, e.g. `10Gi`).", - }, - attrStorageClass: storageClassAttribute(""), - attrExternal: externalAttribute(), - "endpoints": rschema.ListAttribute{ - Optional: true, - ElementType: types.StringType, - MarkdownDescription: "Upstream endpoints as a list of `ip:port`.", - }, - }) - maps.Copy(attributes, statusResourceAttributes()) - maps.Copy(attributes, waitBehaviorAttributes()) - - return rschema.Schema{ - MarkdownDescription: "A Cozystack managed HTTP cache, deployed inside a tenant namespace. " + - "The haproxy and nginx tuning blocks use server defaults.", - Attributes: attributes, - } -} - -func httpcacheDataSourceSchema() dsschema.Schema { - attributes := identityDataSourceAttributes("HTTPCache instance name.") - - maps.Copy(attributes, map[string]dsschema.Attribute{ - attrSize: dsschema.StringAttribute{Computed: true, MarkdownDescription: "Persistent volume size."}, - attrStorageClass: dsschema.StringAttribute{Computed: true, MarkdownDescription: "StorageClass used to store the data."}, - attrExternal: dsschema.BoolAttribute{Computed: true, MarkdownDescription: "Whether external access is enabled."}, - "endpoints": dsschema.ListAttribute{Computed: true, ElementType: types.StringType, MarkdownDescription: "Upstream endpoints."}, - }) - maps.Copy(attributes, statusDataSourceAttributes()) - - return dsschema.Schema{ - MarkdownDescription: "Read an existing Cozystack HTTP cache by name and namespace.", - Attributes: attributes, - } -} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 0bd133a..e10e770 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -237,7 +237,6 @@ func (p *CozystackProvider) Resources(_ context.Context) []func() resource.Resou newAppResource[natsResourceModel, *natsResourceModel](client.NATSResource(), "nats", natsSchema), newAppResource[opensearchResourceModel, *opensearchResourceModel](client.OpenSearchResource(), "opensearch", opensearchSchema), newAppResource[postgresqlResourceModel, *postgresqlResourceModel](client.PostgresResource(), "postgres", postgresSchema), - newAppResource[httpcacheResourceModel, *httpcacheResourceModel](client.HTTPCacheResource(), "httpcache", httpcacheSchema), newAppResource[tcpbalancerResourceModel, *tcpbalancerResourceModel](client.TCPBalancerResource(), "tcpbalancer", tcpbalancerSchema), newAppResource[harborResourceModel, *harborResourceModel](client.HarborResource(), "harbor", harborSchema), newAppResource[vpcResourceModel, *vpcResourceModel](client.VPCResource(), "vpc", vpcSchema), @@ -279,7 +278,6 @@ func (p *CozystackProvider) DataSources(_ context.Context) []func() datasource.D newAppDataSource[natsModel, *natsModel](client.NATSResource(), "nats", natsDataSourceSchema), newAppDataSource[opensearchModel, *opensearchModel](client.OpenSearchResource(), "opensearch", opensearchDataSourceSchema), newAppDataSource[postgresqlModel, *postgresqlModel](client.PostgresResource(), "postgres", postgresDataSourceSchema), - newAppDataSource[httpcacheModel, *httpcacheModel](client.HTTPCacheResource(), "httpcache", httpcacheDataSourceSchema), newAppDataSource[tcpbalancerModel, *tcpbalancerModel](client.TCPBalancerResource(), "tcpbalancer", tcpbalancerDataSourceSchema), newAppDataSource[harborModel, *harborModel](client.HarborResource(), "harbor", harborDataSourceSchema), newAppDataSource[vpcModel, *vpcModel](client.VPCResource(), "vpc", vpcDataSourceSchema), From 5e5dd119d4588bd704f2184ccdd85db1111ab762 Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Thu, 8 Oct 2026 15:20:42 +0300 Subject: [PATCH 02/12] build(deps): pin the cozystack API to the commit that ships tracingCentral No api/apps/v1alpha1 release tag contains the Tenant tracingCentral field yet, so the module is pinned to cozystack commit 11499c30e, the first one carrying both shared-central tracing changes. Go orders that pseudo-version below v1.6.3, so a later `go get -u` or a dependency bot can move it back to a tag without the field; the pin has to be replaced by the first v1.7 tag rather than bumped automatically. The commit also carries the API changes that landed on main before it, which the following commits adapt to. Signed-off-by: Alexey Artamonov --- go.mod | 26 +++++++++++++------------- go.sum | 56 ++++++++++++++++++++++++++++---------------------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index f49248a..b6048b3 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/cozystack/terraform-provider-cozystack go 1.26.4 require ( - github.com/cozystack/cozystack/api/apps/v1alpha1 v1.6.3 + github.com/cozystack/cozystack/api/apps/v1alpha1 v1.6.1-0.20261006102916-11499c30e12d github.com/hashicorp/terraform-plugin-framework v1.19.0 github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 github.com/hashicorp/terraform-plugin-framework-validators v0.19.0 @@ -20,8 +20,8 @@ require ( github.com/cloudflare/circl v1.6.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fatih/color v1.18.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/fxamacker/cbor/v2 v2.9.4 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -62,24 +62,24 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/run v1.2.0 // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zclconf/go-cty v1.18.1 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.52.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.55.0 // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/net v0.59.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/term v0.46.0 // indirect + golang.org/x/text v0.42.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.49.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/grpc v1.82.1 // indirect diff --git a/go.sum b/go.sum index 25b1953..91c9076 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/cozystack/cozystack/api/apps/v1alpha1 v1.6.3 h1:oqPYKiQalb3rd6UjpwZqt79o+zBgCz7Ju5wZNV5ONvQ= -github.com/cozystack/cozystack/api/apps/v1alpha1 v1.6.3/go.mod h1:uPk+qPGFuxQkRXmInAT7hUrP0kIwa+d6iopW2gliqEw= +github.com/cozystack/cozystack/api/apps/v1alpha1 v1.6.1-0.20261006102916-11499c30e12d h1:NmDDpZqH9HMp2lJWUiNOfdtHjavyB0c+lG/KmrtodpE= +github.com/cozystack/cozystack/api/apps/v1alpha1 v1.6.1-0.20261006102916-11499c30e12d/go.mod h1:39KhXUSDHEjIZ8A0Zpoofpq1o3DnVh+uChT3SZl0y5M= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= @@ -31,16 +31,16 @@ github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FM github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo= +github.com/fxamacker/cbor/v2 v2.9.4/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= @@ -181,8 +181,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -193,8 +193,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -223,30 +223,30 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= 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-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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= 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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -258,25 +258,25 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= From 6cdab380f9b6f193c48623957b4abf7013569fe2 Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Thu, 8 Oct 2026 15:20:42 +0300 Subject: [PATCH 03/12] feat(kubernetes)!: drop node_groups and node_health_check Cozystack removed nodeGroups and nodeHealthCheck from the Kubernetes application: worker pools are KubernetesNodes objects now, and the platform upgrade migrates every existing group into one. The provider kept writing both keys, which the API no longer defines, so they go from the resource and the data source; pools are managed through cozystack_kubernetes_nodes. Signed-off-by: Alexey Artamonov --- CHANGELOG.md | 1 + README.md | 1 - .../cozystack_kubernetes/resource.tf | 42 +- .../cozystack_kubernetes_nodes/resource.tf | 11 +- internal/provider/acc_test.go | 63 +-- internal/provider/kubernetes_model.go | 218 +--------- internal/provider/kubernetes_model_test.go | 377 +----------------- internal/provider/kubernetes_nodes_model.go | 5 +- internal/provider/kubernetes_schema.go | 163 +------- internal/provider/kubernetes_schema_test.go | 111 +----- internal/provider/schema_helpers_test.go | 11 +- 11 files changed, 64 insertions(+), 939 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e383f2..019217f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ To ship with the first Cozystack API pin that contains cozystack/cozystack#3936. ### Breaking changes - `cozystack_httpcache` (resource and data source) is removed, following Cozystack, which dropped the HTTPCache application and its ResourceDefinition: a cluster on this API no longer serves the kind. Remove the resource from your configuration and run `terraform state rm` for each instance before upgrading the provider. +- `cozystack_kubernetes`: `node_groups` and `node_health_check` are removed (resource and data source). Cozystack moved worker pools out of the Kubernetes application into KubernetesNodes, and its upgrade migrates every existing node group, the default `md0` included, into a KubernetesNodes object named `-`. Drop both attributes from the configuration and bring each migrated pool under management with `terraform import cozystack_kubernetes_nodes. /-`. - `rabbitmq`: the default `resources_preset` moves from `t1.nano` to `s1.nano`. The attribute is optional-with-default, so the old value sits in existing state: after upgrading the provider, a `cozystack_rabbitmq` that never set it explicitly plans `t1.nano` → `s1.nano`, which updates the cluster and rolls its pods. Set `resources_preset = "t1.nano"` explicitly to keep the previous sizing. ## v1.6.3 diff --git a/README.md b/README.md index e2ed879..357af93 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,6 @@ To avoid persisting secrets in state, the provider offers modern Terraform secre resource "cozystack_kubernetes" "app" { name = "app" namespace = "tenant-root" - node_groups = { md0 = { min_replicas = 1, max_replicas = 3 } } wait_for_ready = true } diff --git a/examples/resources/cozystack_kubernetes/resource.tf b/examples/resources/cozystack_kubernetes/resource.tf index 15316d9..e212c85 100644 --- a/examples/resources/cozystack_kubernetes/resource.tf +++ b/examples/resources/cozystack_kubernetes/resource.tf @@ -7,21 +7,13 @@ resource "cozystack_kubernetes" "cluster" { version = "v1.35" - node_groups = { - md0 = { - instance_type = "u1.medium" - disk_size = "20Gi" - min_replicas = 1 - max_replicas = 10 - roles = ["ingress-nginx"] - } - } - # The admin kubeconfig is published asynchronously, so without waiting it is # only readable from the apply after the one that created the cluster. wait_for_ready = true } +# Worker nodes are a separate resource: see cozystack_kubernetes_nodes. + # Every block below is optional and follows the platform default while unset, # so pin only what you actually need to control. resource "cozystack_kubernetes" "tuned" { @@ -30,29 +22,6 @@ resource "cozystack_kubernetes" "tuned" { version = "v1.35" - node_groups = { - # Sized explicitly instead of by instance_type: cpu and memory go together. - workers = { - min_replicas = 2 - max_replicas = 6 - - resources = { - cpu = "4" - memory = "8Gi" - } - } - - # A GPU pool boots slower, so it overrides the cluster-wide remediation - # timeout without changing it for every other group. - gpu = { - instance_type = "u1.xlarge" - min_replicas = 0 - max_replicas = 4 - node_startup_timeout = "30m" - max_unhealthy = "0%" - } - } - # Worker OS images pulled from a self-hosted image factory. The Talos release # and schematic stay unset, so they keep following the platform. talos = { @@ -60,11 +29,6 @@ resource "cozystack_kubernetes" "tuned" { installer_repository = "registry.example.test/installer" } - node_health_check = { - max_unhealthy = "50%" - node_startup_timeout = "15m" - } - # Platform identity, plus the bindings created inside the tenant cluster. oidc = { mode = "System" @@ -83,8 +47,6 @@ resource "cozystack_kubernetes" "byo_identity" { name = "byo-identity" namespace = "tenant-root" - node_groups = {} - oidc = { mode = "CustomConfig" diff --git a/examples/resources/cozystack_kubernetes_nodes/resource.tf b/examples/resources/cozystack_kubernetes_nodes/resource.tf index 8fc19d9..e0a127d 100644 --- a/examples/resources/cozystack_kubernetes_nodes/resource.tf +++ b/examples/resources/cozystack_kubernetes_nodes/resource.tf @@ -3,18 +3,9 @@ resource "cozystack_kubernetes" "cluster" { namespace = "tenant-root" version = "v1.35" - - node_groups = { - md0 = { - instance_type = "u1.medium" - min_replicas = 1 - max_replicas = 3 - } - } } -# A standalone pool is named "-". The pool part must not collide -# with a node group the parent cluster still manages, such as md0 above. +# A pool is named "-". resource "cozystack_kubernetes_nodes" "gpu" { name = "${cozystack_kubernetes.cluster.name}-gpu" namespace = cozystack_kubernetes.cluster.namespace diff --git a/internal/provider/acc_test.go b/internal/provider/acc_test.go index 92ae012..0bad7a6 100644 --- a/internal/provider/acc_test.go +++ b/internal/provider/acc_test.go @@ -976,26 +976,19 @@ func checkSpecOmits(res client.Resource, namespace, name string, keys ...string) } } -func kubernetesAccConfig(maxReplicas int) string { +func kubernetesAccConfig(host string) string { return fmt.Sprintf(` resource "cozystack_kubernetes" "test" { name = "tfacck8s" namespace = "tenant-root" version = "v1.35" - node_groups = { - md0 = { - instance_type = "u1.medium" - min_replicas = 0 - max_replicas = %[1]d - roles = ["ingress-nginx"] - } - } + host = %[1]q } -`, maxReplicas) +`, host) } func TestAccKubernetesResource(t *testing.T) { - config := kubernetesAccConfig(1) + config := kubernetesAccConfig("tfacck8s.example.test") resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, @@ -1005,19 +998,14 @@ func TestAccKubernetesResource(t *testing.T) { Config: config, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("cozystack_kubernetes.test", "id", "tenant-root/tfacck8s"), - resource.TestCheckResourceAttr("cozystack_kubernetes.test", "node_groups.md0.max_replicas", "1"), - resource.TestCheckResourceAttr("cozystack_kubernetes.test", "node_groups.md0.disk_size", "20Gi"), + resource.TestCheckResourceAttr("cozystack_kubernetes.test", "host", "tfacck8s.example.test"), // The configuration pins none of the 1.6 blocks. The server // materialises its schema defaults on every read, and the // managed resource deliberately does not absorb them: storing // them would put them in the plan, and the next update would // write them into the release as explicit values. resource.TestCheckNoResourceAttr("cozystack_kubernetes.test", "talos.version"), - resource.TestCheckNoResourceAttr("cozystack_kubernetes.test", "node_health_check.max_unhealthy"), resource.TestCheckNoResourceAttr("cozystack_kubernetes.test", "oidc.mode"), - // Per-group health overrides are undefaulted upstream: unset - // stays unset rather than being echoed back as an empty string. - resource.TestCheckNoResourceAttr("cozystack_kubernetes.test", "node_groups.md0.max_unhealthy"), ), }, { @@ -1025,58 +1013,33 @@ func TestAccKubernetesResource(t *testing.T) { // the defaults the previous read put in state, so this is the step // that would write them into the release and pin the cluster to // them; the spec must still come out without those keys. - Config: kubernetesAccConfig(2), + Config: kubernetesAccConfig("tfacck8s-updated.example.test"), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("cozystack_kubernetes.test", "node_groups.md0.max_replicas", "2"), + resource.TestCheckResourceAttr("cozystack_kubernetes.test", "host", "tfacck8s-updated.example.test"), checkSpecOmits(client.KubernetesResource(), "tenant-root", "tfacck8s", - "talos", "oidc", "nodeHealthCheck", "images", "controlPlane"), + "talos", "oidc", "images", "controlPlane"), ), }, { - // Explicitly empty lists at both levels. They mean the same to - // the chart as an absent key, but the provider sends them as - // configured, and only a real server says whether an empty list - // survives the round trip into state. + // An explicitly empty list. It means the same to the chart as an + // absent key, but the provider sends it as configured, and only a + // real server says whether an empty list survives the round trip + // into state. Config: ` resource "cozystack_kubernetes" "test" { name = "tfacck8s" namespace = "tenant-root" version = "v1.35" - node_groups = { - md0 = { - instance_type = "u1.medium" - min_replicas = 0 - max_replicas = 2 - roles = [] - } - } + host = "tfacck8s-updated.example.test" oidc = { users = [] } } `, Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("cozystack_kubernetes.test", "node_groups.md0.roles.#", "0"), resource.TestCheckResourceAttr("cozystack_kubernetes.test", "oidc.users.#", "0"), ), }, - { - // A cluster that declares no node groups at all — the platform's - // own default, and the shape the provider's example uses. Same - // reasoning as the step above: an empty map is only a round trip - // once a server has echoed it. - Config: ` -resource "cozystack_kubernetes" "test" { - name = "tfacck8s" - namespace = "tenant-root" - version = "v1.35" - node_groups = {} -} -`, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("cozystack_kubernetes.test", "node_groups.%", "0"), - ), - }, { ResourceName: "cozystack_kubernetes.test", ImportState: true, diff --git a/internal/provider/kubernetes_model.go b/internal/provider/kubernetes_model.go index 05a715e..fff6894 100644 --- a/internal/provider/kubernetes_model.go +++ b/internal/provider/kubernetes_model.go @@ -20,28 +20,22 @@ const ( attrImages = "images" ) -// Attribute names of the blocks whose spec key is the camelCase form. -const ( - attrNodeHealthCheck = "node_health_check" - attrControlPlane = "control_plane" -) +// Attribute name of the block whose spec key is the camelCase form. +const attrControlPlane = "control_plane" // Spec keys of the cozystack_kubernetes blocks whose camelCase form differs // from the snake_case attribute name. const ( - specNodeGroups = "nodeGroups" - specNodeHealthCheck = "nodeHealthCheck" - specMaxUnhealthy = "maxUnhealthy" - specNodeStartupTimeout = "nodeStartupTimeout" - specCustomConfig = "customConfig" - specSecretRef = "secretRef" - specControlPlane = "controlPlane" - specAPIServer = "apiServer" + specCustomConfig = "customConfig" + specSecretRef = "secretRef" + specControlPlane = "controlPlane" + specAPIServer = "apiServer" ) // kubernetesModel maps the cozystack_kubernetes schema to Go types. The addons -// block, the control-plane component sizing, and per-node-group GPU and kubelet -// tuning are not managed (they use server defaults). +// block and the control-plane component sizing are not managed (they use server +// defaults). Worker pools are not part of this Kind: they are +// cozystack_kubernetes_nodes resources. // // Blocks whose upstream defaults roll with each Cozystack release (talos, // images) are modelled without provider-side defaults: an unset block is left @@ -56,13 +50,11 @@ type kubernetesModel struct { StorageClass types.String `tfsdk:"storage_class"` Version types.String `tfsdk:"version"` Host types.String `tfsdk:"host"` - NodeGroups types.Map `tfsdk:"node_groups"` - Talos types.Object `tfsdk:"talos"` - NodeHealthCheck types.Object `tfsdk:"node_health_check"` - OIDC types.Object `tfsdk:"oidc"` - ControlPlane types.Object `tfsdk:"control_plane"` - Images types.Object `tfsdk:"images"` + Talos types.Object `tfsdk:"talos"` + OIDC types.Object `tfsdk:"oidc"` + ControlPlane types.Object `tfsdk:"control_plane"` + Images types.Object `tfsdk:"images"` Ready types.Bool `tfsdk:"ready"` ChartVersion types.String `tfsdk:"chart_version"` @@ -86,7 +78,7 @@ func (m *kubernetesResourceModel) waitConfig() (types.Bool, types.String) { // // The aggregated apiserver materialises this Kind's schema defaults on every // read, so the plain model flatten — the one the data source wants — reports the -// whole talos, nodeHealthCheck, oidc, controlPlane and images surface whether or +// whole talos, oidc, controlPlane and images surface whether or // not anybody configured it. Letting that into a managed resource's state makes // Terraform's own rules work against the practitioner: the value becomes the // prior state, the prior state becomes the plan wherever the configuration is @@ -112,7 +104,6 @@ func (m *kubernetesResourceModel) flatten(app *client.Application) diag.Diagnost target *types.Object }{ {configured: m.Talos, target: &m.Talos}, - {configured: m.NodeHealthCheck, target: &m.NodeHealthCheck}, {configured: m.OIDC, target: &m.OIDC}, {configured: m.ControlPlane, target: &m.ControlPlane}, {configured: m.Images, target: &m.Images}, @@ -216,45 +207,11 @@ func (m *kubernetesModel) identity() (string, string) { return m.Namespace.ValueString(), m.Name.ValueString() } -func k8sNodeGroupObjectType() map[string]attr.Type { - return map[string]attr.Type{ - "disk_size": types.StringType, - "instance_type": types.StringType, - "min_replicas": types.Int64Type, - "max_replicas": types.Int64Type, - "roles": types.ListType{ElemType: types.StringType}, - "storage_class": types.StringType, - "resources": types.ObjectType{AttrTypes: resourcesObjectType()}, - "max_unhealthy": types.StringType, - "node_startup_timeout": types.StringType, - } -} - -type k8sNodeGroupData struct { - DiskSize types.String `tfsdk:"disk_size"` - InstanceType types.String `tfsdk:"instance_type"` - MinReplicas types.Int64 `tfsdk:"min_replicas"` - MaxReplicas types.Int64 `tfsdk:"max_replicas"` - Roles types.List `tfsdk:"roles"` - StorageClass types.String `tfsdk:"storage_class"` - Resources types.Object `tfsdk:"resources"` - MaxUnhealthy types.String `tfsdk:"max_unhealthy"` - NodeStartupTimeout types.String `tfsdk:"node_startup_timeout"` -} - func (m *kubernetesModel) expand(ctx context.Context) (*client.Application, diag.Diagnostics) { var diags diag.Diagnostics - nodeGroups, ngDiags := expandNodeGroups(ctx, m.NodeGroups) - diags.Append(ngDiags...) - - if diags.HasError() { - return nil, diags - } - spec := map[string]any{ - attrVersion: m.Version.ValueString(), - specNodeGroups: nodeGroups, + attrVersion: m.Version.ValueString(), } // storageClass carries no provider-side default, so a create that does not @@ -285,56 +242,6 @@ func (m *kubernetesModel) expand(ctx context.Context) (*client.Application, diag }, diags } -func expandNodeGroups(ctx context.Context, value types.Map) (map[string]any, diag.Diagnostics) { - var diags diag.Diagnostics - - out := map[string]any{} - - if value.IsNull() || value.IsUnknown() { - return out, diags - } - - elements := map[string]k8sNodeGroupData{} - diags.Append(value.ElementsAs(ctx, &elements, false)...) - - if diags.HasError() { - return out, diags - } - - for name := range elements { - group := elements[name] - - resources, rDiags := expandResources(ctx, group.Resources) - diags.Append(rDiags...) - - entry := map[string]any{ - "diskSize": group.DiskSize.ValueString(), - "instanceType": group.InstanceType.ValueString(), - "minReplicas": group.MinReplicas.ValueInt64(), - "maxReplicas": group.MaxReplicas.ValueInt64(), - specStorageClass: group.StorageClass.ValueString(), - attrResources: resources, - } - - // The chart ranges over roles, so an empty list and an absent key mean - // the same thing to it; writing the list as configured keeps the request - // a faithful copy of the configuration, and keeps an empty list from - // reading back as null against a plan that holds one. - diags.Append(setOptionalStringList(ctx, entry, "roles", group.Roles)...) - - // These two are the group's genuinely presence-sensitive keys: the chart - // reads them with hasKey, and falls back to the cluster-wide - // nodeHealthCheck when they are absent. An empty string is not the same - // as absent here. - setOptionalString(entry, specMaxUnhealthy, group.MaxUnhealthy) - setOptionalString(entry, specNodeStartupTimeout, group.NodeStartupTimeout) - - out[name] = entry - } - - return out, diags -} - func (m *kubernetesModel) flatten(app *client.Application) diag.Diagnostics { var diags diag.Diagnostics @@ -345,12 +252,7 @@ func (m *kubernetesModel) flatten(app *client.Application) diag.Diagnostics { m.Version = types.StringValue(specString(app.Spec, attrVersion)) m.Host = types.StringValue(specString(app.Spec, attrHost)) - nodeGroups, ngDiags := flattenNodeGroups(app.Spec[specNodeGroups]) - diags.Append(ngDiags...) - - m.NodeGroups = nodeGroups m.Talos = flattenTalos(app.Spec[attrTalos]) - m.NodeHealthCheck = flattenNodeHealthCheck(app.Spec[specNodeHealthCheck]) oidc, oidcDiags := flattenOIDC(app.Spec[attrOIDC]) diags.Append(oidcDiags...) @@ -413,7 +315,6 @@ func (m *kubernetesModel) expandBlocks(ctx context.Context, spec map[string]any) expand func(context.Context, types.Object) (map[string]any, diag.Diagnostics) }{ {key: attrTalos, value: m.Talos, expand: expandTalos}, - {key: specNodeHealthCheck, value: m.NodeHealthCheck, expand: expandNodeHealthCheck}, {key: attrOIDC, value: m.OIDC, expand: expandOIDC}, {key: specControlPlane, value: m.ControlPlane, expand: expandControlPlane}, {key: attrImages, value: m.Images, expand: expandImages}, @@ -494,56 +395,6 @@ func flattenTalos(raw any) types.Object { }) } -func k8sNodeHealthCheckObjectType() map[string]attr.Type { - return map[string]attr.Type{ - "max_unhealthy": types.StringType, - "node_startup_timeout": types.StringType, - } -} - -type k8sNodeHealthCheckData struct { - MaxUnhealthy types.String `tfsdk:"max_unhealthy"` - NodeStartupTimeout types.String `tfsdk:"node_startup_timeout"` -} - -// expandNodeHealthCheck renders the nodeHealthCheck block into a spec submap, -// or nil when the block is unset. -func expandNodeHealthCheck(ctx context.Context, obj types.Object) (map[string]any, diag.Diagnostics) { - var diags diag.Diagnostics - - if obj.IsNull() || obj.IsUnknown() { - return nil, diags - } - - var data k8sNodeHealthCheckData - - diags.Append(obj.As(ctx, &data, basetypes.ObjectAsOptions{})...) - - if diags.HasError() { - return nil, diags - } - - out := map[string]any{} - - setOptionalString(out, specMaxUnhealthy, data.MaxUnhealthy) - setOptionalString(out, specNodeStartupTimeout, data.NodeStartupTimeout) - - return out, diags -} - -// flattenNodeHealthCheck builds the nodeHealthCheck block from a spec submap. -func flattenNodeHealthCheck(raw any) types.Object { - check, ok := raw.(map[string]any) - if !ok { - return types.ObjectNull(k8sNodeHealthCheckObjectType()) - } - - return types.ObjectValueMust(k8sNodeHealthCheckObjectType(), map[string]attr.Value{ - "max_unhealthy": specStringOrNull(check, specMaxUnhealthy), - "node_startup_timeout": specStringOrNull(check, specNodeStartupTimeout), - }) -} - func k8sOIDCObjectType() map[string]attr.Type { return map[string]attr.Type{ "mode": types.StringType, @@ -911,42 +762,3 @@ func flattenAPIServer(raw any) (types.Object, diag.Diagnostics) { return value, diags } - -// flattenNodeGroups builds the node-group map from a spec submap. `{}` is a -// configuration upstream documents and defaults to — the chart renders a single -// md0 that provisions nothing until the autoscaler reacts — so an empty map has -// to come back as an empty map. The shared collapsing helper would return null -// and fail the apply against a plan holding an empty map. -func flattenNodeGroups(raw any) (types.Map, diag.Diagnostics) { - var diags diag.Diagnostics - - elementType := types.ObjectType{AttrTypes: k8sNodeGroupObjectType()} - - groups, ok := raw.(map[string]any) - if !ok { - return types.MapNull(elementType), diags - } - - if len(groups) == 0 { - value, valueDiags := types.MapValue(elementType, map[string]attr.Value{}) - diags.Append(valueDiags...) - - return value, diags - } - - return flattenObjectMap(raw, k8sNodeGroupObjectType(), func(group map[string]any) map[string]attr.Value { - resources, _ := flattenResources(group[attrResources]) - - return map[string]attr.Value{ - "disk_size": types.StringValue(specString(group, "diskSize")), - "instance_type": types.StringValue(specString(group, "instanceType")), - "min_replicas": types.Int64Value(anyToInt64(group["minReplicas"])), - "max_replicas": types.Int64Value(anyToInt64(group["maxReplicas"])), - "roles": specStringListOrNull(group["roles"]), - "storage_class": types.StringValue(specString(group, specStorageClass)), - "resources": resources, - "max_unhealthy": specStringOrNull(group, specMaxUnhealthy), - "node_startup_timeout": specStringOrNull(group, specNodeStartupTimeout), - } - }) -} diff --git a/internal/provider/kubernetes_model_test.go b/internal/provider/kubernetes_model_test.go index ee06cc0..67b4750 100644 --- a/internal/provider/kubernetes_model_test.go +++ b/internal/provider/kubernetes_model_test.go @@ -65,30 +65,13 @@ func fullControlPlaneObject() types.Object { } func fullKubernetesModel() kubernetesModel { - group := types.ObjectValueMust(k8sNodeGroupObjectType(), map[string]attr.Value{ - "disk_size": types.StringValue("20Gi"), - "instance_type": types.StringValue("u1.medium"), - "min_replicas": types.Int64Value(0), - "max_replicas": types.Int64Value(3), - "roles": types.ListValueMust(types.StringType, []attr.Value{types.StringValue("ingress-nginx")}), - "storage_class": types.StringValue(""), - "resources": types.ObjectNull(resourcesObjectType()), - "max_unhealthy": types.StringValue("0%"), - "node_startup_timeout": types.StringValue("20m"), - }) - return kubernetesModel{ Name: types.StringValue("cluster"), Namespace: types.StringValue("tenant-root"), StorageClass: types.StringValue("replicated"), Version: types.StringValue("v1.35"), Host: types.StringValue("cluster.example.com"), - NodeGroups: types.MapValueMust(types.ObjectType{AttrTypes: k8sNodeGroupObjectType()}, map[string]attr.Value{"md0": group}), Talos: fullTalosObject(), - NodeHealthCheck: types.ObjectValueMust(k8sNodeHealthCheckObjectType(), map[string]attr.Value{ - "max_unhealthy": types.StringValue("50%"), - "node_startup_timeout": types.StringValue("10m"), - }), OIDC: fullOIDCObject(), ControlPlane: fullControlPlaneObject(), Images: types.ObjectValueMust(k8sImagesObjectType(), map[string]attr.Value{ @@ -132,97 +115,6 @@ func nestedSpecKeys(t *testing.T, spec map[string]any, path ...string) map[strin return keys } -func TestKubernetesExpand_NodeGroups(t *testing.T) { - t.Parallel() - - model := fullKubernetesModel() - - got, diags := model.expand(context.Background()) - if diags.HasError() { - t.Fatalf("expand diagnostics: %v", diags) - } - - groups, _ := got.Spec["nodeGroups"].(map[string]any) - md0, _ := groups["md0"].(map[string]any) - if md0["maxReplicas"] != int64(3) { - t.Errorf("md0.maxReplicas = %v, want 3", md0["maxReplicas"]) - } - - roles, _ := md0["roles"].([]any) - if len(roles) != 1 || roles[0] != "ingress-nginx" { - t.Errorf("md0.roles = %v, want [ingress-nginx]", roles) - } -} - -// The per-group health-check overrides have no upstream default: an absent key -// means "inherit the cluster-wide nodeHealthCheck", so they must not be written -// as empty strings. -func TestKubernetesExpand_NodeGroupHealthOverrides(t *testing.T) { - t.Parallel() - - model := fullKubernetesModel() - - got, diags := model.expand(context.Background()) - if diags.HasError() { - t.Fatalf("expand diagnostics: %v", diags) - } - - md0 := nestedSpecKeys(t, got.Spec, "nodeGroups", "md0") - if !md0["maxUnhealthy"] || !md0["nodeStartupTimeout"] { - t.Errorf("node group keys = %v, want the health-check overrides emitted", md0) - } - - bare := types.ObjectValueMust(k8sNodeGroupObjectType(), map[string]attr.Value{ - "disk_size": types.StringValue("20Gi"), - "instance_type": types.StringValue("u1.medium"), - "min_replicas": types.Int64Value(0), - "max_replicas": types.Int64Value(3), - "roles": types.ListNull(types.StringType), - "storage_class": types.StringValue(""), - "resources": types.ObjectNull(resourcesObjectType()), - "max_unhealthy": types.StringNull(), - "node_startup_timeout": types.StringNull(), - }) - - model.NodeGroups = types.MapValueMust( - types.ObjectType{AttrTypes: k8sNodeGroupObjectType()}, - map[string]attr.Value{"md0": bare}, - ) - - got, diags = model.expand(context.Background()) - if diags.HasError() { - t.Fatalf("expand diagnostics: %v", diags) - } - - md0 = nestedSpecKeys(t, got.Spec, "nodeGroups", "md0") - if md0["maxUnhealthy"] || md0["nodeStartupTimeout"] { - t.Errorf("node group keys = %v, want the unset health-check overrides omitted", md0) - } -} - -func TestKubernetesFlatten_NodeGroupHealthOverrides(t *testing.T) { - t.Parallel() - - groups, diags := flattenNodeGroups(map[string]any{ - "md0": map[string]any{"maxUnhealthy": "1"}, - }) - if diags.HasError() { - t.Fatalf("flatten diagnostics: %v", diags) - } - - md0, _ := groups.Elements()["md0"].(types.Object) - - maxUnhealthy, _ := md0.Attributes()["max_unhealthy"].(types.String) - if maxUnhealthy.ValueString() != "1" { - t.Errorf("max_unhealthy = %q, want 1", maxUnhealthy.ValueString()) - } - - timeout, _ := md0.Attributes()["node_startup_timeout"].(types.String) - if !timeout.IsNull() { - t.Errorf("node_startup_timeout = %v for an absent key, want null", timeout) - } -} - func TestKubernetesExpand_OmitsEmptyHost(t *testing.T) { t.Parallel() @@ -337,67 +229,6 @@ func TestKubernetesExpandTalosKeysMatchTalosSpec(t *testing.T) { assertSpecCoverage(t, nestedSpecKeys(t, got.Spec, "talos"), kubernetes.Talos{}) } -// nodeHealthCheck tunes MachineHealthCheck remediation. An unset block must -// stay out of the spec so the platform's own tuning applies. -func TestKubernetesExpand_NodeHealthCheck(t *testing.T) { - t.Parallel() - - model := fullKubernetesModel() - model.NodeHealthCheck = types.ObjectNull(k8sNodeHealthCheckObjectType()) - - got, diags := model.expand(context.Background()) - if diags.HasError() { - t.Fatalf("expand diagnostics: %v", diags) - } - - if _, ok := got.Spec["nodeHealthCheck"]; ok { - t.Fatalf("nodeHealthCheck key present for an unset block, want omitted") - } - - model = fullKubernetesModel() - - got, diags = model.expand(context.Background()) - if diags.HasError() { - t.Fatalf("expand diagnostics: %v", diags) - } - - check, _ := got.Spec["nodeHealthCheck"].(map[string]any) - if check["maxUnhealthy"] != "50%" || check["nodeStartupTimeout"] != "10m" { - t.Errorf("nodeHealthCheck = %v, want maxUnhealthy=50%% nodeStartupTimeout=10m", check) - } -} - -func TestKubernetesFlatten_NodeHealthCheck(t *testing.T) { - t.Parallel() - - if got := flattenNodeHealthCheck(nil); !got.IsNull() { - t.Errorf("nodeHealthCheck = %v for an absent key, want null", got) - } - - got := flattenNodeHealthCheck(map[string]any{"maxUnhealthy": "0%", "nodeStartupTimeout": "20m"}) - if got.IsNull() { - t.Fatalf("nodeHealthCheck is null for a populated block") - } - - timeout, _ := got.Attributes()["node_startup_timeout"].(types.String) - if timeout.ValueString() != "20m" { - t.Errorf("node_startup_timeout = %q, want 20m", timeout.ValueString()) - } -} - -func TestKubernetesExpandNodeHealthCheckKeysMatchSpec(t *testing.T) { - t.Parallel() - - model := fullKubernetesModel() - - got, diags := model.expand(context.Background()) - if diags.HasError() { - t.Fatalf("expand diagnostics: %v", diags) - } - - assertSpecCoverage(t, nestedSpecKeys(t, got.Spec, "nodeHealthCheck"), kubernetes.NodeHealthCheck{}) -} - func TestKubernetesExpand_OIDCOmittedWhenUnset(t *testing.T) { t.Parallel() @@ -716,13 +547,6 @@ func TestKubernetesFlatten_RoundTrip(t *testing.T) { "storageClass": "replicated", "version": "v1.34", "host": "cluster.example.com", - "nodeGroups": map[string]any{ - "md0": map[string]any{ - "diskSize": "20Gi", "instanceType": "u1.large", "minReplicas": int64(1), - "maxReplicas": int64(5), "roles": []any{"ingress-nginx"}, "storageClass": "", - "resources": map[string]any{}, - }, - }, }, Status: client.ApplicationStatus{Version: "1.0.0", Ready: true}, } @@ -736,11 +560,6 @@ func TestKubernetesFlatten_RoundTrip(t *testing.T) { if model.Version.ValueString() != "v1.34" { t.Errorf("version = %q, want v1.34", model.Version.ValueString()) } - - groups := model.NodeGroups.Elements() - if _, ok := groups["md0"]; !ok { - t.Errorf("node_groups missing md0") - } } func TestKubernetesExpandKeysMatchConfigSpec(t *testing.T) { @@ -761,41 +580,6 @@ func TestKubernetesExpandKeysMatchConfigSpec(t *testing.T) { assertSpecCoverage(t, emitted, kubernetes.ConfigSpec{}, "addons") } -// The node-group spec is a second schema surface the top-level ConfigSpec guard -// does not reach, since it only reflects one level of json tags. Guarding it -// separately means a node-group field added upstream surfaces as a failing test -// naming the field, rather than silently going unmodelled. -func TestKubernetesExpandNodeGroupKeysMatchNodeGroupSpec(t *testing.T) { - t.Parallel() - - model := fullKubernetesModel() - - got, diags := model.expand(context.Background()) - if diags.HasError() { - t.Fatalf("expand diagnostics: %v", diags) - } - - groups, _ := got.Spec["nodeGroups"].(map[string]any) - - md0, ok := groups["md0"].(map[string]any) - if !ok { - t.Fatalf("nodeGroups.md0 missing from the emitted spec") - } - - emitted := make(map[string]bool, len(md0)) - for key := range md0 { - emitted[key] = true - } - - assertSpecCoverage(t, emitted, kubernetes.NodeGroup{}, "gpus", "kubelet") -} - -// The managed resource must not absorb blocks the configuration never wrote. -// The aggregated apiserver materialises this Kind's schema defaults on every -// read, so a resource that stored them would carry them into the plan and, on -// the next update, write them into the release as explicit values — freezing the -// cluster on the Talos release and schematic that were current that day. The -// data source keeps reporting everything; only the resource trims. func TestKubernetesResourceFlatten_KeepsUnconfiguredBlocksOut(t *testing.T) { t.Parallel() @@ -805,7 +589,6 @@ func TestKubernetesResourceFlatten_KeepsUnconfiguredBlocksOut(t *testing.T) { Spec: map[string]any{ "storageClass": "replicated", "version": "v1.35", - "nodeGroups": map[string]any{}, // The whole surface the server materialises for a cluster whose // configuration pinned none of it. "talos": map[string]any{ @@ -814,10 +597,9 @@ func TestKubernetesResourceFlatten_KeepsUnconfiguredBlocksOut(t *testing.T) { "schematicID": "ce4c980550dd2ab1b17bbf2b08801c7eb59418eafe8f279833297925d67c7515", "version": "v1.13.6", }, - "nodeHealthCheck": map[string]any{"maxUnhealthy": "50%", "nodeStartupTimeout": "10m"}, - "oidc": map[string]any{"mode": "None", "users": []any{}}, - "images": map[string]any{"kubectl": "", "talosCsrSigner": "", "waitForKubeconfig": ""}, - "controlPlane": map[string]any{"apiServer": map[string]any{"extraArgs": []any{}}}, + "oidc": map[string]any{"mode": "None", "users": []any{}}, + "images": map[string]any{"kubectl": "", "talosCsrSigner": "", "waitForKubeconfig": ""}, + "controlPlane": map[string]any{"apiServer": map[string]any{"extraArgs": []any{}}}, }, } @@ -828,11 +610,10 @@ func TestKubernetesResourceFlatten_KeepsUnconfiguredBlocksOut(t *testing.T) { } for name, value := range map[string]types.Object{ - "talos": resourceModel.Talos, - "node_health_check": resourceModel.NodeHealthCheck, - "oidc": resourceModel.OIDC, - "control_plane": resourceModel.ControlPlane, - "images": resourceModel.Images, + "talos": resourceModel.Talos, + "oidc": resourceModel.OIDC, + "control_plane": resourceModel.ControlPlane, + "images": resourceModel.Images, } { if !value.IsNull() { t.Errorf("%s = %v after a read of an unconfigured cluster, want null", name, value) @@ -871,7 +652,6 @@ func TestKubernetesResourceFlatten_RefreshesConfiguredFieldsOnly(t *testing.T) { Name: "cluster", Namespace: "tenant-root", Spec: map[string]any{ - "nodeGroups": map[string]any{}, "talos": map[string]any{ "imageFactoryURL": "https://factory.talos.dev", "schematicID": "ce4c980550dd2ab1b17bbf2b08801c7eb59418eafe8f279833297925d67c7515", @@ -919,7 +699,6 @@ func TestKubernetesResourceFlatten_TrimsNestedObjects(t *testing.T) { Name: "cluster", Namespace: "tenant-root", Spec: map[string]any{ - "nodeGroups": map[string]any{}, "oidc": map[string]any{ "mode": "CustomConfig", "users": []any{}, @@ -986,102 +765,6 @@ func TestKubernetesExpand_StorageClassOmittedWhenUnset(t *testing.T) { } } -// `roles = []` reaches the cluster the same way an absent key does — the chart -// just ranges over the list. It matters on the Terraform side: collapsing the -// empty list to an absent key made the server return nothing, the state read -// back null against a plan holding an empty list, and the apply fail on a valid -// configuration. -func TestKubernetesExpand_NodeGroupRolesThreeStates(t *testing.T) { - t.Parallel() - - group := func(roles types.List) types.Map { - return types.MapValueMust( - types.ObjectType{AttrTypes: k8sNodeGroupObjectType()}, - map[string]attr.Value{"md0": types.ObjectValueMust(k8sNodeGroupObjectType(), map[string]attr.Value{ - "disk_size": types.StringValue("20Gi"), - "instance_type": types.StringValue("u1.medium"), - "min_replicas": types.Int64Value(0), - "max_replicas": types.Int64Value(1), - "roles": roles, - "storage_class": types.StringValue(""), - "resources": types.ObjectNull(resourcesObjectType()), - "max_unhealthy": types.StringNull(), - "node_startup_timeout": types.StringNull(), - })}, - ) - } - - tests := []struct { - name string - roles types.List - present bool - length int - }{ - {name: "null omits the key", roles: types.ListNull(types.StringType)}, - { - name: "empty writes an empty list", - roles: types.ListValueMust(types.StringType, []attr.Value{}), - present: true, - }, - { - name: "populated writes the roles", - roles: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("ingress-nginx")}), - present: true, - length: 1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - model := fullKubernetesModel() - model.NodeGroups = group(tt.roles) - - got, diags := model.expand(context.Background()) - if diags.HasError() { - t.Fatalf("expand diagnostics: %v", diags) - } - - md0 := nestedSpec(t, got.Spec, "nodeGroups", "md0") - - raw, ok := md0["roles"] - if ok != tt.present { - t.Fatalf("roles present = %v, want %v", ok, tt.present) - } - - if !tt.present { - return - } - - items, _ := raw.([]any) - if len(items) != tt.length { - t.Errorf("roles has %d entries, want %d", len(items), tt.length) - } - }) - } -} - -func TestKubernetesFlatten_NodeGroupRolesKeepEmptyList(t *testing.T) { - t.Parallel() - - groups, diags := flattenNodeGroups(map[string]any{"md0": map[string]any{"roles": []any{}}}) - if diags.HasError() { - t.Fatalf("flatten diagnostics: %v", diags) - } - - md0, _ := groups.Elements()["md0"].(types.Object) - - roles, _ := md0.Attributes()["roles"].(types.List) - if roles.IsNull() { - t.Errorf("roles = null for a stored empty list, want an empty list") - } -} - -// expand is guarded against the upstream json tags, but flatten's keys are -// hand-written on both sides — in the code and in the tests — so a matched typo -// would pass every other test in this file. Sending the full model through both -// directions makes the agreement transitive instead of eyeballed. func TestKubernetesExpandFlatten_RoundTripsEveryBlock(t *testing.T) { t.Parallel() @@ -1099,12 +782,10 @@ func TestKubernetesExpandFlatten_RoundTripsEveryBlock(t *testing.T) { } blocks := map[string][2]attr.Value{ - "talos": {want.Talos, got.Talos}, - "node_health_check": {want.NodeHealthCheck, got.NodeHealthCheck}, - "oidc": {want.OIDC, got.OIDC}, - "control_plane": {want.ControlPlane, got.ControlPlane}, - "images": {want.Images, got.Images}, - "node_groups": {want.NodeGroups, got.NodeGroups}, + "talos": {want.Talos, got.Talos}, + "oidc": {want.OIDC, got.OIDC}, + "control_plane": {want.ControlPlane, got.ControlPlane}, + "images": {want.Images, got.Images}, } for name, pair := range blocks { @@ -1119,42 +800,6 @@ func TestKubernetesExpandFlatten_RoundTripsEveryBlock(t *testing.T) { } } -// `node_groups = {}` is what upstream defaults to and documents — the chart -// renders a single md0 that provisions nothing until the autoscaler reacts. The -// attribute is Required, so the plan holds an empty map and the read has to -// return one; collapsing it to null fails the apply on a configuration the -// platform explicitly supports, and the provider's own example uses it. -func TestKubernetesFlatten_EmptyNodeGroupsStayEmpty(t *testing.T) { - t.Parallel() - - groups, diags := flattenNodeGroups(map[string]any{}) - if diags.HasError() { - t.Fatalf("flatten diagnostics: %v", diags) - } - - if groups.IsNull() { - t.Fatalf("node_groups = null for a cluster that declares none, want an empty map") - } - - if len(groups.Elements()) != 0 { - t.Errorf("node_groups has %d entries, want 0", len(groups.Elements())) - } - - // An absent key is still absent: that is a server that did not report the - // field at all, not a cluster with no groups. - absent, diags := flattenNodeGroups(nil) - if diags.HasError() { - t.Fatalf("flatten diagnostics: %v", diags) - } - - if !absent.IsNull() { - t.Errorf("node_groups = %v for an absent key, want null", absent) - } -} - -// keepConfiguredAttributes builds a null for anything the server leaves out, so -// it has to work off the attribute's own type rather than the string/list/object -// shapes these blocks happen to use today. func TestKeepConfiguredAttributes_NullsMatchTheAttributeType(t *testing.T) { t.Parallel() diff --git a/internal/provider/kubernetes_nodes_model.go b/internal/provider/kubernetes_nodes_model.go index fd674fd..4b9cba0 100644 --- a/internal/provider/kubernetes_nodes_model.go +++ b/internal/provider/kubernetes_nodes_model.go @@ -12,9 +12,8 @@ import ( ) // kubernetesNodesModel maps the cozystack_kubernetes_nodes schema to Go types. -// A pool is a standalone worker node group: it carries the same sizing fields a -// nodeGroup of cozystack_kubernetes carries, plus the kubelet, Talos image, and -// image-override blocks that only the standalone kind exposes. +// A pool is a worker node group of a cozystack_kubernetes cluster: its sizing +// fields, plus the kubelet, Talos image, and image-override blocks. type kubernetesNodesModel struct { ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` diff --git a/internal/provider/kubernetes_schema.go b/internal/provider/kubernetes_schema.go index 9c7a612..6f025b7 100644 --- a/internal/provider/kubernetes_schema.go +++ b/internal/provider/kubernetes_schema.go @@ -8,7 +8,6 @@ import ( dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/path" rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" @@ -16,91 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -func k8sNodeGroupsResourceAttribute() rschema.MapNestedAttribute { - return rschema.MapNestedAttribute{ - Required: true, - MarkdownDescription: "Worker node groups keyed by name.", - NestedObject: rschema.NestedAttributeObject{ - Attributes: map[string]rschema.Attribute{ - "disk_size": rschema.StringAttribute{ - Optional: true, Computed: true, - Default: stringdefault.StaticString("20Gi"), - MarkdownDescription: "Persistent disk size for kubelet and containerd data.", - }, - "instance_type": rschema.StringAttribute{ - Optional: true, Computed: true, - Default: stringdefault.StaticString("u1.medium"), - MarkdownDescription: "Virtual machine instance type.", - }, - "min_replicas": rschema.Int64Attribute{ - Optional: true, Computed: true, - Default: int64default.StaticInt64(0), - MarkdownDescription: "Minimum number of replicas.", - }, - "max_replicas": rschema.Int64Attribute{ - Optional: true, Computed: true, - Default: int64default.StaticInt64(10), - MarkdownDescription: "Maximum number of replicas.", - }, - "roles": rschema.ListAttribute{ - Optional: true, - ElementType: types.StringType, - MarkdownDescription: "Node roles (e.g. `ingress-nginx`).", - }, - "storage_class": rschema.StringAttribute{ - Optional: true, Computed: true, - Default: stringdefault.StaticString(""), - MarkdownDescription: "StorageClass for worker node persistent disks. Falls back to the " + - "cluster's `storage_class` when empty. Deliberately mutable upstream, unlike the " + - "cluster-level attribute.", - }, - "resources": k8sNodeGroupResourcesAttribute(), - "max_unhealthy": rschema.StringAttribute{ - Optional: true, - MarkdownDescription: "Per-group override for `node_health_check.max_unhealthy`, as a bare " + - "integer (`\"1\"`) or a percentage (`\"0%\"`). Inherits the cluster-wide value while unset.", - }, - "node_startup_timeout": rschema.StringAttribute{ - Optional: true, - MarkdownDescription: "Per-group override for `node_health_check.node_startup_timeout` " + - "(duration, e.g. `20m`). Inherits the cluster-wide value while unset.", - }, - }, - }, - } -} - -// k8sNodeGroupResourcesAttribute returns the per-node-group resources block. -// The node is sized by instance_type unless BOTH cpu and memory are set — -// KubeVirt cannot override an instance type's CPU/memory, so the chart drops the -// instance type only for a fully specified pair and rejects a half-filled block -// at render time. Binding the two here turns that into a plan-time error. -func k8sNodeGroupResourcesAttribute() rschema.SingleNestedAttribute { - alsoRequires := func(sibling string) []validator.String { - return []validator.String{ - stringvalidator.AlsoRequires(path.MatchRelative().AtParent().AtName(sibling)), - } - } - - return rschema.SingleNestedAttribute{ - Optional: true, - MarkdownDescription: "Explicit CPU and memory for each worker node, replacing `instance_type` sizing. " + - "Set both `cpu` and `memory`, or neither.", - Attributes: map[string]rschema.Attribute{ - attrCPU: rschema.StringAttribute{ - Optional: true, - Validators: alsoRequires(attrMemory), - MarkdownDescription: "CPU per worker node (quantity, e.g. `4`). Requires `memory`.", - }, - attrMemory: rschema.StringAttribute{ - Optional: true, - Validators: alsoRequires(attrCPU), - MarkdownDescription: "Memory per worker node (quantity, e.g. `8Gi`). Requires `cpu`.", - }, - }, - } -} - // k8sTalosResourceAttribute returns the talos worker-image block. None of its // fields carries a provider-side default: upstream moves the Talos release and // the tested schematic with every Cozystack release, and a materialised default @@ -154,41 +68,6 @@ func k8sTalosDataSourceAttribute() dsschema.SingleNestedAttribute { } } -// k8sNodeHealthCheckResourceAttribute returns the MachineHealthCheck tuning -// block. Like every 1.6 block it is left to the platform while unset. -func k8sNodeHealthCheckResourceAttribute() rschema.SingleNestedAttribute { - return rschema.SingleNestedAttribute{ - Optional: true, - MarkdownDescription: "MachineHealthCheck tuning applied to every worker node group. " + - "Follows the platform defaults while unset.", - Attributes: map[string]rschema.Attribute{ - "max_unhealthy": rschema.StringAttribute{ - Optional: true, - MarkdownDescription: "Unhealthy nodes tolerated per node group before remediation pauses. " + - "The MachineHealthCheck admission webhook takes a bare integer (`\"1\"`) or a percentage " + - "(`\"50%\"`); a percentage is the safer form. Follows the platform default (`50%`) while unset.", - }, - "node_startup_timeout": rschema.StringAttribute{ - Optional: true, - MarkdownDescription: "How long a Machine may take to reach Ready before it is remediated " + - "(duration, e.g. `20m`). Raise it for slow first boots — a Talos image fetch from the " + - "image factory, or a busy StorageClass. Follows the platform default (`10m`) while unset.", - }, - }, - } -} - -func k8sNodeHealthCheckDataSourceAttribute() dsschema.SingleNestedAttribute { - return dsschema.SingleNestedAttribute{ - Computed: true, - MarkdownDescription: "MachineHealthCheck tuning applied to every worker node group.", - Attributes: map[string]dsschema.Attribute{ - "max_unhealthy": dsschema.StringAttribute{Computed: true, MarkdownDescription: "Unhealthy nodes tolerated per node group."}, - "node_startup_timeout": dsschema.StringAttribute{Computed: true, MarkdownDescription: "Machine startup timeout before remediation."}, - }, - } -} - // k8sOIDCResourceAttribute returns the tenant kube-apiserver identity block. func k8sOIDCResourceAttribute() rschema.SingleNestedAttribute { return rschema.SingleNestedAttribute{ @@ -444,12 +323,10 @@ func kubernetesSchema() rschema.Schema { MarkdownDescription: "External hostname for the cluster. " + "Defaults to `.`.", }, - "node_groups": k8sNodeGroupsResourceAttribute(), - attrTalos: k8sTalosResourceAttribute(), - attrNodeHealthCheck: k8sNodeHealthCheckResourceAttribute(), - attrOIDC: k8sOIDCResourceAttribute(), - attrControlPlane: k8sControlPlaneResourceAttribute(), - attrImages: k8sImagesResourceAttribute(), + attrTalos: k8sTalosResourceAttribute(), + attrOIDC: k8sOIDCResourceAttribute(), + attrControlPlane: k8sControlPlaneResourceAttribute(), + attrImages: k8sImagesResourceAttribute(), "kubeconfig": rschema.StringAttribute{ Computed: true, Sensitive: true, @@ -463,9 +340,9 @@ func kubernetesSchema() rschema.Schema { return rschema.Schema{ MarkdownDescription: "A Cozystack managed Kubernetes cluster, deployed inside a tenant namespace. " + - "The addons block, the control-plane component sizing, and per-node-group GPU and kubelet " + - "tuning use server defaults.\n\n" + - "In the `talos`, `node_health_check`, `oidc`, `control_plane`, and `images` blocks this " + + "The addons block and the control-plane component sizing use server defaults. Worker pools " + + "are separate `cozystack_kubernetes_nodes` resources.\n\n" + + "In the `talos`, `oidc`, `control_plane`, and `images` blocks this " + "resource tracks only what the configuration sets. A field left unset stays out of every " + "request and out of state, so the platform's own default applies and keeps moving with the " + "platform instead of being pinned at apply time; a field that is set refreshes normally, so " + @@ -485,28 +362,10 @@ func kubernetesDataSourceSchema() dsschema.Schema { attrStorageClass: dsschema.StringAttribute{Computed: true, MarkdownDescription: "StorageClass used to store the data."}, attrVersion: dsschema.StringAttribute{Computed: true, MarkdownDescription: "Kubernetes version."}, attrHost: dsschema.StringAttribute{Computed: true, MarkdownDescription: "External hostname for the cluster."}, - "node_groups": dsschema.MapNestedAttribute{ - Computed: true, - MarkdownDescription: "Worker node groups keyed by name.", - NestedObject: dsschema.NestedAttributeObject{ - Attributes: map[string]dsschema.Attribute{ - "disk_size": dsschema.StringAttribute{Computed: true, MarkdownDescription: "Persistent disk size."}, - "instance_type": dsschema.StringAttribute{Computed: true, MarkdownDescription: "Instance type."}, - "min_replicas": dsschema.Int64Attribute{Computed: true, MarkdownDescription: "Minimum replicas."}, - "max_replicas": dsschema.Int64Attribute{Computed: true, MarkdownDescription: "Maximum replicas."}, - "roles": dsschema.ListAttribute{Computed: true, ElementType: types.StringType, MarkdownDescription: "Node roles."}, - "storage_class": dsschema.StringAttribute{Computed: true, MarkdownDescription: "Worker node StorageClass."}, - "resources": resourcesDataSourceAttribute(), - "max_unhealthy": dsschema.StringAttribute{Computed: true, MarkdownDescription: "Per-group unhealthy-node tolerance."}, - "node_startup_timeout": dsschema.StringAttribute{Computed: true, MarkdownDescription: "Per-group machine startup timeout."}, - }, - }, - }, - attrTalos: k8sTalosDataSourceAttribute(), - attrNodeHealthCheck: k8sNodeHealthCheckDataSourceAttribute(), - attrOIDC: k8sOIDCDataSourceAttribute(), - attrControlPlane: k8sControlPlaneDataSourceAttribute(), - attrImages: k8sImagesDataSourceAttribute(), + attrTalos: k8sTalosDataSourceAttribute(), + attrOIDC: k8sOIDCDataSourceAttribute(), + attrControlPlane: k8sControlPlaneDataSourceAttribute(), + attrImages: k8sImagesDataSourceAttribute(), "kubeconfig": dsschema.StringAttribute{ Computed: true, Sensitive: true, diff --git a/internal/provider/kubernetes_schema_test.go b/internal/provider/kubernetes_schema_test.go index b8ecd3c..73041c6 100644 --- a/internal/provider/kubernetes_schema_test.go +++ b/internal/provider/kubernetes_schema_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -47,98 +46,6 @@ func kubernetesConfig(ctx context.Context, t *testing.T, model kubernetesModel) return tfsdk.Config{Schema: kubernetesSchema(), Raw: kubernetesRaw(ctx, t, model)} } -// nodeGroupWithResources rebuilds the fixture's md0 group with an explicit -// resources object. -func nodeGroupWithResources(t *testing.T, model kubernetesModel, resources types.Object) kubernetesModel { - t.Helper() - - group, _ := model.NodeGroups.Elements()["md0"].(types.Object) - - attributes := map[string]attr.Value{} - for name, value := range group.Attributes() { - attributes[name] = value - } - - attributes["resources"] = resources - - model.NodeGroups = types.MapValueMust( - types.ObjectType{AttrTypes: k8sNodeGroupObjectType()}, - map[string]attr.Value{"md0": types.ObjectValueMust(k8sNodeGroupObjectType(), attributes)}, - ) - - return model -} - -// validateNodeGroupCPU runs the validators declared on -// node_groups[*].resources.cpu against a config, the way the framework does. -func validateNodeGroupCPU(ctx context.Context, t *testing.T, config tfsdk.Config, value types.String) bool { - t.Helper() - - cpuPath := path.Root("node_groups").AtMapKey("md0").AtName("resources").AtName(attrCPU) - - attribute, diags := config.Schema.AttributeAtPath(ctx, cpuPath) - if diags.HasError() { - t.Fatalf("AttributeAtPath: %v", diags) - } - - stringAttribute, ok := attribute.(rschema.StringAttribute) - if !ok { - t.Fatalf("cpu attribute is %T, want a string attribute", attribute) - } - - request := validator.StringRequest{ - Path: cpuPath, - PathExpression: cpuPath.Expression(), - Config: config, - ConfigValue: value, - } - - failed := false - - for _, check := range stringAttribute.Validators { - response := &validator.StringResponse{} - check.ValidateString(ctx, request, response) - - if response.Diagnostics.HasError() { - failed = true - } - } - - return failed -} - -// Upstream sizes a node group by instance_type unless both cpu and memory are -// given, and fails the chart render on a half-filled block. The schema binds the -// two so that surfaces at plan time instead. -func TestKubernetesNodeGroupResourcesRequireBothOrNeither(t *testing.T) { - t.Parallel() - - cpuOnly := types.ObjectValueMust(resourcesObjectType(), map[string]attr.Value{ - attrCPU: types.StringValue("4"), - attrMemory: types.StringNull(), - }) - - ctx := context.Background() - - config := kubernetesConfig(ctx, t, nodeGroupWithResources(t, fullKubernetesModel(), cpuOnly)) - if !validateNodeGroupCPU(ctx, t, config, types.StringValue("4")) { - t.Errorf("cpu without memory validated cleanly, want an error") - } - - both := types.ObjectValueMust(resourcesObjectType(), map[string]attr.Value{ - attrCPU: types.StringValue("4"), - attrMemory: types.StringValue("8Gi"), - }) - - config = kubernetesConfig(ctx, t, nodeGroupWithResources(t, fullKubernetesModel(), both)) - if validateNodeGroupCPU(ctx, t, config, types.StringValue("4")) { - t.Errorf("cpu with memory produced an error, want it accepted") - } -} - -// The model binds to the schema by tfsdk tag; a tag that names no attribute (or -// an attribute with no tag) is a runtime failure the expand/flatten tests never -// reach. Round-tripping the full fixture through the schema catches it. func TestKubernetesResourceModelRoundTrip(t *testing.T) { t.Parallel() @@ -172,16 +79,8 @@ func TestKubernetesResourceModelRoundTrip(t *testing.T) { t.Fatalf("state.Get: %v", diags) } - if out.Talos.IsNull() || out.OIDC.IsNull() || out.NodeHealthCheck.IsNull() { - t.Errorf("nested blocks lost in the round trip: talos=%v oidc=%v node_health_check=%v", - out.Talos, out.OIDC, out.NodeHealthCheck) - } - - group, _ := out.NodeGroups.Elements()["md0"].(types.Object) - - timeout, _ := group.Attributes()["node_startup_timeout"].(types.String) - if timeout.ValueString() != "20m" { - t.Errorf("node group startup timeout = %q, want 20m", timeout.ValueString()) + if out.Talos.IsNull() || out.OIDC.IsNull() { + t.Errorf("nested blocks lost in the round trip: talos=%v oidc=%v", out.Talos, out.OIDC) } } @@ -345,8 +244,8 @@ func TestKubernetesDataSourceModelRoundTrip(t *testing.T) { t.Errorf("oidc.mode = %q, want System", oidc.ValueString()) } - if out.ControlPlane.IsNull() || out.Images.IsNull() || out.NodeHealthCheck.IsNull() { - t.Errorf("blocks lost through the data-source schema: control_plane=%v images=%v node_health_check=%v", - out.ControlPlane, out.Images, out.NodeHealthCheck) + if out.ControlPlane.IsNull() || out.Images.IsNull() { + t.Errorf("blocks lost through the data-source schema: control_plane=%v images=%v", + out.ControlPlane, out.Images) } } diff --git a/internal/provider/schema_helpers_test.go b/internal/provider/schema_helpers_test.go index 8439a03..e313787 100644 --- a/internal/provider/schema_helpers_test.go +++ b/internal/provider/schema_helpers_test.go @@ -13,14 +13,9 @@ import ( ) // storageClassReplaceExempt lists the storage class attributes this guard does -// not cover, by resource type and dotted attribute path. -// -// The kubernetes node-group class is deliberately mutable upstream: the field is -// optional and undefaulted there, so an immutability rule would block ever -// setting it on an existing node group. -var storageClassReplaceExempt = map[string]bool{ - "cozystack_kubernetes.node_groups.storage_class": true, -} +// not cover, by resource type and dotted attribute path. An entry belongs here +// only for a class upstream deliberately keeps mutable. +var storageClassReplaceExempt = map[string]bool{} // TestStorageClassRequiresReplace checks that every storage_class attribute, // nested ones included, plans a replacement when a configured value changes. A From d9ce3017c0321631a5576ab1de849dda7b81d0ec Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Thu, 8 Oct 2026 15:21:28 +0300 Subject: [PATCH 04/12] feat(kafka)!: drop the zookeeper block Cozystack moved Kafka to KRaft and migrates existing ZooKeeper clusters on upgrade; the API no longer defines zookeeper, yet the provider kept writing it. The block goes from the resource and the data source. Signed-off-by: Alexey Artamonov --- CHANGELOG.md | 1 + examples/resources/cozystack_kafka/resource.tf | 3 +-- internal/provider/acc_test.go | 1 - internal/provider/kafka_model.go | 10 ---------- internal/provider/kafka_model_test.go | 9 --------- internal/provider/kafka_schema.go | 8 +++----- 6 files changed, 5 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 019217f..ab74b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ To ship with the first Cozystack API pin that contains cozystack/cozystack#3936. - `cozystack_httpcache` (resource and data source) is removed, following Cozystack, which dropped the HTTPCache application and its ResourceDefinition: a cluster on this API no longer serves the kind. Remove the resource from your configuration and run `terraform state rm` for each instance before upgrading the provider. - `cozystack_kubernetes`: `node_groups` and `node_health_check` are removed (resource and data source). Cozystack moved worker pools out of the Kubernetes application into KubernetesNodes, and its upgrade migrates every existing node group, the default `md0` included, into a KubernetesNodes object named `-`. Drop both attributes from the configuration and bring each migrated pool under management with `terraform import cozystack_kubernetes_nodes. /-`. +- `cozystack_kafka`: `zookeeper` is removed (resource and data source). Cozystack runs Kafka in KRaft mode and migrates existing ZooKeeper clusters on upgrade, so the block has nothing left to configure. Drop it from the configuration. - `rabbitmq`: the default `resources_preset` moves from `t1.nano` to `s1.nano`. The attribute is optional-with-default, so the old value sits in existing state: after upgrading the provider, a `cozystack_rabbitmq` that never set it explicitly plans `t1.nano` → `s1.nano`, which updates the cluster and rolls its pods. Set `resources_preset = "t1.nano"` explicitly to keep the previous sizing. ## v1.6.3 diff --git a/examples/resources/cozystack_kafka/resource.tf b/examples/resources/cozystack_kafka/resource.tf index c9399a9..86c1f85 100644 --- a/examples/resources/cozystack_kafka/resource.tf +++ b/examples/resources/cozystack_kafka/resource.tf @@ -5,8 +5,7 @@ resource "cozystack_kafka" "events" { external = true tls = { enabled = true } - kafka = { replicas = 3, size = "10Gi" } - zookeeper = { replicas = 3, size = "5Gi" } + kafka = { replicas = 3, size = "10Gi" } topics = [ { name = "orders", partitions = 6, replicas = 3 }, diff --git a/internal/provider/acc_test.go b/internal/provider/acc_test.go index 0bad7a6..cf27ad5 100644 --- a/internal/provider/acc_test.go +++ b/internal/provider/acc_test.go @@ -855,7 +855,6 @@ resource "cozystack_kafka" "test" { name = "tfacckafka" namespace = "tenant-root" kafka = { replicas = 1 } - zookeeper = { replicas = 1 } topics = [ { name = "events", partitions = 1, replicas = 1 }, ] diff --git a/internal/provider/kafka_model.go b/internal/provider/kafka_model.go index 1ebe810..36eaaf0 100644 --- a/internal/provider/kafka_model.go +++ b/internal/provider/kafka_model.go @@ -20,7 +20,6 @@ type kafkaModel struct { TLS types.Object `tfsdk:"tls"` Topics types.List `tfsdk:"topics"` Kafka types.Object `tfsdk:"kafka"` - Zookeeper types.Object `tfsdk:"zookeeper"` Ready types.Bool `tfsdk:"ready"` ChartVersion types.String `tfsdk:"chart_version"` UID types.String `tfsdk:"uid"` @@ -88,9 +87,6 @@ func (m *kafkaModel) expand(ctx context.Context) (*client.Application, diag.Diag kafka, kDiags := expandKafkaBroker(ctx, m.Kafka) diags.Append(kDiags...) - zookeeper, zDiags := expandKafkaBroker(ctx, m.Zookeeper) - diags.Append(zDiags...) - if diags.HasError() { return nil, diags } @@ -99,7 +95,6 @@ func (m *kafkaModel) expand(ctx context.Context) (*client.Application, diag.Diag attrExternal: m.External.ValueBool(), "topics": topics, "kafka": kafka, - "zookeeper": zookeeper, } setOptionalTLS(spec, m.TLS) @@ -167,11 +162,6 @@ func (m *kafkaModel) flatten(app *client.Application) diag.Diagnostics { m.Kafka = kafka - zookeeper, zDiags := flattenKafkaBroker(app.Spec["zookeeper"]) - diags.Append(zDiags...) - - m.Zookeeper = zookeeper - m.Ready = types.BoolValue(app.Status.Ready) m.ChartVersion = types.StringValue(app.Status.Version) m.UID = types.StringValue(app.UID) diff --git a/internal/provider/kafka_model_test.go b/internal/provider/kafka_model_test.go index 42e6a2e..7f47776 100644 --- a/internal/provider/kafka_model_test.go +++ b/internal/provider/kafka_model_test.go @@ -24,7 +24,6 @@ func fullKafkaModel() kafkaModel { TLS: tlsBlock(true), Topics: types.ListValueMust(types.ObjectType{AttrTypes: kafkaTopicObjectType()}, []attr.Value{topic}), Kafka: kafkaBrokerDefault("10Gi"), - Zookeeper: kafkaBrokerDefault("5Gi"), } } @@ -52,11 +51,6 @@ func TestKafkaExpand_TopicsAndBrokers(t *testing.T) { if kafkaSpec["size"] != "10Gi" { t.Errorf("kafka.size = %v, want 10Gi", kafkaSpec["size"]) } - - zkSpec, _ := got.Spec["zookeeper"].(map[string]any) - if zkSpec["size"] != "5Gi" { - t.Errorf("zookeeper.size = %v, want 5Gi", zkSpec["size"]) - } } func TestKafkaFlatten_RoundTrip(t *testing.T) { @@ -71,9 +65,6 @@ func TestKafkaFlatten_RoundTrip(t *testing.T) { "kafka": map[string]any{ "replicas": int64(3), "resourcesPreset": "c1.small", "size": "10Gi", "storageClass": "", "resources": map[string]any{}, }, - "zookeeper": map[string]any{ - "replicas": int64(3), "resourcesPreset": "c1.small", "size": "5Gi", "storageClass": "", "resources": map[string]any{}, - }, }, Status: client.ApplicationStatus{Version: "1.0.0", Ready: true}, } diff --git a/internal/provider/kafka_schema.go b/internal/provider/kafka_schema.go index 4d44514..6d03924 100644 --- a/internal/provider/kafka_schema.go +++ b/internal/provider/kafka_schema.go @@ -86,9 +86,8 @@ func kafkaSchema() rschema.Schema { "always TLS, and Strimzi manages the cluster PKI itself. Omit the block to follow `external`; " + "disabling TLS while `external` is true publishes Kafka in plaintext on a public address.", ), - "topics": kafkaTopicsResourceAttribute(), - "kafka": kafkaBrokerResourceAttribute("Kafka broker configuration.", "10Gi"), - "zookeeper": kafkaBrokerResourceAttribute("ZooKeeper configuration.", "5Gi"), + "topics": kafkaTopicsResourceAttribute(), + "kafka": kafkaBrokerResourceAttribute("Kafka broker configuration.", "10Gi"), }) maps.Copy(attributes, statusResourceAttributes()) maps.Copy(attributes, waitBehaviorAttributes()) @@ -131,8 +130,7 @@ func kafkaDataSourceSchema() dsschema.Schema { }, }, }, - "kafka": kafkaBrokerDataSourceAttribute("Kafka broker configuration."), - "zookeeper": kafkaBrokerDataSourceAttribute("ZooKeeper configuration."), + "kafka": kafkaBrokerDataSourceAttribute("Kafka broker configuration."), }) maps.Copy(attributes, statusDataSourceAttributes()) From c81ef748574d6c2df3a001ade60c0214ebc9b690 Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Thu, 8 Oct 2026 15:23:16 +0300 Subject: [PATCH 05/12] feat(tenant): model the tracingCentral field Cozystack can keep a tenant's traces in the shared VictoriaTraces store in tenant-root instead of a per-tenant backend; the Tenant field that selects it had no attribute, so Terraform users could not turn it on. tracing_central follows monitoring, including its false default, which matches the chart schema. The prerequisites (the tenant's own monitoring, tenant-root hosting the store) are enforced by the chart at render time and are not repeated here. Signed-off-by: Alexey Artamonov --- CHANGELOG.md | 6 +++- .../resources/cozystack_tenant/resource.tf | 11 +++++++ internal/provider/tenant_data_source.go | 1 + internal/provider/tenant_data_source_test.go | 2 +- internal/provider/tenant_model.go | 24 ++++++++++----- internal/provider/tenant_model_test.go | 10 ++++++- internal/provider/tenant_resource.go | 6 +++- internal/provider/tenant_resource_test.go | 30 ++++++++++++++++++- 8 files changed, 78 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab74b0b..9364920 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ ## Unreleased -To ship with the first Cozystack API pin that contains cozystack/cozystack#3936. +Tracks the Cozystack API at main commit `11499c30e`, ahead of the first v1.7 tag: it is the first commit carrying the shared-central tracing field, and it already contains cozystack/cozystack#3936. + +### Features + +- `cozystack_tenant`: new `tracing_central` attribute (default `false`), also on the data source. It sends the tenant's traces to the shared VictoriaTraces store in `tenant-root` instead of a per-tenant backend, and needs `monitoring = true` on the tenant and `tenant-root` hosting the store. ### Breaking changes diff --git a/examples/resources/cozystack_tenant/resource.tf b/examples/resources/cozystack_tenant/resource.tf index c6aebb8..2c9c0b3 100644 --- a/examples/resources/cozystack_tenant/resource.tf +++ b/examples/resources/cozystack_tenant/resource.tf @@ -31,3 +31,14 @@ resource "cozystack_tenant" "team_c" { host = "team-c.example.com" gateway = true } + +# Keep the tenant's traces in the shared store in tenant-root rather than in a +# store of its own. It needs the tenant's own monitoring, and tenant-root must +# host the store. +resource "cozystack_tenant" "team_d" { + name = "team-d" + namespace = "tenant-root" + + monitoring = true + tracing_central = true +} diff --git a/internal/provider/tenant_data_source.go b/internal/provider/tenant_data_source.go index 0591502..b16f4db 100644 --- a/internal/provider/tenant_data_source.go +++ b/internal/provider/tenant_data_source.go @@ -54,6 +54,7 @@ func (d *tenantDataSource) Schema( attrHost: schema.StringAttribute{Computed: true, MarkdownDescription: "Hostname used to access tenant services."}, attrEtcd: schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether a dedicated etcd cluster is deployed."}, attrMonitoring: schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether a dedicated monitoring stack is deployed."}, + attrTracingCentral: schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether the tenant's traces go to the shared store in `tenant-root`."}, attrIngress: schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether a dedicated ingress controller is deployed."}, "gateway": schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether the tenant has a Gateway of its own; null when it never asked."}, attrSeaweedfs: schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether a dedicated SeaweedFS instance is deployed."}, diff --git a/internal/provider/tenant_data_source_test.go b/internal/provider/tenant_data_source_test.go index dc057f8..6b5b873 100644 --- a/internal/provider/tenant_data_source_test.go +++ b/internal/provider/tenant_data_source_test.go @@ -44,7 +44,7 @@ func TestTenantDataSourceSchema(t *testing.T) { } computed := []string{ - "id", "host", "etcd", "monitoring", "ingress", "seaweedfs", + "id", "host", "etcd", "monitoring", "tracing_central", "ingress", "seaweedfs", "scheduling_class", "resource_quotas", "status_namespace", "ready", "version", } diff --git a/internal/provider/tenant_model.go b/internal/provider/tenant_model.go index bbf161e..b9429a1 100644 --- a/internal/provider/tenant_model.go +++ b/internal/provider/tenant_model.go @@ -20,6 +20,13 @@ const ( attrSeaweedfs = "seaweedfs" ) +// tracing_central is the one tenant toggle whose attribute name and spec key +// differ. +const ( + attrTracingCentral = "tracing_central" + specTracingCentral = "tracingCentral" +) + // tenantModel maps the common cozystack_tenant attributes to Go types. The spec // attributes mirror the json tags of the pinned tenant.ConfigSpec one-to-one. // The resource embeds this model and adds its own behaviour attributes. @@ -30,6 +37,7 @@ type tenantModel struct { Host types.String `tfsdk:"host"` Etcd types.Bool `tfsdk:"etcd"` Monitoring types.Bool `tfsdk:"monitoring"` + TracingCentral types.Bool `tfsdk:"tracing_central"` Ingress types.Bool `tfsdk:"ingress"` Gateway types.Bool `tfsdk:"gateway"` Seaweedfs types.Bool `tfsdk:"seaweedfs"` @@ -58,13 +66,14 @@ func (m *tenantModel) expand(ctx context.Context) (*client.Application, diag.Dia } spec := map[string]any{ - attrHost: m.Host.ValueString(), - attrEtcd: m.Etcd.ValueBool(), - attrMonitoring: m.Monitoring.ValueBool(), - attrIngress: m.Ingress.ValueBool(), - attrSeaweedfs: m.Seaweedfs.ValueBool(), - "schedulingClass": m.SchedulingClass.ValueString(), - "resourceQuotas": quotas, + attrHost: m.Host.ValueString(), + attrEtcd: m.Etcd.ValueBool(), + attrMonitoring: m.Monitoring.ValueBool(), + specTracingCentral: m.TracingCentral.ValueBool(), + attrIngress: m.Ingress.ValueBool(), + attrSeaweedfs: m.Seaweedfs.ValueBool(), + "schedulingClass": m.SchedulingClass.ValueString(), + "resourceQuotas": quotas, } // gateway is emitted only when the practitioner set it. The chart resolves @@ -95,6 +104,7 @@ func (m *tenantModel) flatten(tenant *client.Application) diag.Diagnostics { m.Host = types.StringValue(specString(tenant.Spec, attrHost)) m.Etcd = types.BoolValue(specBool(tenant.Spec, attrEtcd)) m.Monitoring = types.BoolValue(specBool(tenant.Spec, attrMonitoring)) + m.TracingCentral = types.BoolValue(specBool(tenant.Spec, specTracingCentral)) m.Ingress = types.BoolValue(specBool(tenant.Spec, attrIngress)) m.Seaweedfs = types.BoolValue(specBool(tenant.Spec, attrSeaweedfs)) diff --git a/internal/provider/tenant_model_test.go b/internal/provider/tenant_model_test.go index a102431..75002de 100644 --- a/internal/provider/tenant_model_test.go +++ b/internal/provider/tenant_model_test.go @@ -19,6 +19,7 @@ func fullModel() tenantModel { Host: types.StringValue("dev.example.test"), Etcd: types.BoolValue(true), Monitoring: types.BoolValue(true), + TracingCentral: types.BoolValue(true), Ingress: types.BoolValue(false), Gateway: types.BoolValue(true), Seaweedfs: types.BoolValue(false), @@ -52,6 +53,9 @@ func TestExpand_CoreFields(t *testing.T) { if got.Spec["ingress"] != false { t.Errorf("spec.ingress = %v, want false", got.Spec["ingress"]) } + if got.Spec["tracingCentral"] != true { + t.Errorf("spec.tracingCentral = %v, want true", got.Spec["tracingCentral"]) + } if got.Spec["schedulingClass"] != "default" { t.Errorf("spec.schedulingClass = %v, want default", got.Spec["schedulingClass"]) } @@ -172,6 +176,7 @@ func TestFlatten_RoundTrip(t *testing.T) { "host": "dev.example.test", "etcd": true, "monitoring": false, + "tracingCentral": true, "ingress": true, "seaweedfs": false, "schedulingClass": "default", @@ -200,6 +205,9 @@ func TestFlatten_RoundTrip(t *testing.T) { if !model.Etcd.ValueBool() || model.Monitoring.ValueBool() { t.Errorf("etcd/monitoring = %v/%v, want true/false", model.Etcd.ValueBool(), model.Monitoring.ValueBool()) } + if !model.TracingCentral.ValueBool() { + t.Errorf("tracing_central = false, want true") + } if !model.Ready.ValueBool() { t.Errorf("ready = false, want true") } @@ -231,7 +239,7 @@ func TestFlatten_MissingBoolDefaultsFalse(t *testing.T) { t.Fatalf("flatten diagnostics: %v", diags) } - if model.Etcd.ValueBool() || model.Monitoring.ValueBool() { + if model.Etcd.ValueBool() || model.Monitoring.ValueBool() || model.TracingCentral.ValueBool() { t.Errorf("missing bools should default to false") } if model.Host.ValueString() != "" { diff --git a/internal/provider/tenant_resource.go b/internal/provider/tenant_resource.go index 1c85bd9..4df0bc6 100644 --- a/internal/provider/tenant_resource.go +++ b/internal/provider/tenant_resource.go @@ -172,7 +172,11 @@ func tenantSpecAttributes() map[string]schema.Attribute { }, attrEtcd: boolToggle("Deploy a dedicated etcd cluster for the tenant."), attrMonitoring: boolToggle("Deploy a dedicated monitoring stack for the tenant."), - attrIngress: boolToggle("Deploy a dedicated ingress controller for the tenant."), + attrTracingCentral: boolToggle("Send the tenant's traces to the shared VictoriaTraces store in " + + "`tenant-root` instead of a per-tenant backend. Tenants sharing the store cannot read each " + + "other's traces. Requires `monitoring = true` on this tenant and `tenant-root` hosting the " + + "store; without them the tenant's monitoring fails to render."), + attrIngress: boolToggle("Deploy a dedicated ingress controller for the tenant."), "gateway": schema.BoolAttribute{ Optional: true, MarkdownDescription: "Give the tenant a Gateway API Gateway of its own, with its own Service, " + diff --git a/internal/provider/tenant_resource_test.go b/internal/provider/tenant_resource_test.go index c6dede7..9bd03e5 100644 --- a/internal/provider/tenant_resource_test.go +++ b/internal/provider/tenant_resource_test.go @@ -7,6 +7,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -59,7 +61,9 @@ func TestTenantSchemaAttributes(t *testing.T) { } } - optionalComputed := []string{"host", "etcd", "monitoring", "ingress", "seaweedfs", "scheduling_class", "resource_quotas"} + optionalComputed := []string{ + "host", "etcd", "monitoring", "tracing_central", "ingress", "seaweedfs", "scheduling_class", "resource_quotas", + } for _, name := range optionalComputed { attribute, ok := attributes[name] if !ok { @@ -203,3 +207,27 @@ func TestTenantResourceModelRoundTrip(t *testing.T) { t.Errorf("wait attributes round-trip lost: %v/%q", out.WaitForReady.ValueBool(), out.WaitTimeout.ValueString()) } } + +// The provider restates the chart's default rather than leaving the key to the +// server, so a default that drifts from the schema's false would be written +// into every tenant that never set the attribute. +func TestTenantSchema_TracingCentralDefaultsFalse(t *testing.T) { + t.Parallel() + + attribute, ok := tenantSchema().Attributes["tracing_central"].(schema.BoolAttribute) + if !ok { + t.Fatalf("tracing_central is not a bool attribute") + } + + if attribute.Default == nil { + t.Fatalf("tracing_central has no default") + } + + var resp defaults.BoolResponse + + attribute.Default.DefaultBool(context.Background(), defaults.BoolRequest{}, &resp) + + if resp.PlanValue.IsNull() || resp.PlanValue.ValueBool() { + t.Errorf("tracing_central default = %v, want false", resp.PlanValue) + } +} From bf5b9b1c8c819c2b0e9c94ca9529d80036ef8ab6 Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Thu, 8 Oct 2026 15:24:16 +0300 Subject: [PATCH 06/12] test: let the spec guards skip the fields the new pin adds The pin brings optional fields this change does not model. Each guard names them in its omit list, so the guard still fails on any other field the API adds and can be tightened field by field as they are modelled. The tenant, redis and rabbitmq guards move onto assertSpecCoverage to get an omit list at all. Signed-off-by: Alexey Artamonov --- internal/provider/harbor_model_test.go | 3 ++- internal/provider/kafka_model_test.go | 3 ++- internal/provider/kubernetes_model_test.go | 14 +++++++++----- internal/provider/kubernetes_nodes_model_test.go | 11 ++++++++--- internal/provider/mariadb_model_test.go | 5 +++-- internal/provider/opensearch_model_test.go | 3 ++- internal/provider/postgresql_model_test.go | 3 ++- internal/provider/rabbitmq_model_test.go | 15 ++------------- internal/provider/redis_model_test.go | 15 ++------------- internal/provider/tenant_model_test.go | 15 ++------------- internal/provider/vmdisk_model_test.go | 3 ++- internal/provider/vminstance_model_test.go | 3 ++- 12 files changed, 38 insertions(+), 55 deletions(-) diff --git a/internal/provider/harbor_model_test.go b/internal/provider/harbor_model_test.go index 65209a2..cdecbad 100644 --- a/internal/provider/harbor_model_test.go +++ b/internal/provider/harbor_model_test.go @@ -92,6 +92,7 @@ func TestHarborExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } + // managementWhitelist was added by the pinned API and is not modelled yet. assertSpecCoverage(t, emitted, harbor.ConfigSpec{}, - "core", "registry", "jobservice", "trivy", "database", "redis") + "core", "registry", "jobservice", "trivy", "database", "redis", "managementWhitelist") } diff --git a/internal/provider/kafka_model_test.go b/internal/provider/kafka_model_test.go index 7f47776..64808e7 100644 --- a/internal/provider/kafka_model_test.go +++ b/internal/provider/kafka_model_test.go @@ -104,5 +104,6 @@ func TestKafkaExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - assertSpecCoverage(t, emitted, kafka.ConfigSpec{}) + // Added by the pinned API and not modelled yet. + assertSpecCoverage(t, emitted, kafka.ConfigSpec{}, "version") } diff --git a/internal/provider/kubernetes_model_test.go b/internal/provider/kubernetes_model_test.go index 67b4750..94a46eb 100644 --- a/internal/provider/kubernetes_model_test.go +++ b/internal/provider/kubernetes_model_test.go @@ -226,7 +226,8 @@ func TestKubernetesExpandTalosKeysMatchTalosSpec(t *testing.T) { t.Fatalf("expand diagnostics: %v", diags) } - assertSpecCoverage(t, nestedSpecKeys(t, got.Spec, "talos"), kubernetes.Talos{}) + // registryMirrors was added by the pinned API and is not modelled yet. + assertSpecCoverage(t, nestedSpecKeys(t, got.Spec, "talos"), kubernetes.Talos{}, "registryMirrors") } func TestKubernetesExpand_OIDCOmittedWhenUnset(t *testing.T) { @@ -461,11 +462,13 @@ func TestKubernetesExpandControlPlaneKeysMatchSpec(t *testing.T) { } // The sizing of the control-plane components stays with the server, as it - // did before controlPlane was modelled at all. + // did before controlPlane was modelled at all. controlPlane.kubelet and + // apiServer.admissionControllers were added by the pinned API and are not + // modelled yet. assertSpecCoverage(t, nestedSpecKeys(t, got.Spec, "controlPlane"), kubernetes.ControlPlane{}, - "controllerManager", "konnectivity", "replicas", "scheduler") + "controllerManager", "konnectivity", "replicas", "scheduler", "kubelet") assertSpecCoverage(t, nestedSpecKeys(t, got.Spec, "controlPlane", "apiServer"), kubernetes.APIServer{}, - "resources", "resourcesPreset") + "resources", "resourcesPreset", "admissionControllers") } // The image overrides exist for air-gapped and rate-limited registries. Empty @@ -577,7 +580,8 @@ func TestKubernetesExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - assertSpecCoverage(t, emitted, kubernetes.ConfigSpec{}, "addons") + // substrate and proxmox were added by the pinned API and are not modelled yet. + assertSpecCoverage(t, emitted, kubernetes.ConfigSpec{}, "addons", "substrate", "proxmox") } func TestKubernetesResourceFlatten_KeepsUnconfiguredBlocksOut(t *testing.T) { diff --git a/internal/provider/kubernetes_nodes_model_test.go b/internal/provider/kubernetes_nodes_model_test.go index f30f850..62a8b98 100644 --- a/internal/provider/kubernetes_nodes_model_test.go +++ b/internal/provider/kubernetes_nodes_model_test.go @@ -78,7 +78,10 @@ func TestKubernetesNodesExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - assertSpecCoverage(t, emitted, kubernetesnodes.ConfigSpec{}) + // Added by the pinned API and not modelled yet. + assertSpecCoverage(t, emitted, kubernetesnodes.ConfigSpec{}, + "substrate", "proxmox", "osImage", "kernelModules", "logSerialConsole", "podCpuLimit", "podCpuRequest", + "readyUnknownTimeout", "readyFalseTimeout", "maxNodeProvisionTime") } // The top-level guard reflects one level of json tags, so the talos, kubelet, @@ -104,8 +107,10 @@ func TestKubernetesNodesExpandNestedKeysMatchTheirSpecs(t *testing.T) { block string raw any spec any + omit []string }{ - {block: "talos", raw: got.Spec["talos"], spec: kubernetesnodes.Talos{}}, + // registryMirrors was added by the pinned API and is not modelled yet. + {block: "talos", raw: got.Spec["talos"], spec: kubernetesnodes.Talos{}, omit: []string{"registryMirrors"}}, {block: "kubelet", raw: got.Spec["kubelet"], spec: kubernetesnodes.Kubelet{}}, {block: "images", raw: got.Spec["images"], spec: kubernetesnodes.Images{}}, {block: "gpus[0]", raw: gpus[0], spec: kubernetesnodes.GPU{}}, @@ -125,7 +130,7 @@ func TestKubernetesNodesExpandNestedKeysMatchTheirSpecs(t *testing.T) { emitted[key] = true } - assertSpecCoverage(t, emitted, tt.spec) + assertSpecCoverage(t, emitted, tt.spec, tt.omit...) }) } } diff --git a/internal/provider/mariadb_model_test.go b/internal/provider/mariadb_model_test.go index ace4a48..4f123ef 100644 --- a/internal/provider/mariadb_model_test.go +++ b/internal/provider/mariadb_model_test.go @@ -102,6 +102,7 @@ func TestMariadbExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - // The deprecated inline backup block is intentionally not managed. - assertSpecCoverage(t, emitted, mariadb.ConfigSpec{}, "backup") + // The deprecated inline backup block is intentionally not managed; tls was + // added by the pinned API and is not modelled yet. + assertSpecCoverage(t, emitted, mariadb.ConfigSpec{}, "backup", "tls") } diff --git a/internal/provider/opensearch_model_test.go b/internal/provider/opensearch_model_test.go index 2e66735..1909256 100644 --- a/internal/provider/opensearch_model_test.go +++ b/internal/provider/opensearch_model_test.go @@ -100,5 +100,6 @@ func TestOpensearchExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - assertSpecCoverage(t, emitted, opensearch.ConfigSpec{}, "images", "nodeRoles", "dashboards") + // tls was added by the pinned API and is not modelled yet. + assertSpecCoverage(t, emitted, opensearch.ConfigSpec{}, "images", "nodeRoles", "dashboards", "tls") } diff --git a/internal/provider/postgresql_model_test.go b/internal/provider/postgresql_model_test.go index 2d00177..d8d4e5f 100644 --- a/internal/provider/postgresql_model_test.go +++ b/internal/provider/postgresql_model_test.go @@ -105,5 +105,6 @@ func TestPostgresqlExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - assertSpecCoverage(t, emitted, postgresql.ConfigSpec{}, "postgresql", "quorum", "bootstrap") + // autoscaling was added by the pinned API and is not modelled yet. + assertSpecCoverage(t, emitted, postgresql.ConfigSpec{}, "postgresql", "quorum", "bootstrap", "autoscaling") } diff --git a/internal/provider/rabbitmq_model_test.go b/internal/provider/rabbitmq_model_test.go index 46b724b..7078ed9 100644 --- a/internal/provider/rabbitmq_model_test.go +++ b/internal/provider/rabbitmq_model_test.go @@ -165,17 +165,6 @@ func TestRabbitmqExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - tags := configSpecJSONTags(rabbitmq.ConfigSpec{}) - - for key := range emitted { - if !tags[key] { - t.Errorf("expand emits %q, not a rabbitmq.ConfigSpec json tag", key) - } - } - - for tag := range tags { - if !emitted[tag] { - t.Errorf("rabbitmq.ConfigSpec has %q but expand does not emit it", tag) - } - } + // Added by the pinned API and not modelled yet. + assertSpecCoverage(t, emitted, rabbitmq.ConfigSpec{}, "tls") } diff --git a/internal/provider/redis_model_test.go b/internal/provider/redis_model_test.go index a9f510b..937f4ad 100644 --- a/internal/provider/redis_model_test.go +++ b/internal/provider/redis_model_test.go @@ -178,17 +178,6 @@ func TestRedisExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - tags := configSpecJSONTags(redis.ConfigSpec{}) - - for key := range emitted { - if !tags[key] { - t.Errorf("expand emits %q, which is not a redis.ConfigSpec json tag", key) - } - } - - for tag := range tags { - if !emitted[tag] { - t.Errorf("redis.ConfigSpec has %q but expand does not emit it", tag) - } - } + // Added by the pinned API and not modelled yet. + assertSpecCoverage(t, emitted, redis.ConfigSpec{}, "tls") } diff --git a/internal/provider/tenant_model_test.go b/internal/provider/tenant_model_test.go index 75002de..299c9a9 100644 --- a/internal/provider/tenant_model_test.go +++ b/internal/provider/tenant_model_test.go @@ -265,19 +265,8 @@ func TestExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - tags := configSpecJSONTags(tenant.ConfigSpec{}) - - for key := range emitted { - if !tags[key] { - t.Errorf("expand emits %q, which is not a tenant.ConfigSpec json tag", key) - } - } - - for tag := range tags { - if !emitted[tag] { - t.Errorf("tenant.ConfigSpec has %q but expand does not emit it", tag) - } - } + // Added by the pinned API and not modelled yet. + assertSpecCoverage(t, emitted, tenant.ConfigSpec{}, "gatewayClass", "computeplane") } // assertSpecCoverage checks that every emitted spec key is a valid ConfigSpec diff --git a/internal/provider/vmdisk_model_test.go b/internal/provider/vmdisk_model_test.go index cf1729e..ce63408 100644 --- a/internal/provider/vmdisk_model_test.go +++ b/internal/provider/vmdisk_model_test.go @@ -100,5 +100,6 @@ func TestVMDiskExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - assertSpecCoverage(t, emitted, vmdisk.ConfigSpec{}) + // Added by the pinned API and not modelled yet. + assertSpecCoverage(t, emitted, vmdisk.ConfigSpec{}, "bindImmediately") } diff --git a/internal/provider/vminstance_model_test.go b/internal/provider/vminstance_model_test.go index 35d2b74..f91359a 100644 --- a/internal/provider/vminstance_model_test.go +++ b/internal/provider/vminstance_model_test.go @@ -123,5 +123,6 @@ func TestVMInstanceExpandKeysMatchConfigSpec(t *testing.T) { emitted[key] = true } - assertSpecCoverage(t, emitted, vminstance.ConfigSpec{}, "subnets") + // firmware was added by the pinned API and is not modelled yet. + assertSpecCoverage(t, emitted, vminstance.ConfigSpec{}, "subnets", "firmware") } From 6078adb7e3942f6ac4a30abd5c68c96bd0d1385b Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Thu, 8 Oct 2026 15:27:57 +0300 Subject: [PATCH 07/12] docs: regenerate for the API pin make docs output for the tenant, kubernetes and kafka schema changes. Signed-off-by: Alexey Artamonov --- docs/data-sources/kafka.md | 21 -------- docs/data-sources/kubernetes.md | 36 ------------- docs/data-sources/tenant.md | 1 + docs/resources/kafka.md | 24 +-------- docs/resources/kubernetes.md | 86 +++--------------------------- docs/resources/kubernetes_nodes.md | 11 +--- docs/resources/tenant.md | 12 +++++ 7 files changed, 21 insertions(+), 170 deletions(-) diff --git a/docs/data-sources/kafka.md b/docs/data-sources/kafka.md index 7f9b0ad..fdb0986 100644 --- a/docs/data-sources/kafka.md +++ b/docs/data-sources/kafka.md @@ -37,7 +37,6 @@ data "cozystack_kafka" "events" { - `tls` (Attributes) TLS configuration for the external listener. (see [below for nested schema](#nestedatt--tls)) - `topics` (Attributes List) Provisioned topics. (see [below for nested schema](#nestedatt--topics)) - `uid` (String) Server-assigned object UID (`metadata.uid`). -- `zookeeper` (Attributes) ZooKeeper configuration. (see [below for nested schema](#nestedatt--zookeeper)) ### Nested Schema for `kafka` @@ -76,23 +75,3 @@ Read-Only: - `name` (String) Topic name. - `partitions` (Number) Number of partitions. - `replicas` (Number) Number of replicas. - - - -### Nested Schema for `zookeeper` - -Read-Only: - -- `replicas` (Number) Number of replicas. -- `resources` (Attributes) Explicit CPU and memory per replica, when set. (see [below for nested schema](#nestedatt--zookeeper--resources)) -- `resources_preset` (String) Sizing preset. -- `size` (String) Persistent volume size. -- `storage_class` (String) StorageClass used to store the data. - - -### Nested Schema for `zookeeper.resources` - -Read-Only: - -- `cpu` (String) CPU available to each replica. -- `memory` (String) Memory available to each replica. diff --git a/docs/data-sources/kubernetes.md b/docs/data-sources/kubernetes.md index 8d24309..ccaa48c 100644 --- a/docs/data-sources/kubernetes.md +++ b/docs/data-sources/kubernetes.md @@ -45,8 +45,6 @@ output "cluster_oidc_mode" { - `id` (String) Synthetic identifier `namespace/name`. - `images` (Attributes) Image overrides for air-gapped or rate-limited registries. (see [below for nested schema](#nestedatt--images)) - `kubeconfig` (String, Sensitive) Admin kubeconfig for the cluster. -- `node_groups` (Attributes Map) Worker node groups keyed by name. (see [below for nested schema](#nestedatt--node_groups)) -- `node_health_check` (Attributes) MachineHealthCheck tuning applied to every worker node group. (see [below for nested schema](#nestedatt--node_health_check)) - `oidc` (Attributes) OIDC authentication and per-user RBAC for the tenant kube-apiserver. (see [below for nested schema](#nestedatt--oidc)) - `ready` (Boolean) Whether the application's `Ready` condition is true. - `storage_class` (String) StorageClass used to store the data. @@ -82,40 +80,6 @@ Read-Only: - `wait_for_kubeconfig` (String) wait-for-kubeconfig init container image. - -### Nested Schema for `node_groups` - -Read-Only: - -- `disk_size` (String) Persistent disk size. -- `instance_type` (String) Instance type. -- `max_replicas` (Number) Maximum replicas. -- `max_unhealthy` (String) Per-group unhealthy-node tolerance. -- `min_replicas` (Number) Minimum replicas. -- `node_startup_timeout` (String) Per-group machine startup timeout. -- `resources` (Attributes) Explicit CPU and memory per replica, when set. (see [below for nested schema](#nestedatt--node_groups--resources)) -- `roles` (List of String) Node roles. -- `storage_class` (String) Worker node StorageClass. - - -### Nested Schema for `node_groups.resources` - -Read-Only: - -- `cpu` (String) CPU available to each replica. -- `memory` (String) Memory available to each replica. - - - - -### Nested Schema for `node_health_check` - -Read-Only: - -- `max_unhealthy` (String) Unhealthy nodes tolerated per node group. -- `node_startup_timeout` (String) Machine startup timeout before remediation. - - ### Nested Schema for `oidc` diff --git a/docs/data-sources/tenant.md b/docs/data-sources/tenant.md index af7331d..6c1ea9c 100644 --- a/docs/data-sources/tenant.md +++ b/docs/data-sources/tenant.md @@ -49,5 +49,6 @@ output "root_tenant_gateway" { - `scheduling_class` (String) Name of the applied SchedulingClass CR. - `seaweedfs` (Boolean) Whether a dedicated SeaweedFS instance is deployed. - `status_namespace` (String) Namespace created for the tenant. +- `tracing_central` (Boolean) Whether the tenant's traces go to the shared store in `tenant-root`. - `uid` (String) Server-assigned object UID (`metadata.uid`). - `version` (String) Deployed chart version. diff --git a/docs/resources/kafka.md b/docs/resources/kafka.md index 13b17f1..8dd09ca 100644 --- a/docs/resources/kafka.md +++ b/docs/resources/kafka.md @@ -20,8 +20,7 @@ resource "cozystack_kafka" "events" { external = true tls = { enabled = true } - kafka = { replicas = 3, size = "10Gi" } - zookeeper = { replicas = 3, size = "5Gi" } + kafka = { replicas = 3, size = "10Gi" } topics = [ { name = "orders", partitions = 6, replicas = 3 }, @@ -46,7 +45,6 @@ resource "cozystack_kafka" "events" { - `topics` (Attributes List) Topics to provision. (see [below for nested schema](#nestedatt--topics)) - `wait_for_ready` (Boolean) Block on create/update until the `Ready` condition is true and any server-generated outputs the resource exposes are readable. - `wait_timeout` (String) Maximum time to wait when `wait_for_ready` is set (Go duration, e.g. `10m`). Readiness and outputs share this budget; outputs that never appear leave a warning and their attributes stay null until the next refresh. -- `zookeeper` (Attributes) ZooKeeper configuration. (see [below for nested schema](#nestedatt--zookeeper)) ### Read-Only @@ -93,26 +91,6 @@ Required: - `partitions` (Number) Number of partitions. - `replicas` (Number) Number of replicas. - - -### Nested Schema for `zookeeper` - -Optional: - -- `replicas` (Number) Number of replicas. -- `resources` (Attributes) Explicit CPU and memory configuration. When omitted, the preset is applied. (see [below for nested schema](#nestedatt--zookeeper--resources)) -- `resources_preset` (String) Default sizing preset used when `resources` is omitted. -- `size` (String) Persistent volume size (quantity, e.g. `10Gi`). -- `storage_class` (String) StorageClass used to store the data. Changing a value set here replaces the object, because an existing volume is never migrated to another class. Removing the attribute from the configuration does not: the object keeps its volumes and the recorded class reverts to the default. - - -### Nested Schema for `zookeeper.resources` - -Optional: - -- `cpu` (String) CPU available to each replica. -- `memory` (String) Memory available to each replica. - ## Import Import is supported using the following syntax: diff --git a/docs/resources/kubernetes.md b/docs/resources/kubernetes.md index 5ae095b..8cbc093 100644 --- a/docs/resources/kubernetes.md +++ b/docs/resources/kubernetes.md @@ -3,15 +3,15 @@ page_title: "cozystack_kubernetes Resource - cozystack" subcategory: "" description: |- - A Cozystack managed Kubernetes cluster, deployed inside a tenant namespace. The addons block, the control-plane component sizing, and per-node-group GPU and kubelet tuning use server defaults. - In the talos, node_health_check, oidc, control_plane, and images blocks this resource tracks only what the configuration sets. A field left unset stays out of every request and out of state, so the platform's own default applies and keeps moving with the platform instead of being pinned at apply time; a field that is set refreshes normally, so drift against it is still planned away. Read the effective values — including the ones nobody configured — through the cozystack_kubernetes data source. The flip side is that the provider replaces the whole spec on every update: a value set out of band inside one of those blocks — by kubectl, or by an import that never made it into the configuration — is dropped on the next apply unless the configuration names it. + A Cozystack managed Kubernetes cluster, deployed inside a tenant namespace. The addons block and the control-plane component sizing use server defaults. Worker pools are separate cozystack_kubernetes_nodes resources. + In the talos, oidc, control_plane, and images blocks this resource tracks only what the configuration sets. A field left unset stays out of every request and out of state, so the platform's own default applies and keeps moving with the platform instead of being pinned at apply time; a field that is set refreshes normally, so drift against it is still planned away. Read the effective values — including the ones nobody configured — through the cozystack_kubernetes data source. The flip side is that the provider replaces the whole spec on every update: a value set out of band inside one of those blocks — by kubectl, or by an import that never made it into the configuration — is dropped on the next apply unless the configuration names it. --- # cozystack_kubernetes (Resource) -A Cozystack managed Kubernetes cluster, deployed inside a tenant namespace. The addons block, the control-plane component sizing, and per-node-group GPU and kubelet tuning use server defaults. +A Cozystack managed Kubernetes cluster, deployed inside a tenant namespace. The addons block and the control-plane component sizing use server defaults. Worker pools are separate `cozystack_kubernetes_nodes` resources. -In the `talos`, `node_health_check`, `oidc`, `control_plane`, and `images` blocks this resource tracks only what the configuration sets. A field left unset stays out of every request and out of state, so the platform's own default applies and keeps moving with the platform instead of being pinned at apply time; a field that is set refreshes normally, so drift against it is still planned away. Read the effective values — including the ones nobody configured — through the `cozystack_kubernetes` data source. The flip side is that the provider replaces the whole spec on every update: a value set out of band inside one of those blocks — by `kubectl`, or by an import that never made it into the configuration — is dropped on the next apply unless the configuration names it. +In the `talos`, `oidc`, `control_plane`, and `images` blocks this resource tracks only what the configuration sets. A field left unset stays out of every request and out of state, so the platform's own default applies and keeps moving with the platform instead of being pinned at apply time; a field that is set refreshes normally, so drift against it is still planned away. Read the effective values — including the ones nobody configured — through the `cozystack_kubernetes` data source. The flip side is that the provider replaces the whole spec on every update: a value set out of band inside one of those blocks — by `kubectl`, or by an import that never made it into the configuration — is dropped on the next apply unless the configuration names it. ## Example Usage @@ -25,21 +25,13 @@ resource "cozystack_kubernetes" "cluster" { version = "v1.35" - node_groups = { - md0 = { - instance_type = "u1.medium" - disk_size = "20Gi" - min_replicas = 1 - max_replicas = 10 - roles = ["ingress-nginx"] - } - } - # The admin kubeconfig is published asynchronously, so without waiting it is # only readable from the apply after the one that created the cluster. wait_for_ready = true } +# Worker nodes are a separate resource: see cozystack_kubernetes_nodes. + # Every block below is optional and follows the platform default while unset, # so pin only what you actually need to control. resource "cozystack_kubernetes" "tuned" { @@ -48,29 +40,6 @@ resource "cozystack_kubernetes" "tuned" { version = "v1.35" - node_groups = { - # Sized explicitly instead of by instance_type: cpu and memory go together. - workers = { - min_replicas = 2 - max_replicas = 6 - - resources = { - cpu = "4" - memory = "8Gi" - } - } - - # A GPU pool boots slower, so it overrides the cluster-wide remediation - # timeout without changing it for every other group. - gpu = { - instance_type = "u1.xlarge" - min_replicas = 0 - max_replicas = 4 - node_startup_timeout = "30m" - max_unhealthy = "0%" - } - } - # Worker OS images pulled from a self-hosted image factory. The Talos release # and schematic stay unset, so they keep following the platform. talos = { @@ -78,11 +47,6 @@ resource "cozystack_kubernetes" "tuned" { installer_repository = "registry.example.test/installer" } - node_health_check = { - max_unhealthy = "50%" - node_startup_timeout = "15m" - } - # Platform identity, plus the bindings created inside the tenant cluster. oidc = { mode = "System" @@ -101,8 +65,6 @@ resource "cozystack_kubernetes" "byo_identity" { name = "byo-identity" namespace = "tenant-root" - node_groups = {} - oidc = { mode = "CustomConfig" @@ -149,14 +111,12 @@ resource "cozystack_kubernetes" "byo_identity" { - `name` (String) Kubernetes cluster name (`metadata.name`). Immutable. - `namespace` (String) Tenant namespace the application is created in. Immutable. -- `node_groups` (Attributes Map) Worker node groups keyed by name. (see [below for nested schema](#nestedatt--node_groups)) ### Optional - `control_plane` (Attributes) Tenant control-plane configuration. Only the API-server passthrough is managed here; component sizing, the replica count, konnectivity and the scheduler follow the platform. They are not preserved across an apply either way — the provider replaces the whole spec on every update, so anything set out of band under `controlPlane` returns to its platform default whether or not this block is configured. (see [below for nested schema](#nestedatt--control_plane)) - `host` (String) External hostname for the cluster. Defaults to `.`. - `images` (Attributes) Image overrides for air-gapped or rate-limited registries. Each unset field follows the tag pinned by the deployed chart. (see [below for nested schema](#nestedatt--images)) -- `node_health_check` (Attributes) MachineHealthCheck tuning applied to every worker node group. Follows the platform defaults while unset. (see [below for nested schema](#nestedatt--node_health_check)) - `oidc` (Attributes) OIDC authentication and per-user RBAC for the tenant kube-apiserver. Follows the platform default (identity off, static admin kubeconfig only) while unset. (see [below for nested schema](#nestedatt--oidc)) - `storage_class` (String) StorageClass used to store the data. Follows the platform default (`replicated`) while unset. Changing a configured value replaces the cluster: a PersistentVolumeClaim's class is fixed when it is created, so an in-place change would be recorded in state while every existing volume stayed on the old class. - `talos` (Attributes) Talos worker OS image coordinates. Every field follows the platform default while unset; set one only to pin it. (see [below for nested schema](#nestedatt--talos)) @@ -172,31 +132,6 @@ resource "cozystack_kubernetes" "byo_identity" { - `ready` (Boolean) Whether the application's `Ready` condition is true. - `uid` (String) Server-assigned object UID (`metadata.uid`). Stable across updates; changes on recreate. - -### Nested Schema for `node_groups` - -Optional: - -- `disk_size` (String) Persistent disk size for kubelet and containerd data. -- `instance_type` (String) Virtual machine instance type. -- `max_replicas` (Number) Maximum number of replicas. -- `max_unhealthy` (String) Per-group override for `node_health_check.max_unhealthy`, as a bare integer (`"1"`) or a percentage (`"0%"`). Inherits the cluster-wide value while unset. -- `min_replicas` (Number) Minimum number of replicas. -- `node_startup_timeout` (String) Per-group override for `node_health_check.node_startup_timeout` (duration, e.g. `20m`). Inherits the cluster-wide value while unset. -- `resources` (Attributes) Explicit CPU and memory for each worker node, replacing `instance_type` sizing. Set both `cpu` and `memory`, or neither. (see [below for nested schema](#nestedatt--node_groups--resources)) -- `roles` (List of String) Node roles (e.g. `ingress-nginx`). -- `storage_class` (String) StorageClass for worker node persistent disks. Falls back to the cluster's `storage_class` when empty. Deliberately mutable upstream, unlike the cluster-level attribute. - - -### Nested Schema for `node_groups.resources` - -Optional: - -- `cpu` (String) CPU per worker node (quantity, e.g. `4`). Requires `memory`. -- `memory` (String) Memory per worker node (quantity, e.g. `8Gi`). Requires `cpu`. - - - ### Nested Schema for `control_plane` @@ -225,15 +160,6 @@ Optional: - `wait_for_kubeconfig` (String) Image for the wait-for-kubeconfig init container. - -### Nested Schema for `node_health_check` - -Optional: - -- `max_unhealthy` (String) Unhealthy nodes tolerated per node group before remediation pauses. The MachineHealthCheck admission webhook takes a bare integer (`"1"`) or a percentage (`"50%"`); a percentage is the safer form. Follows the platform default (`50%`) while unset. -- `node_startup_timeout` (String) How long a Machine may take to reach Ready before it is remediated (duration, e.g. `20m`). Raise it for slow first boots — a Talos image fetch from the image factory, or a busy StorageClass. Follows the platform default (`10m`) while unset. - - ### Nested Schema for `oidc` diff --git a/docs/resources/kubernetes_nodes.md b/docs/resources/kubernetes_nodes.md index d4f36ab..738dcc3 100644 --- a/docs/resources/kubernetes_nodes.md +++ b/docs/resources/kubernetes_nodes.md @@ -27,18 +27,9 @@ resource "cozystack_kubernetes" "cluster" { namespace = "tenant-root" version = "v1.35" - - node_groups = { - md0 = { - instance_type = "u1.medium" - min_replicas = 1 - max_replicas = 3 - } - } } -# A standalone pool is named "-". The pool part must not collide -# with a node group the parent cluster still manages, such as md0 above. +# A pool is named "-". resource "cozystack_kubernetes_nodes" "gpu" { name = "${cozystack_kubernetes.cluster.name}-gpu" namespace = cozystack_kubernetes.cluster.namespace diff --git a/docs/resources/tenant.md b/docs/resources/tenant.md index 21e4241..d6a32c2 100644 --- a/docs/resources/tenant.md +++ b/docs/resources/tenant.md @@ -46,6 +46,17 @@ resource "cozystack_tenant" "team_c" { host = "team-c.example.com" gateway = true } + +# Keep the tenant's traces in the shared store in tenant-root rather than in a +# store of its own. It needs the tenant's own monitoring, and tenant-root must +# host the store. +resource "cozystack_tenant" "team_d" { + name = "team-d" + namespace = "tenant-root" + + monitoring = true + tracing_central = true +} ``` @@ -66,6 +77,7 @@ resource "cozystack_tenant" "team_c" { - `resource_quotas` (Map of String) Resource quotas for the tenant, as quantity strings (for example `{cpu = "4"}`). - `scheduling_class` (String) Name of a SchedulingClass CR applied to the tenant's workloads. - `seaweedfs` (Boolean) Deploy a dedicated SeaweedFS instance for the tenant. +- `tracing_central` (Boolean) Send the tenant's traces to the shared VictoriaTraces store in `tenant-root` instead of a per-tenant backend. Tenants sharing the store cannot read each other's traces. Requires `monitoring = true` on this tenant and `tenant-root` hosting the store; without them the tenant's monitoring fails to render. - `wait_for_ready` (Boolean) Block on create/update until the `Ready` condition is true and any server-generated outputs the resource exposes are readable. - `wait_timeout` (String) Maximum time to wait when `wait_for_ready` is set (Go duration, e.g. `10m`). Readiness and outputs share this budget; outputs that never appear leave a warning and their attributes stay null until the next refresh. From e8b7900389ec600b89b4bd87cbdd8378b81c4c17 Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Sat, 10 Oct 2026 20:43:02 +0300 Subject: [PATCH 08/12] feat(postgres,mariadb)!: drop users[].password Cozystack generates every user password and no longer reads one from the spec: a leftover password is ignored and draws an admission warning. The provider still offered the attribute and echoed it into state, so a configured password never took effect and state showed a value that was not the live credential. It goes from both resources and data sources. The spec guards only compared top-level keys, which is how this slipped through; a guard on the user entry now compares against the pinned User type. Signed-off-by: Alexey Artamonov --- CHANGELOG.md | 1 + .../resources/cozystack_mariadb/resource.tf | 4 +++- .../resources/cozystack_postgres/resource.tf | 4 +++- internal/provider/acc_test.go | 10 ++++----- internal/provider/mariadb_model.go | 14 ++---------- internal/provider/mariadb_model_test.go | 19 ++++++++++++---- internal/provider/mariadb_schema.go | 10 +++------ internal/provider/postgresql_model.go | 13 ++--------- internal/provider/postgresql_model_test.go | 22 +++++++++++++++---- internal/provider/postgresql_schema.go | 8 ++----- 10 files changed, 54 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9364920..3db37c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Tracks the Cozystack API at main commit `11499c30e`, ahead of the first v1.7 tag - `cozystack_httpcache` (resource and data source) is removed, following Cozystack, which dropped the HTTPCache application and its ResourceDefinition: a cluster on this API no longer serves the kind. Remove the resource from your configuration and run `terraform state rm` for each instance before upgrading the provider. - `cozystack_kubernetes`: `node_groups` and `node_health_check` are removed (resource and data source). Cozystack moved worker pools out of the Kubernetes application into KubernetesNodes, and its upgrade migrates every existing node group, the default `md0` included, into a KubernetesNodes object named `-`. Drop both attributes from the configuration and bring each migrated pool under management with `terraform import cozystack_kubernetes_nodes. /-`. - `cozystack_kafka`: `zookeeper` is removed (resource and data source). Cozystack runs Kafka in KRaft mode and migrates existing ZooKeeper clusters on upgrade, so the block has nothing left to configure. Drop it from the configuration. +- `cozystack_postgres` and `cozystack_mariadb`: `users[].password` is removed (resources and data sources). Cozystack always generates user passwords now and ignores a password in the spec, so a configured one never took effect and state showed a password that was not the live one. Read the live password from the `postgres--credentials` or `mariadb--credentials` Secret. On a release upgraded from an older Cozystack, the password set before the removal stays in that Secret until it is rotated. - `rabbitmq`: the default `resources_preset` moves from `t1.nano` to `s1.nano`. The attribute is optional-with-default, so the old value sits in existing state: after upgrading the provider, a `cozystack_rabbitmq` that never set it explicitly plans `t1.nano` → `s1.nano`, which updates the cluster and rolls its pods. Set `resources_preset = "t1.nano"` explicitly to keep the previous sizing. ## v1.6.3 diff --git a/examples/resources/cozystack_mariadb/resource.tf b/examples/resources/cozystack_mariadb/resource.tf index a0087df..50ae423 100644 --- a/examples/resources/cozystack_mariadb/resource.tf +++ b/examples/resources/cozystack_mariadb/resource.tf @@ -6,8 +6,10 @@ resource "cozystack_mariadb" "app" { version = "v11.8" size = "20Gi" + # Passwords are generated by the platform and kept in the + # mariadb-app-credentials Secret. users = { - app = { password = "change-me", max_user_connections = 100 } + app = { max_user_connections = 100 } } databases = { appdb = { roles = { admin = ["app"] } } diff --git a/examples/resources/cozystack_postgres/resource.tf b/examples/resources/cozystack_postgres/resource.tf index 81cbe27..58bc55a 100644 --- a/examples/resources/cozystack_postgres/resource.tf +++ b/examples/resources/cozystack_postgres/resource.tf @@ -9,8 +9,10 @@ resource "cozystack_postgres" "app" { backup = { use_system_bucket = true } + # Passwords are generated by the platform and kept in the + # postgres-app-credentials Secret. users = { - app = { password = "change-me" } + app = {} } databases = { appdb = { diff --git a/internal/provider/acc_test.go b/internal/provider/acc_test.go index cf27ad5..fb431f5 100644 --- a/internal/provider/acc_test.go +++ b/internal/provider/acc_test.go @@ -550,7 +550,7 @@ resource "cozystack_mariadb" "test" { name = "tfaccmaria" namespace = "tenant-root" replicas = 1 - users = { app = { password = "pw-123" } } + users = { app = {} } databases = { appdb = { roles = { admin = ["app"] } } } } ` @@ -559,7 +559,7 @@ resource "cozystack_mariadb" "test" { name = "tfaccmaria" namespace = "tenant-root" replicas = 1 - users = { app = { password = "pw-123" }, ro = { password = "pw-456" } } + users = { app = {}, ro = {} } databases = { appdb = { roles = { admin = ["app"], readonly = ["ro"] } } } } ` @@ -573,7 +573,7 @@ resource "cozystack_mariadb" "test" { Config: base, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("cozystack_mariadb.test", "id", "tenant-root/tfaccmaria"), - resource.TestCheckResourceAttr("cozystack_mariadb.test", "users.app.password", "pw-123"), + resource.TestCheckResourceAttr("cozystack_mariadb.test", "users.app.max_user_connections", "0"), resource.TestCheckResourceAttr("cozystack_mariadb.test", "databases.appdb.roles.admin.0", "app"), ), }, @@ -692,7 +692,7 @@ resource "cozystack_postgres" "test" { name = "tfaccpg" namespace = "tenant-root" replicas = 1 - users = { app = { password = "pw-123" } } + users = { app = {} } databases = { appdb = { roles = { admin = ["app"] } } } } ` @@ -705,7 +705,7 @@ resource "cozystack_postgres" "test" { Config: config, Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("cozystack_postgres.test", "id", "tenant-root/tfaccpg"), - resource.TestCheckResourceAttr("cozystack_postgres.test", "users.app.password", "pw-123"), + resource.TestCheckResourceAttr("cozystack_postgres.test", "users.app.replication", "false"), resource.TestCheckResourceAttr("cozystack_postgres.test", "databases.appdb.roles.admin.0", "app"), ), }, diff --git a/internal/provider/mariadb_model.go b/internal/provider/mariadb_model.go index c532a32..3d4cb71 100644 --- a/internal/provider/mariadb_model.go +++ b/internal/provider/mariadb_model.go @@ -47,13 +47,11 @@ func (m *mariadbModel) identity() (string, string) { func mariadbUserObjectType() map[string]attr.Type { return map[string]attr.Type{ "max_user_connections": types.Int64Type, - "password": types.StringType, } } type mariadbUserData struct { - MaxUserConnections types.Int64 `tfsdk:"max_user_connections"` - Password types.String `tfsdk:"password"` + MaxUserConnections types.Int64 `tfsdk:"max_user_connections"` } func expandMariadbUsers(ctx context.Context, value types.Map) (map[string]any, diag.Diagnostics) { @@ -73,12 +71,7 @@ func expandMariadbUsers(ctx context.Context, value types.Map) (map[string]any, d } for name, user := range elements { - entry := map[string]any{"maxUserConnections": user.MaxUserConnections.ValueInt64()} - if password := user.Password.ValueString(); password != "" { - entry["password"] = password - } - - out[name] = entry + out[name] = map[string]any{"maxUserConnections": user.MaxUserConnections.ValueInt64()} } return out, diags @@ -86,11 +79,8 @@ func expandMariadbUsers(ctx context.Context, value types.Map) (map[string]any, d func flattenMariadbUsers(raw any) (types.Map, diag.Diagnostics) { return flattenObjectMap(raw, mariadbUserObjectType(), func(user map[string]any) map[string]attr.Value { - password, _ := user["password"].(string) - return map[string]attr.Value{ "max_user_connections": types.Int64Value(anyToInt64(user["maxUserConnections"])), - "password": types.StringValue(password), } }) } diff --git a/internal/provider/mariadb_model_test.go b/internal/provider/mariadb_model_test.go index 4f123ef..6c493f9 100644 --- a/internal/provider/mariadb_model_test.go +++ b/internal/provider/mariadb_model_test.go @@ -26,7 +26,6 @@ func fullMariadbModel() mariadbModel { Users: types.MapValueMust(usersType, map[string]attr.Value{ "app": types.ObjectValueMust(mariadbUserObjectType(), map[string]attr.Value{ "max_user_connections": types.Int64Value(10), - "password": types.StringValue("pw"), }), }), Databases: types.MapNull(types.ObjectType{AttrTypes: rolesEntryObjectType()}), @@ -48,9 +47,21 @@ func TestMariadbExpand_Users(t *testing.T) { if app["maxUserConnections"] != int64(10) { t.Errorf("users.app.maxUserConnections = %v, want 10", app["maxUserConnections"]) } - if app["password"] != "pw" { - t.Errorf("users.app.password = %v, want pw", app["password"]) +} + +// The users map is not a top-level key, so the ConfigSpec guard cannot see a +// field the API drops from a user entry. +func TestMariadbExpandUserKeysMatchUserSpec(t *testing.T) { + t.Parallel() + + model := fullMariadbModel() + + got, diags := model.expand(context.Background()) + if diags.HasError() { + t.Fatalf("expand diagnostics: %v", diags) } + + assertSpecCoverage(t, nestedSpecKeys(t, got.Spec, "users", "app"), mariadb.User{}) } func TestMariadbFlatten_RoundTrip(t *testing.T) { @@ -67,7 +78,7 @@ func TestMariadbFlatten_RoundTrip(t *testing.T) { "external": false, "version": "v10.11", "resources": map[string]any{}, - "users": map[string]any{"app": map[string]any{"maxUserConnections": int64(5), "password": "x"}}, + "users": map[string]any{"app": map[string]any{"maxUserConnections": int64(5)}}, "databases": map[string]any{"appdb": map[string]any{"roles": map[string]any{"admin": []any{"app"}}}}, }, Status: client.ApplicationStatus{Version: "1.0.0", Ready: true}, diff --git a/internal/provider/mariadb_schema.go b/internal/provider/mariadb_schema.go index 960d093..33d911c 100644 --- a/internal/provider/mariadb_schema.go +++ b/internal/provider/mariadb_schema.go @@ -36,8 +36,9 @@ func mariadbSchema() rschema.Schema { MarkdownDescription: "MariaDB version (`v11.8`, `v11.4`, `v10.11`, `v10.6`).", }, "users": rschema.MapNestedAttribute{ - Optional: true, - MarkdownDescription: "MariaDB users keyed by user name.", + Optional: true, + MarkdownDescription: "MariaDB users keyed by user name. The platform generates every user's password " + + "and keeps it in the `mariadb--credentials` Secret; it cannot be set here.", NestedObject: rschema.NestedAttributeObject{ Attributes: map[string]rschema.Attribute{ "max_user_connections": rschema.Int64Attribute{ @@ -45,10 +46,6 @@ func mariadbSchema() rschema.Schema { Default: int64default.StaticInt64(0), MarkdownDescription: "Maximum number of connections for the user.", }, - "password": rschema.StringAttribute{ - Optional: true, Computed: true, Sensitive: true, - MarkdownDescription: "Password for the user (autogenerated when omitted).", - }, }, }, }, @@ -81,7 +78,6 @@ func mariadbDataSourceSchema() dsschema.Schema { NestedObject: dsschema.NestedAttributeObject{ Attributes: map[string]dsschema.Attribute{ "max_user_connections": dsschema.Int64Attribute{Computed: true, MarkdownDescription: "Maximum connections."}, - "password": dsschema.StringAttribute{Computed: true, Sensitive: true, MarkdownDescription: "User password."}, }, }, }, diff --git a/internal/provider/postgresql_model.go b/internal/provider/postgresql_model.go index c92984e..fb8b0e1 100644 --- a/internal/provider/postgresql_model.go +++ b/internal/provider/postgresql_model.go @@ -60,7 +60,6 @@ func (m *postgresqlModel) identity() (string, string) { func pgUserObjectType() map[string]attr.Type { return map[string]attr.Type{ - "password": types.StringType, "replication": types.BoolType, } } @@ -73,8 +72,7 @@ func pgDatabaseObjectType() map[string]attr.Type { } type pgUserData struct { - Password types.String `tfsdk:"password"` - Replication types.Bool `tfsdk:"replication"` + Replication types.Bool `tfsdk:"replication"` } type pgDatabaseData struct { @@ -99,12 +97,7 @@ func expandPgUsers(ctx context.Context, value types.Map) (map[string]any, diag.D } for name, user := range elements { - entry := map[string]any{"replication": user.Replication.ValueBool()} - if password := user.Password.ValueString(); password != "" { - entry["password"] = password - } - - out[name] = entry + out[name] = map[string]any{"replication": user.Replication.ValueBool()} } return out, diags @@ -112,11 +105,9 @@ func expandPgUsers(ctx context.Context, value types.Map) (map[string]any, diag.D func flattenPgUsers(raw any) (types.Map, diag.Diagnostics) { return flattenObjectMap(raw, pgUserObjectType(), func(user map[string]any) map[string]attr.Value { - password, _ := user["password"].(string) replication, _ := user["replication"].(bool) return map[string]attr.Value{ - "password": types.StringValue(password), "replication": types.BoolValue(replication), } }) diff --git a/internal/provider/postgresql_model_test.go b/internal/provider/postgresql_model_test.go index d8d4e5f..9b1fc61 100644 --- a/internal/provider/postgresql_model_test.go +++ b/internal/provider/postgresql_model_test.go @@ -27,7 +27,6 @@ func fullPostgresqlModel() postgresqlModel { Version: types.StringValue("v18"), Users: types.MapValueMust(usersType, map[string]attr.Value{ "app": types.ObjectValueMust(pgUserObjectType(), map[string]attr.Value{ - "password": types.StringValue("pw"), "replication": types.BoolValue(true), }), }), @@ -47,11 +46,26 @@ func TestPostgresqlExpand_Users(t *testing.T) { users, _ := got.Spec["users"].(map[string]any) app, _ := users["app"].(map[string]any) - if app["replication"] != true || app["password"] != "pw" { - t.Errorf("users.app = %v, want {password:pw, replication:true}", app) + if app["replication"] != true { + t.Errorf("users.app = %v, want {replication:true}", app) } } +// The users map is not a top-level key, so the ConfigSpec guard cannot see a +// field the API drops from a user entry. +func TestPostgresqlExpandUserKeysMatchUserSpec(t *testing.T) { + t.Parallel() + + model := fullPostgresqlModel() + + got, diags := model.expand(context.Background()) + if diags.HasError() { + t.Fatalf("expand diagnostics: %v", diags) + } + + assertSpecCoverage(t, nestedSpecKeys(t, got.Spec, "users", "app"), postgresql.User{}) +} + func TestPostgresqlFlatten_RoundTrip(t *testing.T) { t.Parallel() @@ -66,7 +80,7 @@ func TestPostgresqlFlatten_RoundTrip(t *testing.T) { "external": false, "version": "v16", "resources": map[string]any{}, - "users": map[string]any{"app": map[string]any{"password": "x", "replication": false}}, + "users": map[string]any{"app": map[string]any{"replication": false}}, "databases": map[string]any{ "appdb": map[string]any{"extensions": []any{"postgis"}, "roles": map[string]any{"admin": []any{"app"}}}, }, diff --git a/internal/provider/postgresql_schema.go b/internal/provider/postgresql_schema.go index e4e6262..5856e85 100644 --- a/internal/provider/postgresql_schema.go +++ b/internal/provider/postgresql_schema.go @@ -16,13 +16,10 @@ import ( func pgUsersResourceAttribute() rschema.MapNestedAttribute { return rschema.MapNestedAttribute{ Optional: true, - MarkdownDescription: "PostgreSQL users keyed by user name.", + MarkdownDescription: "PostgreSQL users keyed by user name. The platform generates every user's password " + + "and keeps it in the `postgres--credentials` Secret; it cannot be set here.", NestedObject: rschema.NestedAttributeObject{ Attributes: map[string]rschema.Attribute{ - "password": rschema.StringAttribute{ - Optional: true, Computed: true, Sensitive: true, - MarkdownDescription: "Password for the user (autogenerated when omitted).", - }, "replication": rschema.BoolAttribute{ Optional: true, Computed: true, Default: booldefault.StaticBool(false), @@ -143,7 +140,6 @@ func postgresDataSourceSchema() dsschema.Schema { MarkdownDescription: "PostgreSQL users keyed by user name.", NestedObject: dsschema.NestedAttributeObject{ Attributes: map[string]dsschema.Attribute{ - "password": dsschema.StringAttribute{Computed: true, Sensitive: true, MarkdownDescription: "User password."}, "replication": dsschema.BoolAttribute{Computed: true, MarkdownDescription: "Whether the user has replication privileges."}, }, }, From 5f806a28efff27ad8bff1097280fb6a56e5eab03 Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Sat, 10 Oct 2026 20:46:29 +0300 Subject: [PATCH 09/12] chore: record the temporary pseudo-version pin and stop Renovate moving it CLAUDE.md said the API module is only ever pinned to a tag, which no longer matches go.mod. It now says the pin is a temporary main pseudo-version and that the first v1.7 tag replaces it by hand. The Renovate rule disabled only digest updates. The pseudo-version sorts below the v1.6.x tags, so an ordinary update could move the pin to one of them and drop the tracing field; every update of the module is disabled until the tag pin returns. Signed-off-by: Alexey Artamonov --- CLAUDE.md | 2 +- renovate.json | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1b5db9c..278eb98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ The provider talks to the Cozystack aggregated Kubernetes API (`apps.cozystack.i ## Conventions -- The provider version tracks the Cozystack API version it builds against; the `github.com/cozystack/cozystack/api/apps/v1alpha1` module is pinned to a released tag (currently `v1.6.3`), never a pseudo-version. +- The provider version tracks the Cozystack API version it builds against; the `github.com/cozystack/cozystack/api/apps/v1alpha1` module is pinned to a released tag. The one exception is temporary: it is currently pinned to the pseudo-version of cozystack main commit `11499c30e`, the first one carrying the Tenant `tracingCentral` field, because no API tag contains it yet. Go orders that pseudo-version below `v1.6.3`, so moving to any v1.6.x tag would silently drop the field. Replace the pin by hand with the first `api/apps/v1alpha1` v1.7 tag, and in the same change restore the Renovate rule for this module in `renovate.json` to disabling `digest` updates only. - Typed models are guarded against upstream spec drift by `assertSpecCoverage(t, emitted, ConfigSpec{}, omit...)`, which reflects emitted spec keys against the api-module's `ConfigSpec` json tags. For Kinds whose Go types live in the heavy root cozystack module, a small vendored guard struct stands in. - Adding a Kind: add the `client.Resource` constructor, register it in `provider.go`, choose typed vs rawspec vs marker, add a unit test (with the spec-coverage guard for typed models), add `examples/resources/` + `examples/data-sources/` entries, then run `make docs`. A model with server-generated outputs implements `outputsReader` (`readOutputs` + `outputsPending`), which is what `wait_for_ready` polls on. diff --git a/renovate.json b/renovate.json index 5d6d700..9951701 100644 --- a/renovate.json +++ b/renovate.json @@ -49,16 +49,13 @@ "rangeStrategy": "bump" }, { - "description": "Keep the Cozystack API module pinned to a released tag, never a pseudo-version", + "description": "Temporarily no automatic updates of the Cozystack API module: it is pinned to a main pseudo-version that sorts below the v1.6.x tags, and any v1.6.x tag lacks fields the provider models. Replace it by hand with the first v1.7 tag, then limit this rule to digest updates again (see CLAUDE.md)", "matchManagers": [ "gomod" ], "matchPackageNames": [ "github.com/cozystack/cozystack/api/apps/v1alpha1" ], - "matchUpdateTypes": [ - "digest" - ], "enabled": false } ] From c7f46089735731e3687d3ec3ba5795534ea862ac Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Sat, 10 Oct 2026 20:46:35 +0300 Subject: [PATCH 10/12] docs(changelog): note what the new pin adds that is not modelled Without it a reader could take the Valkey kind and the new optional fields as covered by this release. Signed-off-by: Alexey Artamonov --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3db37c7..f082c93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Tracks the Cozystack API at main commit `11499c30e`, ahead of the first v1.7 tag: it is the first commit carrying the shared-central tracing field, and it already contains cozystack/cozystack#3936. +The pinned API also adds a `Valkey` kind and optional fields on several existing kinds; the provider does not model them yet. + ### Features - `cozystack_tenant`: new `tracing_central` attribute (default `false`), also on the data source. It sends the tenant's traces to the shared VictoriaTraces store in `tenant-root` instead of a per-tenant backend, and needs `monitoring = true` on the tenant and `tenant-root` hosting the store. From 17cd0930f65e1b45a17abf89f19b6102a33ce020 Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Sat, 10 Oct 2026 20:47:12 +0300 Subject: [PATCH 11/12] style(postgres): gofmt the users attribute Signed-off-by: Alexey Artamonov --- internal/provider/postgresql_schema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/provider/postgresql_schema.go b/internal/provider/postgresql_schema.go index 5856e85..9d5a4e8 100644 --- a/internal/provider/postgresql_schema.go +++ b/internal/provider/postgresql_schema.go @@ -15,7 +15,7 @@ import ( func pgUsersResourceAttribute() rschema.MapNestedAttribute { return rschema.MapNestedAttribute{ - Optional: true, + Optional: true, MarkdownDescription: "PostgreSQL users keyed by user name. The platform generates every user's password " + "and keeps it in the `postgres--credentials` Secret; it cannot be set here.", NestedObject: rschema.NestedAttributeObject{ From 7ca1a59879a7ddfb95bac0d3417920a059027456 Mon Sep 17 00:00:00 2001 From: Alexey Artamonov Date: Sat, 10 Oct 2026 20:47:12 +0300 Subject: [PATCH 12/12] docs: regenerate for the users password removal Signed-off-by: Alexey Artamonov --- docs/data-sources/mariadb.md | 1 - docs/data-sources/postgres.md | 1 - docs/resources/mariadb.md | 7 ++++--- docs/resources/postgres.md | 7 ++++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/data-sources/mariadb.md b/docs/data-sources/mariadb.md index 003fde8..77c858c 100644 --- a/docs/data-sources/mariadb.md +++ b/docs/data-sources/mariadb.md @@ -75,4 +75,3 @@ Read-Only: Read-Only: - `max_user_connections` (Number) Maximum connections. -- `password` (String, Sensitive) User password. diff --git a/docs/data-sources/postgres.md b/docs/data-sources/postgres.md index 24b2764..378715a 100644 --- a/docs/data-sources/postgres.md +++ b/docs/data-sources/postgres.md @@ -104,5 +104,4 @@ Read-Only: Read-Only: -- `password` (String, Sensitive) User password. - `replication` (Boolean) Whether the user has replication privileges. diff --git a/docs/resources/mariadb.md b/docs/resources/mariadb.md index bc78a7b..6d90527 100644 --- a/docs/resources/mariadb.md +++ b/docs/resources/mariadb.md @@ -21,8 +21,10 @@ resource "cozystack_mariadb" "app" { version = "v11.8" size = "20Gi" + # Passwords are generated by the platform and kept in the + # mariadb-app-credentials Secret. users = { - app = { password = "change-me", max_user_connections = 100 } + app = { max_user_connections = 100 } } databases = { appdb = { roles = { admin = ["app"] } } @@ -47,7 +49,7 @@ resource "cozystack_mariadb" "app" { - `resources_preset` (String) Sizing preset applied when `resources` is omitted. - `size` (String) Persistent volume size (quantity, e.g. `10Gi`). - `storage_class` (String) StorageClass used to store the data. Changing a value set here replaces the object, because an existing volume is never migrated to another class. Removing the attribute from the configuration does not: the object keeps its volumes and the recorded class reverts to the default. -- `users` (Attributes Map) MariaDB users keyed by user name. (see [below for nested schema](#nestedatt--users)) +- `users` (Attributes Map) MariaDB users keyed by user name. The platform generates every user's password and keeps it in the `mariadb--credentials` Secret; it cannot be set here. (see [below for nested schema](#nestedatt--users)) - `version` (String) MariaDB version (`v11.8`, `v11.4`, `v10.11`, `v10.6`). - `wait_for_ready` (Boolean) Block on create/update until the `Ready` condition is true and any server-generated outputs the resource exposes are readable. - `wait_timeout` (String) Maximum time to wait when `wait_for_ready` is set (Go duration, e.g. `10m`). Readiness and outputs share this budget; outputs that never appear leave a warning and their attributes stay null until the next refresh. @@ -91,7 +93,6 @@ Optional: Optional: - `max_user_connections` (Number) Maximum number of connections for the user. -- `password` (String, Sensitive) Password for the user (autogenerated when omitted). ## Import diff --git a/docs/resources/postgres.md b/docs/resources/postgres.md index c2c08f5..25bc0a0 100644 --- a/docs/resources/postgres.md +++ b/docs/resources/postgres.md @@ -24,8 +24,10 @@ resource "cozystack_postgres" "app" { backup = { use_system_bucket = true } + # Passwords are generated by the platform and kept in the + # postgres-app-credentials Secret. users = { - app = { password = "change-me" } + app = {} } databases = { appdb = { @@ -59,7 +61,7 @@ resource "cozystack_postgres" "app" { - `size` (String) Persistent volume size (quantity, e.g. `10Gi`). - `storage_class` (String) StorageClass used to store the data. Changing a value set here replaces the object, because an existing volume is never migrated to another class. Removing the attribute from the configuration does not: the object keeps its volumes and the recorded class reverts to the default. - `tls` (Attributes) TLS configuration. This only controls whether the external hostname is added to the operator-managed server certificate; CNPG keeps TLS on the wire either way, and turning PostgreSQL TLS off entirely is a server-parameter matter. Omit the block to follow `external`. (see [below for nested schema](#nestedatt--tls)) -- `users` (Attributes Map) PostgreSQL users keyed by user name. (see [below for nested schema](#nestedatt--users)) +- `users` (Attributes Map) PostgreSQL users keyed by user name. The platform generates every user's password and keeps it in the `postgres--credentials` Secret; it cannot be set here. (see [below for nested schema](#nestedatt--users)) - `version` (String) PostgreSQL major version (`v18`…`v13`). - `wait_for_ready` (Boolean) Block on create/update until the `Ready` condition is true and any server-generated outputs the resource exposes are readable. - `wait_timeout` (String) Maximum time to wait when `wait_for_ready` is set (Go duration, e.g. `10m`). Readiness and outputs share this budget; outputs that never appear leave a warning and their attributes stay null until the next refresh. @@ -120,7 +122,6 @@ Required: Optional: -- `password` (String, Sensitive) Password for the user (autogenerated when omitted). - `replication` (Boolean) Whether the user has replication privileges.