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
23 changes: 23 additions & 0 deletions docs/cloud-controller-manager.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Cloud controller manager

## Overview

The cloud controller manager implements the [Kubernetes cloud-controller-manager contract](https://kubernetes.io/docs/concepts/architecture/cloud-controller/#functions-of-the-ccm).

### Node controller
Comment thread
hammadzf marked this conversation as resolved.

The node controller is responsible for updating Node objects when new servers are created in STACKIT infrastructure by obtaining information about the servers.

For more information check the [Kubernetes documentation](https://kubernetes.io/docs/concepts/architecture/cloud-controller/#node-controller).

#### Multi Network

If a server has NICs connected to multiple networks, you can designate the primary network for [Node Addresses](https://kubernetes.io/docs/reference/node/node-status/#addresses) by setting the default network in the config:

```yaml
instance:
# either network name or id
defaultNetwork: "foo"
```

This ensures the IP address for that network's NIC is listed first in the [Node status](https://kubernetes.io/docs/reference/node/node-status/#addresses).
2 changes: 2 additions & 0 deletions docs/migration/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ loadBalancer:
extraLabels:
key1: value1
key2: value2
instance: {}
# defaultNetwork: # used for multi-network nodes
```

### CSI Configuration
Expand Down
38 changes: 36 additions & 2 deletions pkg/ccm/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import (
"errors"
"fmt"
"regexp"
"slices"
"strings"

"github.com/stackitcloud/cloud-provider-stackit/pkg/labels"
stackitclient "github.com/stackitcloud/cloud-provider-stackit/pkg/stackit/client"
"github.com/stackitcloud/cloud-provider-stackit/pkg/stackit/config"
"github.com/stackitcloud/cloud-provider-stackit/pkg/stackit/stackiterrors"
iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api"
corev1 "k8s.io/api/core/v1"
Expand All @@ -49,13 +51,15 @@ type Instances struct {
regionProviderID bool
iaasClient stackitclient.IaaSClient
region string
defaultNetwork string
}

func NewInstance(client stackitclient.IaaSClient, region string) (*Instances, error) {
func NewInstance(client stackitclient.IaaSClient, region string, opts config.InstanceOpts) (*Instances, error) {
return &Instances{
iaasClient: client,
region: region,
regionProviderID: false,
defaultNetwork: opts.DefaultNetwork,
}, nil
}

Expand Down Expand Up @@ -104,7 +108,7 @@ func (i *Instances) InstanceMetadata(ctx context.Context, node *corev1.Node) (*c
return nil, fmt.Errorf("server has no network interfaces")
}

nics := server.GetNics()
nics := sortNics(server.GetNics(), i.defaultNetwork)
for i := range nics {
nic := &nics[i]
if nic.HasIpv4() {
Expand Down Expand Up @@ -153,6 +157,36 @@ func (i *Instances) makeInstanceID(server *iaas.Server) string {
return fmt.Sprintf("%s://%s", ProviderName, server.GetId())
}

// sortNics sorts a slice of server network interfaces alphabetically by their network name
// to ensure a deterministic order. If a non-empty defaultNetwork is provided (matching either
// the NetworkName or NetworkId), that specific network interface is moved to the front (index 0)
// of the returned slice.
func sortNics(nics []iaas.ServerNetwork, defaultNetwork string) []iaas.ServerNetwork {
Comment thread
hown3d marked this conversation as resolved.
// nics are returned by IaaS API in a non-deterministic order
// Sort by network name so that every time we use the same order for node addresses
slices.SortFunc(nics, func(a, b iaas.ServerNetwork) int {
return strings.Compare(a.NetworkName, b.NetworkName)
})

if defaultNetwork == "" {
return nics
}

idx := slices.IndexFunc(nics, func(nic iaas.ServerNetwork) bool {
return nic.NetworkName == defaultNetwork || nic.NetworkId == defaultNetwork
})
// network not found
Comment thread
hammadzf marked this conversation as resolved.
if idx == -1 {
klog.Infof("no NIC found for default network %s", defaultNetwork)
return nics
}
defaultNic := nics[idx]
nics = slices.Delete(nics, idx, idx+1)
// prepend default nic
nics = slices.Insert(nics, 0, defaultNic)
return nics
}

// addToNodeAddresses appends the NodeAddresses to the passed-by-pointer slice,
// only if they do not already exist
func addToNodeAddresses(addresses *[]corev1.NodeAddress, addAddresses ...corev1.NodeAddress) {
Expand Down
38 changes: 37 additions & 1 deletion pkg/ccm/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
oapiError "github.com/stackitcloud/stackit-sdk-go/core/oapierror"

stackitclientmock "github.com/stackitcloud/cloud-provider-stackit/pkg/stackit/client/mock"
"github.com/stackitcloud/cloud-provider-stackit/pkg/stackit/config"
iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api"
"go.uber.org/mock/gomock"
corev1 "k8s.io/api/core/v1"
Expand All @@ -49,7 +50,7 @@ var _ = Describe("Node Controller", func() {
nodeMockClient = stackitclientmock.NewMockIaaSClient(ctrl)

var err error
instance, err = NewInstance(nodeMockClient, "eu01")
instance, err = NewInstance(nodeMockClient, "eu01", config.InstanceOpts{})
Expect(err).NotTo(HaveOccurred())
})

Expand Down Expand Up @@ -281,4 +282,39 @@ var _ = Describe("Node Controller", func() {
Expect(metadata).To(BeNil())
})
})

Describe("#sortNics", func() {
It("should return the nic of the default network as primary", func() {
nics := []iaas.ServerNetwork{
{
NetworkName: "abc",
NetworkId: "69",
Ipv4: new("10.0.0.69"),
},
{
NetworkName: "default",
NetworkId: "69",
Ipv4: new("192.168.0.123"),
},
{
NetworkName: "foo",
NetworkId: "123",
Ipv4: new("100.80.0.5"),
},
}
By("with network name")
newNics := sortNics(nics, "default")
Expect(newNics).To(HaveLen(3))
Expect(newNics[0].NetworkName).To(Equal("default"))
Expect(newNics[1].NetworkName).To(Equal("abc"))
Expect(newNics[2].NetworkName).To(Equal("foo"))

By("with network id")
Comment thread
hammadzf marked this conversation as resolved.
newNics = sortNics(nics, "123")
Expect(newNics).To(HaveLen(3))
Expect(newNics[0].NetworkId).To(Equal("123"))
Expect(newNics[1].NetworkId).To(Equal("69"))
Expect(newNics[2].NetworkId).To(Equal("69"))
})
})
})
2 changes: 1 addition & 1 deletion pkg/ccm/stackit.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func NewCloudControllerManager(cfg *stackitconfig.CCMConfig, obs *MetricsRemoteW
return nil, fmt.Errorf("failed to create IaaS client: %v", err)
}

instances, err := NewInstance(iaasClient, cfg.Global.Region)
instances, err := NewInstance(iaasClient, cfg.Global.Region, cfg.Instance)
if err != nil {
return nil, err
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/stackit/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ type CCMConfig struct {
Global GlobalOpts `yaml:"global"`
Metadata metadata.Opts `yaml:"metadata"`
LoadBalancer LoadBalancerOpts `yaml:"loadBalancer"`
Instance InstanceOpts `yaml:"instance"`
}

type InstanceOpts struct {
// DefaultNetwork contains the default network to use for a node.
// It can contain either the network name or ID.
// Can be used in mulit-network scenario to indicate which NIC is the primary one.
DefaultNetwork string `yaml:"defaultNetwork"`
}

type LoadBalancerOpts struct {
Expand Down