From d849c35d7848772acd0fb374e1a2521bd6ae986f Mon Sep 17 00:00:00 2001 From: Lukas Hoehl Date: Tue, 30 Jun 2026 13:29:23 +0200 Subject: [PATCH 1/2] secondary networks for servers Signed-off-by: Lukas Hoehl --- docs/machine-class.md | 1 + pkg/client/mock/client.go | 23 ++++++++++++++++------ pkg/client/sdk.go | 4 ++++ pkg/client/stackit.go | 2 ++ pkg/provider/apis/provider_spec.go | 3 +++ pkg/provider/apis/validation/validation.go | 6 ++++++ pkg/provider/create.go | 21 ++++++++++++++++++++ 7 files changed, 54 insertions(+), 6 deletions(-) diff --git a/docs/machine-class.md b/docs/machine-class.md index 6be679ef..2a56f0f0 100644 --- a/docs/machine-class.md +++ b/docs/machine-class.md @@ -40,6 +40,7 @@ Exactly one of the following must be set: - `networkId` (string): UUID of the network to attach. - `nicIds` ([]string): UUIDs of pre-created NICs. +- `secondaryNetworkIds` ([]string): UUID of additional networks to attach. ## BootVolumeSpec diff --git a/pkg/client/mock/client.go b/pkg/client/mock/client.go index 43a7fbf2..6112663c 100644 --- a/pkg/client/mock/client.go +++ b/pkg/client/mock/client.go @@ -8,15 +8,26 @@ import ( api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis" ) +var _ client.StackitClient = (*StackitClient)(nil) + // StackitClient is a mock implementation of StackitClient for testing // Note: Single-tenant design - each client is bound to one set of credentials type StackitClient struct { - CreateServerFunc func(ctx context.Context, projectID, region string, req *client.CreateServerRequest) (*client.Server, error) - GetServerFunc func(ctx context.Context, projectID, region, serverID string) (*client.Server, error) - DeleteServerFunc func(ctx context.Context, projectID, region, serverID string) error - ListServersFunc func(ctx context.Context, projectID, region string, labelSelector map[string]string) ([]*client.Server, error) - GetNICsFunc func(ctx context.Context, projectID, region, serverID string) ([]*client.NIC, error) - UpdateNICFunc func(ctx context.Context, projectID, region, networkID, nicID string, allowedAddresses []string) (*client.NIC, error) + CreateServerFunc func(ctx context.Context, projectID, region string, req *client.CreateServerRequest) (*client.Server, error) + GetServerFunc func(ctx context.Context, projectID, region, serverID string) (*client.Server, error) + DeleteServerFunc func(ctx context.Context, projectID, region, serverID string) error + ListServersFunc func(ctx context.Context, projectID, region string, labelSelector map[string]string) ([]*client.Server, error) + GetNICsFunc func(ctx context.Context, projectID, region, serverID string) ([]*client.NIC, error) + UpdateNICFunc func(ctx context.Context, projectID, region, networkID, nicID string, allowedAddresses []string) (*client.NIC, error) + AttachNetworkToServerFunc func(ctx context.Context, projectID, region, networkID, serverID string) error +} + +// AttachServerToNetwork implements [client.StackitClient]. +func (m *StackitClient) AttachServerToNetwork(ctx context.Context, projectID, region, networkID, serverID string) error { + if m.AttachNetworkToServerFunc != nil { + return m.AttachNetworkToServerFunc(ctx, projectID, region, networkID, serverID) + } + return nil } func (m *StackitClient) CreateServer(ctx context.Context, projectID, region string, req *client.CreateServerRequest) (*client.Server, error) { diff --git a/pkg/client/sdk.go b/pkg/client/sdk.go index a306ac85..f4f7e57c 100644 --- a/pkg/client/sdk.go +++ b/pkg/client/sdk.go @@ -318,6 +318,10 @@ func (c *SdkStackitClient) UpdateNIC(ctx context.Context, projectID, region, net return convertSDKNICtoNIC(sdkNic), nil } +func (c *SdkStackitClient) AttachServerToNetwork(ctx context.Context, projectID, region, networkID, serverID string) error { + return c.iaasClient.DefaultAPI.AddNetworkToServer(ctx, projectID, region, serverID, networkID).Execute() +} + // Helper functions func convertSDKNICtoNIC(nic *iaas.NIC) *NIC { diff --git a/pkg/client/stackit.go b/pkg/client/stackit.go index 41b4810a..aa37eb4e 100644 --- a/pkg/client/stackit.go +++ b/pkg/client/stackit.go @@ -27,6 +27,8 @@ type StackitClient interface { GetNICsForServer(ctx context.Context, projectID, region, serverID string) ([]*NIC, error) // UpdateNIC updates a network interface UpdateNIC(ctx context.Context, projectID, region, networkID, nicID string, allowedAddresses []string) (*NIC, error) + // AttachServerToNetwork attaches a server to a network + AttachServerToNetwork(ctx context.Context, projectID, region, networkID, serverID string) error } // CreateServerRequest represents the request to create a server diff --git a/pkg/provider/apis/provider_spec.go b/pkg/provider/apis/provider_spec.go index 4a227696..f16aa534 100644 --- a/pkg/provider/apis/provider_spec.go +++ b/pkg/provider/apis/provider_spec.go @@ -103,6 +103,9 @@ type NetworkingSpec struct { // Advanced variant: Allows fine-grained control over NICs, IPs, and security groups // Mutually exclusive with NetworkID NICIDs []string `json:"nicIds,omitempty"` + + // SecondaryNetworkIDs can be used to attach additional networks to the server + SecondaryNetworkIDs []string `json:"secondaryNetworkIds,omitempty"` } // BootVolumeSpec defines the boot disk configuration for a server diff --git a/pkg/provider/apis/validation/validation.go b/pkg/provider/apis/validation/validation.go index 517998c8..227cae63 100644 --- a/pkg/provider/apis/validation/validation.go +++ b/pkg/provider/apis/validation/validation.go @@ -246,6 +246,12 @@ func validateNetworking(networking *api.NetworkingSpec) []error { } } + for i, secondaryNet := range networking.SecondaryNetworkIDs { + if !isValidUUID(secondaryNet) { + errors = append(errors, fmt.Errorf("providerSpec.networking.secondaryNetworkIds[%d] must be a valid UUID", i)) + } + } + return errors } diff --git a/pkg/provider/create.go b/pkg/provider/create.go index 930600b5..c349b882 100644 --- a/pkg/provider/create.go +++ b/pkg/provider/create.go @@ -100,6 +100,12 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to patch NICs for server: %v", err)) } + if providerSpec.Networking != nil && len(providerSpec.Networking.SecondaryNetworkIDs) > 0 { + if err := p.ensureAdditionalNetworks(ctx, projectID, providerSpec.Region, server.ID, providerSpec.Networking, nics); err != nil { + return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to ensure additional networks for server: %v", err)) + } + } + // Generate ProviderID in format: stackit:/// providerID := fmt.Sprintf("%s://%s/%s", StackitProviderName, projectID, server.ID) klog.V(2).Infof("Successfully created server %q with ID %q for machine %q", server.Name, server.ID, req.Machine.Name) @@ -111,6 +117,21 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR }, nil } +func (p *Provider) ensureAdditionalNetworks(ctx context.Context, projectID, region, serverID string, networkingSpec *api.NetworkingSpec, nics []*client.NIC) error { + for _, networkID := range networkingSpec.SecondaryNetworkIDs { + exists := slices.ContainsFunc(nics, func(nic *client.NIC) bool { + return nic.NetworkID == networkID + }) + if exists { + continue + } + if err := p.client.AttachServerToNetwork(ctx, projectID, region, networkID, serverID); err != nil { + return fmt.Errorf("attaching server %s to network %s: %w", serverID, networkID, err) + } + } + return nil +} + // nolint: gocyclo // this function is already pretty simple func (p *Provider) createServerRequest(req *driver.CreateMachineRequest, providerSpec *api.ProviderSpec) *client.CreateServerRequest { // Build labels: merge ProviderSpec labels with MCM-specific labels From 1a8da19a39e0eef691ee17254645cea385ba4c48 Mon Sep 17 00:00:00 2001 From: Lukas Hoehl Date: Tue, 7 Jul 2026 13:33:49 +0200 Subject: [PATCH 2/2] tests Signed-off-by: Lukas Hoehl --- pkg/provider/create_networking_test.go | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/pkg/provider/create_networking_test.go b/pkg/provider/create_networking_test.go index 87be95b8..73165cb8 100644 --- a/pkg/provider/create_networking_test.go +++ b/pkg/provider/create_networking_test.go @@ -465,3 +465,61 @@ var _ = Describe("CreateMachine - Networking", func() { }) }) }) + +var _ = Describe("#ensureAdditionalNetworks", func() { + var ( + provider *Provider + mockClient *mock.StackitClient + projectID = "123" + region = "eu01" + serverID = "server" + spec = api.NetworkingSpec{ + SecondaryNetworkIDs: []string{"my-secondary-network"}, + } + ) + + BeforeEach(func() { + mockClient = &mock.StackitClient{} + provider = &Provider{ + client: mockClient, + } + }) + + It("should attach the server to network if no NIC of network is found", func(ctx context.Context) { + var called bool + mockClient.AttachNetworkToServerFunc = func(_ context.Context, _, _, networkID, _ string) error { + called = true + Expect(networkID).To(Equal("my-secondary-network")) + return nil + } + + existingNics := []*client.NIC{ + { + NetworkID: "not-my-network", + }, + } + + Expect(provider.ensureAdditionalNetworks(ctx, projectID, region, serverID, &spec, existingNics)).To(Succeed()) + Expect(called).To(BeTrue(), "AttachNetworkToServer function called") + }) + + It("should not attach the server to network if NIC of network is already present", func(ctx context.Context) { + var called bool + mockClient.AttachNetworkToServerFunc = func(_ context.Context, _, _, _, _ string) error { + called = true + return nil + } + + existingNics := []*client.NIC{ + { + NetworkID: "my-secondary-network", + }, + { + NetworkID: "not-my-network", + }, + } + + Expect(provider.ensureAdditionalNetworks(ctx, projectID, region, serverID, &spec, existingNics)).To(Succeed()) + Expect(called).To(BeFalse(), "AttachNetworkToServer function called") + }) +})