Files
container/Sources/ContainerPlugin/PluginConfig.swift
T
Kathryn Baldauf 9032d73d07 Change behavior of the default arguments in the plugin config (#1063)
## Type of Change
- [x] New feature  
- [x] Breaking change

## Motivation and Context
This PR changes the behavior of the `defaultArguments` field in the
service plugin config. Previously, `defaultArguments` was functioning as
a way to indicate how to start a plugin in the event that the plugin is
loaded at boot (such as for the `container-core-images plugin`).
However, we now follow a convention where all plugins have a "start"
command that is used when launching the plugin, so this
`defaultArguments` field wasn't really providing much. Instead, there
are use cases where we may want to set default values to pass to a
plugin. This PR repurposes the `defaultArguments` field for those use
cases.

As an example use case, there are scenarios where someone may want to
use the AllocationOnlyVmnetNetwork even when running on macOS 26+. This
PR adds the ability to pass in a command line option to the vmnet
network plugin to specify that request. Combined with the
`defaultArguments` plugin config change, a user may choose to set that
field to ["--variant", "allocationOnly"] in the
`container-network-vmnet-config.json` to use AllocationOnlyVmnetNetwork
by default for all networks.

## Testing
- [x] Tested locally
2026-01-19 19:22:55 -08:00

117 lines
5.3 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025-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 Foundation
/// PluginConfig details all of the fields to describe and register a plugin.
/// A plugin is registered by creating a subdirectory `<application-root>/user-plugins`,
/// where the name of the subdirectory is the name of the plugin, and then placing a
/// file named `config.json` inside with the schema below.
/// If `services` is filled in then there MUST be a binary named matching the plugin name
/// in a `bin` subdirectory inside the same directory as the `config.json`.
/// An example of a valid plugin directory structure would be
/// $ tree foobar
/// foobar
/// ├── bin
/// │ └── foobar
/// └── config.json
public struct PluginConfig: Sendable, Codable {
/// Categories of services that can be offered through plugins.
public enum DaemonPluginType: String, Sendable, Codable {
/// A runtime plugin provides an XPC API through which the lifecycle
/// of a **single** container can be managed.
/// A runtime daemon plugin would typically also have a counterpart
/// CLI plugin which knows how to talk to the API exposed by the runtime plugin.
/// The API server ensures that a single instance of the plugin is configured
/// for a given container such that the client can communicate with it given an instance id.
case runtime
/// A network plugin provides an XPC API through which IP address allocations on a given
/// network can be managed. The API server ensures that a single instance
/// of this plugin is configured for a given network. Similar to the runtime plugin, it typically
/// would be accompanied by a CLI plugin that knows how to communicate with the XPC API
/// given an instance id.
case network
/// A core plugin provides an XPC API to manage a given type of resource.
/// The API server ensures that there exist only a single running instance
/// of this plugin type. A core plugin can be thought of a singleton whose lifecycle
/// is tied to that of the API server. Core plugins can be used to expand the base functionality
/// provided by the API server. As with the other plugin types, it maybe associated with a client
/// side plugin that communicates with the XPC service exposed by the daemon plugin.
case core
/// Reserved for future use. Currently there is no difference between a core and auxiliary daemon plugin.
case auxiliary
}
// An XPC service that the plugin publishes.
public struct Service: Sendable, Codable {
/// The type of the service the daemon is exposing.
/// One plugin can expose multiple services of different types.
///
/// The plugin MUST expose a MachService at
/// `com.apple.container.{type}.{name}.[{id}]` for
/// each service that it exposes.
public let type: DaemonPluginType
/// Optional description of this service.
public let description: String?
}
/// Descriptor for the services that the plugin offers.
public struct ServicesConfig: Sendable, Codable {
/// Load the plugin into launchd when the API server starts.
public let loadAtBoot: Bool
/// Launch the plugin binary as soon as it loads into launchd.
public let runAtLoad: Bool
/// The service types that the plugin provides.
public let services: [Service]
/// An optional parameter that include any command line arguments
/// that must be passed to the plugin binary when it is loaded.
public let defaultArguments: [String]
}
/// Short description of the plugin surface. This will be displayed as the
/// help-text for CLI plugins, and will be returned in API calls to view loaded
/// plugins from the daemon.
public let abstract: String
/// Author of the plugin. This is solely metadata.
public let author: String?
/// Services configuration. Specify nil for a CLI plugin, and an empty array for
/// that does not publish any XPC services.
public let servicesConfig: ServicesConfig?
}
extension PluginConfig {
public var isCLI: Bool { self.servicesConfig == nil }
}
extension PluginConfig {
public init?(configURL: URL) throws {
let fm = FileManager.default
if !fm.fileExists(atPath: configURL.path) {
return nil
}
guard let data = fm.contents(atPath: configURL.path) else {
return nil
}
let decoder: JSONDecoder = JSONDecoder()
self = try decoder.decode(PluginConfig.self, from: data)
}
}