Use FilePath for PublishSocket (#1594)

- Closes #1593.
This commit is contained in:
Chris George
2026-06-01 13:35:58 -07:00
committed by GitHub
parent 37595a734c
commit da8daf31de
4 changed files with 383 additions and 32 deletions
@@ -14,27 +14,148 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationError
import Foundation
import SystemPackage
/// Represents a socket that should be published from container to host.
///
/// - Deprecated: New for 1.0.0, path types changed from `URL` to `FilePath`.
/// - Note: Decoder handles `FilePath` and `URL` for persistent data compatibility;
/// this compatibility will be removed in a later release.
public struct PublishSocket: Sendable, Codable {
/// The path to the socket in the container.
public var containerPath: URL
/// Absolute path to the socket inside the container.
public var containerPath: FilePath
/// The path where the socket should appear on the host.
public var hostPath: URL
/// Absolute path where the socket appears on the host.
public var hostPath: FilePath
/// File permissions for the socket on the host.
public var permissions: FilePermissions?
/// Creates a `PublishSocket` with validated absolute paths.
///
/// - Parameters:
/// - containerPath: Absolute path to the socket inside the container.
/// Must begin with `/`.
/// - hostPath: Absolute path where the socket appears on the host.
/// Must begin with `/`.
/// - permissions: File permissions applied to the socket on the host.
/// - Throws: `ContainerizationError` with code `.invalidArgument` if
/// either path is not absolute.
public init(
containerPath: URL,
hostPath: URL,
containerPath: FilePath,
hostPath: FilePath,
permissions: FilePermissions? = nil
) {
) throws {
guard containerPath.isAbsolute else {
throw ContainerizationError(
.invalidArgument,
message: "containerPath must be absolute: \(containerPath)"
)
}
guard hostPath.isAbsolute else {
throw ContainerizationError(
.invalidArgument,
message: "hostPath must be absolute: \(hostPath)"
)
}
self.containerPath = containerPath
self.hostPath = hostPath
self.permissions = permissions
}
private enum CodingKeys: String, CodingKey {
case containerPath
case hostPath
case permissions
}
/// Encodes each path as its plain absolute string (e.g. `"/var/run/docker.sock"`).
///
/// Pre-1.0 wire-format change from the prior `URL`-typed encoding which
/// emitted `URL.absoluteString` (`"file:///var/run/docker.sock"`). The
/// decoder accepts both forms for compatibility with persisted bundles
/// from earlier releases; that compatibility will be removed in a later
/// release.
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(containerPath.string, forKey: .containerPath)
try container.encode(hostPath.string, forKey: .hostPath)
try container.encodeIfPresent(permissions, forKey: .permissions)
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let containerPath = try Self.decodePath(from: container, forKey: .containerPath)
let hostPath = try Self.decodePath(from: container, forKey: .hostPath)
let permissions = try container.decodeIfPresent(FilePermissions.self, forKey: .permissions)
do {
try self.init(
containerPath: containerPath,
hostPath: hostPath,
permissions: permissions
)
} catch let error as ContainerizationError {
throw DecodingError.dataCorruptedError(
forKey: .containerPath,
in: container,
debugDescription: String(describing: error)
)
}
}
/// Decodes a `FilePath` accepting either the new plain-path form
/// (`"/var/run/docker.sock"`) or the legacy file-URL form emitted by
/// older releases (`"file:///var/run/docker.sock"`). Throws
/// `DecodingError.dataCorrupted` on a malformed file URL, empty input, or a
/// non-absolute path validating decoded paths here guards against
/// manually edited or corrupt persisted configs, complementing the
/// by-construction check in `init(containerPath:hostPath:permissions:)`.
private static func decodePath(
from container: KeyedDecodingContainer<CodingKeys>,
forKey key: CodingKeys
) throws -> FilePath {
let raw = try container.decode(String.self, forKey: key)
let path: String
if raw.hasPrefix("file:") {
guard let url = URL(string: raw), url.isFileURL else {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "malformed file URL: \(raw)"
)
}
if let host = url.host(), !host.isEmpty, host != "localhost" {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "file URL host must be empty or 'localhost': \(raw)"
)
}
path = url.path(percentEncoded: false)
} else {
path = raw
}
guard !path.isEmpty else {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "decoded socket path is empty: \(raw)"
)
}
let filePath = FilePath(path)
guard filePath.isAbsolute else {
throw DecodingError.dataCorruptedError(
forKey: key,
in: container,
debugDescription: "decoded socket path must be absolute: \(raw)"
)
}
return filePath
}
}
@@ -22,6 +22,7 @@ import ContainerizationExtras
import ContainerizationOCI
import ContainerizationOS
import Foundation
import SystemPackage
/// A parsed volume specification from user input
public struct ParsedVolume {
@@ -739,7 +740,6 @@ public struct Parser {
let hostPath = String(parts[0])
let containerPath = String(parts[1])
// Validate paths are not empty
if hostPath.isEmpty {
throw ContainerizationError(
.invalidArgument, message: "host socket path cannot be empty")
@@ -749,28 +749,18 @@ public struct Parser {
.invalidArgument, message: "container socket path cannot be empty")
}
// Ensure container path must start with /
if !containerPath.hasPrefix("/") {
throw ContainerizationError(
.invalidArgument,
message: "container socket path must be absolute: \(containerPath)")
}
let absoluteHostPath = FilePathOps.absolutePath(FilePath(hostPath))
// Convert host path to absolute path for consistency
let hostURL = URL(fileURLWithPath: hostPath)
let absoluteHostPath = hostURL.absoluteURL.path
// Check if host socket already exists and might be in use
if FileManager.default.fileExists(atPath: absoluteHostPath) {
if FileManager.default.fileExists(atPath: absoluteHostPath.string) {
do {
let attrs = try FileManager.default.attributesOfItem(atPath: absoluteHostPath)
let attrs = try FileManager.default.attributesOfItem(atPath: absoluteHostPath.string)
if let fileType = attrs[.type] as? FileAttributeType, fileType == .typeSocket {
throw ContainerizationError(
.invalidArgument,
message: "host socket \(absoluteHostPath) already exists and may be in use")
}
// If it exists but is not a socket, we can remove it and create socket
try FileManager.default.removeItem(atPath: absoluteHostPath)
try FileManager.default.removeItem(atPath: absoluteHostPath.string)
} catch let error as ContainerizationError {
throw error
} catch {
@@ -778,17 +768,15 @@ public struct Parser {
}
}
// Create host directory if it doesn't exist
let hostDir = hostURL.deletingLastPathComponent()
if !FileManager.default.fileExists(atPath: hostDir.path) {
let hostDir = absoluteHostPath.removingLastComponent()
if !FileManager.default.fileExists(atPath: hostDir.string) {
try FileManager.default.createDirectory(
at: hostDir, withIntermediateDirectories: true)
atPath: hostDir.string, withIntermediateDirectories: true)
}
// Create and return PublishSocket object with validated paths
return PublishSocket(
containerPath: URL(fileURLWithPath: containerPath),
hostPath: URL(fileURLWithPath: absoluteHostPath),
return try PublishSocket(
containerPath: FilePath(containerPath),
hostPath: absoluteHostPath,
permissions: nil
)
@@ -1000,9 +1000,10 @@ public actor RuntimeService {
}
for publishedSocket in config.publishedSockets {
// UnixSocketConfiguration (Containerization) takes URL; convert from FilePath at the boundary.
let socketConfig = UnixSocketConfiguration(
source: publishedSocket.containerPath,
destination: publishedSocket.hostPath,
source: URL(filePath: publishedSocket.containerPath.string),
destination: URL(filePath: publishedSocket.hostPath.string),
permissions: publishedSocket.permissions,
direction: .outOf
)