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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ integration: init-block
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIVersion || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLINetwork || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunLifecycle || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunCapabilities || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIExecCommand || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLICreateCommand || exit_code=1 ; \
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunCommand1 || exit_code=1 ; \
Expand Down
1 change: 1 addition & 0 deletions Sources/ContainerCommands/Builder/BuilderStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ extension Application {
var config = ContainerConfiguration(id: Builder.builderContainerId, image: imageDesc, process: processConfig)
config.resources = resources
config.labels = [ResourceLabelKeys.role: ResourceRoleValues.builder]
config.capAdd = ["ALL"]
config.mounts = [
.init(
type: .tmpfs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public struct ContainerConfiguration: Sendable, Codable {
public var readOnly: Bool = false
/// Whether to use a minimal init process inside the container.
public var useInit: Bool = false
/// Linux capabilities to add (normalized CAP_* strings, or "ALL").
public var capAdd: [String] = []
/// Linux capabilities to drop (normalized CAP_* strings, or "ALL").
public var capDrop: [String] = []

enum CodingKeys: String, CodingKey {
case id
Expand All @@ -73,6 +77,8 @@ public struct ContainerConfiguration: Sendable, Codable {
case ssh
case readOnly
case useInit
case capAdd
case capDrop
}

/// Create a configuration from the supplied Decoder, initializing missing
Expand Down Expand Up @@ -104,6 +110,8 @@ public struct ContainerConfiguration: Sendable, Codable {
ssh = try container.decodeIfPresent(Bool.self, forKey: .ssh) ?? false
readOnly = try container.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
useInit = try container.decodeIfPresent(Bool.self, forKey: .useInit) ?? false
capAdd = try container.decodeIfPresent([String].self, forKey: .capAdd) ?? []
capDrop = try container.decodeIfPresent([String].self, forKey: .capDrop) ?? []
}

public struct DNSConfiguration: Sendable, Codable {
Expand Down
16 changes: 16 additions & 0 deletions Sources/Services/ContainerAPIService/Client/Flags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ public struct Flags {

public init(
arch: String,
capAdd: [String],
capDrop: [String],
cidfile: String,
detach: Bool,
dns: Flags.DNS,
Expand All @@ -193,6 +195,8 @@ public struct Flags {
volumes: [String]
) {
self.arch = arch
self.capAdd = capAdd
self.capDrop = capDrop
self.cidfile = cidfile
self.detach = detach
self.dns = dns
Expand Down Expand Up @@ -222,6 +226,18 @@ public struct Flags {
@Option(name: .shortAndLong, help: "Set arch if image can target multiple architectures")
public var arch: String = Arch.hostArchitecture().rawValue

@Option(
name: .customLong("cap-add"),
help: .init("Add a Linux capability (e.g. CAP_NET_RAW, or ALL)", valueName: "cap")
)
public var capAdd: [String] = []

@Option(
name: .customLong("cap-drop"),
help: .init("Drop a Linux capability (e.g. CAP_NET_RAW, or ALL)", valueName: "cap")
)
public var capDrop: [String] = []

@Option(name: .long, help: "Write the container ID to the path provided")
public var cidfile = ""

Expand Down
34 changes: 34 additions & 0 deletions Sources/Services/ContainerAPIService/Client/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,40 @@ public struct Parser {
return parsed
}

// MARK: Capabilities

/// Parse and validate --cap-add / --cap-drop arguments.
/// Returns normalized uppercase CAP_* strings.
public static func capabilities(capAdd: [String], capDrop: [String]) throws -> (capAdd: [String], capDrop: [String]) {
var normalizedAdd: [String] = []
for cap in capAdd {
let upper = cap.uppercased()
if upper == "ALL" {
normalizedAdd.append("ALL")
continue
}
// Validate using CapabilityName from the containerization lib
_ = try CapabilityName(rawValue: upper)
// Normalize to CAP_ prefixed form
let normalized = upper.hasPrefix("CAP_") ? upper : "CAP_\(upper)"
normalizedAdd.append(normalized)
}

var normalizedDrop: [String] = []
for cap in capDrop {
let upper = cap.uppercased()
if upper == "ALL" {
normalizedDrop.append("ALL")
continue
}
_ = try CapabilityName(rawValue: upper)
let normalized = upper.hasPrefix("CAP_") ? upper : "CAP_\(upper)"
normalizedDrop.append(normalized)
}

return (normalizedAdd, normalizedDrop)
}

// MARK: Miscellaneous

public static func parseBool(string: String) -> Bool? {
Expand Down
4 changes: 4 additions & 0 deletions Sources/Services/ContainerAPIService/Client/Utility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ public struct Utility {
config.readOnly = management.readOnly
config.useInit = management.useInit

let caps = try Parser.capabilities(capAdd: management.capAdd, capDrop: management.capDrop)
config.capAdd = caps.capAdd
config.capDrop = caps.capDrop

if let runtime = management.runtime {
config.runtimeHandler = runtime
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,10 @@ public actor SandboxService {
soft: $0.soft
)
}
czConfig.process.capabilities = try Self.effectiveCapabilities(
capAdd: config.capAdd,
capDrop: config.capDrop
)
switch process.user {
case .raw(let name):
czConfig.process.user = .init(
Expand Down Expand Up @@ -984,6 +988,10 @@ public actor SandboxService {
soft: $0.soft
)
}
proc.capabilities = try Self.effectiveCapabilities(
capAdd: containerConfig.capAdd,
capDrop: containerConfig.capDrop
)
switch config.user {
case .raw(let name):
proc.user = .init(
Expand All @@ -1006,6 +1014,36 @@ public actor SandboxService {
return proc
}

/// Compute effective Linux capabilities from the OCI default set, capAdd, and capDrop:
/// 1. If "ALL" in capDrop, start empty; otherwise start from OCI defaults.
/// 2. If "ALL" in capAdd, replace with all caps; otherwise add individual caps.
/// 3. Remove individual capDrop entries (skipping "ALL" sentinel).
private static func effectiveCapabilities(capAdd: [String], capDrop: [String]) throws -> Containerization.LinuxCapabilities {
// Step 1: Determine base set
var caps: Set<CapabilityName>
if capDrop.contains("ALL") {
caps = []
} else {
caps = Set(Containerization.LinuxCapabilities.defaultOCICapabilities.effective)
}

// Step 2: Process adds
if capAdd.contains("ALL") {
caps = Set(CapabilityName.allCases)
} else {
for name in capAdd {
caps.insert(try CapabilityName(rawValue: name))
}
}

// Step 3: Remove individual drops (skip "ALL" sentinel)
for name in capDrop where name != "ALL" {
caps.remove(try CapabilityName(rawValue: name))
}

return Containerization.LinuxCapabilities(capabilities: Array(caps))
}

private nonisolated func closeHandle(_ handle: Int32) throws {
guard close(handle) == 0 else {
guard let errCode = POSIXErrorCode(rawValue: errno) else {
Expand Down
Loading
Loading