Skip to content

Commit

Permalink
build: adds cilium 1.17.1 rocks (#1086)
Browse files Browse the repository at this point in the history
* Report IPv4 and IPv6 node IPs

This is required for cilium 1.17.1 for dualstack scenarios.

* Adds cilium 1.17.1 rocks

The 1.17.1 cilium rocks have been built, and now we're adding it to
k8s-snap. Note that this version requires the nodes (kubelet) to also
report their IPv4 and IPv6 addresses for dualstack scenarios. k8sd
should be adding both of them to kubelet's --node-ip argument.

Updates gateway-api chart to 1.2.0.
  • Loading branch information
claudiubelu authored Mar 6, 2025
1 parent 6fc8c26 commit 31cc393
Show file tree
Hide file tree
Showing 15 changed files with 253 additions and 29 deletions.
2 changes: 1 addition & 1 deletion build-scripts/hack/generate-sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
# FIXME: This information should not be hardcoded here
CILIUM_ROCK_REPO = "https://github.com/canonical/cilium-rocks"
CILIUM_ROCK_TAG = "main"
CILIUM_VERSION = "1.16.3"
CILIUM_VERSION = "1.17.1"
COREDNS_ROCK_REPO = "https://github.com/canonical/coredns-rock"
COREDNS_ROCK_TAG = "main"
COREDNS_VERSION = "1.11.3"
Expand Down
2 changes: 1 addition & 1 deletion build-scripts/hack/update-cilium-chart.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash

VERSION="v1.16.7"
VERSION="v1.17.1"
DIR=$(realpath $(dirname "${0}"))

CHARTS_PATH="$DIR/../../k8s/manifests/charts"
Expand Down
2 changes: 1 addition & 1 deletion build-scripts/hack/update-gateway-api-chart.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/bin/bash

VERSION="v1.1.1"
VERSION="v1.2.0"
DIR=$(realpath $(dirname "${0}"))

CHARTS_PATH="$DIR/../../k8s/manifests/charts"
Expand Down
2 changes: 1 addition & 1 deletion docs/canonicalk8s/snap/howto/image-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ sudo /snap/k8s/current/bin/ctr --address /run/containerd/containerd.sock --names
You should see:

```
ghcr.io/canonical/cilium-operator-generic:1.16.3-ck0
ghcr.io/canonical/cilium-operator-generic:1.17.1-ck0
ghcr.io/canonical/cilium-operator-generic@sha256:e02dcce1e175312bf4dc2da6a97df49456a8eef6b2a1a9f2d68d4342dc0d3664
ghcr.io/canonical/k8s-snap/pause:3.10
ghcr.io/canonical/k8s-snap/pause@sha256:ee6521f290b2168b6e0935a181d4cff9be1ac3f505666ef0e3c98fae8199917a
Expand Down
19 changes: 16 additions & 3 deletions src/k8s/pkg/k8sd/app/hooks_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,17 @@ func (a *App) onBootstrapWorkerNode(ctx context.Context, s state.State, encodedT
}
// TODO(neoaggelos): figure out how to use the microcluster client instead

// nodeIPs will be passed to kubelet as the --node-ip parameter, allowing it to have multiple node IPs,
// including IPv4 and IPv6 addresses for dualstacks.
nodeIPs, err := utils.GetIPv46Addresses(nodeIP)
if err != nil {
return fmt.Errorf("failed to get local node IPs for kubelet: %w", err)
}

// Get remote certificate from the cluster member. We only need one node to be reachable for this.
// One might fail because the node is not part of the cluster anymore but was at the time the token was created.
var cert *x509.Certificate
var address string
var err error
for _, address = range token.JoinAddresses {
cert, err = utils.GetRemoteCertificate(address)
if err == nil {
Expand Down Expand Up @@ -254,7 +260,7 @@ func (a *App) onBootstrapWorkerNode(ctx context.Context, s state.State, encodedT
if err := setup.Containerd(snap, joinConfig.ExtraNodeContainerdConfig, joinConfig.ExtraNodeContainerdArgs); err != nil {
return fmt.Errorf("failed to configure containerd: %w", err)
}
if err := setup.KubeletWorker(snap, s.Name(), nodeIP, response.ClusterDNS, response.ClusterDomain, response.CloudProvider, joinConfig.ExtraNodeKubeletArgs); err != nil {
if err := setup.KubeletWorker(snap, s.Name(), nodeIPs, response.ClusterDNS, response.ClusterDomain, response.CloudProvider, joinConfig.ExtraNodeKubeletArgs); err != nil {
return fmt.Errorf("failed to configure kubelet: %w", err)
}
if err := setup.KubeProxy(ctx, snap, s.Name(), response.PodCIDR, localhostAddress, joinConfig.ExtraNodeKubeProxyArgs); err != nil {
Expand Down Expand Up @@ -305,6 +311,13 @@ func (a *App) onBootstrapControlPlane(ctx context.Context, s state.State, bootst
return fmt.Errorf("failed to parse node IP address %q", s.Address().Hostname())
}

// nodeIPs will be passed to kubelet as the --node-ip parameter, allowing it to have multiple node IPs,
// including IPv4 and IPv6 addresses for dualstacks.
nodeIPs, err := utils.GetIPv46Addresses(nodeIP)
if err != nil {
return fmt.Errorf("failed to get local node IPs for kubelet: %w", err)
}

var localhostAddress string
if nodeIP.To4() == nil {
localhostAddress = "[::1]"
Expand Down Expand Up @@ -457,7 +470,7 @@ func (a *App) onBootstrapControlPlane(ctx context.Context, s state.State, bootst
if err := setup.Containerd(snap, bootstrapConfig.ExtraNodeContainerdConfig, bootstrapConfig.ExtraNodeContainerdArgs); err != nil {
return fmt.Errorf("failed to configure containerd: %w", err)
}
if err := setup.KubeletControlPlane(snap, s.Name(), nodeIP, cfg.Kubelet.GetClusterDNS(), cfg.Kubelet.GetClusterDomain(), cfg.Kubelet.GetCloudProvider(), cfg.Kubelet.GetControlPlaneTaints(), bootstrapConfig.ExtraNodeKubeletArgs); err != nil {
if err := setup.KubeletControlPlane(snap, s.Name(), nodeIPs, cfg.Kubelet.GetClusterDNS(), cfg.Kubelet.GetClusterDomain(), cfg.Kubelet.GetCloudProvider(), cfg.Kubelet.GetControlPlaneTaints(), bootstrapConfig.ExtraNodeKubeletArgs); err != nil {
return fmt.Errorf("failed to configure kubelet: %w", err)
}
if err := setup.KubeProxy(ctx, snap, s.Name(), cfg.Network.GetPodCIDR(), localhostAddress, bootstrapConfig.ExtraNodeKubeProxyArgs); err != nil {
Expand Down
9 changes: 8 additions & 1 deletion src/k8s/pkg/k8sd/app/hooks_join.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ func (a *App) onPostJoin(ctx context.Context, s state.State, initConfig map[stri
return fmt.Errorf("failed to parse node IP address %q", s.Address().Hostname())
}

// nodeIPs will be passed to kubelet as the --node-ip parameter, allowing it to have multiple node IPs,
// including IPv4 and IPv6 addresses for dualstacks.
nodeIPs, err := utils.GetIPv46Addresses(nodeIP)
if err != nil {
return fmt.Errorf("failed to get local node IPs for kubelet: %w", err)
}

var localhostAddress string
if nodeIP.To4() == nil {
localhostAddress = "[::1]"
Expand Down Expand Up @@ -205,7 +212,7 @@ func (a *App) onPostJoin(ctx context.Context, s state.State, initConfig map[stri
if err := setup.Containerd(snap, joinConfig.ExtraNodeContainerdConfig, joinConfig.ExtraNodeContainerdArgs); err != nil {
return fmt.Errorf("failed to configure containerd: %w", err)
}
if err := setup.KubeletControlPlane(snap, s.Name(), nodeIP, cfg.Kubelet.GetClusterDNS(), cfg.Kubelet.GetClusterDomain(), cfg.Kubelet.GetCloudProvider(), cfg.Kubelet.GetControlPlaneTaints(), joinConfig.ExtraNodeKubeletArgs); err != nil {
if err := setup.KubeletControlPlane(snap, s.Name(), nodeIPs, cfg.Kubelet.GetClusterDNS(), cfg.Kubelet.GetClusterDomain(), cfg.Kubelet.GetCloudProvider(), cfg.Kubelet.GetControlPlaneTaints(), joinConfig.ExtraNodeKubeletArgs); err != nil {
return fmt.Errorf("failed to configure kubelet: %w", err)
}
if err := setup.KubeProxy(ctx, snap, s.Name(), cfg.Network.GetPodCIDR(), localhostAddress, joinConfig.ExtraNodeKubeProxyArgs); err != nil {
Expand Down
8 changes: 4 additions & 4 deletions src/k8s/pkg/k8sd/features/cilium/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var (
// ChartCilium represents manifests to deploy Cilium.
ChartCilium = helm.InstallableChart{
Name: "cilium",
Version: "1.16.7",
Version: "1.17.1",
InstallName: "ck-network",
InstallNamespace: "kube-system",
}
Expand All @@ -29,7 +29,7 @@ var (
// chartGateway represents manifests to deploy Gateway API CRDs.
chartGateway = helm.InstallableChart{
Name: "gateway-api",
Version: "1.1.1",
Version: "1.2.0",
InstallName: "ck-gateway",
InstallNamespace: "kube-system",
}
Expand All @@ -46,11 +46,11 @@ var (
ciliumAgentImageRepo = "ghcr.io/canonical/cilium"

// CiliumAgentImageTag is the tag to use for the cilium-agent image.
CiliumAgentImageTag = "1.16.7-ck0"
CiliumAgentImageTag = "1.17.1-ck0"

// ciliumOperatorImageRepo is the image to use for cilium-operator.
ciliumOperatorImageRepo = "ghcr.io/canonical/cilium-operator"

// ciliumOperatorImageTag is the tag to use for the cilium-operator image.
ciliumOperatorImageTag = "1.16.7-ck0"
ciliumOperatorImageTag = "1.17.1-ck0"
)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
21 changes: 14 additions & 7 deletions src/k8s/pkg/k8sd/setup/kubelet.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ var (
)

// KubeletControlPlane configures kubelet on a control plane node.
func KubeletControlPlane(snap snap.Snap, hostname string, nodeIP net.IP, clusterDNS string, clusterDomain string, cloudProvider string, registerWithTaints []string, extraArgs map[string]*string) error {
return kubelet(snap, hostname, nodeIP, clusterDNS, clusterDomain, cloudProvider, registerWithTaints, kubeletControlPlaneLabels, extraArgs)
func KubeletControlPlane(snap snap.Snap, hostname string, nodeIPs []net.IP, clusterDNS string, clusterDomain string, cloudProvider string, registerWithTaints []string, extraArgs map[string]*string) error {
return kubelet(snap, hostname, nodeIPs, clusterDNS, clusterDomain, cloudProvider, registerWithTaints, kubeletControlPlaneLabels, extraArgs)
}

// KubeletWorker configures kubelet on a worker node.
func KubeletWorker(snap snap.Snap, hostname string, nodeIP net.IP, clusterDNS string, clusterDomain string, cloudProvider string, extraArgs map[string]*string) error {
return kubelet(snap, hostname, nodeIP, clusterDNS, clusterDomain, cloudProvider, nil, kubeletWorkerLabels, extraArgs)
func KubeletWorker(snap snap.Snap, hostname string, nodeIPs []net.IP, clusterDNS string, clusterDomain string, cloudProvider string, extraArgs map[string]*string) error {
return kubelet(snap, hostname, nodeIPs, clusterDNS, clusterDomain, cloudProvider, nil, kubeletWorkerLabels, extraArgs)
}

// kubelet configures kubelet on the local node.
func kubelet(snap snap.Snap, hostname string, nodeIP net.IP, clusterDNS string, clusterDomain string, cloudProvider string, taints []string, labels []string, extraArgs map[string]*string) error {
func kubelet(snap snap.Snap, hostname string, nodeIPs []net.IP, clusterDNS string, clusterDomain string, cloudProvider string, taints []string, labels []string, extraArgs map[string]*string) error {
args := map[string]string{
"--authorization-mode": "Webhook",
"--anonymous-auth": "false",
Expand Down Expand Up @@ -80,8 +80,15 @@ func kubelet(snap snap.Snap, hostname string, nodeIP net.IP, clusterDNS string,
if clusterDomain != "" {
args["--cluster-domain"] = clusterDomain
}
if nodeIP != nil && !nodeIP.IsLoopback() {
args["--node-ip"] = nodeIP.String()

ips := []string{}
for _, nodeIP := range nodeIPs {
if !nodeIP.IsLoopback() {
ips = append(ips, nodeIP.String())
}
}
if len(ips) > 0 {
args["--node-ip"] = strings.Join(ips, ",")
}
if _, err := snaputil.UpdateServiceArguments(snap, "kubelet", args, nil); err != nil {
return fmt.Errorf("failed to render arguments file: %w", err)
Expand Down
20 changes: 10 additions & 10 deletions src/k8s/pkg/k8sd/setup/kubelet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestKubelet(t *testing.T) {
s := mustSetupSnapAndDirectories(t, setKubeletMock)

// Call the kubelet control plane setup function
g.Expect(setup.KubeletControlPlane(s, "dev", net.ParseIP("192.168.0.1"), "10.152.1.1", "test-cluster.local", "provider", nil, nil)).To(Succeed())
g.Expect(setup.KubeletControlPlane(s, "dev", []net.IP{net.ParseIP("192.168.0.1")}, "10.152.1.1", "test-cluster.local", "provider", nil, nil)).To(Succeed())

// Ensure the kubelet arguments file has the expected arguments and values
tests := []struct {
Expand Down Expand Up @@ -107,7 +107,7 @@ func TestKubelet(t *testing.T) {
"--my-extra-arg": utils.Pointer("my-extra-val"),
}
// Call the kubelet control plane setup function
g.Expect(setup.KubeletControlPlane(s, "dev", net.ParseIP("192.168.0.1"), "10.152.1.1", "test-cluster.local", "provider", nil, extraArgs)).To(Succeed())
g.Expect(setup.KubeletControlPlane(s, "dev", []net.IP{net.ParseIP("192.168.0.1")}, "10.152.1.1", "test-cluster.local", "provider", nil, extraArgs)).To(Succeed())

// Ensure the kubelet arguments file has the expected arguments and values
tests := []struct {
Expand Down Expand Up @@ -166,7 +166,7 @@ func TestKubelet(t *testing.T) {
s := mustSetupSnapAndDirectories(t, setKubeletMock)

// Call the kubelet control plane setup function
g.Expect(setup.KubeletControlPlane(s, "dev", nil, "", "", "", nil, nil)).To(Succeed())
g.Expect(setup.KubeletControlPlane(s, "dev", []net.IP{}, "", "", "", nil, nil)).To(Succeed())

tests := []struct {
key string
Expand Down Expand Up @@ -214,7 +214,7 @@ func TestKubelet(t *testing.T) {
s := mustSetupSnapAndDirectories(t, setKubeletMock)

// Call the kubelet worker setup function
g.Expect(setup.KubeletWorker(s, "dev", net.ParseIP("192.168.0.1"), "10.152.1.1", "test-cluster.local", "provider", nil)).To(Succeed())
g.Expect(setup.KubeletWorker(s, "dev", []net.IP{net.ParseIP("192.168.0.1")}, "10.152.1.1", "test-cluster.local", "provider", nil)).To(Succeed())

// Ensure the kubelet arguments file has the expected arguments and values
tests := []struct {
Expand Down Expand Up @@ -272,7 +272,7 @@ func TestKubelet(t *testing.T) {
}

// Call the kubelet worker setup function
g.Expect(setup.KubeletWorker(s, "dev", net.ParseIP("192.168.0.1"), "10.152.1.1", "test-cluster.local", "provider", extraArgs)).To(Succeed())
g.Expect(setup.KubeletWorker(s, "dev", []net.IP{net.ParseIP("192.168.0.1")}, "10.152.1.1", "test-cluster.local", "provider", extraArgs)).To(Succeed())

// Ensure the kubelet arguments file has the expected arguments and values
tests := []struct {
Expand Down Expand Up @@ -329,7 +329,7 @@ func TestKubelet(t *testing.T) {
s := mustSetupSnapAndDirectories(t, setKubeletMock)

// Call the kubelet worker setup function
g.Expect(setup.KubeletWorker(s, "dev", nil, "", "", "", nil)).To(Succeed())
g.Expect(setup.KubeletWorker(s, "dev", []net.IP{}, "", "", "", nil)).To(Succeed())

// Ensure the kubelet arguments file has the expected arguments and values
tests := []struct {
Expand Down Expand Up @@ -377,7 +377,7 @@ func TestKubelet(t *testing.T) {

s.Mock.ServiceArgumentsDir = "nonexistent"

g.Expect(setup.KubeletControlPlane(s, "dev", net.ParseIP("192.168.0.1"), "10.152.1.1", "test-cluster.local", "provider", nil, nil)).ToNot(Succeed())
g.Expect(setup.KubeletControlPlane(s, "dev", []net.IP{net.ParseIP("192.168.0.1")}, "10.152.1.1", "test-cluster.local", "provider", nil, nil)).ToNot(Succeed())
})

t.Run("WorkerNoArgsDir", func(t *testing.T) {
Expand All @@ -386,7 +386,7 @@ func TestKubelet(t *testing.T) {

s.Mock.ServiceArgumentsDir = "nonexistent"

g.Expect(setup.KubeletWorker(s, "dev", net.ParseIP("192.168.0.1"), "10.152.1.1", "test-cluster.local", "provider", nil)).ToNot(Succeed())
g.Expect(setup.KubeletWorker(s, "dev", []net.IP{net.ParseIP("192.168.0.1")}, "10.152.1.1", "test-cluster.local", "provider", nil)).ToNot(Succeed())
})

t.Run("HostnameOverride", func(t *testing.T) {
Expand All @@ -397,7 +397,7 @@ func TestKubelet(t *testing.T) {
s.Mock.Hostname = "dev"

// Call the kubelet control plane setup function
g.Expect(setup.KubeletControlPlane(s, "dev", net.ParseIP("192.168.0.1"), "10.152.1.1", "test-cluster.local", "provider", nil, nil)).To(Succeed())
g.Expect(setup.KubeletControlPlane(s, "dev", []net.IP{net.ParseIP("192.168.0.1")}, "10.152.1.1", "test-cluster.local", "provider", nil, nil)).To(Succeed())

val, err := snaputil.GetServiceArgument(s, "kubelet", "--hostname-override")
g.Expect(err).To(Not(HaveOccurred()))
Expand All @@ -412,7 +412,7 @@ func TestKubelet(t *testing.T) {
s.Mock.Hostname = "dev"

// Call the kubelet control plane setup function
g.Expect(setup.KubeletControlPlane(s, "dev", net.ParseIP("2001:db8::"), "2001:db8::1", "test-cluster.local", "provider", nil, nil)).To(Succeed())
g.Expect(setup.KubeletControlPlane(s, "dev", []net.IP{net.ParseIP("2001:db8::")}, "2001:db8::1", "test-cluster.local", "provider", nil, nil)).To(Succeed())

tests := []struct {
key string
Expand Down
87 changes: 87 additions & 0 deletions src/k8s/pkg/utils/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net"
"slices"
"syscall"
)

Expand All @@ -19,3 +20,89 @@ func IsLocalPortOpen(port string) (bool, error) {
return true, nil
}
}

// GetIPv46Addresses returns an IPv4 and IPv6 pair if possible, from the local network interface which has the
// given IP address. This looks through all the interface's IPs to find a global unicast IP of the opposite
// type (IPv4 / IPv6) from the input IP.
// GetIPv46Addresses will return an array containing only the given IP address if the pair could not be found.
// If the given IP address cannot be found locally, an error will be returned.
func GetIPv46Addresses(ip net.IP) ([]net.IP, error) {
ifaceIPs, err := getIPsFromIfaceWithIP(ip)
if err != nil {
return nil, err
}

// Return a [IPv4, IPv6] pair if possible.
isIPv4 := ip.To4() != nil
for _, ifaceIP := range ifaceIPs {
isIfaceIPv4 := ifaceIP.To4() != nil
if isIPv4 == isIfaceIPv4 {
// They are the same type. Skip.
continue
}

if ifaceIP.IsGlobalUnicast() {
return []net.IP{ip, ifaceIP}, nil
}
}

// Couldn't find pair. Return the given IP.
return []net.IP{ip}, nil
}

func getIPsFromIfaceWithIP(ip net.IP) ([]net.IP, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("failed to get local interfaces: %w", err)
}

var lastError error
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
// Continue, we may still find the right interface.
// Return lastError if we don't find it.
lastError = fmt.Errorf("failed to get local interface addresses: %w", err)
continue
}

ifaceIPs, err := parseIPAddresses(addrs)
if err != nil {
// Continue, we may still find the right interface.
// Return lastError if we don't find it.
lastError = err
continue
}

// Check if the given IP is in the list.
if slices.ContainsFunc(ifaceIPs, ip.Equal) {
return ifaceIPs, nil
}
}

if lastError != nil {
return nil, lastError
}

return nil, fmt.Errorf("failed to find a local interface associated with the node IP address '%s'", ip.String())
}

func parseIPAddresses(addrs []net.Addr) ([]net.IP, error) {
ips := []net.IP{}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}

if ip == nil {
return nil, fmt.Errorf("failed to parse node IP address '%s'", addr.String())
}
ips = append(ips, ip)
}

return ips, nil
}
Loading

0 comments on commit 31cc393

Please sign in to comment.