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
13 changes: 13 additions & 0 deletions pkg/client/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func NewStackitClient(serviceAccountKey string) (*SdkStackitClient, error) {
var (
// ErrServerNotFound indicates the server was not found (404)
ErrServerNotFound = errors.New("server not found")
// ErrNetworkNotFound indicates the network was not found (404)
ErrNetworkNotFound = errors.New("network not found")
)

// createIAASClient creates a new STACKIT SDK IAAS API client
Expand Down Expand Up @@ -318,6 +320,17 @@ 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 {
if err := c.iaasClient.DefaultAPI.AddNetworkToServer(ctx, projectID, region, serverID, networkID).Execute(); err != nil {
// Check if error is 404 Not Found
if isNotFoundError(err) {
return fmt.Errorf("%w: %v", ErrNetworkNotFound, err)
}
return err
}
return nil
}

// 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
48 changes: 44 additions & 4 deletions pkg/provider/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package provider
import (
"context"
"encoding/base64"
"errors"
"fmt"
"maps"
"slices"
Expand Down Expand Up @@ -94,10 +95,9 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
return nil, status.Error(codes.DeadlineExceeded, fmt.Sprintf("failed waiting for server to be ACTIVE: %v", err))
}

nics, err := p.patchNetworkInterfaces(ctx, projectID, server.ID, providerSpec)
addrs, err := p.setupNetworking(ctx, projectID, providerSpec, server)
if err != nil {
klog.Errorf("Failed to patch NICs for server %q: %v", req.Machine.Name, err)
return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to patch NICs for server: %v", err))
return nil, fmt.Errorf("setup networking: %w", err)
}

// Generate ProviderID in format: stackit://<projectId>/<serverId>
Expand All @@ -107,10 +107,48 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
return &driver.CreateMachineResponse{
ProviderID: providerID,
NodeName: req.Machine.Name,
Addresses: nicAddresses(nics),
// We exclude the IPs of NICs from secondary networks to ensure,
// if MCM runs without a target cluster,
// consumers of Machine.Status.Addresses always use the IP from the default network.
Addresses: addrs,
}, nil
}

func (p *Provider) setupNetworking(ctx context.Context, projectID string, providerSpec *api.ProviderSpec, server *client.Server) ([]corev1.NodeAddress, error) {
nics, err := p.patchNetworkInterfaces(ctx, projectID, server.ID, providerSpec)
if err != nil {
klog.Errorf("Failed to patch NICs for server %q: %v", server.Name, err)
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 {
code := codes.Unavailable
if errors.Is(err, client.ErrNetworkNotFound) {
code = codes.FailedPrecondition
}
return nil, status.Error(code, fmt.Sprintf("failed to ensure additional networks for server: %v", err))
}
}
return nicAddresses(nics), nil
}

func (p *Provider) ensureAdditionalNetworks(ctx context.Context, projectID, region, serverID string, networkingSpec *api.NetworkingSpec, nics []*client.NIC) error {
var errs 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 {
errs = errors.Join(errs, fmt.Errorf("attaching server %s to network %s: %w", serverID, networkID, err))
}
}
return errs
}

// 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 Expand Up @@ -255,6 +293,8 @@ func (p *Provider) getServerByName(ctx context.Context, projectID, region, serve
return nil, nil
}

// patchNetworkInterfaces updates the primary network interfaces of the server that belong to the primary network with AllowedAddresses.
// NICs of secondary networks are excluded.
func (p *Provider) patchNetworkInterfaces(ctx context.Context, projectID, serverID string, providerSpec *api.ProviderSpec) ([]*client.NIC, error) {
nics, err := p.client.GetNICsForServer(ctx, projectID, providerSpec.Region, serverID)
if err != nil {
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")
})
})