mirror of
https://github.com/apple/container.git
synced 2026-07-12 20:47:04 +00:00
b2994ac369
## 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>
138 lines
4.6 KiB
Swift
138 lines
4.6 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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 ContainerResource
|
|
import Foundation
|
|
import MachineAPIClient
|
|
|
|
extension Application {
|
|
public struct MachineList: AsyncLoggableCommand {
|
|
public init() {}
|
|
|
|
public static let configuration = CommandConfiguration(
|
|
commandName: "list",
|
|
abstract: "List container machines",
|
|
aliases: ["ls"]
|
|
)
|
|
|
|
@Option(name: .long, help: "Format of the output")
|
|
var format: MachineCommand.ListFormat = .table
|
|
|
|
@Flag(name: .shortAndLong, help: "Only output the container machine ID")
|
|
var quiet = false
|
|
|
|
@OptionGroup
|
|
public var logOptions: Flags.Logging
|
|
|
|
public func run() async throws {
|
|
let client = MachineClient()
|
|
let machines = try await client.list()
|
|
|
|
if self.quiet {
|
|
machines.forEach { print($0.id) }
|
|
return
|
|
}
|
|
|
|
let defaultMachine = try await client.getDefault()
|
|
try printMachines(machines: machines, format: format, defaultMachine: defaultMachine)
|
|
}
|
|
|
|
private func printMachines(
|
|
machines: [MachineSnapshot],
|
|
format: MachineCommand.ListFormat,
|
|
defaultMachine: String?
|
|
) throws {
|
|
if format == .json {
|
|
let printables = machines.map {
|
|
PrintableMachine($0, isDefault: $0.id == defaultMachine)
|
|
}
|
|
let encoder = JSONEncoder()
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
let data = try encoder.encode(printables)
|
|
print(String(decoding: data, as: UTF8.self))
|
|
return
|
|
}
|
|
|
|
var rows: [[String]] = [["NAME", "CREATED", "IP", "CPUS", "MEMORY", "DISK", "STATE", "DEFAULT"]]
|
|
for machine in machines {
|
|
rows.append([
|
|
machine.id,
|
|
machine.createdDate.map { formatDate($0) } ?? "-",
|
|
machine.ipAddress ?? "-",
|
|
"\(machine.bootConfig.cpus)",
|
|
formatMemory(machine.bootConfig.memory.toUInt64(unit: .bytes)),
|
|
machine.diskSize.map { formatMemory($0) } ?? "-",
|
|
machine.status.rawValue,
|
|
machine.id == defaultMachine ? "*" : "",
|
|
])
|
|
}
|
|
|
|
let formatter = TableOutput(rows: rows)
|
|
print(formatter.format())
|
|
}
|
|
}
|
|
}
|
|
|
|
private func formatMemory(_ bytes: UInt64) -> String {
|
|
let gib: UInt64 = 1024 * 1024 * 1024
|
|
if bytes >= gib {
|
|
if bytes % gib == 0 {
|
|
return "\(bytes / gib)G"
|
|
}
|
|
let formatted = String(format: "%.1fG", Double(bytes) / Double(gib))
|
|
if formatted.hasSuffix(".0G") {
|
|
return "\(bytes / gib)G"
|
|
}
|
|
return formatted
|
|
}
|
|
return "\(bytes / (1024 * 1024))M"
|
|
}
|
|
|
|
private let dateFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
|
return formatter
|
|
}()
|
|
|
|
private func formatDate(_ date: Date) -> String {
|
|
dateFormatter.string(from: date)
|
|
}
|
|
|
|
private struct PrintableMachine: Codable {
|
|
let id: String
|
|
let status: RuntimeStatus
|
|
let `default`: Bool
|
|
let ipAddress: String?
|
|
let cpus: Int
|
|
let memory: UInt64
|
|
let diskSize: UInt64?
|
|
let createdDate: Date?
|
|
|
|
init(_ machine: MachineSnapshot, isDefault: Bool) {
|
|
self.id = machine.id
|
|
self.status = machine.status
|
|
self.default = isDefault
|
|
self.ipAddress = machine.ipAddress
|
|
self.cpus = machine.bootConfig.cpus
|
|
self.memory = machine.bootConfig.memory.toUInt64(unit: .bytes)
|
|
self.diskSize = machine.diskSize
|
|
self.createdDate = machine.createdDate
|
|
}
|
|
}
|