Skip to content

Commit 4e7dce6

Browse files
authored
fix(k8s): correct k8s control plane components version using the user specified node image version (#2271)
1 parent 57f0b93 commit 4e7dce6

3 files changed

Lines changed: 67 additions & 7 deletions

File tree

Sources/ContainerK8s/Commands/K8sCreate.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ public struct K8sCreate: AsyncParsableCommand {
6868
}
6969
}
7070

71+
// Fail before provisioning the node VM.
72+
_ = try K8sHelper.kubernetesVersion(nodeImage: nodeImage)
73+
7174
let isTTY = isatty(FileHandle.standardError.fileDescriptor) == 1
7275
let progressConfig = try ProgressConfig(
7376
showSpinner: isTTY,
@@ -110,7 +113,7 @@ public struct K8sCreate: AsyncParsableCommand {
110113
progress.set(description: "Running kubeadm init")
111114
try await K8sHelper.prepareNode(nodeID: name, client: client, log: log)
112115
try await K8sHelper.bootstrapControlPlane(
113-
nodeID: name, apiServerSANs: sans, advertiseAddress: vmIP,
116+
nodeID: name, nodeImage: nodeImage, apiServerSANs: sans, advertiseAddress: vmIP,
114117
schedulable: provisioner.roles.contains(StandardRoles.worker),
115118
cniManifestPath: cni,
116119
client: client, log: log)

Sources/ContainerK8s/Support/K8sHelper+Bootstrap.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ extension K8sHelper {
3434
}
3535

3636
static func bootstrapControlPlane(
37-
nodeID: String, apiServerSANs: [String], advertiseAddress: String,
37+
nodeID: String, nodeImage: String, apiServerSANs: [String], advertiseAddress: String,
3838
schedulable: Bool, cniManifestPath: String? = nil, client: ContainerClient, log: Logger
3939
) async throws {
40-
let configYAML = initConfigYAML(advertiseAddress: advertiseAddress, certSANs: apiServerSANs)
40+
let configYAML = try initConfigYAML(nodeImage: nodeImage, advertiseAddress: advertiseAddress, certSANs: apiServerSANs)
4141
var r = try await execCapture(
4242
containerId: nodeID, executable: "/bin/sh",
4343
arguments: ["-c", "cat > /etc/kubernetes/kubeadm-config.yaml <<'EOF'\n\(configYAML)\nEOF"],
@@ -144,7 +144,7 @@ extension K8sHelper {
144144
"""
145145
}
146146

147-
private static func initConfigYAML(advertiseAddress: String, certSANs: [String]) -> String {
147+
private static func initConfigYAML(nodeImage: String, advertiseAddress: String, certSANs: [String]) throws -> String {
148148
let sans = certSANs.map { " - \($0)" }.joined(separator: "\n")
149149
return """
150150
apiVersion: kubeadm.k8s.io/v1beta4
@@ -157,7 +157,7 @@ extension K8sHelper {
157157
---
158158
apiVersion: kubeadm.k8s.io/v1beta4
159159
kind: ClusterConfiguration
160-
kubernetesVersion: \(kubernetesVersion())
160+
kubernetesVersion: \(try kubernetesVersion(nodeImage: nodeImage))
161161
networking:
162162
podSubnet: \(podSubnet)
163163
apiServer:
@@ -171,9 +171,14 @@ extension K8sHelper {
171171
"""
172172
}
173173

174-
private static func kubernetesVersion() -> String {
174+
/// kubeadm needs the exact version, and only the tag carries it.
175+
static func kubernetesVersion(nodeImage: String) throws -> String {
175176
let nameAndTag = nodeImage.split(separator: "@").first.map(String.init) ?? nodeImage
176-
guard let ref = try? Reference.parse(nameAndTag), let tag = ref.tag else { return "v1.35" }
177+
guard let ref = try? Reference.parse(nameAndTag), let tag = ref.tag else {
178+
throw ContainerizationError(
179+
.invalidArgument,
180+
message: "node image \(nodeImage) has no tag; use a tagged image such as docker.io/kindest/node:v1.34.11")
181+
}
177182
return tag
178183
}
179184
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Testing
18+
19+
@testable import ContainerK8s
20+
21+
struct K8sNodeImageVersionTests {
22+
@Test
23+
func versionComesFromTheGivenImage() throws {
24+
#expect(try K8sHelper.kubernetesVersion(nodeImage: "docker.io/kindest/node:v1.34.11") == "v1.34.11")
25+
}
26+
27+
@Test
28+
func versionComesFromTagWhenDigestIsPresent() throws {
29+
let image = "docker.io/kindest/node:v1.34.11@sha256:0000000000000000000000000000000000000000000000000000000000000000"
30+
#expect(try K8sHelper.kubernetesVersion(nodeImage: image) == "v1.34.11")
31+
}
32+
33+
@Test
34+
func defaultImageStillResolves() throws {
35+
#expect(try K8sHelper.kubernetesVersion(nodeImage: K8sHelper.nodeImage) == "v1.35.5")
36+
}
37+
38+
@Test
39+
func untaggedImageThrows() {
40+
#expect(throws: (any Error).self) {
41+
try K8sHelper.kubernetesVersion(nodeImage: "docker.io/kindest/node")
42+
}
43+
}
44+
45+
@Test
46+
func digestOnlyImageThrows() {
47+
let image = "docker.io/kindest/node@sha256:0000000000000000000000000000000000000000000000000000000000000000"
48+
#expect(throws: (any Error).self) {
49+
try K8sHelper.kubernetesVersion(nodeImage: image)
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)