Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Sources/ContainerCommands/Builder/BuilderStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion Sources/ContainerCommands/Container/ContainerCreate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion Sources/ContainerCommands/Container/ContainerRun.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 4 additions & 1 deletion Sources/ContainerCommands/Machine/MachineCreate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import ArgumentParser
import ContainerAPIClient
import ContainerPersistence
import ContainerResource
import ContainerizationError
import ContainerizationOCI
import Foundation
Expand Down Expand Up @@ -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(
Expand Down
7 changes: 5 additions & 2 deletions Sources/ContainerResource/Container/ManagedContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
8 changes: 0 additions & 8 deletions Sources/Services/ContainerAPIService/Client/Utility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
7 changes: 7 additions & 0 deletions Tests/ContainerResourceTests/ManagedContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down