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" {