Skip to content

feat(api): make network.id optional and implement network creation - #34

Open
fabian-schmitt wants to merge 15 commits into
mainfrom
feat/support-managed-network-creation
Open

fabian-schmitt wants to merge 15 commits into
mainfrom
feat/support-managed-network-creation

Conversation

@fabian-schmitt

@fabian-schmitt fabian-schmitt commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Description

This PR implements support for managed network creation in StackitCluster, resolving #9.

Previously, StackitCluster and StackitMachine required a pre-existing STACKIT network ID (.spec.network.id). This PR makes .spec.network.id optional, introducing two operational modes:

  1. Managed Mode (omitted spec.network.id):

    • Automatically provisions a dedicated /24 STACKIT network named <cluster-name>-network.
    • Tags the network with canonical Cluster API tags (cluster.x-k8s.io/cluster-name, managed-by, etc.).
    • Stores { id: "<uuid>", managed: true } in status.network.
    • On cluster teardown: Deletes the managed network after machines, load balancers, and bastions are removed. If ports/interfaces are still detaching (409 Conflict), requeues with a NetworkInUse condition and warning event. If status was lost during deletion, falls back to tag-based discovery.
  2. Bring-Your-Own (BYO) Mode (provided spec.network.id):

    • Validates that the network exists and records { id: "<uuid>", managed: false } in status.network.
    • Leaves the network untouched on cluster deletion.

Key Changes

  • API & CRDs (api/v1alpha1/, config/crd/):

    • Made .spec.network and .spec.network.id optional in StackitCluster, StackitMachine, and their templates.
    • Added Status.Network (StackitNetworkStatus{ id, managed }) to StackitClusterStatus.
    • Updated kubectl printcolumn for Network ID to read from .status.network.id (works for both BYO and managed clusters).
  • Webhook Validation (webhook/v1alpha1/):

    • Allowed empty network IDs on create.
    • Enforced immutability: prevents changing .spec.network.id or switching between managed and BYO after creation.
  • Cloud Client & Abstraction (cloud/):

    • Added CreateNetworkInput{ Name, IPv4Prefix, Tags }.
    • Added CreateNetwork, DeleteNetwork, FindNetworkByTags, and ListNetworksByTags to cloud.Client, SDKClient, and cloudfake.Client.
    • Implemented tag-based idempotency in CreateNetwork to avoid duplicate networks across reconcile retries.
    • Updated cloud.CleanupByTags to clean up managed networks in E2E environments.
  • Scope & Service Layer (scope/, cloud/services/):

    • Created cloud/services/network/network.go for network input assembly and tag generation.
    • Added MachineScope.NetworkID() and ClusterScope.NetworkID() for encapsulated fallback resolution (Machine Spec -> Cluster Spec -> Cluster Status).
    • Updated API server load balancer and bastion services to receive the resolved networkID.
  • Reconciliation & Teardown (controller/):

    • Updated reconcileNormal to provision or adopt managed networks and set ClusterNetworkReadyCondition.
    • Updated reconcileDelete to delete managed networks, handling 409 Conflict requeues gracefully.
    • Updated machine provisioning to use the resolved network ID.
  • Tests & Documentation (*_test.go, docs/):

    • Added unit tests for webhook validation, SDK client network creation/deletion, service input generation, scope fallback resolution, and controller reconciliation/deletion.
    • Updated docs/src/getting-started/cloud-resources.md.

Testing

  • Verified all unit and controller tests pass:
    make test
  • Verified code formatting and linter checks pass with zero issues:
    make lint-fix

ToDo or at least to discuss

  • Cluster-template changes: Should we now default to empty network?

  • LoadBalancer NetworkID:
    @ templates/cluster-template.yaml#L288-294
    How to handle the STACKIT_NETWORK_ID in the cloud.yaml in the generated secret?
    Should a controller patch or generate the value after the network is generated?

@fabian-schmitt fabian-schmitt linked an issue Oct 15, 2026 that may be closed by this pull request
@fabian-schmitt fabian-schmitt self-assigned this Oct 15, 2026
@fabian-schmitt
fabian-schmitt requested a review from tuunit October 22, 2026 10:58
@fabian-schmitt
fabian-schmitt marked this pull request as ready for review October 22, 2026 10:58
@fabian-schmitt
fabian-schmitt requested a review from a team October 22, 2026 10:58
@fabian-schmitt fabian-schmitt changed the title feat(api): make network.id optional and add network status for manage… feat(api): make network.id optional and implement network creation Oct 22, 2026
Comment thread api/v1alpha1/stackitcluster_types.go Outdated
Comment thread api/v1alpha1/stackitcluster_types.go Outdated
Comment on lines 45 to +46
stackitCluster := clusterScope.StackitCluster
input := bastionservice.Input(stackitCluster, nil)
input := bastionservice.Input(stackitCluster, clusterScope.NetworkID(), nil)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i think we should rather provide the cluster scope to the bastion input and call clusterScope.NetworkID() inside of the input method

)

func Input(stackitCluster *infrav1.StackitCluster, cloudInit []byte) cloud.BastionInput {
func Input(stackitCluster *infrav1.StackitCluster, networkID string, cloudInit []byte) cloud.BastionInput {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

as in my comment further below we should change this method signature to use the clusterScope as an input

pseudo code:

Suggested change
func Input(stackitCluster *infrav1.StackitCluster, networkID string, cloudInit []byte) cloud.BastionInput {
func Input(cluster *scope.ClusterScope, cloudInit []byte) cloud.BastionInput {

Comment on lines 42 to 45
func APIServerInput(
stackitCluster *infrav1.StackitCluster,
networkID string,
targets []cloud.LoadBalancerTargetInput,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same as for the bastion input, provide the cluster scope instead of extending the parameters of the method signature

loadBalancer, err := cloudClient.EnsureAPIServerLoadBalancer(
ctx,
loadbalancerservice.APIServerInput(stackitCluster, seed),
loadbalancerservice.APIServerInput(stackitCluster, clusterScope.NetworkID(), seed),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

use clusterScope as input instead of the stackitCluster and networkid

return ctrl.Result{}, err
}
if err := cloudClient.DeleteBastion(ctx, bastionservice.Input(stackitCluster, nil), cloud.Bastion{
if err := cloudClient.DeleteBastion(ctx, bastionservice.Input(stackitCluster, clusterScope.NetworkID(), nil), cloud.Bastion{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

use clusterScope as input instead of the stackitCluster and networkid

Comment on lines +340 to +343
networkID := machineScope.NetworkID()
if networkID == "" {
return nil, false, fmt.Errorf("%w: cluster network ID is not yet available", cloud.ErrTransient)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can this realistically even happen? i don't think the reconiliation for a machine can start if the cluster isn't reconcilled

AvailabilityZone: machineScope.AvailabilityZone(),
SSHKeyName: stackitMachine.Spec.SSHKeyName,
NetworkID: stackitMachine.Spec.Network.ID,
NetworkID: networkID,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

so most likely it is sufficient to use machineScope.NetworkID() here and it shouldn't case any issues

Comment thread scope/machine_scope.go
if s.StackitMachine != nil && s.StackitMachine.Spec.Network.ID != "" {
return s.StackitMachine.Spec.Network.ID
}
if s.StackitCluster != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is this possible?

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.

i tried the most defensive approach and existing availabilityZone() is doing this too. I am not sure tho, open to change that...

fabian-schmitt and others added 2 commits October 23, 2026 14:29
Co-authored-by: Jan Larwig <jan.larwig@digits.schwarz>
Co-authored-by: Jan Larwig <jan.larwig@digits.schwarz>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support managed network creation in StackitCluster

2 participants