//===----------------------------------------------------------------------===// // Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. // // 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 /// Represents metadata operations (ENV, LABEL, ARG, etc.). /// /// Design rationale: /// - Unified handling of all metadata modifications /// - No filesystem changes, only configuration /// - Supports both build-time and runtime metadata public struct MetadataOperation: Operation, Hashable { public static let operationKind = OperationKind.metadata public var operationKind: OperationKind { Self.operationKind } /// Type of metadata operation public let action: MetadataAction /// Operation metadata public let metadata: OperationMetadata public init( action: MetadataAction, metadata: OperationMetadata = OperationMetadata() ) { self.action = action self.metadata = metadata } public func accept(_ visitor: V) throws -> V.Result { try visitor.visit(self) } } // MARK: - Metadata Action /// Type of metadata modification. /// /// Design rationale: /// - Each action is self-contained with its data /// - Clear separation between build and runtime metadata /// - Extensible for future metadata types public enum MetadataAction: Sendable { /// Set environment variable (ENV) case setEnv(key: String, value: EnvironmentValue) /// Set multiple environment variables case setEnvBatch([(key: String, value: EnvironmentValue)]) /// Set label (LABEL) case setLabel(key: String, value: String) /// Set multiple labels case setLabelBatch([String: String]) /// Define build argument (ARG) case declareArg(name: String, defaultValue: String?) /// Set exposed ports (EXPOSE) case expose(ports: [PortSpec]) /// Set working directory (WORKDIR) case setWorkdir(path: String) /// Set user (USER) case setUser(user: User) /// Set entrypoint (ENTRYPOINT) case setEntrypoint(command: Command) /// Set default command (CMD) case setCmd(command: Command) /// Set shell (SHELL) case setShell(shell: [String]) /// Set healthcheck (HEALTHCHECK) case setHealthcheck(healthcheck: Healthcheck?) /// Set stop signal (STOPSIGNAL) case setStopSignal(signal: String) /// Add volume (VOLUME) case addVolume(path: String) /// Add onbuild trigger (ONBUILD) case addOnBuild(instruction: String) } // MARK: - Port Specification /// Port exposure specification. /// /// Design rationale: /// - Supports TCP/UDP/SCTP /// - Range support for multiple ports /// - Documentation via description public struct PortSpec: Hashable, Sendable { public enum NetworkProtocol: String, Hashable, Sendable { case tcp case udp case sctp } /// Port number or range start public let port: UInt16 /// Range end (if range) public let endPort: UInt16? /// Protocol public let `protocol`: NetworkProtocol /// Human-readable description public let description: String? public init( port: UInt16, endPort: UInt16? = nil, protocol: NetworkProtocol = .tcp, description: String? = nil ) { self.port = port self.endPort = endPort self.`protocol` = `protocol` self.description = description } /// String representation (e.g., "80/tcp", "8000-8100/udp") public var stringValue: String { guard let endPort = endPort else { return "\(port)/\(`protocol`.rawValue)" } return "\(port)-\(endPort)/\(`protocol`.rawValue)" } } // MARK: - Healthcheck /// Container healthcheck configuration. /// /// Design rationale: /// - Matches Docker/OCI healthcheck spec /// - Flexible timing configuration /// - Supports disabling inherited healthchecks public struct Healthcheck: Hashable, Sendable { /// Test command public let test: HealthcheckTest /// Time between checks public let interval: TimeInterval? /// Timeout for each check public let timeout: TimeInterval? /// Initial delay before first check public let startPeriod: TimeInterval? /// Number of retries before unhealthy public let retries: Int? public init( test: HealthcheckTest, interval: TimeInterval? = nil, timeout: TimeInterval? = nil, startPeriod: TimeInterval? = nil, retries: Int? = nil ) { self.test = test self.interval = interval self.timeout = timeout self.startPeriod = startPeriod self.retries = retries } } /// Healthcheck test specification. public enum HealthcheckTest: Hashable, Sendable { /// No healthcheck (NONE) case none /// Command to run (CMD) case command(Command) /// Command with shell (CMD-SHELL) case shell(String) } // MARK: - Hashable & Equatable extension MetadataAction: Hashable, Equatable { public static func == (lhs: MetadataAction, rhs: MetadataAction) -> Bool { switch (lhs, rhs) { case (.setEnv(let lk, let lv), .setEnv(let rk, let rv)): return lk == rk && lv == rv case (.setEnvBatch(let l), .setEnvBatch(let r)): guard l.count == r.count else { return false } for (index, (lk, lv)) in l.enumerated() { let (rk, rv) = r[index] if lk != rk || lv != rv { return false } } return true case (.setLabel(let lk, let lv), .setLabel(let rk, let rv)): return lk == rk && lv == rv case (.setLabelBatch(let l), .setLabelBatch(let r)): return l == r case (.declareArg(let ln, let ld), .declareArg(let rn, let rd)): return ln == rn && ld == rd case (.expose(let l), .expose(let r)): return l == r case (.setWorkdir(let l), .setWorkdir(let r)): return l == r case (.setUser(let l), .setUser(let r)): return l == r case (.setEntrypoint(let l), .setEntrypoint(let r)): return l == r case (.setCmd(let l), .setCmd(let r)): return l == r case (.setShell(let l), .setShell(let r)): return l == r case (.setHealthcheck(let l), .setHealthcheck(let r)): return l == r case (.setStopSignal(let l), .setStopSignal(let r)): return l == r case (.addVolume(let l), .addVolume(let r)): return l == r case (.addOnBuild(let l), .addOnBuild(let r)): return l == r default: return false } } public func hash(into hasher: inout Hasher) { switch self { case .setEnv(let key, let value): hasher.combine(0) hasher.combine(key) hasher.combine(value) case .setEnvBatch(let vars): hasher.combine(1) for (key, value) in vars { hasher.combine(key) hasher.combine(value) } case .setLabel(let key, let value): hasher.combine(2) hasher.combine(key) hasher.combine(value) case .setLabelBatch(let labels): hasher.combine(3) hasher.combine(labels) case .declareArg(let name, let defaultValue): hasher.combine(4) hasher.combine(name) hasher.combine(defaultValue) case .expose(let ports): hasher.combine(5) hasher.combine(ports) case .setWorkdir(let path): hasher.combine(6) hasher.combine(path) case .setUser(let user): hasher.combine(7) hasher.combine(user) case .setEntrypoint(let command): hasher.combine(8) hasher.combine(command) case .setCmd(let command): hasher.combine(9) hasher.combine(command) case .setShell(let shell): hasher.combine(10) hasher.combine(shell) case .setHealthcheck(let healthcheck): hasher.combine(11) hasher.combine(healthcheck) case .setStopSignal(let signal): hasher.combine(12) hasher.combine(signal) case .addVolume(let path): hasher.combine(13) hasher.combine(path) case .addOnBuild(let instruction): hasher.combine(14) hasher.combine(instruction) } } } // MARK: - Codable extension MetadataOperation: Codable {} extension MetadataAction: Codable { private enum CodingKeys: String, CodingKey { case type case key case value case envVars case labels case name case defaultValue case ports case path case user case command case shell case healthcheck case signal case instruction } private struct EnvVar: Codable { let key: String let value: EnvironmentValue } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .setEnv(let key, let value): try container.encode("setEnv", forKey: .type) try container.encode(key, forKey: .key) try container.encode(value, forKey: .value) case .setEnvBatch(let vars): try container.encode("setEnvBatch", forKey: .type) let envVars = vars.map { EnvVar(key: $0.key, value: $0.value) } try container.encode(envVars, forKey: .envVars) case .setLabel(let key, let value): try container.encode("setLabel", forKey: .type) try container.encode(key, forKey: .key) try container.encode(value, forKey: .value) case .setLabelBatch(let labels): try container.encode("setLabelBatch", forKey: .type) try container.encode(labels, forKey: .labels) case .declareArg(let name, let defaultValue): try container.encode("declareArg", forKey: .type) try container.encode(name, forKey: .name) try container.encode(defaultValue, forKey: .defaultValue) case .expose(let ports): try container.encode("expose", forKey: .type) try container.encode(ports, forKey: .ports) case .setWorkdir(let path): try container.encode("setWorkdir", forKey: .type) try container.encode(path, forKey: .path) case .setUser(let user): try container.encode("setUser", forKey: .type) try container.encode(user, forKey: .user) case .setEntrypoint(let command): try container.encode("setEntrypoint", forKey: .type) try container.encode(command, forKey: .command) case .setCmd(let command): try container.encode("setCmd", forKey: .type) try container.encode(command, forKey: .command) case .setShell(let shell): try container.encode("setShell", forKey: .type) try container.encode(shell, forKey: .shell) case .setHealthcheck(let healthcheck): try container.encode("setHealthcheck", forKey: .type) try container.encode(healthcheck, forKey: .healthcheck) case .setStopSignal(let signal): try container.encode("setStopSignal", forKey: .type) try container.encode(signal, forKey: .signal) case .addVolume(let path): try container.encode("addVolume", forKey: .type) try container.encode(path, forKey: .path) case .addOnBuild(let instruction): try container.encode("addOnBuild", forKey: .type) try container.encode(instruction, forKey: .instruction) } } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(String.self, forKey: .type) switch type { case "setEnv": let key = try container.decode(String.self, forKey: .key) let value = try container.decode(EnvironmentValue.self, forKey: .value) self = .setEnv(key: key, value: value) case "setEnvBatch": let envVars = try container.decode([EnvVar].self, forKey: .envVars) self = .setEnvBatch(envVars.map { ($0.key, $0.value) }) case "setLabel": let key = try container.decode(String.self, forKey: .key) let value = try container.decode(String.self, forKey: .value) self = .setLabel(key: key, value: value) case "setLabelBatch": let labels = try container.decode([String: String].self, forKey: .labels) self = .setLabelBatch(labels) case "declareArg": let name = try container.decode(String.self, forKey: .name) let defaultValue = try container.decodeIfPresent(String.self, forKey: .defaultValue) self = .declareArg(name: name, defaultValue: defaultValue) case "expose": let ports = try container.decode([PortSpec].self, forKey: .ports) self = .expose(ports: ports) case "setWorkdir": let path = try container.decode(String.self, forKey: .path) self = .setWorkdir(path: path) case "setUser": let user = try container.decode(User.self, forKey: .user) self = .setUser(user: user) case "setEntrypoint": let command = try container.decode(Command.self, forKey: .command) self = .setEntrypoint(command: command) case "setCmd": let command = try container.decode(Command.self, forKey: .command) self = .setCmd(command: command) case "setShell": let shell = try container.decode([String].self, forKey: .shell) self = .setShell(shell: shell) case "setHealthcheck": let healthcheck = try container.decodeIfPresent(Healthcheck.self, forKey: .healthcheck) self = .setHealthcheck(healthcheck: healthcheck) case "setStopSignal": let signal = try container.decode(String.self, forKey: .signal) self = .setStopSignal(signal: signal) case "addVolume": let path = try container.decode(String.self, forKey: .path) self = .addVolume(path: path) case "addOnBuild": let instruction = try container.decode(String.self, forKey: .instruction) self = .addOnBuild(instruction: instruction) default: throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown MetadataAction type: \(type)") } } } extension PortSpec: Codable { enum CodingKeys: String, CodingKey { case port case endPort case `protocol` case description } } extension PortSpec.NetworkProtocol: Codable {} extension Healthcheck: Codable {} extension HealthcheckTest: Codable {}