diff --git a/Sources/ContainerCommands/Builder/BuilderStart.swift b/Sources/ContainerCommands/Builder/BuilderStart.swift index cf1d4e1c0..a7f6ff377 100644 --- a/Sources/ContainerCommands/Builder/BuilderStart.swift +++ b/Sources/ContainerCommands/Builder/BuilderStart.swift @@ -200,7 +200,9 @@ extension Application { useRosetta ? nil : "--enable-qemu", ].compactMap { $0 } - try ContainerAPIClient.Utility.validEntityName(Builder.builderContainerId) + guard ManagedContainer.nameValid(Builder.builderContainerId) else { + throw ContainerizationError(.invalidArgument, message: "container ID \(Builder.builderContainerId) is not a valid container ID") + } let image = try await ClientImage.fetch( reference: builderImage, diff --git a/Sources/ContainerCommands/Container/ContainerCreate.swift b/Sources/ContainerCommands/Container/ContainerCreate.swift index 92f0c02c1..97febb060 100644 --- a/Sources/ContainerCommands/Container/ContainerCreate.swift +++ b/Sources/ContainerCommands/Container/ContainerCreate.swift @@ -70,7 +70,10 @@ extension Application { progress.start() let id = Utility.createContainerID(name: self.managementFlags.name) - try Utility.validEntityName(id) + + guard ManagedContainer.nameValid(id) else { + throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID") + } let ck = try await Utility.containerConfigFromFlags( id: id, diff --git a/Sources/ContainerCommands/Container/ContainerRun.swift b/Sources/ContainerCommands/Container/ContainerRun.swift index 431279ac5..8570a1ae1 100644 --- a/Sources/ContainerCommands/Container/ContainerRun.swift +++ b/Sources/ContainerCommands/Container/ContainerRun.swift @@ -80,7 +80,9 @@ extension Application { } progress.start() - try Utility.validEntityName(id) + guard ManagedContainer.nameValid(id) else { + throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID") + } // Check if container with id already exists. let client = ContainerClient() diff --git a/Sources/ContainerCommands/Machine/MachineCreate.swift b/Sources/ContainerCommands/Machine/MachineCreate.swift index 0ed4ec200..4cf2a6d76 100644 --- a/Sources/ContainerCommands/Machine/MachineCreate.swift +++ b/Sources/ContainerCommands/Machine/MachineCreate.swift @@ -17,6 +17,7 @@ import ArgumentParser import ContainerAPIClient import ContainerPersistence +import ContainerResource import ContainerizationError import ContainerizationOCI import Foundation @@ -115,7 +116,9 @@ extension Application { id = "\(imageName)-\(suffix)" } - try Utility.validEntityName(id) + guard ManagedContainer.nameValid(id) else { + throw ContainerizationError(.invalidArgument, message: "machine ID \(id) is not a valid machine ID") + } let client = MachineClient() let (config, resources) = try await MachineClient.machineConfigFromFlags( diff --git a/Sources/ContainerResource/Container/ManagedContainer.swift b/Sources/ContainerResource/Container/ManagedContainer.swift index 76e421bd6..dbaae064a 100644 --- a/Sources/ContainerResource/Container/ManagedContainer.swift +++ b/Sources/ContainerResource/Container/ManagedContainer.swift @@ -41,9 +41,12 @@ public struct ManagedContainer: ManagedResource { /// not the protocol's 64-hex default. public static func generateId() -> String { UUID().uuidString.lowercased() } - /// Container name rule (mixed case, dots, hyphens, underscores). Duplicated from - /// Utility.validEntityName + /// Container name rule public static func nameValid(_ name: String) -> Bool { + // Maximum Linux hostname length is 64, but limit to maximum DNS label length + guard name.count <= 63 else { + return false + } let pattern = #"^[a-zA-Z0-9][a-zA-Z0-9_.-]+$"# return name.range(of: pattern, options: .regularExpression) != nil } diff --git a/Sources/Services/ContainerAPIService/Client/Utility.swift b/Sources/Services/ContainerAPIService/Client/Utility.swift index ce937ebdf..4d0ba700a 100644 --- a/Sources/Services/ContainerAPIService/Client/Utility.swift +++ b/Sources/Services/ContainerAPIService/Client/Utility.swift @@ -56,14 +56,6 @@ public struct Utility { return String(hex.prefix(12)) } - public static func validEntityName(_ name: String) throws { - let pattern = #"^[a-zA-Z0-9][a-zA-Z0-9_.-]+$"# - let regex = try Regex(pattern) - if try regex.firstMatch(in: name) == nil { - throw ContainerizationError(.invalidArgument, message: "invalid entity name \(name)") - } - } - public static func validMACAddress(_ macAddress: String) throws { let pattern = #"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"# let regex = try Regex(pattern) diff --git a/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift b/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift index d7da46e3d..1871cd149 100644 --- a/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift +++ b/Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift @@ -54,6 +54,9 @@ public struct ContainersHarness: Sendable { message: "id cannot be empty" ) } + guard ManagedContainer.nameValid(id) else { + throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID") + } let stdio = message.stdio() let data = message.dataNoCopy(key: .dynamicEnv) @@ -193,6 +196,9 @@ public struct ContainersHarness: Sendable { options = try JSONDecoder().decode(ContainerCreateOptions.self, from: odata) } let config = try JSONDecoder().decode(ContainerConfiguration.self, from: data) + guard ManagedContainer.nameValid(config.id) else { + throw ContainerizationError(.invalidArgument, message: "container ID \(config.id) is not a valid container ID") + } let kernel = try JSONDecoder().decode(Kernel.self, from: kdata) let initImage = message.string(key: .initImage) @@ -262,6 +268,9 @@ public struct ContainersHarness: Sendable { guard let id else { throw ContainerizationError(.invalidArgument, message: "id cannot be empty") } + guard ManagedContainer.nameValid(id) else { + throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID") + } let forceDelete = message.bool(key: .forceDelete) try await service.delete(id: id, force: forceDelete) return message.reply() @@ -272,6 +281,9 @@ public struct ContainersHarness: Sendable { guard let containerId = message.string(key: .id) else { throw ContainerizationError(.invalidArgument, message: "id cannot be empty") } + guard ManagedContainer.nameValid(containerId) else { + throw ContainerizationError(.invalidArgument, message: "container ID \(containerId) is not a valid container ID") + } let size = try await service.containerDiskUsage(id: containerId) @@ -289,6 +301,9 @@ public struct ContainersHarness: Sendable { message: "id cannot be empty" ) } + guard ManagedContainer.nameValid(id) else { + throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID") + } let fds = try await service.logs(id: id) let reply = message.reply() try reply.set(key: .logs, value: fds) @@ -374,6 +389,9 @@ public struct ContainersHarness: Sendable { message: "id cannot be empty" ) } + guard ManagedContainer.nameValid(id) else { + throw ContainerizationError(.invalidArgument, message: "container ID \(id) is not a valid container ID") + } let archive = message.string(key: .archive) guard let archive else { throw ContainerizationError( diff --git a/Tests/ContainerResourceTests/ManagedContainerTests.swift b/Tests/ContainerResourceTests/ManagedContainerTests.swift index 44a5ee460..496880fb3 100644 --- a/Tests/ContainerResourceTests/ManagedContainerTests.swift +++ b/Tests/ContainerResourceTests/ManagedContainerTests.swift @@ -60,6 +60,13 @@ struct ManagedContainerTests { #expect(!ManagedContainer.nameValid("a b")) } + @Test func nameValidRejectsNamesLongerThan63Characters() { + let maxValidName = String(repeating: "a", count: 63) + let tooLongName = String(repeating: "a", count: 64) + #expect(ManagedContainer.nameValid(maxValidName)) + #expect(!ManagedContainer.nameValid(tooLongName)) + } + @Test func generateIdIsLowercasedUUID() { let id = ManagedContainer.generateId() #expect(id == id.lowercased())