From 68ea711552f6ffe79c33c42127eaf85296ae0abf Mon Sep 17 00:00:00 2001 From: Fabian Schmitt <21021337+fabian-schmitt@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:40:13 +0200 Subject: [PATCH 1/8] feat(api): make network.id optional and add network status for managed networks --- api/v1alpha1/stackitcluster_types.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/stackitcluster_types.go b/api/v1alpha1/stackitcluster_types.go index 413452e..03db8d2 100644 --- a/api/v1alpha1/stackitcluster_types.go +++ b/api/v1alpha1/stackitcluster_types.go @@ -76,10 +76,11 @@ type StackitClusterSpec struct { // StackitClusterNetworkSpec references an existing STACKIT network. type StackitClusterNetworkSpec struct { // id is the STACKIT network ID. - // +required + // Omiting the network ID provisions a dedicated STACKIT network for the cluster, tag it as managed (owned) and stores the ID + // +optional // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:Pattern=`^[0-9a-fA-F-]{36}$` - ID string `json:"id"` + ID string `json:"id,omitempty"` } // StackitAPIServerLoadBalancerSpec configures an optional load balancer for @@ -160,6 +161,10 @@ type StackitClusterStatus struct { // +optional Initialization StackitClusterInitializationStatus `json:"initialization,omitempty"` + // network stores observed provider-managed network resources. + // +optional + Network StackitNetworkStatus `json:"network,omitempty"` + // apiServerEndpoint is the endpoint the Kubernetes API server is reachable at. // +optional APIServerEndpoint clusterv1.APIEndpoint `json:"apiServerEndpoint,omitempty,omitzero"` @@ -217,6 +222,14 @@ type StackitBastionStatus struct { CloudInitHash string `json:"cloudInitHash,omitempty"` } +// network contains status of a STACKIT network created by CAPSTK +type StackitNetworkStatus struct { + // id is the STACKIT network ID + // +optional + ID string `json:"id,omitempty"` + Managed bool `json:"managed,omitempty"` +} + // Condition types maintained by the StackitClusterReconciler. const ( ClusterReadyCondition = "Ready" From b5dc72e12d498fb80b97d52c31d80d830fd2993b Mon Sep 17 00:00:00 2001 From: Fabian Schmitt <21021337+fabian-schmitt@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:01:16 +0200 Subject: [PATCH 2/8] feat(api): make network.id optional and add network status for managed networks --- api/v1alpha1/stackitcluster_types.go | 1 - api/v1alpha1/stackitmachine_types.go | 9 ++++----- api/v1alpha1/zz_generated.deepcopy.go | 16 ++++++++++++++++ ...ructure.cluster.x-k8s.io_stackitclusters.yaml | 16 ++++++++++++---- ...cluster.x-k8s.io_stackitclustertemplates.yaml | 7 +++---- ...ructure.cluster.x-k8s.io_stackitmachines.yaml | 4 ---- ...cluster.x-k8s.io_stackitmachinetemplates.yaml | 4 ---- 7 files changed, 35 insertions(+), 22 deletions(-) diff --git a/api/v1alpha1/stackitcluster_types.go b/api/v1alpha1/stackitcluster_types.go index 03db8d2..712a38c 100644 --- a/api/v1alpha1/stackitcluster_types.go +++ b/api/v1alpha1/stackitcluster_types.go @@ -78,7 +78,6 @@ type StackitClusterNetworkSpec struct { // id is the STACKIT network ID. // Omiting the network ID provisions a dedicated STACKIT network for the cluster, tag it as managed (owned) and stores the ID // +optional - // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:Pattern=`^[0-9a-fA-F-]{36}$` ID string `json:"id,omitempty"` } diff --git a/api/v1alpha1/stackitmachine_types.go b/api/v1alpha1/stackitmachine_types.go index 208e77e..e849301 100644 --- a/api/v1alpha1/stackitmachine_types.go +++ b/api/v1alpha1/stackitmachine_types.go @@ -66,8 +66,8 @@ type StackitMachineSpec struct { RootVolume StackitRootVolumeSpec `json:"rootVolume,omitempty"` // network references the STACKIT network the VM should be attached to. - // +required - Network StackitMachineNetworkSpec `json:"network"` + // +optional + Network StackitMachineNetworkSpec `json:"network,omitempty"` // securityGroups is a list of STACKIT security group IDs applied to the VM. // +optional @@ -101,10 +101,9 @@ type StackitRootVolumeSpec struct { // StackitMachineNetworkSpec references an existing STACKIT network. type StackitMachineNetworkSpec struct { // id is the STACKIT network ID. - // +required - // +kubebuilder:validation:MinLength=1 + // +optional // +kubebuilder:validation:Pattern=`^[0-9a-fA-F-]{36}$` - ID string `json:"id"` + ID string `json:"id,omitempty"` } // StackitMachineStatus defines the observed state of StackitMachine. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d5d3fdf..e7247b8 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -217,6 +217,7 @@ func (in *StackitClusterSpec) DeepCopy() *StackitClusterSpec { func (in *StackitClusterStatus) DeepCopyInto(out *StackitClusterStatus) { *out = *in out.Initialization = in.Initialization + out.Network = in.Network out.APIServerEndpoint = in.APIServerEndpoint out.Bastion = in.Bastion if in.FailureDomains != nil { @@ -578,6 +579,21 @@ func (in *StackitMachineTemplateSpec) DeepCopy() *StackitMachineTemplateSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StackitNetworkStatus) DeepCopyInto(out *StackitNetworkStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackitNetworkStatus. +func (in *StackitNetworkStatus) DeepCopy() *StackitNetworkStatus { + if in == nil { + return nil + } + out := new(StackitNetworkStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StackitRootVolumeSpec) DeepCopyInto(out *StackitRootVolumeSpec) { *out = *in diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml index ac12a64..de59583 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml @@ -204,12 +204,11 @@ spec: For MVP the network must already exist. properties: id: - description: id is the STACKIT network ID. - minLength: 1 + description: |- + id is the STACKIT network ID. + Omiting the network ID provisions a dedicated STACKIT network for the cluster, tag it as managed (owned) and stores the ID pattern: ^[0-9a-fA-F-]{36}$ type: string - required: - - id type: object projectID: description: projectID is the STACKIT project that owns the cluster @@ -396,6 +395,15 @@ spec: is provisioned. type: boolean type: object + network: + description: network stores observed provider-managed network resources. + properties: + id: + description: id is the STACKIT network ID + type: string + managed: + type: boolean + type: object ready: description: ready indicates that the infrastructure required for the cluster is ready. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml index 202fe16..7d584e6 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml @@ -240,12 +240,11 @@ spec: For MVP the network must already exist. properties: id: - description: id is the STACKIT network ID. - minLength: 1 + description: |- + id is the STACKIT network ID. + Omiting the network ID provisions a dedicated STACKIT network for the cluster, tag it as managed (owned) and stores the ID pattern: ^[0-9a-fA-F-]{36}$ type: string - required: - - id type: object projectID: description: projectID is the STACKIT project that owns the diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitmachines.yaml index ae3aa81..bf02366 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitmachines.yaml @@ -96,11 +96,8 @@ spec: properties: id: description: id is the STACKIT network ID. - minLength: 1 pattern: ^[0-9a-fA-F-]{36}$ type: string - required: - - id type: object providerID: description: |- @@ -142,7 +139,6 @@ spec: required: - imageID - machineType - - network type: object status: description: status defines the observed state of StackitMachine diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitmachinetemplates.yaml index b09022b..82aa89b 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitmachinetemplates.yaml @@ -128,11 +128,8 @@ spec: properties: id: description: id is the STACKIT network ID. - minLength: 1 pattern: ^[0-9a-fA-F-]{36}$ type: string - required: - - id type: object providerID: description: |- @@ -175,7 +172,6 @@ spec: required: - imageID - machineType - - network type: object required: - spec From 52024ab6925edee321c9b202bbc9aee8a61e2803 Mon Sep 17 00:00:00 2001 From: Fabian Schmitt <21021337+fabian-schmitt@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:23:41 +0200 Subject: [PATCH 3/8] feat(webhook): allow optional network.id --- webhook/v1alpha1/validation_helpers.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/webhook/v1alpha1/validation_helpers.go b/webhook/v1alpha1/validation_helpers.go index d44d3f8..d6bd2ce 100644 --- a/webhook/v1alpha1/validation_helpers.go +++ b/webhook/v1alpha1/validation_helpers.go @@ -86,12 +86,15 @@ func validateStackitClusterSpec(spec infrav1.StackitClusterSpec, fldPath *field. allErrs = append(allErrs, validateRequiredPattern(spec.ProjectID, uuidPattern, fldPath.Child("projectID"), "must be a STACKIT project UUID")...) allErrs = append(allErrs, validateRequiredPattern(spec.Region, regionPattern, fldPath.Child("region"), "must use STACKIT region format, for example eu01")...) allErrs = append(allErrs, validateSecretReference(spec.CredentialsSecretRef, fldPath.Child("credentialsSecretRef"))...) - allErrs = append(allErrs, validateRequiredPattern(spec.Network.ID, uuidPattern, fldPath.Child("network", "id"), "must be a STACKIT network UUID")...) if !spec.APIServerLoadBalancer.Enabled && spec.ControlPlaneEndpoint.Host == "" { allErrs = append(allErrs, field.Required(fldPath.Child("controlPlaneEndpoint", "host"), "required when apiServerLoadBalancer.enabled is false")) } + if spec.Network.ID != "" && !uuidPattern.MatchString(spec.Network.ID) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("network", "id"), spec.Network.ID, "must be a STACKIT network UUID")) + } + allErrs = append(allErrs, validateBastionSpec(spec.Bastion, fldPath.Child("bastion"))...) allErrs = append(allErrs, validateAdditionalLabels(spec.AdditionalLabels, fldPath.Child("additionalLabels"))...) @@ -205,7 +208,11 @@ func validateStackitMachineSpec(spec infrav1.StackitMachineSpec, fldPath *field. if spec.SSHKeyName != "" && !sshKeyNamePattern.MatchString(spec.SSHKeyName) { allErrs = append(allErrs, field.Invalid(fldPath.Child("sshKeyName"), spec.SSHKeyName, "must be a valid STACKIT SSH key name")) } - allErrs = append(allErrs, validateRequiredPattern(spec.Network.ID, uuidPattern, fldPath.Child("network", "id"), "must be a STACKIT network UUID")...) + + if spec.Network.ID != "" && !uuidPattern.MatchString(spec.Network.ID) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("network", "id"), spec.Network.ID, "must be a STACKIT network UUID")) + } + allErrs = append(allErrs, validateRootVolume(spec.RootVolume, fldPath.Child("rootVolume"))...) allErrs = append(allErrs, validateSecurityGroups(spec.SecurityGroups, fldPath.Child("securityGroups"))...) allErrs = append(allErrs, validateAdditionalLabels(spec.AdditionalLabels, fldPath.Child("additionalLabels"))...) From 12dd6b8c115cd1b51dce258cf046ed2c91a82803 Mon Sep 17 00:00:00 2001 From: Fabian Schmitt <21021337+fabian-schmitt@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:48:22 +0200 Subject: [PATCH 4/8] feat(api): make network.id optional and add network status for managed networks --- api/v1alpha1/stackitcluster_types.go | 8 +-- ...ture.cluster.x-k8s.io_stackitclusters.yaml | 6 +-- ...ster.x-k8s.io_stackitclustertemplates.yaml | 6 +-- controller/stackitcluster_infrastructure.go | 49 ++++++++++--------- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/api/v1alpha1/stackitcluster_types.go b/api/v1alpha1/stackitcluster_types.go index 712a38c..ff38236 100644 --- a/api/v1alpha1/stackitcluster_types.go +++ b/api/v1alpha1/stackitcluster_types.go @@ -47,8 +47,8 @@ type StackitClusterSpec struct { // +required CredentialsSecretRef corev1.SecretReference `json:"credentialsSecretRef"` - // network references the existing STACKIT network the cluster will use. - // For MVP the network must already exist. + // network configures the STACKIT network the cluster will use. + // ommiting the ID will create a managed dedicated network // +required Network StackitClusterNetworkSpec `json:"network"` @@ -76,9 +76,9 @@ type StackitClusterSpec struct { // StackitClusterNetworkSpec references an existing STACKIT network. type StackitClusterNetworkSpec struct { // id is the STACKIT network ID. - // Omiting the network ID provisions a dedicated STACKIT network for the cluster, tag it as managed (owned) and stores the ID - // +optional + // Omitting the network ID provisions a dedicated STACKIT network for the cluster, tag it as managed (owned) and stores the ID // +kubebuilder:validation:Pattern=`^[0-9a-fA-F-]{36}$` + // +optional ID string `json:"id,omitempty"` } diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml index de59583..cef3c8a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml @@ -200,13 +200,13 @@ spec: x-kubernetes-map-type: atomic network: description: |- - network references the existing STACKIT network the cluster will use. - For MVP the network must already exist. + network configures the STACKIT network the cluster will use. + ommiting the ID will create a managed dedicated network properties: id: description: |- id is the STACKIT network ID. - Omiting the network ID provisions a dedicated STACKIT network for the cluster, tag it as managed (owned) and stores the ID + Omitting the network ID provisions a dedicated STACKIT network for the cluster, tag it as managed (owned) and stores the ID pattern: ^[0-9a-fA-F-]{36}$ type: string type: object diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml index 7d584e6..4566f51 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml @@ -236,13 +236,13 @@ spec: x-kubernetes-map-type: atomic network: description: |- - network references the existing STACKIT network the cluster will use. - For MVP the network must already exist. + network configures the STACKIT network the cluster will use. + ommiting the ID will create a managed dedicated network properties: id: description: |- id is the STACKIT network ID. - Omiting the network ID provisions a dedicated STACKIT network for the cluster, tag it as managed (owned) and stores the ID + Omitting the network ID provisions a dedicated STACKIT network for the cluster, tag it as managed (owned) and stores the ID pattern: ^[0-9a-fA-F-]{36}$ type: string type: object diff --git a/controller/stackitcluster_infrastructure.go b/controller/stackitcluster_infrastructure.go index f4c4f8e..de77b9e 100644 --- a/controller/stackitcluster_infrastructure.go +++ b/controller/stackitcluster_infrastructure.go @@ -88,28 +88,32 @@ func (r *StackitClusterReconciler) reconcileNormal(ctx context.Context, clusterS } clusterScope.SetFailureDomains(failureDomainsFromZones(stackitCluster.Spec.Region, zones)) - network, err := cloudClient.GetNetwork(ctx, stackitCluster.Spec.Network.ID) - if err != nil { - log.Error(err, "Failed to get network", - "networkID", stackitCluster.Spec.Network.ID, - "region", stackitCluster.Spec.Region, - ) - record.Warnf( - stackitCluster, "FailedGetNetwork", - "Failed to get network %s in region %s: %v", - stackitCluster.Spec.Network.ID, stackitCluster.Spec.Region, err, - ) - stackitCluster.Status.Ready = false - return util.CloudFailureResult( - &stackitCluster.Status.Conditions, - stackitCluster.Generation, - "NetworkNotFound", - err, - retryableErrorRequeueAfter, - false, - infrav1.ClusterNetworkReadyCondition, - infrav1.ClusterReadyCondition, - ) + if stackitCluster.Spec.Network.ID != "" { + network, err := cloudClient.GetNetwork(ctx, stackitCluster.Spec.Network.ID) + if err != nil { + log.Error(err, "Failed to get network", + "networkID", stackitCluster.Spec.Network.ID, + "region", stackitCluster.Spec.Region, + ) + record.Warnf( + stackitCluster, "FailedGetNetwork", + "Failed to get network %s in region %s: %v", + stackitCluster.Spec.Network.ID, stackitCluster.Spec.Region, err, + ) + stackitCluster.Status.Ready = false + return util.CloudFailureResult( + &stackitCluster.Status.Conditions, + stackitCluster.Generation, + "NetworkNotFound", + err, + retryableErrorRequeueAfter, + false, + infrav1.ClusterNetworkReadyCondition, + infrav1.ClusterReadyCondition, + ) + } + } else { + //TODO: implement cloudClient.EnsureNetwork } clusterScope.SetConditions( metav1.ConditionTrue, @@ -453,6 +457,7 @@ func (r *StackitClusterReconciler) reconcileDelete(ctx context.Context, clusterS clusterScope.ClearBastionStatus() record.Event(stackitCluster, "BastionDeleted", "Deleted bastion") } + // TODO: implement cloudClient.deleteNetwork - "if managed" etc. controllerutil.RemoveFinalizer(stackitCluster, infrav1.ClusterFinalizer) return ctrl.Result{}, nil } From 4c7d5bdaeaf49ec23a912c489f983d529abfbca1 Mon Sep 17 00:00:00 2001 From: Fabian Schmitt <21021337+fabian-schmitt@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:51:54 +0200 Subject: [PATCH 5/8] feat(api): start implementing ensure network creation function --- controller/stackitcluster_infrastructure.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/stackitcluster_infrastructure.go b/controller/stackitcluster_infrastructure.go index de77b9e..657e567 100644 --- a/controller/stackitcluster_infrastructure.go +++ b/controller/stackitcluster_infrastructure.go @@ -88,8 +88,8 @@ func (r *StackitClusterReconciler) reconcileNormal(ctx context.Context, clusterS } clusterScope.SetFailureDomains(failureDomainsFromZones(stackitCluster.Spec.Region, zones)) + network, err := cloudClient.GetNetwork(ctx, stackitCluster.Spec.Network.ID) if stackitCluster.Spec.Network.ID != "" { - network, err := cloudClient.GetNetwork(ctx, stackitCluster.Spec.Network.ID) if err != nil { log.Error(err, "Failed to get network", "networkID", stackitCluster.Spec.Network.ID, From 76cf0d45cf7054d133fb528b40a6a3bc636e2933 Mon Sep 17 00:00:00 2001 From: Fabian Schmitt <21021337+fabian-schmitt@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:16:50 +0200 Subject: [PATCH 6/8] feat(api): start implementing ensure network creation function --- api/v1alpha1/stackitcluster_types.go | 4 +-- cloud/client.go | 8 +++-- cloud/sdk_client.go | 4 +++ cloud/services/network/network.go | 34 +++++++++++++++++++ cloud/types.go | 6 ++++ ...ture.cluster.x-k8s.io_stackitclusters.yaml | 1 - ...ster.x-k8s.io_stackitclustertemplates.yaml | 1 - controller/stackitcluster_infrastructure.go | 3 +- docs/src/getting-started/cloud-resources.md | 15 ++++++-- 9 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 cloud/services/network/network.go diff --git a/api/v1alpha1/stackitcluster_types.go b/api/v1alpha1/stackitcluster_types.go index ff38236..80db4da 100644 --- a/api/v1alpha1/stackitcluster_types.go +++ b/api/v1alpha1/stackitcluster_types.go @@ -49,8 +49,8 @@ type StackitClusterSpec struct { // network configures the STACKIT network the cluster will use. // ommiting the ID will create a managed dedicated network - // +required - Network StackitClusterNetworkSpec `json:"network"` + // +optional + Network StackitClusterNetworkSpec `json:"network,omitempty"` // apiServerLoadBalancer configures the load balancer in front of the // Kubernetes API server. diff --git a/cloud/client.go b/cloud/client.go index a3ab870..f319370 100644 --- a/cloud/client.go +++ b/cloud/client.go @@ -23,6 +23,12 @@ import "context" // - be idempotent (CreateServer/EnsureAPIServerLoadBalancer must not produce // duplicates when a resource with matching tags already exists) type Client interface { + GetNetwork(ctx context.Context, id string) (*Network, error) + + CreateNetwork(ctx context.Context, input CreateNetworkInput) (*Network, error) + + DeleteNetwork(ctx context.Context, id string) error + GetServer(ctx context.Context, id string) (*Server, error) FindServerByTags(ctx context.Context, tags map[string]string) (*Server, error) @@ -33,8 +39,6 @@ type Client interface { DeleteServer(ctx context.Context, id string) error - GetNetwork(ctx context.Context, id string) (*Network, error) - EnsureBastion(ctx context.Context, input BastionInput) (*Bastion, error) DeleteBastion(ctx context.Context, input BastionInput, status Bastion) error diff --git a/cloud/sdk_client.go b/cloud/sdk_client.go index 433e22f..3089c6f 100644 --- a/cloud/sdk_client.go +++ b/cloud/sdk_client.go @@ -234,6 +234,10 @@ func (c *SDKClient) GetNetwork(ctx context.Context, id string) (*Network, error) }, nil } +func (c *SDKClient) CreateNetwork(ctx context.Context, id string) (*Network, error) { + return nil // TODO +} + func (c *SDKClient) EnsureBastion(ctx context.Context, input BastionInput) (*Bastion, error) { if input.Name == "" || input.NetworkID == "" || input.ImageID == "" || input.MachineType == "" || input.SSHKeyName == "" { diff --git a/cloud/services/network/network.go b/cloud/services/network/network.go new file mode 100644 index 0000000..155b047 --- /dev/null +++ b/cloud/services/network/network.go @@ -0,0 +1,34 @@ +/* +Copyright 2026. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +package network + +import ( + infrav1 "github.com/stackitcloud/cluster-api-provider-stackit/api/v1alpha1" + "github.com/stackitcloud/cluster-api-provider-stackit/cloud" + "github.com/stackitcloud/cluster-api-provider-stackit/util" +) + +const ( + // DefaultNetworkPrefix defines the default CIDR allocated for a managed cluster network. + DefaultNetworkPrefix = "10.0.0.0/24" +) + +func NetworkTags(stackitCluster *infrav1.StackitCluster) map[string]string { + return util.ClusterTags(stackitCluster.Name, stackitCluster.Namespace, stackitCluster.Spec.AdditionalLabels) +} + +func Input(stackitCluster *infrav1.StackitCluster) cloud.CreateNetworkInput { + return cloud.CreateNetworkInput{ + Name: stackitCluster.Name + "-network", + IPv4Prefix: DefaultNetworkPrefix, + Tags: NetworkTags(stackitCluster), + } +} diff --git a/cloud/types.go b/cloud/types.go index 289353f..3b5974e 100644 --- a/cloud/types.go +++ b/cloud/types.go @@ -69,6 +69,12 @@ type Bastion struct { SecurityGroupID string } +type CreateNetworkInput struct { + Name string + IPv4Prefix string + Tags map[string]string +} + // CreateServerInput holds all parameters required to create a new VM. type CreateServerInput struct { Name string diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml index cef3c8a..16de6b8 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclusters.yaml @@ -224,7 +224,6 @@ spec: type: string required: - credentialsSecretRef - - network - projectID - region type: object diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml index 4566f51..84a0fff 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_stackitclustertemplates.yaml @@ -260,7 +260,6 @@ spec: type: string required: - credentialsSecretRef - - network - projectID - region type: object diff --git a/controller/stackitcluster_infrastructure.go b/controller/stackitcluster_infrastructure.go index 657e567..e3e5fbc 100644 --- a/controller/stackitcluster_infrastructure.go +++ b/controller/stackitcluster_infrastructure.go @@ -113,7 +113,8 @@ func (r *StackitClusterReconciler) reconcileNormal(ctx context.Context, clusterS ) } } else { - //TODO: implement cloudClient.EnsureNetwork + // TODO: implement cloudClient.EnsureNetwork + } clusterScope.SetConditions( metav1.ConditionTrue, diff --git a/docs/src/getting-started/cloud-resources.md b/docs/src/getting-started/cloud-resources.md index 100f569..37961fc 100644 --- a/docs/src/getting-started/cloud-resources.md +++ b/docs/src/getting-started/cloud-resources.md @@ -18,9 +18,18 @@ network must provide: container image pulls, DNS, NTP, and STACKIT APIs. - Traffic required by the selected CNI. -Network creation is intentionally outside the provider for now because networks -usually encode platform-level policy such as address planning, routing, -peering, VPN, DNS, and shared-service access. +If you don't provide a network ID, CAPS will create one /24 Network for you. + +Planning your Networks usually involves platform-level policy such as address +planning and considers routing, peering, VPN, DNS, and shared-service access +but the default is built for convenience and a newly created (managed) network +will a have an attached tag indicating that it is managed by CAPS. + +Only empty Networks can be deleted, so if you delete your CAPS Cluster and you +have no other manually created resource in that cluster, it will be deleted too. + + +### How to create a STACKIT Network manually Create a network with the STACKIT CLI: From b2ff9b7813941808d2eefb544d99734f00497791 Mon Sep 17 00:00:00 2001 From: Fabian Schmitt <21021337+fabian-schmitt@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:41:25 +0200 Subject: [PATCH 7/8] feat(api): proceed with implementation plan and fix errors --- cloud/fake/client.go | 8 ++++++++ cloud/sdk_client.go | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cloud/fake/client.go b/cloud/fake/client.go index 06465cc..ddeb5b9 100644 --- a/cloud/fake/client.go +++ b/cloud/fake/client.go @@ -251,6 +251,14 @@ func (c *Client) GetNetwork(_ context.Context, id string) (*cloud.Network, error return &out, nil } +func (c *Client) CreateNetwork(_ context.Context, _ cloud.CreateNetworkInput) (*cloud.Network, error) { + return nil, nil // TODO +} + +func (c *Client) DeleteNetwork(_ context.Context, _ string) error { + return nil // TODO +} + func (c *Client) EnsureBastion(_ context.Context, input cloud.BastionInput) (*cloud.Bastion, error) { c.mu.Lock() defer c.mu.Unlock() diff --git a/cloud/sdk_client.go b/cloud/sdk_client.go index 3089c6f..56d7818 100644 --- a/cloud/sdk_client.go +++ b/cloud/sdk_client.go @@ -234,7 +234,11 @@ func (c *SDKClient) GetNetwork(ctx context.Context, id string) (*Network, error) }, nil } -func (c *SDKClient) CreateNetwork(ctx context.Context, id string) (*Network, error) { +func (c *SDKClient) CreateNetwork(ctx context.Context, input CreateNetworkInput) (*Network, error) { + return nil, nil // TODO +} + +func (c *SDKClient) DeleteNetwork(ctx context.Context, id string) error { return nil // TODO } From af46e754a5f35fe6f54780c49d9c4fe2f2c43120 Mon Sep 17 00:00:00 2001 From: Fabian Schmitt <21021337+fabian-schmitt@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:49:27 +0200 Subject: [PATCH 8/8] fix: typo --- api/v1alpha1/stackitcluster_types.go | 2 +- cloud/sdk_client.go | 4 ++++ webhook/v1alpha1/webhook_validation_test.go | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/stackitcluster_types.go b/api/v1alpha1/stackitcluster_types.go index 80db4da..287074e 100644 --- a/api/v1alpha1/stackitcluster_types.go +++ b/api/v1alpha1/stackitcluster_types.go @@ -48,7 +48,7 @@ type StackitClusterSpec struct { CredentialsSecretRef corev1.SecretReference `json:"credentialsSecretRef"` // network configures the STACKIT network the cluster will use. - // ommiting the ID will create a managed dedicated network + // omiting the ID will create a managed dedicated network // +optional Network StackitClusterNetworkSpec `json:"network,omitempty"` diff --git a/cloud/sdk_client.go b/cloud/sdk_client.go index 56d7818..a9dfbf7 100644 --- a/cloud/sdk_client.go +++ b/cloud/sdk_client.go @@ -235,6 +235,10 @@ func (c *SDKClient) GetNetwork(ctx context.Context, id string) (*Network, error) } func (c *SDKClient) CreateNetwork(ctx context.Context, input CreateNetworkInput) (*Network, error) { + if input.Name == "" { + return nil, fmt.Errorf("%w: network name is required", ErrInvalidInput) + } + return nil, nil // TODO } diff --git a/webhook/v1alpha1/webhook_validation_test.go b/webhook/v1alpha1/webhook_validation_test.go index c16f632..a3fa936 100644 --- a/webhook/v1alpha1/webhook_validation_test.go +++ b/webhook/v1alpha1/webhook_validation_test.go @@ -55,6 +55,14 @@ var _ = Describe("STACKIT admission webhooks", func() { Expect(err).NotTo(HaveOccurred()) }) + It("accepts a cluster with omitted network ID for managed networks", func() { + cluster := validStackitCluster("managed-network-cluster") + cluster.Spec.Network.ID = "" + + _, err := validator.ValidateCreate(ctx, cluster) + Expect(err).NotTo(HaveOccurred()) + }) + It("rejects a cluster without credentials secret name", func() { cluster := validStackitCluster("missing-credentials") cluster.Spec.CredentialsSecretRef.Name = "" @@ -126,6 +134,16 @@ var _ = Describe("STACKIT admission webhooks", func() { _, err := validator.ValidateUpdate(ctx, oldCluster, newCluster) expectInvalidField(err, "spec.network.id") }) + + It("rejects setting network ID on a managed cluster", func() { + oldCluster := validStackitCluster("managed-to-byo") + oldCluster.Spec.Network.ID = "" + newCluster := oldCluster.DeepCopy() + newCluster.Spec.Network.ID = validNetworkID + + _, err := validator.ValidateUpdate(ctx, oldCluster, newCluster) + expectInvalidField(err, "spec.network.id") + }) }) Describe("StackitMachine", func() {