From 861d59e259d9e04327cbc317879cb55eae1290be Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Thu, 30 Apr 2026 15:20:47 +0200 Subject: [PATCH 1/8] WIP: feat(ske) access ressource --- docs/data-sources/ske_cluster.md | 18 +++ .../services/ske/cluster/datasource.go | 20 +++ .../internal/services/ske/cluster/resource.go | 131 ++++++++++++++++-- .../services/ske/cluster/resource_test.go | 127 +++++++++++++++++ stackit/internal/services/ske/ske_acc_test.go | 13 +- .../services/ske/testdata/resource-max.tf | 7 + 6 files changed, 305 insertions(+), 11 deletions(-) diff --git a/docs/data-sources/ske_cluster.md b/docs/data-sources/ske_cluster.md index be663d6d9..781b1ec5c 100644 --- a/docs/data-sources/ske_cluster.md +++ b/docs/data-sources/ske_cluster.md @@ -33,6 +33,7 @@ data "stackit_ske_cluster" "example" { ### Read-Only +- `access` (Attributes) Configure access to the cluster (see [below for nested schema](#nestedatt--access)) - `egress_address_ranges` (List of String) The outgoing network ranges (in CIDR notation) of traffic originating from workload on the cluster. - `extensions` (Attributes) A single extensions block as defined below (see [below for nested schema](#nestedatt--extensions)) - `hibernations` (Attributes List) One or more hibernation block as defined below. (see [below for nested schema](#nestedatt--hibernations)) @@ -44,6 +45,23 @@ data "stackit_ske_cluster" "example" { - `node_pools` (Attributes List) One or more `node_pool` block as defined below. (see [below for nested schema](#nestedatt--node_pools)) - `pod_address_ranges` (List of String) The network ranges (in CIDR notation) used by pods of the cluster. + +### Nested Schema for `access` + +Read-Only: + +- `idp` (Attributes) Configure IDP (see [below for nested schema](#nestedatt--access--idp)) + + +### Nested Schema for `access.idp` + +Read-Only: + +- `enabled` (Boolean) Enable IDP integration for the cluster. +- `type` (String) The IDP type. Possible values: 'stackit'. + + + ### Nested Schema for `extensions` diff --git a/stackit/internal/services/ske/cluster/datasource.go b/stackit/internal/services/ske/cluster/datasource.go index b32a0a8c5..242ab2250 100644 --- a/stackit/internal/services/ske/cluster/datasource.go +++ b/stackit/internal/services/ske/cluster/datasource.go @@ -330,6 +330,26 @@ func (r *clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest Optional: true, Description: "The resource region. If not defined, the provider region is used.", }, + "access": schema.SingleNestedAttribute{ + Description: descriptions["access"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "idp": schema.SingleNestedAttribute{ + Description: descriptions["access_idp"], + Computed: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + Description: descriptions["access_idp_enabled"], + Computed: true, + }, + "type": schema.StringAttribute{ + Description: descriptions["access_idp_type"], + Computed: true, + }, + }, + }, + }, + }, }, } } diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index e274754fe..4f84ac30f 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -83,6 +83,7 @@ type Model struct { EgressAddressRanges types.List `tfsdk:"egress_address_ranges"` PodAddressRanges types.List `tfsdk:"pod_address_ranges"` Region types.String `tfsdk:"region"` + Access types.Object `tfsdk:"access"` } // Struct corresponding to Model.NodePools[i] @@ -268,6 +269,24 @@ type clusterResource struct { providerData core.ProviderData } +type access struct { + IDP types.Object `tfsdk:"idp"` +} + +var accessTypes = map[string]attr.Type{ + "idp": basetypes.ObjectType{AttrTypes: idpTypes}, +} + +type idp struct { + Enabled types.Bool `tfsdk:"enabled"` + Type types.String `tfsdk:"type"` +} + +var idpTypes = map[string]attr.Type{ + "enabled": basetypes.BoolType{}, + "type": basetypes.StringType{}, +} + // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. func (r *clusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform @@ -324,17 +343,22 @@ func (r *clusterResource) Configure(ctx context.Context, req resource.ConfigureR tflog.Info(ctx, "SKE cluster clients configured") } +var descriptions = map[string]string{ + "main": "SKE Cluster Resource schema. Must have a `region` specified in the provider configuration.", + "node_pools_plan_note": "When updating `node_pools` of a `stackit_ske_cluster`, the Terraform plan might appear incorrect as it matches the node pools by index rather than by name. " + + "However, the SKE API correctly identifies node pools by name and applies the intended changes. Please review your changes carefully to ensure the correct configuration will be applied.", + "max_surge": "Maximum number of additional VMs that are created during an update.", + "max_unavailable": "Maximum number of VMs that that can be unavailable during an update.", + "nodepool_validators": "If set (larger than 0), then it must be at least the amount of zones configured for the nodepool. The `max_surge` and `max_unavailable` fields cannot both be unset at the same time.", + "region": "The resource region. If not defined, the provider region is used.", + "access": "Configure access to the cluster", + "access_idp": "Configure IDP", + "access_idp_enabled": "Enable IDP integration for the cluster.", + "access_idp_type": "The IDP type. Possible values: 'stackit'.", +} + // Schema defines the schema for the resource. func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "SKE Cluster Resource schema. Must have a `region` specified in the provider configuration.", - "node_pools_plan_note": "When updating `node_pools` of a `stackit_ske_cluster`, the Terraform plan might appear incorrect as it matches the node pools by index rather than by name. " + - "However, the SKE API correctly identifies node pools by name and applies the intended changes. Please review your changes carefully to ensure the correct configuration will be applied.", - "max_surge": "Maximum number of additional VMs that are created during an update.", - "max_unavailable": "Maximum number of VMs that that can be unavailable during an update.", - "nodepool_validators": "If set (larger than 0), then it must be at least the amount of zones configured for the nodepool. The `max_surge` and `max_unavailable` fields cannot both be unset at the same time.", - "region": "The resource region. If not defined, the provider region is used.", - } resp.Schema = schema.Schema{ Description: fmt.Sprintf("%s\n%s", descriptions["main"], descriptions["node_pools_plan_note"]), @@ -738,6 +762,29 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re stringplanmodifier.RequiresReplace(), }, }, + "access": schema.SingleNestedAttribute{ + Description: descriptions["access"], + Optional: true, + Attributes: map[string]schema.Attribute{ + "idp": schema.SingleNestedAttribute{ + Description: descriptions["access_idp"], + Optional: true, + Attributes: map[string]schema.Attribute{ + "enabled": schema.BoolAttribute{ + Description: descriptions["access_idp_enabled"], + Required: true, + }, + "type": schema.StringAttribute{ + Description: descriptions["access_idp_type"], + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("stackit"), + }, + }, + }, + }, + }, + }, }, } } @@ -855,7 +902,7 @@ func sortK8sVersions(versions []ske.KubernetesVersion) { } // loadAvailableVersions loads the available k8s and machine versions from the API. -// The k8s versions are sorted descending order, i.e. the latest versions (including previews) +// The k8s versions are sorted descending order, i.e. the latest versions (including previews) // are listed first func (r *clusterResource) loadAvailableVersions(ctx context.Context, region string) ([]ske.KubernetesVersion, []ske.MachineImage, error) { res, err := r.skeClient.DefaultAPI.ListProviderOptions(ctx, region).Execute() @@ -938,6 +985,11 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating extension API payload: %v", err)) return } + access, err := toAccessPayload(ctx, model) + if err != nil { + core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating access API payload: %v", err)) + return + } payload := ske.CreateOrUpdateClusterPayload{ Extensions: extensions, @@ -946,6 +998,7 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag Maintenance: maintenance, Network: network, Nodepools: nodePools, + Access: access, } _, err = r.skeClient.DefaultAPI.CreateOrUpdateCluster(ctx, projectId, region, name).CreateOrUpdateClusterPayload(payload).Execute() if err != nil { @@ -1352,6 +1405,26 @@ func toExtensionsPayload(ctx context.Context, m *Model) (*ske.Extension, error) }, nil } +func toAccessPayload(ctx context.Context, m *Model) (*ske.Access, error) { + if utils.IsUndefined(m.Access) { + return nil, nil + } + access := access{} + diags := m.Access.As(ctx, &access, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("converting access object: %v", diags.Errors()) + } + idp := idp{} + diags = access.IDP.As(ctx, &idp, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("converting idp object: %v", diags.Errors()) + } + + return &ske.Access{ + Idp: ske.NewIDP(idp.Enabled.ValueBool(), idp.Type.ValueString()), + }, nil +} + func parseMaintenanceWindowTime(t string) (time.Time, error) { v, err := time.Parse("15:04:05-07:00", t) if err != nil { @@ -1491,6 +1564,10 @@ func mapFields(ctx context.Context, cl *ske.Cluster, m *Model, region string) er if err != nil { return fmt.Errorf("map extensions: %w", err) } + err = mapAccess(ctx, cl, m) + if err != nil { + return fmt.Errorf("map access: %w", err) + } return nil } @@ -1986,6 +2063,40 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { return nil } +func mapAccess(ctx context.Context, cl *ske.Cluster, m *Model) error { + if cl.Access == nil { + m.Access = types.ObjectNull(accessTypes) + return nil + } + + var diags diag.Diagnostics + + var idpObject basetypes.ObjectValue + if cl.Access.Idp == nil { + idpObject = types.ObjectNull(idpTypes) + } else { + idp := idp{ + Enabled: types.BoolValue(cl.Access.Idp.Enabled), + Type: types.StringValue(cl.Access.Idp.Type), + } + idpObject, diags = types.ObjectValueFrom(ctx, idpTypes, idp) + if diags.HasError() { + return fmt.Errorf("creating idp object: %w", core.DiagsToError(diags)) + } + } + + access := access{ + IDP: idpObject, + } + accessObject, diags := types.ObjectValueFrom(ctx, accessTypes, access) + if diags.HasError() { + return fmt.Errorf("creating access object: %w", core.DiagsToError(diags)) + } + + m.Access = accessObject + return nil +} + func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion, currentKubernetesVersion *string, diags *diag.Diagnostics) (kubernetesPayload *ske.Kubernetes, hasDeprecatedVersion bool, err error) { providedVersionMin := m.KubernetesVersionMin.ValueStringPointer() versionUsed, hasDeprecatedVersion, err := latestMatchingKubernetesVersion(availableVersions, providedVersionMin, currentKubernetesVersion, diags) diff --git a/stackit/internal/services/ske/cluster/resource_test.go b/stackit/internal/services/ske/cluster/resource_test.go index aec9a8434..9a5d8194b 100644 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ b/stackit/internal/services/ske/cluster/resource_test.go @@ -54,6 +54,7 @@ func TestMapFields(t *testing.T) { PodAddressRanges: types.ListNull(types.StringType), Region: types.StringValue(testRegion), KubernetesVersionUsed: types.StringValue(""), + Access: types.ObjectNull(accessTypes), }, true, }, @@ -145,6 +146,12 @@ func TestMapFields(t *testing.T) { EgressAddressRanges: []string{"0.0.0.0/32", "1.1.1.1/32"}, PodAddressRanges: []string{"0.0.0.0/32", "1.1.1.1/32"}, }, + Access: &ske.Access{ + Idp: &ske.IDP{ + Enabled: true, + Type: "stackit", + }, + }, }, testRegion, Model{ @@ -261,6 +268,12 @@ func TestMapFields(t *testing.T) { }), }), Region: types.StringValue(testRegion), + Access: types.ObjectValueMust(accessTypes, map[string]attr.Value{ + "idp": types.ObjectValueMust(idpTypes, map[string]attr.Value{ + "enabled": types.BoolValue(true), + "type": types.StringValue("stackit"), + }), + }), }, true, }, @@ -2683,3 +2696,117 @@ func TestValidateConfig(t *testing.T) { }) } } + +func TestMapAccess(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input *ske.Access + want basetypes.ObjectValue + wantErr bool + }{ + { + name: "nil access", + input: nil, + want: types.ObjectNull(accessTypes), + }, + { + name: "nil IDP", + input: &ske.Access{}, + want: types.ObjectValueMust(accessTypes, map[string]attr.Value{ + "idp": types.ObjectNull(idpTypes), + }), + }, + { + name: "valid IDP", + input: &ske.Access{ + Idp: &ske.IDP{ + Enabled: true, + Type: "stackit", + }, + }, + want: types.ObjectValueMust(accessTypes, map[string]attr.Value{ + "idp": types.ObjectValueMust(idpTypes, map[string]attr.Value{ + "enabled": types.BoolValue(true), + "type": types.StringValue("stackit"), + }), + }), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + m := &Model{} + cluster := &ske.Cluster{ + Access: tt.input, + } + + err := mapAccess(t.Context(), cluster, m) + if !tt.wantErr && err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tt.wantErr && err == nil { + t.Fatalf("expected error, but got none") + } + if diff := cmp.Diff(tt.want, m.Access); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestToAccessPayload(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input basetypes.ObjectValue + want *ske.Access + wantErr bool + }{ + { + name: "null access", + input: types.ObjectNull(accessTypes), + want: nil, + }, + { + name: "unknown access", + input: types.ObjectUnknown(accessTypes), + want: nil, + }, + { + name: "valid access", + input: types.ObjectValueMust(accessTypes, map[string]attr.Value{ + "idp": types.ObjectValueMust(idpTypes, map[string]attr.Value{ + "enabled": types.BoolValue(true), + "type": types.StringValue("stackit"), + }), + }), + want: &ske.Access{ + Idp: &ske.IDP{ + Enabled: true, + Type: "stackit", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + m := &Model{ + Access: tt.input, + } + got, err := toAccessPayload(t.Context(), m) + if err != nil && !tt.wantErr { + t.Fatalf("unexpected error: %v", err) + } + if err == nil && tt.wantErr { + t.Fatalf("expected error, but got none") + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/stackit/internal/services/ske/ske_acc_test.go b/stackit/internal/services/ske/ske_acc_test.go index 0a5dffab9..b1c40653e 100644 --- a/stackit/internal/services/ske/ske_acc_test.go +++ b/stackit/internal/services/ske/ske_acc_test.go @@ -93,6 +93,7 @@ var testConfigVarsMax = config.Variables{ "dns_zone_name": config.StringVariable("acc-" + acctest.RandStringFromCharSet(6, acctest.CharSetAlpha)), "dns_name": config.StringVariable("acc-" + acctest.RandStringFromCharSet(6, acctest.CharSetAlpha) + ".runs.onstackit.cloud"), "network_control_plane_access_scope": config.StringVariable("PUBLIC"), + "access_idp_enabled": config.BoolVariable(true), } var testConfigDatasource = config.Variables{ @@ -111,6 +112,7 @@ func configVarsMaxUpdated() config.Variables { updatedConfig["kubernetes_version_min"] = config.StringVariable(skeProviderOptions.GetUpdateK8sVersion()) updatedConfig["nodepool_os_version_min"] = config.StringVariable(skeProviderOptions.GetUpdateMachineVersion()) updatedConfig["maintenance_end"] = config.StringVariable("03:03:03+00:00") + updatedConfig["access_idp_enabled"] = config.BoolVariable(false) return updatedConfig } @@ -323,6 +325,9 @@ func TestAccSKEMax(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "kubernetes_version_used"), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "network.control_plane.access_scope", testutil.ConvertConfigVariable(testConfigVarsMax["network_control_plane_access_scope"])), + // Access + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", "true"), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.type", "stackit"), // Kubeconfig resource.TestCheckResourceAttrPair( @@ -400,6 +405,9 @@ func TestAccSKEMax(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "kubernetes_version_used"), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "network.control_plane.access_scope", testutil.ConvertConfigVariable(testConfigVarsMax["network_control_plane_access_scope"])), + // Access + resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "access.idp.enabled", "true"), + resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "access.idp.type", "stackit"), ), }, // 3) Import cluster @@ -426,7 +434,7 @@ func TestAccSKEMax(t *testing.T) { // The fields are not provided in the SKE API when disabled, although set actively. ImportStateVerifyIgnore: []string{"kubernetes_version_min", "node_pools.0.os_version_min", "extensions.observability.%", "extensions.observability.instance_id", "extensions.observability.enabled"}, }, - // 4) Update kubernetes version, OS version and maintenance end, downgrade of kubernetes version + // 4) Update kubernetes version, OS version and maintenance end, downgrade of kubernetes version, set access.idp.enabled to false { Config: resourceMax, ConfigVariables: configVarsMaxUpdated(), @@ -487,6 +495,9 @@ func TestAccSKEMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "pod_address_ranges.#", "1"), resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "pod_address_ranges.0"), resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "kubernetes_version_used"), + // Access + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", "false"), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.type", "stackit"), ), }, // Deletion is done by the framework implicitly diff --git a/stackit/internal/services/ske/testdata/resource-max.tf b/stackit/internal/services/ske/testdata/resource-max.tf index fde7ff1cc..e5347854f 100644 --- a/stackit/internal/services/ske/testdata/resource-max.tf +++ b/stackit/internal/services/ske/testdata/resource-max.tf @@ -36,6 +36,7 @@ variable "refresh_before" {} variable "dns_zone_name" {} variable "dns_name" {} variable "network_control_plane_access_scope" {} +variable "access_idp_enabled" {} resource "stackit_ske_cluster" "cluster" { project_id = var.project_id @@ -98,6 +99,12 @@ resource "stackit_ske_cluster" "cluster" { access_scope = var.network_control_plane_access_scope } } + access = { + idp = { + enabled = var.access_idp_enabled + type = "stackit" + } + } } resource "stackit_ske_kubeconfig" "kubeconfig" { From 8eaf543d605c311ce1a9337571bb713ca46a89d5 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Tue, 23 Jun 2026 14:46:12 +0200 Subject: [PATCH 2/8] fix(ske): adjust access to use default, extend min test --- .../internal/services/ske/cluster/resource.go | 22 +++++++++++++++++-- stackit/internal/services/ske/ske_acc_test.go | 4 ++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index 4f84ac30f..438b5f0d8 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" serviceenablementUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceenablement/utils" skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" stringplanmodifierUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils/planmodifiers/stringplanmodifier" @@ -287,6 +288,21 @@ var idpTypes = map[string]attr.Type{ "type": basetypes.StringType{}, } +var defaultIdp = types.ObjectValueMust( + idpTypes, + map[string]attr.Value{ + "enabled": types.BoolValue(false), + "type": types.StringValue("stackit"), + }, +) + +var defaultAccess = types.ObjectValueMust( + accessTypes, + map[string]attr.Value{ + "idp": defaultIdp, + }, +) + // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. func (r *clusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform @@ -765,6 +781,8 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re "access": schema.SingleNestedAttribute{ Description: descriptions["access"], Optional: true, + Computed: true, + Default: objectdefault.StaticValue(defaultAccess), Attributes: map[string]schema.Attribute{ "idp": schema.SingleNestedAttribute{ Description: descriptions["access_idp"], @@ -2065,7 +2083,7 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { func mapAccess(ctx context.Context, cl *ske.Cluster, m *Model) error { if cl.Access == nil { - m.Access = types.ObjectNull(accessTypes) + m.Access = defaultAccess return nil } @@ -2073,7 +2091,7 @@ func mapAccess(ctx context.Context, cl *ske.Cluster, m *Model) error { var idpObject basetypes.ObjectValue if cl.Access.Idp == nil { - idpObject = types.ObjectNull(idpTypes) + idpObject = defaultIdp } else { idp := idp{ Enabled: types.BoolValue(cl.Access.Idp.Enabled), diff --git a/stackit/internal/services/ske/ske_acc_test.go b/stackit/internal/services/ske/ske_acc_test.go index b1c40653e..0b31cedbb 100644 --- a/stackit/internal/services/ske/ske_acc_test.go +++ b/stackit/internal/services/ske/ske_acc_test.go @@ -158,6 +158,10 @@ func TestAccSKEMin(t *testing.T) { "stackit_ske_kubeconfig.kubeconfig", "cluster_name", "stackit_ske_cluster.cluster", "name", ), + + // Access: resource-min does not define an access block, we expect idp: { enabled: false, type: stackit } here because of the default + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", "false"), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.type", "stackit"), ), }, // 2) Data source From eb171357a88c097f0c4587e848061748e7d7cca1 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Tue, 23 Jun 2026 14:52:29 +0200 Subject: [PATCH 3/8] fix(ske): generate docs --- docs/resources/ske_cluster.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/resources/ske_cluster.md b/docs/resources/ske_cluster.md index 572ba210c..e18ebe7aa 100644 --- a/docs/resources/ske_cluster.md +++ b/docs/resources/ske_cluster.md @@ -65,6 +65,7 @@ To keep your Terraform plans clean and readable, always append new node pools to ### Optional +- `access` (Attributes) Configure access to the cluster (see [below for nested schema](#nestedatt--access)) - `extensions` (Attributes) A single extensions block as defined below. (see [below for nested schema](#nestedatt--extensions)) - `hibernations` (Attributes List) One or more hibernation block as defined below. (see [below for nested schema](#nestedatt--hibernations)) - `kubernetes_version_min` (String) The minimum Kubernetes version. This field will be used to set the minimum kubernetes version on creation/update of the cluster. If unset, the latest supported Kubernetes version will be used. SKE automatically updates the cluster Kubernetes version if you have set `maintenance.enable_kubernetes_version_updates` to true or if there is a mandatory update, as described in [General information for Kubernetes & OS updates](https://docs.stackit.cloud/products/runtime/kubernetes-engine/basics/version-updates/). To get the current kubernetes version being used for your cluster, use the read-only `kubernetes_version_used` field. @@ -122,6 +123,23 @@ Optional: + +### Nested Schema for `access` + +Optional: + +- `idp` (Attributes) Configure IDP (see [below for nested schema](#nestedatt--access--idp)) + + +### Nested Schema for `access.idp` + +Required: + +- `enabled` (Boolean) Enable IDP integration for the cluster. +- `type` (String) The IDP type. Possible values: 'stackit'. + + + ### Nested Schema for `extensions` From f05e51dda260bff5de3a241327db7f50afe9198e Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Tue, 23 Jun 2026 15:05:49 +0200 Subject: [PATCH 4/8] fix(ske): linting issues --- stackit/internal/services/ske/cluster/resource.go | 2 +- stackit/internal/services/ske/testdata/resource-max.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index 4b86de750..f56a9727b 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -11,6 +11,7 @@ import ( "time" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" + serviceenablementUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceenablement/utils" skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" stringplanmodifierUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils/planmodifiers/stringplanmodifier" @@ -431,7 +432,6 @@ var descriptions = map[string]string{ // Schema defines the schema for the resource. func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ Description: fmt.Sprintf("%s\n%s", descriptions["main"], descriptions["node_pools_plan_note"]), // Callout block: https://developer.hashicorp.com/terraform/registry/providers/docs#callouts diff --git a/stackit/internal/services/ske/testdata/resource-max.tf b/stackit/internal/services/ske/testdata/resource-max.tf index e5347854f..92377aa0e 100644 --- a/stackit/internal/services/ske/testdata/resource-max.tf +++ b/stackit/internal/services/ske/testdata/resource-max.tf @@ -102,7 +102,7 @@ resource "stackit_ske_cluster" "cluster" { access = { idp = { enabled = var.access_idp_enabled - type = "stackit" + type = "stackit" } } } From f5c1ca2f389e16b7aa08a5a75b74410bac70a2b7 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Tue, 23 Jun 2026 15:14:49 +0200 Subject: [PATCH 5/8] fix(ske): adapt mapping tests to default values --- .../services/ske/cluster/resource_test.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/stackit/internal/services/ske/cluster/resource_test.go b/stackit/internal/services/ske/cluster/resource_test.go index 88f4f0531..13c2ba5c9 100644 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ b/stackit/internal/services/ske/cluster/resource_test.go @@ -54,7 +54,7 @@ func TestMapFields(t *testing.T) { PodAddressRanges: types.ListNull(types.StringType), Region: types.StringValue(testRegion), KubernetesVersionUsed: types.StringValue(""), - Access: types.ObjectNull(accessTypes), + Access: defaultAccess, }, true, }, @@ -299,6 +299,7 @@ func TestMapFields(t *testing.T) { PodAddressRanges: types.ListNull(types.StringType), KubernetesVersionUsed: types.StringValue(""), Region: types.StringValue(testRegion), + Access: defaultAccess, }, true, }, @@ -350,6 +351,7 @@ func TestMapFields(t *testing.T) { }), KubernetesVersionUsed: types.StringValue(""), Region: types.StringValue(testRegion), + Access: defaultAccess, }, true, }, @@ -402,6 +404,7 @@ func TestMapFields(t *testing.T) { }), KubernetesVersionUsed: types.StringValue(""), Region: types.StringValue(testRegion), + Access: defaultAccess, }, true, }, @@ -467,6 +470,7 @@ func TestMapFields(t *testing.T) { }), KubernetesVersionUsed: types.StringValue(""), Region: types.StringValue(testRegion), + Access: defaultAccess, }, true, }, @@ -491,6 +495,7 @@ func TestMapFields(t *testing.T) { PodAddressRanges: types.ListNull(types.StringType), KubernetesVersionUsed: types.StringValue(""), Region: types.StringValue(testRegion), + Access: defaultAccess, }, true, }, @@ -702,6 +707,7 @@ func TestMapFields(t *testing.T) { }), }), Region: types.StringValue(testRegion), + Access: defaultAccess, }, true, }, @@ -2706,15 +2712,15 @@ func TestMapAccess(t *testing.T) { wantErr bool }{ { - name: "nil access", + name: "nil access defaults to idp: { enabled: false, type: stackit }", input: nil, - want: types.ObjectNull(accessTypes), + want: defaultAccess, }, { - name: "nil IDP", + name: "nil IDP, default", input: &ske.Access{}, want: types.ObjectValueMust(accessTypes, map[string]attr.Value{ - "idp": types.ObjectNull(idpTypes), + "idp": defaultIdp, }), }, { From e37bf6f7915c3d3f11c60f4801ac03e01e615da3 Mon Sep 17 00:00:00 2001 From: cgoetz-inovex Date: Tue, 23 Jun 2026 17:31:37 +0200 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Manuel Vaas <34416897+Manuelvaas@users.noreply.github.com> --- stackit/internal/services/ske/ske_acc_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stackit/internal/services/ske/ske_acc_test.go b/stackit/internal/services/ske/ske_acc_test.go index 287d162a8..b1b97243d 100644 --- a/stackit/internal/services/ske/ske_acc_test.go +++ b/stackit/internal/services/ske/ske_acc_test.go @@ -329,7 +329,7 @@ func TestAccSKEMax(t *testing.T) { resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "network.control_plane.access_scope", testutil.ConvertConfigVariable(testConfigVarsMax["network_control_plane_access_scope"])), // Access - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", "true"), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["access_idp_enabled"])), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.type", "stackit"), // Kubeconfig @@ -406,7 +406,7 @@ func TestAccSKEMax(t *testing.T) { resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "pod_address_ranges.0"), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "network.control_plane.access_scope", testutil.ConvertConfigVariable(testConfigVarsMax["network_control_plane_access_scope"])), // Access - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "access.idp.enabled", "true"), + resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "access.idp.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["access_idp_enabled"])), resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "access.idp.type", "stackit"), ), }, @@ -496,7 +496,7 @@ func TestAccSKEMax(t *testing.T) { resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "pod_address_ranges.0"), resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "kubernetes_version_used"), // Access - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", "false"), + resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.enabled", testutil.ConvertConfigVariable(configVarsMaxUpdated()["access_idp_enabled"])), resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "access.idp.type", "stackit"), ), }, From eb5ebf73a6c351042e432a9bdc2541390ec78a88 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Fri, 26 Jun 2026 15:06:23 +0200 Subject: [PATCH 7/8] fix(ske): moved access defaults deeper in schema --- docs/resources/ske_cluster.md | 2 +- .../internal/services/ske/cluster/resource.go | 51 +++++-------------- .../services/ske/cluster/resource_test.go | 15 ++++++ stackit/internal/services/ske/ske_test.go | 6 +++ 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/docs/resources/ske_cluster.md b/docs/resources/ske_cluster.md index e18ebe7aa..5850f36bc 100644 --- a/docs/resources/ske_cluster.md +++ b/docs/resources/ske_cluster.md @@ -133,7 +133,7 @@ Optional: ### Nested Schema for `access.idp` -Required: +Optional: - `enabled` (Boolean) Enable IDP integration for the cluster. - `type` (String) The IDP type. Possible values: 'stackit'. diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index f56a9727b..17ccba434 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -10,8 +10,6 @@ import ( "strings" "time" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" - serviceenablementUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceenablement/utils" skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" stringplanmodifierUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils/planmodifiers/stringplanmodifier" @@ -301,21 +299,6 @@ var idpTypes = map[string]attr.Type{ "type": basetypes.StringType{}, } -var defaultIdp = types.ObjectValueMust( - idpTypes, - map[string]attr.Value{ - "enabled": types.BoolValue(false), - "type": types.StringValue("stackit"), - }, -) - -var defaultAccess = types.ObjectValueMust( - accessTypes, - map[string]attr.Value{ - "idp": defaultIdp, - }, -) - // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. func (r *clusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform @@ -839,19 +822,23 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re Description: descriptions["access"], Optional: true, Computed: true, - Default: objectdefault.StaticValue(defaultAccess), Attributes: map[string]schema.Attribute{ "idp": schema.SingleNestedAttribute{ Description: descriptions["access_idp"], Optional: true, + Computed: true, Attributes: map[string]schema.Attribute{ "enabled": schema.BoolAttribute{ Description: descriptions["access_idp_enabled"], - Required: true, + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), }, "type": schema.StringAttribute{ Description: descriptions["access_idp_type"], - Required: true, + Optional: true, + Computed: true, + Default: stringdefault.StaticString("stackit"), Validators: []validator.String{ stringvalidator.OneOf("stackit"), }, @@ -2139,25 +2126,15 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { } func mapAccess(ctx context.Context, cl *ske.Cluster, m *Model) error { - if cl.Access == nil { - m.Access = defaultAccess - return nil - } - var diags diag.Diagnostics - var idpObject basetypes.ObjectValue - if cl.Access.Idp == nil { - idpObject = defaultIdp - } else { - idp := idp{ - Enabled: types.BoolValue(cl.Access.Idp.Enabled), - Type: types.StringValue(cl.Access.Idp.Type), - } - idpObject, diags = types.ObjectValueFrom(ctx, idpTypes, idp) - if diags.HasError() { - return fmt.Errorf("creating idp object: %w", core.DiagsToError(diags)) - } + idp := idp{ + Enabled: types.BoolValue(cl.Access.Idp.Enabled), + Type: types.StringValue(cl.Access.Idp.Type), + } + idpObject, diags := types.ObjectValueFrom(ctx, idpTypes, idp) + if diags.HasError() { + return fmt.Errorf("creating idp object: %w", core.DiagsToError(diags)) } access := access{ diff --git a/stackit/internal/services/ske/cluster/resource_test.go b/stackit/internal/services/ske/cluster/resource_test.go index 13c2ba5c9..3dfae49c7 100644 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ b/stackit/internal/services/ske/cluster/resource_test.go @@ -2816,3 +2816,18 @@ func TestToAccessPayload(t *testing.T) { }) } } + +var defaultIdp = types.ObjectValueMust( + idpTypes, + map[string]attr.Value{ + "enabled": types.BoolValue(false), + "type": types.StringValue("stackit"), + }, +) + +var defaultAccess = types.ObjectValueMust( + accessTypes, + map[string]attr.Value{ + "idp": defaultIdp, + }, +) diff --git a/stackit/internal/services/ske/ske_test.go b/stackit/internal/services/ske/ske_test.go index 493de9382..96fcc9bbf 100644 --- a/stackit/internal/services/ske/ske_test.go +++ b/stackit/internal/services/ske/ske_test.go @@ -246,6 +246,12 @@ resource "stackit_ske_cluster" "cluster" { PodAddressRanges: []string{"100.64.0.0/10"}, }), Extensions: new(ske.Extension{}), + Access: &ske.Access{ + Idp: &ske.IDP{ + Enabled: false, + Type: "stackit", + }, + }, } resource.UnitTest(t, resource.TestCase{ From 727ea1656cf73e6aa6c08571f10ae1445eb276c5 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Fri, 26 Jun 2026 15:49:47 +0200 Subject: [PATCH 8/8] fix(ske): adjust tests to new access behaviour, comment missing nil safety --- .../internal/services/ske/cluster/resource.go | 1 + .../services/ske/cluster/resource_test.go | 54 ++++++++++++++----- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index 17ccba434..8c395ce59 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -2128,6 +2128,7 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { func mapAccess(ctx context.Context, cl *ske.Cluster, m *Model) error { var diags diag.Diagnostics + // explicitly no nil checks, the API won't return nil values here, despite the types suggesting the possibility idp := idp{ Enabled: types.BoolValue(cl.Access.Idp.Enabled), Type: types.StringValue(cl.Access.Idp.Type), diff --git a/stackit/internal/services/ske/cluster/resource_test.go b/stackit/internal/services/ske/cluster/resource_test.go index 3dfae49c7..5112cd777 100644 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ b/stackit/internal/services/ske/cluster/resource_test.go @@ -39,6 +39,12 @@ func TestMapFields(t *testing.T) { types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), &ske.Cluster{ Name: new("name"), + Access: &ske.Access{ + Idp: &ske.IDP{ + Enabled: false, + Type: "stackit", + }, + }, }, testRegion, Model{ @@ -284,6 +290,12 @@ func TestMapFields(t *testing.T) { &ske.Cluster{ Name: new("name"), Network: &ske.Network{}, + Access: &ske.Access{ + Idp: &ske.IDP{ + Enabled: false, + Type: "stackit", + }, + }, }, testRegion, Model{ @@ -323,6 +335,12 @@ func TestMapFields(t *testing.T) { }, }, Name: new("name"), + Access: &ske.Access{ + Idp: &ske.IDP{ + Enabled: false, + Type: "stackit", + }, + }, }, testRegion, Model{ @@ -376,6 +394,12 @@ func TestMapFields(t *testing.T) { &ske.Cluster{ Extensions: &ske.Extension{}, Name: new("name"), + Access: &ske.Access{ + Idp: &ske.IDP{ + Enabled: false, + Type: "stackit", + }, + }, }, testRegion, Model{ @@ -440,6 +464,12 @@ func TestMapFields(t *testing.T) { }, }, Name: new("name"), + Access: &ske.Access{ + Idp: &ske.IDP{ + Enabled: false, + Type: "stackit", + }, + }, }, testRegion, Model{ @@ -481,6 +511,12 @@ func TestMapFields(t *testing.T) { &ske.Cluster{ Extensions: &ske.Extension{}, Name: new("name"), + Access: &ske.Access{ + Idp: &ske.IDP{ + Enabled: false, + Type: "stackit", + }, + }, }, testRegion, Model{ @@ -615,6 +651,12 @@ func TestMapFields(t *testing.T) { Error: nil, Hibernated: nil, }, + Access: &ske.Access{ + Idp: &ske.IDP{ + Enabled: false, + Type: "stackit", + }, + }, }, testRegion, Model{ @@ -2711,18 +2753,6 @@ func TestMapAccess(t *testing.T) { want basetypes.ObjectValue wantErr bool }{ - { - name: "nil access defaults to idp: { enabled: false, type: stackit }", - input: nil, - want: defaultAccess, - }, - { - name: "nil IDP, default", - input: &ske.Access{}, - want: types.ObjectValueMust(accessTypes, map[string]attr.Value{ - "idp": defaultIdp, - }), - }, { name: "valid IDP", input: &ske.Access{