Skip to content

Commit cc4be53

Browse files
committed
RuntimeConfiguration: explicit Codable for path; restore Data(contentsOf:).
Address review feedback on the URL → FilePath conversion of RuntimeConfiguration.path: 1. FilePath's default Codable encoding exposes its internal _storage as a keyed container, while URL's default encoding produced a plain absoluteString. Without this fix, runtime-configuration.json files written by daemons before the migration would fail to decode after upgrade. Add explicit CodingKeys + encode(to:) / init(from:) that serialize path as a plain string and accept either form (file:// or bare path) on decode for backward compatibility. Add a regression test for the legacy URL format. 2. Restore Data(contentsOf: URL(fileURLWithPath:)) for reading the config file, matching the codebase precedent (Bundle.swift, ConfigurationLoader.swift, EntityStore.swift). FileManager.contents would have lost typed CocoaError diagnostics on I/O failure.
1 parent 0106155 commit cc4be53

2 files changed

Lines changed: 69 additions & 6 deletions

File tree

Sources/Services/Runtime/RuntimeClient/RuntimeConfiguration.swift

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,50 @@ public struct RuntimeConfiguration: Codable, Sendable {
5151
self.runtimeData = runtimeData
5252
}
5353

54+
private enum CodingKeys: String, CodingKey {
55+
case path
56+
case initialFilesystem
57+
case kernel
58+
case containerConfiguration
59+
case containerRootFilesystem
60+
case options
61+
case runtimeData
62+
}
63+
64+
// FilePath's default Codable encoding exposes its internal _storage and
65+
// is not interchangeable with URL's plain-string form. To stay
66+
// wire-compatible with runtime-configuration.json files written before
67+
// the URL → FilePath migration, encode `path` as a plain string and
68+
// accept either the file:// URL form or a bare path on decode.
69+
public init(from decoder: Decoder) throws {
70+
let container = try decoder.container(keyedBy: CodingKeys.self)
71+
let pathString = try container.decode(String.self, forKey: .path)
72+
if pathString.hasPrefix("file://"),
73+
let url = URL(string: pathString), url.isFileURL
74+
{
75+
self.path = FilePath(url.path(percentEncoded: false))
76+
} else {
77+
self.path = FilePath(pathString)
78+
}
79+
self.initialFilesystem = try container.decode(Filesystem.self, forKey: .initialFilesystem)
80+
self.kernel = try container.decode(Kernel.self, forKey: .kernel)
81+
self.containerConfiguration = try container.decodeIfPresent(ContainerConfiguration.self, forKey: .containerConfiguration)
82+
self.containerRootFilesystem = try container.decodeIfPresent(Filesystem.self, forKey: .containerRootFilesystem)
83+
self.options = try container.decodeIfPresent(ContainerCreateOptions.self, forKey: .options)
84+
self.runtimeData = try container.decodeIfPresent(Data.self, forKey: .runtimeData)
85+
}
86+
87+
public func encode(to encoder: Encoder) throws {
88+
var container = encoder.container(keyedBy: CodingKeys.self)
89+
try container.encode(self.path.string, forKey: .path)
90+
try container.encode(self.initialFilesystem, forKey: .initialFilesystem)
91+
try container.encode(self.kernel, forKey: .kernel)
92+
try container.encodeIfPresent(self.containerConfiguration, forKey: .containerConfiguration)
93+
try container.encodeIfPresent(self.containerRootFilesystem, forKey: .containerRootFilesystem)
94+
try container.encodeIfPresent(self.options, forKey: .options)
95+
try container.encodeIfPresent(self.runtimeData, forKey: .runtimeData)
96+
}
97+
5498
public var runtimeConfigurationPath: FilePath {
5599
self.path.appending(Self.runtimeConfigurationFilename)
56100
}
@@ -72,12 +116,7 @@ public struct RuntimeConfiguration: Codable, Sendable {
72116
)
73117
}
74118

75-
guard let data = FileManager.default.contents(atPath: configurationPath.string) else {
76-
throw ContainerizationError(
77-
.internalError,
78-
message: "failed to read runtime configuration file at path: \(configurationPath.string)"
79-
)
80-
}
119+
let data = try Data(contentsOf: URL(fileURLWithPath: configurationPath.string))
81120
return try JSONDecoder().decode(RuntimeConfiguration.self, from: data)
82121
}
83122
}

Tests/ContainerAPIServiceTests/RuntimeConfigurationTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,28 @@ struct RuntimeConfigurationTests {
136136
let decodedData = try JSONDecoder().decode(LinuxRuntimeData.self, from: readRuntimeConfig.runtimeData!)
137137
#expect(decodedData.variant == "test-variant", "Variant should round-trip through RuntimeConfiguration")
138138
}
139+
140+
/// Verify that runtime-configuration.json files written before the
141+
/// URL → FilePath migration (where `path` was a URL absoluteString
142+
/// like "file:///foo/bar") still decode correctly. Otherwise an upgrade
143+
/// would render existing containers unstartable.
144+
@Test
145+
func testRuntimeConfigurationDecodesLegacyURLPathFormat() throws {
146+
let kernel = Kernel(path: URL(fileURLWithPath: "/path/to/kernel"), platform: .linuxArm)
147+
let initFs = Filesystem.virtiofs(source: "/path/to/initfs", destination: "/", options: ["ro"])
148+
149+
let kernelJSON = try String(data: JSONEncoder().encode(kernel), encoding: .utf8) ?? ""
150+
let initFsJSON = try String(data: JSONEncoder().encode(initFs), encoding: .utf8) ?? ""
151+
152+
let legacyJSON = """
153+
{"path":"file:///tmp/legacy-bundle","initialFilesystem":\(initFsJSON),"kernel":\(kernelJSON)}
154+
"""
155+
let data = Data(legacyJSON.utf8)
156+
157+
let decoded = try JSONDecoder().decode(RuntimeConfiguration.self, from: data)
158+
159+
#expect(decoded.path == FilePath("/tmp/legacy-bundle"))
160+
#expect(decoded.kernel.path == kernel.path)
161+
#expect(decoded.initialFilesystem.source == initFs.source)
162+
}
139163
}

0 commit comments

Comments
 (0)