mirror of
https://github.com/apple/container.git
synced 2026-08-27 02:46:31 +00:00
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:
+12
-5
@@ -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(
|
||||
+5
-3
@@ -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)
|
||||
|
||||
+5
-3
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user