Files
container/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift
Dmitry Kovba 3e52a3c305 Fix failing network tests (#620)
This PR fixes failing network tests by using lowercased names.
Additionally, it reduces code duplication.
2025-09-17 09:59:43 -07:00

531 lines
21 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
//
import AsyncHTTPClient
import ContainerClient
import ContainerizationExtras
import ContainerizationOS
import Foundation
import Testing
class TestCLIRunCommand: CLITest {
private func getTestName() -> String {
Test.current!.name.trimmingCharacters(in: ["(", ")"]).lowercased()
}
private func getLowercasedTestName() -> String {
getTestName().lowercased()
}
@Test func testRunCommand() throws {
do {
let name = getTestName()
try doLongRun(name: name, args: [])
defer {
try? doStop(name: name)
}
let _ = try doExec(name: name, cmd: ["date"])
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandCWD() throws {
do {
let name = getTestName()
let expectedCWD = "/tmp"
try doLongRun(name: name, args: ["--cwd", expectedCWD])
defer {
try? doStop(name: name)
}
var output = try doExec(name: name, cmd: ["pwd"])
output = output.trimmingCharacters(in: .whitespacesAndNewlines)
#expect(output == expectedCWD, "expected current working directory to be \(expectedCWD), instead got \(output)")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandEnv() throws {
do {
let name = getTestName()
let envData = "FOO=bar"
try doLongRun(name: name, args: ["--env", envData])
defer {
try? doStop(name: name)
}
let inspectResp = try inspectContainer(name)
#expect(
inspectResp.configuration.initProcess.environment.contains(envData),
"environment variable \(envData) not set in container configuration")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandEnvFile() throws {
do {
let name = getTestName()
let envData = "FOO=bar"
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("test.env")
guard FileManager.default.createFile(atPath: tempFile.path(), contents: Data(envData.utf8)) else {
Issue.record("failed to create temporary file \(tempFile.path())")
return
}
defer {
try? FileManager.default.removeItem(at: tempFile)
}
try doLongRun(name: name, args: ["--env-file", tempFile.path()])
defer {
try? doStop(name: name)
}
let inspectResp = try inspectContainer(name)
#expect(
inspectResp.configuration.initProcess.environment.contains(envData),
"environment variable \(envData) not set in container configuration")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandUserIDGroupID() throws {
do {
let name = getTestName()
let uid = "10"
let gid = "100"
try doLongRun(name: name, args: ["--uid", uid, "--gid", gid])
defer {
try? doStop(name: name)
}
var output = try doExec(name: name, cmd: ["id"])
output = output.trimmingCharacters(in: .whitespacesAndNewlines)
try #expect(output.contains(Regex("uid=\(uid).*?gid=\(gid).*")), "invalid user/group id, got \(output)")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandUser() throws {
do {
let name = getTestName()
let user = "nobody"
try doLongRun(name: name, args: ["--user", user])
defer {
try? doStop(name: name)
}
var output = try doExec(name: name, cmd: ["whoami"])
output = output.trimmingCharacters(in: .whitespacesAndNewlines)
#expect(output == user, "expected user \(user), got \(output)")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandCPUs() throws {
do {
let name = getTestName()
let cpus = "2"
try doLongRun(name: name, args: ["--cpus", cpus])
defer {
try? doStop(name: name)
}
var output = try doExec(name: name, cmd: ["nproc"])
output = output.trimmingCharacters(in: .whitespacesAndNewlines)
#expect(output == cpus, "expected \(cpus), instead got \(output)")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandMemory() throws {
do {
let name = getTestName()
let expectedMBs = 1024
try doLongRun(name: name, args: ["--memory", "\(expectedMBs)M"])
defer {
try? doStop(name: name)
}
let inspectResp = try inspectContainer(name)
let actualInBytes = inspectResp.configuration.resources.memoryInBytes
#expect(actualInBytes == expectedMBs.mib(), "expected \(expectedMBs.mib()) bytes, instead got \(actualInBytes) bytes")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandMount() throws {
do {
let name = getTestName()
let targetContainerPath = "/tmp/testmount"
let testData = "hello world"
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
let tempFile = tempDir.appendingPathComponent(UUID().uuidString)
guard FileManager.default.createFile(atPath: tempFile.path(), contents: Data(testData.utf8)) else {
Issue.record("failed to create temporary file \(tempFile.path())")
return
}
defer {
try? FileManager.default.removeItem(at: tempDir)
}
try doLongRun(name: name, args: ["--mount", "type=virtiofs,source=\(tempDir.path()),target=\(targetContainerPath),readonly"])
defer {
try? doStop(name: name)
}
var output = try doExec(name: name, cmd: ["cat", "\(targetContainerPath)/\(tempFile.lastPathComponent)"])
output = output.trimmingCharacters(in: .whitespacesAndNewlines)
#expect(output == testData, "expected file with content '\(testData)', instead got '\(output)'")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandUnixSocketMount() throws {
do {
let name = getTestName()
let socketPath = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
let socketType = try UnixType(path: socketPath.path, unlinkExisting: true)
let socket = try Socket(type: socketType, closeOnDeinit: true)
try socket.listen()
defer {
try? socket.close()
try? FileManager.default.removeItem(at: socketPath)
}
try doLongRun(
name: name,
args: ["-v", "\(socketPath.path):/woo"]
)
defer {
try? doStop(name: name)
}
let output = try doExec(name: name, cmd: ["ls", "-alh", "woo"])
let splitOutput = output.components(separatedBy: .whitespaces)
#expect(splitOutput.count > 0, "expected split output of 'ls -alh' to be at least 1, instead got \(splitOutput.count)")
let perms = splitOutput[0]
let firstChar = perms[perms.startIndex]
#expect(firstChar == "s", "expected file in guest to be of type socket, instead got '\(firstChar)'")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandTmpfs() throws {
do {
let name = getTestName()
let targetContainerPath = "/tmp/testtmpfs"
let expectedFilesystem = "tmpfs"
try doLongRun(name: name, args: ["--tmpfs", targetContainerPath])
defer {
try? doStop(name: name)
}
let output = try doExec(name: name, cmd: ["df", targetContainerPath])
let lines = output.split(separator: "\n")
#expect(lines.count == 2, "expected only two rows of output, instead got \(lines.count)")
let words = lines[1].split(separator: " ")
#expect(words.count > 1, "expected information to contain multiple words, got \(words.count)")
#expect(words[0].lowercased() == expectedFilesystem, "expected filesystem type to be \(expectedFilesystem), instead got \(output)")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandOSArch() throws {
do {
let name = getLowercasedTestName()
let os = "linux"
let arch = "amd64"
let expectedArch = "x86_64"
try doLongRun(name: name, args: ["--os", os, "--arch", arch])
defer {
try? doStop(name: name)
}
var output = try doExec(name: name, cmd: ["uname", "-sm"])
output = output.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
#expect(output == "\(os) \(expectedArch)", "expected container to use '\(os) \(expectedArch)', instead got '\(output)'")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandPlatform() throws {
do {
let name = getTestName()
let os = "linux"
let platform = "linux/amd64"
let expectedArch = "x86_64"
try doLongRun(name: name, args: ["--platform", platform])
defer {
try? doStop(name: name)
}
var output = try doExec(name: name, cmd: ["uname", "-sm"])
output = output.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
#expect(output == "\(os) \(expectedArch)", "expected container to use '\(os) \(expectedArch)', instead got '\(output)'")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandVolume() throws {
do {
let name = getTestName()
let targetContainerPath = "/tmp/testvolume"
let testData = "one small step"
let volume = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: volume, withIntermediateDirectories: true)
let volumeFile = volume.appendingPathComponent(UUID().uuidString)
guard FileManager.default.createFile(atPath: volumeFile.path(), contents: Data(testData.utf8)) else {
Issue.record("failed to create file at \(volumeFile)")
return
}
defer {
try? FileManager.default.removeItem(at: volume)
}
try doLongRun(name: name, args: ["--volume", "\(volume.path):\(targetContainerPath)"])
defer {
try? doStop(name: name)
}
var output = try doExec(name: name, cmd: ["cat", "\(targetContainerPath)/\(volumeFile.lastPathComponent)"])
output = output.trimmingCharacters(in: .whitespacesAndNewlines)
#expect(output == testData, "expected file with content '\(testData)', instead got '\(output)'")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandCidfile() throws {
do {
let name = getTestName()
let filePath = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer {
try? FileManager.default.removeItem(at: filePath)
}
try doLongRun(name: name, args: ["--cidfile", filePath.path()])
defer {
try? doStop(name: name)
}
let actualID = try String(contentsOf: filePath, encoding: .utf8)
#expect(actualID == name, "expected container ID '\(name)', instead got '\(actualID)'")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandNoDNS() throws {
do {
let name = getTestName()
try doLongRun(name: name, args: ["--no-dns"])
defer {
try? doStop(name: name)
}
#expect(throws: (any Error).self) {
try doExec(name: name, cmd: ["cat", "/etc/resolv.conf"])
}
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandDefaultResolvConf() throws {
do {
let name = getTestName()
try doLongRun(name: name, args: [])
defer {
try? doStop(name: name)
}
let output = try doExec(name: name, cmd: ["cat", "/etc/resolv.conf"])
let actualLines = output.components(separatedBy: .newlines)
.filter { !$0.isEmpty }
.map { $0.components(separatedBy: .whitespaces) }
.map { $0.joined(separator: " ") }
let inspectOutput = try inspectContainer(name)
let ip = String(inspectOutput.networks[0].address.split(separator: "/")[0])
let ipv4Address = try IPv4Address(ip)
let expectedNameserver = IPv4Address(fromValue: ipv4Address.prefix(prefixLength: 24).value + 1).description
let defaultDomain = try getDefaultDomain()
let expectedLines: [String] = [
"nameserver \(expectedNameserver)",
defaultDomain.map { "domain \($0)" },
].compactMap { $0 }
#expect(expectedLines == actualLines)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunCommandNonDefaultResolvConf() throws {
do {
let expectedDns: String = "8.8.8.8"
let expectedDomain = "example.com"
let expectedSearch = "test.com"
let expectedOption = "debug"
let name = getTestName()
try doLongRun(
name: name,
args: [
"--dns", expectedDns,
"--dns-domain", expectedDomain,
"--dns-search", expectedSearch,
"--dns-option", expectedOption,
])
defer {
try? doStop(name: name)
}
let output = try doExec(name: name, cmd: ["cat", "/etc/resolv.conf"])
let actualLines = output.components(separatedBy: .newlines)
.filter { !$0.isEmpty }
.map { $0.components(separatedBy: .whitespaces) }
.map { $0.joined(separator: " ") }
let expectedLines: [String] = [
"nameserver \(expectedDns)",
"domain \(expectedDomain)",
"search \(expectedSearch)",
"opts \(expectedOption)",
]
#expect(expectedLines == actualLines)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testRunDefaultHostsEntries() throws {
do {
let name = getTestName()
try doLongRun(name: name)
defer {
try? doStop(name: name)
}
let inspectOutput = try inspectContainer(name)
let ip = String(inspectOutput.networks[0].address.split(separator: "/")[0])
let output = try doExec(name: name, cmd: ["cat", "/etc/hosts"])
let lines = output.split(separator: "\n")
let expectedEntries = [("127.0.0.1", "localhost"), (ip, name)]
for (i, line) in lines.enumerated() {
let words = line.split(separator: " ").map { String($0) }
#expect(words.count >= 2, "expected /etc/hosts entry to have 2 or more entries")
let expected = expectedEntries[i]
#expect(expected.0 == words[0], "expected /etc/hosts entries IP to be \(expected.0), instead got \(words[0])")
#expect(expected.1 == words[1], "expected /etc/hosts entries host to be \(expected.1), instead got \(words[1])")
}
} catch {
Issue.record("failed to run container \(error)")
return
}
}
@Test func testForwardTCP() async throws {
let retries = 10
let retryDelaySeconds = Int64(3)
do {
let name = getLowercasedTestName()
let proxyIp = "127.0.0.1"
let proxyPort = UInt16.random(in: 50000..<55000)
let serverPort = UInt16.random(in: 55000..<60000)
try doLongRun(
name: name,
image: "docker.io/library/python:alpine",
args: ["--publish", "\(proxyIp):\(proxyPort):\(serverPort)/tcp"],
containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(serverPort)"])
defer {
try? doStop(name: name)
}
let url = "http://\(proxyIp):\(proxyPort)"
var request = HTTPClientRequest(url: url)
request.method = .GET
let config = HTTPClient.Configuration(proxy: nil)
let client = HTTPClient(eventLoopGroupProvider: .singleton, configuration: config)
defer { _ = client.shutdown() }
var retriesRemaining = retries
var success = false
while !success && retriesRemaining > 0 {
do {
let response = try await client.execute(request, timeout: .seconds(retryDelaySeconds))
try #require(response.status == .ok)
success = true
} catch {
print("request to \(url) failed, error \(error)")
try await Task.sleep(for: .seconds(retryDelaySeconds))
}
retriesRemaining -= 1
}
#expect(success, "Request to \(url) failed after \(retries - retriesRemaining) retries")
try doStop(name: name)
} catch {
Issue.record("failed to run container \(error)")
return
}
}
func getDefaultDomain() throws -> String? {
let (output, err, status) = try run(arguments: ["system", "property", "get", "dns.domain"])
try #require(status == 0, "default DNS domain retrieval returned status \(status): \(err)")
let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmedOutput == "" {
return nil
}
return trimmedOutput
}
}