fix(k8s): correct k8s control plane components version using the user specified node image version (#2271)

This commit is contained in:
Di Warachet S.
2026-09-18 12:45:06 -07:00
committed by GitHub
parent 57f0b9392b
commit 4e7dce66ad
3 changed files with 67 additions and 7 deletions
@@ -68,6 +68,9 @@ public struct K8sCreate: AsyncParsableCommand {
}
}
// Fail before provisioning the node VM.
_ = try K8sHelper.kubernetesVersion(nodeImage: nodeImage)
let isTTY = isatty(FileHandle.standardError.fileDescriptor) == 1
let progressConfig = try ProgressConfig(
showSpinner: isTTY,
@@ -110,7 +113,7 @@ public struct K8sCreate: AsyncParsableCommand {
progress.set(description: "Running kubeadm init")
try await K8sHelper.prepareNode(nodeID: name, client: client, log: log)
try await K8sHelper.bootstrapControlPlane(
nodeID: name, apiServerSANs: sans, advertiseAddress: vmIP,
nodeID: name, nodeImage: nodeImage, apiServerSANs: sans, advertiseAddress: vmIP,
schedulable: provisioner.roles.contains(StandardRoles.worker),
cniManifestPath: cni,
client: client, log: log)
@@ -34,10 +34,10 @@ extension K8sHelper {
}
static func bootstrapControlPlane(
nodeID: String, apiServerSANs: [String], advertiseAddress: String,
nodeID: String, nodeImage: String, apiServerSANs: [String], advertiseAddress: String,
schedulable: Bool, cniManifestPath: String? = nil, client: ContainerClient, log: Logger
) async throws {
let configYAML = initConfigYAML(advertiseAddress: advertiseAddress, certSANs: apiServerSANs)
let configYAML = try initConfigYAML(nodeImage: nodeImage, advertiseAddress: advertiseAddress, certSANs: apiServerSANs)
var r = try await execCapture(
containerId: nodeID, executable: "/bin/sh",
arguments: ["-c", "cat > /etc/kubernetes/kubeadm-config.yaml <<'EOF'\n\(configYAML)\nEOF"],
@@ -144,7 +144,7 @@ extension K8sHelper {
"""
}
private static func initConfigYAML(advertiseAddress: String, certSANs: [String]) -> String {
private static func initConfigYAML(nodeImage: String, advertiseAddress: String, certSANs: [String]) throws -> String {
let sans = certSANs.map { " - \($0)" }.joined(separator: "\n")
return """
apiVersion: kubeadm.k8s.io/v1beta4
@@ -157,7 +157,7 @@ extension K8sHelper {
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: \(kubernetesVersion())
kubernetesVersion: \(try kubernetesVersion(nodeImage: nodeImage))
networking:
podSubnet: \(podSubnet)
apiServer:
@@ -171,9 +171,14 @@ extension K8sHelper {
"""
}
private static func kubernetesVersion() -> String {
/// kubeadm needs the exact version, and only the tag carries it.
static func kubernetesVersion(nodeImage: String) throws -> String {
let nameAndTag = nodeImage.split(separator: "@").first.map(String.init) ?? nodeImage
guard let ref = try? Reference.parse(nameAndTag), let tag = ref.tag else { return "v1.35" }
guard let ref = try? Reference.parse(nameAndTag), let tag = ref.tag else {
throw ContainerizationError(
.invalidArgument,
message: "node image \(nodeImage) has no tag; use a tagged image such as docker.io/kindest/node:v1.34.11")
}
return tag
}
}
@@ -0,0 +1,52 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Testing
@testable import ContainerK8s
struct K8sNodeImageVersionTests {
@Test
func versionComesFromTheGivenImage() throws {
#expect(try K8sHelper.kubernetesVersion(nodeImage: "docker.io/kindest/node:v1.34.11") == "v1.34.11")
}
@Test
func versionComesFromTagWhenDigestIsPresent() throws {
let image = "docker.io/kindest/node:v1.34.11@sha256:0000000000000000000000000000000000000000000000000000000000000000"
#expect(try K8sHelper.kubernetesVersion(nodeImage: image) == "v1.34.11")
}
@Test
func defaultImageStillResolves() throws {
#expect(try K8sHelper.kubernetesVersion(nodeImage: K8sHelper.nodeImage) == "v1.35.5")
}
@Test
func untaggedImageThrows() {
#expect(throws: (any Error).self) {
try K8sHelper.kubernetesVersion(nodeImage: "docker.io/kindest/node")
}
}
@Test
func digestOnlyImageThrows() {
let image = "docker.io/kindest/node@sha256:0000000000000000000000000000000000000000000000000000000000000000"
#expect(throws: (any Error).self) {
try K8sHelper.kubernetesVersion(nodeImage: image)
}
}
}