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
1 change: 1 addition & 0 deletions docs/machine-class.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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
6 changes: 6 additions & 0 deletions pkg/provider/apis/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

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 {
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")
})
})