Skip to content

Commit 9fd5f2d

Browse files
authored
Merge branch 'main' into add-new-network-metadata
2 parents ed05998 + ae27910 commit 9fd5f2d

File tree

18 files changed

+731
-32
lines changed

18 files changed

+731
-32
lines changed

.github/workflows/common.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
name: container project - common jobs
22

3+
permissions:
4+
contents: read
5+
36
on:
47
workflow_call:
58
inputs:

.github/workflows/docs-release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Manual workflow for releasing docs ad-hoc. Workflow can only be run for main or release branches.
22
# Workflow does NOT publish a release of container.
33
name: Deploy application website
4+
5+
permissions:
6+
contents: read
7+
48
on:
59
workflow_dispatch:
610

.github/workflows/merge-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
name: container project - merge build
22

3+
permissions:
4+
contents: read
5+
36
on:
47
push:
58
branches:

.github/workflows/pr-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
name: container project - PR build
22

3+
permissions:
4+
contents: read
5+
36
on:
47
pull_request:
58
types: [opened, reopened, synchronize]

.github/workflows/release-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
name: container project - release build
22

3+
permissions:
4+
contents: read
5+
36
on:
47
push:
58
tags:

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
name: container project - release build
22

3+
permissions:
4+
contents: read
5+
36
on:
47
push:
58
tags:

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ integration: init-block
181181
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIBuildBase || exit_code=1 ; \
182182
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIVolumes || exit_code=1 ; \
183183
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIKernelSet || exit_code=1 ; \
184+
$(SWIFT) test -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --filter TestCLIAnonymousVolumes || exit_code=1 ; \
184185
echo Ensuring apiserver stopped after the CLI integration tests ; \
185186
scripts/ensure-container-stopped.sh ; \
186187
exit $${exit_code} ; \

Sources/ContainerClient/Core/Volume.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import Foundation
1818

19-
/// A named volume that can be mounted in containers.
19+
/// A named or anonymous volume that can be mounted in containers.
2020
public struct Volume: Sendable, Codable, Equatable, Identifiable {
2121
// id of the volume.
2222
public var id: String { name }
@@ -45,7 +45,7 @@ public struct Volume: Sendable, Codable, Equatable, Identifiable {
4545
createdAt: Date = Date(),
4646
labels: [String: String] = [:],
4747
options: [String: String] = [:],
48-
sizeInBytes: UInt64? = nil,
48+
sizeInBytes: UInt64? = nil
4949
) {
5050
self.name = name
5151
self.driver = driver
@@ -58,6 +58,16 @@ public struct Volume: Sendable, Codable, Equatable, Identifiable {
5858
}
5959
}
6060

61+
extension Volume {
62+
/// Reserved label key for marking anonymous volumes
63+
public static let anonymousLabel = "com.apple.container.resource.anonymous"
64+
65+
/// Whether this is an anonymous volume (detected via label)
66+
public var isAnonymous: Bool {
67+
labels[Self.anonymousLabel] != nil
68+
}
69+
}
70+
6171
/// Error types for volume operations.
6272
public enum VolumeError: Error, LocalizedError {
6373
case volumeNotFound(String)
@@ -95,9 +105,14 @@ public struct VolumeStorage {
95105

96106
do {
97107
let regex = try Regex(volumeNamePattern)
98-
return name.contains(regex)
108+
return (try? regex.wholeMatch(in: name)) != nil
99109
} catch {
100110
return false
101111
}
102112
}
113+
114+
/// Generates an anonymous volume name with UUID format
115+
public static func generateAnonymousVolumeName() -> String {
116+
UUID().uuidString.lowercased()
117+
}
103118
}

Sources/ContainerClient/Parser.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ public struct ParsedVolume {
2525
public let name: String
2626
public let destination: String
2727
public let options: [String]
28+
public let isAnonymous: Bool
2829

29-
public init(name: String, destination: String, options: [String] = []) {
30+
public init(name: String, destination: String, options: [String] = [], isAnonymous: Bool = false) {
3031
self.name = name
3132
self.destination = destination
3233
self.options = options
34+
self.isAnonymous = isAnonymous
3335
}
3436
}
3537

@@ -368,8 +370,7 @@ public struct Parser {
368370
case "tmpfs":
369371
fs.type = Filesystem.FSType.tmpfs
370372
case "volume":
371-
// Volume type will be set later in source parsing when we create the actual volume filesystem
372-
break
373+
isVolume = true
373374
default:
374375
throw ContainerizationError(.invalidArgument, message: "unsupported mount type \(val)")
375376
}
@@ -416,7 +417,6 @@ public struct Parser {
416417
}
417418

418419
// This is a named volume
419-
isVolume = true
420420
volumeName = val
421421
fs.source = val
422422
case "tmpfs":
@@ -434,11 +434,19 @@ public struct Parser {
434434
guard isVolume else {
435435
return .filesystem(fs)
436436
}
437+
438+
// If it's a volume type but no source was provided, create an anonymous volume
439+
let isAnonymous = volumeName.isEmpty
440+
if isAnonymous {
441+
volumeName = VolumeStorage.generateAnonymousVolumeName()
442+
}
443+
437444
return .volume(
438445
ParsedVolume(
439446
name: volumeName,
440447
destination: fs.destination,
441-
options: fs.options
448+
options: fs.options,
449+
isAnonymous: isAnonymous
442450
))
443451
}
444452

@@ -459,7 +467,19 @@ public struct Parser {
459467
let parts = vol.split(separator: ":")
460468
switch parts.count {
461469
case 1:
462-
throw ContainerizationError(.invalidArgument, message: "anonymous volumes are not supported")
470+
// Anonymous volume: -v /path
471+
// Generate a random name for the anonymous volume
472+
let anonymousName = VolumeStorage.generateAnonymousVolumeName()
473+
let destination = String(parts[0])
474+
let options: [String] = []
475+
476+
return .volume(
477+
ParsedVolume(
478+
name: anonymousName,
479+
destination: destination,
480+
options: options,
481+
isAnonymous: true
482+
))
463483
case 2, 3:
464484
let src = String(parts[0])
465485
let dst = String(parts[1])

Sources/ContainerClient/Utility.swift

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -160,19 +160,15 @@ public struct Utility {
160160
case .filesystem(let fs):
161161
resolvedMounts.append(fs)
162162
case .volume(let parsed):
163-
do {
164-
let volume = try await ClientVolume.inspect(parsed.name)
165-
let volumeMount = Filesystem.volume(
166-
name: parsed.name,
167-
format: volume.format,
168-
source: volume.source,
169-
destination: parsed.destination,
170-
options: parsed.options
171-
)
172-
resolvedMounts.append(volumeMount)
173-
} catch {
174-
throw ContainerizationError(.invalidArgument, message: "volume '\(parsed.name)' not found")
175-
}
163+
let volume = try await getOrCreateVolume(parsed: parsed)
164+
let volumeMount = Filesystem.volume(
165+
name: parsed.name,
166+
format: volume.format,
167+
source: volume.source,
168+
destination: parsed.destination,
169+
options: parsed.options
170+
)
171+
resolvedMounts.append(volumeMount)
176172
}
177173
}
178174

@@ -281,4 +277,36 @@ public struct Utility {
281277
}
282278
return result
283279
}
280+
281+
/// Gets an existing volume or creates it if it doesn't exist.
282+
/// Shows a warning for named volumes when auto-creating.
283+
private static func getOrCreateVolume(parsed: ParsedVolume) async throws -> Volume {
284+
let labels = parsed.isAnonymous ? [Volume.anonymousLabel: ""] : [:]
285+
286+
let volume: Volume
287+
do {
288+
volume = try await ClientVolume.create(
289+
name: parsed.name,
290+
driver: "local",
291+
driverOpts: [:],
292+
labels: labels
293+
)
294+
} catch let error as VolumeError {
295+
guard case .volumeAlreadyExists = error else {
296+
throw error
297+
}
298+
// Volume already exists, just inspect it
299+
volume = try await ClientVolume.inspect(parsed.name)
300+
} catch let error as ContainerizationError {
301+
// Handle XPC-wrapped volumeAlreadyExists error
302+
guard error.message.contains("already exists") else {
303+
throw error
304+
}
305+
volume = try await ClientVolume.inspect(parsed.name)
306+
}
307+
308+
// TODO: Warn user if named volume was auto-created
309+
310+
return volume
311+
}
284312
}

0 commit comments

Comments
 (0)