Files
container/Sources/cctl/RunCommand.swift
T
Danny Canter 2a96a0d447 Containerization: Always set TERM (#146)
Make sure we always set TERM for containers that ask for a tty. Right
now this handling was spread around in a bunch of spots, but I'd wager
setting it for the client on the host via LinuxContainer/Process is more
sane and already what we do for a lot of the other fields.
2025-06-18 17:00:00 -07:00

162 lines
5.9 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
//
// 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
@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: .customLong("init"), help: "Init block path", completion: .file(),
transform: { str in
URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false)
})
public var initBlock: String
@Option(name: .long, help: "Current working directory")
var cwd: String = "/"
@Argument var arguments: [String] = ["/bin/sh"]
func run() async throws {
let kernel = Kernel(
path: URL(fileURLWithPath: kernel),
platform: .linuxArm
)
let store = try await ContainerStore(
root: Self.appRoot,
kernel: kernel,
initPath: .init(filePath: initBlock)
)
let sigwinchStream = AsyncSignalHandler.create(notify: [SIGWINCH])
let current = try Terminal.current
try current.setraw()
defer { current.tryReset() }
let container = try await store.create(
id: id,
reference: imageReference,
fsSizeInBytes: fsSizeInMB.mib()
)
container.cpus = cpus
container.memoryInBytes = memory.mib()
container.terminalDevice = current
container.arguments = arguments
container.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
)
container.mounts.append(czMount)
}
if let ip {
guard let gateway else {
throw ContainerizationError(.invalidArgument, message: "gateway must be specified")
}
container.interfaces.append(NATInterface(address: ip, gateway: gateway))
container.dns = .init(nameservers: [gateway])
if nameservers.count > 0 {
container.dns = .init(nameservers: nameservers)
}
}
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")
}()
}
}