056a909081
Today this protocols create method is just odd. Our only implementation of it immediately casts to LinuxContainer, failing if it cannot do so. I think what might make more sense is to pass in a configuration itself with core parameters that we expect every vmm to be able to support, and then in a specific implementation they can continue to cast this type to a specific one to possibly extract some extra configuration values (rosetta for VZ for example). This rework will also make it simpler to support a Pod type, as the vm setup is identical and simple.
170 lines
6.3 KiB
Swift
170 lines
6.3 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025 Apple Inc. and the Containerization 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 Containerization
|
|
import ContainerizationError
|
|
import ContainerizationOCI
|
|
import ContainerizationOS
|
|
import Foundation
|
|
|
|
extension Application {
|
|
struct Run: AsyncParsableCommand {
|
|
static let configuration = CommandConfiguration(
|
|
commandName: "run",
|
|
abstract: "Run a container"
|
|
)
|
|
|
|
@Option(name: [.customLong("image"), .customShort("i")], help: "Image reference to base the container on")
|
|
var imageReference: String = "docker.io/library/alpine:3.16"
|
|
|
|
@Option(name: .long, help: "id for the container")
|
|
var id: String = "cctl"
|
|
|
|
@Option(name: [.customLong("cpus"), .customShort("c")], help: "Number of CPUs to allocate to the container")
|
|
var cpus: Int = 2
|
|
|
|
@Option(name: [.customLong("memory"), .customShort("m")], help: "Amount of memory in megabytes")
|
|
var memory: UInt64 = 1024
|
|
|
|
@Option(name: .customLong("fs-size"), help: "The size to create the block filesystem as")
|
|
var fsSizeInMB: UInt64 = 2048
|
|
|
|
@Flag(name: .customLong("rosetta"), help: "Enable rosetta x64 emulation")
|
|
var rosetta = false
|
|
|
|
@Option(name: .customLong("mount"), help: "Directory to share into the container (Example: /foo:/bar)")
|
|
var mounts: [String] = []
|
|
|
|
@Option(name: .long, help: "IP address with subnet")
|
|
var ip: String?
|
|
|
|
@Option(name: .long, help: "Gateway address")
|
|
var gateway: String?
|
|
|
|
@Option(name: .customLong("ns"), help: "Nameserver addresses")
|
|
var nameservers: [String] = []
|
|
|
|
@Option(
|
|
name: [.customLong("kernel"), .customShort("k")], help: "Kernel binary path", completion: .file(),
|
|
transform: { str in
|
|
URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false)
|
|
})
|
|
public var kernel: String
|
|
|
|
@Option(name: .long, help: "Current working directory")
|
|
var cwd: String = "/"
|
|
|
|
@Argument(parsing: .captureForPassthrough)
|
|
var arguments: [String] = ["/bin/sh"]
|
|
|
|
func run() async throws {
|
|
let kernel = Kernel(
|
|
path: URL(fileURLWithPath: kernel),
|
|
platform: .linuxArm
|
|
)
|
|
var manager = try await ContainerManager(
|
|
kernel: kernel,
|
|
initfsReference: "vminit:latest",
|
|
rosetta: rosetta
|
|
)
|
|
let sigwinchStream = AsyncSignalHandler.create(notify: [SIGWINCH])
|
|
|
|
let current = try Terminal.current
|
|
try current.setraw()
|
|
defer { current.tryReset() }
|
|
|
|
let container = try await manager.create(
|
|
id,
|
|
reference: imageReference,
|
|
rootfsSizeInBytes: fsSizeInMB.mib()
|
|
) { config in
|
|
config.cpus = cpus
|
|
config.memoryInBytes = memory.mib()
|
|
config.process.setTerminalIO(terminal: current)
|
|
config.process.arguments = arguments
|
|
config.process.workingDirectory = cwd
|
|
|
|
for mount in self.mounts {
|
|
let paths = mount.split(separator: ":")
|
|
if paths.count != 2 {
|
|
throw ContainerizationError(
|
|
.invalidArgument,
|
|
message: "incorrect mount format detected: \(mount)"
|
|
)
|
|
}
|
|
let host = String(paths[0])
|
|
let guest = String(paths[1])
|
|
let czMount = Containerization.Mount.share(
|
|
source: host,
|
|
destination: guest
|
|
)
|
|
config.mounts.append(czMount)
|
|
}
|
|
|
|
var hosts = Hosts.default
|
|
if let ip {
|
|
guard let gateway else {
|
|
throw ContainerizationError(.invalidArgument, message: "gateway must be specified")
|
|
}
|
|
config.interfaces.append(NATInterface(address: ip, gateway: gateway))
|
|
config.dns = .init(nameservers: [gateway])
|
|
if nameservers.count > 0 {
|
|
config.dns = .init(nameservers: nameservers)
|
|
}
|
|
hosts.entries.append(
|
|
Hosts.Entry(
|
|
ipAddress: ip,
|
|
hostnames: [id]
|
|
))
|
|
}
|
|
config.hosts = hosts
|
|
}
|
|
|
|
defer {
|
|
try? manager.delete(id)
|
|
}
|
|
|
|
try await container.create()
|
|
try await container.start()
|
|
|
|
// Resize the containers pty to the current terminal window.
|
|
try? await container.resize(to: try current.size)
|
|
|
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
group.addTask {
|
|
for await _ in sigwinchStream.signals {
|
|
try await container.resize(to: try current.size)
|
|
}
|
|
}
|
|
|
|
try await container.wait()
|
|
group.cancelAll()
|
|
|
|
try await container.stop()
|
|
}
|
|
}
|
|
|
|
private static let appRoot: URL = {
|
|
FileManager.default.urls(
|
|
for: .applicationSupportDirectory,
|
|
in: .userDomainMask
|
|
).first!
|
|
.appendingPathComponent("com.apple.containerization")
|
|
}()
|
|
}
|
|
}
|