mirror of
https://github.com/moghtech/komodo.git
synced 2026-10-31 05:05:57 -05:00
917 lines
23 KiB
Rust
917 lines
23 KiB
Rust
use std::{borrow::Cow, fmt::Write, path::PathBuf};
|
|
|
|
use anyhow::{Context, anyhow};
|
|
use command::{
|
|
KomodoCommandMode, run_komodo_command_with_sanitization,
|
|
run_komodo_shell_command, run_komodo_standard_command,
|
|
};
|
|
use formatting::format_serror;
|
|
use git::write_commit_file;
|
|
use interpolate::Interpolator;
|
|
use komodo_client::{
|
|
entities::{
|
|
FileContents, RepoExecutionResponse, all_logs_success,
|
|
stack::{
|
|
AdditionalEnvFile, ComposeFile, ComposeService,
|
|
ComposeServiceDeploy, StackRemoteFileContents,
|
|
StackServiceNames,
|
|
},
|
|
to_path_compatible_name,
|
|
update::Log,
|
|
},
|
|
parsers::parse_multiline_command,
|
|
};
|
|
use periphery_client::api::{DeployStackResponse, compose::*};
|
|
use resolver_api::Resolve;
|
|
use shell_escape::unix::escape;
|
|
use tracing::Instrument;
|
|
|
|
use crate::{
|
|
config::periphery_config,
|
|
docker::compose::docker_compose,
|
|
helpers::{format_extra_args, format_log_grep},
|
|
stack::{
|
|
maybe_login_registry, pull_or_clone_stack, validate_files,
|
|
write::write_stack,
|
|
},
|
|
};
|
|
|
|
impl Resolve<crate::api::Args> for GetComposeLog {
|
|
async fn resolve(
|
|
self,
|
|
_: &crate::api::Args,
|
|
) -> anyhow::Result<Log> {
|
|
let GetComposeLog {
|
|
project,
|
|
services,
|
|
tail,
|
|
timestamps,
|
|
} = self;
|
|
let docker_compose = docker_compose();
|
|
let timestamps = if timestamps {
|
|
" --timestamps"
|
|
} else {
|
|
Default::default()
|
|
};
|
|
let command = format!(
|
|
"{docker_compose} -p {project} logs --tail {tail}{timestamps} {}",
|
|
services.join(" ")
|
|
);
|
|
Ok(
|
|
run_komodo_standard_command("Get Stack Log", None, command)
|
|
.await,
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Resolve<crate::api::Args> for GetComposeLogSearch {
|
|
async fn resolve(
|
|
self,
|
|
_: &crate::api::Args,
|
|
) -> anyhow::Result<Log> {
|
|
let GetComposeLogSearch {
|
|
project,
|
|
services,
|
|
terms,
|
|
combinator,
|
|
invert,
|
|
timestamps,
|
|
} = self;
|
|
let docker_compose = docker_compose();
|
|
let grep = format_log_grep(&terms, combinator, invert);
|
|
let timestamps = if timestamps {
|
|
" --timestamps"
|
|
} else {
|
|
Default::default()
|
|
};
|
|
let command = format!(
|
|
"{docker_compose} -p {project} logs --tail 5000{timestamps} {} 2>&1 | {grep}",
|
|
services.join(" ")
|
|
);
|
|
Ok(
|
|
run_komodo_shell_command("Search Stack Log", None, command)
|
|
.await,
|
|
)
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
impl Resolve<crate::api::Args> for GetComposeContentsOnHost {
|
|
async fn resolve(
|
|
self,
|
|
_: &crate::api::Args,
|
|
) -> anyhow::Result<GetComposeContentsOnHostResponse> {
|
|
let GetComposeContentsOnHost {
|
|
name,
|
|
run_directory,
|
|
file_paths,
|
|
} = self;
|
|
let root = periphery_config()
|
|
.stack_dir()
|
|
.join(to_path_compatible_name(&name));
|
|
let run_directory =
|
|
root.join(&run_directory).components().collect::<PathBuf>();
|
|
|
|
let mut res = GetComposeContentsOnHostResponse::default();
|
|
|
|
for file in file_paths {
|
|
let full_path = run_directory
|
|
.join(&file.path)
|
|
.components()
|
|
.collect::<PathBuf>();
|
|
match tokio::fs::read_to_string(&full_path).await.with_context(
|
|
|| {
|
|
format!(
|
|
"Failed to read compose file contents at {full_path:?}"
|
|
)
|
|
},
|
|
) {
|
|
Ok(contents) => {
|
|
// The path we store here has to be the same as incoming file path in the array,
|
|
// in order for WriteComposeContentsToHost to write to the correct path.
|
|
res.contents.push(StackRemoteFileContents {
|
|
path: file.path,
|
|
contents,
|
|
services: file.services,
|
|
requires: file.requires,
|
|
});
|
|
}
|
|
Err(e) => {
|
|
res.errors.push(FileContents {
|
|
path: file.path,
|
|
contents: format_serror(&e.into()),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
impl Resolve<crate::api::Args> for WriteComposeContentsToHost {
|
|
#[instrument(
|
|
"WriteComposeContentsToHost",
|
|
skip_all,
|
|
fields(
|
|
id = args.id.to_string(),
|
|
core = args.core,
|
|
stack = self.name,
|
|
run_directory = self.run_directory,
|
|
file_path = self.file_path,
|
|
)
|
|
)]
|
|
async fn resolve(
|
|
self,
|
|
args: &crate::api::Args,
|
|
) -> anyhow::Result<Log> {
|
|
let WriteComposeContentsToHost {
|
|
name,
|
|
run_directory,
|
|
file_path,
|
|
contents,
|
|
} = self;
|
|
let file_path = periphery_config()
|
|
.stack_dir()
|
|
.join(to_path_compatible_name(&name))
|
|
.join(&run_directory)
|
|
.join(file_path)
|
|
.components()
|
|
.collect::<PathBuf>();
|
|
secret_file::write_async(&file_path, contents)
|
|
.await
|
|
.with_context(|| {
|
|
format!(
|
|
"Failed to write compose file contents to {file_path:?}"
|
|
)
|
|
})?;
|
|
Ok(Log::simple(
|
|
"Write contents to host",
|
|
format!("File contents written to {file_path:?}"),
|
|
))
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
impl Resolve<crate::api::Args> for WriteCommitComposeContents {
|
|
#[instrument(
|
|
"WriteCommitComposeContents",
|
|
skip_all,
|
|
fields(
|
|
id = args.id.to_string(),
|
|
core = args.core,
|
|
stack = &self.stack.name,
|
|
username = &self.username,
|
|
file_path = &self.file_path,
|
|
)
|
|
)]
|
|
async fn resolve(
|
|
self,
|
|
args: &crate::api::Args,
|
|
) -> anyhow::Result<RepoExecutionResponse> {
|
|
let WriteCommitComposeContents {
|
|
stack,
|
|
repo,
|
|
username,
|
|
file_path,
|
|
contents,
|
|
git_token,
|
|
} = self;
|
|
|
|
let root =
|
|
pull_or_clone_stack(&stack, repo.as_ref(), git_token, args)
|
|
.await?;
|
|
|
|
let file_path = stack
|
|
.config
|
|
.run_directory
|
|
.parse::<PathBuf>()
|
|
.context("Run directory is not a valid path")?
|
|
.join(&file_path);
|
|
|
|
let msg = if let Some(username) = username {
|
|
format!("{username}: Write Compose File")
|
|
} else {
|
|
"Write Compose File".to_string()
|
|
};
|
|
|
|
write_commit_file(
|
|
&msg,
|
|
&root,
|
|
&file_path,
|
|
&contents,
|
|
&stack.config.branch,
|
|
)
|
|
.await
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
impl Resolve<crate::api::Args> for ComposePull {
|
|
#[instrument(
|
|
"ComposePull",
|
|
skip_all,
|
|
fields(
|
|
id = args.id.to_string(),
|
|
core = args.core,
|
|
stack = &self.stack.name,
|
|
services = format!("{:?}", self.services),
|
|
)
|
|
)]
|
|
async fn resolve(
|
|
self,
|
|
args: &crate::api::Args,
|
|
) -> anyhow::Result<ComposePullResponse> {
|
|
let ComposePull {
|
|
mut stack,
|
|
repo,
|
|
services,
|
|
git_token,
|
|
registry_token,
|
|
mut replacers,
|
|
} = self;
|
|
|
|
let mut res = ComposePullResponse::default();
|
|
|
|
let mut interpolator =
|
|
Interpolator::new(None, &periphery_config().secrets);
|
|
// Only interpolate Stack. Repo interpolation will be handled
|
|
// by the CloneRepo / PullOrCloneRepo call.
|
|
interpolator
|
|
.interpolate_stack(&mut stack)?
|
|
.push_logs(&mut res.logs);
|
|
replacers.extend(interpolator.secret_replacers);
|
|
|
|
let (run_directory, env_file_path) = match write_stack(
|
|
&stack,
|
|
repo.as_ref(),
|
|
git_token,
|
|
replacers.clone(),
|
|
&mut res,
|
|
args,
|
|
)
|
|
.await
|
|
{
|
|
Ok(res) => res,
|
|
Err(e) => {
|
|
res
|
|
.logs
|
|
.push(Log::error("Write Stack", format_serror(&e.into())));
|
|
return Ok(res);
|
|
}
|
|
};
|
|
|
|
// Canonicalize the path to ensure it exists, and is the cleanest path to the run directory.
|
|
let run_directory = run_directory.canonicalize().context(
|
|
"Failed to validate run directory on host after stack write (canonicalize error)",
|
|
)?;
|
|
|
|
let file_paths = stack
|
|
.all_tracked_file_paths()
|
|
.into_iter()
|
|
.map(|path| {
|
|
(
|
|
// This will remove any intermediate uneeded '/./' in the path
|
|
run_directory.join(&path).components().collect::<PathBuf>(),
|
|
path,
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
// Validate files
|
|
for (full_path, path) in &file_paths {
|
|
if !full_path.exists() {
|
|
return Err(anyhow!("Missing compose file at {path}"));
|
|
}
|
|
}
|
|
|
|
maybe_login_registry(&stack, registry_token, &mut res.logs).await;
|
|
if !all_logs_success(&res.logs) {
|
|
return Ok(res);
|
|
}
|
|
|
|
let docker_compose = docker_compose();
|
|
|
|
let service_args = if services.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!(" {}", services.join(" "))
|
|
};
|
|
|
|
let file_args = stack.compose_file_paths().join(" -f ");
|
|
|
|
let env_file_args = env_file_args(
|
|
env_file_path,
|
|
&stack.config.additional_env_files,
|
|
)?;
|
|
|
|
let project_name = stack.project_name(false);
|
|
|
|
let span = info_span!("RunComposePull");
|
|
let log = run_komodo_standard_command(
|
|
"Compose Pull",
|
|
run_directory.as_ref(),
|
|
format!(
|
|
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull{service_args}",
|
|
),
|
|
)
|
|
.instrument(span)
|
|
.await;
|
|
|
|
res.logs.push(log);
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
impl Resolve<crate::api::Args> for ComposeUp {
|
|
#[instrument(
|
|
"ComposeUp",
|
|
skip_all,
|
|
fields(
|
|
id = args.id.to_string(),
|
|
core = args.core,
|
|
stack = self.stack.name,
|
|
repo = self.repo.as_ref().map(|repo| &repo.name),
|
|
services = format!("{:?}", self.services),
|
|
)
|
|
)]
|
|
async fn resolve(
|
|
self,
|
|
args: &crate::api::Args,
|
|
) -> anyhow::Result<DeployStackResponse> {
|
|
let ComposeUp {
|
|
mut stack,
|
|
repo,
|
|
services,
|
|
git_token,
|
|
registry_token,
|
|
mut replacers,
|
|
} = self;
|
|
|
|
let mut res = DeployStackResponse::default();
|
|
|
|
let mut interpolator =
|
|
Interpolator::new(None, &periphery_config().secrets);
|
|
// Only interpolate Stack. Repo interpolation will be handled
|
|
// by the CloneRepo / PullOrCloneRepo call.
|
|
interpolator
|
|
.interpolate_stack(&mut stack)?
|
|
.push_logs(&mut res.logs);
|
|
replacers.extend(interpolator.secret_replacers);
|
|
|
|
let (run_directory, env_file_path) = match write_stack(
|
|
&stack,
|
|
repo.as_ref(),
|
|
git_token,
|
|
replacers.clone(),
|
|
&mut res,
|
|
args,
|
|
)
|
|
.await
|
|
{
|
|
Ok(res) => res,
|
|
Err(e) => {
|
|
res
|
|
.logs
|
|
.push(Log::error("Write Stack", format_serror(&e.into())));
|
|
return Ok(res);
|
|
}
|
|
};
|
|
|
|
// Canonicalize the path to ensure it exists, and is the cleanest path to the run directory.
|
|
let run_directory = run_directory.canonicalize().context(
|
|
"Failed to validate run directory on host after stack write (canonicalize error)",
|
|
)?;
|
|
|
|
validate_files(&stack, &run_directory, &mut res).await;
|
|
if !all_logs_success(&res.logs) {
|
|
return Ok(res);
|
|
}
|
|
|
|
maybe_login_registry(&stack, registry_token, &mut res.logs).await;
|
|
if !all_logs_success(&res.logs) {
|
|
return Ok(res);
|
|
}
|
|
|
|
// Pre deploy
|
|
if !stack.config.pre_deploy.is_none() {
|
|
let pre_deploy_path =
|
|
run_directory.join(&stack.config.pre_deploy.path);
|
|
let span = info_span!("ExecutePreDeploy");
|
|
if let Some(log) = run_komodo_command_with_sanitization(
|
|
"Pre Deploy",
|
|
pre_deploy_path.as_path(),
|
|
&stack.config.pre_deploy.command,
|
|
KomodoCommandMode::Multiline,
|
|
&replacers,
|
|
)
|
|
.instrument(span)
|
|
.await
|
|
{
|
|
res.logs.push(log);
|
|
if !all_logs_success(&res.logs) {
|
|
return Ok(res);
|
|
}
|
|
};
|
|
}
|
|
|
|
let docker_compose = docker_compose();
|
|
|
|
let service_args = if services.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!(" {}", services.join(" "))
|
|
};
|
|
|
|
let file_args = stack.compose_file_paths().join(" -f ");
|
|
|
|
// This will be the last project name, which is the one that needs to be destroyed.
|
|
// Might be different from the current project name, if user renames stack / changes to custom project name.
|
|
let last_project_name = stack.project_name(false);
|
|
let project_name = stack.project_name(true);
|
|
|
|
let env_file_args = env_file_args(
|
|
env_file_path,
|
|
&stack.config.additional_env_files,
|
|
)?;
|
|
|
|
// Uses 'docker compose config' command to extract services (including image)
|
|
// after performing interpolation
|
|
{
|
|
let command = format!(
|
|
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} config",
|
|
);
|
|
let span = info_span!("GetComposeConfig", command);
|
|
let Some(config_log) = run_komodo_command_with_sanitization(
|
|
"Compose Config",
|
|
run_directory.as_path(),
|
|
command,
|
|
KomodoCommandMode::Standard,
|
|
&replacers,
|
|
)
|
|
.instrument(span)
|
|
.await
|
|
else {
|
|
// Only reachable if command is empty,
|
|
// not the case since it is provided above.
|
|
unreachable!()
|
|
};
|
|
if !config_log.success {
|
|
res.logs.push(config_log);
|
|
return Ok(res);
|
|
}
|
|
let compose =
|
|
serde_yaml_ng::from_str::<ComposeFile>(&config_log.stdout)
|
|
.context("Failed to parse compose contents")?;
|
|
// Store sanitized compose config output
|
|
res.merged_config = Some(config_log.stdout);
|
|
for (
|
|
service_name,
|
|
ComposeService {
|
|
container_name,
|
|
deploy,
|
|
image,
|
|
},
|
|
) in compose.services
|
|
{
|
|
let image = image.unwrap_or_default();
|
|
match deploy {
|
|
Some(ComposeServiceDeploy {
|
|
replicas: Some(replicas),
|
|
}) if replicas > 1 => {
|
|
for i in 1..1 + replicas {
|
|
res.services.push(StackServiceNames {
|
|
container_name: format!(
|
|
"{project_name}-{service_name}-{i}"
|
|
),
|
|
service_name: format!("{service_name}-{i}"),
|
|
image: image.clone(),
|
|
});
|
|
}
|
|
}
|
|
_ => {
|
|
res.services.push(StackServiceNames {
|
|
container_name: container_name.unwrap_or_else(|| {
|
|
format!("{project_name}-{service_name}")
|
|
}),
|
|
service_name,
|
|
image,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if stack.config.run_build {
|
|
let build_extra_args =
|
|
format_extra_args(&stack.config.build_extra_args);
|
|
let command = format!(
|
|
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} build{build_extra_args}{service_args}",
|
|
);
|
|
let span = info_span!("ExecuteComposeBuild");
|
|
let Some(log) = run_komodo_command_with_sanitization(
|
|
"Compose Build",
|
|
run_directory.as_path(),
|
|
command,
|
|
KomodoCommandMode::Shell,
|
|
&replacers,
|
|
)
|
|
.instrument(span)
|
|
.await
|
|
else {
|
|
unreachable!()
|
|
};
|
|
res.logs.push(log);
|
|
if !all_logs_success(&res.logs) {
|
|
return Ok(res);
|
|
}
|
|
}
|
|
|
|
// Pull images before deploying
|
|
if stack.config.auto_pull {
|
|
// Pull images before destroying to minimize downtime.
|
|
// If this fails, do not continue.
|
|
let command = format!(
|
|
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull{service_args}",
|
|
);
|
|
let span = info_span!("RunComposePull");
|
|
let log = run_komodo_standard_command(
|
|
"Compose Pull",
|
|
run_directory.as_ref(),
|
|
command,
|
|
)
|
|
.instrument(span)
|
|
.await;
|
|
res.logs.push(log);
|
|
if !all_logs_success(&res.logs) {
|
|
return Ok(res);
|
|
}
|
|
}
|
|
|
|
if stack.config.destroy_before_deploy
|
|
// Also check if project name changed, which also requires taking down.
|
|
|| last_project_name != project_name
|
|
{
|
|
// Take down the existing compose stack.
|
|
// This one tries to use the previously deployed service name, to ensure the right stack is taken down.
|
|
compose_down(&last_project_name, &services, &mut res)
|
|
.await
|
|
.context("Failed to take down existing compose stack")?;
|
|
}
|
|
|
|
// Run compose up
|
|
let extra_args = format_extra_args(&stack.config.extra_args);
|
|
let mut command = format!(
|
|
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} up -d{extra_args}{service_args}",
|
|
);
|
|
|
|
// Apply compose cmd wrapper if configured
|
|
let compose_cmd_wrapper =
|
|
parse_multiline_command(&stack.config.compose_cmd_wrapper);
|
|
if !compose_cmd_wrapper.is_empty() {
|
|
if !compose_cmd_wrapper.contains("[[COMPOSE_COMMAND]]") {
|
|
res.logs.push(Log::error(
|
|
"Compose Command Wrapper",
|
|
"compose_cmd_wrapper is configured but does not contain [[COMPOSE_COMMAND]] placeholder. The placeholder is required to inject the compose command.".to_string(),
|
|
));
|
|
return Ok(res);
|
|
}
|
|
command =
|
|
compose_cmd_wrapper.replace("[[COMPOSE_COMMAND]]", &command);
|
|
}
|
|
|
|
let span = info_span!("ExecuteComposeUp");
|
|
let Some(log) = run_komodo_command_with_sanitization(
|
|
"Compose Up",
|
|
run_directory.as_path(),
|
|
command,
|
|
KomodoCommandMode::Shell,
|
|
&replacers,
|
|
)
|
|
.instrument(span)
|
|
.await
|
|
else {
|
|
unreachable!()
|
|
};
|
|
|
|
res.deployed = log.success;
|
|
res.logs.push(log);
|
|
|
|
if res.deployed && !stack.config.post_deploy.is_none() {
|
|
let post_deploy_path =
|
|
run_directory.join(&stack.config.post_deploy.path);
|
|
let span = info_span!("ExecutePostDeploy");
|
|
if let Some(log) = run_komodo_command_with_sanitization(
|
|
"Post Deploy",
|
|
post_deploy_path.as_path(),
|
|
&stack.config.post_deploy.command,
|
|
KomodoCommandMode::Multiline,
|
|
&replacers,
|
|
)
|
|
.instrument(span)
|
|
.await
|
|
{
|
|
res.logs.push(log);
|
|
};
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
impl Resolve<crate::api::Args> for ComposeExecution {
|
|
#[instrument(
|
|
"ComposeExecution",
|
|
skip_all,
|
|
fields(
|
|
id = args.id.to_string(),
|
|
core = args.core,
|
|
project = self.project,
|
|
command = self.command,
|
|
)
|
|
)]
|
|
async fn resolve(
|
|
self,
|
|
args: &crate::api::Args,
|
|
) -> anyhow::Result<Log> {
|
|
let ComposeExecution { project, command } = self;
|
|
let docker_compose = docker_compose();
|
|
let log = run_komodo_standard_command(
|
|
"Compose Command",
|
|
None,
|
|
format!("{docker_compose} -p {project} {command}"),
|
|
)
|
|
.await;
|
|
Ok(log)
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
impl Resolve<crate::api::Args> for ComposeRun {
|
|
#[instrument(
|
|
"ComposeRun",
|
|
skip_all,
|
|
fields(
|
|
id = args.id.to_string(),
|
|
core = args.core,
|
|
stack = self.stack.name,
|
|
repo = self.repo.as_ref().map(|repo| &repo.name),
|
|
service = &self.service
|
|
)
|
|
)]
|
|
async fn resolve(
|
|
self,
|
|
args: &crate::api::Args,
|
|
) -> anyhow::Result<Log> {
|
|
let ComposeRun {
|
|
mut stack,
|
|
repo,
|
|
git_token,
|
|
registry_token,
|
|
mut replacers,
|
|
service,
|
|
command,
|
|
no_tty,
|
|
no_deps,
|
|
detach,
|
|
service_ports,
|
|
env,
|
|
workdir,
|
|
user,
|
|
entrypoint,
|
|
pull,
|
|
} = self;
|
|
|
|
let mut interpolator =
|
|
Interpolator::new(None, &periphery_config().secrets);
|
|
interpolator
|
|
.interpolate_stack(&mut stack)?
|
|
.push_logs(&mut Vec::new());
|
|
replacers.extend(interpolator.secret_replacers);
|
|
|
|
let mut res = ComposeRunResponse::default();
|
|
let (run_directory, env_file_path) = match write_stack(
|
|
&stack,
|
|
repo.as_ref(),
|
|
git_token,
|
|
replacers.clone(),
|
|
&mut res,
|
|
args,
|
|
)
|
|
.await
|
|
{
|
|
Ok(res) => res,
|
|
Err(e) => {
|
|
return Ok(Log::error(
|
|
"Write Stack",
|
|
format_serror(&e.into()),
|
|
));
|
|
}
|
|
};
|
|
|
|
let run_directory = run_directory.canonicalize().context(
|
|
"Failed to validate run directory on host after stack write (canonicalize error)",
|
|
)?;
|
|
|
|
maybe_login_registry(&stack, registry_token, &mut Vec::new())
|
|
.await;
|
|
|
|
let docker_compose = docker_compose();
|
|
|
|
let file_args = if stack.config.file_paths.is_empty() {
|
|
String::from("compose.yaml")
|
|
} else {
|
|
stack.config.file_paths.join(" -f ")
|
|
};
|
|
|
|
let env_file_args = env_file_args(
|
|
env_file_path,
|
|
&stack.config.additional_env_files,
|
|
)?;
|
|
|
|
let project_name = stack.project_name(true);
|
|
|
|
if pull.unwrap_or_default() {
|
|
let pull_log = run_komodo_standard_command(
|
|
"Compose Pull",
|
|
run_directory.as_ref(),
|
|
format!(
|
|
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} pull {service}",
|
|
),
|
|
)
|
|
.await;
|
|
if !pull_log.success {
|
|
return Ok(pull_log);
|
|
}
|
|
}
|
|
|
|
let mut run_flags = String::from(" --rm");
|
|
if detach.unwrap_or_default() {
|
|
run_flags.push_str(" -d");
|
|
}
|
|
if no_tty.unwrap_or_default() {
|
|
run_flags.push_str(" --no-tty");
|
|
}
|
|
if no_deps.unwrap_or_default() {
|
|
run_flags.push_str(" --no-deps");
|
|
}
|
|
if service_ports.unwrap_or_default() {
|
|
run_flags.push_str(" --service-ports");
|
|
}
|
|
if let Some(dir) = workdir.as_ref() {
|
|
run_flags.push_str(&format!(" --workdir {dir}"));
|
|
}
|
|
if let Some(user) = user.as_ref() {
|
|
run_flags.push_str(&format!(" --user {user}"));
|
|
}
|
|
if let Some(entrypoint) = entrypoint.as_ref() {
|
|
run_flags.push_str(&format!(" --entrypoint {entrypoint}"));
|
|
}
|
|
if let Some(env) = env {
|
|
for (k, v) in env {
|
|
run_flags.push_str(&format!(" -e {}={} ", k, v));
|
|
}
|
|
}
|
|
|
|
let command_args = command
|
|
.as_ref()
|
|
.filter(|v| !v.is_empty())
|
|
.map(|argv| {
|
|
let joined = argv
|
|
.iter()
|
|
.map(|s| escape(Cow::Borrowed(s)).into_owned())
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
format!(" {joined}")
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let command = format!(
|
|
"{docker_compose} -p {project_name} -f {file_args}{env_file_args} run{run_flags} {service}{command_args}",
|
|
);
|
|
|
|
let span = info_span!("RunComposeRun", command);
|
|
let Some(log) = run_komodo_command_with_sanitization(
|
|
"Compose Run",
|
|
run_directory.as_path(),
|
|
command,
|
|
KomodoCommandMode::Shell,
|
|
&replacers,
|
|
)
|
|
.instrument(span)
|
|
.await
|
|
else {
|
|
unreachable!()
|
|
};
|
|
|
|
Ok(log)
|
|
}
|
|
}
|
|
|
|
fn env_file_args(
|
|
env_file_path: Option<&str>,
|
|
additional_env_files: &[AdditionalEnvFile],
|
|
) -> anyhow::Result<String> {
|
|
let mut res = String::new();
|
|
|
|
// Add additional env files (except komodo's own, which comes last)
|
|
for file in additional_env_files
|
|
.iter()
|
|
.filter(|f| env_file_path != Some(f.path.as_str()))
|
|
{
|
|
let path = &file.path;
|
|
write!(res, " --env-file {path}").with_context(|| {
|
|
format!("Failed to write --env-file arg for {path}")
|
|
})?;
|
|
}
|
|
|
|
// Add komodo's env file last for highest priority
|
|
if let Some(file) = env_file_path {
|
|
write!(res, " --env-file {file}").with_context(|| {
|
|
format!("Failed to write --env-file arg for {file}")
|
|
})?;
|
|
}
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
#[instrument("ComposeDown", skip(res))]
|
|
async fn compose_down(
|
|
project: &str,
|
|
services: &[String],
|
|
res: &mut DeployStackResponse,
|
|
) -> anyhow::Result<()> {
|
|
let docker_compose = docker_compose();
|
|
let service_args = if services.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!(" {}", services.join(" "))
|
|
};
|
|
let log = run_komodo_standard_command(
|
|
"Compose Down",
|
|
None,
|
|
format!("{docker_compose} -p {project} down{service_args}"),
|
|
)
|
|
.await;
|
|
let success = log.success;
|
|
res.logs.push(log);
|
|
if !success {
|
|
return Err(anyhow!(
|
|
"Failed to bring down existing container(s) with docker compose down. Stopping run."
|
|
));
|
|
}
|
|
|
|
Ok(())
|
|
}
|