Skip to content
Open
18 changes: 18 additions & 0 deletions docs/data-sources/ske_cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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.

<a id="nestedatt--access"></a>
### Nested Schema for `access`

Read-Only:

- `idp` (Attributes) Configure IDP (see [below for nested schema](#nestedatt--access--idp))

<a id="nestedatt--access--idp"></a>
### Nested Schema for `access.idp`

Read-Only:

- `enabled` (Boolean) Enable IDP integration for the cluster.
- `type` (String) The IDP type. Possible values: 'stackit'.



<a id="nestedatt--extensions"></a>
### Nested Schema for `extensions`

Expand Down
18 changes: 18 additions & 0 deletions docs/resources/ske_cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -122,6 +123,23 @@ Optional:



<a id="nestedatt--access"></a>
### Nested Schema for `access`

Optional:

- `idp` (Attributes) Configure IDP (see [below for nested schema](#nestedatt--access--idp))

<a id="nestedatt--access--idp"></a>
### Nested Schema for `access.idp`

Optional:

- `enabled` (Boolean) Enable IDP integration for the cluster.
- `type` (String) The IDP type. Possible values: 'stackit'.



<a id="nestedatt--extensions"></a>
### Nested Schema for `extensions`

Expand Down
20 changes: 20 additions & 0 deletions stackit/internal/services/ske/cluster/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
},
},
},
}
}
Expand Down
129 changes: 118 additions & 11 deletions stackit/internal/services/ske/cluster/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,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]
Expand Down Expand Up @@ -280,6 +281,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
Expand Down Expand Up @@ -380,18 +399,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"]),
// Callout block: https://developer.hashicorp.com/terraform/registry/providers/docs#callouts
Expand Down Expand Up @@ -795,6 +818,35 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
stringplanmodifier.RequiresReplace(),
},
},
"access": schema.SingleNestedAttribute{
Description: descriptions["access"],
Optional: true,
Computed: true,
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"],
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
},
"type": schema.StringAttribute{
Description: descriptions["access_idp_type"],
Optional: true,
Computed: true,
Default: stringdefault.StaticString("stackit"),
Validators: []validator.String{
stringvalidator.OneOf("stackit"),
Comment thread
Manuelvaas marked this conversation as resolved.
},
},
},
},
},
},
},
}
}
Expand Down Expand Up @@ -912,7 +964,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()
Expand Down Expand Up @@ -995,6 +1047,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,
Expand All @@ -1003,6 +1060,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 {
Expand Down Expand Up @@ -1409,6 +1467,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{}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cgoetz-inovex for upcoming PRs, please don't shadow struct definitions with variable names if possible 😅

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but we can leave it here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm weird, usually the linter complains about shadowing

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 {
Expand Down Expand Up @@ -1548,6 +1626,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
}

Expand Down Expand Up @@ -2043,6 +2125,31 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error {
return nil
}

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),
}
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)
Expand Down
Loading
Loading