Skip to content

Commit 3ad3289

Browse files
committed
BundleMetadata is now RuntimeConfiguration + fixing container persistance bug
1 parent d45b8ab commit 3ad3289

6 files changed

Lines changed: 135 additions & 122 deletions

File tree

Package.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,14 @@ let package = Package(
371371
"ContainerPlugin"
372372
]
373373
),
374+
.testTarget(
375+
name: "ContainerSandboxServiceTests",
376+
dependencies: [
377+
.product(name: "Containerization", package: "containerization"),
378+
"ContainerResource",
379+
"ContainerSandboxServiceClient"
380+
]
381+
),
374382
.target(
375383
name: "ContainerXPC",
376384
dependencies: [

Sources/ContainerResource/Container/Bundle.swift

Lines changed: 7 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -18,59 +18,6 @@ import Containerization
1818
import ContainerizationError
1919
import Foundation
2020

21-
public struct BundleMetadata: Codable, Sendable {
22-
static let bundleMetadataFileName = "bundle-metadata.json"
23-
24-
public let path: URL
25-
public let initialFilesystem: Filesystem
26-
public let kernel: Kernel
27-
public let containerConfiguration: ContainerConfiguration?
28-
public let containerRootFilesystem: Filesystem?
29-
public let options: ContainerCreateOptions?
30-
31-
public init(
32-
path: URL,
33-
initialFilesystem: Filesystem,
34-
kernel: Kernel,
35-
containerConfiguration: ContainerConfiguration? = nil,
36-
containerRootFilesystem: Filesystem? = nil,
37-
options: ContainerCreateOptions? = nil
38-
) {
39-
self.path = path
40-
self.initialFilesystem = initialFilesystem
41-
self.kernel = kernel
42-
self.containerConfiguration = containerConfiguration
43-
self.containerRootFilesystem = containerRootFilesystem
44-
self.options = options
45-
}
46-
47-
public var bundleMetadataPath: URL {
48-
self.path.appendingPathComponent(Self.bundleMetadataFileName)
49-
}
50-
51-
public func writeMetadata() throws {
52-
// Ensure the parent directory exists
53-
let directory = self.bundleMetadataPath.deletingLastPathComponent()
54-
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
55-
56-
let data = try JSONEncoder().encode(self)
57-
try data.write(to: self.bundleMetadataPath)
58-
}
59-
60-
public static func readMetadata(from bundlePath: URL) throws -> BundleMetadata {
61-
let metadataPath = bundlePath.appendingPathComponent(BundleMetadata.bundleMetadataFileName)
62-
guard FileManager.default.fileExists(atPath: metadataPath.path) else {
63-
throw ContainerizationError(
64-
.notFound,
65-
message: "bundle metadata file not found at path: \(metadataPath.path)"
66-
)
67-
}
68-
69-
let data = try Data(contentsOf: metadataPath)
70-
return try JSONDecoder().decode(BundleMetadata.self, from: data)
71-
}
72-
}
73-
7421
public struct Bundle: Sendable {
7522
private static let initfsFilename = "initfs.ext4"
7623
private static let kernelFilename = "kernel.json"
@@ -135,7 +82,9 @@ extension Bundle {
13582
path: URL,
13683
initialFilesystem: Filesystem,
13784
kernel: Kernel,
138-
containerConfiguration: ContainerConfiguration? = nil
85+
containerConfiguration: ContainerConfiguration? = nil,
86+
containerRootFilesystem: Filesystem? = nil,
87+
options: ContainerCreateOptions? = nil
13988
) throws -> Bundle {
14089
try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true)
14190
let kbin = path.appendingPathComponent(Self.kernelBinaryFilename)
@@ -160,26 +109,15 @@ extension Bundle {
160109
if let containerConfiguration {
161110
try bundle.write(filename: Self.containerConfigFilename, value: containerConfiguration)
162111
}
163-
return bundle
164-
}
165112

166-
public static func createFromMetadata(_ metadata: BundleMetadata) throws -> Bundle {
167-
let bundle = try create(
168-
path: metadata.path,
169-
initialFilesystem: metadata.initialFilesystem,
170-
kernel: metadata.kernel,
171-
containerConfiguration: metadata.containerConfiguration
172-
)
173-
174-
if let containerRootFs = metadata.containerRootFilesystem {
175-
let readonly = metadata.containerConfiguration?.readOnly ?? false
176-
try bundle.setContainerRootFs(cloning: containerRootFs, readonly: readonly)
113+
if let containerRootFilesystem {
114+
let readonly = containerConfiguration?.readOnly ?? false
115+
try bundle.setContainerRootFs(cloning: containerRootFilesystem, readonly: readonly)
177116
}
178117

179-
if let options = metadata.options {
118+
if let options {
180119
try bundle.write(filename: "options.json", value: options)
181120
}
182-
183121
return bundle
184122
}
185123
}

Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ public actor ContainersService {
8181
var results = [String: ContainerState]()
8282
for dir in directories {
8383
do {
84-
let bundle = ContainerResource.Bundle(path: dir)
85-
let config = try bundle.configuration
84+
let config = try Self.getContainerConfiguration(at: dir)
85+
8686
let state = ContainerState(
8787
snapshot: .init(
8888
configuration: config,
@@ -100,7 +100,7 @@ public actor ContainersService {
100100
}
101101
} catch {
102102
try? FileManager.default.removeItem(at: dir)
103-
log.warning("failed to load container bundle at \(dir.path)")
103+
log.warning("failed to load container at \(dir.path): \(error)")
104104
}
105105
}
106106
return results
@@ -242,7 +242,7 @@ public actor ContainersService {
242242
let containerImage = ClientImage(description: configuration.image)
243243
let imageFs = try await containerImage.getCreateSnapshot(platform: configuration.platform)
244244

245-
let completeMetadata = BundleMetadata(
245+
let runtimeConfig = RuntimeConfiguration(
246246
path: path,
247247
initialFilesystem: initFilesystem,
248248
kernel: kernel,
@@ -251,7 +251,7 @@ public actor ContainersService {
251251
options: options
252252
)
253253

254-
try completeMetadata.writeMetadata()
254+
try runtimeConfig.writeRuntimeConfiguration()
255255

256256
let snapshot = ContainerSnapshot(
257257
configuration: configuration,
@@ -280,7 +280,7 @@ public actor ContainersService {
280280
}
281281

282282
let path = self.containerRoot.appendingPathComponent(id)
283-
let config = try await self.getContainerConfiguration(at: path)
283+
let config = try Self.getContainerConfiguration(at: path)
284284

285285
do {
286286
try Self.registerService(
@@ -692,33 +692,20 @@ public actor ContainersService {
692692
id == processID
693693
}
694694

695-
/// Check if a bundle exists at the given path
696-
private func bundleExists(at path: URL) async -> ContainerResource.ContainerConfiguration? {
697-
guard FileManager.default.fileExists(atPath: path.path) else {
698-
return nil
699-
}
700-
695+
/// Get container configuration, either from existing bundle or from metadata
696+
private static func getContainerConfiguration(at path: URL) throws -> ContainerConfiguration {
701697
let bundle = ContainerResource.Bundle(path: path)
702698
do {
703-
let config = try bundle.configuration
704-
return config
699+
return try bundle.configuration
705700
} catch {
706-
return nil
707-
}
708-
}
709-
710-
/// Get container configuration, either from existing bundle or from metadata
711-
private func getContainerConfiguration(at path: URL) async throws -> ContainerConfiguration {
712-
guard let config = await bundleExists(at: path) else {
713-
// Bundle doesn't exist, get config from metadata
714-
let metadata = try ContainerResource.BundleMetadata.readMetadata(from: path)
715-
guard let config = metadata.containerConfiguration else {
716-
throw ContainerizationError(.internalError, message: "metadata missing container configuration")
701+
// Bundle doesn't exist or incomplete, try runtime configuration
702+
// This handles containers that were created but not started yet
703+
let runtimeConfig = try RuntimeConfiguration.readRuntimeConfiguration(from: path)
704+
guard let config = runtimeConfig.containerConfiguration else {
705+
throw ContainerizationError(.internalError, message: "runtime configuration missing container configuration")
717706
}
718707
return config
719708
}
720-
// Bundle exists, read config normally
721-
return config
722709
}
723710
}
724711

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import ContainerResource
18+
import Containerization
19+
import ContainerizationError
20+
import Foundation
21+
22+
public struct RuntimeConfiguration: Codable, Sendable {
23+
static let runtimeConfigurationFilename = "runtime-configuration.json"
24+
25+
public let path: URL
26+
public let initialFilesystem: Filesystem
27+
public let kernel: Kernel
28+
public let containerConfiguration: ContainerConfiguration?
29+
public let containerRootFilesystem: Filesystem?
30+
public let options: ContainerCreateOptions?
31+
32+
public init(
33+
path: URL,
34+
initialFilesystem: Filesystem,
35+
kernel: Kernel,
36+
containerConfiguration: ContainerConfiguration? = nil,
37+
containerRootFilesystem: Filesystem? = nil,
38+
options: ContainerCreateOptions? = nil
39+
) {
40+
self.path = path
41+
self.initialFilesystem = initialFilesystem
42+
self.kernel = kernel
43+
self.containerConfiguration = containerConfiguration
44+
self.containerRootFilesystem = containerRootFilesystem
45+
self.options = options
46+
}
47+
48+
public var runtimeConfigurationPath: URL {
49+
self.path.appendingPathComponent(Self.runtimeConfigurationFilename)
50+
}
51+
52+
public func writeRuntimeConfiguration() throws {
53+
// Ensure the parent directory exists
54+
let directory = self.runtimeConfigurationPath.deletingLastPathComponent()
55+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
56+
57+
let data = try JSONEncoder().encode(self)
58+
try data.write(to: self.runtimeConfigurationPath)
59+
}
60+
61+
public static func readRuntimeConfiguration(from runtimeConfigurationPath: URL) throws -> RuntimeConfiguration {
62+
let configurationPath = runtimeConfigurationPath.appendingPathComponent(RuntimeConfiguration.runtimeConfigurationFilename)
63+
guard FileManager.default.fileExists(atPath: configurationPath.path) else {
64+
throw ContainerizationError(
65+
.notFound,
66+
message: "runtime configuration file not found at path: \(configurationPath.path)"
67+
)
68+
}
69+
70+
let data = try Data(contentsOf: configurationPath)
71+
return try JSONDecoder().decode(RuntimeConfiguration.self, from: data)
72+
}
73+
}

Sources/Services/ContainerSandboxService/Server/SandboxService.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,11 +1318,16 @@ extension SandboxService {
13181318
/// Create bundle from metadata
13191319
private func createBundle() throws {
13201320
do {
1321-
let metadata = try ContainerResource.BundleMetadata.readMetadata(from: self.root)
1322-
_ = try ContainerResource.Bundle.createFromMetadata(metadata)
1323-
self.log.info("Created bundle from metadata at \(metadata.path)")
1324-
// Could remove the metadata file at this point, but will be
1325-
// cleaned up along with the bundle anyway
1321+
let runtimeConfig = try RuntimeConfiguration.readRuntimeConfiguration(from: self.root)
1322+
_ = try ContainerResource.Bundle.create(
1323+
path: runtimeConfig.path,
1324+
initialFilesystem: runtimeConfig.initialFilesystem,
1325+
kernel: runtimeConfig.kernel,
1326+
containerConfiguration: runtimeConfig.containerConfiguration,
1327+
containerRootFilesystem: runtimeConfig.containerRootFilesystem,
1328+
options: runtimeConfig.options
1329+
)
1330+
self.log.info("Created bundle from runtime configuration at \(runtimeConfig.path)")
13261331
} catch {
13271332
self.log.error("Failed to create bundle \(error)")
13281333
throw error

Tests/ContainerResourceTests/BundleMetadataTests.swift renamed to Tests/ContainerSandboxServiceTests/RuntimeConfigurationTests.swift

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,35 @@
1414
// limitations under the License.
1515
//===----------------------------------------------------------------------===//
1616

17-
import ContainerAPIService
17+
// import ContainerAPIService
1818
import ContainerResource
19+
import ContainerSandboxServiceClient
1920
import Containerization
20-
import ContainerizationOCI
21+
// import ContainerizationOCI
2122
import Foundation
2223
import Testing
2324

24-
/// Unit tests for Bundle metadata functionality.
25+
/// Unit tests for RuntimeConfiguration functionality.
2526
///
26-
/// These tests verify the Bundle metadata serialization and deserialization,
27-
/// ensuring that metadata can be properly written, read, and used to create bundles.
28-
struct BundleMetadataTests {
27+
/// These tests verify the runtime configuration serialization and deserialization,
28+
/// ensuring that configuration can be properly written, read, and used to create bundles.
29+
struct RuntimeConfigurationTests {
2930

30-
/// Test that reading non-existent metadata file throws appropriate error
31+
/// Test that reading non-existent runtime configuration file throws
32+
/// appropriate error
3133
@Test
32-
func testReadNonExistentMetadata() throws {
34+
func testReadNonExistentRuntimeConfiguration() throws {
3335
let tempDir = FileManager.default.temporaryDirectory
3436
let nonExistentPath = tempDir.appendingPathComponent("non-existent-\(UUID()).json")
3537

3638
#expect(throws: Error.self) {
37-
_ = try ContainerResource.BundleMetadata.readMetadata(from: nonExistentPath)
39+
_ = try RuntimeConfiguration.readRuntimeConfiguration(from: nonExistentPath)
3840
}
3941
}
4042

41-
/// Test that metadata reads and writes as expected
43+
/// Test that runtime configuration reads and writes as expected
4244
@Test
43-
func testMetadataReadWrite() throws {
45+
func testRuntimeConfigurationReadWrite() throws {
4446
let tempDir = FileManager.default.temporaryDirectory
4547
let bundlePath = tempDir.appendingPathComponent("test-bundle-\(UUID())")
4648

@@ -59,7 +61,7 @@ struct BundleMetadataTests {
5961
platform: .linuxArm
6062
)
6163

62-
let metadata = BundleMetadata(
64+
let runtimeConfig = RuntimeConfiguration(
6365
path: bundlePath,
6466
initialFilesystem: initFs,
6567
kernel: kernel,
@@ -68,31 +70,31 @@ struct BundleMetadataTests {
6870
options: nil
6971
)
7072

71-
try metadata.writeMetadata()
73+
try runtimeConfig.writeRuntimeConfiguration()
7274

7375
defer {
74-
try? FileManager.default.removeItem(at: metadata.bundleMetadataPath)
76+
try? FileManager.default.removeItem(at: runtimeConfig.runtimeConfigurationPath)
7577
}
7678

77-
let readMetadata = try ContainerResource.BundleMetadata.readMetadata(from: bundlePath)
79+
let readRuntimeConfig = try RuntimeConfiguration.readRuntimeConfiguration(from: bundlePath)
7880

7981
#expect(
80-
readMetadata.path == bundlePath,
82+
readRuntimeConfig.path == bundlePath,
8183
"Path should match")
8284
#expect(
83-
readMetadata.kernel.path == kernel.path,
85+
readRuntimeConfig.kernel.path == kernel.path,
8486
"Kernel path should match")
8587
#expect(
86-
readMetadata.initialFilesystem.source == initFs.source,
88+
readRuntimeConfig.initialFilesystem.source == initFs.source,
8789
"Initial filesystem source should match")
8890
#expect(
89-
readMetadata.containerConfiguration == nil,
91+
readRuntimeConfig.containerConfiguration == nil,
9092
"Container configuration should be nil")
9193
#expect(
92-
readMetadata.containerRootFilesystem == nil,
94+
readRuntimeConfig.containerRootFilesystem == nil,
9395
"Root filesystem should be nil")
9496
#expect(
95-
readMetadata.options == nil,
97+
readRuntimeConfig.options == nil,
9698
"Options should be nil")
9799
}
98100
}

0 commit comments

Comments
 (0)