From 833981ea941f4f8dfa1c4d75f9984dbb23ef92a4 Mon Sep 17 00:00:00 2001 From: Jakub Boguslaw Date: Wed, 28 Oct 2026 15:08:31 +0100 Subject: [PATCH] feat(ske): add cni selection (calico or cilium) to cluster network --- docs/data-sources/ske_cluster.md | 18 +++ docs/resources/ske_cluster.md | 21 ++++ .../resources/stackit_ske_cluster/resource.tf | 3 + .../services/ske/cluster/datasource.go | 16 +++ .../internal/services/ske/cluster/resource.go | 111 +++++++++++++++++- .../services/ske/cluster/resource_test.go | 34 +++++- stackit/internal/services/ske/ske_test.go | 3 + stackit/internal/services/ske/utils/util.go | 2 +- 8 files changed, 201 insertions(+), 7 deletions(-) diff --git a/docs/data-sources/ske_cluster.md b/docs/data-sources/ske_cluster.md index b2a46ee9d..48a19458d 100644 --- a/docs/data-sources/ske_cluster.md +++ b/docs/data-sources/ske_cluster.md @@ -155,9 +155,27 @@ Read-Only: Read-Only: +- `cni` (Attributes) Container Network Interface (CNI) of the cluster. (see [below for nested schema](#nestedatt--network--cni)) - `control_plane` (Attributes) Control plane for the cluster. (see [below for nested schema](#nestedatt--network--control_plane)) - `id` (String) ID of the STACKIT Network Area (SNA) network into which the cluster will be deployed. + +### Nested Schema for `network.cni` + +Read-Only: + +- `calico` (Attributes) Set if Calico is used as CNI. (see [below for nested schema](#nestedatt--network--cni--calico)) +- `cilium` (Attributes) Set if Cilium is used as CNI. (see [below for nested schema](#nestedatt--network--cni--cilium)) + + +### Nested Schema for `network.cni.calico` + + + +### Nested Schema for `network.cni.cilium` + + + ### Nested Schema for `network.control_plane` diff --git a/docs/resources/ske_cluster.md b/docs/resources/ske_cluster.md index 58c02c079..f9aa2c6f6 100644 --- a/docs/resources/ske_cluster.md +++ b/docs/resources/ske_cluster.md @@ -38,6 +38,9 @@ resource "stackit_ske_cluster" "example" { control_plane = { access_scope = "PUBLIC" } + cni = { + cilium = {} + } } # Cluster audit log forwarding to a Telemetry Router. # Private preview: only configurable for enabled accounts. @@ -240,9 +243,27 @@ Optional: Optional: +- `cni` (Attributes) Container Network Interface (CNI) of the cluster. Exactly one of `calico` or `cilium` can be set. Defaults to Cilium. The field is immutable! (see [below for nested schema](#nestedatt--network--cni)) - `control_plane` (Attributes) Control plane for the cluster. (see [below for nested schema](#nestedatt--network--control_plane)) - `id` (String) ID of the STACKIT Network Area (SNA) network into which the cluster will be deployed. + +### Nested Schema for `network.cni` + +Optional: + +- `calico` (Attributes) Use Calico as CNI. Set to `{}` to enable. (see [below for nested schema](#nestedatt--network--cni--calico)) +- `cilium` (Attributes) Use Cilium as CNI. Set to `{}` to enable. (see [below for nested schema](#nestedatt--network--cni--cilium)) + + +### Nested Schema for `network.cni.calico` + + + +### Nested Schema for `network.cni.cilium` + + + ### Nested Schema for `network.control_plane` diff --git a/examples/resources/stackit_ske_cluster/resource.tf b/examples/resources/stackit_ske_cluster/resource.tf index 3cb37acd6..75949a5c6 100644 --- a/examples/resources/stackit_ske_cluster/resource.tf +++ b/examples/resources/stackit_ske_cluster/resource.tf @@ -20,6 +20,9 @@ resource "stackit_ske_cluster" "example" { control_plane = { access_scope = "PUBLIC" } + cni = { + cilium = {} + } } # Cluster audit log forwarding to a Telemetry Router. # Private preview: only configurable for enabled accounts. diff --git a/stackit/internal/services/ske/cluster/datasource.go b/stackit/internal/services/ske/cluster/datasource.go index 86794bd1a..21e8ce51b 100644 --- a/stackit/internal/services/ske/cluster/datasource.go +++ b/stackit/internal/services/ske/cluster/datasource.go @@ -238,6 +238,22 @@ func (r *clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest }, }, }, + "cni": schema.SingleNestedAttribute{ + Description: "Container Network Interface (CNI) of the cluster.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "calico": schema.SingleNestedAttribute{ + Description: "Set if Calico is used as CNI.", + Computed: true, + Attributes: map[string]schema.Attribute{}, + }, + "cilium": schema.SingleNestedAttribute{ + Description: "Set if Cilium is used as CNI.", + Computed: true, + Attributes: map[string]schema.Attribute{}, + }, + }, + }, }, }, diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index 3130f8065..9abbab035 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -179,14 +179,31 @@ var maintenanceTypes = map[string]attr.Type{ type network struct { ID types.String `tfsdk:"id"` ControlPlane types.Object `tfsdk:"control_plane"` + CNI types.Object `tfsdk:"cni"` } // Types corresponding to network var networkTypes = map[string]attr.Type{ "id": basetypes.StringType{}, "control_plane": types.ObjectType{AttrTypes: controlPlaneTypes}, + "cni": types.ObjectType{AttrTypes: cniTypes}, } +// Struct corresponding to network.CNI +type cni struct { + Calico types.Object `tfsdk:"calico"` + Cilium types.Object `tfsdk:"cilium"` +} + +// Types corresponding to cni +var cniTypes = map[string]attr.Type{ + "calico": types.ObjectType{AttrTypes: map[string]attr.Type{}}, + "cilium": types.ObjectType{AttrTypes: map[string]attr.Type{}}, +} + +// The Cilium CNI is not modelled by the SDK yet and is sent as an additional property. +const ciliumCNIKey = "cilium" + type controlPlane struct { AccessScope types.String `tfsdk:"access_scope"` } @@ -752,6 +769,36 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re }, }, }, + "cni": schema.SingleNestedAttribute{ + Description: "Container Network Interface (CNI) of the cluster. Exactly one of `calico` or `cilium` can be set. Defaults to Cilium. The field is immutable!", + Optional: true, + Computed: true, + Default: objectdefault.StaticValue(types.ObjectValueMust(cniTypes, map[string]attr.Value{ + "calico": types.ObjectNull(map[string]attr.Type{}), + "cilium": types.ObjectValueMust(map[string]attr.Type{}, map[string]attr.Value{}), + })), + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplace(), + }, + Attributes: map[string]schema.Attribute{ + "calico": schema.SingleNestedAttribute{ + Description: "Use Calico as CNI. Set to `{}` to enable.", + Optional: true, + Validators: []validator.Object{ + objectvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("cilium")), + }, + Attributes: map[string]schema.Attribute{}, + }, + "cilium": schema.SingleNestedAttribute{ + Description: "Use Cilium as CNI. Set to `{}` to enable.", + Optional: true, + Validators: []validator.Object{ + objectvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("calico")), + }, + Attributes: map[string]schema.Attribute{}, + }, + }, + }, }, }, "hibernations": schema.ListNestedAttribute{ @@ -1556,7 +1603,7 @@ func toMaintenancePayload(ctx context.Context, m *Model) (*ske.Maintenance, erro func toNetworkPayload(ctx context.Context, m *Model) (*ske.Network, error) { if m.Network.IsNull() || m.Network.IsUnknown() { - return nil, nil + return &ske.Network{Cni: defaultCNIPayload()}, nil } network := network{} @@ -1577,12 +1624,43 @@ func toNetworkPayload(ctx context.Context, m *Model) (*ske.Network, error) { } } + networkCNI, err := toCNIPayload(ctx, network.CNI) + if err != nil { + return nil, err + } + return &ske.Network{ Id: conversion.StringValueToPointer(network.ID), ControlPlane: networkControlPlane, + Cni: networkCNI, }, nil } +func defaultCNIPayload() *ske.CNI { + return &ske.CNI{AdditionalProperties: map[string]interface{}{ciliumCNIKey: map[string]interface{}{}}} +} + +func toCNIPayload(ctx context.Context, o types.Object) (*ske.CNI, error) { + if utils.IsUndefined(o) { + return defaultCNIPayload(), nil + } + + cniModel := cni{} + diags := o.As(ctx, &cniModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("converting network cni: %w", core.DiagsToError(diags)) + } + + payload := &ske.CNI{} + switch { + case !utils.IsUndefined(cniModel.Calico): + payload.Calico = map[string]interface{}{} + case !utils.IsUndefined(cniModel.Cilium): + payload.AdditionalProperties = map[string]interface{}{ciliumCNIKey: map[string]interface{}{}} + } + return payload, nil +} + func mapFields(ctx context.Context, cl *ske.Cluster, m *Model, region string) error { if cl == nil { return fmt.Errorf("response input is nil") @@ -1914,9 +1992,15 @@ func mapNetwork(cl *ske.Cluster, m *Model) error { } } + networkCNI, err := mapCNI(cl.Network.Cni) + if err != nil { + return err + } + networkValues := map[string]attr.Value{ "id": id, "control_plane": networkControlPlane, + "cni": networkCNI, } networkObject, diags := types.ObjectValue(networkTypes, networkValues) if diags.HasError() { @@ -1926,6 +2010,31 @@ func mapNetwork(cl *ske.Cluster, m *Model) error { return nil } +func mapCNI(c *ske.CNI) (types.Object, error) { + if c == nil { + return types.ObjectNull(cniTypes), nil + } + + emptyObject := types.ObjectValueMust(map[string]attr.Type{}, map[string]attr.Value{}) + calico := types.ObjectNull(cniTypes["calico"].(types.ObjectType).AttrTypes) + cilium := types.ObjectNull(cniTypes["cilium"].(types.ObjectType).AttrTypes) + if c.Calico != nil { + calico = emptyObject + } + if _, ok := c.AdditionalProperties[ciliumCNIKey]; ok { + cilium = emptyObject + } + + obj, diags := types.ObjectValue(cniTypes, map[string]attr.Value{ + "calico": calico, + "cilium": cilium, + }) + if diags.HasError() { + return types.ObjectNull(cniTypes), fmt.Errorf("create network cni object: %w", core.DiagsToError(diags)) + } + return obj, nil +} + func getMaintenanceTimes(ctx context.Context, cl *ske.Cluster, m *Model) (startTime, endTime string, err error) { var startTimeAPI, endTimeAPI time.Time if cl.Maintenance.TimeWindow != nil { diff --git a/stackit/internal/services/ske/cluster/resource_test.go b/stackit/internal/services/ske/cluster/resource_test.go index 8e7b4f7f7..e2591a119 100644 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ b/stackit/internal/services/ske/cluster/resource_test.go @@ -243,7 +243,8 @@ func TestMapFields(t *testing.T) { "end": types.StringValue("13:14:15Z"), }), Network: types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "id": types.StringValue("nid"), + "cni": types.ObjectNull(cniTypes), + "id": types.StringValue("nid"), "control_plane": types.ObjectValueMust(controlPlaneTypes, map[string]attr.Value{ "access_scope": types.StringValue("SNA"), }), @@ -770,7 +771,8 @@ func TestMapFields(t *testing.T) { "end": types.StringValue("13:14:15Z"), }), Network: types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "id": types.StringValue("nid"), + "cni": types.ObjectNull(cniTypes), + "id": types.StringValue("nid"), "control_plane": types.ObjectValueMust(controlPlaneTypes, map[string]attr.Value{ "access_scope": types.StringValue("SNA"), }), @@ -2365,7 +2367,8 @@ func TestToNetworkPayload(t *testing.T) { ProjectId: types.StringValue("pid"), Name: types.StringValue("name"), Network: types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "id": types.StringValue("nid"), + "cni": types.ObjectNull(cniTypes), + "id": types.StringValue("nid"), "control_plane": types.ObjectValueMust(controlPlaneTypes, map[string]attr.Value{ "access_scope": types.StringValue("SNA"), }), @@ -2376,6 +2379,7 @@ func TestToNetworkPayload(t *testing.T) { ControlPlane: &ske.V2ControlPlaneNetwork{ AccessScope: ske.ACCESSSCOPE_SNA.Ptr(), }, + Cni: &ske.CNI{AdditionalProperties: map[string]interface{}{"cilium": map[string]interface{}{}}}, }, true, }, @@ -2385,11 +2389,12 @@ func TestToNetworkPayload(t *testing.T) { ProjectId: types.StringValue("pid"), Name: types.StringValue("name"), Network: types.ObjectValueMust(networkTypes, map[string]attr.Value{ + "cni": types.ObjectNull(cniTypes), "id": types.StringNull(), "control_plane": types.ObjectNull(controlPlaneTypes), }), }, - &ske.Network{}, + &ske.Network{Cni: &ske.CNI{AdditionalProperties: map[string]interface{}{"cilium": map[string]interface{}{}}}}, true, }, { @@ -2398,6 +2403,7 @@ func TestToNetworkPayload(t *testing.T) { ProjectId: types.StringValue("pid"), Name: types.StringValue("name"), Network: types.ObjectValueMust(networkTypes, map[string]attr.Value{ + "cni": types.ObjectNull(cniTypes), "id": types.StringValue("nid"), "control_plane": types.ObjectNull(controlPlaneTypes), }), @@ -2405,6 +2411,7 @@ func TestToNetworkPayload(t *testing.T) { &ske.Network{ Id: new("nid"), ControlPlane: nil, + Cni: &ske.CNI{AdditionalProperties: map[string]interface{}{"cilium": map[string]interface{}{}}}, }, true, }, @@ -2415,7 +2422,24 @@ func TestToNetworkPayload(t *testing.T) { Name: types.StringValue("name"), Network: types.ObjectNull(networkTypes), }, - nil, + &ske.Network{Cni: &ske.CNI{AdditionalProperties: map[string]interface{}{"cilium": map[string]interface{}{}}}}, + true, + }, + { + "calico", + &Model{ + ProjectId: types.StringValue("pid"), + Name: types.StringValue("name"), + Network: types.ObjectValueMust(networkTypes, map[string]attr.Value{ + "id": types.StringNull(), + "control_plane": types.ObjectNull(controlPlaneTypes), + "cni": types.ObjectValueMust(cniTypes, map[string]attr.Value{ + "calico": types.ObjectValueMust(map[string]attr.Type{}, map[string]attr.Value{}), + "cilium": types.ObjectNull(map[string]attr.Type{}), + }), + }), + }, + &ske.Network{Cni: &ske.CNI{Calico: map[string]interface{}{}}}, true, }, } diff --git a/stackit/internal/services/ske/ske_test.go b/stackit/internal/services/ske/ske_test.go index 7c78e3d11..7a3f725fe 100644 --- a/stackit/internal/services/ske/ske_test.go +++ b/stackit/internal/services/ske/ske_test.go @@ -229,6 +229,9 @@ resource "stackit_ske_cluster" "cluster" { ControlPlane: new(ske.V2ControlPlaneNetwork{ AccessScope: new(ske.ACCESSSCOPE_PUBLIC), }), + Cni: &ske.CNI{ + AdditionalProperties: map[string]interface{}{"cilium": map[string]interface{}{}}, + }, }, Maintenance: new(ske.Maintenance{ AutoUpdate: &ske.MaintenanceAutoUpdate{ diff --git a/stackit/internal/services/ske/utils/util.go b/stackit/internal/services/ske/utils/util.go index 22c997bd4..3ae4f5732 100644 --- a/stackit/internal/services/ske/utils/util.go +++ b/stackit/internal/services/ske/utils/util.go @@ -5,7 +5,7 @@ import ( ) func IsEmptyNetwork(network *ske.Network) bool { - if !network.HasId() && !network.HasControlPlane() { + if !network.HasId() && !network.HasControlPlane() && !network.HasCni() { return true } return false