Files
container/Tests/IntegrationTests/Utilities/ContainerFixture+ContainerHelpers.swift

176 lines
6.2 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 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 ContainerResource
import Foundation
import SystemPackage
// MARK: - Inspect types
extension ContainerFixture {
/// Decoded output of `container inspect <name>`.
struct InspectOutput: Codable {
struct Status: Codable {
let state: String
let networks: [ContainerResource.Attachment]
}
let configuration: ContainerConfiguration
let status: Status
var networks: [ContainerResource.Attachment] { status.networks }
}
}
// MARK: - Container lifecycle helpers
extension ContainerFixture {
/// `-e` flags forwarding proxy env vars into container commands.
var proxyEnvironmentArgs: [String] {
let vars = Set(["HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "NO_PROXY", "no_proxy"])
return ProcessInfo.processInfo.environment
.filter { vars.contains($0.key) }
.flatMap { ["-e", "\($0.key)=\($0.value)"] }
}
/// Starts a detached container. Uses the first warmup image when `image` is nil.
///
/// `containerEnv` injects environment variables into the container via `-e` flags.
/// To set the CLI subprocess environment (e.g. for `--ssh`), use ``run(_:env:)`` directly.
func doLongRun(
name: String,
image: String? = nil,
args: [String] = [],
containerArgs: [String] = ["sleep", "infinity"],
autoRemove: Bool = true,
containerEnv: [String: String] = [:]
) throws {
let imageRef = image ?? ContainerFixture.warmupImages[0]
var runArgs = ["run"]
if autoRemove { runArgs.append("--rm") }
runArgs += ["--name", name, "-d"]
runArgs += proxyEnvironmentArgs
runArgs += args
for (k, v) in containerEnv { runArgs += ["-e", "\(k)=\(v)"] }
runArgs.append(imageRef)
runArgs += containerArgs
try run(runArgs).check()
}
/// Creates a stopped container (`container create`).
func doCreate(
name: String,
image: String? = nil,
args: [String] = ["sleep", "infinity"],
volumes: [String] = [],
networks: [String] = [],
ports: [String] = []
) throws {
let imageRef = image ?? ContainerFixture.warmupImages[0]
var createArgs = ["create", "--rm", "--name", name]
createArgs += proxyEnvironmentArgs
for v in volumes { createArgs += ["-v", v] }
for n in networks { createArgs += ["--network", n] }
for p in ports { createArgs += ["--publish", "\(p):\(p)"] }
createArgs.append(imageRef)
createArgs += args
try run(createArgs).check()
}
/// Starts a stopped container.
func doStart(_ name: String) throws {
try run(["start", name]).check()
}
/// Stops a container. Pass `signal: nil` to use the server's default.
func doStop(_ name: String, signal: String? = "SIGKILL") throws {
var args = ["stop"]
if let signal { args += ["-s", signal] }
args.append(name)
try run(args).check()
}
/// Deletes a container.
func doRemove(_ name: String, force: Bool = false) throws {
var args = ["delete"]
if force { args.append("--force") }
args.append(name)
try run(args).check()
}
/// Deletes a container.
///
/// When `ignoreFailure` is `false` (default) any error is rethrown — use
/// this when the container is expected to exist and removal must succeed.
/// Set `ignoreFailure: true` in cleanup contexts where best-effort removal
/// is acceptable (e.g. the container may have already been removed).
func doRemoveIfExists(_ name: String, force: Bool = false, ignoreFailure: Bool = false) throws {
do {
try doRemove(name, force: force)
} catch {
if !ignoreFailure { throw error }
}
}
/// Runs a command inside a container, returns stdout. Throws on non-zero exit.
@discardableResult
func doExec(
_ name: String,
cmd: [String],
detach: Bool = false,
user: String? = nil
) throws -> String {
var args = ["exec"]
args += proxyEnvironmentArgs
if detach { args.append("-d") }
if let user { args += ["-u", user] }
args.append(name)
args += cmd
return try run(args).check().output
}
/// Exports a container filesystem to a tar archive at `path`.
func doExport(_ name: String, to path: FilePath) throws {
try run(["export", name, "-o", path.string]).check()
}
}
// MARK: - Inspect helpers
extension ContainerFixture {
/// Returns the parsed inspect output for a container.
func inspectContainer(_ name: String) throws -> InspectOutput {
let result = try run(["inspect", name]).check()
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let outputs = try decoder.decode([InspectOutput].self, from: result.outputData)
guard let first = outputs.first else {
throw CommandError.executionFailed("container '\(name)' not found in inspect output")
}
return first
}
/// Returns the `status.state` string for a container (e.g. `"running"`, `"stopped"`).
func getContainerStatus(_ name: String) throws -> String {
try inspectContainer(name).status.state
}
/// Returns the `configuration.id` for a container.
func getContainerId(_ name: String) throws -> String {
try inspectContainer(name).configuration.id
}
}