mirror of
https://github.com/apple/container.git
synced 2026-09-15 04:05:37 +00:00
## Motivation and Context
Now that we're in 2026, we need to update the license headers on all the
files. Unfortunately, Hawkeye doesn't have an attribute for the current
year to help us avoid this in the future. Instead, I had to work around
this by doing the following:
1. Update licenserc.toml with:
```
[properties]
... (other properties)
currentYear = "2026"
```
2. Update scripts/license-header.txt with
```
Copyright ©{{ " " }}{%- set created = attrs.git_file_created_year or
attrs.disk_file_created_year -%}{%- set modified = props["currentYear"]
-%}{%- if created != modified -%} {{created}}-{{modified}}{%- else
-%}{{created}}{%- endif -%}{{ " " }}{{ props["copyrightOwner"] }}.
```
Then I removed these two changes before committing. After this PR is
merged, all files will have recently had git updates, so the existing
code for setting the modified year should work as intended.
Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
653 lines
25 KiB
Swift
653 lines
25 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
|
//
|
|
// 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 content = """
|
|
# Really cool comment
|
|
FOO=bar
|
|
BAR=baz wow
|
|
URL=https://foo.bar?baz=wow
|
|
"""
|
|
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("test.env")
|
|
guard FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.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)
|
|
let expected = [
|
|
"FOO=bar",
|
|
"BAR=baz wow",
|
|
"URL=https://foo.bar?baz=wow",
|
|
]
|
|
for item in expected {
|
|
#expect(
|
|
inspectResp.configuration.initProcess.environment.contains(item),
|
|
"environment variable \(item) 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 = inspectOutput.networks[0].ipv4Address.address
|
|
let expectedNameserver = IPv4Address((ip.value & Prefix(length: 24)!.prefixMask32) + 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)",
|
|
"options \(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 = inspectOutput.networks[0].ipv4Address.address
|
|
|
|
let output = try doExec(name: name, cmd: ["cat", "/etc/hosts"])
|
|
let lines = output.split(separator: "\n")
|
|
|
|
let expectedEntries = [("127.0.0.1", "localhost"), (ip.description, 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
|
|
print("request to \(url) succeeded")
|
|
} catch {
|
|
print("request to \(url) failed, error \(error)")
|
|
try await Task.sleep(for: .seconds(retryDelaySeconds))
|
|
}
|
|
retriesRemaining -= 1
|
|
}
|
|
try #require(success, "Request to \(url) failed after \(retries - retriesRemaining) retries")
|
|
try doStop(name: name)
|
|
} catch {
|
|
Issue.record("failed to run container \(error)")
|
|
return
|
|
}
|
|
}
|
|
|
|
@Test func testForwardTCPPortRange() async throws {
|
|
let range = UInt16(10)
|
|
for portOffset in 0..<range {
|
|
let retries = 10
|
|
let retryDelaySeconds = Int64(3)
|
|
do {
|
|
let name = getLowercasedTestName()
|
|
let proxyIp = "127.0.0.1"
|
|
let proxyPortStart = UInt16.random(in: 50000..<55000)
|
|
let serverPortStart = UInt16.random(in: 55000..<60000)
|
|
let proxyPortEnd = proxyPortStart + range
|
|
let serverPortEnd = serverPortStart + range
|
|
try doLongRun(
|
|
name: name,
|
|
image: "docker.io/library/python:alpine",
|
|
args: ["--publish", "\(proxyIp):\(proxyPortStart)-\(proxyPortEnd):\(serverPortStart)-\(serverPortEnd)/tcp"],
|
|
containerArgs: ["python3", "-m", "http.server", "--bind", "0.0.0.0", "\(serverPortStart + portOffset)"])
|
|
defer {
|
|
try? doStop(name: name)
|
|
}
|
|
|
|
let url = "http://\(proxyIp):\(proxyPortStart + portOffset)"
|
|
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
|
|
print("request to \(url) succeeded")
|
|
} catch {
|
|
print("request to \(url) failed, error: \(error)")
|
|
try await Task.sleep(for: .seconds(retryDelaySeconds))
|
|
}
|
|
retriesRemaining -= 1
|
|
}
|
|
try #require(success, "Request to \(url) failed after \(retries - retriesRemaining) retries")
|
|
try doStop(name: name)
|
|
} catch {
|
|
Issue.record("failed to run container \(error)")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test func testRunCommandEnvFileFromNamedPipe() throws {
|
|
do {
|
|
let name = getTestName()
|
|
let pipePath = FileManager.default.temporaryDirectory.appendingPathComponent("envfile-pipe\(UUID().uuidString)")
|
|
|
|
// create pipe
|
|
let result = mkfifo(pipePath.path(), 0o600)
|
|
guard result == 0 else {
|
|
Issue.record("failed to create named pipe: \(String(cString: strerror(errno)))")
|
|
return
|
|
}
|
|
|
|
defer {
|
|
try? FileManager.default.removeItem(at: pipePath)
|
|
}
|
|
|
|
let content = """
|
|
FOO=bar
|
|
BAR=baz
|
|
"""
|
|
|
|
let group = DispatchGroup()
|
|
|
|
group.enter()
|
|
DispatchQueue.global().async {
|
|
do {
|
|
let handle = try FileHandle(forWritingTo: pipePath)
|
|
try handle.write(contentsOf: Data(content.utf8))
|
|
try handle.close()
|
|
} catch {
|
|
Issue.record(error)
|
|
return
|
|
}
|
|
|
|
group.leave()
|
|
}
|
|
|
|
try doLongRun(name: name, args: ["--env-file", pipePath.path()])
|
|
defer {
|
|
try? doStop(name: name)
|
|
}
|
|
|
|
group.wait()
|
|
|
|
let inspectResult = try inspectContainer(name)
|
|
let expected = [
|
|
"FOO=bar",
|
|
"BAR=baz",
|
|
]
|
|
|
|
for item in expected {
|
|
#expect(
|
|
inspectResult.configuration.initProcess.environment.contains(item),
|
|
"expected environment variable \(item) not found"
|
|
)
|
|
}
|
|
try doStop(name: name)
|
|
} catch {
|
|
Issue.record(error)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|