Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/machine-class.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ A MachineClass defines how STACKIT servers should be created. The ProviderSpec i

## NetworkingSpec

Exactly one of the following must be set:
`networkId` / `nicIds` is mutually exclusive and required while `secondaryNetworkIds` is additional.

- `networkId` (string): UUID of the network to attach.
- `nicIds` ([]string): UUIDs of pre-created NICs.
- `secondaryNetworkIds` ([]string): UUID of additional networks to attach.

## BootVolumeSpec

Expand Down
23 changes: 17 additions & 6 deletions pkg/client/mock/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions pkg/client/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions pkg/client/stackit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pkg/provider/apis/provider_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions pkg/provider/apis/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ func validateNetworking(networking *api.NetworkingSpec) []error {
}
}

for i, secondaryNet := range networking.SecondaryNetworkIDs {
Comment thread
hown3d marked this conversation as resolved.
if !isValidUUID(secondaryNet) {
errors = append(errors, fmt.Errorf("providerSpec.networking.secondaryNetworkIds[%d] must be a valid UUID", i))
Comment thread
breuerfelix marked this conversation as resolved.
}
if secondaryNet == networking.NetworkID {
errors = append(errors, fmt.Errorf("providerSpec.networking.secondaryNetworkIds[%d] has the same ID as providerSpec.networking.networkId", i))
}
}

return errors
}

Expand Down
21 changes: 21 additions & 0 deletions pkg/provider/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

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.

doesn't this has to happen before patching the networkinterfaces?

if this happens after patchNetworkInterface, the address from the new interface is not reflected in the CreateMachineResponse.Addresses and also it does not get the allowedAddresses patched.

This also means in the ensureAdditionalNetworks we have to attach the network and then wait until all networks are attached since this operation is async.
If we do not wait until all networks are attached, the patch function could skip some nics.

All this "waiting" feels a little cumbersome but its just how the create interface is designed. One call to rule em all.

@hown3d hown3d Aug 3, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We should think about how we want to implement secondary networks in general.
In my idea, the primary network would be the one designed to route pod traffic. So this should be the only one where allowedAddresses are set to the pod CIDR.

Returning the address of the secondary network in the addresses might be something we want to do but also not want to use. These IPs are used by gardener for example to create SelfHostedShoot exposure resources.
Would a user want to use only the primary network for this or all networks where a server is attached to.

/cc @dergeberl

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://<projectId>/<serverId>
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)
Expand All @@ -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
Expand Down
58 changes: 58 additions & 0 deletions pkg/provider/create_networking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
})