mirror of
https://github.com/moghtech/komodo.git
synced 2026-05-02 18:17:28 -05:00
* setup network page * add Network, Image, Container * Docker ListItems and Inspects * frontend build * dev0 * network info working * fix cargo lock * dev1 * pages for the things * implement Active in dashboard * RunBuild update trigger list refresh * rename deployment executions to StartDeployment etc * add server level container control * dev2 * add Config field to Image * can get image labels from Config.Labels * mount container page * server show resource count * add GetContainerLog api * add _AllContainers api * dev3 * move ResourceTarget to entities mod * GetResourceMatchingContainer api * connect container to resource * dev4 add volume names to container list items * ts types * volume / image / network unused management * add image history to image page * fix PruneContainers incorret Operation * update cache for server for server after server actions * dev5 * add singapore to Hetzner * implement delete single network / image / volume api * dev6 * include "in use" on Docker Lists * add docker resource delete buttons * is nice * fix volume all in use * remove google font dependency * use host networking in test compose * implement Secret Variables (hidden in logs) * remove unneeded borrow * interpolate variables / secrets into extra args / onclone / onpull / command etc * validate empty strings before SelectItem * rename everything to Komodo * rename workspace to komodo * rc1
568 lines
14 KiB
Rust
568 lines
14 KiB
Rust
use std::{
|
|
collections::HashMap,
|
|
path::{Path, PathBuf},
|
|
str::FromStr,
|
|
};
|
|
|
|
use anyhow::Context;
|
|
use command::run_komodo_command;
|
|
use formatting::{bold, format_serror, muted};
|
|
use komodo_client::entities::{
|
|
all_logs_success, environment_vars_to_string, komodo_timestamp,
|
|
to_komodo_name, update::Log, CloneArgs, EnvironmentVar,
|
|
LatestCommit, SystemCommand,
|
|
};
|
|
use run_command::async_run_command;
|
|
use tokio::fs;
|
|
use tracing::instrument;
|
|
|
|
/// Return (logs, commit hash, commit msg)
|
|
#[tracing::instrument(
|
|
level = "debug",
|
|
skip(environment, secrets, on_pull, core_replacers)
|
|
)]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn pull(
|
|
path: &Path,
|
|
branch: &Option<String>,
|
|
commit: &Option<String>,
|
|
on_pull: &Option<SystemCommand>,
|
|
environment: &[EnvironmentVar],
|
|
env_file_path: &str,
|
|
// if skip_secret_interp is none, make sure to pass None here
|
|
secrets: Option<&HashMap<String, String>>,
|
|
core_replacers: &[(String, String)],
|
|
) -> (Vec<Log>, Option<String>, Option<String>, Option<PathBuf>) {
|
|
let branch = match branch {
|
|
Some(branch) => branch.to_owned(),
|
|
None => "main".to_string(),
|
|
};
|
|
|
|
let command =
|
|
format!("cd {} && git pull -f origin {branch}", path.display());
|
|
|
|
let pull_log = run_komodo_command("git pull", command).await;
|
|
|
|
let mut logs = vec![pull_log];
|
|
|
|
if !logs[0].success {
|
|
return (logs, None, None, None);
|
|
}
|
|
|
|
if let Some(commit) = commit {
|
|
let reset_log = run_komodo_command(
|
|
"set commit",
|
|
format!("cd {} && git reset --hard {commit}", path.display()),
|
|
)
|
|
.await;
|
|
logs.push(reset_log);
|
|
}
|
|
|
|
let (hash, message) = match get_commit_hash_log(path).await {
|
|
Ok((log, hash, message)) => {
|
|
logs.push(log);
|
|
(Some(hash), Some(message))
|
|
}
|
|
Err(e) => {
|
|
logs.push(Log::simple(
|
|
"latest commit",
|
|
format_serror(
|
|
&e.context("failed to get latest commit").into(),
|
|
),
|
|
));
|
|
(None, None)
|
|
}
|
|
};
|
|
|
|
let Ok(env_file_path) = write_environment_file(
|
|
environment,
|
|
env_file_path,
|
|
secrets,
|
|
path,
|
|
&mut logs,
|
|
)
|
|
.await
|
|
else {
|
|
return (logs, hash, message, None);
|
|
};
|
|
|
|
if let Some(command) = on_pull {
|
|
if !command.path.is_empty() && !command.command.is_empty() {
|
|
let on_pull_path = path.join(&command.path);
|
|
if let Some(secrets) = secrets {
|
|
let (full_command, mut replacers) =
|
|
match svi::interpolate_variables(
|
|
&command.command,
|
|
secrets,
|
|
svi::Interpolator::DoubleBrackets,
|
|
true,
|
|
)
|
|
.context(
|
|
"failed to interpolate secrets into on_pull command",
|
|
) {
|
|
Ok(res) => res,
|
|
Err(e) => {
|
|
logs.push(Log::error(
|
|
"interpolate secrets - on_pull",
|
|
format_serror(&e.into()),
|
|
));
|
|
return (logs, hash, message, None);
|
|
}
|
|
};
|
|
replacers.extend(core_replacers.to_owned());
|
|
let mut on_pull_log = run_komodo_command(
|
|
"on pull",
|
|
format!("cd {} && {full_command}", on_pull_path.display()),
|
|
)
|
|
.await;
|
|
|
|
on_pull_log.command =
|
|
svi::replace_in_string(&on_pull_log.command, &replacers);
|
|
on_pull_log.stdout =
|
|
svi::replace_in_string(&on_pull_log.stdout, &replacers);
|
|
on_pull_log.stderr =
|
|
svi::replace_in_string(&on_pull_log.stderr, &replacers);
|
|
|
|
tracing::debug!(
|
|
"run repo on_pull command | command: {} | cwd: {:?}",
|
|
on_pull_log.command,
|
|
on_pull_path
|
|
);
|
|
|
|
logs.push(on_pull_log);
|
|
} else {
|
|
let on_pull_log = run_komodo_command(
|
|
"on pull",
|
|
format!(
|
|
"cd {} && {}",
|
|
on_pull_path.display(),
|
|
command.command
|
|
),
|
|
)
|
|
.await;
|
|
tracing::debug!(
|
|
"run repo on_pull command | command: {} | cwd: {:?}",
|
|
command.command,
|
|
on_pull_path
|
|
);
|
|
logs.push(on_pull_log);
|
|
}
|
|
}
|
|
}
|
|
|
|
(logs, hash, message, env_file_path)
|
|
}
|
|
|
|
/// (logs, commit hash, commit message, env_file_path)
|
|
pub type CloneRes =
|
|
(Vec<Log>, Option<String>, Option<String>, Option<PathBuf>);
|
|
|
|
/// returns (logs, commit hash, commit message, env_file_path)
|
|
#[tracing::instrument(
|
|
level = "debug",
|
|
skip(
|
|
clone_args,
|
|
access_token,
|
|
environment,
|
|
secrets,
|
|
core_replacers
|
|
)
|
|
)]
|
|
pub async fn clone<T>(
|
|
clone_args: T,
|
|
repo_dir: &Path,
|
|
access_token: Option<String>,
|
|
environment: &[EnvironmentVar],
|
|
env_file_path: &str,
|
|
// if skip_secret_interp is none, make sure to pass None here
|
|
secrets: Option<&HashMap<String, String>>,
|
|
core_replacers: &[(String, String)],
|
|
) -> anyhow::Result<CloneRes>
|
|
where
|
|
T: Into<CloneArgs> + std::fmt::Debug,
|
|
{
|
|
let CloneArgs {
|
|
name,
|
|
provider,
|
|
https,
|
|
repo,
|
|
branch,
|
|
commit,
|
|
destination,
|
|
on_clone,
|
|
on_pull,
|
|
..
|
|
} = clone_args.into();
|
|
|
|
let provider = provider
|
|
.as_ref()
|
|
.context("resource has no provider attached")?;
|
|
let repo =
|
|
repo.as_ref().context("resource has no repo attached")?;
|
|
let name = to_komodo_name(&name);
|
|
|
|
let repo_dir = match destination {
|
|
Some(destination) => PathBuf::from_str(&destination)
|
|
.context("destination is not valid path")?,
|
|
None => repo_dir.join(name),
|
|
};
|
|
|
|
let mut logs = clone_inner(
|
|
provider,
|
|
https,
|
|
repo,
|
|
&branch,
|
|
&commit,
|
|
&repo_dir,
|
|
access_token,
|
|
)
|
|
.await;
|
|
|
|
if !all_logs_success(&logs) {
|
|
tracing::warn!("failed to clone repo at {repo_dir:?} | {logs:?}");
|
|
return Ok((logs, None, None, None));
|
|
}
|
|
|
|
tracing::debug!("repo at {repo_dir:?} cloned");
|
|
|
|
let (hash, message) = match get_commit_hash_log(&repo_dir).await {
|
|
Ok((log, hash, message)) => {
|
|
logs.push(log);
|
|
(Some(hash), Some(message))
|
|
}
|
|
Err(e) => {
|
|
logs.push(Log::simple(
|
|
"latest commit",
|
|
format_serror(
|
|
&e.context("failed to get latest commit").into(),
|
|
),
|
|
));
|
|
(None, None)
|
|
}
|
|
};
|
|
|
|
let Ok(env_file_path) = write_environment_file(
|
|
environment,
|
|
env_file_path,
|
|
secrets,
|
|
&repo_dir,
|
|
&mut logs,
|
|
)
|
|
.await
|
|
else {
|
|
return Ok((logs, hash, message, None));
|
|
};
|
|
|
|
if let Some(command) = on_clone {
|
|
if !command.path.is_empty() && !command.command.is_empty() {
|
|
let on_clone_path = repo_dir.join(&command.path);
|
|
if let Some(secrets) = secrets {
|
|
let (full_command, mut replacers) =
|
|
svi::interpolate_variables(
|
|
&command.command,
|
|
secrets,
|
|
svi::Interpolator::DoubleBrackets,
|
|
true,
|
|
)
|
|
.context(
|
|
"failed to interpolate secrets into on_clone command",
|
|
)?;
|
|
replacers.extend(core_replacers.to_owned());
|
|
let mut on_clone_log = run_komodo_command(
|
|
"on clone",
|
|
format!("cd {} && {full_command}", on_clone_path.display()),
|
|
)
|
|
.await;
|
|
|
|
on_clone_log.command =
|
|
svi::replace_in_string(&on_clone_log.command, &replacers);
|
|
on_clone_log.stdout =
|
|
svi::replace_in_string(&on_clone_log.stdout, &replacers);
|
|
on_clone_log.stderr =
|
|
svi::replace_in_string(&on_clone_log.stderr, &replacers);
|
|
|
|
tracing::debug!(
|
|
"run repo on_clone command | command: {} | cwd: {:?}",
|
|
on_clone_log.command,
|
|
on_clone_path
|
|
);
|
|
|
|
logs.push(on_clone_log);
|
|
} else {
|
|
let on_clone_log = run_komodo_command(
|
|
"on clone",
|
|
format!(
|
|
"cd {} && {}",
|
|
on_clone_path.display(),
|
|
command.command
|
|
),
|
|
)
|
|
.await;
|
|
tracing::debug!(
|
|
"run repo on_clone command | command: {} | cwd: {:?}",
|
|
command.command,
|
|
on_clone_path
|
|
);
|
|
logs.push(on_clone_log);
|
|
}
|
|
}
|
|
}
|
|
if let Some(command) = on_pull {
|
|
if !command.path.is_empty() && !command.command.is_empty() {
|
|
let on_pull_path = repo_dir.join(&command.path);
|
|
if let Some(secrets) = secrets {
|
|
let (full_command, mut replacers) =
|
|
svi::interpolate_variables(
|
|
&command.command,
|
|
secrets,
|
|
svi::Interpolator::DoubleBrackets,
|
|
true,
|
|
)
|
|
.context(
|
|
"failed to interpolate secrets into on_pull command",
|
|
)?;
|
|
replacers.extend(core_replacers.to_owned());
|
|
let mut on_pull_log = run_komodo_command(
|
|
"on pull",
|
|
format!("cd {} && {full_command}", on_pull_path.display()),
|
|
)
|
|
.await;
|
|
|
|
on_pull_log.command =
|
|
svi::replace_in_string(&on_pull_log.command, &replacers);
|
|
on_pull_log.stdout =
|
|
svi::replace_in_string(&on_pull_log.stdout, &replacers);
|
|
on_pull_log.stderr =
|
|
svi::replace_in_string(&on_pull_log.stderr, &replacers);
|
|
|
|
tracing::debug!(
|
|
"run repo on_pull command | command: {} | cwd: {:?}",
|
|
on_pull_log.command,
|
|
on_pull_path
|
|
);
|
|
|
|
logs.push(on_pull_log);
|
|
} else {
|
|
let on_pull_log = run_komodo_command(
|
|
"on pull",
|
|
format!(
|
|
"cd {} && {}",
|
|
on_pull_path.display(),
|
|
command.command
|
|
),
|
|
)
|
|
.await;
|
|
tracing::debug!(
|
|
"run repo on_pull command | command: {} | cwd: {:?}",
|
|
command.command,
|
|
on_pull_path
|
|
);
|
|
logs.push(on_pull_log);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok((logs, hash, message, env_file_path))
|
|
}
|
|
|
|
#[tracing::instrument(
|
|
level = "debug",
|
|
skip(destination, access_token)
|
|
)]
|
|
async fn clone_inner(
|
|
provider: &str,
|
|
https: bool,
|
|
repo: &str,
|
|
branch: &Option<String>,
|
|
commit: &Option<String>,
|
|
destination: &Path,
|
|
access_token: Option<String>,
|
|
) -> Vec<Log> {
|
|
let _ = std::fs::remove_dir_all(destination);
|
|
let access_token_at = match &access_token {
|
|
Some(token) => format!("{token}@"),
|
|
None => String::new(),
|
|
};
|
|
let branch = match branch {
|
|
Some(branch) => format!(" -b {branch}"),
|
|
None => String::new(),
|
|
};
|
|
let protocol = if https { "https" } else { "http" };
|
|
let repo_url =
|
|
format!("{protocol}://{access_token_at}{provider}/{repo}.git");
|
|
let command =
|
|
format!("git clone {repo_url} {}{branch}", destination.display());
|
|
let start_ts = komodo_timestamp();
|
|
let output = async_run_command(&command).await;
|
|
let success = output.success();
|
|
let (command, stderr) = if !access_token_at.is_empty() {
|
|
// know that access token can't be none if access token non-empty
|
|
let access_token = access_token.unwrap();
|
|
(
|
|
command.replace(&access_token, "<TOKEN>"),
|
|
output.stderr.replace(&access_token, "<TOKEN>"),
|
|
)
|
|
} else {
|
|
(command, output.stderr)
|
|
};
|
|
let mut logs = vec![Log {
|
|
stage: "clone repo".to_string(),
|
|
command,
|
|
success,
|
|
stdout: output.stdout,
|
|
stderr,
|
|
start_ts,
|
|
end_ts: komodo_timestamp(),
|
|
}];
|
|
|
|
if !logs[0].success {
|
|
return logs;
|
|
}
|
|
|
|
if let Some(commit) = commit {
|
|
let reset_log = run_komodo_command(
|
|
"set commit",
|
|
format!(
|
|
"cd {} && git reset --hard {commit}",
|
|
destination.display()
|
|
),
|
|
)
|
|
.await;
|
|
logs.push(reset_log);
|
|
}
|
|
|
|
logs
|
|
}
|
|
|
|
#[instrument(level = "debug")]
|
|
pub async fn get_commit_hash_info(
|
|
repo_dir: &Path,
|
|
) -> anyhow::Result<LatestCommit> {
|
|
let command = format!("cd {} && git rev-parse --short HEAD && git rev-parse HEAD && git log -1 --pretty=%B", repo_dir.display());
|
|
let output = async_run_command(&command).await;
|
|
let mut split = output.stdout.split('\n');
|
|
let (hash, _, message) = (
|
|
split
|
|
.next()
|
|
.context("failed to get short commit hash")?
|
|
.to_string(),
|
|
split.next().context("failed to get long commit hash")?,
|
|
split
|
|
.next()
|
|
.context("failed to get commit message")?
|
|
.to_string(),
|
|
);
|
|
Ok(LatestCommit { hash, message })
|
|
}
|
|
|
|
/// returns (Log, commit hash, commit message)
|
|
#[instrument(level = "debug")]
|
|
pub async fn get_commit_hash_log(
|
|
repo_dir: &Path,
|
|
) -> anyhow::Result<(Log, String, String)> {
|
|
let start_ts = komodo_timestamp();
|
|
let command = format!("cd {} && git rev-parse --short HEAD && git rev-parse HEAD && git log -1 --pretty=%B", repo_dir.display());
|
|
let output = async_run_command(&command).await;
|
|
let mut split = output.stdout.split('\n');
|
|
let (short_hash, _, msg) = (
|
|
split
|
|
.next()
|
|
.context("failed to get short commit hash")?
|
|
.to_string(),
|
|
split.next().context("failed to get long commit hash")?,
|
|
split
|
|
.next()
|
|
.context("failed to get commit message")?
|
|
.to_string(),
|
|
);
|
|
let log = Log {
|
|
stage: "latest commit".into(),
|
|
command,
|
|
stdout: format!(
|
|
"{} {}\n{} {}",
|
|
muted("hash:"),
|
|
bold(&short_hash),
|
|
muted("message:"),
|
|
bold(&msg),
|
|
),
|
|
stderr: String::new(),
|
|
success: true,
|
|
start_ts,
|
|
end_ts: komodo_timestamp(),
|
|
};
|
|
Ok((log, short_hash, msg))
|
|
}
|
|
|
|
/// If the environment was written and needs to be passed to the compose command,
|
|
/// will return the env file PathBuf
|
|
pub async fn write_environment_file(
|
|
environment: &[EnvironmentVar],
|
|
env_file_path: &str,
|
|
secrets: Option<&HashMap<String, String>>,
|
|
folder: &Path,
|
|
logs: &mut Vec<Log>,
|
|
) -> Result<Option<PathBuf>, ()> {
|
|
if environment.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
|
|
let contents = environment_vars_to_string(environment);
|
|
|
|
let contents = if let Some(secrets) = secrets {
|
|
let res = svi::interpolate_variables(
|
|
&contents,
|
|
secrets,
|
|
svi::Interpolator::DoubleBrackets,
|
|
true,
|
|
)
|
|
.context("failed to interpolate secrets into environment");
|
|
|
|
let (contents, replacers) = match res {
|
|
Ok(res) => res,
|
|
Err(e) => {
|
|
logs.push(Log::error(
|
|
"interpolate periphery secrets",
|
|
format_serror(&e.into()),
|
|
));
|
|
return Err(());
|
|
}
|
|
};
|
|
|
|
if !replacers.is_empty() {
|
|
logs.push(Log::simple(
|
|
"interpolate periphery secrets",
|
|
replacers
|
|
.iter()
|
|
.map(|(_, variable)| format!("<span class=\"text-muted-foreground\">replaced:</span> {variable}"))
|
|
.collect::<Vec<_>>()
|
|
.join("\n"),
|
|
))
|
|
}
|
|
|
|
contents
|
|
} else {
|
|
contents
|
|
};
|
|
|
|
let file = folder.join(env_file_path);
|
|
|
|
if let Err(e) =
|
|
fs::write(&file, contents).await.with_context(|| {
|
|
format!("failed to write environment file to {file:?}")
|
|
})
|
|
{
|
|
logs.push(Log::error(
|
|
"write environment file",
|
|
format_serror(&e.into()),
|
|
));
|
|
return Err(());
|
|
}
|
|
|
|
logs.push(Log::simple(
|
|
"write environment file",
|
|
format!("environment written to {file:?}"),
|
|
));
|
|
|
|
Ok(Some(file))
|
|
}
|