mirror of
https://github.com/apple/container.git
synced 2026-10-14 01:59:18 +00:00
This expands `cctl run` to utilize entrypoint and cmd when present in the OCI image, instead of always defaulting to `/bin/sh`. The user can still provide explicit commands if necessary to override these values.
126 lines
5.5 KiB
Swift
126 lines
5.5 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2026 Apple Inc. and the Containerization 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 Foundation
|
|
|
|
#if os(macOS)
|
|
extension IntegrationSuite {
|
|
|
|
/// Image used by the `cctl run` command-resolution tests. It declares no
|
|
/// ENTRYPOINT and `CMD ["/bin/sh"]`, which is what makes the default-command
|
|
/// case observable.
|
|
static let cctlRunImage = "docker.io/library/alpine:3.16"
|
|
|
|
/// With no trailing command, `cctl run` runs the image's ENTRYPOINT + CMD.
|
|
///
|
|
/// The alpine test image declares no ENTRYPOINT and `CMD ["/bin/sh"]`, so a
|
|
/// bare `cctl run` should land in a shell. Feeding a command on stdin proves
|
|
/// it really is a shell rather than something that merely exited cleanly.
|
|
func testCctlRunUsesImageDefaultCommand() async throws {
|
|
let marker = "DEFAULT_CMD_RAN"
|
|
let (output, exitCode) = try runCctl(
|
|
arguments: [
|
|
"run", "--kernel", self.kernel, "--id", "test-cctl-run-default-cmd",
|
|
"-i", Self.cctlRunImage,
|
|
],
|
|
input: "echo \(marker)\nexit\n"
|
|
)
|
|
|
|
guard exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "cctl run exited with \(exitCode): \(output)")
|
|
}
|
|
guard output.contains(marker) else {
|
|
throw IntegrationError.assert(msg: "expected the image's CMD shell to run, got: \(output)")
|
|
}
|
|
}
|
|
|
|
/// A trailing command replaces the image's CMD.
|
|
func testCctlRunExplicitCommandOverridesDefault() async throws {
|
|
let marker = "EXPLICIT_COMMAND_RAN"
|
|
let (output, exitCode) = try runCctl(arguments: [
|
|
"run", "--kernel", self.kernel, "--id", "test-cctl-run-explicit-command",
|
|
"-i", Self.cctlRunImage,
|
|
"/bin/echo", marker,
|
|
])
|
|
|
|
guard exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "cctl run exited with \(exitCode): \(output)")
|
|
}
|
|
guard output.contains(marker) else {
|
|
throw IntegrationError.assert(msg: "expected the explicit command to run, got: \(output)")
|
|
}
|
|
}
|
|
|
|
/// An image declaring neither ENTRYPOINT nor CMD has nothing to run, so a
|
|
/// bare `cctl run` must say so rather than falling back to a hardcoded
|
|
/// shell. `vminit:latest` is built by `cctl rootfs create`, which sets only
|
|
/// labels on the image config — no command of any kind.
|
|
///
|
|
/// This is the case that cannot pass under the old behaviour, where the
|
|
/// command defaulted to `/bin/sh` regardless of the image.
|
|
func testCctlRunWithoutEntrypointOrCmdFails() async throws {
|
|
let (output, exitCode) = try runCctl(arguments: [
|
|
"run", "--kernel", self.kernel, "--id", "test-cctl-run-no-command",
|
|
"-i", Self.initImage,
|
|
])
|
|
|
|
guard exitCode != 0 else {
|
|
throw IntegrationError.assert(msg: "expected a failure for an image with no entrypoint or cmd: \(output)")
|
|
}
|
|
guard output.contains("declares no entrypoint or cmd") else {
|
|
throw IntegrationError.assert(msg: "expected an entrypoint/cmd error, got: \(output)")
|
|
}
|
|
}
|
|
|
|
/// `--entrypoint` replaces the image's ENTRYPOINT while leaving its CMD in
|
|
/// place. The alpine test image has no ENTRYPOINT and `CMD ["/bin/sh"]`, so
|
|
/// overriding with `/bin/echo` and passing no command must print the CMD
|
|
/// rather than start a shell — which is what makes the appending observable.
|
|
func testCctlRunEntrypointOverrideKeepsImageCmd() async throws {
|
|
let (output, exitCode) = try runCctl(arguments: [
|
|
"run", "--kernel", self.kernel, "--id", "test-cctl-run-entrypoint",
|
|
"-i", Self.cctlRunImage,
|
|
"--entrypoint", "/bin/echo",
|
|
])
|
|
|
|
guard exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "cctl run exited with \(exitCode): \(output)")
|
|
}
|
|
guard output.contains("/bin/sh") else {
|
|
throw IntegrationError.assert(msg: "expected the image's cmd to be appended to the override, got: \(output)")
|
|
}
|
|
}
|
|
|
|
/// An override plus a trailing command: the override supplies the executable
|
|
/// and the command replaces the image's CMD.
|
|
func testCctlRunEntrypointOverrideWithCommand() async throws {
|
|
let marker = "ENTRYPOINT_OVERRIDE_RAN"
|
|
let (output, exitCode) = try runCctl(arguments: [
|
|
"run", "--kernel", self.kernel, "--id", "test-cctl-run-entrypoint-command",
|
|
"-i", Self.cctlRunImage,
|
|
"--entrypoint", "/bin/echo", marker,
|
|
])
|
|
|
|
guard exitCode == 0 else {
|
|
throw IntegrationError.assert(msg: "cctl run exited with \(exitCode): \(output)")
|
|
}
|
|
guard output.contains(marker) else {
|
|
throw IntegrationError.assert(msg: "expected the override to run with the trailing command, got: \(output)")
|
|
}
|
|
}
|
|
}
|
|
#endif
|