Use labels instead of id to discriminate the builtin network. (#1123)

- Closes #1122.
- Adds placeholder ManagedResource and unit tests. Nothing is using
these yet.
- Adds system-defined resource labels for owning plugin and resource
role. The system discriminates the builtin network using role "builtin".
- Adds builtin role when creating builtin network at startup, and
ensures that a preexisting network with ID "default" gets updated with
the role label.
- Replace all network ID checks for "default" with the builtin role
check.
- Adds "builder" role to builder VM.

## Type of Change
- [ ] Bug fix
- [x] New feature  
- [ ] Breaking change
- [ ] Documentation update

## Motivation and Context
Role and owner labels should make cross-cutting resource policy easier
to implement.

## Testing
- [x] Tested locally
- [x] Added/updated tests
- [ ] Added/updated docs
This commit is contained in:
J Logan
2026-02-02 12:24:27 -08:00
committed by GitHub
parent 1dae1cdd56
commit b3b5c3e609
13 changed files with 246 additions and 35 deletions
+2
View File
@@ -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
@@ -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,
@@ -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,
@@ -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
@@ -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
@@ -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]()
@@ -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"
@@ -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()
}
}
// 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 } }
}
@@ -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.
//===----------------------------------------------------------------------===//
/// 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"
}
@@ -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
}
}
}
@@ -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)
}
@@ -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 }
}
}
}
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -66,7 +66,17 @@ public actor NetworksService {
self.networkPlugin = networkPlugin
let configurations = try await store.list()
for configuration in configurations {
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 {
@@ -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
@@ -0,0 +1,75 @@
//===----------------------------------------------------------------------===//
// 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
import Testing
@testable import ContainerResource
struct ManagedResourceTests {
// Mock implementation to test the randomId function
struct MockManagedResource: ManagedResource {
var id: String
var name: String
var creationDate: Date
var labels: [String: String]
static func generateId() -> String {
randomId()
}
static func nameValid(_ name: String) -> Bool {
true
}
}
@Test("randomId generates valid hex string SHA256 hash format")
func testRandomIdFormat() {
let id = MockManagedResource.randomId()
// SHA256 hash is 64 hex characters (256 bits / 4 bits per hex char)
#expect(id.count == 64, "randomId should generate 64 character string")
// Should only contain valid hexadecimal characters (0-9, a-f)
let hexCharacterSet = CharacterSet(charactersIn: "0123456789abcdef")
let idCharacterSet = CharacterSet(charactersIn: id)
#expect(
hexCharacterSet.isSuperset(of: idCharacterSet),
"randomId should only contain hexadecimal characters (0-9, a-f)")
}
@Test("randomId generates unique values")
func testRandomIdUniqueness() {
// Generate multiple IDs and verify they're all different
let ids = (0..<100).map { _ in MockManagedResource.randomId() }
let uniqueIds = Set(ids)
#expect(uniqueIds.count == 100, "All generated IDs should be unique")
}
@Test("randomId uses lowercase hexadecimal")
func testRandomIdLowercase() {
let id = MockManagedResource.randomId()
// Should not contain uppercase letters
let uppercaseLetters = CharacterSet.uppercaseLetters
let idCharacterSet = CharacterSet(charactersIn: id)
#expect(
uppercaseLetters.isDisjoint(with: idCharacterSet),
"randomId should use lowercase hexadecimal characters")
}
}