Skip to content

Commit a557ce8

Browse files
authored
Add capabilities support (apple#1383)
Closes apple#1352 Containerization has had support for a bit, it was just never brought over here. It's exposed on the CLI via the classic `--cap-add` and `--cap-drop` UX.
1 parent e37dcc1 commit a557ce8

11 files changed

Lines changed: 733 additions & 0 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ integration: init-block
198198
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIVersion || exit_code=1 ; \
199199
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLINetwork || exit_code=1 ; \
200200
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunLifecycle || exit_code=1 ; \
201+
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunCapabilities || exit_code=1 ; \
201202
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIExecCommand || exit_code=1 ; \
202203
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLICreateCommand || exit_code=1 ; \
203204
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIRunCommand1 || exit_code=1 ; \

Sources/ContainerCommands/Builder/BuilderStart.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ extension Application {
247247
var config = ContainerConfiguration(id: Builder.builderContainerId, image: imageDesc, process: processConfig)
248248
config.resources = resources
249249
config.labels = [ResourceLabelKeys.role: ResourceRoleValues.builder]
250+
config.capAdd = ["ALL"]
250251
config.mounts = [
251252
.init(
252253
type: .tmpfs,

Sources/ContainerResource/Container/ContainerConfiguration.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public struct ContainerConfiguration: Sendable, Codable {
5353
public var readOnly: Bool = false
5454
/// Whether to use a minimal init process inside the container.
5555
public var useInit: Bool = false
56+
/// Linux capabilities to add (normalized CAP_* strings, or "ALL").
57+
public var capAdd: [String] = []
58+
/// Linux capabilities to drop (normalized CAP_* strings, or "ALL").
59+
public var capDrop: [String] = []
5660

5761
enum CodingKeys: String, CodingKey {
5862
case id
@@ -73,6 +77,8 @@ public struct ContainerConfiguration: Sendable, Codable {
7377
case ssh
7478
case readOnly
7579
case useInit
80+
case capAdd
81+
case capDrop
7682
}
7783

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

109117
public struct DNSConfiguration: Sendable, Codable {

Sources/Services/ContainerAPIService/Client/Flags.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ public struct Flags {
167167

168168
public init(
169169
arch: String,
170+
capAdd: [String],
171+
capDrop: [String],
170172
cidfile: String,
171173
detach: Bool,
172174
dns: Flags.DNS,
@@ -193,6 +195,8 @@ public struct Flags {
193195
volumes: [String]
194196
) {
195197
self.arch = arch
198+
self.capAdd = capAdd
199+
self.capDrop = capDrop
196200
self.cidfile = cidfile
197201
self.detach = detach
198202
self.dns = dns
@@ -222,6 +226,18 @@ public struct Flags {
222226
@Option(name: .shortAndLong, help: "Set arch if image can target multiple architectures")
223227
public var arch: String = Arch.hostArchitecture().rawValue
224228

229+
@Option(
230+
name: .customLong("cap-add"),
231+
help: .init("Add a Linux capability (e.g. CAP_NET_RAW, or ALL)", valueName: "cap")
232+
)
233+
public var capAdd: [String] = []
234+
235+
@Option(
236+
name: .customLong("cap-drop"),
237+
help: .init("Drop a Linux capability (e.g. CAP_NET_RAW, or ALL)", valueName: "cap")
238+
)
239+
public var capDrop: [String] = []
240+
225241
@Option(name: .long, help: "Write the container ID to the path provided")
226242
public var cidfile = ""
227243

Sources/Services/ContainerAPIService/Client/Parser.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,40 @@ public struct Parser {
10161016
return parsed
10171017
}
10181018

1019+
// MARK: Capabilities
1020+
1021+
/// Parse and validate --cap-add / --cap-drop arguments.
1022+
/// Returns normalized uppercase CAP_* strings.
1023+
public static func capabilities(capAdd: [String], capDrop: [String]) throws -> (capAdd: [String], capDrop: [String]) {
1024+
var normalizedAdd: [String] = []
1025+
for cap in capAdd {
1026+
let upper = cap.uppercased()
1027+
if upper == "ALL" {
1028+
normalizedAdd.append("ALL")
1029+
continue
1030+
}
1031+
// Validate using CapabilityName from the containerization lib
1032+
_ = try CapabilityName(rawValue: upper)
1033+
// Normalize to CAP_ prefixed form
1034+
let normalized = upper.hasPrefix("CAP_") ? upper : "CAP_\(upper)"
1035+
normalizedAdd.append(normalized)
1036+
}
1037+
1038+
var normalizedDrop: [String] = []
1039+
for cap in capDrop {
1040+
let upper = cap.uppercased()
1041+
if upper == "ALL" {
1042+
normalizedDrop.append("ALL")
1043+
continue
1044+
}
1045+
_ = try CapabilityName(rawValue: upper)
1046+
let normalized = upper.hasPrefix("CAP_") ? upper : "CAP_\(upper)"
1047+
normalizedDrop.append(normalized)
1048+
}
1049+
1050+
return (normalizedAdd, normalizedDrop)
1051+
}
1052+
10191053
// MARK: Miscellaneous
10201054

10211055
public static func parseBool(string: String) -> Bool? {

Sources/Services/ContainerAPIService/Client/Utility.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ public struct Utility {
251251
config.readOnly = management.readOnly
252252
config.useInit = management.useInit
253253

254+
let caps = try Parser.capabilities(capAdd: management.capAdd, capDrop: management.capDrop)
255+
config.capAdd = caps.capAdd
256+
config.capDrop = caps.capDrop
257+
254258
if let runtime = management.runtime {
255259
config.runtimeHandler = runtime
256260
}

Sources/Services/ContainerSandboxService/Server/SandboxService.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,10 @@ public actor SandboxService {
935935
soft: $0.soft
936936
)
937937
}
938+
czConfig.process.capabilities = try Self.effectiveCapabilities(
939+
capAdd: config.capAdd,
940+
capDrop: config.capDrop
941+
)
938942
switch process.user {
939943
case .raw(let name):
940944
czConfig.process.user = .init(
@@ -981,6 +985,10 @@ public actor SandboxService {
981985
soft: $0.soft
982986
)
983987
}
988+
proc.capabilities = try Self.effectiveCapabilities(
989+
capAdd: containerConfig.capAdd,
990+
capDrop: containerConfig.capDrop
991+
)
984992
switch config.user {
985993
case .raw(let name):
986994
proc.user = .init(
@@ -1003,6 +1011,37 @@ public actor SandboxService {
10031011
return proc
10041012
}
10051013

1014+
/// Compute effective Linux capabilities from the OCI default set, capAdd, and capDrop.
1015+
/// Steps are processed in order, so later steps override earlier ones:
1016+
/// 1. If "ALL" in capDrop, start empty; otherwise start from OCI defaults.
1017+
/// 2. If "ALL" in capAdd, replace with all caps (overriding step 1); otherwise add individual caps.
1018+
/// 3. Remove individual capDrop entries (skipping "ALL" sentinel).
1019+
private static func effectiveCapabilities(capAdd: [String], capDrop: [String]) throws -> Containerization.LinuxCapabilities {
1020+
// Step 1: Determine base set
1021+
var caps: Set<CapabilityName>
1022+
if capDrop.contains("ALL") {
1023+
caps = []
1024+
} else {
1025+
caps = Set(Containerization.LinuxCapabilities.defaultOCICapabilities.effective)
1026+
}
1027+
1028+
// Step 2: Process adds
1029+
if capAdd.contains("ALL") {
1030+
caps = Set(CapabilityName.allCases)
1031+
} else {
1032+
for name in capAdd {
1033+
caps.insert(try CapabilityName(rawValue: name))
1034+
}
1035+
}
1036+
1037+
// Step 3: Remove individual drops (skip "ALL" sentinel)
1038+
for name in capDrop where name != "ALL" {
1039+
caps.remove(try CapabilityName(rawValue: name))
1040+
}
1041+
1042+
return Containerization.LinuxCapabilities(capabilities: Array(caps))
1043+
}
1044+
10061045
private nonisolated func closeHandle(_ handle: Int32) throws {
10071046
guard close(handle) == 0 else {
10081047
guard let errCode = POSIXErrorCode(rawValue: errno) else {

0 commit comments

Comments
 (0)