Skip to content

Commit b2994ac

Browse files
realrajaryanJaewonHurjglogancrosbymichaelegernst
authored
Add container machine for managing persistent Linux VMs (apple#1662)
## Type of Change - [ ] Bug fix - [x] New feature - [ ] Breaking change - [ ] Documentation update ## Motivation and Context `container` runs each workload in an ephemeral VM, so there's no built-in way to keep a persistent Linux environment you can log into and work in. `container machine` adds one. A container machine is a lightweight, persistent, and integrated Linux environments that feel like an extension of your Mac, created from standard OCI images with a familiar UX. The login user matches your host account with passwordless `sudo`, your home directory is mounted inside the VM, and each machine keeps its filesystem and runs the image's own init system (such as`systemd` or `openrc`). ```bash container machine create alpine:3.22 --name my-machine container machine run -n my-machine # interactive shell container machine set -n my-machine cpus=4 memory=8G ``` Subcommands: `create`, `run`, `list` (`ls`), `inspect`, `set`, `set-default`, `logs`, `stop`, `delete` (`rm`); `m` aliases `machine`. Docs added to `docs/command-reference.md` (Machine Management) and `docs/how-to.md` ("Use container machines"). ## Testing - [x] Tested locally - [x] Added/updated tests - [x] Added/updated docs Signed-off-by: Raj Aryan Singh <rajaryan_singh@apple.com> Co-authored-by: Jaewon Hur <jaewon_hur@apple.com> Co-authored-by: John Logan <john_logan@apple.com> Co-authored-by: Michael Crosby <michael_crosby@apple.com> Co-authored-by: Eric Ernst <eric_ernst@apple.com> Co-authored-by: Danny Canter <danny_canter@apple.com>
1 parent 1b55763 commit b2994ac

51 files changed

Lines changed: 5125 additions & 36 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ $(STAGING_DIR):
100100
@mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/bin)"
101101
@mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/bin)"
102102
@mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin)"
103+
@mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/bin)"
104+
@mkdir -p "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/resources)"
103105

104106
@install "$(BUILD_BIN_DIR)/container" "$(join $(STAGING_DIR), bin/container)"
105107
@install "$(BUILD_BIN_DIR)/container-apiserver" "$(join $(STAGING_DIR), bin/container-apiserver)"
@@ -109,6 +111,10 @@ $(STAGING_DIR):
109111
@install Sources/Plugins/NetworkVmnet/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/config.toml)"
110112
@install "$(BUILD_BIN_DIR)/container-core-images" "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin/container-core-images)"
111113
@install Sources/Plugins/CoreImages/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/config.toml)"
114+
@install "$(BUILD_BIN_DIR)/machine-apiserver" "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/bin/machine-apiserver)"
115+
@install Sources/Plugins/MachineAPIServer/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/config.toml)"
116+
@install Sources/Plugins/MachineAPIServer/Resources/init "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/resources/init)"
117+
@install Sources/Plugins/MachineAPIServer/Resources/create-user.sh "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/resources/create-user.sh)"
112118

113119
@echo Install update script
114120
@install scripts/update-container.sh "$(join $(STAGING_DIR), bin/update-container.sh)"
@@ -123,6 +129,7 @@ installer-pkg: $(STAGING_DIR)
123129
@codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin/container-core-images)"
124130
@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)"
125131
@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)"
132+
@codesign $(CODESIGN_OPTS) --prefix=com.apple.container. "$(join $(STAGING_DIR), libexec/container/plugins/machine-apiserver/bin/machine-apiserver)"
126133

127134
@echo Creating application installer
128135
@pkgbuild --root "$(STAGING_DIR)" --identifier com.apple.container-installer --install-location /usr/local --version ${RELEASE_VERSION} $(PKG_PATH)
@@ -211,8 +218,10 @@ INTEGRATION_TEST_SUITES ?= \
211218
TestCLIKernelSet \
212219
TestCLIAnonymousVolumes \
213220
TestCLINotFound \
214-
TestCLINoParallelCases \
215-
TestCLISystemDF
221+
TestCLISystemDF \
222+
TestCLIMachineCommand \
223+
TestCLIMachineRuntime \
224+
TestCLINoParallelCases
216225

217226
empty :=
218227
space := $(empty) $(empty)

Package.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ let package = Package(
4949
.library(name: "ContainerOS", targets: ["ContainerOS"]),
5050
.library(name: "SocketForwarder", targets: ["SocketForwarder"]),
5151
.library(name: "TerminalProgress", targets: ["TerminalProgress"]),
52+
.library(name: "MachineAPIClient", targets: ["MachineAPIClient"]),
53+
.library(name: "MachineAPIService", targets: ["MachineAPIService"]),
5254
],
5355
dependencies: [
5456
.package(url: "https://github.com/apple/containerization.git", exact: Version(stringLiteral: scVersion)),
@@ -116,6 +118,7 @@ let package = Package(
116118
"ContainerVersion",
117119
.product(name: "SystemPackage", package: "swift-system"),
118120
"ContainerXPC",
121+
"MachineAPIClient",
119122
"TerminalProgress",
120123
"Yams",
121124
],
@@ -562,5 +565,53 @@ let package = Package(
562565
.product(name: "SystemPackage", package: "swift-system")
563566
]
564567
),
568+
.target(
569+
name: "MachineAPIClient",
570+
dependencies: [
571+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
572+
.product(name: "ContainerizationOCI", package: "containerization"),
573+
.product(name: "Logging", package: "swift-log"),
574+
"ContainerAPIClient",
575+
"ContainerPersistence",
576+
"ContainerResource",
577+
"ContainerXPC",
578+
"TerminalProgress",
579+
],
580+
path: "Sources/Services/MachineAPIService/Client"
581+
),
582+
.target(
583+
name: "MachineAPIService",
584+
dependencies: [
585+
.product(name: "Containerization", package: "containerization"),
586+
.product(name: "ContainerizationEXT4", package: "containerization"),
587+
.product(name: "ContainerizationExtras", package: "containerization"),
588+
.product(name: "ContainerizationOCI", package: "containerization"),
589+
.product(name: "Logging", package: "swift-log"),
590+
.product(name: "SystemPackage", package: "swift-system"),
591+
"ContainerAPIClient",
592+
"ContainerResource",
593+
"ContainerRuntimeClient",
594+
"ContainerXPC",
595+
"MachineAPIClient",
596+
],
597+
path: "Sources/Services/MachineAPIService/Server"
598+
),
599+
.executableTarget(
600+
name: "machine-apiserver",
601+
dependencies: [
602+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
603+
.product(name: "Logging", package: "swift-log"),
604+
"ContainerAPIClient",
605+
"ContainerLog",
606+
"ContainerPersistence",
607+
"ContainerPlugin",
608+
"ContainerVersion",
609+
"ContainerXPC",
610+
"MachineAPIClient",
611+
"MachineAPIService",
612+
],
613+
path: "Sources/Plugins/MachineAPIServer",
614+
exclude: ["config.toml", "Resources"]
615+
),
565616
]
566617
)

Sources/APIServer/APIServer+Start.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ extension APIServer {
6363
var routes = [XPCRoute: XPCServer.RouteHandler]()
6464
let pluginLoader = try initializePluginLoader(log: log)
6565

66-
try await initializePlugins(pluginLoader: pluginLoader, log: log, routes: &routes)
66+
try await initializePlugins(pluginLoader: pluginLoader, log: log, routes: &routes, debug: debug)
6767
let containersService = try initializeContainersService(
6868
pluginLoader: pluginLoader,
6969
containerSystemConfig: containerSystemConfig,
@@ -227,14 +227,15 @@ extension APIServer {
227227
private func initializePlugins(
228228
pluginLoader: PluginLoader,
229229
log: Logger,
230-
routes: inout [XPCRoute: XPCServer.RouteHandler]
230+
routes: inout [XPCRoute: XPCServer.RouteHandler],
231+
debug: Bool = false
231232
) async throws {
232233
log.info("initializing plugins")
233234

234235
let bootPlugins = pluginLoader.findPlugins().filter { $0.shouldBoot }
235236

236237
let service = PluginsService(pluginLoader: pluginLoader, log: log)
237-
try await service.loadAll(bootPlugins)
238+
try await service.loadAll(bootPlugins, debug: debug)
238239

239240
let harness = PluginsHarness(service: service, log: log)
240241
routes[XPCRoute.pluginGet] = XPCServer.route(harness.get)

Sources/ContainerCommands/Application.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ public struct Application: AsyncLoggableCommand {
7878
RegistryCommand.self,
7979
]
8080
),
81+
CommandGroup(
82+
name: "Machine",
83+
subcommands: [
84+
MachineCommand.self
85+
]
86+
),
8187
CommandGroup(
8288
name: "Volume",
8389
subcommands: [

Sources/ContainerCommands/Container/ContainerDelete.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ extension Application {
5959

6060
let containers: [String]
6161
if all {
62-
containers = try await client.list().compactMap { c in
62+
let filters = ContainerListFilters().withoutMachines()
63+
containers = try await client.list(filters: filters).compactMap { c in
6364
// Skip running containers when using --all without --force
6465
if c.status == .running && !force {
6566
return nil

Sources/ContainerCommands/Container/ContainerKill.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ extension Application {
5656

5757
let containers: [String]
5858
if self.all {
59-
containers = try await client.list(filters: ContainerListFilters(status: .running)).map { $0.id }
59+
let filters = ContainerListFilters(status: .running).withoutMachines()
60+
containers = try await client.list(filters: filters).map { $0.id }
6061
} else {
6162
containers = containerIds
6263
}

Sources/ContainerCommands/Container/ContainerList.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ extension Application {
4444

4545
public func run() async throws {
4646
let client = ContainerClient()
47-
let filters = self.all ? ContainerListFilters.all : ContainerListFilters(status: .running)
47+
48+
let filters = ContainerListFilters(status: self.all ? nil : .running).withoutMachines()
4849
let containers = try await client.list(filters: filters)
4950
let items = containers.map { ManagedContainer($0) }
5051
try Output.render(payload: items, display: items, format: format, quiet: quiet)

Sources/ContainerCommands/Container/ContainerPrune.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import ArgumentParser
1818
import ContainerAPIClient
19+
import ContainerResource
1920
import ContainerizationError
2021
import Foundation
2122

@@ -33,7 +34,8 @@ extension Application {
3334

3435
public func run() async throws {
3536
let client = ContainerClient()
36-
let containersToPrune = try await client.list().filter { $0.status == .stopped }
37+
let filters = ContainerListFilters(status: .stopped).withoutMachines()
38+
let containersToPrune = try await client.list(filters: filters)
3739

3840
var prunedContainerIds = [String]()
3941
var totalSize: UInt64 = 0

Sources/ContainerCommands/Container/ContainerStop.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ extension Application {
6060

6161
let containers: [String]
6262
if self.all {
63-
containers = try await client.list().map { $0.id }
63+
let filters = ContainerListFilters().withoutMachines()
64+
containers = try await client.list(filters: filters).map { $0.id }
6465
} else {
6566
containers = containerIds
6667
}

Sources/ContainerCommands/Image/ImagePrune.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ extension Application {
2424
public init() {}
2525
public static let configuration = CommandConfiguration(
2626
commandName: "prune",
27-
abstract: "Remove all dangling images. If -a is specified, also remove all images not referenced by any container.")
27+
abstract: "Remove unused or all images")
2828

2929
@OptionGroup
3030
public var logOptions: Flags.Logging

0 commit comments

Comments
 (0)