Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ $(STAGING_DIR):
@install "$(BUILD_BIN_DIR)/container" "$(join $(STAGING_DIR), bin/container)"
@install "$(BUILD_BIN_DIR)/container-apiserver" "$(join $(STAGING_DIR), bin/container-apiserver)"
@install "$(BUILD_BIN_DIR)/container-runtime-linux" "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/bin/container-runtime-linux)"
@install Sources/Plugins/RuntimeLinux/config.json "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/config.json)"
@install Sources/Plugins/RuntimeLinux/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-runtime-linux/config.toml)"
@install "$(BUILD_BIN_DIR)/container-network-vmnet" "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/bin/container-network-vmnet)"
@install Sources/Plugins/NetworkVmnet/config.json "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/config.json)"
@install Sources/Plugins/NetworkVmnet/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-network-vmnet/config.toml)"
@install "$(BUILD_BIN_DIR)/container-core-images" "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/bin/container-core-images)"
@install Sources/Plugins/CoreImages/config.json "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/config.json)"
@install Sources/Plugins/CoreImages/config.toml "$(join $(STAGING_DIR), libexec/container/plugins/container-core-images/config.toml)"

@echo Install update script
@install scripts/update-container.sh "$(join $(STAGING_DIR), bin/update-container.sh)"
Expand Down
11 changes: 10 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ let package = Package(
.package(url: "https://github.com/grpc/grpc-swift.git", from: "1.26.0"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin.git", from: "1.1.0"),
.package(url: "https://github.com/mattt/swift-toml.git", from: "2.0.0"),
],
targets: [
.executableTarget(
Expand Down Expand Up @@ -219,7 +220,7 @@ let package = Package(
"ContainerXPC",
],
path: "Sources/Plugins/CoreImages",
exclude: ["config.json"]
exclude: ["config.toml"]
),
.target(
name: "ContainerImagesService",
Expand Down Expand Up @@ -268,7 +269,7 @@ let package = Package(
"ContainerXPC",
],
path: "Sources/Plugins/NetworkVmnet",
exclude: ["config.json"]
exclude: ["config.toml"]
),
.target(
name: "ContainerNetworkService",
Expand Down Expand Up @@ -318,7 +319,7 @@ let package = Package(
"ContainerXPC",
],
path: "Sources/Plugins/RuntimeLinux",
exclude: ["config.json"]
exclude: ["config.toml"]
),
.target(
name: "ContainerSandboxService",
Expand Down Expand Up @@ -388,6 +389,7 @@ let package = Package(
.product(name: "Logging", package: "swift-log"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "TOML", package: "swift-toml"),
"ContainerVersion",
]
),
Expand Down
4 changes: 2 additions & 2 deletions Sources/APIServer/APIServer+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ extension APIServer {
].compactMap { $0 }

let pluginFactories: [PluginFactory] = [
DefaultPluginFactory(),
AppBundlePluginFactory(),
DefaultPluginFactory(logger: log),
AppBundlePluginFactory(logger: log),
]

for pluginDirectory in pluginDirectories {
Expand Down
4 changes: 2 additions & 2 deletions Sources/ContainerCommands/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ public struct Application: AsyncLoggableCommand {
].compactMap { $0 }

let pluginFactories: [any PluginFactory] = [
DefaultPluginFactory(),
AppBundlePluginFactory(),
DefaultPluginFactory(logger: bootstrapLogger),
AppBundlePluginFactory(logger: bootstrapLogger),
]

guard let systemHealth = try? await ClientHealthCheck.ping(timeout: .seconds(10)) else {
Expand Down
32 changes: 25 additions & 7 deletions Sources/ContainerPlugin/PluginConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,20 @@

//
import Foundation
import TOML

/// 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
/// file named `config.toml` (or fall back to the legacy `config.json` file) 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.toml`. An example of a valid plugin directory structure would be
/// $ tree foobar
/// foobar
/// ├── bin
/// │ └── foobar
/// └── config.json
/// └── config.toml (`config.json`)
public struct PluginConfig: Sendable, Codable {
/// Categories of services that can be offered through plugins.
public enum DaemonPluginType: String, Sendable, Codable {
Expand Down Expand Up @@ -93,13 +94,21 @@ public struct PluginConfig: Sendable, Codable {
/// Services configuration. Specify nil for a CLI plugin, and an empty array for
/// that does not publish any XPC services.
public let servicesConfig: ServicesConfig?

public init(abstract: String, author: String?, servicesConfig: ServicesConfig?) {
self.abstract = abstract
self.author = author
self.servicesConfig = servicesConfig
}
}

extension PluginConfig {
public var isCLI: Bool { self.servicesConfig == nil }
}

extension PluginConfig {
/// Initialize from a config file, selecting the decoder based on file extension.
/// Supports `.toml` (via TOMLDecoder) and `.json` (via JSONDecoder).
public init?(configURL: URL) throws {
let fm = FileManager.default
if !fm.fileExists(atPath: configURL.path) {
Expand All @@ -110,7 +119,16 @@ extension PluginConfig {
return nil
}

let decoder: JSONDecoder = JSONDecoder()
self = try decoder.decode(PluginConfig.self, from: data)
switch configURL.pathExtension {
case "toml":
guard let content = String(data: data, encoding: .utf8) else {
return nil
}
self = try TOMLDecoder().decode(PluginConfig.self, from: content)
case "json":
self = try JSONDecoder().decode(PluginConfig.self, from: data)
default:
return nil
}
}
}
48 changes: 34 additions & 14 deletions Sources/ContainerPlugin/PluginFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
//===----------------------------------------------------------------------===//

import Foundation

private let configFilename: String = "config.json"
import Logging

/// Describes the configuration and binary file locations for a plugin.
public protocol PluginFactory: Sendable {
Expand All @@ -28,13 +27,36 @@ public protocol PluginFactory: Sendable {

/// Default layout which uses a Unix-like structure.
public struct DefaultPluginFactory: PluginFactory {
public init() {}
// Order matters: earlier entries take priority during config file discovery.
private static let configFilenames: [String] = ["config.toml", "config.json"]
private let logger: Logger

public init(logger: Logger) {
self.logger = logger
}

/// Returns the URL of the first config file found in `directory`, preferring TOML over JSON.
static func findConfigURL(in directory: URL, logger: Logger) -> URL? {
let fm = FileManager.default
for filename in configFilenames {
let url = directory.appending(path: filename)
if fm.fileExists(atPath: url.path) {
if url.pathExtension == "json" {
logger.warning(
"Plugin using legacy config.json; please migrate to config.toml",
metadata: ["path": "\(url.path)"]
)
}
return url
}
}
return nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe throw notFound instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It more or less conforms to the previous contract, in which create returns Plugin?. If we want to use ContainerizationError.notFound we will have to add Containerization as a dependency of ContainerPlugin.

This comment was marked as spam.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd only need to import ContainerizationError as a dependency, which is already used elsewhere in the same package

import ContainerizationError

}

public func create(installURL: URL) throws -> Plugin? {
let fm = FileManager.default

let configURL = installURL.appending(path: configFilename)
guard fm.fileExists(atPath: configURL.path) else {
guard let configURL = Self.findConfigURL(in: installURL, logger: logger) else {
return nil
}

Expand Down Expand Up @@ -63,18 +85,21 @@ public struct DefaultPluginFactory: PluginFactory {
/// Layout which uses a macOS application bundle structure.
public struct AppBundlePluginFactory: PluginFactory {
private static let appSuffix = ".app"
private let logger: Logger

public init() {}
public init(logger: Logger) {
self.logger = logger
}

public func create(installURL: URL) throws -> Plugin? {
let fm = FileManager.default

let configURL =
let contentResources =
installURL
.appending(path: "Contents")
.appending(path: "Resources")
.appending(path: configFilename)
guard fm.fileExists(atPath: configURL.path) else {

guard let configURL = DefaultPluginFactory.findConfigURL(in: contentResources, logger: logger) else {
return nil
}

Expand All @@ -96,11 +121,6 @@ public struct AppBundlePluginFactory: PluginFactory {
return nil
}

let contentResources =
installURL
.appending(path: "Contents")
.appending(path: "Resources")

var resourceURL: URL? = nil
if case let url = contentResources.appending(path: "resources"), fm.fileExists(atPath: url.path) {
resourceURL = url
Expand Down
16 changes: 0 additions & 16 deletions Sources/Plugins/CoreImages/config.json

This file was deleted.

12 changes: 12 additions & 0 deletions Sources/Plugins/CoreImages/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
abstract = "Core image management plugin"
author = "Apple"
version = 0.1

[servicesConfig]
loadAtBoot = true
runAtLoad = false
defaultArguments = []

[[servicesConfig.services]]
type = "core"
description = "Provide an XPC interface to interact with an image store."
15 changes: 0 additions & 15 deletions Sources/Plugins/NetworkVmnet/config.json

This file was deleted.

11 changes: 11 additions & 0 deletions Sources/Plugins/NetworkVmnet/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
abstract = "vmnet network management plugin"
author = "Apple"
version = 0.1

[servicesConfig]
loadAtBoot = false
runAtLoad = true
defaultArguments = []

[[servicesConfig.services]]
type = "network"
15 changes: 0 additions & 15 deletions Sources/Plugins/RuntimeLinux/config.json

This file was deleted.

11 changes: 11 additions & 0 deletions Sources/Plugins/RuntimeLinux/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
abstract = "Linux container runtime plugin"
author = "Apple"
version = 0.1

[servicesConfig]
loadAtBoot = false
runAtLoad = false
defaultArguments = []

[[servicesConfig.services]]
type = "runtime"
Loading