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
2 changes: 2 additions & 0 deletions Sources/ContainerBuild/Builder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import NIOHPACK
import NIOHTTP2

public struct Builder: Sendable {
public static let builderContainerId = "buildkit"

let client: BuilderClientProtocol
let clientAsync: BuilderClientAsyncProtocol
let group: EventLoopGroup
Expand Down
17 changes: 10 additions & 7 deletions Sources/ContainerCommands/Builder/BuilderStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,7 @@ extension Application {
useRosetta ? nil : "--enable-qemu",
].compactMap { $0 }

let id = "buildkit"
try ContainerAPIClient.Utility.validEntityName(id)
try ContainerAPIClient.Utility.validEntityName(Builder.builderContainerId)

let image = try await ClientImage.fetch(
reference: builderImage,
Expand Down Expand Up @@ -244,9 +243,9 @@ extension Application {
memory: memory
)

var config = ContainerConfiguration(id: id, image: imageDesc, process: processConfig)
var config = ContainerConfiguration(id: Builder.builderContainerId, image: imageDesc, process: processConfig)
config.resources = resources
config.labels = ["com.apple.container.resource.role": "builder"]
config.labels = [ResourceLabelKeys.role: ResourceRoleValues.builder]
config.mounts = [
.init(
type: .tmpfs,
Expand All @@ -264,11 +263,15 @@ extension Application {
// Enable Rosetta only if the user didn't ask to disable it
config.rosetta = useRosetta

let network = try await ClientNetwork.get(id: ClientNetwork.defaultNetworkName)
guard case .running(_, let networkStatus) = network else {
guard let defaultNetwork = try await ClientNetwork.builtin else {
throw ContainerizationError(.invalidState, message: "default network is not present")
}
guard case .running(_, let networkStatus) = defaultNetwork else {
throw ContainerizationError(.invalidState, message: "default network is not running")
}
config.networks = [AttachmentConfiguration(network: network.id, options: AttachmentOptions(hostname: id))]
config.networks = [
AttachmentConfiguration(network: defaultNetwork.id, options: AttachmentOptions(hostname: Builder.builderContainerId))
]
let subnet = networkStatus.ipv4Subnet
let nameserver = IPv4Address(subnet.lower.value + 1).description
let nameservers = dnsNameservers.isEmpty ? [nameserver] : dnsNameservers
Expand Down
20 changes: 11 additions & 9 deletions Sources/ContainerCommands/Network/NetworkDelete.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,22 @@ extension Application {
let uniqueNetworkNames = Set<String>(networkNames)
let networks: [NetworkState]

if uniqueNetworkNames.contains(ClientNetwork.defaultNetworkName) {
throw ContainerizationError(
.invalidArgument,
message: "cannot delete the default network"
)
}

if all {
networks = try await ClientNetwork.list()
.filter { $0.id != ClientNetwork.defaultNetworkName }
.filter { !$0.isBuiltin }
} else {
networks = try await ClientNetwork.list()
.filter { c in
uniqueNetworkNames.contains(c.id)
guard uniqueNetworkNames.contains(c.id) else {
return false
}
guard !c.isBuiltin else {
throw ContainerizationError(
.invalidArgument,
message: "cannot delete a builtin network: \(c.id)"
)
}
return true
}

// If one of the networks requested isn't present lets throw. We don't need to do
Expand Down
2 changes: 1 addition & 1 deletion Sources/ContainerCommands/Network/NetworkPrune.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ extension Application.NetworkCommand {
}

let networksToPrune = allNetworks.filter { network in
network.id != ClientNetwork.defaultNetworkName && !networksInUse.contains(network.id)
!network.isBuiltin && !networksInUse.contains(network.id)
}

var prunedNetworks = [String]()
Expand Down
2 changes: 1 addition & 1 deletion Sources/ContainerPersistence/DefaultsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import ContainerizationError
import Foundation

public enum DefaultsStore {
private static let userDefaultDomain = "com.apple.container.defaults"
public static let userDefaultDomain = "com.apple.container.defaults"

public enum Keys: String {
case buildRosetta = "build.rosetta"
Expand Down
58 changes: 58 additions & 0 deletions Sources/ContainerResource/Common/ManagedResource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//===----------------------------------------------------------------------===//
// 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 Foundation

/// Common properties for all managed resources.
public protocol ManagedResource: Identifiable, Sendable, Codable {
/// A 64 byte hexadecimal string, assigned by the system, that uniquely
/// identifies the resource.
var id: String { get }

/// A user assigned name that shall be unique within the namespace of
/// the resource category. If the user does not assign a name, this value
/// shall be the same as the system-assigned identifier.
var name: String { get }

/// The time at which the system created the resource.
var creationDate: Date { get }

/// Key-value properties for the resource. The user and system may both
/// make use of labels to read and write annotations or other metadata.
/// A good practice is to use
var labels: [String: String] { get }

/// Generates a unique resource ID value.
static func generateId() -> String

/// Returns true only if the specified resource name is syntactically valid.
static func nameValid(_ name: String) -> Bool
}

extension ManagedResource {
/// Generate a random identifier that has the format of an ASCII SHA-256 hash.
public static func randomId() -> String {
(0..<2)
.map { _ in UInt128.random(in: 0...UInt128.max) }
.map { String($0, radix: 16).padding(toLength: 32, withPad: "0", startingAt: 0) }
.joined()
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

This extension on [String: String] feels a bit too broad - it adds isBuiltin to every dictionary in the codebase. Could we create a ResourceLabels typealias or wrapper type instead? Something like:

public typealias ResourceLabels = [String: String]
extension ResourceLabels {
    public var isBuiltin: Bool { ... }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

see the FIXME?

Copy link
Contributor

Choose a reason for hiding this comment

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

see the FIXME?

yeah I saw i thought in this pr if we are implementing we can do it at same time. prolly wont have to do it later. if you are thinking we can implement later.

// FIXME: This moves to ManagedResource and/or a ResourceLabels typealias eventually.
extension [String: String] {
public var isBuiltin: Bool { self.contains { $0 == ResourceLabelKeys.role && $1 == ResourceRoleValues.builtin } }
}
33 changes: 33 additions & 0 deletions Sources/ContainerResource/Common/ResourceLabels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice clean API! Could we add some usage examples in the doc comments? It would help clarify when to use builtin vs builder and what other roles we might support in the future

/// System-defined keys for resource labels.
public struct ResourceLabelKeys {
/// Indicates a owner of a resource managed by a plugin.
public static let plugin = "com.apple.container.plugin"

/// Indicates a resource with a reserved or dedicated purpose.
public static let role = "com.apple.container.resource.role"
}

/// System-defined values for resource the resource role label.
public struct ResourceRoleValues {
/// Indicates a container that can build images.
public static let builder = "builder"

/// Indicates a system-created resource that cannot be deleted by the user.
public static let builtin = "builtin"
}
12 changes: 8 additions & 4 deletions Sources/ContainerResource/Network/NetworkState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,19 @@ public enum NetworkState: Codable, Sendable {

public var id: String {
switch self {
case .created(let configuration): configuration.id
case .running(let configuration, _): configuration.id
case .created(let config), .running(let config, _): config.id
}
}

public var creationDate: Date {
switch self {
case .created(let configuration): configuration.creationDate
case .running(let configuration, _): configuration.creationDate
case .created(let config), .running(let config, _): config.creationDate
}
}

public var isBuiltin: Bool {
switch self {
case .created(let config), .running(let config, _): config.labels.isBuiltin
}
}
}
8 changes: 6 additions & 2 deletions Sources/Helpers/APIServer/APIServer+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,14 @@ extension APIServer {
)

let defaultNetwork = try await service.list()
.filter { $0.id == ClientNetwork.defaultNetworkName }
.filter { $0.isBuiltin }
.first
if defaultNetwork == nil {
let config = try NetworkConfiguration(id: ClientNetwork.defaultNetworkName, mode: .nat)
let config = try NetworkConfiguration(
id: ClientNetwork.defaultNetworkName,
mode: .nat,
labels: [ResourceLabelKeys.role: ResourceRoleValues.builtin]
)
_ = try await service.create(configuration: config)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,11 @@ extension ClientNetwork {
request.set(key: .networkId, value: id)
try await client.send(request)
}

/// Retrieve the builtin network.
public static var builtin: NetworkState? {
get async throws {
try await list().first { $0.isBuiltin }
}
}
}
21 changes: 17 additions & 4 deletions Sources/Services/ContainerAPIService/Client/Utility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,12 @@ public struct Utility {
}
config.networks = []
} else {
config.networks = try getAttachmentConfigurations(containerId: config.id, networks: parsedNetworks)
let builtinNetworkId = try await ClientNetwork.builtin?.id
config.networks = try getAttachmentConfigurations(
containerId: config.id,
builtinNetworkId: builtinNetworkId,
networks: parsedNetworks
)
for attachmentConfiguration in config.networks {
let network: NetworkState = try await ClientNetwork.get(id: attachmentConfiguration.network)
guard case .running(_, _) = network else {
Expand Down Expand Up @@ -244,7 +249,11 @@ public struct Utility {
return (config, kernel)
}

static func getAttachmentConfigurations(containerId: String, networks: [Parser.ParsedNetwork]) throws -> [AttachmentConfiguration] {
static func getAttachmentConfigurations(
containerId: String,
builtinNetworkId: String?,
networks: [Parser.ParsedNetwork]
) throws -> [AttachmentConfiguration] {
// Validate MAC addresses if provided
for network in networks {
if let mac = network.macAddress {
Expand All @@ -268,7 +277,7 @@ public struct Utility {

guard networks.isEmpty else {
// Check if this is only the default network with properties (e.g., MAC address)
let isOnlyDefaultNetwork = networks.count == 1 && networks[0].name == ClientNetwork.defaultNetworkName
let isOnlyDefaultNetwork = networks.count == 1 && networks[0].name == builtinNetworkId

// networks may only be specified for macOS 26+ (except for default network with properties)
if !isOnlyDefaultNetwork {
Expand All @@ -292,8 +301,12 @@ public struct Utility {
)
}
}

// if no networks specified, attach to the default network
return [AttachmentConfiguration(network: ClientNetwork.defaultNetworkName, options: AttachmentOptions(hostname: fqdn ?? containerId, macAddress: nil))]
guard let builtinNetworkId else {
throw ContainerizationError(.invalidState, message: "builtin network is not present")
}
return [AttachmentConfiguration(network: builtinNetworkId, options: AttachmentOptions(hostname: fqdn ?? containerId, macAddress: nil))]
}

private static func getKernel(management: Flags.Management) async throws -> Kernel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,17 @@ public actor NetworksService {
self.networkPlugin = networkPlugin

let configurations = try await store.list()
for configuration in configurations {
Copy link
Contributor

Choose a reason for hiding this comment

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

This migration logic will run on every startup even after all networks are migrated. Are we considereing adding a one-time migration flag or checking a schema version? Running this check for every network on every boot seems wasteful once everyone's migrated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a very inexpensive operation; there's no need to pursue optimizations here.

A one-time migration flag requires extra effort from the user which means more support burden for us.

We're not investing effort in schema versioning at this point as the API and on-disk schema are very fluid and we expect to be breaking things for a little while longer.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a very inexpensive operation; there's no need to pursue optimizations here.

A one-time migration flag requires extra effort from the user which means more support burden for us.

We're not investing effort in schema versioning at this point as the API and on-disk schema are very fluid and we expect to be breaking things for a little while longer.

make sense, thanks for explaining! Given the schema is still evolving, keeping it simple is definitely the right decision here.

for var configuration in configurations {
// Ensure the network with id "default" is marked as builtin.
if configuration.id == ClientNetwork.defaultNetworkName {
let role = configuration.labels[ResourceLabelKeys.role]
if role == nil || role != ResourceRoleValues.builtin {
configuration.labels[ResourceLabelKeys.role] = ResourceRoleValues.builtin
try await store.update(configuration)
}
}

// Start up the network.
do {
try await registerService(configuration: configuration)
} catch {
Expand Down Expand Up @@ -186,17 +196,17 @@ public actor NetworksService {
"id": "\(id)"
])

// basic sanity checks on network itself
if id == ClientNetwork.defaultNetworkName {
throw ContainerizationError(.invalidArgument, message: "cannot delete system subnet \(ClientNetwork.defaultNetworkName)")
}

guard let networkState = networkStates[id] else {
throw ContainerizationError(.notFound, message: "no network for id \(id)")
}

// basic sanity checks on network itself
if networkState.isBuiltin {
throw ContainerizationError(.invalidArgument, message: "cannot delete builtin network: \(id)")
}

guard case .running = networkState else {
throw ContainerizationError(.invalidState, message: "cannot delete subnet \(id) in state \(networkState.state)")
throw ContainerizationError(.invalidState, message: "cannot delete network \(id) in state \(networkState.state)")
}

// prevent container operations while we atomically check and delete
Expand Down
Loading