refactor: extract K8s logic into ContainerK8s library target (#2079)

- Closes #2078.
- Move all K8s sources from the container-k8s executable
  into a new ContainerK8s library target. Sources/Plugins/K8s/
  becomes a thin entry point (K8sMain.swift) that calls
  K8sCommand.main().
This commit is contained in:
jshi991
2026-08-07 10:08:16 -07:00
committed by GitHub
parent abff4183b4
commit cec124f207
19 changed files with 92 additions and 39 deletions
+12 -5
View File
@@ -52,6 +52,7 @@ let package = Package(
.library(name: "TerminalProgress", targets: ["TerminalProgress"]),
.library(name: "MachineAPIClient", targets: ["MachineAPIClient"]),
.library(name: "MachineAPIService", targets: ["MachineAPIService"]),
.library(name: "ContainerK8s", targets: ["ContainerK8s"]),
],
dependencies: [
.package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)),
@@ -167,19 +168,21 @@ let package = Package(
.testTarget(
name: "K8sTests",
dependencies: [
"k8s",
"ContainerK8s",
"ContainerResource",
"Yams",
],
path: "Tests/K8sPluginTests"
),
.executableTarget(
name: "k8s",
.target(
name: "ContainerK8s",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Logging", package: "swift-log"),
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationOCI", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "SystemPackage", package: "swift-system"),
"ContainerAPIClient",
"ContainerLog",
"ContainerPersistence",
@@ -188,10 +191,14 @@ let package = Package(
"TerminalProgress",
"Yams",
],
path: "Sources/Plugins/K8s",
exclude: ["config.toml"],
resources: [.process("Resources/kindnet.yaml")]
),
.executableTarget(
name: "k8s",
dependencies: ["ContainerK8s"],
path: "Sources/Plugins/K8s",
exclude: ["config.toml"]
),
.executableTarget(
name: "container-apiserver",
dependencies: [
@@ -25,8 +25,10 @@ import Foundation
import Logging
import TerminalProgress
struct K8sCreate: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct K8sCreate: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "create",
abstract: "Create and start a local Kubernetes cluster"
)
@@ -49,7 +51,7 @@ struct K8sCreate: AsyncParsableCommand {
@Option(help: "Node image reference (default: \(K8sHelper.nodeImage))")
var nodeImage: String = K8sHelper.nodeImage
func run() async throws {
public func run() async throws {
LoggingSystem.bootstrap { _ in StderrLogHandler() }
let log = Logger(label: K8sHelper.pluginName)
@@ -21,8 +21,10 @@ import ContainerResource
import ContainerizationError
import Logging
struct K8sDelete: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct K8sDelete: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Delete a Kubernetes cluster",
aliases: ["rm"]
@@ -31,7 +33,7 @@ struct K8sDelete: AsyncParsableCommand {
@Option(name: .long, help: "Cluster name (default: \(K8sHelper.defaultName))")
var name: String = K8sHelper.defaultName
func run() async throws {
public func run() async throws {
LoggingSystem.bootstrap { _ in StderrLogHandler() }
let log = Logger(label: K8sHelper.pluginName)
@@ -20,14 +20,16 @@ import ContainerLog
import ContainerResource
import Logging
struct K8sList: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct K8sList: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List clusters and their nodes",
aliases: ["ls"]
)
func run() async throws {
public func run() async throws {
LoggingSystem.bootstrap { _ in StderrLogHandler() }
let snapshots = try await ContainerClient().list(
@@ -25,10 +25,12 @@ import Foundation
import Logging
import SystemPackage
struct K8sLoadImage: AsyncParsableCommand {
public struct K8sLoadImage: AsyncParsableCommand {
public init() {}
private static let ctrPath = "/usr/local/bin/ctr"
static let configuration = CommandConfiguration(
public static let configuration = CommandConfiguration(
commandName: "load-image",
abstract: "Load a container image into a cluster's containerd"
)
@@ -44,7 +46,7 @@ struct K8sLoadImage: AsyncParsableCommand {
)
var platform: String?
func run() async throws {
public func run() async throws {
LoggingSystem.bootstrap { _ in StderrLogHandler() }
let log = Logger(label: K8sHelper.pluginName)
@@ -22,8 +22,10 @@ import ContainerizationError
import Foundation
import Logging
struct K8sStart: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct K8sStart: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "start",
abstract: "Start a stopped Kubernetes cluster"
)
@@ -31,7 +33,7 @@ struct K8sStart: AsyncParsableCommand {
@Option(name: .long, help: "Cluster name (default: \(K8sHelper.defaultName))")
var name: String = K8sHelper.defaultName
func run() async throws {
public func run() async throws {
LoggingSystem.bootstrap { _ in StderrLogHandler() }
let log = Logger(label: K8sHelper.pluginName)
@@ -21,8 +21,10 @@ import Foundation
import Logging
import SystemPackage
struct K8sWriteConfig: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct K8sWriteConfig: AsyncParsableCommand {
public init() {}
public static let configuration = CommandConfiguration(
commandName: "write-config",
abstract: "Write the cluster context to a Kubernetes configuration file"
)
@@ -33,7 +35,7 @@ struct K8sWriteConfig: AsyncParsableCommand {
@Option(name: .long, help: "Path to the kubeconfig file to write or append to (default: ~/.kube/config)")
var kubeconfig: String?
func run() async throws {
public func run() async throws {
LoggingSystem.bootstrap { _ in StderrLogHandler() }
let log = Logger(label: K8sHelper.pluginName)
@@ -199,7 +199,7 @@ struct K8sHelper {
executable: executable, arguments: arguments, environment: [], terminal: false)
let proc = try await client.createProcess(
containerId: containerId, processId: UUID().uuidString.lowercased(),
configuration: config, stdio: [nil, pipe.fileHandleForWriting, nil])
configuration: config, stdio: [nil, pipe.fileHandleForWriting, pipe.fileHandleForWriting])
try await proc.start()
pipe.fileHandleForWriting.closeFile()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
@@ -291,8 +291,8 @@ struct K8sHelper {
sysctl -w net.bridge.bridge-nf-call-ip6tables=1 2>/dev/null || true
systemctl restart containerd
ctr -n k8s.io images tag registry.k8s.io/pause:3.10 registry.k8s.io/pause:3.10.1 2>/dev/null || true
iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1220
iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1220
/usr/sbin/iptables-nft -t mangle -A OUTPUT -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1220
/usr/sbin/iptables-nft -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1220
"""
}()
@@ -538,7 +538,7 @@ struct K8sHelper {
existing.clusters.append(contentsOf: config.clusters)
existing.contexts.append(contentsOf: config.contexts)
existing.users.append(contentsOf: config.users)
if setCurrentContext && existing.currentContext == nil {
if setCurrentContext {
existing.currentContext = containerId
}
@@ -23,6 +23,7 @@ public enum WarmupImage: String, CaseIterable, Sendable {
case alpine320 = "ghcr.io/linuxcontainers/alpine:3.20"
case alpine318 = "ghcr.io/linuxcontainers/alpine:3.18"
case busybox136 = "ghcr.io/containerd/busybox:1.36"
case kindestNodeV1_35_5 = "docker.io/kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95"
/// Directory under app-root holding OCI tar archives of each warmup image.
///
+5 -3
View File
@@ -15,11 +15,11 @@
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerK8s
import ContainerVersion
@main
struct K8sCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
public struct K8sCommand: AsyncParsableCommand {
public static let configuration = CommandConfiguration(
commandName: "k8s",
abstract: "Manage local Kubernetes development clusters (EXPERIMENTAL)",
discussion: """
@@ -56,4 +56,6 @@ struct K8sCommand: AsyncParsableCommand {
K8sWriteConfig.self,
]
)
public init() {}
}
+23
View File
@@ -0,0 +1,23 @@
//===----------------------------------------------------------------------===//
// 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 ContainerK8s
@main struct K8sPlugin {
static func main() async {
await K8sCommand.main()
}
}
@@ -74,6 +74,7 @@ struct TestK8sLoadImageSerial {
let name = "k8s-\(f.testID)"
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) }
try f.restoreWarmupImage(.kindestNodeV1_35_5)
print("[k8s-load] k8s create --name \(name)")
let result = try f.run(["k8s", "create", "--name", name])
print("[k8s-load] k8s create exit=\(result.status)")
@@ -85,7 +86,7 @@ struct TestK8sLoadImageSerial {
try result.check()
print("[k8s-load] pulling \(Self.testImage)")
try f.doPull(Self.testImage)
try f.restoreWarmupImage(.alpine320)
print("[k8s-load] k8s load-image --name \(name) \(Self.testImage)")
let loadResult = try f.run(["k8s", "load-image", "--name", name, Self.testImage])
@@ -95,6 +95,7 @@ struct TestK8sNetworkingSerial {
let name = "k8s-\(f.testID)"
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) }
try f.restoreWarmupImage(.kindestNodeV1_35_5)
print("[k8s-net] k8s create --name \(name)")
let result = try f.run(["k8s", "create", "--name", name])
print("[k8s-net] k8s create exit=\(result.status)")
@@ -106,7 +107,7 @@ struct TestK8sNetworkingSerial {
try result.check()
print("[k8s-net] pulling \(Self.testImage)")
try f.doPull(Self.testImage)
try f.restoreWarmupImage(.alpine320)
print("[k8s-net] k8s load-image --name \(name) \(Self.testImage)")
let loadResult = try f.run(["k8s", "load-image", "--name", name, Self.testImage])
@@ -143,6 +144,7 @@ struct TestK8sNetworkingSerial {
let name = "k8s-\(f.testID)"
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) }
try f.restoreWarmupImage(.kindestNodeV1_35_5)
print("[k8s-net] k8s create --name \(name)")
let result = try f.run(["k8s", "create", "--name", name])
print("[k8s-net] k8s create exit=\(result.status)")
@@ -153,7 +155,7 @@ struct TestK8sNetworkingSerial {
try result.check()
print("[k8s-net] pulling \(Self.testImage)")
try f.doPull(Self.testImage)
try f.restoreWarmupImage(.alpine320)
print("[k8s-net] k8s load-image --name \(name) \(Self.testImage)")
let loadResult = try f.run(["k8s", "load-image", "--name", name, Self.testImage])
@@ -60,6 +60,7 @@ struct TestK8sRunSerial {
let name = "k8s-\(f.testID)"
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) }
try f.restoreWarmupImage(.kindestNodeV1_35_5)
print("[k8s-run] k8s create --name \(name)")
let result = try f.run(["k8s", "create", "--name", name])
print("[k8s-run] k8s create exit=\(result.status)")
@@ -94,6 +95,7 @@ struct TestK8sRunSerial {
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name1]) }
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name2]) }
try f.restoreWarmupImage(.kindestNodeV1_35_5)
print("[k8s-run] k8s create --name \(name1)")
let result1 = try f.run(["k8s", "create", "--name", name1])
print("[k8s-run] k8s create exit=\(result1.status)")
@@ -99,6 +99,7 @@ struct TestK8sWriteConfigSerial {
let name = "k8s-\(f.testID)"
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) }
try f.restoreWarmupImage(.kindestNodeV1_35_5)
print("[k8s-cfg] k8s create --name \(name)")
let result = try f.run(["k8s", "create", "--name", name])
print("[k8s-cfg] k8s create exit=\(result.status)")
@@ -136,6 +137,7 @@ struct TestK8sWriteConfigSerial {
let name = "k8s-\(f.testID)"
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) }
try f.restoreWarmupImage(.kindestNodeV1_35_5)
print("[k8s-cfg] k8s create --name \(name)")
let result = try f.run(["k8s", "create", "--name", name])
print("[k8s-cfg] k8s create exit=\(result.status)")
@@ -165,6 +167,7 @@ struct TestK8sWriteConfigSerial {
let name = "k8s-\(f.testID)"
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) }
try f.restoreWarmupImage(.kindestNodeV1_35_5)
print("[k8s-cfg] k8s create --name \(name)")
let result = try f.run(["k8s", "create", "--name", name])
print("[k8s-cfg] k8s create exit=\(result.status)")
+1 -1
View File
@@ -19,7 +19,7 @@ import Foundation
import Testing
import Yams
@testable import k8s
@testable import ContainerK8s
// MARK: - Fixtures
@@ -21,7 +21,7 @@ import SystemPackage
import Testing
import Yams
@testable import k8s
@testable import ContainerK8s
// MARK: - Helpers
@@ -330,17 +330,17 @@ struct MergeConfigTests {
}
}
@Test func mergeSetsCurrentContextOnFirstClusterOnly() throws {
@Test func mergeAlwaysSetsCurrentContext() throws {
let (path, cleanup) = try makeTempFile()
defer { cleanup() }
// First create: sets current-context because kubeconfig is empty
// First create: sets current-context
try K8sHelper.mergeConfig(makeConfig(clusterName: "first"), containerId: "first", targetPath: path, setCurrentContext: true, log: log)
// Second create: does NOT overwrite current-context because it's already set
// Second create: overwrites current-context to point to the new cluster
try K8sHelper.mergeConfig(makeConfig(clusterName: "second"), containerId: "second", targetPath: path, setCurrentContext: true, log: log)
let result = try decode(String(contentsOfFile: path.string, encoding: .utf8))
#expect(result.currentContext == "first") // first-use semantics: not overwritten by second cluster
#expect(result.currentContext == "second") // always switches to the most recently created cluster
#expect(result.clusters.count == 2)
}
}