diff --git a/Makefile b/Makefile index ffc9f787..32ef0222 100644 --- a/Makefile +++ b/Makefile @@ -143,8 +143,8 @@ $(STAGING_DIR): @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin)" @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/bin)" @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/resources)" - @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/k8s/bin)" - @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/k8s/resources)" + @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/cluster/bin)" + @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/cluster/resources)" @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/build/bin)" @mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/builder/bin)" @@ -159,9 +159,9 @@ $(STAGING_DIR): @install "$(BUILD_BIN_DIR)/machine-apiserver" "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/bin/machine-apiserver)" @install Sources/Plugins/MachineAPIServer/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/config.toml)" @install Sources/Plugins/MachineAPIServer/Resources/init "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/resources/init)" - @install "$(BUILD_BIN_DIR)/k8s" "$(join $(STAGING_DIR), libexec/container/plugins/k8s/bin/k8s)" - @install Sources/Plugins/K8s/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/k8s/config.toml)" - @install Sources/Plugins/K8s/Resources/kindnet.yaml "$(join $(STAGING_DIR), libexec/container/plugins/k8s/resources/kindnet.yaml)" + @install "$(BUILD_BIN_DIR)/cluster" "$(join $(STAGING_DIR), libexec/container/plugins/cluster/bin/cluster)" + @install Sources/Plugins/Cluster/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/cluster/config.toml)" + @install Sources/Plugins/Cluster/Resources/kindnet.yaml "$(join $(STAGING_DIR), libexec/container/plugins/cluster/resources/kindnet.yaml)" @install "$(BUILD_BIN_DIR)/container-build" "$(join $(STAGING_DIR), libexec/container/plugins/build/bin/build)" @install Sources/Plugins/ContainerBuild/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/build/config.toml)" @install "$(BUILD_BIN_DIR)/container-builder" "$(join $(STAGING_DIR), libexec/container/plugins/builder/bin/builder)" @@ -181,7 +181,7 @@ installer-pkg: $(STAGING_DIR) @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. --entitlements=signing/container-runtime-linux.entitlements "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/bin/container-runtime-linux)" @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. --entitlements=signing/container-network-vmnet.entitlements "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/bin/container-network-vmnet)" @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/bin/machine-apiserver)" - @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/k8s/bin/k8s)" + @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/cluster/bin/cluster)" @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/build/bin/build)" @codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/builder/bin/builder)" diff --git a/Package.swift b/Package.swift index 7fa46f1b..4182b1ee 100644 --- a/Package.swift +++ b/Package.swift @@ -52,7 +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"]), + .library(name: "ContainerCluster", targets: ["ContainerCluster"]), ], dependencies: [ .package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)), @@ -179,16 +179,16 @@ let package = Package( ] ), .testTarget( - name: "K8sPluginTests", + name: "ClusterPluginTests", dependencies: [ "ContainerAPIClient", - "ContainerK8s", + "ContainerCluster", "ContainerResource", "Yams", ] ), .target( - name: "ContainerK8s", + name: "ContainerCluster", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "Logging", package: "swift-log"), @@ -207,9 +207,9 @@ let package = Package( ] ), .executableTarget( - name: "k8s", - dependencies: ["ContainerK8s"], - path: "Sources/Plugins/K8s", + name: "cluster", + dependencies: ["ContainerCluster"], + path: "Sources/Plugins/Cluster", exclude: ["config.toml", "Resources"] ), .executableTarget( diff --git a/README.md b/README.md index e6d0cfdc..8522fe32 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ Contributions to `container` are welcome and encouraged. Please see our [main co The container project is under active development. Its release versions are product versions, not semantic versions. -The `container` CLI compatibility generally preserves backward compatibility within a major release (not breaking existing scripts), however, there may be the odd case where breaking compatibility may be necessary. **Note:** Features marked *experimental* (for example, the `k8s` subcommand) may change and do not guarantee backward compatibility. +The `container` CLI compatibility generally preserves backward compatibility within a major release (not breaking existing scripts), however, there may be the odd case where breaking compatibility may be necessary. **Note:** Features marked *experimental* (for example, the `cluster` subcommand) may change and do not guarantee backward compatibility. The `container-apiserver` XPC API compatibility preserves forward and backward compatibility within a major version. Other non-public XPC helpers do not guarantee CLI or API compatibility across different versions. diff --git a/Sources/ContainerK8s/K8sHelper.swift b/Sources/ContainerCluster/ClusterHelper.swift similarity index 93% rename from Sources/ContainerK8s/K8sHelper.swift rename to Sources/ContainerCluster/ClusterHelper.swift index f264c563..86ffef17 100644 --- a/Sources/ContainerK8s/K8sHelper.swift +++ b/Sources/ContainerCluster/ClusterHelper.swift @@ -21,11 +21,11 @@ import ContainerizationOS import Foundation import Logging -// MARK: - K8sHelper +// MARK: - ClusterHelper -public struct K8sHelper { - public static let pluginName: String = "k8s" - public static let defaultName: String = "k8s-dev" +public struct ClusterHelper { + public static let pluginName: String = "cluster" + public static let defaultName: String = "cluster-dev" public static let controlPlaneRoleName: String = "control-plane" public static let workerRoleName: String = "worker" private static var defaultCPUs: Int64 { @@ -61,7 +61,7 @@ public struct K8sHelper { log_error() { echo "ERROR: $1" >&2; } # Disable the createContainer hook that copies /kind/product_* into every - # container's rootfs. Testing whether container k8s needs this at all -- + # container's rootfs. Testing whether container cluster needs this at all -- # unconfirmed whether container's VMs already assign a unique product_uuid # per node without it. if [ -f /etc/containerd/cri-base.json ]; then @@ -91,7 +91,7 @@ public struct K8sHelper { CURRENT_IP=$(ip -4 -o addr show | awk '$2 != "lo" && $2 !~ /^veth/ {print $4}' | head -1 | cut -d/ -f1) fi # Read the IP recorded on the previous boot, if any. - IP_MARKER=/etc/.container-k8s-last-ip + IP_MARKER=/etc/.container-cluster-last-ip OLD_IP="" [ -f "$IP_MARKER" ] && OLD_IP=$(cat "$IP_MARKER") @@ -182,7 +182,7 @@ public struct K8sHelper { // MARK: - List rows - static func buildK8sRows(from snapshots: [ContainerSnapshot]) -> [K8sNodeResource] { + static func buildClusterRows(from snapshots: [ContainerSnapshot]) -> [ClusterNodeResource] { var controlPlanes: [ContainerSnapshot] = [] var workers: [ContainerSnapshot] = [] for snapshot in snapshots { @@ -196,18 +196,18 @@ public struct K8sHelper { } } - var rows: [K8sNodeResource] = [] + var rows: [ClusterNodeResource] = [] var assignedWorkerIDs = Set() for cp in controlPlanes.sorted(by: { $0.configuration.id < $1.configuration.id }) { let clusterName = cp.configuration.id - rows.append(K8sNodeResource(clusterName: clusterName, snapshot: cp)) + rows.append(ClusterNodeResource(clusterName: clusterName, snapshot: cp)) let cpWorkers = workers .filter { $0.configuration.id.hasPrefix("\(clusterName)-worker-") } .sorted { $0.configuration.id < $1.configuration.id } for w in cpWorkers { - rows.append(K8sNodeResource(clusterName: clusterName, snapshot: w)) + rows.append(ClusterNodeResource(clusterName: clusterName, snapshot: w)) assignedWorkerIDs.insert(w.configuration.id) } } @@ -219,7 +219,7 @@ public struct K8sHelper { { let clusterName = w.configuration.id .components(separatedBy: "-worker-").dropLast().joined(separator: "-worker-") - rows.append(K8sNodeResource(clusterName: clusterName, snapshot: w)) + rows.append(ClusterNodeResource(clusterName: clusterName, snapshot: w)) } return rows @@ -234,9 +234,9 @@ public struct K8sHelper { } } -// MARK: - K8sNodeResource +// MARK: - ClusterNodeResource -struct K8sNodeResource: ManagedResource, ListDisplayable { +struct ClusterNodeResource: ManagedResource, ListDisplayable { let clusterName: String let snapshot: ContainerSnapshot diff --git a/Sources/ContainerK8s/Commands/K8sCreate.swift b/Sources/ContainerCluster/Commands/ClusterCreate.swift similarity index 83% rename from Sources/ContainerK8s/Commands/K8sCreate.swift rename to Sources/ContainerCluster/Commands/ClusterCreate.swift index 5ccb4136..260e4fc3 100644 --- a/Sources/ContainerK8s/Commands/K8sCreate.swift +++ b/Sources/ContainerCluster/Commands/ClusterCreate.swift @@ -25,7 +25,7 @@ import Foundation import Logging import TerminalProgress -public struct K8sCreate: AsyncParsableCommand { +public struct ClusterCreate: AsyncParsableCommand { public init() {} public static let configuration = CommandConfiguration( @@ -34,7 +34,7 @@ public struct K8sCreate: AsyncParsableCommand { ) @Option(name: .long, help: "Cluster name") - var name: String = K8sHelper.defaultName + var name: String = ClusterHelper.defaultName @Flag(name: [.customLong("rm"), .long], help: "Remove the cluster container after it stops") var remove: Bool = false @@ -49,7 +49,7 @@ public struct K8sCreate: AsyncParsableCommand { var imageFetchFlags: Flags.ImageFetch @Option(help: "Node image reference") - var nodeImage: String = K8sHelper.nodeImage + var nodeImage: String = ClusterHelper.nodeImage @Option(name: .long, help: "Optional path to a CNI manifest to apply, or \"NONE\" to skip installing a CNI.") var cni: String? @@ -68,13 +68,13 @@ public struct K8sCreate: AsyncParsableCommand { public func run() async throws { LoggingSystem.bootstrap { _ in StderrLogHandler() } - let log = Logger(label: K8sHelper.pluginName) + let log = Logger(label: ClusterHelper.pluginName) guard ManagedContainer.nameValid(name) else { throw ContainerizationError(.invalidArgument, message: "cluster name \(name) is not a valid container ID") } - let skipCNI = cni?.uppercased() == K8sHelper.noCNIName + let skipCNI = cni?.uppercased() == ClusterHelper.noCNIName if let cni, !skipCNI { guard FileManager.default.fileExists(atPath: cni) else { throw ContainerizationError(.invalidArgument, message: "CNI manifest not found at \(cni)") @@ -82,7 +82,7 @@ public struct K8sCreate: AsyncParsableCommand { } // Fail before provisioning the node VM. - _ = try K8sHelper.kubernetesVersion(nodeImage: nodeImage) + _ = try ClusterHelper.kubernetesVersion(nodeImage: nodeImage) let isTTY = isatty(FileHandle.standardError.fileDescriptor) == 1 // TotalTasks include: @@ -102,7 +102,7 @@ public struct K8sCreate: AsyncParsableCommand { progress.start() let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load() - let fqdn = K8sHelper.fqdn(for: name, domain: containerSystemConfig.dns.domain) + let fqdn = ClusterHelper.fqdn(for: name, domain: containerSystemConfig.dns.domain) let provisioner = try LinuxNodeProvisioner( clusterName: name, @@ -127,16 +127,16 @@ public struct K8sCreate: AsyncParsableCommand { if let fqdn { sans.append(contentsOf: [vmIP, fqdn]) } progress.set(description: "Running kubeadm init") - try await K8sHelper.prepareNode(nodeID: name, client: client, log: log) - try await K8sHelper.bootstrapControlPlane( + try await ClusterHelper.prepareNode(nodeID: name, client: client, log: log) + try await ClusterHelper.bootstrapControlPlane( nodeID: name, nodeImage: nodeImage, apiServerSANs: sans, advertiseAddress: vmIP, schedulable: provisioner.roles.contains(StandardRoles.worker), cniManifestPath: cni, client: client, log: log) if workers > 0 { - let (token, caCertHash) = try await K8sHelper.createJoinToken(nodeID: name, client: client) - let controlPlaneEndpoint = "\(vmIP):\(K8sHelper.clusterContainerPort)" + let (token, caCertHash) = try await ClusterHelper.createJoinToken(nodeID: name, client: client) + let controlPlaneEndpoint = "\(vmIP):\(ClusterHelper.clusterContainerPort)" for i in 1...workers { let workerName = "\(name)-worker-\(i)" progress.set(description: "Joining worker \(workerName)") @@ -165,27 +165,27 @@ public struct K8sCreate: AsyncParsableCommand { if skipCNI { progress.set(description: "Waiting for API server") - try await K8sHelper.waitForAPIServer(containerId: name, client: client, log: log) + try await ClusterHelper.waitForAPIServer(containerId: name, client: client, log: log) } else { progress.set(description: "Waiting for cluster to be ready") - try await K8sHelper.waitForReady(containerId: name, client: client, log: log) + try await ClusterHelper.waitForReady(containerId: name, client: client, log: log) } progress.set(description: "Writing kubeconfig") - let rawConfig = try await K8sHelper.fetchConfig(containerId: name, client: client, log: log) - let kubeConfig = try await K8sHelper.transformConfig(rawConfig, containerId: name, fqdn: fqdn, client: client) + let rawConfig = try await ClusterHelper.fetchConfig(containerId: name, client: client, log: log) + let kubeConfig = try await ClusterHelper.transformConfig(rawConfig, containerId: name, fqdn: fqdn, client: client) do { - try K8sHelper.mergeConfig(kubeConfig, containerId: name, setCurrentContext: true, log: log) + try ClusterHelper.mergeConfig(kubeConfig, containerId: name, setCurrentContext: true, log: log) } catch { log.warning("failed to write kubeconfig", metadata: ["name": "\(name)", "error": "\(error)"]) - log.info("cluster is running; use 'container k8s write-config --name \(name)' to write the kubeconfig") + log.info("cluster is running; use 'container cluster write-config --name \(name)' to write the kubeconfig") } } catch { for workerName in workerNames { await Self.teardownIfOwned(workerName, client: client, provisioner: provisioner, log: log) } await Self.teardownIfOwned(name, client: client, provisioner: provisioner, log: log) - try? K8sHelper.removeConfig(containerId: name, log: log) + try? ClusterHelper.removeConfig(containerId: name, log: log) throw error } @@ -198,7 +198,7 @@ public struct K8sCreate: AsyncParsableCommand { _ containerName: String, client: ContainerClient, provisioner: LinuxNodeProvisioner, log: Logger ) async { guard let container = try? await client.get(id: containerName) else { return } - guard container.configuration.labels[ResourceLabelKeys.plugin] == K8sHelper.pluginName else { + guard container.configuration.labels[ResourceLabelKeys.plugin] == ClusterHelper.pluginName else { log.warning( "container exists but is not owned by this plugin, refusing teardown", metadata: ["name": "\(containerName)"]) diff --git a/Sources/ContainerK8s/Commands/K8sDelete.swift b/Sources/ContainerCluster/Commands/ClusterDelete.swift similarity index 86% rename from Sources/ContainerK8s/Commands/K8sDelete.swift rename to Sources/ContainerCluster/Commands/ClusterDelete.swift index 1cae73e5..7878a258 100644 --- a/Sources/ContainerK8s/Commands/K8sDelete.swift +++ b/Sources/ContainerCluster/Commands/ClusterDelete.swift @@ -21,7 +21,7 @@ import ContainerResource import ContainerizationError import Logging -public struct K8sDelete: AsyncParsableCommand { +public struct ClusterDelete: AsyncParsableCommand { public init() {} public static let configuration = CommandConfiguration( @@ -31,23 +31,23 @@ public struct K8sDelete: AsyncParsableCommand { ) @Option(name: .long, help: "Cluster name") - var name: String = K8sHelper.defaultName + var name: String = ClusterHelper.defaultName public func run() async throws { LoggingSystem.bootstrap { _ in StderrLogHandler() } - let log = Logger(label: K8sHelper.pluginName) + let log = Logger(label: ClusterHelper.pluginName) let client = ContainerClient() let clusterContainer = try? await client.get(id: name) if let container = clusterContainer { - guard container.configuration.labels[ResourceLabelKeys.plugin] == K8sHelper.pluginName else { - log.error("container is not a k8s cluster, refusing delete", metadata: ["name": "\(name)"]) - throw ContainerizationError(.invalidArgument, message: "\(name) is not a k8s cluster") + guard container.configuration.labels[ResourceLabelKeys.plugin] == ClusterHelper.pluginName else { + log.error("container is not a cluster, refusing delete", metadata: ["name": "\(name)"]) + throw ContainerizationError(.invalidArgument, message: "\(name) is not a cluster") } } - let workerNames = (try? await K8sHelper.workerContainerNames(clusterName: name, client: client)) ?? [] + let workerNames = (try? await ClusterHelper.workerContainerNames(clusterName: name, client: client)) ?? [] var failures: [(name: String, error: Error)] = [] for workerName in workerNames { @@ -75,7 +75,7 @@ public struct K8sDelete: AsyncParsableCommand { // Only touch the kubeconfig for a cluster we found. Otherwise the entries // named `name` belong to someone else, so leave them alone. if clusterContainer != nil || !workerNames.isEmpty { - try K8sHelper.removeConfig(containerId: name, log: log) + try ClusterHelper.removeConfig(containerId: name, log: log) } else { log.debug("no cluster container found, leaving kubeconfig untouched", metadata: ["name": "\(name)"]) } diff --git a/Sources/ContainerK8s/Commands/K8sList.swift b/Sources/ContainerCluster/Commands/ClusterList.swift similarity index 86% rename from Sources/ContainerK8s/Commands/K8sList.swift rename to Sources/ContainerCluster/Commands/ClusterList.swift index 3e01bfba..37f54dd8 100644 --- a/Sources/ContainerK8s/Commands/K8sList.swift +++ b/Sources/ContainerCluster/Commands/ClusterList.swift @@ -20,7 +20,7 @@ import ContainerLog import ContainerResource import Logging -public struct K8sList: AsyncParsableCommand { +public struct ClusterList: AsyncParsableCommand { public init() {} public static let configuration = CommandConfiguration( @@ -33,9 +33,9 @@ public struct K8sList: AsyncParsableCommand { LoggingSystem.bootstrap { _ in StderrLogHandler() } let snapshots = try await ContainerClient().list( - filters: ContainerListFilters(labels: [ResourceLabelKeys.plugin: K8sHelper.pluginName]) + filters: ContainerListFilters(labels: [ResourceLabelKeys.plugin: ClusterHelper.pluginName]) ) - let rows = K8sHelper.buildK8sRows(from: snapshots) - print(K8sHelper.renderTable(rows)) + let rows = ClusterHelper.buildClusterRows(from: snapshots) + print(ClusterHelper.renderTable(rows)) } } diff --git a/Sources/ContainerK8s/Commands/K8sLoadImage.swift b/Sources/ContainerCluster/Commands/ClusterLoadImage.swift similarity index 90% rename from Sources/ContainerK8s/Commands/K8sLoadImage.swift rename to Sources/ContainerCluster/Commands/ClusterLoadImage.swift index 81575a65..45475002 100644 --- a/Sources/ContainerK8s/Commands/K8sLoadImage.swift +++ b/Sources/ContainerCluster/Commands/ClusterLoadImage.swift @@ -25,7 +25,7 @@ import Foundation import Logging import SystemPackage -public struct K8sLoadImage: AsyncParsableCommand { +public struct ClusterLoadImage: AsyncParsableCommand { public init() {} private static let ctrPath = "/usr/local/bin/ctr" @@ -36,7 +36,7 @@ public struct K8sLoadImage: AsyncParsableCommand { ) @Option(name: .long, help: "Cluster name") - var name: String = K8sHelper.defaultName + var name: String = ClusterHelper.defaultName @Argument(help: "Image reference to load (e.g. demo-api:latest)") var image: String @@ -51,10 +51,10 @@ public struct K8sLoadImage: AsyncParsableCommand { public func run() async throws { LoggingSystem.bootstrap { _ in StderrLogHandler() } - let log = Logger(label: K8sHelper.pluginName) + let log = Logger(label: ClusterHelper.pluginName) let tmpFile = FilePath(FileManager.default.temporaryDirectory.path(percentEncoded: false)) - .appending("k8s-image-\(UUID().uuidString).tar") + .appending("cluster-image-\(UUID().uuidString).tar") defer { try? FileManager.default.removeItem(atPath: tmpFile.string) } let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load() @@ -63,16 +63,16 @@ public struct K8sLoadImage: AsyncParsableCommand { // Guard: refuse to operate on a container not owned by this plugin. let container = try await client.get(id: name) - guard container.configuration.labels[ResourceLabelKeys.plugin] == K8sHelper.pluginName else { - log.error("container is not a k8s cluster, refusing image load", metadata: ["name": "\(name)"]) - throw ContainerizationError(.invalidArgument, message: "\(name) is not a k8s cluster") + guard container.configuration.labels[ResourceLabelKeys.plugin] == ClusterHelper.pluginName else { + log.error("container is not a cluster, refusing image load", metadata: ["name": "\(name)"]) + throw ContainerizationError(.invalidArgument, message: "\(name) is not a cluster") } - let workers = try await K8sHelper.workerContainerNames(clusterName: name, client: client) + let workers = try await ClusterHelper.workerContainerNames(clusterName: name, client: client) let targets = try Self.resolveTargets(clusterName: name, nodes: node, workers: workers) log.info("Saving image", metadata: ["ref": "\(image)"]) - let fq = K8sHelper.fqReference(image) + let fq = ClusterHelper.fqReference(image) let resolvedPlatform = try platform.map { try Platform(from: $0) } ?? Platform(from: "linux/\(Arch.hostArchitecture().rawValue)") try await ClientImage.save(references: [fq], out: tmpFile.string, platform: resolvedPlatform, containerSystemConfig: containerSystemConfig) diff --git a/Sources/ContainerK8s/Commands/K8sWriteConfig.swift b/Sources/ContainerCluster/Commands/ClusterWriteConfig.swift similarity index 73% rename from Sources/ContainerK8s/Commands/K8sWriteConfig.swift rename to Sources/ContainerCluster/Commands/ClusterWriteConfig.swift index 79fca032..632a3988 100644 --- a/Sources/ContainerK8s/Commands/K8sWriteConfig.swift +++ b/Sources/ContainerCluster/Commands/ClusterWriteConfig.swift @@ -21,7 +21,7 @@ import Foundation import Logging import SystemPackage -public struct K8sWriteConfig: AsyncParsableCommand { +public struct ClusterWriteConfig: AsyncParsableCommand { public init() {} public static let configuration = CommandConfiguration( @@ -30,20 +30,20 @@ public struct K8sWriteConfig: AsyncParsableCommand { ) @Option(name: .long, help: "Cluster name") - var name: String = K8sHelper.defaultName + var name: String = ClusterHelper.defaultName @Option(name: .long, help: "Path to the kubeconfig file to write or append to (default: ~/.kube/config)") var kubeconfig: String? public func run() async throws { LoggingSystem.bootstrap { _ in StderrLogHandler() } - let log = Logger(label: K8sHelper.pluginName) + let log = Logger(label: ClusterHelper.pluginName) let targetPath = kubeconfig.map { FilePath($0) } let client = ContainerClient() - let fqdn = await K8sHelper.detectFQDN(name: name) - let rawConfig = try await K8sHelper.fetchConfig(containerId: name, client: client, log: log) - let config = try await K8sHelper.transformConfig(rawConfig, containerId: name, fqdn: fqdn, client: client) - try K8sHelper.mergeConfig(config, containerId: name, targetPath: targetPath, log: log) + let fqdn = await ClusterHelper.detectFQDN(name: name) + let rawConfig = try await ClusterHelper.fetchConfig(containerId: name, client: client, log: log) + let config = try await ClusterHelper.transformConfig(rawConfig, containerId: name, fqdn: fqdn, client: client) + try ClusterHelper.mergeConfig(config, containerId: name, targetPath: targetPath, log: log) } } diff --git a/Sources/ContainerK8s/KubeConfig.swift b/Sources/ContainerCluster/KubeConfig.swift similarity index 100% rename from Sources/ContainerK8s/KubeConfig.swift rename to Sources/ContainerCluster/KubeConfig.swift diff --git a/Sources/ContainerK8s/Provisioners/LinuxNodeProvisioner.swift b/Sources/ContainerCluster/Provisioners/LinuxNodeProvisioner.swift similarity index 88% rename from Sources/ContainerK8s/Provisioners/LinuxNodeProvisioner.swift rename to Sources/ContainerCluster/Provisioners/LinuxNodeProvisioner.swift index 03382e56..4ca990d5 100644 --- a/Sources/ContainerK8s/Provisioners/LinuxNodeProvisioner.swift +++ b/Sources/ContainerCluster/Provisioners/LinuxNodeProvisioner.swift @@ -23,7 +23,7 @@ import Darwin import Foundation import Logging -/// A `NodeProvisioner` that runs a k8s node as a Linux container on the same host. +/// A `NodeProvisioner` that runs a cluster node as a Linux container on the same host. public struct LinuxNodeProvisioner: NodeProvisioner { public let roles: [String] @@ -70,16 +70,16 @@ public struct LinuxNodeProvisioner: NodeProvisioner { public func provision(name: String, log: Logger) async throws { let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load() - let resolvedImage = nodeImage ?? K8sHelper.nodeImage - try await K8sHelper.ensureImage(nodeImage: resolvedImage, log: log, containerSystemConfig: containerSystemConfig) + let resolvedImage = nodeImage ?? ClusterHelper.nodeImage + try await ClusterHelper.ensureImage(nodeImage: resolvedImage, log: log, containerSystemConfig: containerSystemConfig) let isControlPlane = roles.contains(StandardRoles.controlPlane) - let stateRoot = try PluginStateRoot(plugin: K8sHelper.pluginName).path + let stateRoot = try PluginStateRoot(plugin: ClusterHelper.pluginName).path try FileManager.default.createDirectory(atPath: stateRoot.string, withIntermediateDirectories: true) let portLock = open(stateRoot.appending("port.lock").string, O_CREAT | O_RDONLY, 0o600) defer { close(portLock) } if isControlPlane && fqdn == nil { flock(portLock, LOCK_EX) } - let publishPorts = isControlPlane && fqdn == nil ? [try await K8sHelper.clusterPort()] : [] + let publishPorts = isControlPlane && fqdn == nil ? [try await ClusterHelper.clusterPort()] : [] let management = Flags.Management( arch: Arch.hostArchitecture().rawValue, @@ -94,7 +94,7 @@ public struct LinuxNodeProvisioner: NodeProvisioner { kernel: nil, kernelArgs: [], labels: [ - "\(ResourceLabelKeys.plugin)=\(K8sHelper.pluginName)", + "\(ResourceLabelKeys.plugin)=\(ClusterHelper.pluginName)", "\(ResourceLabelKeys.role)=\(roles.joined(separator: ","))", ], maskedPaths: [], @@ -118,10 +118,10 @@ public struct LinuxNodeProvisioner: NodeProvisioner { volumes: [] ) - let updatedResource = K8sHelper.defaultedResourceFlags(Flags.Resource(cpus: cpus, memory: memory)) + let updatedResource = ClusterHelper.defaultedResourceFlags(Flags.Resource(cpus: cpus, memory: memory)) let processFlags = Flags.Process( cwd: nil, - env: K8sHelper.nodeProxyEnv(), + env: ClusterHelper.nodeProxyEnv(), envFile: [], gid: nil, interactive: false, @@ -134,7 +134,7 @@ public struct LinuxNodeProvisioner: NodeProvisioner { var (config, kernel, initfs) = try await Utility.containerConfigFromFlags( id: name, image: resolvedImage, - arguments: ["-c", K8sHelper.nodeEntrypointScript, "sh", "/sbin/init"], + arguments: ["-c", ClusterHelper.nodeEntrypointScript, "sh", "/sbin/init"], process: processFlags, management: management, resource: updatedResource, @@ -163,7 +163,7 @@ public struct LinuxNodeProvisioner: NodeProvisioner { try await process.start() try io.closeAfterStart() - try await K8sHelper.waitForNodeBooted(containerId: name, client: client, log: log) + try await ClusterHelper.waitForNodeBooted(containerId: name, client: client, log: log) } catch { try? await teardown(name: name, log: log) throw error @@ -187,16 +187,16 @@ public struct LinuxNodeProvisioner: NodeProvisioner { log: Logger ) async throws { let client = ContainerClient() - try await K8sHelper.prepareNode(nodeID: name, client: client, log: log) + try await ClusterHelper.prepareNode(nodeID: name, client: client, log: log) - let (code, output) = try await K8sHelper.execCapture( + let (code, output) = try await ClusterHelper.execCapture( containerId: name, - executable: K8sHelper.kubeadmPath, + executable: ClusterHelper.kubeadmPath, arguments: [ "join", controlPlaneEndpoint, "--token", token, "--discovery-token-ca-cert-hash", caCertHash, - "--ignore-preflight-errors", K8sHelper.ignorePreflightErrors, + "--ignore-preflight-errors", ClusterHelper.ignorePreflightErrors, "--cri-socket", "unix:///run/containerd/containerd.sock", ], client: client) @@ -212,7 +212,7 @@ public struct LinuxNodeProvisioner: NodeProvisioner { for attempt in 1...timeout { let code: Int32 do { - code = try await K8sHelper.runProbe( + code = try await ClusterHelper.runProbe( client: client, containerId: clusterName, arguments: ["wait", "--for=condition=Ready", "node/\(name)", "--timeout=2s"]) @@ -240,7 +240,7 @@ public struct LinuxNodeProvisioner: NodeProvisioner { for attempt in 1...timeout { let code: Int32 do { - code = try await K8sHelper.runProbe( + code = try await ClusterHelper.runProbe( client: client, containerId: clusterName, arguments: ["get", "node/\(name)", "--request-timeout=2s"]) diff --git a/Sources/ContainerK8s/Provisioners/NodeProvisioner.swift b/Sources/ContainerCluster/Provisioners/NodeProvisioner.swift similarity index 92% rename from Sources/ContainerK8s/Provisioners/NodeProvisioner.swift rename to Sources/ContainerCluster/Provisioners/NodeProvisioner.swift index 3b2ecc2d..fc428a8a 100644 --- a/Sources/ContainerK8s/Provisioners/NodeProvisioner.swift +++ b/Sources/ContainerCluster/Provisioners/NodeProvisioner.swift @@ -24,9 +24,9 @@ public struct StandardRoles { /// Manages the lifecycle of a cluster node (control plane or worker). /// -/// Implement this protocol to provision nodes for a cluster created by `K8sCreate`. +/// Implement this protocol to provision nodes for a cluster created by `ClusterCreate`. /// -/// For a **control-plane** node, `K8sCreate` calls: +/// For a **control-plane** node, `ClusterCreate` calls: /// 1. `provision` — start the machine before kubeadm init runs /// 2. `address` — return the node IP, used as the advertise address and cert SAN /// 3. `teardown` — called on failure to clean up the node @@ -36,9 +36,9 @@ public struct StandardRoles { /// 5. `waitForReady` — poll until the node is registered and Ready in the cluster, or /// `waitForRegistered` when no CNI is installed (nodes stay NotReady without one) /// -/// `K8sDelete` calls `teardown` before removing cluster containers. +/// `ClusterDelete` calls `teardown` before removing cluster containers. /// -/// If any provisioner step throws, `K8sCreate` tears down the cluster before re-throwing. +/// If any provisioner step throws, `ClusterCreate` tears down the cluster before re-throwing. public protocol NodeProvisioner: Sendable { /// The roles this node will serve (e.g. `[StandardRoles.controlPlane]`). var roles: [String] { get } diff --git a/Sources/ContainerK8s/Support/K8sHelper+Bootstrap.swift b/Sources/ContainerCluster/Support/ClusterHelper+Bootstrap.swift similarity index 99% rename from Sources/ContainerK8s/Support/K8sHelper+Bootstrap.swift rename to Sources/ContainerCluster/Support/ClusterHelper+Bootstrap.swift index 72079451..1708f2cd 100644 --- a/Sources/ContainerK8s/Support/K8sHelper+Bootstrap.swift +++ b/Sources/ContainerCluster/Support/ClusterHelper+Bootstrap.swift @@ -21,7 +21,7 @@ import ContainerizationOCI import Foundation import Logging -extension K8sHelper { +extension ClusterHelper { public static func prepareNode(nodeID: String, client: ContainerClient, log: Logger) async throws { log.info("Preparing node", metadata: ["id": "\(nodeID)"]) @@ -124,7 +124,7 @@ extension K8sHelper { guard let plugin = pluginLoader.findPlugin(forExecutable: CommandLine.executablePath), let resourceURL = plugin.resourceURL else { - throw ContainerizationError(.internalError, message: "unable to locate k8s plugin installation or resources") + throw ContainerizationError(.internalError, message: "unable to locate cluster plugin installation or resources") } let url = resourceURL.appendingPathComponent("kindnet.yaml") guard let contents = try? String(contentsOf: url, encoding: .utf8) else { diff --git a/Sources/ContainerK8s/Support/K8sHelper+Image.swift b/Sources/ContainerCluster/Support/ClusterHelper+Image.swift similarity index 84% rename from Sources/ContainerK8s/Support/K8sHelper+Image.swift rename to Sources/ContainerCluster/Support/ClusterHelper+Image.swift index 3574561c..88589c36 100644 --- a/Sources/ContainerK8s/Support/K8sHelper+Image.swift +++ b/Sources/ContainerCluster/Support/ClusterHelper+Image.swift @@ -20,16 +20,16 @@ import ContainerizationError import ContainerizationOCI import Logging -extension K8sHelper { +extension ClusterHelper { // MARK: - Image management - public static func ensureImage(nodeImage: String = K8sHelper.nodeImage, log: Logger, containerSystemConfig: ContainerSystemConfig) async throws { + public static func ensureImage(nodeImage: String = ClusterHelper.nodeImage, log: Logger, containerSystemConfig: ContainerSystemConfig) async throws { do { _ = try await ClientImage.get(reference: nodeImage, containerSystemConfig: containerSystemConfig) - log.debug("k8s node image present", metadata: ["ref": "\(nodeImage)"]) + log.debug("cluster node image present", metadata: ["ref": "\(nodeImage)"]) return } catch let error as ContainerizationError where error.code == .notFound { - log.info("Pulling k8s node image", metadata: ["ref": "\(nodeImage)"]) + log.info("Pulling cluster node image", metadata: ["ref": "\(nodeImage)"]) } let platform = try Platform(from: "linux/\(Arch.hostArchitecture().rawValue)") _ = try await ClientImage.fetch( diff --git a/Sources/ContainerK8s/Support/K8sHelper+Kubeconfig.swift b/Sources/ContainerCluster/Support/ClusterHelper+Kubeconfig.swift similarity index 98% rename from Sources/ContainerK8s/Support/K8sHelper+Kubeconfig.swift rename to Sources/ContainerCluster/Support/ClusterHelper+Kubeconfig.swift index 9cb72e00..3f2581af 100644 --- a/Sources/ContainerK8s/Support/K8sHelper+Kubeconfig.swift +++ b/Sources/ContainerCluster/Support/ClusterHelper+Kubeconfig.swift @@ -23,7 +23,7 @@ import Logging import SystemPackage import Yams -extension K8sHelper { +extension ClusterHelper { // MARK: - Kubeconfig private static var kubeconfigDir: FilePath { @@ -36,8 +36,8 @@ extension K8sHelper { let container = try await client.get(id: containerId) guard container.configuration.labels[ResourceLabelKeys.plugin] == pluginName else { - log.error("container is not a k8s cluster, refusing config fetch", metadata: ["name": "\(containerId)"]) - throw ContainerizationError(.invalidArgument, message: "\(containerId) is not a k8s cluster") + log.error("container is not a cluster, refusing config fetch", metadata: ["name": "\(containerId)"]) + throw ContainerizationError(.invalidArgument, message: "\(containerId) is not a cluster") } let (exitCode, yaml) = try await execCapture( diff --git a/Sources/ContainerK8s/Support/K8sHelper+Networking.swift b/Sources/ContainerCluster/Support/ClusterHelper+Networking.swift similarity index 99% rename from Sources/ContainerK8s/Support/K8sHelper+Networking.swift rename to Sources/ContainerCluster/Support/ClusterHelper+Networking.swift index 96564f95..cfed1f68 100644 --- a/Sources/ContainerK8s/Support/K8sHelper+Networking.swift +++ b/Sources/ContainerCluster/Support/ClusterHelper+Networking.swift @@ -22,7 +22,7 @@ import Darwin import Foundation import Logging -extension K8sHelper { +extension ClusterHelper { // MARK: - Networking private static var clusterHostPortBase: UInt16 { 6445 } diff --git a/Sources/ContainerK8s/Support/K8sHelper+Readiness.swift b/Sources/ContainerCluster/Support/ClusterHelper+Readiness.swift similarity index 87% rename from Sources/ContainerK8s/Support/K8sHelper+Readiness.swift rename to Sources/ContainerCluster/Support/ClusterHelper+Readiness.swift index e2dd3915..351c5322 100644 --- a/Sources/ContainerK8s/Support/K8sHelper+Readiness.swift +++ b/Sources/ContainerCluster/Support/ClusterHelper+Readiness.swift @@ -21,7 +21,7 @@ import ContainerizationOS import Foundation import Logging -extension K8sHelper { +extension ClusterHelper { // MARK: - Readiness public static func runProbe(client: ContainerClient, containerId: String, arguments: [String]) async throws -> Int32 { @@ -73,14 +73,14 @@ extension K8sHelper { arguments: ["get", "--raw", "/readyz", "--request-timeout=2s"]) } catch { throw ContainerizationError( - .internalError, message: "k8s cluster \(containerId) stopped unexpectedly during startup: \(error)") + .internalError, message: "cluster \(containerId) stopped unexpectedly during startup: \(error)") } if code == 0 { return } if attempt == timeout { log.info("check control-plane logs with 'container logs \(containerId)'") throw ContainerizationError( .timeout, - message: "k8s cluster \(containerId) API server did not become available within \(timeout * 2)s") + message: "cluster \(containerId) API server did not become available within \(timeout * 2)s") } try await Task.sleep(for: .seconds(2)) } @@ -101,14 +101,14 @@ extension K8sHelper { arguments: ["wait", "--for=condition=Ready", "node", "--all", "--timeout=2s"]) } catch { throw ContainerizationError( - .internalError, message: "k8s cluster \(containerId) stopped unexpectedly during startup: \(error)") + .internalError, message: "cluster \(containerId) stopped unexpectedly during startup: \(error)") } if code == 0 { break } if attempt == nodeReadyTimeout { log.info("inspect node state with 'container exec \(containerId) kubectl get nodes -o wide'") throw ContainerizationError( .timeout, - message: "k8s cluster \(containerId) control-plane node did not become Ready within \(nodeReadyTimeout * 2)s" + message: "cluster \(containerId) control-plane node did not become Ready within \(nodeReadyTimeout * 2)s" ) } try await Task.sleep(for: .seconds(2)) @@ -123,7 +123,7 @@ extension K8sHelper { arguments: ["wait", "--for=condition=Available", "deployment/coredns", "-n", "kube-system", "--timeout=2s"]) } catch { throw ContainerizationError( - .internalError, message: "k8s cluster \(containerId) stopped unexpectedly during startup: \(error)") + .internalError, message: "cluster \(containerId) stopped unexpectedly during startup: \(error)") } if code == 0 { return } if attempt < podReadyTimeout { @@ -133,7 +133,7 @@ extension K8sHelper { log.info("inspect pod state with 'container exec \(containerId) kubectl get pods -n kube-system'") throw ContainerizationError( .timeout, - message: "k8s cluster \(containerId) kube-system pods did not become available within \(podReadyTimeout * 2)s" + message: "cluster \(containerId) kube-system pods did not become available within \(podReadyTimeout * 2)s" ) } } diff --git a/Sources/ContainerK8s/Support/TableOutput.swift b/Sources/ContainerCluster/Support/TableOutput.swift similarity index 100% rename from Sources/ContainerK8s/Support/TableOutput.swift rename to Sources/ContainerCluster/Support/TableOutput.swift diff --git a/Sources/Plugins/K8s/K8sCommand.swift b/Sources/Plugins/Cluster/ClusterCommand.swift similarity index 70% rename from Sources/Plugins/K8s/K8sCommand.swift rename to Sources/Plugins/Cluster/ClusterCommand.swift index 0b245daf..9e86ce10 100644 --- a/Sources/Plugins/K8s/K8sCommand.swift +++ b/Sources/Plugins/Cluster/ClusterCommand.swift @@ -15,43 +15,43 @@ //===----------------------------------------------------------------------===// import ArgumentParser -import ContainerK8s +import ContainerCluster import ContainerVersion -public struct K8sCommand: AsyncParsableCommand { +public struct ClusterCommand: AsyncParsableCommand { public static let configuration = CommandConfiguration( - commandName: "k8s", + commandName: "cluster", abstract: "Manage local Kubernetes development clusters (EXPERIMENTAL)", discussion: """ EXAMPLES: Create a cluster by name and list clusters: - $ container k8s create --name my-cluster - $ container k8s list + $ container cluster create --name my-cluster + $ container cluster list Switch between clusters: - $ container k8s create --name second-cluster + $ container cluster create --name second-cluster $ kubectl config use-context my-cluster Write the cluster context to an alternate configuration file: - $ container k8s write-config --name my-cluster --kubeconfig ~/.kube/my-cluster.kubeconfig + $ container cluster write-config --name my-cluster --kubeconfig ~/.kube/my-cluster.kubeconfig $ KUBECONFIG=~/.kube/my-cluster.kubeconfig kubectl cluster-info Load a local image into the cluster and run it: $ container image pull docker.io/library/hello-world:latest $ container image tag docker.io/library/hello-world:latest registry.example.com/my-hello-world:latest - $ container k8s load-image --name my-cluster registry.example.com/my-hello-world:latest + $ container cluster load-image --name my-cluster registry.example.com/my-hello-world:latest $ kubectl run hello-job --image=registry.example.com/my-hello-world:latest --image-pull-policy=Never --restart=Never --attach --rm -i Stop and delete the cluster: - $ container k8s delete --name my-cluster + $ container cluster delete --name my-cluster """, - version: ReleaseVersion.singleLine(appName: "k8s"), + version: ReleaseVersion.singleLine(appName: "cluster"), subcommands: [ - K8sCreate.self, - K8sDelete.self, - K8sList.self, - K8sLoadImage.self, - K8sWriteConfig.self, + ClusterCreate.self, + ClusterDelete.self, + ClusterList.self, + ClusterLoadImage.self, + ClusterWriteConfig.self, ] ) diff --git a/Sources/Plugins/K8s/K8sMain.swift b/Sources/Plugins/Cluster/ClusterMain.swift similarity index 90% rename from Sources/Plugins/K8s/K8sMain.swift rename to Sources/Plugins/Cluster/ClusterMain.swift index c56fc907..c3939d1c 100644 --- a/Sources/Plugins/K8s/K8sMain.swift +++ b/Sources/Plugins/Cluster/ClusterMain.swift @@ -14,10 +14,10 @@ // limitations under the License. //===----------------------------------------------------------------------===// -import ContainerK8s +import ContainerCluster -@main struct K8sPlugin { +@main struct ClusterPlugin { static func main() async { - await K8sCommand.main() + await ClusterCommand.main() } } diff --git a/Sources/Plugins/K8s/Resources/kindnet.yaml b/Sources/Plugins/Cluster/Resources/kindnet.yaml similarity index 100% rename from Sources/Plugins/K8s/Resources/kindnet.yaml rename to Sources/Plugins/Cluster/Resources/kindnet.yaml diff --git a/Sources/Plugins/K8s/config.toml b/Sources/Plugins/Cluster/config.toml similarity index 100% rename from Sources/Plugins/K8s/config.toml rename to Sources/Plugins/Cluster/config.toml diff --git a/Tests/K8sPluginTests/K8sCreateCNITests.swift b/Tests/ClusterPluginTests/ClusterCreateCNITests.swift similarity index 77% rename from Tests/K8sPluginTests/K8sCreateCNITests.swift rename to Tests/ClusterPluginTests/ClusterCreateCNITests.swift index 17e45f6b..d39a3d0b 100644 --- a/Tests/K8sPluginTests/K8sCreateCNITests.swift +++ b/Tests/ClusterPluginTests/ClusterCreateCNITests.swift @@ -19,32 +19,32 @@ import Foundation import Logging import Testing -@testable import ContainerK8s +@testable import ContainerCluster -// MARK: - K8sCreate flag parsing +// MARK: - ClusterCreate flag parsing -@Suite("K8sCreate --cni flag") -struct K8sCreateCNIFlagTests { +@Suite("ClusterCreate --cni flag") +struct ClusterCreateCNIFlagTests { @Test func cniDefaultsToNilWhenNotProvided() throws { - let command = try K8sCreate.parse([]) + let command = try ClusterCreate.parse([]) #expect(command.cni == nil) } @Test func cniCapturesProvidedPath() throws { - let command = try K8sCreate.parse(["--cni", "/tmp/my-cni.yaml"]) + let command = try ClusterCreate.parse(["--cni", "/tmp/my-cni.yaml"]) #expect(command.cni == "/tmp/my-cni.yaml") } @Test(arguments: ["NONE", "none", "None"]) func cniAcceptsNone(value: String) throws { - let command = try K8sCreate.parse(["--cni", value]) + let command = try ClusterCreate.parse(["--cni", value]) #expect(command.cni == value) } } -// MARK: - K8sHelper.loadCNIManifest +// MARK: - ClusterHelper.loadCNIManifest -@Suite("K8sHelper.loadCNIManifest") +@Suite("ClusterHelper.loadCNIManifest") struct LoadCNIManifestTests { private let log = Logger(label: "test") @@ -55,7 +55,7 @@ struct LoadCNIManifestTests { try contents.write(to: url, atomically: true, encoding: .utf8) defer { try? FileManager.default.removeItem(at: url) } - let result = try await K8sHelper.loadCNIManifest(path: url.path, log: log) + let result = try await ClusterHelper.loadCNIManifest(path: url.path, log: log) #expect(result == contents) } @@ -64,7 +64,7 @@ struct LoadCNIManifestTests { .appendingPathComponent(UUID().uuidString + "-does-not-exist.yaml").path await #expect(throws: ContainerizationError.self) { - _ = try await K8sHelper.loadCNIManifest(path: missingPath, log: log) + _ = try await ClusterHelper.loadCNIManifest(path: missingPath, log: log) } } } diff --git a/Tests/K8sPluginTests/K8sListTests.swift b/Tests/ClusterPluginTests/ClusterListTests.swift similarity index 75% rename from Tests/K8sPluginTests/K8sListTests.swift rename to Tests/ClusterPluginTests/ClusterListTests.swift index 0f3776ca..2caa5601 100644 --- a/Tests/K8sPluginTests/K8sListTests.swift +++ b/Tests/ClusterPluginTests/ClusterListTests.swift @@ -19,7 +19,7 @@ import Foundation import Testing import Yams -@testable import ContainerK8s +@testable import ContainerCluster // MARK: - Fixtures @@ -37,7 +37,7 @@ private func makeSnapshot( memoryMiB: UInt64 = 2048, addr: String = "" ) throws -> ContainerSnapshot { - let labelsJSON = #"{"com.apple.container.plugin":"k8s","com.apple.container.resource.role":"\#(role)"}"# + let labelsJSON = #"{"com.apple.container.plugin":"cluster","com.apple.container.resource.role":"\#(role)"}"# let networksJSON: String if addr.isEmpty { networksJSON = "[]" @@ -65,40 +65,40 @@ private func makeSnapshot( } private func makeControlPlane(_ name: String, cpus: Int = fixtureDefaultCPUs, memoryMiB: UInt64 = fixtureDefaultMemoryMiB, addr: String = "") throws -> ContainerSnapshot { - try makeSnapshot(id: name, role: K8sHelper.controlPlaneRoleName, cpus: cpus, memoryMiB: memoryMiB, addr: addr) + try makeSnapshot(id: name, role: ClusterHelper.controlPlaneRoleName, cpus: cpus, memoryMiB: memoryMiB, addr: addr) } private func makeWorker(_ id: String, cpus: Int = max(fixtureDefaultCPUs / 2, 1), memoryMiB: UInt64 = max(fixtureDefaultMemoryMiB / 2, 512)) throws -> ContainerSnapshot { try makeSnapshot(id: id, role: "worker", cpus: cpus, memoryMiB: memoryMiB) } -// MARK: - K8sNodeResource header / row shape +// MARK: - ClusterNodeResource header / row shape -@Suite("K8sNodeResource") -struct K8sNodeRowTests { +@Suite("ClusterNodeResource") +struct ClusterNodeRowTests { @Test func tableHeaderHasEightColumns() throws { - #expect(K8sNodeResource.tableHeader.count == 8) + #expect(ClusterNodeResource.tableHeader.count == 8) } @Test func tableHeaderLabels() throws { - #expect(K8sNodeResource.tableHeader == ["CLUSTER", "NODE", "ROLE", "STATE", "CPUS", "MEMORY", "ADDR", "PORTS"]) + #expect(ClusterNodeResource.tableHeader == ["CLUSTER", "NODE", "ROLE", "STATE", "CPUS", "MEMORY", "ADDR", "PORTS"]) } @Test func rowColumnCountMatchesHeader() throws { let snapshot = try makeControlPlane("dev") - let row = K8sNodeResource(clusterName: "dev", snapshot: snapshot) - #expect(row.tableRow.count == K8sNodeResource.tableHeader.count) + let row = ClusterNodeResource(clusterName: "dev", snapshot: snapshot) + #expect(row.tableRow.count == ClusterNodeResource.tableHeader.count) } @Test func controlPlaneRowValues() throws { let cpus = fixtureDefaultCPUs let memoryMiB = fixtureDefaultMemoryMiB let snapshot = try makeControlPlane("dev", cpus: cpus, memoryMiB: memoryMiB, addr: "10.0.0.1") - let row = K8sNodeResource(clusterName: "dev", snapshot: snapshot) + let row = ClusterNodeResource(clusterName: "dev", snapshot: snapshot) let cols = row.tableRow #expect(cols[0] == "dev") // CLUSTER #expect(cols[1] == "dev") // NODE - #expect(cols[2] == K8sHelper.controlPlaneRoleName) // ROLE + #expect(cols[2] == ClusterHelper.controlPlaneRoleName) // ROLE #expect(cols[3] == "running") // STATE #expect(cols[4] == "\(cpus)") // CPUS #expect(cols[5] == "\(memoryMiB) MB") // MEMORY @@ -111,7 +111,7 @@ struct K8sNodeRowTests { let cpus = max(fixtureDefaultCPUs / 2, 1) let memoryMiB = max(fixtureDefaultMemoryMiB / 2, 512) let snapshot = try makeWorker("dev-worker-1", cpus: cpus, memoryMiB: memoryMiB) - let row = K8sNodeResource(clusterName: "dev", snapshot: snapshot) + let row = ClusterNodeResource(clusterName: "dev", snapshot: snapshot) let cols = row.tableRow #expect(cols[0] == "dev") // CLUSTER #expect(cols[1] == "dev-worker-1") // NODE @@ -122,22 +122,22 @@ struct K8sNodeRowTests { @Test func quietValueIsNodeID() throws { let snapshot = try makeWorker("dev-worker-2") - let row = K8sNodeResource(clusterName: "dev", snapshot: snapshot) + let row = ClusterNodeResource(clusterName: "dev", snapshot: snapshot) #expect(row.quietValue == "dev-worker-2") } } -// MARK: - K8sHelper.workerContainerNames +// MARK: - ClusterHelper.workerContainerNames -@Suite("K8sHelper.workerContainerNames") +@Suite("ClusterHelper.workerContainerNames") struct WorkerContainerNamesTests { @Test func emptyInputProducesNoNames() { - #expect(K8sHelper.workerContainerNames(from: [], clusterName: "dev").isEmpty) + #expect(ClusterHelper.workerContainerNames(from: [], clusterName: "dev").isEmpty) } @Test func excludesControlPlane() throws { let cp = try makeControlPlane("dev") - let names = K8sHelper.workerContainerNames(from: [cp], clusterName: "dev") + let names = ClusterHelper.workerContainerNames(from: [cp], clusterName: "dev") #expect(names.isEmpty) } @@ -145,29 +145,29 @@ struct WorkerContainerNamesTests { let cp = try makeControlPlane("dev") let w1 = try makeWorker("dev-worker-1") let w2 = try makeWorker("dev-worker-2") - let names = K8sHelper.workerContainerNames(from: [cp, w2, w1], clusterName: "dev") + let names = ClusterHelper.workerContainerNames(from: [cp, w2, w1], clusterName: "dev") #expect(names == ["dev-worker-1", "dev-worker-2"]) } @Test func excludesWorkersFromOtherClusters() throws { let w1 = try makeWorker("dev-worker-1") let other = try makeWorker("dev2-worker-1") - let names = K8sHelper.workerContainerNames(from: [w1, other], clusterName: "dev") + let names = ClusterHelper.workerContainerNames(from: [w1, other], clusterName: "dev") #expect(names == ["dev-worker-1"]) } } -// MARK: - buildK8sRows ordering and grouping +// MARK: - buildClusterRows ordering and grouping -@Suite("K8sHelper.buildK8sRows") -struct BuildK8sRowsTests { +@Suite("ClusterHelper.buildClusterRows") +struct BuildClusterRowsTests { @Test func emptyInputProducesNoRows() { - #expect(K8sHelper.buildK8sRows(from: []).isEmpty) + #expect(ClusterHelper.buildClusterRows(from: []).isEmpty) } @Test func singleControlPlaneAlone() throws { let cp = try makeControlPlane("dev") - let rows = K8sHelper.buildK8sRows(from: [cp]) + let rows = ClusterHelper.buildClusterRows(from: [cp]) #expect(rows.count == 1) #expect(rows[0].snapshot.configuration.id == "dev") } @@ -176,7 +176,7 @@ struct BuildK8sRowsTests { let cp = try makeControlPlane("dev") let w1 = try makeWorker("dev-worker-1") let w2 = try makeWorker("dev-worker-2") - let rows = K8sHelper.buildK8sRows(from: [w2, w1, cp]) + let rows = ClusterHelper.buildClusterRows(from: [w2, w1, cp]) #expect(rows.count == 3) #expect(rows[0].snapshot.id == "dev") #expect(rows[1].snapshot.id == "dev-worker-1") @@ -185,17 +185,17 @@ struct BuildK8sRowsTests { @Test func mixedControlPlaneRoleIsGroupedAsControlPlane() throws { let cp = try makeSnapshot( - id: "k8s-dev", - role: "\(K8sHelper.controlPlaneRoleName),worker") - let worker = try makeWorker("k8s-dev-worker-1") + id: "cluster-dev", + role: "\(ClusterHelper.controlPlaneRoleName),worker") + let worker = try makeWorker("cluster-dev-worker-1") - let rows = K8sHelper.buildK8sRows(from: [worker, cp]) + let rows = ClusterHelper.buildClusterRows(from: [worker, cp]) #expect(rows.count == 2) - #expect(rows[0].snapshot.id == "k8s-dev") - #expect(rows[0].clusterName == "k8s-dev") - #expect(rows[1].snapshot.id == "k8s-dev-worker-1") - #expect(rows[1].clusterName == "k8s-dev") + #expect(rows[0].snapshot.id == "cluster-dev") + #expect(rows[0].clusterName == "cluster-dev") + #expect(rows[1].snapshot.id == "cluster-dev-worker-1") + #expect(rows[1].clusterName == "cluster-dev") } @Test func workersGroupedUnderCorrectControlPlane() throws { @@ -203,7 +203,7 @@ struct BuildK8sRowsTests { let cp2 = try makeControlPlane("beta") let w1 = try makeWorker("alpha-worker-1") let w2 = try makeWorker("beta-worker-1") - let rows = K8sHelper.buildK8sRows(from: [w2, cp2, w1, cp1]) + let rows = ClusterHelper.buildClusterRows(from: [w2, cp2, w1, cp1]) #expect(rows[0].snapshot.id == "alpha") #expect(rows[1].snapshot.id == "alpha-worker-1") #expect(rows[1].clusterName == "alpha") @@ -215,7 +215,7 @@ struct BuildK8sRowsTests { @Test func multipleClustersAreSortedByName() throws { let cpZ = try makeControlPlane("zebra") let cpA = try makeControlPlane("apple") - let rows = K8sHelper.buildK8sRows(from: [cpZ, cpA]) + let rows = ClusterHelper.buildClusterRows(from: [cpZ, cpA]) #expect(rows[0].snapshot.id == "apple") #expect(rows[1].snapshot.id == "zebra") } @@ -223,7 +223,7 @@ struct BuildK8sRowsTests { @Test func orphanedWorkerAppearsAtEnd() throws { let cp = try makeControlPlane("dev") let orphan = try makeWorker("old-worker-1") - let rows = K8sHelper.buildK8sRows(from: [orphan, cp]) + let rows = ClusterHelper.buildClusterRows(from: [orphan, cp]) #expect(rows.count == 2) #expect(rows[0].snapshot.id == "dev") #expect(rows[1].snapshot.id == "old-worker-1") @@ -232,7 +232,7 @@ struct BuildK8sRowsTests { @Test func orphanedWorkerClusterNameDerivedFromID() throws { let orphan = try makeWorker("mycluster-worker-3") - let rows = K8sHelper.buildK8sRows(from: [orphan]) + let rows = ClusterHelper.buildClusterRows(from: [orphan]) #expect(rows.count == 1) #expect(rows[0].clusterName == "mycluster") } @@ -240,7 +240,7 @@ struct BuildK8sRowsTests { @Test func clusterNameWithWorkerInItIsHandledCorrectly() throws { let cp = try makeControlPlane("foo-worker") let w = try makeWorker("foo-worker-worker-1") - let rows = K8sHelper.buildK8sRows(from: [w, cp]) + let rows = ClusterHelper.buildClusterRows(from: [w, cp]) #expect(rows.count == 2) #expect(rows[0].snapshot.id == "foo-worker") #expect(rows[1].snapshot.id == "foo-worker-worker-1") @@ -250,31 +250,31 @@ struct BuildK8sRowsTests { @Test func workerNotAssignedToWrongCluster() throws { let cp = try makeControlPlane("dev") let w = try makeWorker("dev2-worker-1") - let rows = K8sHelper.buildK8sRows(from: [w, cp]) + let rows = ClusterHelper.buildClusterRows(from: [w, cp]) #expect(rows[0].snapshot.id == "dev") #expect(rows[1].snapshot.id == "dev2-worker-1") #expect(rows[1].clusterName == "dev2") } } -// MARK: - K8sHelper.fqdn +// MARK: - ClusterHelper.fqdn -@Suite("K8sHelper.fqdn") +@Suite("ClusterHelper.fqdn") struct FQDNTests { @Test func nilDomainReturnsNil() { - #expect(K8sHelper.fqdn(for: "dev", domain: nil) == nil) + #expect(ClusterHelper.fqdn(for: "dev", domain: nil) == nil) } @Test func emptyDomainReturnsNil() { - #expect(K8sHelper.fqdn(for: "dev", domain: "") == nil) + #expect(ClusterHelper.fqdn(for: "dev", domain: "") == nil) } @Test func nameWithDotReturnedAsIs() { - #expect(K8sHelper.fqdn(for: "dev.local", domain: "example.com") == "dev.local") + #expect(ClusterHelper.fqdn(for: "dev.local", domain: "example.com") == "dev.local") } @Test func plainNameGetsDomainAppended() { - #expect(K8sHelper.fqdn(for: "dev", domain: "example.com") == "dev.example.com") + #expect(ClusterHelper.fqdn(for: "dev", domain: "example.com") == "dev.example.com") } } diff --git a/Tests/ClusterPluginTests/ClusterLoadImageTests.swift b/Tests/ClusterPluginTests/ClusterLoadImageTests.swift new file mode 100644 index 00000000..d4f81bb4 --- /dev/null +++ b/Tests/ClusterPluginTests/ClusterLoadImageTests.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// 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 ContainerizationError +import Foundation +import Testing + +@testable import ContainerCluster + +// MARK: - ClusterLoadImage flag parsing + +@Suite("ClusterLoadImage --node flag") +struct ClusterLoadImageNodeFlagTests { + @Test func nodeDefaultsToEmptyWhenNotProvided() throws { + let command = try ClusterLoadImage.parse(["my-app:latest"]) + #expect(command.node.isEmpty) + } + + @Test func nodeCapturesProvidedValue() throws { + let command = try ClusterLoadImage.parse(["--node", "cluster-dev-worker-1", "my-app:latest"]) + #expect(command.node == ["cluster-dev-worker-1"]) + } + + @Test func nodeCapturesMultipleProvidedValues() throws { + let command = try ClusterLoadImage.parse([ + "--node", "cluster-dev-worker-1", "--node", "cluster-dev-worker-2", "my-app:latest", + ]) + #expect(command.node == ["cluster-dev-worker-1", "cluster-dev-worker-2"]) + } +} + +// MARK: - ClusterLoadImage.resolveTargets + +@Suite("ClusterLoadImage.resolveTargets") +struct ResolveTargetsTests { + @Test func noNodesReturnsControlPlaneAndAllWorkers() throws { + let targets = try ClusterLoadImage.resolveTargets( + clusterName: "cluster-dev", nodes: [], workers: ["cluster-dev-worker-1", "cluster-dev-worker-2"]) + #expect(targets == ["cluster-dev", "cluster-dev-worker-1", "cluster-dev-worker-2"]) + } + + @Test func noNodesWithNoWorkersReturnsOnlyControlPlane() throws { + let targets = try ClusterLoadImage.resolveTargets(clusterName: "cluster-dev", nodes: [], workers: []) + #expect(targets == ["cluster-dev"]) + } + + @Test func nodeMatchingControlPlaneReturnsJustControlPlane() throws { + let targets = try ClusterLoadImage.resolveTargets( + clusterName: "cluster-dev", nodes: ["cluster-dev"], workers: ["cluster-dev-worker-1"]) + #expect(targets == ["cluster-dev"]) + } + + @Test func nodeMatchingWorkerReturnsJustThatWorker() throws { + let targets = try ClusterLoadImage.resolveTargets( + clusterName: "cluster-dev", nodes: ["cluster-dev-worker-2"], workers: ["cluster-dev-worker-1", "cluster-dev-worker-2"]) + #expect(targets == ["cluster-dev-worker-2"]) + } + + @Test func multipleNodesReturnsEachInOrder() throws { + let targets = try ClusterLoadImage.resolveTargets( + clusterName: "cluster-dev", nodes: ["cluster-dev-worker-2", "cluster-dev"], + workers: ["cluster-dev-worker-1", "cluster-dev-worker-2"]) + #expect(targets == ["cluster-dev-worker-2", "cluster-dev"]) + } + + @Test func duplicateNodesAreDeduplicated() throws { + let targets = try ClusterLoadImage.resolveTargets( + clusterName: "cluster-dev", nodes: ["cluster-dev-worker-1", "cluster-dev-worker-1"], + workers: ["cluster-dev-worker-1"]) + #expect(targets == ["cluster-dev-worker-1"]) + } + + @Test func unknownNodeThrowsInvalidArgument() throws { + #expect(throws: ContainerizationError.self) { + try ClusterLoadImage.resolveTargets( + clusterName: "cluster-dev", nodes: ["not-a-node"], workers: ["cluster-dev-worker-1"]) + } + } +} diff --git a/Tests/K8sPluginTests/K8sNodeImageVersionTests.swift b/Tests/ClusterPluginTests/ClusterNodeImageVersionTests.swift similarity index 73% rename from Tests/K8sPluginTests/K8sNodeImageVersionTests.swift rename to Tests/ClusterPluginTests/ClusterNodeImageVersionTests.swift index a0cb79b5..cf4a711d 100644 --- a/Tests/K8sPluginTests/K8sNodeImageVersionTests.swift +++ b/Tests/ClusterPluginTests/ClusterNodeImageVersionTests.swift @@ -16,29 +16,29 @@ import Testing -@testable import ContainerK8s +@testable import ContainerCluster -struct K8sNodeImageVersionTests { +struct ClusterNodeImageVersionTests { @Test func versionComesFromTheGivenImage() throws { - #expect(try K8sHelper.kubernetesVersion(nodeImage: "docker.io/kindest/node:v1.34.11") == "v1.34.11") + #expect(try ClusterHelper.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") + #expect(try ClusterHelper.kubernetesVersion(nodeImage: image) == "v1.34.11") } @Test func defaultImageStillResolves() throws { - #expect(try K8sHelper.kubernetesVersion(nodeImage: K8sHelper.nodeImage) == "v1.35.5") + #expect(try ClusterHelper.kubernetesVersion(nodeImage: ClusterHelper.nodeImage) == "v1.35.5") } @Test func untaggedImageThrows() { #expect(throws: (any Error).self) { - try K8sHelper.kubernetesVersion(nodeImage: "docker.io/kindest/node") + try ClusterHelper.kubernetesVersion(nodeImage: "docker.io/kindest/node") } } @@ -46,7 +46,7 @@ struct K8sNodeImageVersionTests { func digestOnlyImageThrows() { let image = "docker.io/kindest/node@sha256:0000000000000000000000000000000000000000000000000000000000000000" #expect(throws: (any Error).self) { - try K8sHelper.kubernetesVersion(nodeImage: image) + try ClusterHelper.kubernetesVersion(nodeImage: image) } } } diff --git a/Tests/K8sPluginTests/KubeconfigMergeTests.swift b/Tests/ClusterPluginTests/KubeconfigMergeTests.swift similarity index 89% rename from Tests/K8sPluginTests/KubeconfigMergeTests.swift rename to Tests/ClusterPluginTests/KubeconfigMergeTests.swift index 66edaecf..49104a9e 100644 --- a/Tests/K8sPluginTests/KubeconfigMergeTests.swift +++ b/Tests/ClusterPluginTests/KubeconfigMergeTests.swift @@ -22,7 +22,7 @@ import SystemPackage import Testing import Yams -@testable import ContainerK8s +@testable import ContainerCluster // MARK: - Helpers @@ -208,7 +208,7 @@ struct KubeconfigRoundTripTests { // MARK: - transformConfig sanitization -@Suite("K8sHelper.transformConfig") +@Suite("ClusterHelper.transformConfig") struct TransformConfigTests { @Test func rejectsExecFromUntrustedKubeconfig() async throws { @@ -216,7 +216,7 @@ struct TransformConfigTests { config.users[0].authInfo.exec = ExecConfig(command: "/bin/sh", args: ["-c", "id"], apiVersion: "client.authentication.k8s.io/v1") await #expect(throws: (any Error).self) { - _ = try await K8sHelper.transformConfig(config, containerId: "victim", fqdn: "victim.local", client: ContainerClient()) + _ = try await ClusterHelper.transformConfig(config, containerId: "victim", fqdn: "victim.local", client: ContainerClient()) } } @@ -225,7 +225,7 @@ struct TransformConfigTests { config.users[0].authInfo.authProvider = AuthProviderConfig(name: "gcp") await #expect(throws: (any Error).self) { - _ = try await K8sHelper.transformConfig(config, containerId: "victim", fqdn: "victim.local", client: ContainerClient()) + _ = try await ClusterHelper.transformConfig(config, containerId: "victim", fqdn: "victim.local", client: ContainerClient()) } } @@ -236,7 +236,7 @@ struct TransformConfigTests { config.clusters[0].cluster.tlsServerName = "attacker.example.com" config.contexts[0].context.namespace = "default" - let result = try await K8sHelper.transformConfig(config, containerId: "victim", fqdn: "victim.local", client: ContainerClient()) + let result = try await ClusterHelper.transformConfig(config, containerId: "victim", fqdn: "victim.local", client: ContainerClient()) #expect(result.clusters.count == 1) #expect(result.clusters[0].name == "victim") @@ -266,21 +266,21 @@ struct TransformConfigTests { config.clusters.append(NamedCluster(name: "b", cluster: Cluster(server: "https://5.6.7.8"))) await #expect(throws: (any Error).self) { - _ = try await K8sHelper.transformConfig(config, containerId: "victim", fqdn: "victim.local", client: ContainerClient()) + _ = try await ClusterHelper.transformConfig(config, containerId: "victim", fqdn: "victim.local", client: ContainerClient()) } } } // MARK: - mergeConfig behavior -@Suite("K8sHelper.mergeConfig") +@Suite("ClusterHelper.mergeConfig") struct MergeConfigTests { @Test func mergeIntoEmptyFileCreatesFile() throws { let (path, cleanup) = try makeTempFile() defer { cleanup() } - try K8sHelper.mergeConfig(makeConfig(clusterName: "dev"), containerId: "dev", targetPath: path, setCurrentContext: true, log: log) + try ClusterHelper.mergeConfig(makeConfig(clusterName: "dev"), containerId: "dev", targetPath: path, setCurrentContext: true, log: log) let written = try decode(String(contentsOfFile: path.string, encoding: .utf8)) #expect(written.clusters.count == 1) @@ -319,7 +319,7 @@ struct MergeConfigTests { """ try existingYAML.write(toFile: path.string, atomically: true, encoding: .utf8) - try K8sHelper.mergeConfig(makeConfig(clusterName: "dev"), containerId: "dev", targetPath: path, log: log) + try ClusterHelper.mergeConfig(makeConfig(clusterName: "dev"), containerId: "dev", targetPath: path, log: log) let result = try decode(String(contentsOfFile: path.string, encoding: .utf8)) @@ -365,7 +365,7 @@ struct MergeConfigTests { """ try existingYAML.write(toFile: path.string, atomically: true, encoding: .utf8) - try K8sHelper.mergeConfig(makeConfig(clusterName: "dev"), containerId: "dev", targetPath: path, log: log) + try ClusterHelper.mergeConfig(makeConfig(clusterName: "dev"), containerId: "dev", targetPath: path, log: log) let result = try decode(String(contentsOfFile: path.string, encoding: .utf8)) let stagingUser = try #require(result.users.first { $0.name == "staging-user" }) @@ -376,8 +376,8 @@ struct MergeConfigTests { let (path, cleanup) = try makeTempFile() defer { cleanup() } - try K8sHelper.mergeConfig(makeConfig(clusterName: "dev", server: "https://127.0.0.1:6445"), containerId: "dev", targetPath: path, log: log) - try K8sHelper.mergeConfig(makeConfig(clusterName: "dev", server: "https://127.0.0.1:6446"), containerId: "dev", targetPath: path, log: log) + try ClusterHelper.mergeConfig(makeConfig(clusterName: "dev", server: "https://127.0.0.1:6445"), containerId: "dev", targetPath: path, log: log) + try ClusterHelper.mergeConfig(makeConfig(clusterName: "dev", server: "https://127.0.0.1:6446"), containerId: "dev", targetPath: path, log: log) let result = try decode(String(contentsOfFile: path.string, encoding: .utf8)) #expect(result.clusters.filter { $0.name == "dev" }.count == 1) @@ -393,7 +393,7 @@ struct MergeConfigTests { try "this: is: not: valid: yaml: [[[".write(toFile: path.string, atomically: true, encoding: .utf8) #expect(throws: (any Error).self) { - try K8sHelper.mergeConfig(makeConfig(clusterName: "dev"), containerId: "dev", targetPath: path, log: log) + try ClusterHelper.mergeConfig(makeConfig(clusterName: "dev"), containerId: "dev", targetPath: path, log: log) } } @@ -402,9 +402,9 @@ struct MergeConfigTests { defer { cleanup() } // First create: sets current-context - try K8sHelper.mergeConfig(makeConfig(clusterName: "first"), containerId: "first", targetPath: path, setCurrentContext: true, log: log) + try ClusterHelper.mergeConfig(makeConfig(clusterName: "first"), containerId: "first", targetPath: path, setCurrentContext: true, log: log) // 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) + try ClusterHelper.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 == "second") // always switches to the most recently created cluster @@ -416,12 +416,12 @@ struct MergeConfigTests { // Both suites mutate the KUBECONFIG environment variable; wrap them in a // common .serialized parent so they cannot race against each other. -@Suite("K8sHelper env-dependent", .serialized) +@Suite("ClusterHelper env-dependent", .serialized) struct KubeconfigEnvTests { // MARK: - resolveKubeconfigMergePath - @Suite("K8sHelper.resolveKubeconfigMergePath") + @Suite("ClusterHelper.resolveKubeconfigMergePath") struct ResolveKubeconfigMergePathTests { private func withKubeconfigEnv(_ value: String?, _ body: () -> Void) { @@ -442,14 +442,14 @@ struct KubeconfigEnvTests { @Test func noEnvDefaultsToHomeKubeConfig() { withKubeconfigEnv(nil) { - let path = K8sHelper.resolveKubeconfigMergePath() + let path = ClusterHelper.resolveKubeconfigMergePath() #expect(path.string.hasSuffix(".kube/config")) } } @Test func singleEnvPathUsedDirectly() { withKubeconfigEnv("/tmp/my-kubeconfig") { - let path = K8sHelper.resolveKubeconfigMergePath() + let path = ClusterHelper.resolveKubeconfigMergePath() #expect(path.string == "/tmp/my-kubeconfig") } } @@ -461,14 +461,14 @@ struct KubeconfigEnvTests { defer { try? FileManager.default.removeItem(atPath: existing.string) } withKubeconfigEnv("/tmp/does-not-exist-a:\(existing.string):/tmp/does-not-exist-b") { - let path = K8sHelper.resolveKubeconfigMergePath() + let path = ClusterHelper.resolveKubeconfigMergePath() #expect(path.string == existing.string) } } @Test func multiplePathsNoneExistUsesLast() { withKubeconfigEnv("/tmp/no-exist-a:/tmp/no-exist-b:/tmp/no-exist-c") { - let path = K8sHelper.resolveKubeconfigMergePath() + let path = ClusterHelper.resolveKubeconfigMergePath() #expect(path.string == "/tmp/no-exist-c") } } @@ -476,7 +476,7 @@ struct KubeconfigEnvTests { // MARK: - removeConfig behavior - @Suite("K8sHelper.removeConfig") + @Suite("ClusterHelper.removeConfig") struct RemoveConfigTests { private func withKubeconfig(_ initial: KubeConfig, _ body: () throws -> Void) throws { @@ -493,7 +493,7 @@ struct KubeconfigEnvTests { } private func currentKubeconfig() throws -> KubeConfig { - let path = K8sHelper.resolveKubeconfigMergePath() + let path = ClusterHelper.resolveKubeconfigMergePath() return try decode(String(contentsOfFile: path.string, encoding: .utf8)) } @@ -506,7 +506,7 @@ struct KubeconfigEnvTests { initial.currentContext = "other" try withKubeconfig(initial) { - try K8sHelper.removeConfig(containerId: "dev", log: log) + try ClusterHelper.removeConfig(containerId: "dev", log: log) let result = try currentKubeconfig() #expect(result.clusters.count == 1) #expect(!result.clusters.contains { $0.name == "dev" }) @@ -520,7 +520,7 @@ struct KubeconfigEnvTests { initial.currentContext = "dev" try withKubeconfig(initial) { - try K8sHelper.removeConfig(containerId: "dev", log: log) + try ClusterHelper.removeConfig(containerId: "dev", log: log) #expect(try currentKubeconfig().currentContext == nil) } } @@ -534,7 +534,7 @@ struct KubeconfigEnvTests { initial.currentContext = "other" try withKubeconfig(initial) { - try K8sHelper.removeConfig(containerId: "dev", log: log) + try ClusterHelper.removeConfig(containerId: "dev", log: log) #expect(try currentKubeconfig().currentContext == "other") } } @@ -547,7 +547,7 @@ struct KubeconfigEnvTests { defer { if let original { setenv("KUBECONFIG", original, 1) } else { unsetenv("KUBECONFIG") } } - try K8sHelper.removeConfig(containerId: "dev", log: log) + try ClusterHelper.removeConfig(containerId: "dev", log: log) } } } diff --git a/Tests/IntegrationTests/K8s/K8sDiagnostics.swift b/Tests/IntegrationTests/Cluster/ClusterDiagnostics.swift similarity index 100% rename from Tests/IntegrationTests/K8s/K8sDiagnostics.swift rename to Tests/IntegrationTests/Cluster/ClusterDiagnostics.swift diff --git a/Tests/IntegrationTests/K8s/ContainerFixture+K8sHelpers.swift b/Tests/IntegrationTests/Cluster/ContainerFixture+ClusterHelpers.swift similarity index 81% rename from Tests/IntegrationTests/K8s/ContainerFixture+K8sHelpers.swift rename to Tests/IntegrationTests/Cluster/ContainerFixture+ClusterHelpers.swift index 35ce88e7..c85b3d89 100644 --- a/Tests/IntegrationTests/K8s/ContainerFixture+K8sHelpers.swift +++ b/Tests/IntegrationTests/Cluster/ContainerFixture+ClusterHelpers.swift @@ -20,14 +20,14 @@ import Foundation extension ContainerFixture { @discardableResult func kubectl(node: String, args: [String]) throws -> (output: String, status: Int32) { - print("[k8s] kubectl \(args.joined(separator: " ")) (node: \(node))") + print("[cluster] kubectl \(args.joined(separator: " ")) (node: \(node))") let result = try self.run(["exec", node, "kubectl"] + args) - print("[k8s] kubectl exit=\(result.status) output=\(result.output.prefix(120).trimmingCharacters(in: .whitespacesAndNewlines))") + print("[cluster] kubectl exit=\(result.status) output=\(result.output.prefix(120).trimmingCharacters(in: .whitespacesAndNewlines))") let filteredStderr = result.error.components(separatedBy: "\n") .filter { !$0.contains("Warning! Running debug build") && !$0.isEmpty } .joined(separator: "\n") if !filteredStderr.isEmpty { - print("[k8s] kubectl stderr: \(filteredStderr.prefix(300))") + print("[cluster] kubectl stderr: \(filteredStderr.prefix(300))") } return (result.output, result.status) } diff --git a/Tests/IntegrationTests/K8s/TestK8sCNISerial.swift b/Tests/IntegrationTests/Cluster/TestClusterCNISerial.swift similarity index 79% rename from Tests/IntegrationTests/K8s/TestK8sCNISerial.swift rename to Tests/IntegrationTests/Cluster/TestClusterCNISerial.swift index e74e2629..cf43c6a8 100644 --- a/Tests/IntegrationTests/K8s/TestK8sCNISerial.swift +++ b/Tests/IntegrationTests/Cluster/TestClusterCNISerial.swift @@ -19,19 +19,19 @@ import Foundation import Testing @Suite(.serialized) -struct TestK8sCNISerial { +struct TestClusterCNISerial { @Test func testCreateWithCNINoneSkipsCNIInstallation() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + let name = "cluster-\(f.testID)" + f.addCleanup { _ = try? f.run(["cluster", "delete", "--name", name]) } try f.restoreWarmupImage(.kindestNodeV1_35_5) - print("[k8s-cni] k8s create --name \(name) --cni NONE") - let result = try f.run(["k8s", "create", "--name", name, "--cni", "NONE"]) - print("[k8s-cni] k8s create exit=\(result.status)") + print("[cluster-cni] cluster create --name \(name) --cni NONE") + let result = try f.run(["cluster", "create", "--name", name, "--cni", "NONE"]) + print("[cluster-cni] cluster create exit=\(result.status)") if result.status != 0 { - print("[k8s-cni] k8s create stderr: \(result.error)") + print("[cluster-cni] cluster create stderr: \(result.error)") f.dumpNodeDiagnostics(node: name) } diff --git a/Tests/IntegrationTests/K8s/TestK8sCreateCleanupSerial.swift b/Tests/IntegrationTests/Cluster/TestClusterCreateCleanupSerial.swift similarity index 78% rename from Tests/IntegrationTests/K8s/TestK8sCreateCleanupSerial.swift rename to Tests/IntegrationTests/Cluster/TestClusterCreateCleanupSerial.swift index ec385fdf..471eebaa 100644 --- a/Tests/IntegrationTests/K8s/TestK8sCreateCleanupSerial.swift +++ b/Tests/IntegrationTests/Cluster/TestClusterCreateCleanupSerial.swift @@ -19,13 +19,13 @@ import Foundation import Testing @Suite(.serialized) -struct TestK8sCreateCleanupSerial { +struct TestClusterCreateCleanupSerial { /// If a container occupying the control-plane name already exists and isn't owned by - /// the k8s plugin, `k8s create` must fail (name collision) without deleting it. + /// the cluster plugin, `cluster create` must fail (name collision) without deleting it. @Test func testFailedCreateDoesNotTeardownUnrelatedControlPlaneNameContainer() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" + let name = "cluster-\(f.testID)" try await f.doLongRun(name: name, autoRemove: false, waitUntilRunning: true) f.addCleanup { @@ -35,8 +35,8 @@ struct TestK8sCreateCleanupSerial { let preExistingId = try f.getContainerId(name) try f.restoreWarmupImage(.kindestNodeV1_35_5) - let result = try f.run(["k8s", "create", "--name", name]) - print("[k8s-create-cleanup] k8s create stderr: \(result.error)") + let result = try f.run(["cluster", "create", "--name", name]) + print("[cluster-create-cleanup] cluster create stderr: \(result.error)") #expect(result.status != 0) // The pre-existing, unrelated container must survive untouched. @@ -45,13 +45,13 @@ struct TestK8sCreateCleanupSerial { } /// If a container occupying a would-be worker name already exists and isn't owned by - /// the k8s plugin, the control-plane node that *was* legitimately created should be + /// the cluster plugin, the control-plane node that *was* legitimately created should be /// torn down on failure, but the unrelated worker-named container must be left alone. @Test func testFailedCreateDoesNotTeardownUnrelatedWorkerNameContainer() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" + let name = "cluster-\(f.testID)" let workerName = "\(name)-worker-1" - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + f.addCleanup { _ = try? f.run(["cluster", "delete", "--name", name]) } try await f.doLongRun(name: workerName, autoRemove: false, waitUntilRunning: true) f.addCleanup { @@ -61,8 +61,8 @@ struct TestK8sCreateCleanupSerial { let preExistingId = try f.getContainerId(workerName) try f.restoreWarmupImage(.kindestNodeV1_35_5) - let result = try f.run(["k8s", "create", "--name", name, "--workers", "1"]) - print("[k8s-create-cleanup] k8s create stderr: \(result.error)") + let result = try f.run(["cluster", "create", "--name", name, "--workers", "1"]) + print("[cluster-create-cleanup] cluster create stderr: \(result.error)") #expect(result.status != 0) // The unrelated worker-named container must survive untouched. diff --git a/Tests/IntegrationTests/K8s/TestK8sLoadImageSerial.swift b/Tests/IntegrationTests/Cluster/TestClusterLoadImageSerial.swift similarity index 71% rename from Tests/IntegrationTests/K8s/TestK8sLoadImageSerial.swift rename to Tests/IntegrationTests/Cluster/TestClusterLoadImageSerial.swift index 8773b2d5..1cb1b9e9 100644 --- a/Tests/IntegrationTests/K8s/TestK8sLoadImageSerial.swift +++ b/Tests/IntegrationTests/Cluster/TestClusterLoadImageSerial.swift @@ -19,14 +19,14 @@ import Foundation import Testing @Suite(.serialized) -struct TestK8sLoadImageSerial { +struct TestClusterLoadImageSerial { private static let testImage = WarmupImage.alpine320.rawValue private func imageExistsInNode(_ f: ContainerFixture, node: String, image: String) throws -> Bool { - print("[k8s-load] ctr images list (node: \(node), checking: \(image))") + print("[cluster-load] ctr images list (node: \(node), checking: \(image))") let result = try f.run(["exec", node, "ctr", "--namespace", "k8s.io", "images", "list"]) - print("[k8s-load] ctr images list exit=\(result.status) found=\(result.output.contains(image))") + print("[cluster-load] ctr images list exit=\(result.status) found=\(result.output.contains(image))") guard result.status == 0 else { return false } return result.output.contains(image) } @@ -50,28 +50,28 @@ struct TestK8sLoadImageSerial { @Test func testLoadImageIntoSingleNode() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + let name = "cluster-\(f.testID)" + f.addCleanup { _ = try? f.run(["cluster", "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)") + print("[cluster-load] cluster create --name \(name)") + let result = try f.run(["cluster", "create", "--name", name]) + print("[cluster-load] cluster create exit=\(result.status)") if result.status != 0 { - print("[k8s-load] k8s create stderr: \(result.error)") + print("[cluster-load] cluster create stderr: \(result.error)") dumpEnv(f, clusterName: name) f.dumpNodeDiagnostics(node: name) } try result.check() - print("[k8s-load] pulling \(Self.testImage)") + print("[cluster-load] pulling \(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]) - print("[k8s-load] k8s load-image exit=\(loadResult.status)") + print("[cluster-load] cluster load-image --name \(name) \(Self.testImage)") + let loadResult = try f.run(["cluster", "load-image", "--name", name, Self.testImage]) + print("[cluster-load] cluster load-image exit=\(loadResult.status)") if loadResult.status != 0 { - print("[k8s-load] load-image stderr: \(loadResult.error)") + print("[cluster-load] load-image stderr: \(loadResult.error)") } #expect(loadResult.status == 0) @@ -81,7 +81,7 @@ struct TestK8sLoadImageSerial { @Test func testLoadImageReferenceForms() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" + let name = "cluster-\(f.testID)" let sourceImage = Self.testImage let images = [ "img-\(f.testID)", @@ -90,18 +90,18 @@ struct TestK8sLoadImageSerial { "docker.io/hub-\(f.testID)", "ghcr.io/org/img-\(f.testID):v1", ] - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + f.addCleanup { _ = try? f.run(["cluster", "delete", "--name", name]) } f.addCleanup { try? f.doRemoveImages(images) } try f.restoreWarmupImage(.kindestNodeV1_35_5) - try f.run(["k8s", "create", "--name", name]).check() + try f.run(["cluster", "create", "--name", name]).check() try f.restoreWarmupImage(.alpine320) for image in images { try f.doImageTag(sourceImage, newName: image) let fullImage = try f.inspectImage(image) - try f.run(["k8s", "load-image", "--name", name, image]).check() + try f.run(["cluster", "load-image", "--name", name, image]).check() #expect(try imageExistsInNode(f, node: name, image: fullImage)) } diff --git a/Tests/IntegrationTests/K8s/TestK8sMultiNodeSerial.swift b/Tests/IntegrationTests/Cluster/TestClusterMultiNodeSerial.swift similarity index 79% rename from Tests/IntegrationTests/K8s/TestK8sMultiNodeSerial.swift rename to Tests/IntegrationTests/Cluster/TestClusterMultiNodeSerial.swift index 6f135e2b..6b8a1a77 100644 --- a/Tests/IntegrationTests/K8s/TestK8sMultiNodeSerial.swift +++ b/Tests/IntegrationTests/Cluster/TestClusterMultiNodeSerial.swift @@ -19,11 +19,11 @@ import Foundation import Testing @Suite(.serialized) -struct TestK8sMultiNodeSerial { +struct TestClusterMultiNodeSerial { @discardableResult private func kubectl(_ f: ContainerFixture, node: String, args: [String]) throws -> (output: String, status: Int32) { - print("[k8s-multi] kubectl \(args.joined(separator: " ")) (node: \(node))") + print("[cluster-multi] kubectl \(args.joined(separator: " ")) (node: \(node))") let result = try f.run(["exec", node, "kubectl"] + args) return (result.output, result.status) } @@ -40,14 +40,14 @@ struct TestK8sMultiNodeSerial { @Test func testCreateWithWorkersRegistersAllNodes() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" + let name = "cluster-\(f.testID)" let workerNames = [1, 2].map { "\(name)-worker-\($0)" } - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + f.addCleanup { _ = try? f.run(["cluster", "delete", "--name", name]) } try f.restoreWarmupImage(.kindestNodeV1_35_5) - let result = try f.run(["k8s", "create", "--name", name, "--workers", "2"]) + let result = try f.run(["cluster", "create", "--name", name, "--workers", "2"]) if result.status != 0 { - print("[k8s-multi] k8s create stderr: \(result.error)") + print("[cluster-multi] cluster create stderr: \(result.error)") f.dumpNodeDiagnostics(node: name) } try result.check() @@ -77,8 +77,8 @@ struct TestK8sMultiNodeSerial { #expect(workerRow?.status == "Ready") } - // container k8s list should surface every node under the same cluster. - let listResult = try f.run(["k8s", "list"]) + // container cluster list should surface every node under the same cluster. + let listResult = try f.run(["cluster", "list"]) #expect(listResult.status == 0) #expect(listResult.output.contains(name)) for workerName in workerNames { @@ -89,14 +89,14 @@ struct TestK8sMultiNodeSerial { @Test func testDeleteRemovesAllWorkerContainers() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" + let name = "cluster-\(f.testID)" let workerNames = [1, 2].map { "\(name)-worker-\($0)" } - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + f.addCleanup { _ = try? f.run(["cluster", "delete", "--name", name]) } try f.restoreWarmupImage(.kindestNodeV1_35_5) - let createResult = try f.run(["k8s", "create", "--name", name, "--workers", "2"]) + let createResult = try f.run(["cluster", "create", "--name", name, "--workers", "2"]) if createResult.status != 0 { - print("[k8s-multi] k8s create stderr: \(createResult.error)") + print("[cluster-multi] cluster create stderr: \(createResult.error)") f.dumpNodeDiagnostics(node: name) } try createResult.check() @@ -105,17 +105,17 @@ struct TestK8sMultiNodeSerial { #expect(try f.getContainerStatus(workerName) == "running") } - let deleteResult = try f.run(["k8s", "delete", "--name", name]) + let deleteResult = try f.run(["cluster", "delete", "--name", name]) try deleteResult.check() // The control plane and every worker container should be gone, not just the - // control plane — this is exactly what `K8sDelete`'s worker enumeration covers. + // control plane — this is exactly what `ClusterDelete`'s worker enumeration covers. #expect(throws: (any Error).self) { try f.getContainerStatus(name) } for workerName in workerNames { #expect(throws: (any Error).self) { try f.getContainerStatus(workerName) } } - let listResult = try f.run(["k8s", "list"]) + let listResult = try f.run(["cluster", "list"]) #expect(!listResult.output.contains(name)) } } diff --git a/Tests/IntegrationTests/K8s/TestK8sNetworkingSerial.swift b/Tests/IntegrationTests/Cluster/TestClusterNetworkingSerial.swift similarity index 71% rename from Tests/IntegrationTests/K8s/TestK8sNetworkingSerial.swift rename to Tests/IntegrationTests/Cluster/TestClusterNetworkingSerial.swift index 34c17909..b1a7d7e4 100644 --- a/Tests/IntegrationTests/K8s/TestK8sNetworkingSerial.swift +++ b/Tests/IntegrationTests/Cluster/TestClusterNetworkingSerial.swift @@ -19,7 +19,7 @@ import Foundation import Testing @Suite(.serialized) -struct TestK8sNetworkingSerial { +struct TestClusterNetworkingSerial { private static let testImage = WarmupImage.alpine320 @@ -38,7 +38,7 @@ struct TestK8sNetworkingSerial { } private func waitForPod(_ f: ContainerFixture, node: String, podName: String, timeoutSeconds: Int) throws { - print("[k8s-net] waitForPod \(podName) on \(node) (timeout=\(timeoutSeconds)s)") + print("[cluster-net] waitForPod \(podName) on \(node) (timeout=\(timeoutSeconds)s)") let (_, status) = try f.kubectl( node: node, args: [ @@ -46,38 +46,38 @@ struct TestK8sNetworkingSerial { ]) guard status == 0 else { let (podStatus, _) = try f.kubectl(node: node, args: ["get", "pod", podName, "--no-headers"]) - print("[k8s-net] pod \(podName) status: \(podStatus.trimmingCharacters(in: .whitespacesAndNewlines))") + print("[cluster-net] pod \(podName) status: \(podStatus.trimmingCharacters(in: .whitespacesAndNewlines))") let (podDesc, _) = try f.kubectl(node: node, args: ["describe", "pod", podName]) - print("[k8s-net] pod \(podName) describe:\n\(podDesc.prefix(1000))") + print("[cluster-net] pod \(podName) describe:\n\(podDesc.prefix(1000))") throw CommandError.executionFailed("pod \(podName) did not become ready within \(timeoutSeconds)s") } - print("[k8s-net] pod \(podName) is Ready") + print("[cluster-net] pod \(podName) is Ready") } // Verify that a pod schedules, reaches Running, and can be exec'd into. @Test func testPodsScheduleAndRun() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + let name = "cluster-\(f.testID)" + f.addCleanup { _ = try? f.run(["cluster", "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)") + print("[cluster-net] cluster create --name \(name)") + let result = try f.run(["cluster", "create", "--name", name]) + print("[cluster-net] cluster create exit=\(result.status)") if result.status != 0 { - print("[k8s-net] k8s create stderr: \(result.error)") + print("[cluster-net] cluster create stderr: \(result.error)") dumpEnv(f, clusterName: name) f.dumpNodeDiagnostics(node: name) } try result.check() - print("[k8s-net] pulling \(Self.testImage.rawValue)") + print("[cluster-net] pulling \(Self.testImage.rawValue)") try f.restoreWarmupImage(Self.testImage) - print("[k8s-net] k8s load-image --name \(name) \(Self.testImage.rawValue)") - let loadResult = try f.run(["k8s", "load-image", "--name", name, Self.testImage.rawValue]) - print("[k8s-net] k8s load-image exit=\(loadResult.status)") - if loadResult.status != 0 { print("[k8s-net] k8s load-image stderr: \(loadResult.error)") } + print("[cluster-net] cluster load-image --name \(name) \(Self.testImage.rawValue)") + let loadResult = try f.run(["cluster", "load-image", "--name", name, Self.testImage.rawValue]) + print("[cluster-net] cluster load-image exit=\(loadResult.status)") + if loadResult.status != 0 { print("[cluster-net] cluster load-image stderr: \(loadResult.error)") } #expect(loadResult.status == 0) let (_, createStatus) = try f.kubectl( @@ -106,26 +106,26 @@ struct TestK8sNetworkingSerial { // Verify pod-to-service communication and CoreDNS resolution on a single node. @Test func testPodToServiceCommunication() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + let name = "cluster-\(f.testID)" + f.addCleanup { _ = try? f.run(["cluster", "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)") + print("[cluster-net] cluster create --name \(name)") + let result = try f.run(["cluster", "create", "--name", name]) + print("[cluster-net] cluster create exit=\(result.status)") if result.status != 0 { - print("[k8s-net] k8s create stderr: \(result.error)") + print("[cluster-net] cluster create stderr: \(result.error)") dumpEnv(f, clusterName: name) } try result.check() - print("[k8s-net] pulling \(Self.testImage.rawValue)") + print("[cluster-net] pulling \(Self.testImage.rawValue)") try f.restoreWarmupImage(Self.testImage) - print("[k8s-net] k8s load-image --name \(name) \(Self.testImage.rawValue)") - let loadResult = try f.run(["k8s", "load-image", "--name", name, Self.testImage.rawValue]) - print("[k8s-net] k8s load-image exit=\(loadResult.status)") - if loadResult.status != 0 { print("[k8s-net] k8s load-image stderr: \(loadResult.error)") } + print("[cluster-net] cluster load-image --name \(name) \(Self.testImage.rawValue)") + let loadResult = try f.run(["cluster", "load-image", "--name", name, Self.testImage.rawValue]) + print("[cluster-net] cluster load-image exit=\(loadResult.status)") + if loadResult.status != 0 { print("[cluster-net] cluster load-image stderr: \(loadResult.error)") } #expect(loadResult.status == 0) // Server: alpine busybox httpd serving a static response. @@ -165,14 +165,14 @@ struct TestK8sNetworkingSerial { try waitForPod(f, node: name, podName: "client", timeoutSeconds: 300) // Reach server via the service DNS name — exercises CoreDNS + kube-proxy. - print("[k8s-net] wget from client to server-svc:8080") + print("[cluster-net] wget from client to server-svc:8080") let (response, wgetStatus) = try f.kubectl( node: name, args: [ "exec", "client", "--", "sh", "-c", "sleep 2 && wget -qO- http://server-svc:8080", ]) - print("[k8s-net] wget exit=\(wgetStatus) response=\(response.trimmingCharacters(in: .whitespacesAndNewlines))") + print("[cluster-net] wget exit=\(wgetStatus) response=\(response.trimmingCharacters(in: .whitespacesAndNewlines))") #expect(wgetStatus == 0) #expect(response.contains("ok")) } diff --git a/Tests/IntegrationTests/K8s/TestK8sRunSerial.swift b/Tests/IntegrationTests/Cluster/TestClusterRunSerial.swift similarity index 72% rename from Tests/IntegrationTests/K8s/TestK8sRunSerial.swift rename to Tests/IntegrationTests/Cluster/TestClusterRunSerial.swift index 04f17e02..755a299a 100644 --- a/Tests/IntegrationTests/K8s/TestK8sRunSerial.swift +++ b/Tests/IntegrationTests/Cluster/TestClusterRunSerial.swift @@ -21,7 +21,7 @@ import Testing import Yams @Suite(.serialized) -struct TestK8sRunSerial { +struct TestClusterRunSerial { private func loadKubeconfig() throws -> [String: Any] { let path = FilePath(FileManager.default.homeDirectoryForCurrentUser.path) @@ -46,22 +46,22 @@ struct TestK8sRunSerial { @Test func testRunSingleNode() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + let name = "cluster-\(f.testID)" + f.addCleanup { _ = try? f.run(["cluster", "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)") + print("[cluster-run] cluster create --name \(name)") + let result = try f.run(["cluster", "create", "--name", name]) + print("[cluster-run] cluster create exit=\(result.status)") if result.status != 0 { - print("[k8s-run] k8s create stderr: \(result.error)") + print("[cluster-run] cluster create stderr: \(result.error)") f.dumpNodeDiagnostics(node: name) } try result.check() #expect(result.output.contains(name)) let containerStatus = try f.getContainerStatus(name) - print("[k8s-run] container status=\(containerStatus)") + print("[cluster-run] container status=\(containerStatus)") #expect(containerStatus == "running") let kubeconfig = try loadKubeconfig() @@ -79,26 +79,26 @@ struct TestK8sRunSerial { @Test func testConcurrentCreateGetsDifferentServerAddresses() async throws { try await ContainerFixture.with { f in - let name1 = "k8s-\(f.testID)-a" - let name2 = "k8s-\(f.testID)-b" - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name1]) } - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name2]) } + let name1 = "cluster-\(f.testID)-a" + let name2 = "cluster-\(f.testID)-b" + f.addCleanup { _ = try? f.run(["cluster", "delete", "--name", name1]) } + f.addCleanup { _ = try? f.run(["cluster", "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)") + print("[cluster-run] cluster create --name \(name1)") + let result1 = try f.run(["cluster", "create", "--name", name1]) + print("[cluster-run] cluster create exit=\(result1.status)") if result1.status != 0 { - print("[k8s-run] k8s create stderr: \(result1.error)") + print("[cluster-run] cluster create stderr: \(result1.error)") f.dumpNodeDiagnostics(node: name1) } #expect(result1.status == 0) - print("[k8s-run] k8s create --name \(name2)") - let result2 = try f.run(["k8s", "create", "--name", name2]) - print("[k8s-run] k8s create exit=\(result2.status)") + print("[cluster-run] cluster create --name \(name2)") + let result2 = try f.run(["cluster", "create", "--name", name2]) + print("[cluster-run] cluster create exit=\(result2.status)") if result2.status != 0 { - print("[k8s-run] k8s create stderr: \(result2.error)") + print("[cluster-run] cluster create stderr: \(result2.error)") f.dumpNodeDiagnostics(node: name2) } #expect(result2.status == 0) @@ -109,7 +109,7 @@ struct TestK8sRunSerial { let kubeconfig = try loadKubeconfig() let server1 = serverAddress(for: name1, in: kubeconfig) let server2 = serverAddress(for: name2, in: kubeconfig) - print("[k8s-run] server1=\(server1 ?? "nil") server2=\(server2 ?? "nil")") + print("[cluster-run] server1=\(server1 ?? "nil") server2=\(server2 ?? "nil")") #expect(server1 != nil) #expect(server2 != nil) #expect(server1 != server2) diff --git a/Tests/IntegrationTests/K8s/TestK8sWriteConfigSerial.swift b/Tests/IntegrationTests/Cluster/TestClusterWriteConfigSerial.swift similarity index 65% rename from Tests/IntegrationTests/K8s/TestK8sWriteConfigSerial.swift rename to Tests/IntegrationTests/Cluster/TestClusterWriteConfigSerial.swift index bb12becc..5c5881c6 100644 --- a/Tests/IntegrationTests/K8s/TestK8sWriteConfigSerial.swift +++ b/Tests/IntegrationTests/Cluster/TestClusterWriteConfigSerial.swift @@ -21,7 +21,7 @@ import Testing import Yams @Suite(.serialized) -struct TestK8sWriteConfigSerial { +struct TestClusterWriteConfigSerial { private func kubeconfigPath() -> FilePath { FilePath(FileManager.default.homeDirectoryForCurrentUser.path) @@ -40,17 +40,17 @@ struct TestK8sWriteConfigSerial { private func dumpKubeconfig(for name: String) { let path = kubeconfigPath() guard FileManager.default.fileExists(atPath: path.string) else { - print("[k8s-cfg] \(path.lastComponent?.string ?? "config") does not exist") + print("[cluster-cfg] \(path.lastComponent?.string ?? "config") does not exist") return } guard let kubeconfig = try? loadKubeconfig() else { - print("[k8s-cfg] \(path.lastComponent?.string ?? "config") could not be parsed") + print("[cluster-cfg] \(path.lastComponent?.string ?? "config") could not be parsed") return } let clusters = (kubeconfig["clusters"] as? [[String: Any]])?.compactMap { $0["name"] as? String } ?? [] let contexts = (kubeconfig["contexts"] as? [[String: Any]])?.compactMap { $0["name"] as? String } ?? [] let current = kubeconfig["current-context"] as? String ?? "" - print("[k8s-cfg] kubeconfig: clusters=\(clusters) contexts=\(contexts) current-context=\(current)") + print("[cluster-cfg] kubeconfig: clusters=\(clusters) contexts=\(contexts) current-context=\(current)") } private func dumpEnv(_ f: ContainerFixture, clusterName: String) { @@ -75,25 +75,25 @@ struct TestK8sWriteConfigSerial { @Test func testWriteConfigMergesContext() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + let name = "cluster-\(f.testID)" + f.addCleanup { _ = try? f.run(["cluster", "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)") + print("[cluster-cfg] cluster create --name \(name)") + let result = try f.run(["cluster", "create", "--name", name]) + print("[cluster-cfg] cluster create exit=\(result.status)") if result.status != 0 { - print("[k8s-cfg] k8s create stderr: \(result.error)") + print("[cluster-cfg] cluster create stderr: \(result.error)") dumpEnv(f, clusterName: name) f.dumpNodeDiagnostics(node: name) } try result.check() - print("[k8s-cfg] k8s write-config --name \(name)") - let writeResult = try f.run(["k8s", "write-config", "--name", name]) - print("[k8s-cfg] k8s write-config exit=\(writeResult.status)") + print("[cluster-cfg] cluster write-config --name \(name)") + let writeResult = try f.run(["cluster", "write-config", "--name", name]) + print("[cluster-cfg] cluster write-config exit=\(writeResult.status)") if writeResult.status != 0 { - print("[k8s-cfg] k8s write-config stderr: \(writeResult.error)") + print("[cluster-cfg] cluster write-config stderr: \(writeResult.error)") dumpEnv(f, clusterName: name) } #expect(writeResult.status == 0) @@ -113,25 +113,25 @@ struct TestK8sWriteConfigSerial { @Test func testWriteConfigIsIdempotent() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + let name = "cluster-\(f.testID)" + f.addCleanup { _ = try? f.run(["cluster", "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)") + print("[cluster-cfg] cluster create --name \(name)") + let result = try f.run(["cluster", "create", "--name", name]) + print("[cluster-cfg] cluster create exit=\(result.status)") if result.status != 0 { - print("[k8s-cfg] k8s create stderr: \(result.error)") + print("[cluster-cfg] cluster create stderr: \(result.error)") dumpEnv(f, clusterName: name) f.dumpNodeDiagnostics(node: name) } try result.check() - print("[k8s-cfg] k8s write-config --name \(name) (first)") - _ = try f.run(["k8s", "write-config", "--name", name]) - print("[k8s-cfg] k8s write-config --name \(name) (second)") - let secondResult = try f.run(["k8s", "write-config", "--name", name]) - print("[k8s-cfg] k8s write-config (second) exit=\(secondResult.status)") + print("[cluster-cfg] cluster write-config --name \(name) (first)") + _ = try f.run(["cluster", "write-config", "--name", name]) + print("[cluster-cfg] cluster write-config --name \(name) (second)") + let secondResult = try f.run(["cluster", "write-config", "--name", name]) + print("[cluster-cfg] cluster write-config (second) exit=\(secondResult.status)") #expect(secondResult.status == 0) let kubeconfig = try loadKubeconfig() @@ -143,27 +143,27 @@ struct TestK8sWriteConfigSerial { @Test func testDeleteClusterCleansUpKubeconfig() async throws { try await ContainerFixture.with { f in - let name = "k8s-\(f.testID)" - f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) } + let name = "cluster-\(f.testID)" + f.addCleanup { _ = try? f.run(["cluster", "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)") + print("[cluster-cfg] cluster create --name \(name)") + let result = try f.run(["cluster", "create", "--name", name]) + print("[cluster-cfg] cluster create exit=\(result.status)") if result.status != 0 { - print("[k8s-cfg] k8s create stderr: \(result.error)") + print("[cluster-cfg] cluster create stderr: \(result.error)") dumpEnv(f, clusterName: name) f.dumpNodeDiagnostics(node: name) } try result.check() - print("[k8s-cfg] k8s write-config --name \(name)") - _ = try f.run(["k8s", "write-config", "--name", name]) + print("[cluster-cfg] cluster write-config --name \(name)") + _ = try f.run(["cluster", "write-config", "--name", name]) #expect(kubeconfigContains(name: name)) - print("[k8s-cfg] k8s delete --name \(name)") - let deleteResult = try f.run(["k8s", "delete", "--name", name]) - print("[k8s-cfg] k8s delete exit=\(deleteResult.status) stderr=\(deleteResult.error)") + print("[cluster-cfg] cluster delete --name \(name)") + let deleteResult = try f.run(["cluster", "delete", "--name", name]) + print("[cluster-cfg] cluster delete exit=\(deleteResult.status) stderr=\(deleteResult.error)") #expect(deleteResult.status == 0) #expect(!kubeconfigContains(name: name)) diff --git a/Tests/K8sPluginTests/K8sLoadImageTests.swift b/Tests/K8sPluginTests/K8sLoadImageTests.swift deleted file mode 100644 index b214e486..00000000 --- a/Tests/K8sPluginTests/K8sLoadImageTests.swift +++ /dev/null @@ -1,92 +0,0 @@ -//===----------------------------------------------------------------------===// -// 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 ContainerizationError -import Foundation -import Testing - -@testable import ContainerK8s - -// MARK: - K8sLoadImage flag parsing - -@Suite("K8sLoadImage --node flag") -struct K8sLoadImageNodeFlagTests { - @Test func nodeDefaultsToEmptyWhenNotProvided() throws { - let command = try K8sLoadImage.parse(["my-app:latest"]) - #expect(command.node.isEmpty) - } - - @Test func nodeCapturesProvidedValue() throws { - let command = try K8sLoadImage.parse(["--node", "k8s-dev-worker-1", "my-app:latest"]) - #expect(command.node == ["k8s-dev-worker-1"]) - } - - @Test func nodeCapturesMultipleProvidedValues() throws { - let command = try K8sLoadImage.parse([ - "--node", "k8s-dev-worker-1", "--node", "k8s-dev-worker-2", "my-app:latest", - ]) - #expect(command.node == ["k8s-dev-worker-1", "k8s-dev-worker-2"]) - } -} - -// MARK: - K8sLoadImage.resolveTargets - -@Suite("K8sLoadImage.resolveTargets") -struct ResolveTargetsTests { - @Test func noNodesReturnsControlPlaneAndAllWorkers() throws { - let targets = try K8sLoadImage.resolveTargets( - clusterName: "k8s-dev", nodes: [], workers: ["k8s-dev-worker-1", "k8s-dev-worker-2"]) - #expect(targets == ["k8s-dev", "k8s-dev-worker-1", "k8s-dev-worker-2"]) - } - - @Test func noNodesWithNoWorkersReturnsOnlyControlPlane() throws { - let targets = try K8sLoadImage.resolveTargets(clusterName: "k8s-dev", nodes: [], workers: []) - #expect(targets == ["k8s-dev"]) - } - - @Test func nodeMatchingControlPlaneReturnsJustControlPlane() throws { - let targets = try K8sLoadImage.resolveTargets( - clusterName: "k8s-dev", nodes: ["k8s-dev"], workers: ["k8s-dev-worker-1"]) - #expect(targets == ["k8s-dev"]) - } - - @Test func nodeMatchingWorkerReturnsJustThatWorker() throws { - let targets = try K8sLoadImage.resolveTargets( - clusterName: "k8s-dev", nodes: ["k8s-dev-worker-2"], workers: ["k8s-dev-worker-1", "k8s-dev-worker-2"]) - #expect(targets == ["k8s-dev-worker-2"]) - } - - @Test func multipleNodesReturnsEachInOrder() throws { - let targets = try K8sLoadImage.resolveTargets( - clusterName: "k8s-dev", nodes: ["k8s-dev-worker-2", "k8s-dev"], - workers: ["k8s-dev-worker-1", "k8s-dev-worker-2"]) - #expect(targets == ["k8s-dev-worker-2", "k8s-dev"]) - } - - @Test func duplicateNodesAreDeduplicated() throws { - let targets = try K8sLoadImage.resolveTargets( - clusterName: "k8s-dev", nodes: ["k8s-dev-worker-1", "k8s-dev-worker-1"], - workers: ["k8s-dev-worker-1"]) - #expect(targets == ["k8s-dev-worker-1"]) - } - - @Test func unknownNodeThrowsInvalidArgument() throws { - #expect(throws: ContainerizationError.self) { - try K8sLoadImage.resolveTargets( - clusterName: "k8s-dev", nodes: ["not-a-node"], workers: ["k8s-dev-worker-1"]) - } - } -} diff --git a/docs/kubernetes.md b/docs/cluster.md similarity index 73% rename from docs/kubernetes.md rename to docs/cluster.md index 2e7882fa..4dcfcf78 100644 --- a/docs/kubernetes.md +++ b/docs/cluster.md @@ -1,4 +1,4 @@ -# Kubernetes clusters +# Container cluster Kubernetes is an open-source orchestration platform for automating deployment, scaling, and management of containerized workloads across clusters of machines. With `container`, you can run local Kubernetes clusters for development and testing. @@ -16,17 +16,17 @@ Running Kubernetes locally with `container` gives you a streamlined workflow to ```bash # Create a cluster -container k8s create +container cluster create # Verify the cluster is running -container k8s list +container cluster list # Interact with the cluster using kubectl kubectl cluster-info kubectl get nodes # Clean up when done -container k8s delete +container cluster delete ``` The cluster is automatically added to your `~/.kube/config`, so standard Kubernetes tools just work. @@ -35,23 +35,23 @@ The cluster is automatically added to your `~/.kube/config`, so standard Kuberne ### Create and list clusters -Create a cluster with `container k8s create`. By default, it creates a cluster named `k8s-dev`: +Create a cluster with `container cluster create`. By default, it creates a cluster named `cluster-dev`: ```bash -container k8s create +container cluster create ``` You can create multiple named clusters: ```bash -container k8s create --name staging -container k8s create --name testing +container cluster create --name staging +container cluster create --name testing ``` List all clusters and their status: ```bash -container k8s list +container cluster list ``` ### Cluster lifecycle @@ -60,13 +60,13 @@ Create a cluster, use it with kubectl, and delete it when finished: ```bash # Create a cluster -container k8s create --name my-cluster +container cluster create --name my-cluster # Use the cluster with kubectl kubectl --context my-cluster get pods # Delete the cluster when finished -container k8s delete --name my-cluster +container cluster delete --name my-cluster ``` ### Customize resources @@ -75,18 +75,18 @@ Allocate CPU and memory based on your needs: ```bash # Create a cluster with 4 CPUs and 8GB memory -container k8s create --name high-resource --cpus 4 --memory 8g +container cluster create --name high-resource --cpus 4 --memory 8g ``` By default, clusters use 1/4 of your host's CPUs (minimum 2) and 1/4 of your host's memory (minimum 2GB). ### Multi-node clusters -By default, `container k8s create` creates a single node that acts as both control plane and worker. Use `--workers` to add dedicated worker nodes instead: +By default, `container cluster create` creates a single node that acts as both control plane and worker. Use `--workers` to add dedicated worker nodes instead: ```bash # Create a cluster with a control plane and 3 worker nodes -container k8s create --name my-cluster --workers 3 +container cluster create --name my-cluster --workers 3 ``` When `--workers` is greater than `0`, the control-plane node is tainted so pods only schedule onto worker nodes, matching typical multi-node cluster behavior. Worker nodes share the same `--node-image`, `--cpus`, and `--memory` settings as the control plane, and are named `-worker-`. @@ -97,8 +97,8 @@ Once a cluster is created, `kubectl` works normally: ```bash # Use your created cluster -kubectl --context k8s-dev get pods -kubectl --context k8s-dev describe node +kubectl --context cluster-dev get pods +kubectl --context cluster-dev describe node # Switch between clusters kubectl config use-context staging @@ -117,13 +117,13 @@ Build an image and load it into your cluster: container build -t my-app:latest . # Load the image into every node of the cluster -container k8s load-image my-app:latest +container cluster load-image my-app:latest # Load the image into a single node instead -container k8s load-image --node k8s-dev-worker-1 my-app:latest +container cluster load-image --node cluster-dev-worker-1 my-app:latest # Load the image into specific nodes only -container k8s load-image --node k8s-dev-worker-1 --node k8s-dev-worker-2 my-app:latest +container cluster load-image --node cluster-dev-worker-1 --node cluster-dev-worker-2 my-app:latest ``` The image is placed in the `k8s.io` namespace, making it available for pod scheduling: @@ -146,8 +146,8 @@ spec: If you have multiple clusters, specify which one to load into: ```bash -container k8s load-image --name staging my-app:latest -container k8s load-image --name testing my-app:latest +container cluster load-image --name staging my-app:latest +container cluster load-image --name testing my-app:latest ``` ### Multi-architecture images @@ -156,7 +156,7 @@ When loading multi-architecture images, specify the architecture if needed: ```bash # Load the amd64 variant of a multi-arch image -container k8s load-image --platform linux/amd64 my-app:latest +container cluster load-image --platform linux/amd64 my-app:latest ``` ## Kubernetes node image @@ -164,7 +164,7 @@ container k8s load-image --platform linux/amd64 my-app:latest By default, clusters use `kindest/node:v1.35.5`, a Kubernetes-in-Docker image optimized for local development. You can use a different node image when creating a cluster: ```bash -container k8s create --node-image docker.io/kindest/node:v1.34.11 +container cluster create --node-image docker.io/kindest/node:v1.34.11 ``` ## Custom CNI @@ -172,7 +172,7 @@ container k8s create --node-image docker.io/kindest/node:v1.34.11 By default, clusters install the bundled kindnet CNI for pod networking. Use `--cni` to apply a different CNI manifest instead: ```bash -container k8s create --name my-cluster --cni ./my-cni.yaml +container cluster create --name my-cluster --cni ./my-cni.yaml ``` The manifest must be a plain Kubernetes YAML file (the same shape `kubectl apply -f` expects), not a Helm chart. @@ -192,7 +192,7 @@ helm template cilium cilium/cilium --version 1.20.1 --namespace kube-system --se Then create the cluster with that manifest: ```bash -container k8s create --name cilium-demo --cni ./cilium.yaml +container cluster create --name cilium-demo --cni ./cilium.yaml ``` Verify Cilium came up: @@ -207,13 +207,13 @@ kubectl --context cilium-demo get nodes -o wide Remove a cluster and its data: ```bash -container k8s delete --name my-cluster +container cluster delete --name my-cluster ``` To have a cluster automatically remove itself when stopped, create it with `--rm`: ```bash -container k8s create --name temp-cluster --rm +container cluster create --name temp-cluster --rm ``` ### Recovering a stopped cluster @@ -221,8 +221,8 @@ container k8s create --name temp-cluster --rm If a cluster's control plane node stops (for example, after `container stop`), delete and re-create the cluster: ```bash -container k8s delete --name my-cluster -container k8s create --name my-cluster +container cluster delete --name my-cluster +container cluster create --name my-cluster ``` ## Common workflows @@ -231,13 +231,13 @@ container k8s create --name my-cluster ```bash # Create a test cluster -container k8s create --name test +container cluster create --name test # Build your image locally container build -t my-service:v1.0 . # Load it into the test cluster -container k8s load-image --name test my-service:v1.0 +container cluster load-image --name test my-service:v1.0 # Deploy to the test cluster kubectl --context test apply -f deployment.yaml @@ -246,22 +246,22 @@ kubectl --context test apply -f deployment.yaml kubectl --context test logs deployment/my-service # Clean up when done -container k8s delete --name test +container cluster delete --name test ``` ### One cluster per feature branch ```bash # Create isolated clusters for concurrent development -container k8s create --name feature-auth -container k8s create --name feature-payments +container cluster create --name feature-auth +container cluster create --name feature-payments # Work on each feature in isolation, test against its own cluster -container k8s load-image --name feature-auth my-service:feature-auth -container k8s load-image --name feature-payments my-service:feature-payments +container cluster load-image --name feature-auth my-service:feature-auth +container cluster load-image --name feature-payments my-service:feature-payments ``` ## See also -- [Command reference](./command-reference.md#kubernetes-cluster-management) — full details of all `container k8s` subcommands +- [Command reference](./command-reference.md#container-cluster-management) — full details of all `container cluster` subcommands - [Container machines](./container-machine.md) — persistent Linux environments for general-purpose development diff --git a/docs/command-reference.md b/docs/command-reference.md index edb6bc02..e535b7f7 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1622,26 +1622,26 @@ container system property list container system property list --format json ``` -## Kubernetes Cluster Management +## Container Cluster Management -`container k8s` manages local Kubernetes clusters backed by container VMs. Each cluster runs a Kubernetes control-plane node inside a container using `kindest/node` and `kubeadm`, with optional additional worker nodes. +`container cluster` manages local Kubernetes clusters backed by container VMs. Each cluster runs a Kubernetes control-plane node inside a container using `kindest/node` and `kubeadm`, with optional additional worker nodes. > [!IMPORTANT] -> The `k8s` command is an experimental feature and its subcommands and options are subject to change. +> The `cluster` command is an experimental feature and its subcommands and options are subject to change. -### `container k8s create` +### `container cluster create` Creates and starts a local Kubernetes cluster and optional worker nodes. Pulls the node image if needed, runs `kubeadm init`, installs a CNI (default: bundled kindnet), and merges the cluster credentials into `~/.kube/config`. **Usage** ```bash -container k8s create [--name ] [--node-image ] [--cni ] [--workers ] [--rm] [] [--debug] +container cluster create [--name ] [--node-image ] [--cni ] [--workers ] [--rm] [] [--debug] ``` **Options** -* `--name `: Cluster name (default: `k8s-dev`) +* `--name `: Cluster name (default: `cluster-dev`) * `--node-image `: Node image reference (default: `docker.io/kindest/node:v1.35.5`) * `--cni `: Optional path to a CNI manifest to apply, or `NONE` (case-insensitive) to skip installing a CNI. If not provided, the bundled kindnet CNI is used. With `NONE`, the command returns without waiting for nodes to become `Ready`, since that requires a CNI; apply your own afterward with `kubectl apply`. * `--workers `: Number of worker nodes to create (default: `0`, meaning the control-plane node also acts as a worker) @@ -1663,75 +1663,75 @@ container k8s create [--name ] [--node-image ] [--cni ] [--wo **Examples** ```bash -# create a cluster with the default name (k8s-dev) -container k8s create +# create a cluster with the default name (cluster-dev) +container cluster create # create a cluster with a custom name and resource allocation -container k8s create --name my-cluster --cpus 4 --memory 8g +container cluster create --name my-cluster --cpus 4 --memory 8g # create a cluster that removes itself when stopped -container k8s create --name temp-cluster --rm +container cluster create --name temp-cluster --rm # create a cluster using a custom CNI manifest instead of the bundled kindnet -container k8s create --cni ./my-cni.yaml +container cluster create --cni ./my-cni.yaml # create a cluster with no CNI installed -container k8s create --cni NONE +container cluster create --cni NONE # create a cluster with a control plane and 3 worker nodes -container k8s create --name my-cluster --workers 3 +container cluster create --name my-cluster --workers 3 ``` -### `container k8s delete (rm)` +### `container cluster delete (rm)` Stops and deletes a Kubernetes cluster container, including any worker nodes, and removes its entry from `~/.kube/config`. **Usage** ```bash -container k8s delete [--name ] [--debug] +container cluster delete [--name ] [--debug] ``` **Options** -* `--name `: Cluster name (default: `k8s-dev`) +* `--name `: Cluster name (default: `cluster-dev`) **Examples** ```bash # delete the default cluster -container k8s delete +container cluster delete # delete a named cluster -container k8s delete --name my-cluster -container k8s rm --name my-cluster +container cluster delete --name my-cluster +container cluster rm --name my-cluster ``` -### `container k8s list (ls)` +### `container cluster list (ls)` Lists all Kubernetes clusters with their status and node image. **Usage** ```bash -container k8s list [--debug] +container cluster list [--debug] ``` **Examples** ```bash -container k8s list -container k8s ls +container cluster list +container cluster ls ``` -### `container k8s load-image` +### `container cluster load-image` Exports an image from the local `container` image store and imports it into every node's containerd (in the `k8s.io` namespace) so that Kubernetes can schedule pods that reference it on any node. **Usage** ```bash -container k8s load-image [--name ] [--platform ] [--node ...] [--debug] +container cluster load-image [--name ] [--platform ] [--node ...] [--debug] ``` **Arguments** @@ -1740,7 +1740,7 @@ container k8s load-image [--name ] [--platform ] [--node **Options** -* `--name `: Cluster name (default: `k8s-dev`) +* `--name `: Cluster name (default: `cluster-dev`) * `--platform `: Platform of the image variant to load from a multi-arch image (format: os/arch[/variant], default: `linux/`). Use this when the local store contains a multi-arch manifest list and you want to select a specific variant. * `--node `: Load into specific nodes instead of every node in the cluster. Repeat to target multiple nodes. @@ -1748,42 +1748,42 @@ container k8s load-image [--name ] [--platform ] [--node ```bash # load an image into every node of the default cluster -container k8s load-image my-app:latest +container cluster load-image my-app:latest # load an image into every node of a named cluster -container k8s load-image --name my-cluster my-app:latest +container cluster load-image --name my-cluster my-app:latest # load the amd64 variant of a multi-arch image -container k8s load-image --platform linux/amd64 my-app:latest +container cluster load-image --platform linux/amd64 my-app:latest # load an image into a single worker node only -container k8s load-image --node k8s-dev-worker-1 my-app:latest +container cluster load-image --node cluster-dev-worker-1 my-app:latest # load an image into specific worker nodes only -container k8s load-image --node k8s-dev-worker-1 --node k8s-dev-worker-2 my-app:latest +container cluster load-image --node cluster-dev-worker-1 --node cluster-dev-worker-2 my-app:latest ``` -### `container k8s write-config` +### `container cluster write-config` Fetches the current kubeconfig from a running cluster and merges its context into a kubeconfig file. Use this to write to an alternate config file. **Usage** ```bash -container k8s write-config [--name ] [--kubeconfig ] [--debug] +container cluster write-config [--name ] [--kubeconfig ] [--debug] ``` **Options** -* `--name `: Cluster name (default: `k8s-dev`) +* `--name `: Cluster name (default: `cluster-dev`) * `--kubeconfig `: Path to the kubeconfig file to write or append to (default: `~/.kube/config`) **Examples** ```bash # refresh credentials for the default cluster into ~/.kube/config -container k8s write-config +container cluster write-config # write the context for a named cluster to an alternate kubeconfig file -container k8s write-config --name my-cluster --kubeconfig ~/.kube/my-cluster.kubeconfig +container cluster write-config --name my-cluster --kubeconfig ~/.kube/my-cluster.kubeconfig ``` diff --git a/docs/how-to.md b/docs/how-to.md index f2eb2632..5cfb1512 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -20,5 +20,5 @@ - [Logs](./logs.md) — container output, VM boot logs, and the `container` system's own logs. - [`config.toml` reference](./container-system-config.md) — every configuration key, its default, and how to view your merged configuration. - [Container machines](./container-machine.md) — persistent Linux environments built from OCI images, with your home directory mounted in and the filesystem surviving stop/start. -- [Kubernetes clusters](./kubernetes.md) — run local Kubernetes clusters, single-node or multi-node, for development and testing, load your own images, and test deployments before production. +- [Container cluster](./cluster.md) — run local Kubernetes clusters, single-node or multi-node, for development and testing, load your own images, and test deployments before production. - [Shell completions](./shell-completions.md) — generate and install completion scripts for `zsh`, `bash`, and `fish`.