From e38ecca4ca0d55929183f92d68a4127c3a19fd30 Mon Sep 17 00:00:00 2001 From: Carlos Eduardo Arango Gutierrez Date: Fri, 13 Feb 2026 11:38:46 +0100 Subject: [PATCH] fix(templates): detect Kubernetes arch at runtime instead of defaulting to amd64 When the user doesn't explicitly set Kubernetes.Arch, use runtime detection (dpkg --print-architecture) instead of hardcoding "amd64". This ensures arm64 nodes download the correct architecture binaries for kubeadm, kubelet, kubectl, CNI plugins, and crictl. The user can still explicitly set Arch to override runtime detection. Signed-off-by: Carlos Eduardo Arango Gutierrez --- pkg/provisioner/templates/kubernetes.go | 9 +- pkg/provisioner/templates/kubernetes_test.go | 180 ++++++++++++++++++- 2 files changed, 178 insertions(+), 11 deletions(-) diff --git a/pkg/provisioner/templates/kubernetes.go b/pkg/provisioner/templates/kubernetes.go index f9fe66a60..97f6c64bb 100644 --- a/pkg/provisioner/templates/kubernetes.go +++ b/pkg/provisioner/templates/kubernetes.go @@ -34,7 +34,7 @@ K8S_VERSION="{{.Version}}" CNI_PLUGINS_VERSION="{{.CniPluginsVersion}}" CALICO_VERSION="{{.CalicoVersion}}" CRICTL_VERSION="{{.CrictlVersion}}" -ARCH="{{.Arch}}" +{{if .Arch}}ARCH="{{.Arch}}"{{else}}ARCH="$(dpkg --print-architecture 2>/dev/null || (uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/'))"{{end}} KUBELET_RELEASE_VERSION="{{.KubeletReleaseVersion}}" K8S_ENDPOINT_HOST="{{.K8sEndpointHost}}" @@ -822,7 +822,7 @@ GIT_COMMIT="{{.GitCommit}}" CNI_PLUGINS_VERSION="{{.CniPluginsVersion}}" CALICO_VERSION="{{.CalicoVersion}}" CRICTL_VERSION="{{.CrictlVersion}}" -ARCH="{{.Arch}}" +{{if .Arch}}ARCH="{{.Arch}}"{{else}}ARCH="$(dpkg --print-architecture 2>/dev/null || (uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/'))"{{end}} KUBELET_RELEASE_VERSION="{{.KubeletReleaseVersion}}" K8S_ENDPOINT_HOST="{{.K8sEndpointHost}}" @@ -1153,7 +1153,7 @@ TRACK_BRANCH="{{.TrackBranch}}" CNI_PLUGINS_VERSION="{{.CniPluginsVersion}}" CALICO_VERSION="{{.CalicoVersion}}" CRICTL_VERSION="{{.CrictlVersion}}" -ARCH="{{.Arch}}" +{{if .Arch}}ARCH="{{.Arch}}"{{else}}ARCH="$(dpkg --print-architecture 2>/dev/null || (uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/'))"{{end}} KUBELET_RELEASE_VERSION="{{.KubeletReleaseVersion}}" K8S_ENDPOINT_HOST="{{.K8sEndpointHost}}" @@ -1545,9 +1545,8 @@ func NewKubernetes(env v1alpha1.Environment) (*Kubernetes, error) { } if env.Spec.Kubernetes.Arch != "" { kubernetes.Arch = env.Spec.Kubernetes.Arch - } else { - kubernetes.Arch = "amd64" } + // When Arch is empty, the template uses runtime detection if env.Spec.Kubernetes.CniPluginsVersion != "" { kubernetes.CniPluginsVersion = env.Spec.Kubernetes.CniPluginsVersion } else { diff --git a/pkg/provisioner/templates/kubernetes_test.go b/pkg/provisioner/templates/kubernetes_test.go index 77d06be40..1a6b2597d 100644 --- a/pkg/provisioner/templates/kubernetes_test.go +++ b/pkg/provisioner/templates/kubernetes_test.go @@ -31,7 +31,7 @@ func TestNewKubernetes(t *testing.T) { want: &Kubernetes{ Version: "v1.30.0", KubeletReleaseVersion: defaultKubeletReleaseVersion, - Arch: defaultArch, + Arch: "", // empty = runtime detection CniPluginsVersion: defaultCNIPluginsVersion, CalicoVersion: defaultCalicoVersion, CrictlVersion: defaultCRIVersion, @@ -56,7 +56,7 @@ func TestNewKubernetes(t *testing.T) { want: &Kubernetes{ Version: "v1.31.0", KubeletReleaseVersion: defaultKubeletReleaseVersion, - Arch: defaultArch, + Arch: "", // empty = runtime detection CniPluginsVersion: defaultCNIPluginsVersion, CalicoVersion: defaultCalicoVersion, CrictlVersion: defaultCRIVersion, @@ -116,7 +116,7 @@ func TestNewKubernetes(t *testing.T) { }, want: &Kubernetes{ KubeletReleaseVersion: defaultKubeletReleaseVersion, - Arch: defaultArch, + Arch: "", // empty = runtime detection CniPluginsVersion: defaultCNIPluginsVersion, CalicoVersion: defaultCalicoVersion, CrictlVersion: defaultCRIVersion, @@ -144,7 +144,7 @@ func TestNewKubernetes(t *testing.T) { }, want: &Kubernetes{ KubeletReleaseVersion: defaultKubeletReleaseVersion, - Arch: defaultArch, + Arch: "", // empty = runtime detection CniPluginsVersion: defaultCNIPluginsVersion, CalicoVersion: defaultCalicoVersion, CrictlVersion: defaultCRIVersion, @@ -172,7 +172,7 @@ func TestNewKubernetes(t *testing.T) { }, want: &Kubernetes{ KubeletReleaseVersion: defaultKubeletReleaseVersion, - Arch: defaultArch, + Arch: "", // empty = runtime detection CniPluginsVersion: defaultCNIPluginsVersion, CalicoVersion: defaultCalicoVersion, CrictlVersion: defaultCRIVersion, @@ -197,7 +197,7 @@ func TestNewKubernetes(t *testing.T) { }, want: &Kubernetes{ KubeletReleaseVersion: defaultKubeletReleaseVersion, - Arch: defaultArch, + Arch: "", // empty = runtime detection CniPluginsVersion: defaultCNIPluginsVersion, CalicoVersion: defaultCalicoVersion, CrictlVersion: defaultCRIVersion, @@ -818,3 +818,171 @@ func TestIsLegacyKubernetesVersion(t *testing.T) { }) } } + +func TestKubernetes_ArchRuntimeDetection(t *testing.T) { + tests := []struct { + name string + arch string + expectedString string + notExpected string + }{ + { + name: "empty arch uses runtime detection", + arch: "", + expectedString: `dpkg --print-architecture`, + notExpected: "\nARCH=\"amd64\"\n", + }, + { + name: "explicit arm64 arch", + arch: "arm64", + expectedString: `ARCH="arm64"`, + notExpected: `dpkg --print-architecture`, + }, + { + name: "explicit amd64 arch", + arch: "amd64", + expectedString: `ARCH="amd64"`, + notExpected: `dpkg --print-architecture`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + Kubernetes: v1alpha1.Kubernetes{ + KubernetesVersion: "v1.30.0", + KubernetesInstaller: "kubeadm", + Arch: tt.arch, + }, + ContainerRuntime: v1alpha1.ContainerRuntime{ + Name: "containerd", + }, + }, + } + + k, err := NewKubernetes(env) + assert.NoError(t, err) + + var buf bytes.Buffer + err = k.Execute(&buf, env) + assert.NoError(t, err) + + out := buf.String() + assert.Contains(t, out, tt.expectedString, + "template output should contain %q", tt.expectedString) + assert.NotContains(t, out, tt.notExpected, + "template output should not contain %q", tt.notExpected) + }) + } +} + +func TestKubernetes_ArchRuntimeDetection_GitSource(t *testing.T) { + tests := []struct { + name string + arch string + expectedString string + notExpected string + }{ + { + name: "git source empty arch uses runtime detection", + arch: "", + expectedString: `dpkg --print-architecture`, + notExpected: "\nARCH=\"amd64\"\n", + }, + { + name: "git source explicit arm64 arch", + arch: "arm64", + expectedString: `ARCH="arm64"`, + notExpected: `dpkg --print-architecture`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + Kubernetes: v1alpha1.Kubernetes{ + KubernetesInstaller: "kubeadm", + Arch: tt.arch, + Source: v1alpha1.K8sSourceGit, + Git: &v1alpha1.K8sGitSpec{ + Ref: "v1.32.0-alpha.1", + }, + }, + ContainerRuntime: v1alpha1.ContainerRuntime{ + Name: "containerd", + }, + }, + } + + k, err := NewKubernetes(env) + assert.NoError(t, err) + + var buf bytes.Buffer + err = k.Execute(&buf, env) + assert.NoError(t, err) + + out := buf.String() + assert.Contains(t, out, tt.expectedString, + "template output should contain %q", tt.expectedString) + assert.NotContains(t, out, tt.notExpected, + "template output should not contain %q", tt.notExpected) + }) + } +} + +func TestKubernetes_ArchRuntimeDetection_LatestSource(t *testing.T) { + tests := []struct { + name string + arch string + expectedString string + notExpected string + }{ + { + name: "latest source empty arch uses runtime detection", + arch: "", + expectedString: `dpkg --print-architecture`, + notExpected: "\nARCH=\"amd64\"\n", + }, + { + name: "latest source explicit arm64 arch", + arch: "arm64", + expectedString: `ARCH="arm64"`, + notExpected: `dpkg --print-architecture`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + env := v1alpha1.Environment{ + Spec: v1alpha1.EnvironmentSpec{ + Kubernetes: v1alpha1.Kubernetes{ + KubernetesInstaller: "kubeadm", + Arch: tt.arch, + Source: v1alpha1.K8sSourceLatest, + Latest: &v1alpha1.K8sLatestSpec{ + Track: "master", + }, + }, + ContainerRuntime: v1alpha1.ContainerRuntime{ + Name: "containerd", + }, + }, + } + + k, err := NewKubernetes(env) + assert.NoError(t, err) + + var buf bytes.Buffer + err = k.Execute(&buf, env) + assert.NoError(t, err) + + out := buf.String() + assert.Contains(t, out, tt.expectedString, + "template output should contain %q", tt.expectedString) + assert.NotContains(t, out, tt.notExpected, + "template output should not contain %q", tt.notExpected) + }) + } +}