Files
Kathryn Baldauf 16f2630126 Add initial native builder code (#399)
We're working on making a pure swift container image build system that
leverages containerization. This PR represents our initial design and
initial work towards this goal.

The native builder is still in active development and most of the
implementation has not been started or completed. We will be opening a
series of issues that represent various (but not necessarily all) pieces
of work that need to be done here.

There are docs included in this PR that describe the overall design of
each component and outline some of our goals. The easiest way to view
the docs by themselves (since this is a massive PR) is to look at the
docs commit in the `Commits` tab.

We'd love any feedback! 

@wlan0

---------

Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
2025-07-31 13:13:20 -07:00

366 lines
9.8 KiB
Swift

//===----------------------------------------------------------------------===//
// 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 a command execution operation (RUN in Dockerfile).
///
/// Design rationale:
/// - Captures all execution context (env, mounts, user, workdir)
/// - Supports advanced features like secrets and SSH forwarding
/// - Shell vs exec form preserved for accurate execution
/// - Network and security controls built-in
public struct ExecOperation: Operation, Hashable {
public static let operationKind = OperationKind.exec
public var operationKind: OperationKind { Self.operationKind }
/// The command to execute
public let command: Command
/// Environment variables
public let environment: Environment
/// Mounts (cache, bind, tmpfs, secret, ssh)
public let mounts: [Mount]
/// Working directory
public let workingDirectory: String?
/// User to run as
public let user: User?
/// Network mode
public let network: NetworkMode
/// Security options
public let security: SecurityOptions
/// Operation metadata
public let metadata: OperationMetadata
public init(
command: Command,
environment: Environment = .empty,
mounts: [Mount] = [],
workingDirectory: String? = nil,
user: User? = nil,
network: NetworkMode = .default,
security: SecurityOptions = .default,
metadata: OperationMetadata = OperationMetadata()
) {
self.command = command
self.environment = environment
self.mounts = mounts
self.workingDirectory = workingDirectory
self.user = user
self.network = network
self.security = security
self.metadata = metadata
}
public func accept<V: OperationVisitor>(_ visitor: V) throws -> V.Result {
try visitor.visit(self)
}
}
// MARK: - Command
/// Represents a command to execute.
///
/// Design rationale:
/// - Preserves shell vs exec form from Dockerfile
/// - Shell form uses default shell with command as string
/// - Exec form bypasses shell for direct execution
public enum Command: Hashable, Sendable {
/// Shell form: command is passed to shell
case shell(String)
/// Exec form: direct execution without shell
case exec([String])
/// The command string for display
public var displayString: String {
switch self {
case .shell(let cmd):
return cmd
case .exec(let args):
return args.joined(separator: " ")
}
}
/// Arguments for execution
public var arguments: [String] {
switch self {
case .shell(let cmd):
return ["/bin/sh", "-c", cmd]
case .exec(let args):
return args
}
}
}
// MARK: - Environment
/// Environment variables for execution.
///
/// Design rationale:
/// - Preserves order for predictable overwrites
/// - Supports both literal values and build args
/// - Case-sensitive on Linux, case-insensitive on Windows
public struct Environment: Hashable, Sendable {
public let variables: [(key: String, value: EnvironmentValue)]
public init(_ variables: [(key: String, value: EnvironmentValue)] = []) {
self.variables = variables
}
public static let empty = Environment()
/// Get effective environment as dictionary (last value wins)
public var effectiveEnvironment: [String: String] {
var result: [String: String] = [:]
for (key, value) in variables {
if case .literal(let str) = value {
result[key] = str
}
}
return result
}
}
// Custom Hashable conformance for Environment
extension Environment {
public static func == (lhs: Environment, rhs: Environment) -> Bool {
guard lhs.variables.count == rhs.variables.count else { return false }
for (index, (lkey, lvalue)) in lhs.variables.enumerated() {
let (rkey, rvalue) = rhs.variables[index]
if lkey != rkey || lvalue != rvalue {
return false
}
}
return true
}
public func hash(into hasher: inout Hasher) {
for (key, value) in variables {
hasher.combine(key)
hasher.combine(value)
}
}
}
/// Environment variable value.
public enum EnvironmentValue: Hashable, Sendable {
/// Literal string value
case literal(String)
/// Reference to build argument
case buildArg(String)
/// Expansion with default
case expansion(name: String, default: String?)
}
// MARK: - Mounts
/// Represents a mount in the container.
///
/// Design rationale:
/// - Type-safe mount specifications
/// - Supports all Dockerfile mount types
/// - Extensible for future mount types
public struct Mount: Hashable, Sendable {
public let type: MountType
public let target: String?
public let envTarget: String?
public let source: MountSource?
public let options: MountOptions
public init(
type: MountType,
target: String? = nil,
envTarget: String? = nil,
source: MountSource? = nil,
options: MountOptions = MountOptions()
) {
self.type = type
self.target = target
self.envTarget = envTarget
self.source = source
self.options = options
}
}
/// Type of mount.
public enum MountType: String, Hashable, Sendable {
case bind
case cache
case tmpfs
case secret
case ssh
}
/// Source of mount data.
public enum MountSource: Hashable, Sendable {
/// Local path
case local(String)
/// From another stage
case stage(StageReference, path: String)
/// From image
case image(ImageReference, path: String)
/// Build context
case context(String, path: String)
/// Secret by ID
case secret(String)
/// SSH agent socket
case sshAgent
}
/// Mount options.
public struct MountOptions: Hashable, Sendable {
public let readOnly: Bool
public let uid: UInt32?
public let gid: UInt32?
public let mode: UInt32?
public let size: UInt32? // For tmpfs
public let sharing: SharingMode? // For cache mounts
public let required: Bool?
public init(
readOnly: Bool = false,
uid: UInt32? = nil,
gid: UInt32? = nil,
mode: UInt32? = nil,
size: UInt32? = nil,
sharing: SharingMode? = nil,
required: Bool? = nil,
) {
self.readOnly = readOnly
self.uid = uid
self.gid = gid
self.mode = mode
self.size = size
self.sharing = sharing
self.required = required
}
}
// MARK: - User
/// User specification for command execution.
public enum User: Hashable, Sendable {
/// User by name
case named(String)
/// User by UID
case uid(UInt32)
/// User and group
case userGroup(user: String, group: String)
/// UID and GID
case uidGid(uid: UInt32, gid: UInt32)
}
// MARK: - Network
/// Network mode for execution.
public enum NetworkMode: String, Hashable, Sendable {
/// Default network
case `default`
/// No network
case none
/// Host network
case host
}
// MARK: - Security
/// Security options for execution.
public struct SecurityOptions: Hashable, Sendable {
public let privileged: Bool
public let capabilities: SecurityCapabilities?
public let seccompProfile: String?
public let apparmorProfile: String?
public let noNewPrivileges: Bool
public init(
privileged: Bool = false,
capabilities: SecurityCapabilities? = nil,
seccompProfile: String? = nil,
apparmorProfile: String? = nil,
noNewPrivileges: Bool = true
) {
self.privileged = privileged
self.capabilities = capabilities
self.seccompProfile = seccompProfile
self.apparmorProfile = apparmorProfile
self.noNewPrivileges = noNewPrivileges
}
public static let `default` = SecurityOptions()
}
/// Linux capabilities.
public struct SecurityCapabilities: Hashable, Sendable {
public let add: Set<String>
public let drop: Set<String>
public init(add: Set<String> = [], drop: Set<String> = []) {
self.add = add
self.drop = drop
}
}
// MARK: - Codable
extension ExecOperation: Codable {}
extension Command: Codable {}
extension Environment: Codable {
private struct EnvVar: Codable {
let key: String
let value: EnvironmentValue
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let envVars = try container.decode([EnvVar].self)
self.variables = envVars.map { ($0.key, $0.value) }
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let envVars = variables.map { EnvVar(key: $0.key, value: $0.value) }
try container.encode(envVars)
}
}
extension EnvironmentValue: Codable {}
extension Mount: Codable {}
extension MountType: Codable {}
extension MountSource: Codable {}
extension MountOptions: Codable {}
extension User: Codable {}
extension NetworkMode: Codable {}
extension SecurityOptions: Codable {}
extension SecurityCapabilities: Codable {}