Skip to content
Open
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: 1 addition & 1 deletion Sources/APIServer/APIServer+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ extension APIServer {
let config = try NetworkConfiguration(
id: ClientNetwork.defaultNetworkName,
mode: .nat,
labels: [ResourceLabelKeys.role: ResourceRoleValues.builtin],
labels: try .init([ResourceLabelKeys.role: ResourceRoleValues.builtin]),
pluginInfo: NetworkPluginInfo(plugin: "container-network-vmnet")
)
_ = try await service.create(configuration: config)
Expand Down
2 changes: 1 addition & 1 deletion Sources/ContainerCommands/Network/NetworkCreate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ extension Application {
public init() {}

public func run() async throws {
let parsedLabels = Utility.parseKeyValuePairs(labels)
let parsedLabels = try ResourceLabels(Utility.parseKeyValuePairs(labels))
let mode: NetworkMode = hostOnly ? .hostOnly : .nat
let config = try NetworkConfiguration(
id: self.name,
Expand Down
36 changes: 36 additions & 0 deletions Sources/ContainerResource/Common/ApplicationError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//

/// Protocol for errors with a stable code and structured metadata.
/// This allows the client to present the error as it chooses.

import Collections

public protocol AppError: Error {
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.

Is this going to be the used for the highest level and for generic type of errors (that all resources will follow)? I kind of worked on creating more lower level, managed resource specific handling approach (that each resource will follow but will vary per resource).

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.

I don't know the answer to that for sure yet. I feel like we'll want to try as much as we can in the coming month and evolve to something sensible by the next release.

What I was going for with this error is:

  • Supports a caused-by chain.
  • Supports structured log output
  • Delegates presentation to an error receiver that is responsible for making sense of chained errors, compound errors, and error metadata and mapping it to one or more log messages.
  • For easier compatibility, doesn't rely on enum for error typing.

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.

Sounds good to me. I am going to take a step back for now and let you lead this. Excited to see what it becomes! Happy to jump back in when it’s ready to be used for managed resources.

var code: AppErrorCode { get }
var metadata: OrderedDictionary<String, String> { get }
var underlyingError: Error? { get }
}

public struct AppErrorCode: RawRepresentable, Hashable, Sendable {
public let rawValue: String

public init(rawValue: String) {
self.rawValue = rawValue
}

public static let invalidArgument = AppErrorCode(rawValue: "invalid_argument")
}
4 changes: 2 additions & 2 deletions Sources/ContainerResource/Common/ManagedResource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public protocol ManagedResource: Identifiable, Sendable, Codable {
/// 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 }
var labels: ResourceLabels { get }

/// Generates a unique resource ID value.
static func generateId() -> String
Expand All @@ -53,6 +53,6 @@ extension ManagedResource {
}

// FIXME: This moves to ManagedResource and/or a ResourceLabels typealias eventually.
extension [String: String] {
extension ResourceLabels {
public var isBuiltin: Bool { self.contains { $0 == ResourceLabelKeys.role && $1 == ResourceRoleValues.builtin } }
}
84 changes: 84 additions & 0 deletions Sources/ContainerResource/Common/ResourceLabels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,90 @@
// limitations under the License.
//===----------------------------------------------------------------------===//

import Collections

/// Metadata for a managed resource.
public struct ResourceLabels: Sendable, Equatable {
public static let keyLengthMax = 128

public static let labelLengthMax = 4096

public let dictionary: [String: String]

public struct LabelError: AppError {
public var code: AppErrorCode

public var metadata: OrderedDictionary<String, String>

public var underlyingError: (any Error)? { nil }
}

public init() {
dictionary = [:]
}

public init(_ labels: [String: String]) throws {
for (key, value) in labels {
try Self.validateLabel(key: key, value: value)
}
self.dictionary = labels
}

public static func validateLabelKey(_ key: String) throws {
guard key.count <= Self.keyLengthMax else {
throw LabelError(code: .invalidLabelKeyLength, metadata: ["key": key, "maxLength": "\(Self.keyLengthMax)"])
}
let dockerPattern = #/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$/#
let ociPattern = #/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?:/(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*))*$/#
let dockerMatch = !key.ranges(of: dockerPattern).isEmpty
let ociMatch = !key.ranges(of: ociPattern).isEmpty
guard dockerMatch || ociMatch else {
throw LabelError(code: .invalidLabelKeyContent, metadata: ["key": key])
}
}

public static func validateLabel(key: String, value: String) throws {
try validateLabelKey(key)
let fullLabel = "\(key)=\(value)"
guard fullLabel.count <= labelLengthMax else {
throw LabelError(code: .invalidLabelLength, metadata: ["label": fullLabel, "maxLength": "\(Self.labelLengthMax)"])
}
}
}

extension ResourceLabels: Codable {
public func encode(to encoder: Encoder) throws {
try dictionary.encode(to: encoder)
}

public init(from decoder: Decoder) throws {
let dict = try [String: String](from: decoder)
try self.init(dict)
}
}

extension ResourceLabels: Collection {
public typealias Index = Dictionary<String, String>.Index
public typealias Element = Dictionary<String, String>.Element

public var startIndex: Index { dictionary.startIndex }
public var endIndex: Index { dictionary.endIndex }

public subscript(position: Index) -> Element { dictionary[position] }
public func index(after i: Index) -> Index { dictionary.index(after: i) }

// Direct key access
public subscript(key: String) -> String? {
get { dictionary[key] }
}
}

extension AppErrorCode {
public static let invalidLabelKeyContent = AppErrorCode(rawValue: "invalid_label_key_content")
public static let invalidLabelKeyLength = AppErrorCode(rawValue: "invalid_label_key_length")
public static let invalidLabelLength = AppErrorCode(rawValue: "invalid_label_length")
}

/// System-defined keys for resource labels.
public struct ResourceLabelKeys {
/// Indicates a owner of a resource managed by a plugin.
Expand Down
44 changes: 7 additions & 37 deletions Sources/ContainerResource/Network/NetworkConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,22 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
public let ipv6Subnet: CIDRv6?

/// Key-value labels for the network.
public var labels: [String: String] = [:]
/// Resource labels should not be mutated, except while building a network configurations.
public let labels: ResourceLabels

/// Details about the network plugin that manages this network.
/// FIXME: This field only needs to be optional while we wait for the field
/// to be proliferated to most users when they update container.
public var pluginInfo: NetworkPluginInfo?
public let pluginInfo: NetworkPluginInfo?

/// Creates a network configuration
public init(
id: String,
mode: NetworkMode,
ipv4Subnet: CIDRv4? = nil,
ipv6Subnet: CIDRv6? = nil,
labels: [String: String] = [:],
pluginInfo: NetworkPluginInfo
labels: ResourceLabels = .init(),
pluginInfo: NetworkPluginInfo?
) throws {
self.id = id
self.creationDate = Date()
Expand Down Expand Up @@ -98,7 +99,8 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
ipv4Subnet = try subnetText.map { try CIDRv4($0) }
ipv6Subnet = try container.decodeIfPresent(String.self, forKey: .ipv6Subnet)
.map { try CIDRv6($0) }
labels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:]
let decodedLabels = try container.decodeIfPresent([String: String].self, forKey: .labels) ?? [:]
labels = try .init(decodedLabels)
pluginInfo = try container.decodeIfPresent(NetworkPluginInfo.self, forKey: .pluginInfo)
try validate()
}
Expand All @@ -120,28 +122,6 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
guard id.isValidNetworkID() else {
throw ContainerizationError(.invalidArgument, message: "invalid network ID: \(id)")
}

for (key, value) in labels {
try validateLabel(key: key, value: value)
}
}

/// TODO: Extract when we clean up client dependencies.
private func validateLabel(key: String, value: String) throws {
let keyLengthMax = 128
let labelLengthMax = 4096
guard key.count <= keyLengthMax else {
throw ContainerizationError(.invalidArgument, message: "invalid label, key length is greater than \(keyLengthMax): \(key)")
}

guard key.isValidLabelKey() else {
throw ContainerizationError(.invalidArgument, message: "invalid label key: \(key)")
}

let fullLabel = "\(key)=\(value)"
guard fullLabel.count <= labelLengthMax else {
throw ContainerizationError(.invalidArgument, message: "invalid label, key length is greater than \(labelLengthMax): \(fullLabel)")
}
}
}

Expand All @@ -151,14 +131,4 @@ extension String {
let pattern = #"^[a-z0-9](?:[a-z0-9._-]{0,61}[a-z0-9])?$"#
return self.range(of: pattern, options: .regularExpression) != nil
}

/// Ensure label key conforms to OCI or Docker label guidelines.
/// TODO: Extract when we clean up client dependencies.
fileprivate func isValidLabelKey() -> Bool {
let dockerPattern = #/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$/#
let ociPattern = #/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?:/(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*))*$/#
let dockerMatch = !self.ranges(of: dockerPattern).isEmpty
let ociMatch = !self.ranges(of: ociPattern).isEmpty
return dockerMatch || ociMatch
}
}
10 changes: 5 additions & 5 deletions Sources/ContainerResource/Registry/RegistryResource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,22 @@ public struct RegistryResource: ManagedResource {
///
/// This value must be a valid DNS hostname or IPv6 address, optionally
/// followed by a port number (e.g., "docker.io", "localhost:5000", "[::1]:5000").
public var name: String
public let name: String

/// The username used for authentication with this registry.
public let username: String

/// The time at which the system created this registry resource.
public var creationDate: Date
public let creationDate: Date

/// The time at which the registry resource was last modified.
public var modificationDate: Date
public let modificationDate: Date

/// Key-value properties for the resource.
///
/// The user and system may both make use of labels to read and write
/// annotations or other metadata.
public var labels: [String: String]
public let labels: ResourceLabels

/// Validates a registry hostname according to OCI distribution specification.
///
Expand Down Expand Up @@ -94,7 +94,7 @@ public struct RegistryResource: ManagedResource {
username: String,
creationDate: Date,
modificationDate: Date,
labels: [String: String] = [:]
labels: ResourceLabels = .init()
) {
self.id = hostname
self.name = hostname
Expand Down
Loading
Loading