Files
container/Sources/APIServer/APIServer+Start.swift
T
Noah Thornton 0328f5c4c5 Centralize utilities for configuration loading and path parsing (#1448)
This creates a more centralized utility to find and load configuration files, as a follow up to (pr: https://github.com/apple/container/pull/1425).
2026-05-08 12:29:28 -04:00

391 lines
16 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 ContainerAPIService
import ContainerLog
import ContainerNetworkService
import ContainerPersistence
import ContainerPlugin
import ContainerResource
import ContainerXPC
import ContainerizationExtras
import DNSServer
import Foundation
import Logging
import SystemPackage
extension APIServer {
struct Start: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "start",
abstract: "Start helper for the API server"
)
static let listenAddress = "127.0.0.1"
static let localhostDNSPort = 1053
static let dnsPort = 2053
@Flag(name: .long, help: "Enable debug logging")
var debug = false
var appRoot = ApplicationRoot.url
var installRoot = InstallRoot.url
var logRoot = LogRoot.path
func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let commandName = APIServer._commandName
let logPath = logRoot.map { $0.appending("\(commandName).log") }
let log = ServiceLogger.bootstrap(category: "APIServer", debug: debug, logPath: logPath)
log.info("starting helper", metadata: ["name": "\(commandName)"])
defer {
log.info("stopping helper", metadata: ["name": "\(commandName)"])
}
do {
log.info("configuring XPC server")
var routes = [XPCRoute: XPCServer.RouteHandler]()
let pluginLoader = try initializePluginLoader(log: log)
try await initializePlugins(pluginLoader: pluginLoader, log: log, routes: &routes)
let containersService = try initializeContainersService(
pluginLoader: pluginLoader,
containerSystemConfig: containerSystemConfig,
log: log,
routes: &routes
)
let networkService = try await initializeNetworksService(
pluginLoader: pluginLoader,
containersService: containersService,
containerSystemConfig: containerSystemConfig,
log: log,
routes: &routes
)
await containersService.setNetworksService(networkService)
initializeHealthCheckService(log: log, routes: &routes)
try initializeKernelService(log: log, routes: &routes)
let volumesService = try initializeVolumeService(containersService: containersService, log: log, routes: &routes)
try initializeDiskUsageService(
containersService: containersService,
volumesService: volumesService,
log: log,
routes: &routes
)
let server = XPCServer(
identifier: "com.apple.container.apiserver",
routes: routes.reduce(
into: [String: XPCServer.RouteHandler](),
{
$0[$1.key.rawValue] = $1.value
}), log: log)
await withTaskGroup(of: Result<Void, Error>.self) { group in
group.addTask {
log.info("starting XPC server")
do {
try await server.listen()
return .success(())
} catch {
return .failure(error)
}
}
// start up host table DNS
group.addTask {
let hostsResolver = ContainerDNSHandler(networkService: networkService)
let nxDomainResolver = NxDomainResolver()
let compositeResolver = CompositeResolver(handlers: [hostsResolver, nxDomainResolver])
let hostsQueryValidator = StandardQueryValidator(handler: compositeResolver)
let dnsServer: DNSServer = DNSServer(handler: hostsQueryValidator, log: log)
log.info(
"starting DNS resolver for container hostnames",
metadata: [
"host": "\(Self.listenAddress)",
"port": "\(Self.dnsPort)",
]
)
do {
try await dnsServer.run(host: Self.listenAddress, port: Self.dnsPort)
return .success(())
} catch {
return .failure(error)
}
}
// start up realhost DNS
group.addTask {
do {
let localhostResolver = LocalhostDNSHandler(log: log)
await localhostResolver.monitorResolvers()
let nxDomainResolver = NxDomainResolver()
let compositeResolver = CompositeResolver(handlers: [localhostResolver, nxDomainResolver])
let hostsQueryValidator = StandardQueryValidator(handler: compositeResolver)
let dnsServer: DNSServer = DNSServer(handler: hostsQueryValidator, log: log)
log.info(
"starting DNS resolver for localhost",
metadata: [
"host": "\(Self.listenAddress)",
"port": "\(Self.localhostDNSPort)",
]
)
try await dnsServer.run(host: Self.listenAddress, port: Self.localhostDNSPort)
return .success(())
} catch {
return .failure(error)
}
}
for await result in group {
switch result {
case .success():
continue
case .failure(let error):
log.error("API server task failed: \(error)")
}
}
}
} catch {
log.error(
"helper failed",
metadata: [
"name": "\(commandName)",
"error": "\(error)",
])
APIServer.exit(withError: error)
}
}
private func initializePluginLoader(log: Logger) throws -> PluginLoader {
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
// plugins built into the application installed as a Unix-like application
let installRootPluginsURL =
installRoot
.appendingPathComponent("libexec")
.appendingPathComponent("container")
.appendingPathComponent("plugins")
.standardized
let pluginDirectories = [
userPluginsURL,
installRootPluginsURL,
].compactMap { $0 }
let pluginFactories: [PluginFactory] = [
DefaultPluginFactory(logger: log),
AppBundlePluginFactory(logger: log),
]
for pluginDirectory in pluginDirectories {
log.info("discovered plugin directory", metadata: ["path": "\(pluginDirectory.path(percentEncoded: false))"])
}
return try PluginLoader(
appRoot: appRoot,
installRoot: installRoot,
logRoot: logRoot,
pluginDirectories: pluginDirectories,
pluginFactories: pluginFactories,
log: log
)
}
// First load all of the plugins we can find. Then just expose
// the handlers for clients to do whatever they want.
private func initializePlugins(
pluginLoader: PluginLoader,
log: Logger,
routes: inout [XPCRoute: XPCServer.RouteHandler]
) async throws {
log.info("initializing plugins")
let bootPlugins = pluginLoader.findPlugins().filter { $0.shouldBoot }
let service = PluginsService(pluginLoader: pluginLoader, log: log)
try await service.loadAll(bootPlugins)
let harness = PluginsHarness(service: service, log: log)
routes[XPCRoute.pluginGet] = harness.get
routes[XPCRoute.pluginList] = harness.list
routes[XPCRoute.pluginLoad] = harness.load
routes[XPCRoute.pluginUnload] = harness.unload
routes[XPCRoute.pluginRestart] = harness.restart
}
private func initializeHealthCheckService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) {
log.info("initializing health check service")
let svc = HealthCheckHarness(
appRoot: appRoot,
installRoot: installRoot,
logRoot: logRoot,
log: log
)
routes[XPCRoute.ping] = svc.ping
}
private func initializeKernelService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws {
log.info("initializing kernel service")
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 initializeContainersService(
pluginLoader: PluginLoader,
containerSystemConfig: ContainerSystemConfig,
log: Logger,
routes: inout [XPCRoute: XPCServer.RouteHandler]
) throws -> ContainersService {
log.info("initializing containers service")
let service = try ContainersService(
appRoot: appRoot,
pluginLoader: pluginLoader,
containerSystemConfig: containerSystemConfig,
log: log,
debugHelpers: debug
)
let harness = ContainersHarness(service: service, log: log)
routes[XPCRoute.containerList] = harness.list
routes[XPCRoute.containerCreate] = harness.create
routes[XPCRoute.containerDelete] = harness.delete
routes[XPCRoute.containerLogs] = harness.logs
routes[XPCRoute.containerBootstrap] = harness.bootstrap
routes[XPCRoute.containerDial] = harness.dial
routes[XPCRoute.containerStop] = harness.stop
routes[XPCRoute.containerStartProcess] = harness.startProcess
routes[XPCRoute.containerCreateProcess] = harness.createProcess
routes[XPCRoute.containerResize] = harness.resize
routes[XPCRoute.containerWait] = harness.wait
routes[XPCRoute.containerKill] = harness.kill
routes[XPCRoute.containerStats] = harness.stats
routes[XPCRoute.containerDiskUsage] = harness.diskUsage
routes[XPCRoute.containerExport] = harness.export
return service
}
private func initializeNetworksService(
pluginLoader: PluginLoader,
containersService: ContainersService,
containerSystemConfig: ContainerSystemConfig,
log: Logger,
routes: inout [XPCRoute: XPCServer.RouteHandler]
) async throws -> NetworksService {
log.info("initializing networks service")
// TODO: This goes away when we convert our roots to FilePath
let appPath = FilePath(appRoot.absolutePath())
let resourceRoot = appPath.appending("networks")
let service = try await NetworksService(
pluginLoader: pluginLoader,
resourceRoot: resourceRoot,
containersService: containersService,
log: log,
debugHelpers: debug
)
let defaultNetwork = try await service.list()
.filter { $0.isBuiltin }
.first
if defaultNetwork == nil {
// FIXME: default network should be configurable elsewhere
let config = try NetworkConfiguration(
id: NetworkClient.defaultNetworkName,
mode: .nat,
ipv4Subnet: containerSystemConfig.network.subnet,
ipv6Subnet: containerSystemConfig.network.subnetv6,
labels: try .init([ResourceLabelKeys.role: ResourceRoleValues.builtin]),
pluginInfo: NetworkPluginInfo(plugin: "container-network-vmnet")
)
_ = try await service.create(configuration: config)
}
let harness = NetworksHarness(service: service, log: log)
// network creation is not supported pre-macOS 26 (refer to AllocationOnlyVmnetNetwork)
if #available(macOS 26, *) {
routes[XPCRoute.networkCreate] = harness.create
}
routes[XPCRoute.networkList] = harness.list
routes[XPCRoute.networkDelete] = harness.delete
return service
}
private func initializeVolumeService(
containersService: ContainersService,
log: Logger,
routes: inout [XPCRoute: XPCServer.RouteHandler]
) throws -> VolumesService {
log.info("initializing volume service")
// TODO: This goes away when we convert our roots to FilePath
let appPath = FilePath(appRoot.absolutePath())
let resourceRoot = appPath.appending("volumes")
let service = try VolumesService(resourceRoot: resourceRoot, containersService: containersService, log: log)
let harness = VolumesHarness(service: service, log: log)
routes[XPCRoute.volumeCreate] = harness.create
routes[XPCRoute.volumeDelete] = harness.delete
routes[XPCRoute.volumeList] = harness.list
routes[XPCRoute.volumeInspect] = harness.inspect
routes[XPCRoute.volumeDiskUsage] = harness.diskUsage
return service
}
private func initializeDiskUsageService(
containersService: ContainersService,
volumesService: VolumesService,
log: Logger,
routes: inout [XPCRoute: XPCServer.RouteHandler]
) throws {
log.info("initializing disk usage service")
let service = DiskUsageService(
containersService: containersService,
volumesService: volumesService,
log: log
)
let harness = DiskUsageHarness(service: service, log: log)
routes[XPCRoute.systemDiskUsage] = harness.get
}
}
}