From 2cfae525e95d30e52b4978cac824788458631977 Mon Sep 17 00:00:00 2001 From: Maxwell Becker <49575486+mbecker20@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:07:38 +0300 Subject: [PATCH] 1.15.3 (#109) * fix parser support single quote ' * add stack reclone toggle * git clone with token uses token: for gitlab compatability * support stack pre deploy shell command * rename compose down update log stage * deployment configure registry login account * local testing setup * bump version to 1.15.3 * new resources auto assign server if only one * better error log when try to create resource with duplicate name * end description with . * ConfirmUpdate multi language * fix compose write to host logic * improve instrumentation * improve update diff when small array improve 2 * fix compose env file passing when repo_dir is not absolute --- .dockerignore | 8 +- .gitignore | 9 +- Cargo.lock | 27 +-- Cargo.toml | 2 +- bin/core/src/alert/mod.rs | 36 ++-- bin/core/src/api/execute/mod.rs | 16 +- bin/core/src/api/execute/stack.rs | 8 + bin/core/src/api/write/mod.rs | 5 +- bin/core/src/resource/build.rs | 1 + bin/core/src/resource/mod.rs | 14 +- bin/periphery/Cargo.toml | 1 + bin/periphery/src/api/compose.rs | 169 +++++++++--------- bin/periphery/src/api/mod.rs | 8 +- bin/periphery/src/compose.rs | 150 +++++++++++++--- bin/periphery/src/router.rs | 15 +- client/core/rs/src/entities/build.rs | 2 + .../core/rs/src/entities/config/periphery.rs | 4 +- client/core/rs/src/entities/mod.rs | 8 +- client/core/rs/src/entities/stack.rs | 15 +- client/core/rs/src/parser.rs | 15 +- client/core/ts/src/types.ts | 13 +- frontend/src/components/config/index.tsx | 7 + frontend/src/components/config/util.tsx | 61 +++++-- frontend/src/components/layouts.tsx | 6 +- .../src/components/resources/build/index.tsx | 10 +- .../components/resources/builder/index.tsx | 2 +- frontend/src/components/resources/common.tsx | 31 +++- .../resources/deployment/config/index.tsx | 126 ++++++++----- .../components/resources/deployment/index.tsx | 32 ++-- .../resources/resource-sync/config.tsx | 1 + .../resources/server-template/index.tsx | 2 +- .../src/components/resources/stack/config.tsx | 22 ++- .../src/components/resources/stack/index.tsx | 10 +- .../src/components/resources/stack/info.tsx | 3 +- frontend/src/components/users/new.tsx | 4 +- frontend/src/lib/utils.ts | 10 ++ lib/git/src/clone.rs | 4 +- runfile.toml | 8 +- test.core.config.toml | 30 ++-- test.periphery.config.toml | 24 +++ 40 files changed, 633 insertions(+), 286 deletions(-) create mode 100644 test.periphery.config.toml diff --git a/.dockerignore b/.dockerignore index 37c369561..c4899da37 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,4 +5,10 @@ LICENSE *.code-workspace */node_modules -*/dist \ No newline at end of file +*/dist + +creds.toml +.core-repos +.repos +.stacks +.ssl \ No newline at end of file diff --git a/.gitignore b/.gitignore index fd8d847c0..dd73dc6e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,14 @@ target /frontend/build -node_modules /lib/ts_client/build +node_modules dist .env .env.development +.DS_Store + creds.toml -.syncs +.core-repos +.repos .stacks -.DS_Store \ No newline at end of file +.ssl \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3be18e919..690e690cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,7 +41,7 @@ dependencies = [ [[package]] name = "alerter" -version = "1.15.2" +version = "1.15.3" dependencies = [ "anyhow", "axum", @@ -943,7 +943,7 @@ dependencies = [ [[package]] name = "command" -version = "1.15.2" +version = "1.15.3" dependencies = [ "komodo_client", "run_command", @@ -1355,7 +1355,7 @@ dependencies = [ [[package]] name = "environment_file" -version = "1.15.2" +version = "1.15.3" dependencies = [ "thiserror", ] @@ -1439,7 +1439,7 @@ dependencies = [ [[package]] name = "formatting" -version = "1.15.2" +version = "1.15.3" dependencies = [ "serror", ] @@ -1571,7 +1571,7 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "git" -version = "1.15.2" +version = "1.15.3" dependencies = [ "anyhow", "command", @@ -2192,7 +2192,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "1.15.2" +version = "1.15.3" dependencies = [ "anyhow", "clap", @@ -2208,7 +2208,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "1.15.2" +version = "1.15.3" dependencies = [ "anyhow", "async_timing_util", @@ -2239,7 +2239,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "1.15.2" +version = "1.15.3" dependencies = [ "anyhow", "async_timing_util", @@ -2296,7 +2296,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "1.15.2" +version = "1.15.3" dependencies = [ "anyhow", "async_timing_util", @@ -2306,6 +2306,7 @@ dependencies = [ "bollard", "clap", "command", + "derive_variants", "dotenvy", "environment_file", "envy", @@ -2382,7 +2383,7 @@ dependencies = [ [[package]] name = "logger" -version = "1.15.2" +version = "1.15.3" dependencies = [ "anyhow", "komodo_client", @@ -2446,7 +2447,7 @@ dependencies = [ [[package]] name = "migrator" -version = "1.15.2" +version = "1.15.3" dependencies = [ "anyhow", "dotenvy", @@ -3101,7 +3102,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "periphery_client" -version = "1.15.2" +version = "1.15.3" dependencies = [ "anyhow", "komodo_client", @@ -4879,7 +4880,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "update_logger" -version = "1.15.2" +version = "1.15.3" dependencies = [ "anyhow", "komodo_client", diff --git a/Cargo.toml b/Cargo.toml index 9e1836e02..eae6a5115 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"] [workspace.package] -version = "1.15.2" +version = "1.15.3" edition = "2021" authors = ["mbecker20 "] license = "GPL-3.0-or-later" diff --git a/bin/core/src/alert/mod.rs b/bin/core/src/alert/mod.rs index 11a1ecaef..9b9295953 100644 --- a/bin/core/src/alert/mod.rs +++ b/bin/core/src/alert/mod.rs @@ -10,36 +10,42 @@ use komodo_client::entities::{ ResourceTargetVariant, }; use mungos::{find::find_collect, mongodb::bson::doc}; +use tracing::Instrument; use crate::{config::core_config, state::db_client}; mod discord; mod slack; -#[instrument] pub async fn send_alerts(alerts: &[Alert]) { if alerts.is_empty() { return; } - let Ok(alerters) = find_collect( - &db_client().alerters, - doc! { "config.enabled": true }, - None, - ) - .await - .inspect_err(|e| { - error!( + let span = + info_span!("send_alerts", alerts = format!("{alerts:?}")); + async { + let Ok(alerters) = find_collect( + &db_client().alerters, + doc! { "config.enabled": true }, + None, + ) + .await + .inspect_err(|e| { + error!( "ERROR sending alerts | failed to get alerters from db | {e:#}" ) - }) else { - return; - }; + }) else { + return; + }; - let handles = - alerts.iter().map(|alert| send_alert(&alerters, alert)); + let handles = + alerts.iter().map(|alert| send_alert(&alerters, alert)); - join_all(handles).await; + join_all(handles).await; + } + .instrument(span) + .await } #[instrument(level = "debug")] diff --git a/bin/core/src/api/execute/mod.rs b/bin/core/src/api/execute/mod.rs index d319b78ab..f56cde1c9 100644 --- a/bin/core/src/api/execute/mod.rs +++ b/bin/core/src/api/execute/mod.rs @@ -2,6 +2,7 @@ use std::time::Instant; use anyhow::{anyhow, Context}; use axum::{middleware, routing::post, Extension, Router}; +use derive_variants::{EnumVariants, ExtractVariant}; use formatting::format_serror; use komodo_client::{ api::execute::*, @@ -33,7 +34,10 @@ mod stack; mod sync; #[typeshare] -#[derive(Serialize, Deserialize, Debug, Clone, Resolver)] +#[derive( + Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants, +)] +#[variant_derive(Debug)] #[resolver_target(State)] #[resolver_args((User, Update))] #[serde(tag = "type", content = "params")] @@ -154,7 +158,15 @@ async fn handler( Ok(Json(update)) } -#[instrument(name = "ExecuteRequest", skip(user, update), fields(user_id = user.id, update_id = update.id))] +#[instrument( + name = "ExecuteRequest", + skip(user, update), + fields( + user_id = user.id, + update_id = update.id, + request = format!("{:?}", request.extract_variant())) + ) +] async fn task( req_id: Uuid, request: ExecuteRequest, diff --git a/bin/core/src/api/execute/stack.rs b/bin/core/src/api/execute/stack.rs index 13c57d646..cb73b0edb 100644 --- a/bin/core/src/api/execute/stack.rs +++ b/bin/core/src/api/execute/stack.rs @@ -19,6 +19,7 @@ use crate::{ add_interp_update_log, interpolate_variables_secrets_into_extra_args, interpolate_variables_secrets_into_string, + interpolate_variables_secrets_into_system_command, }, periphery_client, query::get_variables_and_secrets, @@ -102,6 +103,13 @@ impl Resolve for State { &mut secret_replacers, )?; + interpolate_variables_secrets_into_system_command( + &vars_and_secrets, + &mut stack.config.pre_deploy, + &mut global_replacers, + &mut secret_replacers, + )?; + add_interp_update_log( &mut update, &global_replacers, diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs index 62a4dcc1a..2f1fded8c 100644 --- a/bin/core/src/api/write/mod.rs +++ b/bin/core/src/api/write/mod.rs @@ -188,7 +188,10 @@ async fn handler( #[instrument( name = "WriteRequest", skip(user, request), - fields(user_id = user.id, request = format!("{:?}", request.extract_variant())) + fields( + user_id = user.id, + request = format!("{:?}", request.extract_variant()) + ) )] async fn task( req_id: Uuid, diff --git a/bin/core/src/resource/build.rs b/bin/core/src/resource/build.rs index 6abdc0808..ea362c0e7 100644 --- a/bin/core/src/resource/build.rs +++ b/bin/core/src/resource/build.rs @@ -57,6 +57,7 @@ impl super::KomodoResource for Build { version: build.config.version, builder_id: build.config.builder_id, git_provider: build.config.git_provider, + image_registry_domain: build.config.image_registry.domain, repo: build.config.repo, branch: build.config.branch, built_hash: build.info.built_hash, diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index 5eeca2b5d..68915d24a 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -15,7 +15,7 @@ use komodo_client::{ tag::Tag, to_komodo_name, update::Update, - user::User, + user::{system_user, User}, Operation, ResourceTarget, ResourceTargetVariant, }, }; @@ -508,6 +508,18 @@ pub async fn create( return Err(anyhow!("valid ObjectIds cannot be used as names.")); } + // Ensure an existing resource with same name doesn't already exist + // The database indexing also ensures this but doesn't give a good error message. + if list_full_for_user::(Default::default(), system_user()) + .await + .context("Failed to list all resources for duplicate name check")? + .into_iter() + .find(|r| r.name == name) + .is_some() + { + return Err(anyhow!("Must provide unique name for resource.")); + } + let start_ts = komodo_timestamp(); T::validate_create_config(&mut config, user).await?; diff --git a/bin/periphery/Cargo.toml b/bin/periphery/Cargo.toml index 123b79c78..5e727dc9d 100644 --- a/bin/periphery/Cargo.toml +++ b/bin/periphery/Cargo.toml @@ -26,6 +26,7 @@ git.workspace = true serror = { workspace = true, features = ["axum"] } merge_config_files.workspace = true async_timing_util.workspace = true +derive_variants.workspace = true resolver_api.workspace = true run_command.workspace = true svi.workspace = true diff --git a/bin/periphery/src/api/compose.rs b/bin/periphery/src/api/compose.rs index 3680e44f8..bbe0d08af 100644 --- a/bin/periphery/src/api/compose.rs +++ b/bin/periphery/src/api/compose.rs @@ -76,85 +76,6 @@ pub struct DockerComposeLsItem { // -const DEFAULT_COMPOSE_CONTENTS: &str = "## 🦎 Hello Komodo 🦎 -services: - hello_world: - image: hello-world - # networks: - # - default - # ports: - # - 3000:3000 - # volumes: - # - data:/data - -# networks: -# default: {} - -# volumes: -# data: -"; - -impl Resolve for State { - async fn resolve( - &self, - GetComposeContentsOnHost { - name, - run_directory, - file_paths, - }: GetComposeContentsOnHost, - _: (), - ) -> anyhow::Result { - let root = - periphery_config().stack_dir.join(to_komodo_name(&name)); - let run_directory = - root.join(&run_directory).components().collect::(); - - if !run_directory.exists() { - fs::create_dir_all(&run_directory) - .await - .context("Failed to initialize run directory")?; - } - - let file_paths = file_paths - .iter() - .map(|path| { - run_directory.join(path).components().collect::() - }) - .collect::>(); - - let mut res = GetComposeContentsOnHostResponse::default(); - - for full_path in &file_paths { - if !full_path.exists() { - fs::write(&full_path, DEFAULT_COMPOSE_CONTENTS) - .await - .context("Failed to init missing compose file on host")?; - } - match fs::read_to_string(&full_path).await.with_context(|| { - format!( - "Failed to read compose file contents at {full_path:?}" - ) - }) { - Ok(contents) => { - res.contents.push(FileContents { - path: full_path.display().to_string(), - contents, - }); - } - Err(e) => { - res.errors.push(FileContents { - path: full_path.display().to_string(), - contents: format_serror(&e.into()), - }); - } - } - } - Ok(res) - } -} - -// - impl Resolve for State { #[instrument( name = "GetComposeServiceLog", @@ -204,6 +125,85 @@ impl Resolve for State { // +const DEFAULT_COMPOSE_CONTENTS: &str = "## 🦎 Hello Komodo 🦎 +services: + hello_world: + image: hello-world + # networks: + # - default + # ports: + # - 3000:3000 + # volumes: + # - data:/data + +# networks: +# default: {} + +# volumes: +# data: +"; + +impl Resolve for State { + #[instrument( + name = "GetComposeContentsOnHost", + level = "debug", + skip(self) + )] + async fn resolve( + &self, + GetComposeContentsOnHost { + name, + run_directory, + file_paths, + }: GetComposeContentsOnHost, + _: (), + ) -> anyhow::Result { + let root = + periphery_config().stack_dir.join(to_komodo_name(&name)); + let run_directory = + root.join(&run_directory).components().collect::(); + + if !run_directory.exists() { + fs::create_dir_all(&run_directory) + .await + .context("Failed to initialize run directory")?; + } + + let mut res = GetComposeContentsOnHostResponse::default(); + + for path in file_paths { + let full_path = + run_directory.join(&path).components().collect::(); + if !full_path.exists() { + fs::write(&full_path, DEFAULT_COMPOSE_CONTENTS) + .await + .context("Failed to init missing compose file on host")?; + } + match 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(FileContents { path, contents }); + } + Err(e) => { + res.errors.push(FileContents { + path, + contents: format_serror(&e.into()), + }); + } + } + } + + Ok(res) + } +} + +// + impl Resolve for State { #[instrument(name = "WriteComposeContentsToHost", skip(self))] async fn resolve( @@ -216,13 +216,10 @@ impl Resolve for State { }: WriteComposeContentsToHost, _: (), ) -> anyhow::Result { - let root = - periphery_config().stack_dir.join(to_komodo_name(&name)); - let run_directory = root.join(&run_directory); - let run_directory = run_directory.canonicalize().context( - "failed to validate run directory on host (canonicalize error)", - )?; - let file_path = run_directory + let file_path = periphery_config() + .stack_dir + .join(to_komodo_name(&name)) + .join(&run_directory) .join(file_path) .components() .collect::(); diff --git a/bin/periphery/src/api/mod.rs b/bin/periphery/src/api/mod.rs index fa8c9a168..9ec2a0112 100644 --- a/bin/periphery/src/api/mod.rs +++ b/bin/periphery/src/api/mod.rs @@ -1,5 +1,6 @@ use anyhow::Context; use command::run_komodo_command; +use derive_variants::EnumVariants; use futures::TryFutureExt; use komodo_client::entities::{update::Log, SystemCommand}; use periphery_client::api::{ @@ -30,7 +31,10 @@ mod network; mod stats; mod volume; -#[derive(Serialize, Deserialize, Debug, Clone, Resolver)] +#[derive( + Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants, +)] +#[variant_derive(Debug)] #[serde(tag = "type", content = "params")] #[resolver_target(State)] #[allow(clippy::enum_variant_names, clippy::large_enum_variant)] @@ -206,7 +210,7 @@ impl ResolveToString for State { } impl Resolve for State { - #[instrument(name = "GetDockerLists", skip(self))] + #[instrument(name = "GetDockerLists", level = "debug", skip(self))] async fn resolve( &self, GetDockerLists {}: GetDockerLists, diff --git a/bin/periphery/src/compose.rs b/bin/periphery/src/compose.rs index ea8eaa589..6dde274d1 100644 --- a/bin/periphery/src/compose.rs +++ b/bin/periphery/src/compose.rs @@ -10,7 +10,7 @@ use komodo_client::entities::{ }; use periphery_client::api::{ compose::ComposeUpResponse, - git::{PullOrCloneRepo, RepoActionResponse}, + git::{CloneRepo, PullOrCloneRepo, RepoActionResponse}, }; use resolver_api::Resolve; use tokio::fs; @@ -71,7 +71,7 @@ pub async fn compose_up( return Err(anyhow!("A compose file doesn't exist after writing stack. Ensure the run_directory and file_paths are correct.")); } - for (_, full_path) in &file_paths { + for (path, full_path) in &file_paths { let file_contents = match fs::read_to_string(&full_path).await.with_context(|| { format!( @@ -86,7 +86,7 @@ pub async fn compose_up( .push(Log::error("read compose file", error.clone())); // This should only happen for repo stacks, ie remote error res.remote_errors.push(FileContents { - path: full_path.display().to_string(), + path: path.to_string(), contents: error, }); return Err(anyhow!( @@ -95,7 +95,7 @@ pub async fn compose_up( } }; res.file_contents.push(FileContents { - path: full_path.display().to_string(), + path: path.to_string(), contents: file_contents, }); } @@ -137,7 +137,7 @@ pub async fn compose_up( } let env_file = env_file_path - .map(|path| format!(" --env-file {}", path.display())) + .map(|path| format!(" --env-file {path}")) .unwrap_or_default(); // Build images before destroying to minimize downtime. @@ -197,6 +197,64 @@ pub async fn compose_up( } } + if !stack.config.pre_deploy.command.is_empty() { + let pre_deploy_path = + run_directory.join(&stack.config.pre_deploy.path); + if !stack.config.skip_secret_interp { + let (full_command, mut replacers) = svi::interpolate_variables( + &stack.config.pre_deploy.command, + &periphery_config().secrets, + svi::Interpolator::DoubleBrackets, + true, + ) + .context( + "failed to interpolate secrets into pre_deploy command", + )?; + replacers.extend(core_replacers.to_owned()); + let mut pre_deploy_log = run_komodo_command( + "pre deploy", + format!("cd {} && {full_command}", pre_deploy_path.display()), + ) + .await; + + pre_deploy_log.command = + svi::replace_in_string(&pre_deploy_log.command, &replacers); + pre_deploy_log.stdout = + svi::replace_in_string(&pre_deploy_log.stdout, &replacers); + pre_deploy_log.stderr = + svi::replace_in_string(&pre_deploy_log.stderr, &replacers); + + tracing::debug!( + "run Stack pre_deploy command | command: {} | cwd: {:?}", + pre_deploy_log.command, + pre_deploy_path + ); + + res.logs.push(pre_deploy_log); + } else { + let pre_deploy_log = run_komodo_command( + "pre deploy", + format!( + "cd {} && {}", + pre_deploy_path.display(), + stack.config.pre_deploy.command + ), + ) + .await; + tracing::debug!( + "run Stack pre_deploy command | command: {} | cwd: {:?}", + &stack.config.pre_deploy.command, + pre_deploy_path + ); + res.logs.push(pre_deploy_log); + } + if !all_logs_success(&res.logs) { + return Err(anyhow!( + "Failed at running pre_deploy command, stopping the run." + )); + } + } + // Take down the existing containers. // This one tries to use the previously deployed service name, to ensure the right stack is taken down. compose_down(&last_project_name, service, res) @@ -237,11 +295,11 @@ pub async fn compose_up( /// Either writes the stack file_contents to a file, or clones the repo. /// Returns (run_directory, env_file_path) -async fn write_stack( - stack: &Stack, +async fn write_stack<'a>( + stack: &'a Stack, git_token: Option, res: &mut ComposeUpResponse, -) -> anyhow::Result<(PathBuf, Option)> { +) -> anyhow::Result<(PathBuf, Option<&'a str>)> { let root = periphery_config() .stack_dir .join(to_komodo_name(&stack.name)); @@ -275,7 +333,14 @@ async fn write_stack( return Err(anyhow!("failed to write environment file")); } }; - Ok((run_directory, env_file_path)) + Ok(( + run_directory, + // Env file paths are already relative to run directory, + // so need to pass original env_file_path here. + env_file_path + .is_some() + .then_some(&stack.config.env_file_path), + )) } else if stack.config.repo.is_empty() { if stack.config.file_contents.trim().is_empty() { return Err(anyhow!("Must either input compose file contents directly, or use file one host / git repo options.")); @@ -324,7 +389,12 @@ async fn write_stack( format!("failed to write compose file to {file_path:?}") })?; - Ok((run_directory, env_file_path)) + Ok(( + run_directory, + env_file_path + .is_some() + .then_some(&stack.config.env_file_path), + )) } else { // ================ // REPO BASED FILES @@ -362,27 +432,46 @@ async fn write_stack( } }; + let clone_or_pull_res = if stack.config.reclone { + State + .resolve( + CloneRepo { + args, + git_token, + environment: env_vars, + env_file_path: stack.config.env_file_path.clone(), + skip_secret_interp: stack.config.skip_secret_interp, + // repo replacer only needed for on_clone / on_pull, + // which aren't available for stacks + replacers: Default::default(), + }, + (), + ) + .await + } else { + State + .resolve( + PullOrCloneRepo { + args, + git_token, + environment: env_vars, + env_file_path: stack.config.env_file_path.clone(), + skip_secret_interp: stack.config.skip_secret_interp, + // repo replacer only needed for on_clone / on_pull, + // which aren't available for stacks + replacers: Default::default(), + }, + (), + ) + .await + }; + let RepoActionResponse { logs, commit_hash, commit_message, env_file_path, - } = match State - .resolve( - PullOrCloneRepo { - args, - git_token, - environment: env_vars, - env_file_path: stack.config.env_file_path.clone(), - skip_secret_interp: stack.config.skip_secret_interp, - // repo replacer only needed for on_clone / on_pull, - // which aren't available for stacks - replacers: Default::default(), - }, - (), - ) - .await - { + } = match clone_or_pull_res { Ok(res) => res, Err(e) => { let error = format_serror( @@ -407,7 +496,12 @@ async fn write_stack( return Err(anyhow!("Stopped after repo pull failure")); } - Ok((run_directory, env_file_path)) + Ok(( + run_directory, + env_file_path + .is_some() + .then_some(&stack.config.env_file_path), + )) } } @@ -422,7 +516,7 @@ async fn compose_down( .map(|service| format!(" {service}")) .unwrap_or_default(); let log = run_komodo_command( - "destroy container", + "compose down", format!("{docker_compose} -p {project} down{service_arg}"), ) .await; diff --git a/bin/periphery/src/router.rs b/bin/periphery/src/router.rs index b9922a644..b2738a1d5 100644 --- a/bin/periphery/src/router.rs +++ b/bin/periphery/src/router.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, time::Instant}; +use std::net::SocketAddr; use anyhow::{anyhow, Context}; use axum::{ @@ -11,6 +11,7 @@ use axum::{ Router, }; use axum_extra::{headers::ContentType, TypedHeader}; +use derive_variants::ExtractVariant; use resolver_api::Resolver; use serror::{AddStatusCode, AddStatusCodeError, Json}; use uuid::Uuid; @@ -40,13 +41,12 @@ async fn handler( Ok((TypedHeader(ContentType::json()), res??)) } -#[instrument(name = "PeripheryHandler")] async fn task( req_id: Uuid, request: crate::api::PeripheryRequest, ) -> anyhow::Result { - let timer = Instant::now(); - + let variant = request.extract_variant(); + let res = State .resolve_request(request, ()) @@ -59,16 +59,12 @@ async fn task( }); if let Err(e) = &res { - warn!("request {req_id} error: {e:#}"); + warn!("request {req_id} | type: {variant:?} | error: {e:#}"); } - let elapsed = timer.elapsed(); - debug!("request {req_id} | resolve time: {elapsed:?}"); - res } -#[instrument(level = "debug")] async fn guard_request_by_passkey( req: Request, next: Next, @@ -100,7 +96,6 @@ async fn guard_request_by_passkey( } } -#[instrument(level = "debug")] async fn guard_request_by_ip( req: Request, next: Next, diff --git a/client/core/rs/src/entities/build.rs b/client/core/rs/src/entities/build.rs index 6fdfa92d2..57baa31b6 100644 --- a/client/core/rs/src/entities/build.rs +++ b/client/core/rs/src/entities/build.rs @@ -33,6 +33,8 @@ pub struct BuildListItemInfo { pub builder_id: String, /// The git provider domain pub git_provider: String, + /// The image registry domain + pub image_registry_domain: String, /// The repo used as the source of the build pub repo: String, /// The branch of the repo diff --git a/client/core/rs/src/entities/config/periphery.rs b/client/core/rs/src/entities/config/periphery.rs index 2e6864cb2..9e46bdf9b 100644 --- a/client/core/rs/src/entities/config/periphery.rs +++ b/client/core/rs/src/entities/config/periphery.rs @@ -87,7 +87,7 @@ pub struct Env { /// If not provided, will use Default config. /// /// Note. This is overridden if the equivalent arg is passed in [CliArgs]. - #[serde(default)] + #[serde(default, alias = "periphery_config_path")] pub periphery_config_paths: Vec, /// If specifying folders, use this to narrow down which /// files will be matched to parse into the final [PeripheryConfig]. @@ -95,7 +95,7 @@ pub struct Env { /// provided to `config_keywords` will be included. /// /// Note. This is overridden if the equivalent arg is passed in [CliArgs]. - #[serde(default)] + #[serde(default, alias = "periphery_config_keyword")] pub periphery_config_keywords: Vec, /// Will merge nested config object (eg. secrets, providers) across multiple diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index e974944e8..bc070c619 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -164,7 +164,11 @@ pub fn get_image_name( } pub fn to_komodo_name(name: &str) -> String { - name.to_lowercase().replace([' ', '.'], "_") + name + .to_lowercase() + .replace([' ', '.'], "_") + .trim() + .to_string() } /// Unix timestamp in milliseconds as i64 @@ -620,7 +624,7 @@ impl CloneArgs { access_token: Option<&str>, ) -> anyhow::Result { let access_token_at = match &access_token { - Some(token) => format!("{token}@"), + Some(token) => format!("token:{token}@"), None => String::new(), }; let protocol = if self.https { "https" } else { "http" }; diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 71bf12793..a8d435ee6 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -11,7 +11,7 @@ use typeshare::typeshare; use super::{ docker::container::ContainerListItem, resource::{Resource, ResourceListItem, ResourceQuery}, - to_komodo_name, FileContents, + to_komodo_name, FileContents, SystemCommand, }; #[typeshare] @@ -284,6 +284,12 @@ pub struct StackConfig { #[builder(default)] pub commit: String, + /// By default, the Stack will `git pull` the repo after it is first cloned. + /// If this option is enabled, the repo folder will be deleted and recloned instead. + #[serde(default)] + #[builder(default)] + pub reclone: bool, + /// Whether incoming webhooks actually trigger action. #[serde(default = "default_webhook_enabled")] #[builder(default = "default_webhook_enabled()")] @@ -312,6 +318,11 @@ pub struct StackConfig { #[builder(default)] pub registry_account: String, + /// The optional command to run before the Stack is deployed. + #[serde(default)] + #[builder(default)] + pub pre_deploy: SystemCommand, + /// The extra arguments to pass after `docker compose up -d`. /// If empty, no extra arguments will be passed. #[serde(default)] @@ -410,6 +421,7 @@ impl Default for StackConfig { file_contents: Default::default(), auto_pull: default_auto_pull(), ignore_services: Default::default(), + pre_deploy: Default::default(), extra_args: Default::default(), environment: Default::default(), env_file_path: default_env_file_path(), @@ -421,6 +433,7 @@ impl Default for StackConfig { repo: Default::default(), branch: default_branch(), commit: Default::default(), + reclone: Default::default(), git_account: Default::default(), webhook_enabled: default_webhook_enabled(), webhook_secret: Default::default(), diff --git a/client/core/rs/src/parser.rs b/client/core/rs/src/parser.rs index 67eb82a98..3fa20ecd8 100644 --- a/client/core/rs/src/parser.rs +++ b/client/core/rs/src/parser.rs @@ -27,8 +27,8 @@ pub fn parse_key_value_list( .trim_start_matches('-') .trim(); // Remove wrapping quotes (from yaml list) - let line = if let Some(line) = line.strip_prefix('"') { - line.strip_suffix('"').unwrap_or(line) + let line = if let Some(line) = line.strip_prefix(['"', '\'']) { + line.strip_suffix(['"', '\'']).unwrap_or(line) } else { line }; @@ -43,11 +43,12 @@ pub fn parse_key_value_list( .map(|(key, value)| { let value = value.trim(); // Remove wrapping quotes around value - if let Some(value) = value.strip_prefix('"') { - value.strip_suffix('"').unwrap_or(value) - } else { - value - }; + let value = + if let Some(value) = value.strip_prefix(['"', '\'']) { + value.strip_suffix(['"', '\'']).unwrap_or(value) + } else { + value + }; (key.trim().to_string(), value.trim().to_string()) })?; anyhow::Ok((key, value)) diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 59fa19743..ee8c5d234 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -438,8 +438,6 @@ export interface BuildConfig { webhook_secret?: string; /** The optional command run after repo clone and before docker build. */ pre_build?: SystemCommand; - /** Configuration for the registry to push the built image to. */ - image_registry?: ImageRegistryConfig; /** * The path of the docker build context relative to the root of the repo. * Default: "." (the root of the repo). @@ -447,6 +445,8 @@ export interface BuildConfig { build_path: string; /** The path of the dockerfile relative to the build path. */ dockerfile_path: string; + /** Configuration for the registry to push the built image to. */ + image_registry?: ImageRegistryConfig; /** Whether to skip secret interpolation in the build_args. */ skip_secret_interp?: boolean; /** Whether to use buildx to build (eg `docker buildx build ...`) */ @@ -512,6 +512,8 @@ export interface BuildListItemInfo { builder_id: string; /** The git provider domain */ git_provider: string; + /** The image registry domain */ + image_registry_domain: string; /** The repo used as the source of the build */ repo: string; /** The branch of the repo */ @@ -2365,6 +2367,11 @@ export interface StackConfig { branch: string; /** Optionally set a specific commit hash. */ commit?: string; + /** + * By default, the Stack will `git pull` the repo after it is first cloned. + * If this option is enabled, the repo folder will be deleted and recloned instead. + */ + reclone?: boolean; /** Whether incoming webhooks actually trigger action. */ webhook_enabled: boolean; /** @@ -2378,6 +2385,8 @@ export interface StackConfig { registry_provider?: string; /** Used with `registry_provider` to login to a registry before docker compose up. */ registry_account?: string; + /** The optional command to run before the Stack is deployed. */ + pre_deploy?: SystemCommand; /** * The extra arguments to pass after `docker compose up -d`. * If empty, no extra arguments will be passed. diff --git a/frontend/src/components/config/index.tsx b/frontend/src/components/config/index.tsx index a75727025..2663df615 100644 --- a/frontend/src/components/config/index.tsx +++ b/frontend/src/components/config/index.tsx @@ -4,6 +4,7 @@ import { ConfirmUpdate, } from "@components/config/util"; import { Section } from "@components/layouts"; +import { MonacoLanguage } from "@components/monaco"; import { Types } from "@komodo/client"; import { cn } from "@lib/utils"; import { Button } from "@ui/button"; @@ -31,6 +32,7 @@ export const ConfigLayout = < onReset, selector, titleOther, + file_contents_language, }: { original: T; config: Partial; @@ -40,6 +42,7 @@ export const ConfigLayout = < onReset: () => void; selector?: ReactNode; titleOther?: ReactNode; + file_contents_language?: MonacoLanguage; }) => { const titleProps = titleOther ? { titleOther } @@ -74,6 +77,7 @@ export const ConfigLayout = < content={config} onConfirm={onConfirm} disabled={disabled} + file_contents_language={file_contents_language} /> )} @@ -120,6 +124,7 @@ export const Config = ({ components, selector, titleOther, + file_contents_language, }: { resource_id: string; resource_type: Types.ResourceTarget["type"]; @@ -134,6 +139,7 @@ export const Config = ({ string, // sidebar key ConfigComponent[] | false | undefined >; + file_contents_language?: MonacoLanguage; }) => { // let component_keys = keys(components); // const [_show, setShow] = useLocalStorage( @@ -164,6 +170,7 @@ export const Config = ({ }} onReset={() => set({})} selector={selector} + file_contents_language={file_contents_language} >
diff --git a/frontend/src/components/config/util.tsx b/frontend/src/components/config/util.tsx index 42bf88574..f8016b629 100644 --- a/frontend/src/components/config/util.tsx +++ b/frontend/src/components/config/util.tsx @@ -46,7 +46,11 @@ import { soft_text_color_class_by_intention, text_color_class_by_intention, } from "@lib/color"; -import { MonacoDiffEditor, MonacoEditor } from "@components/monaco"; +import { + MonacoDiffEditor, + MonacoEditor, + MonacoLanguage, +} from "@components/monaco"; export const ConfigItem = ({ label, @@ -398,6 +402,7 @@ export const AccountSelector = ({ provider, selected, onSelect, + placeholder = "Select Account", }: { disabled: boolean; type: "Server" | "Builder" | "None"; @@ -406,6 +411,7 @@ export const AccountSelector = ({ provider: string; selected: string | undefined; onSelect: (id: string) => void; + placeholder?: string; }) => { const [db_request, config_request]: | ["ListGitProviderAccounts", "ListGitProvidersFromConfig"] @@ -447,7 +453,7 @@ export const AccountSelector = ({ className="w-full lg:w-[200px] max-w-[50%]" disabled={disabled} > - + None @@ -471,12 +477,16 @@ export const AccountSelectorConfig = (params: { provider: string; selected: string | undefined; onSelect: (id: string) => void; - placeholder: string; + placeholder?: string; + description?: string; }) => { return ( @@ -569,6 +579,8 @@ interface ConfirmUpdateProps { content: Partial; onConfirm: () => void; disabled: boolean; + language?: MonacoLanguage; + file_contents_language?: MonacoLanguage; } export function ConfirmUpdate({ @@ -576,6 +588,8 @@ export function ConfirmUpdate({ content, onConfirm, disabled, + language, + file_contents_language, }: ConfirmUpdateProps) { const [open, set] = useState(false); useCtrlKeyListener("s", () => { @@ -608,6 +622,8 @@ export function ConfirmUpdate({ _key={key as any} val={val as any} previous={previous} + language={language} + file_contents_language={file_contents_language} /> ))}
@@ -629,23 +645,24 @@ function ConfirmUpdateItem({ _key, val: _val, previous, + language, + file_contents_language, }: { _key: keyof T; val: T[keyof T]; previous: T; + language?: MonacoLanguage; + file_contents_language?: MonacoLanguage; }) { const [show, setShow] = useState(true); const val = typeof _val === "string" - ? _key === "environment" || - _key === "build_args" || - _key === "secret_args" - ? _val - .split("\n") - .filter((line) => !line.startsWith("#")) - .map((line) => line.split(" #")[0]) - .join("\n") - : _val + ? _val + : Array.isArray(_val) + ? _val.length > 0 && + ["string", "number", "boolean"].includes(typeof _val[0]) + ? JSON.stringify(_val) + : JSON.stringify(_val, null, 2) : JSON.stringify(_val, null, 2); const prev_val = typeof previous[_key] === "string" @@ -653,7 +670,12 @@ function ConfirmUpdateItem({ : _key === "environment" || _key === "build_args" || _key === "secret_args" - ? env_to_text(previous[_key] as any) ?? "" + ? env_to_text(previous[_key] as any) ?? "" // For backward compat with 1.14 + : Array.isArray(previous[_key]) + ? previous[_key].length > 0 && + ["string", "number", "boolean"].includes(typeof previous[_key][0]) + ? JSON.stringify(previous[_key]) + : JSON.stringify(previous[_key], null, 2) : JSON.stringify(previous[_key], null, 2); const showDiff = val?.includes("\n") || @@ -676,7 +698,16 @@ function ConfirmUpdateItem({ ) : (
diff --git a/frontend/src/components/layouts.tsx b/frontend/src/components/layouts.tsx
index 504bf36fe..fe11bb4d3 100644
--- a/frontend/src/components/layouts.tsx
+++ b/frontend/src/components/layouts.tsx
@@ -203,13 +203,13 @@ export const NewLayout = ({
   entityType,
   children,
   enabled,
-  onSuccess,
+  onConfirm,
   onOpenChange,
 }: {
   entityType: string;
   children: ReactNode;
   enabled: boolean;
-  onSuccess: () => Promise;
+  onConfirm: () => Promise;
   onOpenChange?: (open: boolean) => void;
 }) => {
   const [open, set] = useState(false);
@@ -237,7 +237,7 @@ export const NewLayout = ({
             variant="outline"
             onClick={async () => {
               setLoading(true);
-              await onSuccess();
+              await onConfirm();
               setLoading(false);
               set(false);
             }}
diff --git a/frontend/src/components/resources/build/index.tsx b/frontend/src/components/resources/build/index.tsx
index 50d5a4ebe..915e47a22 100644
--- a/frontend/src/components/resources/build/index.tsx
+++ b/frontend/src/components/resources/build/index.tsx
@@ -115,9 +115,17 @@ export const BuildComponents: RequiredResourceComponents = {
 
   New: () => {
     const user = useUser().data;
+    const builders = useRead("ListBuilders", {}).data;
     if (!user) return null;
     if (!user.admin && !user.create_build_permissions) return null;
-    return ;
+    return (
+      
+    );
   },
 
   Table: ({ resources }) => (
diff --git a/frontend/src/components/resources/builder/index.tsx b/frontend/src/components/resources/builder/index.tsx
index 7a49604f0..ba1a6f739 100644
--- a/frontend/src/components/resources/builder/index.tsx
+++ b/frontend/src/components/resources/builder/index.tsx
@@ -74,7 +74,7 @@ export const BuilderComponents: RequiredResourceComponents = {
     return (
        {
+        onConfirm={async () => {
           if (!type) return;
           const id = (await mutateAsync({ name, config: { type, params: {} } }))
             ._id?.$oid!;
diff --git a/frontend/src/components/resources/common.tsx b/frontend/src/components/resources/common.tsx
index c1cd697ca..ffdd11248 100644
--- a/frontend/src/components/resources/common.tsx
+++ b/frontend/src/components/resources/common.tsx
@@ -119,7 +119,11 @@ export const ResourceSelector = ({
   return (
     
       
-        
@@ -258,16 +262,19 @@ export const NewResource = ({
   type,
   readable_type,
   server_id,
+  builder_id,
   build_id,
   name: _name = "",
 }: {
   type: UsableResource;
   readable_type?: string;
   server_id?: string;
+  builder_id?: string;
   build_id?: string;
   name?: string;
 }) => {
   const nav = useNavigate();
+  const { toast } = useToast();
   const { mutateAsync } = useWrite(`Create${type}`);
   const [name, setName] = useState(_name);
   const type_display =
@@ -276,7 +283,7 @@ export const NewResource = ({
       : type === "ResourceSync"
       ? "resource-sync"
       : type.toLowerCase();
-  const config: Types._PartialDeploymentConfig =
+  const config: Types._PartialDeploymentConfig | Types._PartialRepoConfig =
     type === "Deployment"
       ? {
           server_id,
@@ -287,15 +294,19 @@ export const NewResource = ({
       : type === "Stack"
       ? { server_id }
       : type === "Repo"
-      ? { server_id }
+      ? { server_id, builder_id }
+      : type === "Build"
+      ? { builder_id }
       : {};
+  const onConfirm = async () => {
+    if (!name) toast({ title: "Name cannot be empty" });
+    const id = (await mutateAsync({ name, config }))._id?.$oid!;
+    nav(`/${usableResourcePath(type)}/${id}`);
+  };
   return (
      {
-        const id = (await mutateAsync({ name, config }))._id?.$oid!;
-        nav(`/${usableResourcePath(type)}/${id}`);
-      }}
+      onConfirm={onConfirm}
       enabled={!!name}
       onOpenChange={() => setName("")}
     >
@@ -305,6 +316,12 @@ export const NewResource = ({
           placeholder={`${type_display}-name`}
           value={name}
           onChange={(e) => setName(e.target.value)}
+          onKeyDown={(e) => {
+            if (!name) return;
+            if (e.key === "Enter") {
+              onConfirm();
+            }
+          }}
         />
       
diff --git a/frontend/src/components/resources/deployment/config/index.tsx b/frontend/src/components/resources/deployment/config/index.tsx index 949a142ee..51247e6f1 100644 --- a/frontend/src/components/resources/deployment/config/index.tsx +++ b/frontend/src/components/resources/deployment/config/index.tsx @@ -2,6 +2,7 @@ import { useRead, useWrite } from "@lib/hooks"; import { Types } from "@komodo/client"; import { ReactNode, useState } from "react"; import { + AccountSelectorConfig, AddExtraArgMenu, ConfigItem, ConfigList, @@ -19,6 +20,7 @@ import { DefaultTerminationSignal, TerminationTimeout, } from "./components/term-signal"; +import { extract_registry_domain } from "@lib/utils"; export const DeploymentConfig = ({ id, @@ -31,6 +33,7 @@ export const DeploymentConfig = ({ target: { type: "Deployment", id }, }).data; const config = useRead("GetDeployment", { deployment: id }).data?.config; + const builds = useRead("ListBuilds", {}).data; const global_disabled = useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false; const [update, set] = useState>({}); @@ -100,6 +103,37 @@ export const DeploymentConfig = ({ image: (value, set) => ( ), + image_registry_account: (account, set) => { + const image = update.image ?? config.image; + const provider = + image?.type === "Image" && image.params.image + ? extract_registry_domain(image.params.image) + : image?.type === "Build" && image.params.build_id + ? builds?.find((b) => b.id === image.params.build_id)?.info + .image_registry_domain + : undefined; + return ( + + set({ image_registry_account }) + } + disabled={disabled} + placeholder={ + image?.type === "Build" ? "Same as Build" : undefined + } + description={ + image?.type === "Build" + ? "Select an alternate account used to log in to the provider" + : undefined + } + /> + ); + }, redeploy_on_build: (update.image?.type ?? config.image?.type) === "Build" && { description: "Automatically redeploy when the image is built.", @@ -182,6 +216,53 @@ export const DeploymentConfig = ({ ), }, }, + { + label: "Restart", + labelHidden: true, + components: { + restart: (value, set) => ( + + ), + }, + }, + ], + advanced: [ + { + label: "Command", + labelHidden: true, + components: { + command: (value, set) => ( + +
Replace the CMD, or extend the ENTRYPOINT.
+ + See docker docs. + {/* */} + + + } + > + set({ command })} + readOnly={disabled} + /> +
+ ), + }, + }, { label: "Labels", description: "Attach --labels to the container.", @@ -197,19 +278,6 @@ export const DeploymentConfig = ({ ), }, }, - { - label: "Restart", - labelHidden: true, - components: { - restart: (value, set) => ( - - ), - }, - }, { label: "Extra Args", labelHidden: true, @@ -255,38 +323,6 @@ export const DeploymentConfig = ({ ), }, }, - { - label: "Command", - labelHidden: true, - components: { - command: (value, set) => ( - -
Replace the CMD, or extend the ENTRYPOINT.
- - See docker docs. - {/* */} - - - } - > - set({ command })} - readOnly={disabled} - /> -
- ), - }, - }, { label: "Termination", boldLabel: false, diff --git a/frontend/src/components/resources/deployment/index.tsx b/frontend/src/components/resources/deployment/index.tsx index 46a08d3d1..bea6fd795 100644 --- a/frontend/src/components/resources/deployment/index.tsx +++ b/frontend/src/components/resources/deployment/index.tsx @@ -1,7 +1,7 @@ import { useLocalStorage, useRead } from "@lib/hooks"; import { Types } from "@komodo/client"; import { RequiredResourceComponents } from "@types"; -import { AlertTriangle, HardDrive, Rocket, Server } from "lucide-react"; +import { HardDrive, Rocket, Server } from "lucide-react"; import { cn } from "@lib/utils"; import { useServer } from "../server"; import { @@ -22,7 +22,6 @@ import { DeleteResource, NewResource, ResourceLink } from "../common"; import { RunBuild } from "../build/actions"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs"; import { DeploymentConfig } from "./config"; -import { Link } from "react-router-dom"; import { DashboardPieChart } from "@pages/home/dashboard"; import { ResourcePageHeader, StatusBadge } from "@components/util"; @@ -145,9 +144,21 @@ export const DeploymentComponents: RequiredResourceComponents = { ); }, - New: ({ server_id, build_id }) => ( - - ), + New: ({ server_id: _server_id, build_id }) => { + const servers = useRead("ListServers", {}).data; + const server_id = _server_id + ? _server_id + : servers && servers.length === 1 + ? servers[0].id + : undefined; + return ( + + ); + }, Table: ({ resources }) => { return ( @@ -192,17 +203,6 @@ export const DeploymentComponents: RequiredResourceComponents = { ); }, - Alerts: ({ id }) => { - return ( - - - Alerts - - ); - }, }, Actions: { diff --git a/frontend/src/components/resources/resource-sync/config.tsx b/frontend/src/components/resources/resource-sync/config.tsx index 0013d4539..6e0fa5243 100644 --- a/frontend/src/components/resources/resource-sync/config.tsx +++ b/frontend/src/components/resources/resource-sync/config.tsx @@ -57,6 +57,7 @@ export const ResourceSyncConfig = ({ onSave={async () => { await mutateAsync({ id, config: update }); }} + file_contents_language="toml" components={{ "": [ { diff --git a/frontend/src/components/resources/server-template/index.tsx b/frontend/src/components/resources/server-template/index.tsx index 4b792fd75..24d3c740e 100644 --- a/frontend/src/components/resources/server-template/index.tsx +++ b/frontend/src/components/resources/server-template/index.tsx @@ -58,7 +58,7 @@ export const ServerTemplateComponents: RequiredResourceComponents = { return ( { + onConfirm={async () => { if (!type) return; const id = (await mutateAsync({ name, config: { type, params: {} } })) ._id?.$oid!; diff --git a/frontend/src/components/resources/stack/config.tsx b/frontend/src/components/resources/stack/config.tsx index 90fef2b7f..218e8a11e 100644 --- a/frontend/src/components/resources/stack/config.tsx +++ b/frontend/src/components/resources/stack/config.tsx @@ -6,6 +6,7 @@ import { ConfigList, InputList, ProviderSelectorConfig, + SystemCommand, } from "@components/config/util"; import { Types } from "@komodo/client"; import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks"; @@ -233,10 +234,24 @@ export const StackConfig = ({ placeholder: "Compose project name", boldLabel: true, description: - "Optionally set a different compose project name. It should match the compose project name on your host.", + "Optionally set a different compose project name. If importing existing stack, this should match the compose project name on your host.", }, }, }, + { + label: "Pre Deploy", + description: + "Execute a shell command before running docker compose up. The 'path' is relative to the Run Directory", + components: { + pre_deploy: (value, set) => ( + set({ pre_deploy: value })} + disabled={disabled} + /> + ), + }, + }, { label: "Extra Args", labelHidden: true, @@ -467,6 +482,10 @@ export const StackConfig = ({ description: "Switch to a specific hash after cloning the branch.", }, + reclone: { + description: + "Delete the repo folder and clone it again, instead of using 'git pull'.", + }, }, }, { @@ -718,6 +737,7 @@ export const StackConfig = ({ await mutateAsync({ id, config: update }); }} components={components} + file_contents_language="yaml" /> ); }; diff --git a/frontend/src/components/resources/stack/index.tsx b/frontend/src/components/resources/stack/index.tsx index 1844e1f42..fe241929d 100644 --- a/frontend/src/components/resources/stack/index.tsx +++ b/frontend/src/components/resources/stack/index.tsx @@ -151,7 +151,15 @@ export const StackComponents: RequiredResourceComponents = { ); }, - New: ({ server_id }) => , + New: ({ server_id: _server_id }) => { + const servers = useRead("ListServers", {}).data; + const server_id = _server_id + ? _server_id + : servers && servers.length === 1 + ? servers[0].id + : undefined; + return ; + }, Table: ({ resources }) => ( diff --git a/frontend/src/components/resources/stack/info.tsx b/frontend/src/components/resources/stack/info.tsx index 06f0ee8fb..272f74ac1 100644 --- a/frontend/src/components/resources/stack/info.tsx +++ b/frontend/src/components/resources/stack/info.tsx @@ -80,7 +80,7 @@ export const StackInfo = ({ )} - + {/* Update deployed contents with diff */} {/* {!is_down && deployed_contents.length > 0 && ( @@ -189,6 +189,7 @@ export const StackInfo = ({ } }} disabled={!edits[content.path]} + language="yaml" /> )} diff --git a/frontend/src/components/users/new.tsx b/frontend/src/components/users/new.tsx index 2590f6200..15c0b9a6a 100644 --- a/frontend/src/components/users/new.tsx +++ b/frontend/src/components/users/new.tsx @@ -17,7 +17,7 @@ export const NewUserGroup = () => { return ( mutateAsync({ name })} + onConfirm={() => mutateAsync({ name })} enabled={!!name} onOpenChange={() => setName("")} > @@ -46,7 +46,7 @@ export const NewServiceUser = () => { return ( mutateAsync({ username, description: "" })} + onConfirm={() => mutateAsync({ username, description: "" })} enabled={!!username} onOpenChange={() => setUsername("")} > diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index ece62c3b0..4a5860873 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -244,3 +244,13 @@ export const is_service_user = (user_id: string) => { user_id === "Repo Manager" ); }; + +export const extract_registry_domain = (image_name: string) => { + if (!image_name) return "docker.io"; + const maybe_domain = image_name.split("/")[0]; + if (maybe_domain.includes(".")) { + return maybe_domain + } else { + return "docker.io" + } +} \ No newline at end of file diff --git a/lib/git/src/clone.rs b/lib/git/src/clone.rs index af7bf2de4..5fa8d20fd 100644 --- a/lib/git/src/clone.rs +++ b/lib/git/src/clone.rs @@ -100,7 +100,7 @@ where }; if let Some(command) = args.on_clone { - if !command.path.is_empty() && !command.command.is_empty() { + if !command.command.is_empty() { let on_clone_path = repo_dir.join(&command.path); if let Some(secrets) = secrets { let (full_command, mut replacers) = @@ -154,7 +154,7 @@ where } } if let Some(command) = args.on_pull { - if !command.path.is_empty() && !command.command.is_empty() { + if !command.command.is_empty() { let on_pull_path = repo_dir.join(&command.path); if let Some(secrets) = secrets { let (full_command, mut replacers) = diff --git a/runfile.toml b/runfile.toml index e7d49e845..64f763c72 100644 --- a/runfile.toml +++ b/runfile.toml @@ -30,6 +30,10 @@ docker compose -p komodo-dev -f test.compose.yaml build""" description = "runs core --release pointing to test.core.config.toml" cmd = "KOMODO_CONFIG_PATH=test.core.config.toml cargo run -p komodo_core --release" +[test-periphery] +description = "runs periphery --release pointing to test.periphery.config.toml" +cmd = "PERIPHERY_CONFIG_PATH=test.periphery.config.toml cargo run -p komodo_periphery --release" + [docsite-start] path = "docsite" cmd = "yarn start" @@ -38,5 +42,5 @@ cmd = "yarn start" path = "docsite" cmd = "yarn deploy" -[rustdoc-server] -cmd = "cargo watch -s 'cargo doc --no-deps -p komodo_client' & http --quiet target/doc" \ No newline at end of file +# [rustdoc-server] +# cmd = "cargo watch -s 'cargo doc --no-deps -p komodo_client' & http --quiet target/doc" \ No newline at end of file diff --git a/test.core.config.toml b/test.core.config.toml index 3d78f69f8..394eb5e7f 100644 --- a/test.core.config.toml +++ b/test.core.config.toml @@ -1,14 +1,18 @@ +########################### +# 🦎 KOMODO CORE CONFIG 🦎 # +########################### title = "Komodo Test" host = "http://localhost:9121" port = 9121 passkey = "a_random_passkey" -repo_directory = "/Users/max/komodo-repos" +repo_directory = ".core-repos" +frontend_path = "frontend/dist" +first_server = "http://localhost:8121" + disable_confirm_dialog = true enable_new_users = true # database.address = "localhost:27017" -# database.address = "ferretdb.komodo.orb.local" -# database.address = "ferretdb.komodo-dev.orb.local" database.address = "test-ferretdb.orb.local" # database.username = "admin" # database.password = "admin" @@ -18,16 +22,20 @@ local_auth = true jwt_secret = "your_random_secret" jwt_ttl = "2-wk" -oidc_enabled = true -oidc_provider = "http://server.authentik2.orb.local:9000/application/o/komodo" -oidc_client_id = "komodo" -oidc_client_secret = "komodo" +oidc_enabled = false +# oidc_provider = "http://server.authentik2.orb.local:9000/application/o/komodo" +# oidc_client_id = "komodo" +# oidc_client_secret = "komodo" webhook_secret = "a_random_webhook_secret" -stack_poll_interval = "1-min" -sync_poll_interval = "1-min" -build_poll_interval = "1-min" -repo_poll_interval = "1-min" monitoring_interval = "5-sec" +resource_poll_interval = "1-min" +########### +# LOGGING # +########### +logging.level = "info" +logging.stdio = "standard" +logging.otlp_endpoint = "http://tempo.grafana.orb.local:4317" +logging.opentelemetry_service_name = "Komodo" diff --git a/test.periphery.config.toml b/test.periphery.config.toml new file mode 100644 index 000000000..485495afb --- /dev/null +++ b/test.periphery.config.toml @@ -0,0 +1,24 @@ +################################ +# 🦎 KOMODO PERIPHERY CONFIG 🦎 # +################################ +port = 8121 +repo_dir = ".repos" +stack_dir = ".stacks" +stats_polling_rate = "1-sec" +legacy_compose_cli = false +include_disk_mounts = [] + +############ +# Security # +############ +ssl_enabled = false +ssl_key_file = ".ssl/key.pem" +ssl_cert_file = ".ssl/cert.pem" + +########### +# LOGGING # +########### +logging.level = "info" +logging.stdio = "standard" +logging.otlp_endpoint = "http://tempo.grafana.orb.local:4317" +logging.opentelemetry_service_name = "Periphery" \ No newline at end of file