diff --git a/Sources/Services/ContainerAPIService/Client/Parser.swift b/Sources/Services/ContainerAPIService/Client/Parser.swift index 74319cd7..753f9818 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -251,16 +251,25 @@ public struct Parser { var hasEntrypointOverride: Bool = false // ensure the entrypoint is honored if it has been explicitly set by the user if let entrypoint = managementFlags.entrypoint, !entrypoint.isEmpty { - result = [entrypoint] + result = [resolveExecutablePath(entrypoint, workingDir: workingDir)] hasEntrypointOverride = true } else if let entrypoint = config?.entrypoint, !entrypoint.isEmpty { - result = entrypoint + var resolved = entrypoint + if let first = entrypoint.first { + resolved[0] = resolveExecutablePath(first, workingDir: workingDir) + } + result = resolved } + if !arguments.isEmpty { result.append(contentsOf: arguments) } else { if let cmd = config?.cmd, !hasEntrypointOverride, !cmd.isEmpty { - result.append(contentsOf: cmd) + var resolved = cmd + if let first = cmd.first { + resolved[0] = resolveExecutablePath(first, workingDir: workingDir) + } + result.append(contentsOf: resolved) } } return result.count > 0 ? result : nil @@ -877,4 +886,21 @@ public struct Parser { default: return nil } } + + // MARK: Private + + private static func resolveExecutablePath(_ path: String, workingDir: String) -> String { + if path.hasPrefix("/") { + return path + } + + if path.hasPrefix("./") || path.hasPrefix("../") { + return URL(fileURLWithPath: workingDir) + .appendingPathComponent(path) + .standardized + .path + } + + return path + } } diff --git a/Tests/ContainerAPIClientTests/ParserTest.swift b/Tests/ContainerAPIClientTests/ParserTest.swift index b88b4c5a..6b70c339 100644 --- a/Tests/ContainerAPIClientTests/ParserTest.swift +++ b/Tests/ContainerAPIClientTests/ParserTest.swift @@ -14,8 +14,10 @@ // limitations under the License. //===----------------------------------------------------------------------===// +import ArgumentParser import ContainerizationError import ContainerizationExtras +import ContainerizationOCI import Foundation import Testing @@ -845,4 +847,150 @@ struct ParserTest { return error.description.contains("invalid property format") } } + + @Test + func testProcessEntrypointRelativePathWithDotSlash() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/usr/local/cargo/bin"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "./rustc"]) + + let result = try Parser.process( + arguments: ["--version"], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/usr/local/cargo/bin/rustc") + #expect(result.arguments == ["--version"]) + #expect(result.workingDirectory == "/usr/local/cargo/bin") + } + + @Test + func testProcessEntrypointBareCommand() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/usr/bin"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "python3"]) + + let result = try Parser.process( + arguments: ["-c", "print('hello')"], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "python3") + #expect(result.arguments == ["-c", "print('hello')"]) + } + + @Test + func testProcessEntrypointBareCommandWithRootWorkdir() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "ls"]) + + let result = try Parser.process( + arguments: ["-la"], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "ls") + #expect(result.arguments == ["-la"]) + } + + @Test + func testProcessEntrypointAbsolutePathUnchanged() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/home/user"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "/bin/bash"]) + + let result = try Parser.process( + arguments: ["-c", "echo hello"], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/bin/bash") + #expect(result.arguments == ["-c", "echo hello"]) + } + + @Test + func testProcessEntrypointRelativePathNormalization() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/usr/local/bin"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "../lib/node"]) + + let result = try Parser.process( + arguments: ["--version"], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/usr/local/lib/node") + } + + @Test + func testProcessEntrypointRelativePathWithDefaultWorkdir() throws { + let processFlags = try Flags.Process.parse([]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "./app"]) + + let result = try Parser.process( + arguments: [], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/app") + } + + @Test + func testProcessEntrypointRelativePathWithComplexPath() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/home/user/project"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "./bin/../scripts/./run.sh"]) + + let result = try Parser.process( + arguments: [], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/home/user/project/scripts/run.sh") + } + + @Test + func testProcessRelativeEntrypointInImageConfig() throws { + let config = ContainerizationOCI.ImageConfig( + entrypoint: ["./start.sh"], + workingDir: "/app" + ) + + let result = try Parser.process( + arguments: [], + processFlags: try Flags.Process.parse([]), + managementFlags: try Flags.Management.parse([]), + config: config + ) + + #expect(result.executable == "/app/start.sh") + } + + @Test + func testProcessRelativeCmdInImageConfig() throws { + let config = ContainerizationOCI.ImageConfig( + entrypoint: nil, + cmd: ["./run.py", "--fast"], + workingDir: "/scripts" + ) + + let result = try Parser.process( + arguments: [], + processFlags: try Flags.Process.parse([]), + managementFlags: try Flags.Management.parse([]), + config: config + ) + + #expect(result.executable == "/scripts/run.py") + #expect(result.arguments == ["--fast"]) + } }