Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,4 @@ sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxO
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
4 changes: 4 additions & 0 deletions pkg/provider/apis/provider_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ type ProviderSpec struct {
// Optional field. If not specified, the server may use default networking or require manual configuration.
Networking *NetworkingSpec `json:"networking,omitempty"`

// AllowedAddresses are the IP address ranges (CIDRs) allowed to originate traffic from the server's network interface.
// Optional field. If specified, these ranges are configured as AllowedAddresses on the network interface of the server to bypass anti-spoofing rules.
AllowedAddresses []string `json:"allowedAddresses,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

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

Should be part of NetworkingSpec

Copy link
Member Author

Choose a reason for hiding this comment

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

I would not put that in NetworkingSpec since other MCM providers are also passing almost all config in the top level.
The NetworkingSpec has only 2 configuration options which are mutually exclusive. And even Networking is optional because if it is not set, the VM is put into the "default" network. So the Networking field has its own logic separate from the allowedAddresses.


// SecurityGroups are the names of security groups to attach to the server
// Optional field. If not specified, the project's default security group will be used.
SecurityGroups []string `json:"securityGroups,omitempty"`
Expand Down
10 changes: 10 additions & 0 deletions pkg/provider/apis/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package validation
import (
"encoding/json"
"fmt"
"net"
"regexp"

api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis"
Expand Down Expand Up @@ -165,6 +166,15 @@ func ValidateProviderSpecNSecret(spec *api.ProviderSpec, secrets *corev1.Secret)
}
}

// Validate AllowedAddresses
Copy link
Member

Choose a reason for hiding this comment

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

move into validateNetworking function

Copy link
Member Author

Choose a reason for hiding this comment

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

same as above

if len(spec.AllowedAddresses) > 0 {
for _, cidr := range spec.AllowedAddresses {
if _, _, err := net.ParseCIDR(cidr); err != nil {
errors = append(errors, fmt.Errorf("providerSpec.allowedAddresses has an invalid CIDR: %s", cidr))
}
}
}

// Validate AffinityGroup
if spec.AffinityGroup != "" {
if !isValidUUID(spec.AffinityGroup) {
Expand Down
145 changes: 124 additions & 21 deletions pkg/provider/core.go
Copy link
Member

Choose a reason for hiding this comment

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

nit: Might be worth to separate functions into other files (e.g. network.go for network related functions).
Having a single core.go that will grow is not really clear to overlook in the long run.

Copy link
Member Author

Choose a reason for hiding this comment

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

solve in another story

Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import (
"encoding/base64"
"errors"
"fmt"
"slices"
"strings"

"github.com/gardener/machine-controller-manager/pkg/util/provider/driver"
"github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/codes"
"github.com/gardener/machine-controller-manager/pkg/util/provider/machinecodes/status"
api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis"
"github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis/validation"
"k8s.io/klog/v2"
)
Expand Down Expand Up @@ -182,27 +185,106 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
createReq.Metadata = providerSpec.Metadata
}

// Call STACKIT API to create server
server, err := p.client.CreateServer(ctx, projectID, providerSpec.Region, createReq)
// check if server already exists
server, err := p.getServerByName(ctx, projectID, providerSpec.Region, req.Machine.Name)
if err != nil {
klog.Errorf("Failed to create server for machine %q: %v", req.Machine.Name, err)
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to create server: %v", err))
klog.Errorf("Failed to fetch server for machine %q: %v", req.Machine.Name, err)
return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to fetch server: %v", err))
}

// Generate ProviderID in format: stackit://<projectId>/<serverId>
providerID := fmt.Sprintf("%s://%s/%s", StackitProviderName, projectID, server.ID)
if server == nil {
// Call STACKIT API to create server
server, err = p.client.CreateServer(ctx, projectID, providerSpec.Region, createReq)
if err != nil {
klog.Errorf("Failed to create server for machine %q: %v", req.Machine.Name, err)
return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to create server: %v", err))
}
}

// NodeName is the machine name (will register with this name in Kubernetes)
nodeName := req.Machine.Name
if err := p.patchNetworkInterface(ctx, projectID, server.ID, providerSpec); err != nil {
klog.Errorf("Failed to patch network interface for server %q: %v", req.Machine.Name, err)
return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to patch network interface 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)

return &driver.CreateMachineResponse{
ProviderID: providerID,
NodeName: nodeName,
NodeName: req.Machine.Name,
}, nil
}

func (p *Provider) getServerByName(ctx context.Context, projectID, region, serverName string) (*Server, error) {
// Check if the server got already created
labelSelector := map[string]string{
StackitMachineLabel: serverName,
}
servers, err := p.client.ListServers(ctx, projectID, region, labelSelector)
if err != nil {
return nil, fmt.Errorf("SDK ListServers with labelSelector: %v failed: %w", labelSelector, err)
}

if len(servers) > 1 {
return nil, fmt.Errorf("%v servers found for server name %v", len(servers), serverName)
}

if len(servers) == 1 {
return servers[0], nil
}

// no servers found len == 0
return nil, nil
}

func (p *Provider) patchNetworkInterface(ctx context.Context, projectID, serverID string, providerSpec *api.ProviderSpec) error {
if len(providerSpec.AllowedAddresses) == 0 {
return nil
}

nics, err := p.client.GetNICsForServer(ctx, projectID, providerSpec.Region, serverID)
if err != nil {
return fmt.Errorf("failed to get NICs for server %q: %w", serverID, err)
}

if len(nics) == 0 {
return fmt.Errorf("failed to find NIC for server %q", serverID)
}

for _, nic := range nics {
// if networking is not set, server is inside the default network
// just patch the interface since the server should only have one
if providerSpec.Networking != nil {
// only process interfaces that are either in the configured network (NetworkID) or are defined in NICIDs
if providerSpec.Networking.NetworkID != nic.NetworkID && !slices.Contains(providerSpec.Networking.NICIDs, nic.ID) {
continue
}
}

updateNic := false
// check if every cidr in providerspec.allowedAddresses is inside the nic allowedAddresses
for _, allowedAddress := range providerSpec.AllowedAddresses {
if !slices.Contains(nic.AllowedAddresses, allowedAddress) {
nic.AllowedAddresses = append(nic.AllowedAddresses, allowedAddress)
updateNic = true
}
}

if !updateNic {
continue
}

if _, err := p.client.UpdateNIC(ctx, projectID, providerSpec.Region, nic.NetworkID, nic.ID, nic.AllowedAddresses); err != nil {
return fmt.Errorf("failed to update allowed addresses for NIC %s: %w", nic.ID, err)
}

klog.V(2).Infof("Updated allowed addresses for NIC %s to %v", nic.ID, nic.AllowedAddresses)
}

return nil
}

// DeleteMachine handles a machine deletion request by deleting the STACKIT server
//
// This method deletes the server identified by the ProviderID from STACKIT infrastructure.
Expand All @@ -216,33 +298,52 @@ func (p *Provider) DeleteMachine(ctx context.Context, req *driver.DeleteMachineR
klog.V(2).Infof("Machine deletion request has been received for %q", req.Machine.Name)
defer klog.V(2).Infof("Machine deletion request has been processed for %q", req.Machine.Name)

// Validate ProviderID exists
if req.Machine.Spec.ProviderID == "" {
return nil, status.Error(codes.InvalidArgument, "ProviderID is required")
}

// Extract credentials from Secret
serviceAccountKey := string(req.Secret.Data["serviceaccount.json"])

// Initialize client on first use (lazy initialization)
if err := p.ensureClient(serviceAccountKey); err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to initialize STACKIT client: %v", err))
}

// Parse ProviderID to extract projectID and serverID
projectID, serverID, err := parseProviderID(req.Machine.Spec.ProviderID)
var projectID, serverID string
var err error
if req.Machine.Spec.ProviderID != "" {
if !strings.HasPrefix(req.Machine.Spec.ProviderID, StackitProviderName) {
return nil, status.Error(codes.InvalidArgument, "providerID is not empty and does not start with stackit://")
}

// Parse ProviderID to extract projectID and serverID
projectID, serverID, err = parseProviderID(req.Machine.Spec.ProviderID)
if err != nil {
klog.V(2).Infof("invalid ProviderID format: %v", err)
}
}

if projectID == "" {
projectID = string(req.Secret.Data["project-id"])
}
if err != nil {
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid ProviderID format: %v", err))
}

providerSpec, err := decodeProviderSpec(req.MachineClass)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}

if serverID == "" {
server, err := p.getServerByName(ctx, projectID, providerSpec.Region, req.Machine.Name)
if err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("failed to find server by name: %v", err))
}

if server != nil {
serverID = server.ID
}
}

if serverID == "" {
klog.V(2).Infof("Server is already deleted for machine %q", req.Machine.Name)
return &driver.DeleteMachineResponse{}, nil
}

// Call STACKIT API to delete server
err = p.client.DeleteServer(ctx, projectID, providerSpec.Region, serverID)
if err != nil {
Expand Down Expand Up @@ -364,7 +465,9 @@ func (p *Provider) ListMachines(ctx context.Context, req *driver.ListMachinesReq
}

// Call STACKIT API to list all servers
labelSelector := fmt.Sprintf("%s=%s", StackitMachineClassLabel, req.MachineClass.Name)
labelSelector := map[string]string{
StackitMachineClassLabel: req.MachineClass.Name,
}
servers, err := p.client.ListServers(ctx, projectID, providerSpec.Region, labelSelector)
if err != nil {
klog.Errorf("Failed to list servers for MachineClass %q: %v", req.MachineClass.Name, err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/core_create_machine_basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ var _ = Describe("CreateMachine", func() {
Expect(err).To(HaveOccurred())
statusErr, ok := status.FromError(err)
Expect(ok).To(BeTrue())
Expect(statusErr.Code()).To(Equal(codes.Internal))
Expect(statusErr.Code()).To(Equal(codes.Unavailable))
})
})
})
Loading