Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ $(STAGING_DIR):
@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/compose/bin)"
@mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/compose/resources)"

@install "$(BUILD_BIN_DIR)/container" "$(join $(STAGING_DIR), bin/container)"
@install "$(BUILD_BIN_DIR)/container-apiserver" "$(join $(STAGING_DIR), bin/container-apiserver)"
Expand All @@ -151,6 +153,11 @@ $(STAGING_DIR):
@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)/compose" "$(join $(STAGING_DIR), libexec/container/plugins/compose/bin/compose)"
@install Sources/Plugins/Compose/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/compose/config.toml)"
@install -m 0644 Sources/ContainerCompose/Resources/Containerfile "$(join $(STAGING_DIR), libexec/container/plugins/compose/resources/Containerfile)"
@install -m 0755 Sources/ContainerCompose/Resources/container-compose-idle-shutdown "$(join $(STAGING_DIR), libexec/container/plugins/compose/resources/container-compose-idle-shutdown)"
@install -m 0644 Sources/ContainerCompose/Resources/container-compose-idle-shutdown.service "$(join $(STAGING_DIR), libexec/container/plugins/compose/resources/container-compose-idle-shutdown.service)"

@echo Install update script
@install scripts/update-container.sh "$(join $(STAGING_DIR), bin/update-container.sh)"
Expand All @@ -167,6 +174,7 @@ installer-pkg: $(STAGING_DIR)
@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/compose/bin/compose)"

@echo Creating application installer
@pkgbuild --root "$(STAGING_DIR)" --identifier com.apple.container-installer --install-location /usr/local --version ${RELEASE_VERSION} $(PKG_PATH)
Expand All @@ -182,6 +190,7 @@ dsym:
@cp -a "$(BUILD_BIN_DIR)/container-core-images.dSYM" "$(DSYM_DIR)"
@cp -a "$(BUILD_BIN_DIR)/container-apiserver.dSYM" "$(DSYM_DIR)"
@cp -a "$(BUILD_BIN_DIR)/container.dSYM" "$(DSYM_DIR)"
@cp -a "$(BUILD_BIN_DIR)/compose.dSYM" "$(DSYM_DIR)"

@echo Packaging the debug symbols...
@(cd "$(dir $(DSYM_DIR))" ; zip -r $(notdir $(DSYM_PATH)) $(notdir $(DSYM_DIR)))
Expand Down Expand Up @@ -212,7 +221,8 @@ COV_BINARIES := \
$(BUILD_BIN_DIR)/container-runtime-linux \
$(BUILD_BIN_DIR)/container-network-vmnet \
$(BUILD_BIN_DIR)/container-core-images \
$(BUILD_BIN_DIR)/machine-apiserver
$(BUILD_BIN_DIR)/machine-apiserver \
$(BUILD_BIN_DIR)/compose
COV_OBJECT_FLAGS := $(patsubst %,-object %,$(COV_BINARIES))
# Set of files we do not want to get caught in the coverage generation
LLVM_COV_IGNORE := \
Expand Down
57 changes: 57 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ let package = Package(
.library(name: "MachineAPIClient", targets: ["MachineAPIClient"]),
.library(name: "MachineAPIService", targets: ["MachineAPIService"]),
.library(name: "ContainerK8s", targets: ["ContainerK8s"]),
.library(name: "ContainerCompose", targets: ["ContainerCompose"]),
],
dependencies: [
.package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)),
Expand Down Expand Up @@ -174,6 +175,21 @@ let package = Package(
],
path: "Tests/K8sPluginTests"
),
.testTarget(
name: "ComposePluginTests",
dependencies: [
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationOCI", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "Logging", package: "swift-log"),
.product(name: "SystemPackage", package: "swift-system"),
"ContainerAPIClient",
"ContainerCompose",
"ContainerResource",
"MachineAPIClient",
],
path: "Tests/ComposePluginTests"
),
.target(
name: "ContainerK8s",
dependencies: [
Expand All @@ -193,12 +209,37 @@ let package = Package(
"Yams",
]
),
.target(
name: "ContainerCompose",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationExtras", package: "containerization"),
.product(name: "Logging", package: "swift-log"),
.product(name: "SystemPackage", package: "swift-system"),
"ContainerAPIClient",
"ContainerPersistence",
"ContainerResource",
"ContainerVersion",
"MachineAPIClient",
"TerminalProgress",
],
resources: [
.copy("Resources")
]
),
.executableTarget(
name: "k8s",
dependencies: ["ContainerK8s"],
path: "Sources/Plugins/K8s",
exclude: ["config.toml", "Resources"]
),
.executableTarget(
name: "compose",
dependencies: ["ContainerCompose"],
path: "Sources/Plugins/Compose",
exclude: ["config.toml"]
),
.executableTarget(
name: "container-apiserver",
dependencies: [
Expand Down Expand Up @@ -281,10 +322,18 @@ let package = Package(
],
path: "Sources/Services/ContainerAPIService/Client"
),
.testTarget(
name: "ContainerXPCTests",
dependencies: [
.product(name: "Containerization", package: "containerization"),
"ContainerXPC",
]
),
.testTarget(
name: "ContainerAPIClientTests",
dependencies: [
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "SystemPackage", package: "swift-system"),
"ContainerAPIClient",
"ContainerPersistence",
Expand Down Expand Up @@ -656,6 +705,14 @@ let package = Package(
],
path: "Sources/Services/MachineAPIService/Server"
),
.testTarget(
name: "MachineAPIServiceTests",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "SystemPackage", package: "swift-system"),
"MachineAPIService",
]
),
.executableTarget(
name: "machine-apiserver",
dependencies: [
Expand Down
25 changes: 23 additions & 2 deletions Sources/APIServer/ContainerDNSHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

import ContainerAPIClient
import ContainerAPIService
import ContainerResource
import ContainerizationExtras
import DNSServer

/// Handler that uses table lookup to resolve hostnames.
struct ContainerDNSHandler: DNSHandler {
private static let composeMachineHostname = "compose.machine"

private let networkService: NetworksService
private let ttl: UInt32

Expand Down Expand Up @@ -76,7 +80,7 @@ struct ContainerDNSHandler: DNSHandler {
}

private func answerHost(question: Question) async throws -> ResourceRecord? {
guard let ipAllocation = try await networkService.lookup(hostname: question.name) else {
guard let ipAllocation = try await lookup(hostname: question.name) else {
return nil
}
let ipv4 = ipAllocation.ipv4Address.address.description
Expand All @@ -88,7 +92,7 @@ struct ContainerDNSHandler: DNSHandler {
}

private func answerHost6(question: Question) async throws -> (record: ResourceRecord?, hostnameExists: Bool) {
guard let ipAllocation = try await networkService.lookup(hostname: question.name) else {
guard let ipAllocation = try await lookup(hostname: question.name) else {
return (nil, false)
}
guard let ipv6Address = ipAllocation.ipv6Address else {
Expand All @@ -101,4 +105,21 @@ struct ContainerDNSHandler: DNSHandler {

return (HostRecord<IPv6Address>(name: question.name, ttl: ttl, ip: ip), true)
}

private func lookup(hostname: String) async throws -> Attachment? {
if let attachment = try await networkService.lookup(hostname: hostname) {
return attachment
}

guard
let baseHostname = HostDNSResolver.wildcardBaseHostname(
for: hostname,
baseHostname: Self.composeMachineHostname
)
else {
return nil
}

return try await networkService.lookup(hostname: baseHostname)
}
}
2 changes: 2 additions & 0 deletions Sources/ContainerCommands/Machine/MachineCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension Application {
Change the container machine configuration (takes effect after restart):
$ container machine set -n my-machine cpus=4 memory=8G home-mount=ro
$ container machine stop my-machine
$ container machine start my-machine
$ container machine run -n my-machine -- nproc

Stop and delete the container machine:
Expand All @@ -49,6 +50,7 @@ extension Application {
MachineRun.self,
MachineSet.self,
MachineSetDefault.self,
MachineStart.self,
MachineStop.self,
],
aliases: ["m"]
Expand Down
65 changes: 2 additions & 63 deletions Sources/ContainerCommands/Machine/MachineHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

import ContainerAPIClient
import ContainerResource
import ContainerizationError
import Foundation
import Logging
import MachineAPIClient

Expand All @@ -36,71 +33,13 @@ func resolveMachineId(_ id: String?, client: MachineClient) async throws -> Stri
}

/// Boots a container machine and, on first ever boot, runs the in-VM init script
/// to set up the host user. Returns the resulting snapshot.
///
/// When `interactive` is true the init script is wired to the host's terminal
/// (used by `machine run`); otherwise it runs detached so non-TTY callers like
/// `machine create` don't require a TTY or pollute host stdout.
///
/// On any failure during user setup the machine is stopped to leave it in a clean state.
/// to set up the host user. The lifecycle implementation is shared with plugins.
@discardableResult
func bootMachine(
id: String?,
client: MachineClient,
log: Logger,
interactive: Bool
) async throws -> MachineSnapshot {
var dynamicEnv: [String: String] = [:]
if let sshAuthSock = ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] {
dynamicEnv["SSH_AUTH_SOCK"] = sshAuthSock
}
let snapshot = try await client.boot(id: id, dynamicEnv: dynamicEnv)

guard !snapshot.initialized else {
return snapshot
}

do {
guard let containerId = snapshot.containerId else {
throw ContainerizationError(
.invalidState,
message: "container machine is running but has no container ID"
)
}

let io = try ProcessIO.create(
tty: interactive,
interactive: interactive,
detach: !interactive
)
defer {
try? io.close()
}

let processConfig = ProcessConfiguration(
executable: "/\(MachineBundle.sbinDirectory)/\(MachineBundle.initFile)",
arguments: ["-u"],
environment: snapshot.configuration.processEnvironment,
terminal: interactive
)

let process = try await ContainerClient().createProcess(
containerId: containerId,
processId: UUID().uuidString.lowercased(),
configuration: processConfig,
stdio: io.stdio)

let exitCode = try await io.handleProcess(process: process, log: log)
guard exitCode == 0 else {
throw ContainerizationError(
.invalidState,
message: "container machine failed to create user"
)
}
} catch {
try? await client.stop(id: snapshot.id)
throw error
}

return snapshot
try await client.bootAndInitialize(id: id, log: log, interactive: interactive)
}
43 changes: 43 additions & 0 deletions Sources/ContainerCommands/Machine/MachineStart.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//===----------------------------------------------------------------------===//
// 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 ArgumentParser
import ContainerAPIClient
import MachineAPIClient

extension Application {
public struct MachineStart: AsyncLoggableCommand {
public init() {}

public static let configuration = CommandConfiguration(
commandName: "start",
abstract: "Start a stopped container machine"
)

@OptionGroup
public var logOptions: Flags.Logging

@Argument(help: "Container machine ID (uses default if not specified)")
var id: String?

public func run() async throws {
let client = MachineClient()
let machineId = try await resolveMachineId(id, client: client)
_ = try await bootMachine(id: machineId, client: client, log: log, interactive: false)
print(machineId)
}
}
}
Loading