mirror of
https://github.com/apple/container.git
synced 2026-08-24 10:05:43 -05:00
Select alternate data path with container system start --app-root path. (#419)
Closes #418.
This commit is contained in:
@@ -68,7 +68,10 @@ jobs:
|
||||
- name: Test the container project
|
||||
run: |
|
||||
launchctl setenv HTTP_PROXY $HTTP_PROXY
|
||||
make test cleancontent install-kernel integration
|
||||
APP_ROOT=$(mktemp -d -p "${RUNNER_TEMP}")
|
||||
trap 'rm -rf "${APP_ROOT}"; echo Removing data directory ${APP_ROOT}' EXIT
|
||||
echo "Created data directory ${APP_ROOT}"
|
||||
make APP_ROOT="${APP_ROOT}" test install-kernel integration
|
||||
env:
|
||||
DEVELOPER_DIR: "/Applications/Xcode-latest.app/Contents/Developer"
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@ DSYM_DIR := bin/$(BUILD_CONFIGURATION)/bundle/container-dSYM
|
||||
DSYM_PATH := bin/$(BUILD_CONFIGURATION)/bundle/container-dSYM.zip
|
||||
CODESIGN_OPTS ?= --force --sign - --timestamp=none
|
||||
|
||||
# Conditionally use a temporary data directory for integration tests
|
||||
ifeq ($(strip $(APP_ROOT)),)
|
||||
SYSTEM_START_OPTS :=
|
||||
else
|
||||
SYSTEM_START_OPTS := --app-root "$(strip $(APP_ROOT))"
|
||||
endif
|
||||
|
||||
MACOS_VERSION := $(shell sw_vers -productVersion)
|
||||
MACOS_MAJOR := $(shell echo $(MACOS_VERSION) | cut -d. -f1)
|
||||
|
||||
@@ -124,28 +131,30 @@ test:
|
||||
.PHONY: install-kernel
|
||||
install-kernel:
|
||||
@bin/container system stop || true
|
||||
@bin/container system start --enable-kernel-install
|
||||
@bin/container system start --enable-kernel-install $(SYSTEM_START_OPTS)
|
||||
|
||||
.PHONY: integration
|
||||
integration: init-block
|
||||
@echo Ensuring apiserver stopped before the CLI integration tests...
|
||||
@bin/container system stop && sleep 3 && scripts/ensure-container-stopped.sh
|
||||
@echo Running the integration tests...
|
||||
@bin/container system start
|
||||
@echo "Removing any existing containers"
|
||||
@bin/container rm --all
|
||||
@echo "Starting CLI integration tests"
|
||||
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLINetwork
|
||||
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunLifecycle
|
||||
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIExecCommand
|
||||
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLICreateCommand
|
||||
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunCommand
|
||||
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIImagesCommand
|
||||
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunBase
|
||||
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIBuildBase
|
||||
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIVolumes
|
||||
@echo Ensuring apiserver stopped after the CLI integration tests...
|
||||
@scripts/ensure-container-stopped.sh
|
||||
bin/container system start $(SYSTEM_START_OPTS) && \
|
||||
echo "Starting CLI integration tests" && \
|
||||
{ \
|
||||
exit_code=0; \
|
||||
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLINetwork || exit_code=1 ; \
|
||||
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunLifecycle || exit_code=1 ; \
|
||||
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIExecCommand || exit_code=1 ; \
|
||||
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLICreateCommand || exit_code=1 ; \
|
||||
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunCommand || exit_code=1 ; \
|
||||
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIImagesCommand || exit_code=1 ; \
|
||||
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunBase || exit_code=1 ; \
|
||||
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIBuildBase || exit_code=1 ; \
|
||||
$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIVolumes || exit_code=1 ; \
|
||||
echo Ensuring apiserver stopped after the CLI integration tests ; \
|
||||
scripts/ensure-container-stopped.sh ; \
|
||||
exit $${exit_code} ; \
|
||||
}
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: swift-fmt update-licenses
|
||||
|
||||
@@ -164,6 +164,7 @@ let package = Package(
|
||||
.product(name: "Containerization", package: "containerization"),
|
||||
"CVersion",
|
||||
"ContainerLog",
|
||||
"ContainerPlugin",
|
||||
"ContainerXPC",
|
||||
"ContainerImagesService",
|
||||
],
|
||||
|
||||
@@ -43,16 +43,11 @@ struct APIServer: AsyncParsableCommand {
|
||||
@Flag(name: .long, help: "Enable debug logging")
|
||||
var debug = false
|
||||
|
||||
@Option(name: .shortAndLong, help: "Daemon root directory")
|
||||
var root = Self.appRoot.path
|
||||
var appRoot = ApplicationRoot.url
|
||||
|
||||
static let appRoot: URL = {
|
||||
FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!
|
||||
.appendingPathComponent("com.apple.container")
|
||||
}()
|
||||
static func releaseVersion() -> String {
|
||||
(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? get_release_version().map { String(cString: $0) } ?? "0.0.0"
|
||||
}
|
||||
|
||||
func run() async throws {
|
||||
let commandName = Self.configuration.commandName ?? "container-apiserver"
|
||||
@@ -64,20 +59,18 @@ struct APIServer: AsyncParsableCommand {
|
||||
|
||||
do {
|
||||
log.info("configuring XPC server")
|
||||
let root = URL(filePath: root)
|
||||
var routes = [XPCRoute: XPCServer.RouteHandler]()
|
||||
let pluginLoader = try initializePluginLoader(log: log)
|
||||
try await initializePlugins(pluginLoader: pluginLoader, log: log, routes: &routes)
|
||||
let containersService = try initializeContainerService(root: root, pluginLoader: pluginLoader, log: log, routes: &routes)
|
||||
let containersService = try initializeContainerService(pluginLoader: pluginLoader, log: log, routes: &routes)
|
||||
let networkService = try await initializeNetworkService(
|
||||
root: root,
|
||||
pluginLoader: pluginLoader,
|
||||
log: log,
|
||||
routes: &routes
|
||||
)
|
||||
initializeHealthCheckService(log: log, routes: &routes)
|
||||
try initializeKernelService(log: log, routes: &routes)
|
||||
try initializeVolumeService(root: root, containersService: containersService, log: log, routes: &routes)
|
||||
try initializeVolumeService(containersService: containersService, log: log, routes: &routes)
|
||||
|
||||
let server = XPCServer(
|
||||
identifier: "com.apple.container.apiserver",
|
||||
@@ -134,7 +127,10 @@ struct APIServer: AsyncParsableCommand {
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("..")
|
||||
.standardized
|
||||
let pluginsURL = PluginLoader.userPluginsDir(root: installRoot)
|
||||
log.info("initializing plugin loader", metadata: ["installRoot": "\(installRoot.path(percentEncoded: false))"])
|
||||
|
||||
let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot)
|
||||
log.info("detecting user plugins directory", metadata: ["path": "\(pluginsURL.path(percentEncoded: false))"])
|
||||
var directoryExists: ObjCBool = false
|
||||
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
|
||||
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil
|
||||
@@ -161,10 +157,19 @@ struct APIServer: AsyncParsableCommand {
|
||||
AppBundlePluginFactory(),
|
||||
]
|
||||
|
||||
log.info("PLUGINS: \(pluginDirectories)")
|
||||
let statePath = PluginLoader.defaultPluginResourcePath(root: Self.appRoot)
|
||||
for pluginDirectory in pluginDirectories {
|
||||
log.info("discovered plugin directory", metadata: ["path": "\(pluginDirectory.path(percentEncoded: false))"])
|
||||
}
|
||||
|
||||
let statePath = PluginLoader.defaultPluginResourcePath(root: appRoot)
|
||||
try FileManager.default.createDirectory(at: statePath, withIntermediateDirectories: true)
|
||||
return PluginLoader(pluginDirectories: pluginDirectories, pluginFactories: pluginFactories, defaultResourcePath: statePath, log: log)
|
||||
return PluginLoader(
|
||||
appRoot: appRoot,
|
||||
pluginDirectories: pluginDirectories,
|
||||
pluginFactories: pluginFactories,
|
||||
defaultResourcePath: statePath,
|
||||
log: log
|
||||
)
|
||||
}
|
||||
|
||||
// First load all of the plugins we can find. Then just expose
|
||||
@@ -188,20 +193,20 @@ struct APIServer: AsyncParsableCommand {
|
||||
}
|
||||
|
||||
private func initializeHealthCheckService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) {
|
||||
let svc = HealthCheckHarness(log: log)
|
||||
let svc = HealthCheckHarness(appRoot: appRoot, log: log)
|
||||
routes[XPCRoute.ping] = svc.ping
|
||||
}
|
||||
|
||||
private func initializeKernelService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws {
|
||||
let svc = try KernelService(log: log, appRoot: Self.appRoot)
|
||||
let svc = try KernelService(log: log, appRoot: appRoot)
|
||||
let harness = KernelHarness(service: svc, log: log)
|
||||
routes[XPCRoute.installKernel] = harness.install
|
||||
routes[XPCRoute.getDefaultKernel] = harness.getDefaultKernel
|
||||
}
|
||||
|
||||
private func initializeContainerService(root: URL, pluginLoader: PluginLoader, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws -> ContainersService {
|
||||
private func initializeContainerService(pluginLoader: PluginLoader, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws -> ContainersService {
|
||||
let service = try ContainersService(
|
||||
root: root,
|
||||
appRoot: appRoot,
|
||||
pluginLoader: pluginLoader,
|
||||
log: log
|
||||
)
|
||||
@@ -217,12 +222,11 @@ struct APIServer: AsyncParsableCommand {
|
||||
}
|
||||
|
||||
private func initializeNetworkService(
|
||||
root: URL,
|
||||
pluginLoader: PluginLoader,
|
||||
log: Logger,
|
||||
routes: inout [XPCRoute: XPCServer.RouteHandler]
|
||||
) async throws -> NetworksService {
|
||||
let resourceRoot = root.appendingPathComponent("networks")
|
||||
let resourceRoot = appRoot.appendingPathComponent("networks")
|
||||
let service = try await NetworksService(
|
||||
pluginLoader: pluginLoader,
|
||||
resourceRoot: resourceRoot,
|
||||
@@ -245,8 +249,8 @@ struct APIServer: AsyncParsableCommand {
|
||||
return service
|
||||
}
|
||||
|
||||
private func initializeVolumeService(root: URL, containersService: ContainersService, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws {
|
||||
let resourceRoot = root.appendingPathComponent("volumes")
|
||||
private func initializeVolumeService(containersService: ContainersService, log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws {
|
||||
let resourceRoot = appRoot.appendingPathComponent("volumes")
|
||||
let service = try VolumesService(resourceRoot: resourceRoot, containersService: containersService, log: log)
|
||||
let harness = VolumesHarness(service: service, log: log)
|
||||
|
||||
@@ -255,8 +259,4 @@ struct APIServer: AsyncParsableCommand {
|
||||
routes[XPCRoute.volumeList] = harness.list
|
||||
routes[XPCRoute.volumeInspect] = harness.inspect
|
||||
}
|
||||
|
||||
private static func releaseVersion() -> String {
|
||||
(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? get_release_version().map { String(cString: $0) } ?? "0.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ actor ContainersService {
|
||||
}
|
||||
}
|
||||
|
||||
public init(root: URL, pluginLoader: PluginLoader, log: Logger) throws {
|
||||
let containerRoot = root.appendingPathComponent("containers")
|
||||
public init(appRoot: URL, pluginLoader: PluginLoader, log: Logger) throws {
|
||||
let containerRoot = appRoot.appendingPathComponent("containers")
|
||||
try FileManager.default.createDirectory(at: containerRoot, withIntermediateDirectories: true)
|
||||
self.containerRoot = containerRoot
|
||||
self.pluginLoader = pluginLoader
|
||||
@@ -200,7 +200,7 @@ actor ContainersService {
|
||||
]
|
||||
try loader.registerWithLaunchd(
|
||||
plugin: plugin,
|
||||
rootURL: path,
|
||||
pluginStateRoot: path,
|
||||
args: args,
|
||||
instanceId: configuration.id
|
||||
)
|
||||
|
||||
@@ -14,20 +14,28 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import CVersion
|
||||
import ContainerClient
|
||||
import ContainerXPC
|
||||
import Containerization
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
actor HealthCheckHarness {
|
||||
private let appRoot: URL
|
||||
private let log: Logger
|
||||
|
||||
public init(log: Logger) {
|
||||
public init(appRoot: URL, log: Logger) {
|
||||
self.appRoot = appRoot
|
||||
self.log = log
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func ping(_ message: XPCMessage) async -> XPCMessage {
|
||||
message.reply()
|
||||
let reply = message.reply()
|
||||
reply.set(key: .appRoot, value: appRoot.absoluteString)
|
||||
reply.set(key: .apiServerVersion, value: APIServer.releaseVersion())
|
||||
reply.set(key: .apiServerCommit, value: get_git_commit().map { String(cString: $0) } ?? "unknown")
|
||||
return reply
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ actor NetworksService {
|
||||
|
||||
try await pluginLoader.registerWithLaunchd(
|
||||
plugin: networkPlugin,
|
||||
rootURL: store.entityUrl(configuration.id),
|
||||
pluginStateRoot: store.entityUrl(configuration.id),
|
||||
args: args,
|
||||
instanceId: configuration.id
|
||||
)
|
||||
|
||||
@@ -91,50 +91,6 @@ struct Application: AsyncParsableCommand {
|
||||
defaultSubcommand: DefaultCommand.self
|
||||
)
|
||||
|
||||
static let appRoot: URL = {
|
||||
FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!
|
||||
.appendingPathComponent("com.apple.container")
|
||||
}()
|
||||
|
||||
static let pluginLoader: PluginLoader = {
|
||||
let installRoot = CommandLine.executablePathUrl
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("..")
|
||||
.standardized
|
||||
let pluginsURL = PluginLoader.userPluginsDir(root: installRoot)
|
||||
var directoryExists: ObjCBool = false
|
||||
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
|
||||
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil
|
||||
|
||||
// plugins built into the application installed as a macOS app bundle
|
||||
let appBundlePluginsURL = Bundle.main.resourceURL?.appending(path: "plugins")
|
||||
|
||||
// plugins built into the application installed as a Unix-like application
|
||||
let installRootPluginsURL =
|
||||
installRoot
|
||||
.appendingPathComponent("libexec")
|
||||
.appendingPathComponent("container")
|
||||
.appendingPathComponent("plugins")
|
||||
.standardized
|
||||
|
||||
let pluginDirectories = [
|
||||
userPluginsURL,
|
||||
appBundlePluginsURL,
|
||||
installRootPluginsURL,
|
||||
].compactMap { $0 }
|
||||
|
||||
let pluginFactories = [
|
||||
DefaultPluginFactory()
|
||||
]
|
||||
|
||||
let statePath = PluginLoader.defaultPluginResourcePath(root: Self.appRoot)
|
||||
try! FileManager.default.createDirectory(at: statePath, withIntermediateDirectories: true)
|
||||
return PluginLoader(pluginDirectories: pluginDirectories, pluginFactories: pluginFactories, defaultResourcePath: statePath, log: log)
|
||||
}()
|
||||
|
||||
public static func main() async throws {
|
||||
restoreCursorAtExit()
|
||||
|
||||
@@ -161,7 +117,8 @@ struct Application: AsyncParsableCommand {
|
||||
// on the root command will land here.
|
||||
let containsHelp = fullArgs.contains("-h") || fullArgs.contains("--help")
|
||||
if fullArgs.count <= 2 && containsHelp {
|
||||
Self.printModifiedHelpText()
|
||||
let pluginLoader = try? await createPluginLoader()
|
||||
await Self.printModifiedHelpText(pluginLoader: pluginLoader)
|
||||
return
|
||||
}
|
||||
let errorAsString: String = String(describing: error)
|
||||
@@ -174,6 +131,53 @@ struct Application: AsyncParsableCommand {
|
||||
}
|
||||
}
|
||||
|
||||
static func createPluginLoader() async throws -> PluginLoader {
|
||||
let installRoot = CommandLine.executablePathUrl
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("..")
|
||||
.standardized
|
||||
let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot)
|
||||
var directoryExists: ObjCBool = false
|
||||
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
|
||||
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil
|
||||
|
||||
// plugins built into the application installed as a macOS app bundle
|
||||
let appBundlePluginsURL = Bundle.main.resourceURL?.appending(path: "plugins")
|
||||
|
||||
// plugins built into the application installed as a Unix-like application
|
||||
let installRootPluginsURL =
|
||||
installRoot
|
||||
.appendingPathComponent("libexec")
|
||||
.appendingPathComponent("container")
|
||||
.appendingPathComponent("plugins")
|
||||
.standardized
|
||||
|
||||
let pluginDirectories = [
|
||||
userPluginsURL,
|
||||
appBundlePluginsURL,
|
||||
installRootPluginsURL,
|
||||
].compactMap { $0 }
|
||||
|
||||
let pluginFactories = [
|
||||
DefaultPluginFactory()
|
||||
]
|
||||
|
||||
guard let systemHealth = try? await ClientHealthCheck.ping(timeout: .seconds(10)) else {
|
||||
throw ContainerizationError(.timeout, message: "unable to retrieve application data root from API server")
|
||||
}
|
||||
let statePath = PluginLoader.defaultPluginResourcePath(root: systemHealth.appRoot)
|
||||
guard (try? FileManager.default.createDirectory(at: statePath, withIntermediateDirectories: true)) != nil else {
|
||||
throw ContainerizationError(.invalidState, message: "unable to create plugin state directory")
|
||||
}
|
||||
return PluginLoader(
|
||||
appRoot: systemHealth.appRoot,
|
||||
pluginDirectories: pluginDirectories,
|
||||
pluginFactories: pluginFactories,
|
||||
defaultResourcePath: statePath,
|
||||
log: log
|
||||
)
|
||||
}
|
||||
|
||||
static func handleProcess(io: ProcessIO, process: ClientProcess) async throws -> Int32 {
|
||||
let signals = AsyncSignalHandler.create(notify: Application.signalSet)
|
||||
return try await withThrowingTaskGroup(of: Int32?.self, returning: Int32.self) { group in
|
||||
@@ -304,10 +308,14 @@ struct Application: AsyncParsableCommand {
|
||||
extension Application {
|
||||
// Because we support plugins, we need to modify the help text to display
|
||||
// any if we found some.
|
||||
static func printModifiedHelpText() {
|
||||
let altered = Self.pluginLoader.alterCLIHelpText(
|
||||
original: Application.helpMessage(for: Application.self)
|
||||
)
|
||||
static func printModifiedHelpText(pluginLoader: PluginLoader?) async {
|
||||
let original = Application.helpMessage(for: Application.self)
|
||||
guard let pluginLoader else {
|
||||
print(original)
|
||||
print("PLUGINS: not available, run `container system start`")
|
||||
return
|
||||
}
|
||||
let altered = pluginLoader.alterCLIHelpText(original: original)
|
||||
print(altered)
|
||||
}
|
||||
|
||||
|
||||
@@ -170,8 +170,8 @@ extension Application {
|
||||
}
|
||||
|
||||
let dockerfile = try Data(contentsOf: URL(filePath: file))
|
||||
let exportPath = Application.appRoot.appendingPathComponent(".build")
|
||||
|
||||
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
|
||||
let exportPath = systemHealth.appRoot.appendingPathComponent(".build")
|
||||
let buildID = UUID().uuidString
|
||||
let tempURL = exportPath.appendingPathComponent(buildID)
|
||||
try FileManager.default.createDirectory(at: tempURL, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
@@ -71,7 +71,8 @@ extension Application {
|
||||
let fetchTask = await taskManager.startTask()
|
||||
|
||||
let builderImage: String = ClientDefaults.get(key: .defaultBuilderImage)
|
||||
let exportsMount: String = Application.appRoot.appendingPathComponent(".build").absolutePath()
|
||||
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
|
||||
let exportsMount: String = systemHealth.appRoot.appendingPathComponent(".build").absolutePath()
|
||||
|
||||
if !FileManager.default.fileExists(atPath: exportsMount) {
|
||||
try FileManager.default.createDirectory(
|
||||
|
||||
@@ -32,8 +32,9 @@ struct DefaultCommand: AsyncParsableCommand {
|
||||
|
||||
func run() async throws {
|
||||
// See if we have a possible plugin command.
|
||||
let pluginLoader = try? await Application.createPluginLoader()
|
||||
guard let command = remaining.first else {
|
||||
Application.printModifiedHelpText()
|
||||
await Application.printModifiedHelpText(pluginLoader: pluginLoader)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -44,8 +45,7 @@ struct DefaultCommand: AsyncParsableCommand {
|
||||
throw ValidationError("Unknown option '\(command)'")
|
||||
}
|
||||
|
||||
let pluginLoader = Application.pluginLoader
|
||||
guard let plugin = pluginLoader.findPlugin(name: command), plugin.config.isCLI else {
|
||||
guard let plugin = pluginLoader?.findPlugin(name: command), plugin.config.isCLI else {
|
||||
throw ValidationError("failed to find plugin named container-\(command)")
|
||||
}
|
||||
// Exec performs execvp (with no fork).
|
||||
|
||||
@@ -31,6 +31,12 @@ extension Application {
|
||||
@Option(name: .shortAndLong, help: "Path to the `container-apiserver` binary")
|
||||
var path: String = Bundle.main.executablePath ?? ""
|
||||
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Application data directory",
|
||||
transform: { URL(filePath: $0) })
|
||||
var appRoot: URL = ApplicationRoot.defaultURL
|
||||
|
||||
@Flag(name: .long, help: "Enable debug logging for the runtime daemon.")
|
||||
var debug = false
|
||||
|
||||
@@ -47,15 +53,17 @@ extension Application {
|
||||
.appendingPathComponent("container-apiserver")
|
||||
|
||||
var args = [executableUrl.absolutePath()]
|
||||
|
||||
if debug {
|
||||
args.append("--debug")
|
||||
}
|
||||
|
||||
let apiServerDataUrl = appRoot.appending(path: "apiserver")
|
||||
try! FileManager.default.createDirectory(at: apiServerDataUrl, withIntermediateDirectories: true)
|
||||
let env = ProcessInfo.processInfo.environment.filter { key, _ in
|
||||
var env = ProcessInfo.processInfo.environment.filter { key, _ in
|
||||
key.hasPrefix("CONTAINER_")
|
||||
}
|
||||
env["CONTAINER_APP_ROOT"] = appRoot.path(percentEncoded: false)
|
||||
|
||||
let logURL = apiServerDataUrl.appending(path: "apiserver.log")
|
||||
let plist = LaunchPlist(
|
||||
@@ -78,7 +86,7 @@ extension Application {
|
||||
// Now ping our friendly daemon. Fail if we don't get a response.
|
||||
do {
|
||||
print("Verifying apiserver is running...")
|
||||
try await ClientHealthCheck.ping(timeout: .seconds(10))
|
||||
_ = try await ClientHealthCheck.ping(timeout: .seconds(10))
|
||||
} catch {
|
||||
throw ContainerizationError(
|
||||
.internalError,
|
||||
|
||||
@@ -41,8 +41,11 @@ extension Application {
|
||||
// Now ping our friendly daemon. Fail after 10 seconds with no response.
|
||||
do {
|
||||
print("Verifying apiserver is running...")
|
||||
try await ClientHealthCheck.ping(timeout: .seconds(10))
|
||||
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
|
||||
print("apiserver is running")
|
||||
print("application data root: \(systemHealth.appRoot.path(percentEncoded: false))")
|
||||
print("container-apiserver version: \(systemHealth.apiServerVersion)")
|
||||
print("container-apiserver commit: \(systemHealth.apiServerCommit)")
|
||||
} catch {
|
||||
print("apiserver is not running")
|
||||
Application.exit(withError: ExitCode(1))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import ContainerXPC
|
||||
import ContainerizationError
|
||||
import Foundation
|
||||
|
||||
public enum ClientHealthCheck {
|
||||
@@ -26,9 +27,19 @@ extension ClientHealthCheck {
|
||||
XPCClient(service: serviceIdentifier)
|
||||
}
|
||||
|
||||
public static func ping(timeout: Duration? = .seconds(5)) async throws {
|
||||
public static func ping(timeout: Duration? = .seconds(5)) async throws -> SystemHealth {
|
||||
let client = Self.newClient()
|
||||
let request = XPCMessage(route: .ping)
|
||||
try await client.send(request, responseTimeout: timeout)
|
||||
let reply = try await client.send(request, responseTimeout: timeout)
|
||||
guard let appRootValue = reply.string(key: .appRoot), let appRoot = URL(string: appRootValue) else {
|
||||
throw ContainerizationError(.internalError, message: "failed to decode appRoot in health check")
|
||||
}
|
||||
guard let apiServerVersion = reply.string(key: .apiServerVersion) else {
|
||||
throw ContainerizationError(.internalError, message: "failed to decode apiServerVersion in health check")
|
||||
}
|
||||
guard let apiServerCommit = reply.string(key: .apiServerCommit) else {
|
||||
throw ContainerizationError(.internalError, message: "failed to decode apiServerCommit in health check")
|
||||
}
|
||||
return .init(appRoot: appRoot, apiServerVersion: apiServerVersion, apiServerCommit: apiServerCommit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the container 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 Foundation
|
||||
|
||||
/// Snapshot of the health of container services and resources
|
||||
public struct SystemHealth: Sendable, Codable {
|
||||
/// The full pathname of the application data root.
|
||||
public let appRoot: URL
|
||||
|
||||
/// The release version of the container services.
|
||||
public let apiServerVersion: String
|
||||
|
||||
/// The Git commit ID for the container services.
|
||||
public let apiServerCommit: String
|
||||
}
|
||||
@@ -53,6 +53,9 @@ public enum XPCKeys: String {
|
||||
|
||||
/// Health check request.
|
||||
case ping
|
||||
case appRoot
|
||||
case apiServerVersion
|
||||
case apiServerCommit
|
||||
|
||||
/// Process request keys.
|
||||
case signal
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the container 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 Foundation
|
||||
|
||||
public struct ApplicationRoot {
|
||||
public static let environmentName = "CONTAINER_APP_ROOT"
|
||||
|
||||
public static let defaultURL = FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!.appendingPathComponent("com.apple.container")
|
||||
|
||||
private static let envPath = ProcessInfo.processInfo.environment[Self.environmentName]
|
||||
|
||||
public static let url = envPath.map { URL(fileURLWithPath: $0) } ?? defaultURL
|
||||
|
||||
public static let path = url.path(percentEncoded: false)
|
||||
}
|
||||
@@ -19,10 +19,7 @@ import Foundation
|
||||
import Logging
|
||||
|
||||
public struct PluginLoader: Sendable {
|
||||
// A path on disk managed by the PluginLoader, where it stores
|
||||
// runtime data for loaded plugins. This includes the launchd plists
|
||||
// and logs files.
|
||||
private let defaultPluginResourcePath: URL
|
||||
private let appRoot: URL
|
||||
|
||||
private let pluginDirectories: [URL]
|
||||
|
||||
@@ -32,7 +29,13 @@ public struct PluginLoader: Sendable {
|
||||
|
||||
public typealias PluginQualifier = ((Plugin) -> Bool)
|
||||
|
||||
public init(pluginDirectories: [URL], pluginFactories: [PluginFactory], defaultResourcePath: URL, log: Logger? = nil) {
|
||||
// A path on disk managed by the PluginLoader, where it stores
|
||||
// runtime data for loaded plugins. This includes the launchd plists
|
||||
// and logs files.
|
||||
private let defaultPluginResourcePath: URL
|
||||
|
||||
public init(appRoot: URL, pluginDirectories: [URL], pluginFactories: [PluginFactory], defaultResourcePath: URL, log: Logger? = nil) {
|
||||
self.appRoot = appRoot
|
||||
self.pluginDirectories = pluginDirectories
|
||||
self.pluginFactories = pluginFactories
|
||||
self.log = log
|
||||
@@ -43,8 +46,8 @@ public struct PluginLoader: Sendable {
|
||||
root.appending(path: "plugin-state")
|
||||
}
|
||||
|
||||
static public func userPluginsDir(root: URL) -> URL {
|
||||
root
|
||||
static public func userPluginsDir(installRoot: URL) -> URL {
|
||||
installRoot
|
||||
.appending(path: "libexec")
|
||||
.appending(path: "container-plugins")
|
||||
.resolvingSymlinksInPath()
|
||||
@@ -164,7 +167,7 @@ extension PluginLoader {
|
||||
extension PluginLoader {
|
||||
public func registerWithLaunchd(
|
||||
plugin: Plugin,
|
||||
rootURL: URL? = nil,
|
||||
pluginStateRoot: URL? = nil,
|
||||
args: [String]? = nil,
|
||||
instanceId: String? = nil
|
||||
) throws {
|
||||
@@ -176,11 +179,13 @@ extension PluginLoader {
|
||||
|
||||
let id = plugin.getLaunchdLabel(instanceId: instanceId)
|
||||
log?.info("Registering plugin", metadata: ["id": "\(id)"])
|
||||
let rootURL = rootURL ?? self.defaultPluginResourcePath.appending(path: plugin.name)
|
||||
let rootURL = pluginStateRoot ?? self.defaultPluginResourcePath.appending(path: plugin.name)
|
||||
try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true)
|
||||
let env = ProcessInfo.processInfo.environment.filter { key, _ in
|
||||
var env = ProcessInfo.processInfo.environment.filter { key, _ in
|
||||
key.hasPrefix("CONTAINER_")
|
||||
}
|
||||
env[ApplicationRoot.environmentName] = appRoot.path(percentEncoded: false)
|
||||
|
||||
let logUrl = rootURL.appendingPathComponent("service.log")
|
||||
let plist = LaunchPlist(
|
||||
label: id,
|
||||
|
||||
@@ -19,6 +19,7 @@ import CVersion
|
||||
import ContainerImagesService
|
||||
import ContainerImagesServiceClient
|
||||
import ContainerLog
|
||||
import ContainerPlugin
|
||||
import ContainerXPC
|
||||
import Containerization
|
||||
import Foundation
|
||||
@@ -49,16 +50,11 @@ extension ImagesHelper {
|
||||
@Option(name: .long, help: "XPC service prefix")
|
||||
var serviceIdentifier: String = "com.apple.container.core.container-core-images"
|
||||
|
||||
@Option(name: .shortAndLong, help: "Daemon root directory")
|
||||
var root = Self.appRoot.path
|
||||
|
||||
static let appRoot: URL = {
|
||||
FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!
|
||||
.appendingPathComponent("com.apple.container")
|
||||
}()
|
||||
@Option(
|
||||
name: .shortAndLong,
|
||||
help: "Application data directory",
|
||||
transform: { URL(filePath: $0) })
|
||||
var appRoot = ApplicationRoot.defaultURL
|
||||
|
||||
private static let unpackStrategy = SnapshotStore.defaultUnpackStrategy
|
||||
|
||||
@@ -71,10 +67,9 @@ extension ImagesHelper {
|
||||
}
|
||||
do {
|
||||
log.info("configuring XPC server")
|
||||
let root = URL(filePath: root)
|
||||
var routes = [String: XPCServer.RouteHandler]()
|
||||
try self.initializeContentService(root: root, log: log, routes: &routes)
|
||||
try self.initializeImagesService(root: root, log: log, routes: &routes)
|
||||
try self.initializeContentService(root: appRoot, log: log, routes: &routes)
|
||||
try self.initializeImagesService(root: appRoot, log: log, routes: &routes)
|
||||
let xpc = XPCServer(
|
||||
identifier: serviceIdentifier,
|
||||
routes: routes,
|
||||
|
||||
@@ -28,6 +28,7 @@ struct PluginLoaderTest {
|
||||
defer { try? FileManager.default.removeItem(at: tempURL) }
|
||||
let factory = try setupMock(tempURL: tempURL)
|
||||
let loader = PluginLoader(
|
||||
appRoot: URL(filePath: "/foo"),
|
||||
pluginDirectories: [tempURL],
|
||||
pluginFactories: [factory], defaultResourcePath: tempURL)
|
||||
let plugins = loader.findPlugins()
|
||||
@@ -41,6 +42,7 @@ struct PluginLoaderTest {
|
||||
defer { try? FileManager.default.removeItem(at: tempURL) }
|
||||
let factory = try setupMock(tempURL: tempURL)
|
||||
let loader = PluginLoader(
|
||||
appRoot: URL(filePath: "/foo"),
|
||||
pluginDirectories: [tempURL],
|
||||
pluginFactories: [factory], defaultResourcePath: tempURL
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user