Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 58 additions & 11 deletions src/commands/compose_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ pub(crate) use crate::commands::container_name::LONGEST_CONTAINER_NAME_SUFFIX;
use crate::commands::dotenv::read_dotenv;
use crate::i18n::Messages;

/// The executable every Docker invocation runs.
const DOCKER_BIN: &str = "docker";
/// The executable every Docker invocation runs when the caller names
/// none.
///
/// The only spelling of the literal the executable seam uses: the
/// spawn helpers default to it, and the context values production
/// builds start out holding it. A spawn site never reads it — it
/// spawns whatever its caller supplied.
pub(crate) const DOCKER_BIN: &str = "docker";

/// The `docker` subcommand every Compose invocation starts with.
///
Expand Down Expand Up @@ -317,7 +323,22 @@ impl ComposeInvocation {
/// rename this invocation's containers out from under the recorded
/// identity. The order is the enforcement; there is no other guard.
pub(crate) fn command(&self, extra_env: &[(&str, &str)]) -> ProcessCommand {
let mut command = ProcessCommand::new(DOCKER_BIN);
self.command_with_exec(extra_env, Path::new(DOCKER_BIN))
}

/// Builds the command for this invocation, spawning the executable
/// `docker` names rather than whatever `PATH` resolves.
///
/// The executable is the only thing that varies: `extra_env`, the
/// argument vector and the instance pin behave exactly as they do
/// in [`ComposeInvocation::command`], which is this function with
/// the default supplied.
pub(crate) fn command_with_exec(
&self,
extra_env: &[(&str, &str)],
docker: &Path,
) -> ProcessCommand {
let mut command = ProcessCommand::new(docker);
command.args(&self.args);
for (key, value) in extra_env {
command.env(key, value);
Expand Down Expand Up @@ -372,6 +393,27 @@ mod tests {
.expect("write .env");
}

/// The compose spawn site runs whatever the caller named, and
/// [`ComposeInvocation::command`] is that call with the default
/// supplied — so the program the two build differs only by what was
/// asked for, and nothing else about the invocation moves.
#[test]
fn compose_command_runs_the_supplied_executable() {
let identity = ComposeIdentity::for_instance(DEFAULT_INSTANCE_NAME);
let invocation = identity.compose(&["docker-compose.yml"], None, &["up", "-d"]);

let default = invocation.command(&[]);
assert_eq!(default.get_program(), "docker");

let supplied = invocation.command_with_exec(&[], Path::new("/tmp/fake-docker"));
assert_eq!(supplied.get_program(), "/tmp/fake-docker");
assert_eq!(
supplied.get_args().collect::<Vec<_>>(),
default.get_args().collect::<Vec<_>>(),
"only the executable may differ from the default path"
);
}

#[test]
fn instance_name_accepts_the_documented_character_set() {
let messages = test_messages();
Expand Down Expand Up @@ -788,11 +830,15 @@ mod tests {
}

/// The guard that makes the property above unbypassable: the
/// invocation hands out no argument vector, so `command()` — the one
/// method that sets the environment — is the only way to spawn one.
/// A new accessor returning the args would let a call site build a
/// compose vector and spawn it bare, which is exactly what this
/// module exists to prevent.
/// invocation hands out no argument vector, so the command builders
/// — the only methods that set the environment — are the only way to
/// spawn one. A new accessor returning the args would let a call
/// site build a compose vector and spawn it bare, which is exactly
/// what this module exists to prevent.
///
/// `command_with_exec` is on the list because it is the same builder
/// with the executable named; `command` delegates to it, so the
/// instance pin is applied once, in one place, on both paths.
#[test]
fn compose_invocation_exposes_only_the_command_builder() {
let source = std::fs::read_to_string(
Expand Down Expand Up @@ -823,9 +869,10 @@ mod tests {
.collect();
assert_eq!(
methods,
vec!["command"],
"`ComposeInvocation` must expose nothing but `command`, which is \
what pins `{INSTANCE_NAME_ENV_KEY}`; found {methods:?}"
vec!["command", "command_with_exec"],
"`ComposeInvocation` must expose nothing but its command \
builders, which are what pin `{INSTANCE_NAME_ENV_KEY}`; \
found {methods:?}"
);
}

Expand Down
59 changes: 54 additions & 5 deletions src/commands/infra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use bootroot::openbao::OpenBaoClient;

use crate::cli::args::{InfraInstallArgs, InfraUpArgs};
use crate::commands::compose_project::{
ComposeIdentity, ComposeInvocation, INSTANCE_NAME_ENV_KEY, resolve_recorded_instance_name,
validate_instance_name,
ComposeIdentity, ComposeInvocation, DOCKER_BIN, INSTANCE_NAME_ENV_KEY,
resolve_recorded_instance_name, validate_instance_name,
};
use crate::commands::constants::RESPONDER_SERVICE_NAME;
use crate::commands::dns_alias::replay_dns_aliases;
Expand Down Expand Up @@ -2067,7 +2067,24 @@ pub(crate) fn run_docker<S: AsRef<OsStr>>(
context: &str,
messages: &Messages,
) -> Result<()> {
let mut cmd = ProcessCommand::new("docker");
run_docker_with_exec(args, context, Path::new(DOCKER_BIN), messages)
}

/// Runs the executable `docker` names with a plain (non-Compose)
/// argument vector.
///
/// [`run_docker`] is this function with the default executable
/// supplied; a caller that has to name the program — a test pointing at
/// a fake, above all — reaches it through here instead. A bare program
/// name carrying no path separator is still resolved against `PATH` by
/// `Command` itself, so the default loses nothing.
pub(crate) fn run_docker_with_exec<S: AsRef<OsStr>>(
args: &[S],
context: &str,
docker: &Path,
messages: &Messages,
) -> Result<()> {
let mut cmd = ProcessCommand::new(docker);
cmd.args(args.iter().map(AsRef::as_ref));
run_to_completion(&mut cmd, context, messages)
}
Expand All @@ -2078,7 +2095,19 @@ pub(crate) fn run_compose(
context: &str,
messages: &Messages,
) -> Result<()> {
run_compose_with_env(invocation, &[], context, messages)
run_compose_with_exec(invocation, context, Path::new(DOCKER_BIN), messages)
}

/// Runs a `docker compose` invocation with `docker` as the executable.
///
/// [`run_compose`] is this function with the default supplied.
pub(crate) fn run_compose_with_exec(
invocation: &ComposeInvocation,
context: &str,
docker: &Path,
messages: &Messages,
) -> Result<()> {
run_compose_with_env_and_exec(invocation, &[], context, docker, messages)
}

/// Runs a `docker compose` invocation with additional child-environment
Expand All @@ -2095,7 +2124,27 @@ pub(crate) fn run_compose_with_env(
context: &str,
messages: &Messages,
) -> Result<()> {
run_to_completion(&mut invocation.command(env), context, messages)
run_compose_with_env_and_exec(invocation, env, context, Path::new(DOCKER_BIN), messages)
}

/// Runs a `docker compose` invocation with additional child-environment
/// entries and `docker` as the executable.
///
/// [`run_compose_with_env`] is this function with the default supplied.
/// The instance pin is applied by [`ComposeInvocation::command_with_exec`]
/// after `env`, exactly as it is on the default path.
pub(crate) fn run_compose_with_env_and_exec(
invocation: &ComposeInvocation,
env: &[(&str, &str)],
context: &str,
docker: &Path,
messages: &Messages,
) -> Result<()> {
run_to_completion(
&mut invocation.command_with_exec(env, docker),
context,
messages,
)
}

/// Spawns `command`, waits for it, and turns a non-zero exit into an
Expand Down
Loading
Loading