diff --git a/.gitignore b/.gitignore index dd73dc6e1..80333d821 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,4 @@ dist .DS_Store creds.toml -.core-repos -.repos -.stacks -.ssl \ No newline at end of file +.komodo \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a471cb3ce..62df46b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,7 +41,7 @@ dependencies = [ [[package]] name = "alerter" -version = "1.15.5" +version = "1.15.6" dependencies = [ "anyhow", "axum", @@ -943,7 +943,7 @@ dependencies = [ [[package]] name = "command" -version = "1.15.5" +version = "1.15.6" dependencies = [ "komodo_client", "run_command", @@ -1355,7 +1355,7 @@ dependencies = [ [[package]] name = "environment_file" -version = "1.15.5" +version = "1.15.6" dependencies = [ "thiserror", ] @@ -1439,7 +1439,7 @@ dependencies = [ [[package]] name = "formatting" -version = "1.15.5" +version = "1.15.6" dependencies = [ "serror", ] @@ -1571,7 +1571,7 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "git" -version = "1.15.5" +version = "1.15.6" dependencies = [ "anyhow", "command", @@ -2192,7 +2192,7 @@ dependencies = [ [[package]] name = "komodo_cli" -version = "1.15.5" +version = "1.15.6" dependencies = [ "anyhow", "clap", @@ -2208,7 +2208,7 @@ dependencies = [ [[package]] name = "komodo_client" -version = "1.15.5" +version = "1.15.6" dependencies = [ "anyhow", "async_timing_util", @@ -2239,7 +2239,7 @@ dependencies = [ [[package]] name = "komodo_core" -version = "1.15.5" +version = "1.15.6" dependencies = [ "anyhow", "async_timing_util", @@ -2296,7 +2296,7 @@ dependencies = [ [[package]] name = "komodo_periphery" -version = "1.15.5" +version = "1.15.6" dependencies = [ "anyhow", "async_timing_util", @@ -2383,7 +2383,7 @@ dependencies = [ [[package]] name = "logger" -version = "1.15.5" +version = "1.15.6" dependencies = [ "anyhow", "komodo_client", @@ -2447,7 +2447,7 @@ dependencies = [ [[package]] name = "migrator" -version = "1.15.5" +version = "1.15.6" dependencies = [ "anyhow", "dotenvy", @@ -3102,7 +3102,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "periphery_client" -version = "1.15.5" +version = "1.15.6" dependencies = [ "anyhow", "komodo_client", @@ -4880,7 +4880,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "update_logger" -version = "1.15.5" +version = "1.15.6" dependencies = [ "anyhow", "komodo_client", diff --git a/Cargo.toml b/Cargo.toml index 0d262802c..cc6adbfa3 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.5" +version = "1.15.6" edition = "2021" authors = ["mbecker20 "] license = "GPL-3.0-or-later" diff --git a/bin/cli/src/exec.rs b/bin/cli/src/exec.rs index 6ba837a76..37d15a295 100644 --- a/bin/cli/src/exec.rs +++ b/bin/cli/src/exec.rs @@ -132,6 +132,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> { Execution::DeployStack(data) => { println!("{}: {data:?}", "Data".dimmed()) } + Execution::DeployStackIfChanged(data) => { + println!("{}: {data:?}", "Data".dimmed()) + } Execution::StartStack(data) => { println!("{}: {data:?}", "Data".dimmed()) } @@ -273,6 +276,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> { Execution::DeployStack(request) => { komodo_client().execute(request).await } + Execution::DeployStackIfChanged(request) => { + komodo_client().execute(request).await + } Execution::StartStack(request) => { komodo_client().execute(request).await } diff --git a/bin/core/src/api/execute/mod.rs b/bin/core/src/api/execute/mod.rs index f56cde1c9..893b3e51c 100644 --- a/bin/core/src/api/execute/mod.rs +++ b/bin/core/src/api/execute/mod.rs @@ -76,6 +76,7 @@ pub enum ExecuteRequest { // ==== STACK ==== DeployStack(DeployStack), + DeployStackIfChanged(DeployStackIfChanged), StartStack(StartStack), RestartStack(RestartStack), StopStack(StopStack), diff --git a/bin/core/src/api/execute/stack.rs b/bin/core/src/api/execute/stack.rs index cb73b0edb..ffa7391bb 100644 --- a/bin/core/src/api/execute/stack.rs +++ b/bin/core/src/api/execute/stack.rs @@ -3,9 +3,11 @@ use std::collections::HashSet; use anyhow::Context; use formatting::format_serror; use komodo_client::{ - api::execute::*, + api::{execute::*, write::RefreshStackCache}, entities::{ - permission::PermissionLevel, stack::StackInfo, update::Update, + permission::PermissionLevel, + stack::{Stack, StackInfo}, + update::Update, user::User, }, }; @@ -23,9 +25,10 @@ use crate::{ }, periphery_client, query::get_variables_and_secrets, - update::update_update, + update::{add_update_without_send, update_update}, }, monitor::update_cache_for_server, + resource, stack::{ execute::execute_compose, get_stack_and_server, services::extract_services_into_res, @@ -243,6 +246,77 @@ impl Resolve for State { } } +impl Resolve for State { + async fn resolve( + &self, + DeployStackIfChanged { stack, stop_time }: DeployStackIfChanged, + (user, mut update): (User, Update), + ) -> anyhow::Result { + let stack = resource::get_check_permissions::( + &stack, + &user, + PermissionLevel::Execute, + ) + .await?; + State + .resolve( + RefreshStackCache { + stack: stack.id.clone(), + }, + user.clone(), + ) + .await?; + let stack = resource::get::(&stack.id).await?; + let changed = match ( + &stack.info.deployed_contents, + &stack.info.remote_contents, + ) { + (Some(deployed_contents), Some(latest_contents)) => { + let changed = || { + for latest in latest_contents { + let Some(deployed) = deployed_contents + .iter() + .find(|c| c.path == latest.path) + else { + return true; + }; + if latest.contents != deployed.contents { + return true; + } + } + false + }; + changed() + } + (None, _) => true, + _ => false, + }; + + if !changed { + update.push_simple_log( + "Diff compose files", + String::from("Deploy cancelled after no changes detected."), + ); + update.finalize(); + return Ok(update); + } + + // Don't actually send it here, let the handler send it after it can set action state. + // This is usually done in crate::helpers::update::init_execution_update. + update.id = add_update_without_send(&update).await?; + + State + .resolve( + DeployStack { + stack: stack.name, + stop_time, + }, + (user, update), + ) + .await + } +} + impl Resolve for State { #[instrument(name = "StartStack", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] async fn resolve( diff --git a/bin/core/src/api/execute/sync.rs b/bin/core/src/api/execute/sync.rs index 9d6136c44..890c959ea 100644 --- a/bin/core/src/api/execute/sync.rs +++ b/bin/core/src/api/execute/sync.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; use anyhow::{anyhow, Context}; use formatting::{colored, format_serror, Color}; @@ -20,23 +20,27 @@ use komodo_client::{ sync::ResourceSync, update::{Log, Update}, user::{sync_user, User}, + ResourceTargetVariant, }, }; use mongo_indexed::doc; -use mungos::{by_id::update_one_by_id, mongodb::bson::to_document}; +use mungos::{ + by_id::update_one_by_id, + mongodb::bson::{oid::ObjectId, to_document}, +}; use resolver_api::Resolve; use crate::{ helpers::{query::get_id_to_tags, update::update_update}, resource::{self, refresh_resource_sync_state_cache}, - state::{db_client, State}, + state::{action_states, db_client, State}, sync::{ deploy::{ build_deploy_cache, deploy_from_cache, SyncDeployParams, }, execute::{get_updates_for_execution, ExecuteResourceSync}, remote::RemoteResources, - AllResourcesById, + AllResourcesById, ResourceSyncTrait, }, }; @@ -44,7 +48,11 @@ impl Resolve for State { #[instrument(name = "RunSync", skip(self, user, update), fields(user_id = user.id, update_id = update.id))] async fn resolve( &self, - RunSync { sync }: RunSync, + RunSync { + sync, + resource_type: match_resource_type, + resources: match_resources, + }: RunSync, (user, mut update): (User, Update), ) -> anyhow::Result { let sync = resource::get_check_permissions::< @@ -52,6 +60,17 @@ impl Resolve for State { >(&sync, &user, PermissionLevel::Execute) .await?; + // get the action state for the sync (or insert default). + let action_state = action_states() + .resource_sync + .get_or_insert_default(&sync.id) + .await; + + // This will set action state back to default when dropped. + // Will also check to ensure sync not already busy before updating. + let _action_guard = + action_state.update(|state| state.syncing = true)?; + // Send update here for FE to recheck action state update_update(update.clone()).await?; @@ -70,22 +89,101 @@ impl Resolve for State { update_update(update.clone()).await?; if !file_errors.is_empty() { - return Err(anyhow!("Found file errors. Cannot execute sync.")) + return Err(anyhow!("Found file errors. Cannot execute sync.")); } let resources = resources?; let id_to_tags = get_id_to_tags(None).await?; let all_resources = AllResourcesById::load().await?; + // Convert all match_resources to names + let match_resources = match_resources.map(|resources| { + resources + .into_iter() + .filter_map(|name_or_id| { + let Some(resource_type) = match_resource_type else { + return Some(name_or_id); + }; + match ObjectId::from_str(&name_or_id) { + Ok(_) => match resource_type { + ResourceTargetVariant::Alerter => all_resources + .alerters + .get(&name_or_id) + .map(|a| a.name.clone()), + ResourceTargetVariant::Build => all_resources + .builds + .get(&name_or_id) + .map(|b| b.name.clone()), + ResourceTargetVariant::Builder => all_resources + .builders + .get(&name_or_id) + .map(|b| b.name.clone()), + ResourceTargetVariant::Deployment => all_resources + .deployments + .get(&name_or_id) + .map(|d| d.name.clone()), + ResourceTargetVariant::Procedure => all_resources + .procedures + .get(&name_or_id) + .map(|p| p.name.clone()), + ResourceTargetVariant::Repo => all_resources + .repos + .get(&name_or_id) + .map(|r| r.name.clone()), + ResourceTargetVariant::Server => all_resources + .servers + .get(&name_or_id) + .map(|s| s.name.clone()), + ResourceTargetVariant::ServerTemplate => all_resources + .templates + .get(&name_or_id) + .map(|t| t.name.clone()), + ResourceTargetVariant::Stack => all_resources + .stacks + .get(&name_or_id) + .map(|s| s.name.clone()), + ResourceTargetVariant::ResourceSync => all_resources + .syncs + .get(&name_or_id) + .map(|s| s.name.clone()), + ResourceTargetVariant::System => None, + }, + Err(_) => Some(name_or_id), + } + }) + .collect::>() + }); let deployments_by_name = all_resources .deployments .values() + .filter(|deployment| { + Deployment::include_resource( + &deployment.name, + &deployment.config, + match_resource_type, + match_resources.as_deref(), + &deployment.tags, + &id_to_tags, + &sync.config.match_tags, + ) + }) .map(|deployment| (deployment.name.clone(), deployment.clone())) .collect::>(); let stacks_by_name = all_resources .stacks .values() + .filter(|stack| { + Stack::include_resource( + &stack.name, + &stack.config, + match_resource_type, + match_resources.as_deref(), + &stack.tags, + &id_to_tags, + &sync.config.match_tags, + ) + }) .map(|stack| (stack.name.clone(), stack.clone())) .collect::>(); @@ -105,6 +203,8 @@ impl Resolve for State { resources.servers, delete, &all_resources, + match_resource_type, + match_resources.as_deref(), &id_to_tags, &sync.config.match_tags, ) @@ -117,6 +217,8 @@ impl Resolve for State { resources.deployments, delete, &all_resources, + match_resource_type, + match_resources.as_deref(), &id_to_tags, &sync.config.match_tags, ) @@ -126,6 +228,8 @@ impl Resolve for State { resources.stacks, delete, &all_resources, + match_resource_type, + match_resources.as_deref(), &id_to_tags, &sync.config.match_tags, ) @@ -135,6 +239,8 @@ impl Resolve for State { resources.builds, delete, &all_resources, + match_resource_type, + match_resources.as_deref(), &id_to_tags, &sync.config.match_tags, ) @@ -144,6 +250,8 @@ impl Resolve for State { resources.repos, delete, &all_resources, + match_resource_type, + match_resources.as_deref(), &id_to_tags, &sync.config.match_tags, ) @@ -156,6 +264,8 @@ impl Resolve for State { resources.procedures, delete, &all_resources, + match_resource_type, + match_resources.as_deref(), &id_to_tags, &sync.config.match_tags, ) @@ -165,6 +275,8 @@ impl Resolve for State { resources.builders, delete, &all_resources, + match_resource_type, + match_resources.as_deref(), &id_to_tags, &sync.config.match_tags, ) @@ -174,6 +286,8 @@ impl Resolve for State { resources.alerters, delete, &all_resources, + match_resource_type, + match_resources.as_deref(), &id_to_tags, &sync.config.match_tags, ) @@ -186,6 +300,8 @@ impl Resolve for State { resources.server_templates, delete, &all_resources, + match_resource_type, + match_resources.as_deref(), &id_to_tags, &sync.config.match_tags, ) @@ -198,31 +314,48 @@ impl Resolve for State { resources.resource_syncs, delete, &all_resources, + match_resource_type, + match_resources.as_deref(), &id_to_tags, &sync.config.match_tags, ) .await?; + let ( variables_to_create, variables_to_update, variables_to_delete, - ) = crate::sync::variables::get_updates_for_execution( - resources.variables, - // Delete doesn't work with variables when match tags are set - sync.config.match_tags.is_empty() && delete, - ) - .await?; + ) = if match_resource_type.is_none() + && match_resources.is_none() + && sync.config.match_tags.is_empty() + { + crate::sync::variables::get_updates_for_execution( + resources.variables, + // Delete doesn't work with variables when match tags are set + sync.config.match_tags.is_empty() && delete, + ) + .await? + } else { + Default::default() + }; let ( user_groups_to_create, user_groups_to_update, user_groups_to_delete, - ) = crate::sync::user_groups::get_updates_for_execution( - resources.user_groups, - // Delete doesn't work with user groups when match tags are set - sync.config.match_tags.is_empty() && delete, - &all_resources, - ) - .await?; + ) = if match_resource_type.is_none() + && match_resources.is_none() + && sync.config.match_tags.is_empty() + { + crate::sync::user_groups::get_updates_for_execution( + resources.user_groups, + // Delete doesn't work with user groups when match tags are set + sync.config.match_tags.is_empty() && delete, + &all_resources, + ) + .await? + } else { + Default::default() + }; if deploy_cache.is_empty() && resource_syncs_to_create.is_empty() diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index edecf62b6..ba82a6d3b 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -111,6 +111,7 @@ enum ReadRequest { InspectDockerImage(InspectDockerImage), ListDockerImageHistory(ListDockerImageHistory), InspectDockerVolume(InspectDockerVolume), + ListAllDockerContainers(ListAllDockerContainers), #[to_string_resolver] ListDockerContainers(ListDockerContainers), #[to_string_resolver] diff --git a/bin/core/src/api/read/server.rs b/bin/core/src/api/read/server.rs index fafa4aa77..cc3e8ef4a 100644 --- a/bin/core/src/api/read/server.rs +++ b/bin/core/src/api/read/server.rs @@ -13,7 +13,7 @@ use komodo_client::{ entities::{ deployment::Deployment, docker::{ - container::Container, + container::{Container, ContainerListItem}, image::{Image, ImageHistoryResponseItem}, network::Network, volume::Volume, @@ -289,7 +289,7 @@ impl ResolveToString for State { } } -const STATS_PER_PAGE: i64 = 500; +const STATS_PER_PAGE: i64 = 200; impl Resolve for State { async fn resolve( @@ -368,6 +368,37 @@ impl ResolveToString for State { } } +impl Resolve for State { + async fn resolve( + &self, + ListAllDockerContainers { servers }: ListAllDockerContainers, + user: User, + ) -> anyhow::Result> { + let servers = + resource::list_for_user::(Default::default(), &user) + .await? + .into_iter() + .filter(|server| { + servers.is_empty() + || servers.contains(&server.id) + || servers.contains(&server.name) + }); + + let mut containers = Vec::::new(); + + for server in servers { + let cache = server_status_cache() + .get_or_insert_default(&server.id) + .await; + if let Some(more_containers) = &cache.containers { + containers.extend(more_containers.clone()); + } + } + + Ok(containers) + } +} + impl Resolve for State { async fn resolve( &self, diff --git a/bin/core/src/api/read/toml.rs b/bin/core/src/api/read/toml.rs index 312a6c1d4..d61049b97 100644 --- a/bin/core/src/api/read/toml.rs +++ b/bin/core/src/api/read/toml.rs @@ -184,9 +184,12 @@ impl Resolve for State { PermissionLevel::Read, ) .await?; - res - .alerters - .push(convert_resource::(alerter, &id_to_tags)) + res.alerters.push(convert_resource::( + alerter, + false, + vec![], + &id_to_tags, + )) } ResourceTarget::ResourceSync(id) => { let sync = resource::get_check_permissions::( @@ -201,6 +204,8 @@ impl Resolve for State { { res.resource_syncs.push(convert_resource::( sync, + false, + vec![], &id_to_tags, )) } @@ -213,7 +218,12 @@ impl Resolve for State { ) .await?; res.server_templates.push( - convert_resource::(template, &id_to_tags), + convert_resource::( + template, + false, + vec![], + &id_to_tags, + ), ) } ResourceTarget::Server(id) => { @@ -223,9 +233,12 @@ impl Resolve for State { PermissionLevel::Read, ) .await?; - res - .servers - .push(convert_resource::(server, &id_to_tags)) + res.servers.push(convert_resource::( + server, + false, + vec![], + &id_to_tags, + )) } ResourceTarget::Builder(id) => { let mut builder = @@ -236,9 +249,12 @@ impl Resolve for State { ) .await?; Builder::replace_ids(&mut builder, &all); - res - .builders - .push(convert_resource::(builder, &id_to_tags)) + res.builders.push(convert_resource::( + builder, + false, + vec![], + &id_to_tags, + )) } ResourceTarget::Build(id) => { let mut build = resource::get_check_permissions::( @@ -248,9 +264,12 @@ impl Resolve for State { ) .await?; Build::replace_ids(&mut build, &all); - res - .builds - .push(convert_resource::(build, &id_to_tags)) + res.builds.push(convert_resource::( + build, + false, + vec![], + &id_to_tags, + )) } ResourceTarget::Deployment(id) => { let mut deployment = resource::get_check_permissions::< @@ -262,6 +281,8 @@ impl Resolve for State { Deployment::replace_ids(&mut deployment, &all); res.deployments.push(convert_resource::( deployment, + false, + vec![], &id_to_tags, )) } @@ -273,7 +294,12 @@ impl Resolve for State { ) .await?; Repo::replace_ids(&mut repo, &all); - res.repos.push(convert_resource::(repo, &id_to_tags)) + res.repos.push(convert_resource::( + repo, + false, + vec![], + &id_to_tags, + )) } ResourceTarget::Stack(id) => { let mut stack = resource::get_check_permissions::( @@ -283,9 +309,12 @@ impl Resolve for State { ) .await?; Stack::replace_ids(&mut stack, &all); - res - .stacks - .push(convert_resource::(stack, &id_to_tags)) + res.stacks.push(convert_resource::( + stack, + false, + vec![], + &id_to_tags, + )) } ResourceTarget::Procedure(id) => { let mut procedure = resource::get_check_permissions::< @@ -297,6 +326,8 @@ impl Resolve for State { Procedure::replace_ids(&mut procedure, &all); res.procedures.push(convert_resource::( procedure, + false, + vec![], &id_to_tags, )); } diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs index d1b6a2a87..c7b60c5c6 100644 --- a/bin/core/src/api/write/mod.rs +++ b/bin/core/src/api/write/mod.rs @@ -130,8 +130,9 @@ pub enum WriteRequest { CopyResourceSync(CopyResourceSync), DeleteResourceSync(DeleteResourceSync), UpdateResourceSync(UpdateResourceSync), - RefreshResourceSyncPending(RefreshResourceSyncPending), + WriteSyncFileContents(WriteSyncFileContents), CommitSync(CommitSync), + RefreshResourceSyncPending(RefreshResourceSyncPending), CreateSyncWebhook(CreateSyncWebhook), DeleteSyncWebhook(DeleteSyncWebhook), diff --git a/bin/core/src/api/write/stack.rs b/bin/core/src/api/write/stack.rs index 5ad0610af..b0051464b 100644 --- a/bin/core/src/api/write/stack.rs +++ b/bin/core/src/api/write/stack.rs @@ -22,14 +22,14 @@ use octorust::types::{ }; use periphery_client::api::compose::{ GetComposeContentsOnHost, GetComposeContentsOnHostResponse, - WriteComposeContentsToHost, + WriteCommitComposeContents, WriteComposeContentsToHost, }; use resolver_api::Resolve; use crate::{ config::core_config, helpers::{ - periphery_client, + git_token, periphery_client, query::get_server_with_state, update::{add_update, make_update}, }, @@ -143,7 +143,7 @@ impl Resolve for State { }: WriteStackFileContents, user: User, ) -> anyhow::Result { - let (stack, server) = get_stack_and_server( + let (mut stack, server) = get_stack_and_server( &stack, &user, PermissionLevel::Write, @@ -151,9 +151,9 @@ impl Resolve for State { ) .await?; - if !stack.config.files_on_host { + if !stack.config.files_on_host && stack.config.repo.is_empty() { return Err(anyhow!( - "Stack is not configured to use files on host, can't write file contents" + "Stack is not configured to use Files on Host or Git Repo, can't write file contents" )); } @@ -162,30 +162,71 @@ impl Resolve for State { update.push_simple_log("File contents to write", &contents); - match periphery_client(&server)? - .request(WriteComposeContentsToHost { - name: stack.name, - run_directory: stack.config.run_directory, - file_path, - contents, - }) - .await - .context("Failed to write contents to host") - { - Ok(log) => { - update.logs.push(log); - } - Err(e) => { - update.push_error_log( - "Write file contents", - format_serror(&e.into()), - ); - } - }; + let stack_id = stack.id.clone(); + + if stack.config.files_on_host { + match periphery_client(&server)? + .request(WriteComposeContentsToHost { + name: stack.name, + run_directory: stack.config.run_directory, + file_path, + contents, + }) + .await + .context("Failed to write contents to host") + { + Ok(log) => { + update.logs.push(log); + } + Err(e) => { + update.push_error_log( + "Write file contents", + format_serror(&e.into()), + ); + } + }; + } else { + let git_token = if !stack.config.git_account.is_empty() { + git_token( + &stack.config.git_provider, + &stack.config.git_account, + |https| stack.config.git_https = https, + ) + .await + .with_context(|| { + format!( + "Failed to get git token. | {} | {}", + stack.config.git_account, stack.config.git_provider + ) + })? + } else { + None + }; + match periphery_client(&server)? + .request(WriteCommitComposeContents { + stack, + file_path, + contents, + git_token, + }) + .await + .context("Failed to write contents to host") + { + Ok(res) => { + update.logs.extend(res.logs); + } + Err(e) => { + update.push_error_log( + "Write file contents", + format_serror(&e.into()), + ); + } + }; + } if let Err(e) = State .resolve( - RefreshStackCache { stack: stack.id }, + RefreshStackCache { stack: stack_id }, stack_user().to_owned(), ) .await @@ -227,10 +268,11 @@ impl Resolve for State { .await?; let file_contents_empty = stack.config.file_contents.is_empty(); + let repo_empty = stack.config.repo.is_empty(); if !stack.config.files_on_host && file_contents_empty - && stack.config.repo.is_empty() + && repo_empty { // Nothing to do without one of these return Ok(NoData {}); @@ -297,7 +339,7 @@ impl Resolve for State { (services, Some(contents), Some(errors), None, None) } } - } else if file_contents_empty { + } else if !repo_empty { // ================ // REPO BASED STACK // ================ diff --git a/bin/core/src/api/write/sync.rs b/bin/core/src/api/write/sync.rs index 6e7b3a067..002ff25f0 100644 --- a/bin/core/src/api/write/sync.rs +++ b/bin/core/src/api/write/sync.rs @@ -8,6 +8,7 @@ use komodo_client::{ self, alert::{Alert, AlertData, SeverityLevel}, alerter::Alerter, + all_logs_success, build::Build, builder::Builder, config::core::CoreConfig, @@ -22,9 +23,10 @@ use komodo_client::{ sync::{ PartialResourceSyncConfig, ResourceSync, ResourceSyncInfo, }, - update::Log, + to_komodo_name, + update::{Log, Update}, user::{sync_user, User}, - NoData, Operation, ResourceTarget, + CloneArgs, NoData, Operation, ResourceTarget, }, }; use mungos::{ @@ -35,6 +37,7 @@ use octorust::types::{ ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig, }; use resolver_api::Resolve; +use tokio::fs; use crate::{ alert::send_alerts, @@ -103,6 +106,271 @@ impl Resolve for State { } } +impl Resolve for State { + async fn resolve( + &self, + WriteSyncFileContents { + sync, + resource_path, + file_path, + contents, + }: WriteSyncFileContents, + user: User, + ) -> anyhow::Result { + let sync = resource::get_check_permissions::( + &sync, + &user, + PermissionLevel::Write, + ) + .await?; + + if !sync.config.files_on_host && sync.config.repo.is_empty() { + return Err(anyhow!( + "This method is only for files on host, or repo based syncs." + )); + } + + let mut update = + make_update(&sync, Operation::WriteSyncContents, &user); + + update.push_simple_log("File contents", &contents); + + let root = if sync.config.files_on_host { + core_config() + .sync_directory + .join(to_komodo_name(&sync.name)) + } else { + let clone_args: CloneArgs = (&sync).into(); + clone_args.unique_path(&core_config().repo_directory)? + }; + let file_path = + file_path.parse::().context("Invalid file path")?; + let resource_path = resource_path + .parse::() + .context("Invalid resource path")?; + let full_path = root.join(&resource_path).join(&file_path); + + if let Some(parent) = full_path.parent() { + let _ = fs::create_dir_all(parent).await; + } + + if let Err(e) = + fs::write(&full_path, &contents).await.with_context(|| { + format!("Failed to write file contents to {full_path:?}") + }) + { + update.push_error_log("Write file", format_serror(&e.into())); + } else { + update.push_simple_log( + "Write file", + format!("File written to {full_path:?}"), + ); + }; + + if !all_logs_success(&update.logs) { + update.finalize(); + update.id = add_update(update.clone()).await?; + + return Ok(update); + } + + if sync.config.files_on_host { + if let Err(e) = State + .resolve(RefreshResourceSyncPending { sync: sync.name }, user) + .await + { + update + .push_error_log("Refresh failed", format_serror(&e.into())); + } + + update.finalize(); + update.id = add_update(update.clone()).await?; + + return Ok(update); + } + + let commit_res = git::commit_file( + "Commit Resource File", + &root, + &resource_path.join(&file_path), + ) + .await; + + update.logs.extend(commit_res.logs); + + if let Err(e) = State + .resolve(RefreshResourceSyncPending { sync: sync.name }, user) + .await + { + update + .push_error_log("Refresh failed", format_serror(&e.into())); + } + + update.finalize(); + update.id = add_update(update.clone()).await?; + + Ok(update) + } +} + +impl Resolve for State { + #[instrument(name = "CommitSync", skip(self, user))] + async fn resolve( + &self, + CommitSync { sync }: CommitSync, + user: User, + ) -> anyhow::Result { + let sync = resource::get_check_permissions::< + entities::sync::ResourceSync, + >(&sync, &user, PermissionLevel::Write) + .await?; + + let fresh_sync = !sync.config.files_on_host + && sync.config.file_contents.is_empty() + && sync.config.repo.is_empty(); + + if !sync.config.managed && !fresh_sync { + return Err(anyhow!( + "Cannot commit to sync. Enabled 'managed' mode." + )); + } + + let resource_path = sync + .config + .resource_path + .first() + .context("Sync does not have resource path configured.")? + .parse::() + .context("Invalid resource path")?; + + if resource_path + .extension() + .context("Resource path missing '.toml' extension")? + != "toml" + { + return Err(anyhow!("Resource path missing '.toml' extension")); + } + + let res = State + .resolve( + ExportAllResourcesToToml { + tags: sync.config.match_tags.clone(), + }, + sync_user().to_owned(), + ) + .await?; + + let mut update = make_update(&sync, Operation::CommitSync, &user); + update.id = add_update(update.clone()).await?; + + update.logs.push(Log::simple("Resources", res.toml.clone())); + + if sync.config.files_on_host { + let file_path = core_config() + .sync_directory + .join(to_komodo_name(&sync.name)) + .join(&resource_path); + if let Some(parent) = file_path.parent() { + let _ = tokio::fs::create_dir_all(&parent).await; + }; + if let Err(e) = tokio::fs::write(&file_path, &res.toml) + .await + .with_context(|| { + format!("Failed to write resource file to {file_path:?}",) + }) + { + update.push_error_log( + "Write resource file", + format_serror(&e.into()), + ); + update.finalize(); + add_update(update).await?; + return resource::get::(&sync.name).await; + } else { + update.push_simple_log( + "Write contents", + format!("File contents written to {file_path:?}"), + ); + } + } else if !sync.config.repo.is_empty() { + // GIT REPO + let args: CloneArgs = (&sync).into(); + let root = args.unique_path(&core_config().repo_directory)?; + match git::write_commit_file( + "Commit Sync", + &root, + &resource_path, + &res.toml, + ) + .await + { + Ok(res) => update.logs.extend(res.logs), + Err(e) => { + update.push_error_log( + "Write resource file", + format_serror(&e.into()), + ); + update.finalize(); + add_update(update).await?; + return resource::get::(&sync.name).await; + } + } + // =========== + // UI DEFINED + } else if let Err(e) = db_client() + .resource_syncs + .update_one( + doc! { "name": &sync.name }, + doc! { "$set": { "config.file_contents": res.toml } }, + ) + .await + .context("failed to update file_contents on db") + { + update.push_error_log( + "Write resource to database", + format_serror(&e.into()), + ); + update.finalize(); + add_update(update).await?; + return resource::get::(&sync.name).await; + } + + let res = match State + .resolve(RefreshResourceSyncPending { sync: sync.name }, user) + .await + { + Ok(sync) => Ok(sync), + Err(e) => { + update.push_error_log( + "Refresh sync pending", + format_serror(&(&e).into()), + ); + Err(e) + } + }; + + update.finalize(); + + // Need to manually update the update before cache refresh, + // and before broadcast with add_update. + // The Err case of to_document should be unreachable, + // but will fail to update cache in that case. + if let Ok(update_doc) = to_document(&update) { + let _ = update_one_by_id( + &db_client().updates, + &update.id, + mungos::update::Update::Set(update_doc), + None, + ) + .await; + refresh_resource_sync_state_cache().await; + } + update_update(update).await?; + + res + } +} + impl Resolve for State { #[instrument( name = "RefreshResourceSyncPending", @@ -190,6 +458,8 @@ impl Resolve for State { resources.servers, delete, &all_resources, + None, + None, &id_to_tags, &sync.config.match_tags, &mut diffs, @@ -199,6 +469,8 @@ impl Resolve for State { resources.stacks, delete, &all_resources, + None, + None, &id_to_tags, &sync.config.match_tags, &mut diffs, @@ -208,6 +480,8 @@ impl Resolve for State { resources.deployments, delete, &all_resources, + None, + None, &id_to_tags, &sync.config.match_tags, &mut diffs, @@ -217,6 +491,8 @@ impl Resolve for State { resources.builds, delete, &all_resources, + None, + None, &id_to_tags, &sync.config.match_tags, &mut diffs, @@ -226,6 +502,8 @@ impl Resolve for State { resources.repos, delete, &all_resources, + None, + None, &id_to_tags, &sync.config.match_tags, &mut diffs, @@ -235,6 +513,8 @@ impl Resolve for State { resources.procedures, delete, &all_resources, + None, + None, &id_to_tags, &sync.config.match_tags, &mut diffs, @@ -244,6 +524,8 @@ impl Resolve for State { resources.builders, delete, &all_resources, + None, + None, &id_to_tags, &sync.config.match_tags, &mut diffs, @@ -253,6 +535,8 @@ impl Resolve for State { resources.alerters, delete, &all_resources, + None, + None, &id_to_tags, &sync.config.match_tags, &mut diffs, @@ -262,6 +546,8 @@ impl Resolve for State { resources.server_templates, delete, &all_resources, + None, + None, &id_to_tags, &sync.config.match_tags, &mut diffs, @@ -271,6 +557,8 @@ impl Resolve for State { resources.resource_syncs, delete, &all_resources, + None, + None, &id_to_tags, &sync.config.match_tags, &mut diffs, @@ -278,22 +566,28 @@ impl Resolve for State { .await?; } - let variable_updates = + let variable_updates = if sync.config.match_tags.is_empty() { crate::sync::variables::get_updates_for_view( &resources.variables, // Delete doesn't work with variables when match tags are set sync.config.match_tags.is_empty() && delete, ) - .await?; + .await? + } else { + Default::default() + }; - let user_group_updates = + let user_group_updates = if sync.config.match_tags.is_empty() { crate::sync::user_groups::get_updates_for_view( resources.user_groups, // Delete doesn't work with user groups when match tags are set sync.config.match_tags.is_empty() && delete, &all_resources, ) - .await?; + .await? + } else { + Default::default() + }; anyhow::Ok(( diffs, @@ -418,135 +712,6 @@ impl Resolve for State { } } -impl Resolve for State { - #[instrument(name = "CommitSync", skip(self, user))] - async fn resolve( - &self, - CommitSync { sync }: CommitSync, - user: User, - ) -> anyhow::Result { - let sync = resource::get_check_permissions::< - entities::sync::ResourceSync, - >(&sync, &user, PermissionLevel::Write) - .await?; - - let fresh_sync = !sync.config.files_on_host - && sync.config.file_contents.is_empty() - && sync.config.repo.is_empty(); - - if !sync.config.managed && !fresh_sync { - return Err(anyhow!( - "Cannot commit to sync. Enabled 'managed' mode." - )); - } - - let res = State - .resolve( - ExportAllResourcesToToml { - tags: sync.config.match_tags, - }, - sync_user().to_owned(), - ) - .await?; - - let mut update = make_update( - ResourceTarget::ResourceSync(sync.id), - Operation::CommitSync, - &user, - ); - update.id = add_update(update.clone()).await?; - - if sync.config.files_on_host { - let path = sync - .config - .resource_path - .parse::() - .context("Resource path is not valid file path")?; - let extension = path - .extension() - .context("Resource path missing '.toml' extension")?; - if extension != "toml" { - return Err(anyhow!("Wrong file extension. Expected '.toml', got '.{extension:?}'")); - } - if let Some(parent) = path.parent() { - let _ = tokio::fs::create_dir_all(&parent).await; - }; - if let Err(e) = - tokio::fs::write(&sync.config.resource_path, &res.toml) - .await - .with_context(|| { - format!( - "Failed to write resource file to {}", - sync.config.resource_path - ) - }) - { - update.push_error_log( - "Write resource file", - format_serror(&e.into()), - ); - update.finalize(); - add_update(update).await?; - return resource::get::(&sync.name).await; - } - } else if let Err(e) = db_client() - .resource_syncs - .update_one( - doc! { "name": &sync.name }, - doc! { "$set": { "config.file_contents": &res.toml } }, - ) - .await - .context("failed to update file_contents on db") - { - update.push_error_log( - "Write resource to database", - format_serror(&e.into()), - ); - update.finalize(); - add_update(update).await?; - return resource::get::(&sync.name).await; - } - - update - .logs - .push(Log::simple("Committed resources", res.toml)); - - let res = match State - .resolve(RefreshResourceSyncPending { sync: sync.name }, user) - .await - { - Ok(sync) => Ok(sync), - Err(e) => { - update.push_error_log( - "Refresh sync pending", - format_serror(&(&e).into()), - ); - Err(e) - } - }; - - update.finalize(); - - // Need to manually update the update before cache refresh, - // and before broadcast with add_update. - // The Err case of to_document should be unreachable, - // but will fail to update cache in that case. - if let Ok(update_doc) = to_document(&update) { - let _ = update_one_by_id( - &db_client().updates, - &update.id, - mungos::update::Update::Set(update_doc), - None, - ) - .await; - refresh_resource_sync_state_cache().await; - } - update_update(update).await?; - - res - } -} - impl Resolve for State { #[instrument(name = "CreateSyncWebhook", skip(self, user))] async fn resolve( diff --git a/bin/core/src/config.rs b/bin/core/src/config.rs index deee4833f..624de99d3 100644 --- a/bin/core/src/config.rs +++ b/bin/core/src/config.rs @@ -144,6 +144,9 @@ pub fn core_config() -> &'static CoreConfig { jwt_ttl: env .komodo_jwt_ttl .unwrap_or(config.jwt_ttl), + sync_directory: env + .komodo_sync_directory + .unwrap_or(config.sync_directory), repo_directory: env .komodo_repo_directory .unwrap_or(config.repo_directory), diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs index fbdf784a7..9dfbd57be 100644 --- a/bin/core/src/helpers/procedure.rs +++ b/bin/core/src/helpers/procedure.rs @@ -722,6 +722,22 @@ async fn execute_execution( ) .await? } + Execution::DeployStackIfChanged(req) => { + let req = ExecuteRequest::DeployStackIfChanged(req); + let update = init_execution_update(&req, &user).await?; + let ExecuteRequest::DeployStackIfChanged(req) = req else { + unreachable!() + }; + let update_id = update.id.clone(); + handle_resolve_result( + State + .resolve(req, (user, update)) + .await + .context("Failed at DeployStackIfChanged"), + &update_id, + ) + .await? + } Execution::StartStack(req) => { let req = ExecuteRequest::StartStack(req); let update = init_execution_update(&req, &user).await?; diff --git a/bin/core/src/helpers/update.rs b/bin/core/src/helpers/update.rs index 4b91bfa15..2512964a0 100644 --- a/bin/core/src/helpers/update.rs +++ b/bin/core/src/helpers/update.rs @@ -370,6 +370,12 @@ pub async fn init_execution_update( resource::get::(&data.stack).await?.id, ), ), + ExecuteRequest::DeployStackIfChanged(data) => ( + Operation::DeployStack, + ResourceTarget::Stack( + resource::get::(&data.stack).await?.id, + ), + ), ExecuteRequest::StartStack(data) => ( if data.service.is_some() { Operation::StartStackService @@ -429,7 +435,10 @@ pub async fn init_execution_update( }; let mut update = make_update(target, operation, user); update.in_progress(); - // Don't actually send it here, let the handlers send it after they can set action state. - update.id = add_update_without_send(&update).await?; + // Hold off on even adding update for DeployStackIfChanged + if !matches!(&request, ExecuteRequest::DeployStackIfChanged(_)) { + // Don't actually send it here, let the handlers send it after they can set action state. + update.id = add_update_without_send(&update).await?; + } Ok(update) } diff --git a/bin/core/src/listener/github/stack.rs b/bin/core/src/listener/github/stack.rs index 92b8cc90b..f97b9e30c 100644 --- a/bin/core/src/listener/github/stack.rs +++ b/bin/core/src/listener/github/stack.rs @@ -3,7 +3,10 @@ use std::sync::OnceLock; use anyhow::anyhow; use axum::http::HeaderMap; use komodo_client::{ - api::{execute::DeployStack, write::RefreshStackCache}, + api::{ + execute::{DeployStack, DeployStackIfChanged}, + write::RefreshStackCache, + }, entities::{stack::Stack, user::git_webhook_user}, }; use resolver_api::Resolve; @@ -78,14 +81,28 @@ pub async fn handle_stack_deploy_webhook( } let user = git_webhook_user().to_owned(); - let req = ExecuteRequest::DeployStack(DeployStack { - stack: stack_id, - stop_time: None, - }); - let update = init_execution_update(&req, &user).await?; - let ExecuteRequest::DeployStack(req) = req else { - unreachable!() - }; - State.resolve(req, (user, update)).await?; + if stack.config.webhook_force_deploy { + let req = ExecuteRequest::DeployStack(DeployStack { + stack: stack_id, + stop_time: None, + }); + let update = init_execution_update(&req, &user).await?; + let ExecuteRequest::DeployStack(req) = req else { + unreachable!() + }; + State.resolve(req, (user, update)).await?; + } else { + let req = + ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged { + stack: stack_id, + stop_time: None, + }); + let update = init_execution_update(&req, &user).await?; + let ExecuteRequest::DeployStackIfChanged(req) = req else { + unreachable!() + }; + State.resolve(req, (user, update)).await?; + } + Ok(()) } diff --git a/bin/core/src/listener/github/sync.rs b/bin/core/src/listener/github/sync.rs index e254766e3..5d06df0dd 100644 --- a/bin/core/src/listener/github/sync.rs +++ b/bin/core/src/listener/github/sync.rs @@ -78,7 +78,11 @@ pub async fn handle_sync_execute_webhook( } let user = git_webhook_user().to_owned(); - let req = ExecuteRequest::RunSync(RunSync { sync: sync_id }); + let req = ExecuteRequest::RunSync(RunSync { + sync: sync_id, + resource_type: None, + resources: None, + }); let update = init_execution_update(&req, &user).await?; let ExecuteRequest::RunSync(req) = req else { unreachable!() diff --git a/bin/core/src/monitor/mod.rs b/bin/core/src/monitor/mod.rs index 925b7e7df..236473d31 100644 --- a/bin/core/src/monitor/mod.rs +++ b/bin/core/src/monitor/mod.rs @@ -206,7 +206,10 @@ pub async fn update_cache_for_server(server: &Server) { }; match lists::get_docker_lists(&periphery).await { - Ok((containers, networks, images, volumes, projects)) => { + Ok((mut containers, networks, images, volumes, projects)) => { + containers.iter_mut().for_each(|container| { + container.server_id = Some(server.id.clone()) + }); tokio::join!( resources::update_deployment_cache(deployments, &containers), resources::update_stack_cache(stacks, &containers), diff --git a/bin/core/src/resource/build.rs b/bin/core/src/resource/build.rs index ea362c0e7..5a8782dc5 100644 --- a/bin/core/src/resource/build.rs +++ b/bin/core/src/resource/build.rs @@ -180,9 +180,13 @@ async fn validate_config( ) -> anyhow::Result<()> { if let Some(builder_id) = &config.builder_id { if !builder_id.is_empty() { - let builder = super::get_check_permissions::(builder_id, user, PermissionLevel::Read) - .await - .context("cannot create build using this builder. user must have at least read permissions on the builder.")?; + let builder = super::get_check_permissions::( + builder_id, + user, + PermissionLevel::Read, + ) + .await + .context("Cannot attach Build to this Builder")?; config.builder_id = Some(builder.id) } } diff --git a/bin/core/src/resource/deployment.rs b/bin/core/src/resource/deployment.rs index 2397ebbfc..68cce062f 100644 --- a/bin/core/src/resource/deployment.rs +++ b/bin/core/src/resource/deployment.rs @@ -262,9 +262,13 @@ async fn validate_config( ) -> anyhow::Result<()> { if let Some(server_id) = &config.server_id { if !server_id.is_empty() { - let server = get_check_permissions::(server_id, user, PermissionLevel::Write) - .await - .context("cannot create deployment on this server. user must have update permissions on the server to perform this action.")?; + let server = get_check_permissions::( + server_id, + user, + PermissionLevel::Write, + ) + .await + .context("Cannot attach Deployment to this Server")?; config.server_id = Some(server.id); } } @@ -272,9 +276,15 @@ async fn validate_config( &config.image { if !build_id.is_empty() { - let build = get_check_permissions::(build_id, user, PermissionLevel::Read) - .await - .context("cannot create deployment with this build attached. user must have at least read permissions on the build to perform this action.")?; + let build = get_check_permissions::( + build_id, + user, + PermissionLevel::Read, + ) + .await + .context( + "Cannot update deployment with this build attached.", + )?; config.image = Some(DeploymentImage::Build { build_id: build.id, version: *version, diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index 0254973ce..45739841e 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -228,7 +228,7 @@ pub async fn get_check_permissions( Ok(resource) } else { Err(anyhow!( - "user does not have required permissions on this {}", + "User does not have required permissions on this {}. Must have at least {permission_level} permissions", T::resource_type() )) } diff --git a/bin/core/src/resource/procedure.rs b/bin/core/src/resource/procedure.rs index 388b252e0..3f922ff96 100644 --- a/bin/core/src/resource/procedure.rs +++ b/bin/core/src/resource/procedure.rs @@ -503,6 +503,15 @@ async fn validate_config( .await?; params.stack = stack.id; } + Execution::DeployStackIfChanged(params) => { + let stack = super::get_check_permissions::( + ¶ms.stack, + user, + PermissionLevel::Execute, + ) + .await?; + params.stack = stack.id; + } Execution::StartStack(params) => { let stack = super::get_check_permissions::( ¶ms.stack, diff --git a/bin/core/src/resource/repo.rs b/bin/core/src/resource/repo.rs index 7ec509f69..96b272778 100644 --- a/bin/core/src/resource/repo.rs +++ b/bin/core/src/resource/repo.rs @@ -213,15 +213,19 @@ async fn validate_config( PermissionLevel::Write, ) .await - .context("Cannot attach repo to this server. User must have write permissions on the server.")?; + .context("Cannot attach Repo to this Server")?; config.server_id = Some(server.id); } } if let Some(builder_id) = &config.builder_id { if !builder_id.is_empty() { - let builder = super::get_check_permissions::(builder_id, user, PermissionLevel::Read) - .await - .context("Cannot attach repo to this builder. User must have at least read permissions on the builder.")?; + let builder = super::get_check_permissions::( + builder_id, + user, + PermissionLevel::Read, + ) + .await + .context("Cannot attach Repo to this Builder")?; config.builder_id = Some(builder.id); } } diff --git a/bin/core/src/resource/stack.rs b/bin/core/src/resource/stack.rs index 9aa6e86e6..688ceab2f 100644 --- a/bin/core/src/resource/stack.rs +++ b/bin/core/src/resource/stack.rs @@ -301,9 +301,13 @@ async fn validate_config( ) -> anyhow::Result<()> { if let Some(server_id) = &config.server_id { if !server_id.is_empty() { - let server = get_check_permissions::(server_id, user, PermissionLevel::Write) - .await - .context("cannot create stack on this server. user must have update permissions on the server to perform this action.")?; + let server = get_check_permissions::( + server_id, + user, + PermissionLevel::Write, + ) + .await + .context("Cannot attach stack to this Server")?; // in case it comes in as name config.server_id = Some(server.id); } diff --git a/bin/core/src/sync/deploy.rs b/bin/core/src/sync/deploy.rs index c8b52e597..4421320ec 100644 --- a/bin/core/src/sync/deploy.rs +++ b/bin/core/src/sync/deploy.rs @@ -18,7 +18,7 @@ use komodo_client::{ toml::ResourceToml, update::Log, user::sync_user, - ResourceTarget, + FileContents, ResourceTarget, }, }; use resolver_api::Resolve; @@ -26,7 +26,6 @@ use resolver_api::Resolve; use crate::{ api::execute::ExecuteRequest, helpers::update::init_execution_update, - stack::remote::ensure_remote_repo, state::{deployment_status_cache, stack_status_cache, State}, }; @@ -541,7 +540,45 @@ fn build_cache_for_stack<'a>( StackState::Running => { // Here can diff the changes, to see if they merit a redeploy. - // First merge toml resource config (partial) onto default resource config. + // See if any remote contents don't match deployed contents + match ( + &original.info.deployed_contents, + &original.info.remote_contents, + ) { + (Some(deployed_contents), Some(remote_contents)) => { + for FileContents { path, contents } in remote_contents { + if let Some(deployed) = + deployed_contents.iter().find(|c| &c.path == path) + { + if &deployed.contents != contents { + cache.insert( + target, + Some(( + format!( + "File contents for {path} have changed" + ), + after, + )), + ); + return Ok(()); + } + } else { + cache.insert( + target, + Some(( + format!("New file contents at {path}"), + after, + )), + ); + return Ok(()); + } + } + } + // Maybe should handle other cases + _ => {} + } + + // Merge toml resource config (partial) onto default resource config. // Makes sure things that aren't defined in toml (come through as None) actually get removed. let config: StackConfig = stack.config.clone().into(); let mut config: PartialStackConfig = config.into(); @@ -589,40 +626,6 @@ fn build_cache_for_stack<'a>( } }; - // We know the config hasn't changed at this point, but still need - // to check if its a repo based stack, and the hash has updated. - // Can use 'original' for this (config hasn't changed) - if stack.latest_hash { - if let Some(deployed_hash) = &original.info.deployed_hash { - let (_, _, hash, _) = ensure_remote_repo(original.into()) - .await - .context("failed to get latest hash for repo based stack") - .with_context(|| { - format!( - "Stack {} {}", - bold(&stack.name), - colored("has errors", Color::Red) - ) - })?; - if let Some(hash) = hash { - if &hash != deployed_hash { - cache.insert( - target, - Some(( - format!( - "outdated hash. deployed: {} -> latest: {}", - colored(deployed_hash, Color::Red), - colored(hash, Color::Green) - ), - after, - )), - ); - return Ok(()); - } - } - } - } - // Check 'after' to see if they deploy. insert_target_using_after_list( target, diff --git a/bin/core/src/sync/execute.rs b/bin/core/src/sync/execute.rs index 06f030aa5..3ff063dc7 100644 --- a/bin/core/src/sync/execute.rs +++ b/bin/core/src/sync/execute.rs @@ -6,6 +6,7 @@ use komodo_client::{ api::write::{UpdateDescription, UpdateTagsOnResource}, entities::{ tag::Tag, toml::ResourceToml, update::Log, user::sync_user, + ResourceTargetVariant, }, }; use mungos::find::find_collect; @@ -26,6 +27,8 @@ pub async fn get_updates_for_execution< resources: Vec>, delete: bool, all_resources: &AllResourcesById, + match_resource_type: Option, + match_resources: Option<&[String]>, id_to_tags: &HashMap, match_tags: &[String], ) -> anyhow::Result> { @@ -35,11 +38,31 @@ pub async fn get_updates_for_execution< .into_iter() .filter(|r| { Resource::include_resource( - &r.config, &r.tags, id_to_tags, match_tags, + &r.name, + &r.config, + match_resource_type, + match_resources, + &r.tags, + id_to_tags, + match_tags, ) }) .map(|r| (r.name.clone(), r)) .collect::>(); + let resources = resources + .into_iter() + .filter(|r| { + Resource::include_resource_partial( + &r.name, + &r.config, + match_resource_type, + match_resources, + &r.tags, + id_to_tags, + match_tags, + ) + }) + .collect::>(); let mut to_create = ToCreate::::new(); let mut to_update = ToUpdate::::new(); @@ -54,15 +77,6 @@ pub async fn get_updates_for_execution< } for mut resource in resources { - // only resource that might not be included is resource sync - if !Resource::include_resource_partial( - &resource.config, - &resource.tags, - id_to_tags, - match_tags, - ) { - continue; - } match map.get(&resource.name) { Some(original) => { // First merge toml resource config (partial) onto default resource config. diff --git a/bin/core/src/sync/file.rs b/bin/core/src/sync/file.rs index da4e4068f..1ad736f77 100644 --- a/bin/core/src/sync/file.rs +++ b/bin/core/src/sync/file.rs @@ -1,91 +1,201 @@ -use std::{fs, path::Path}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use anyhow::{anyhow, Context}; -use formatting::{colored, format_serror, muted, Color}; +use formatting::{bold, colored, format_serror, muted, Color}; use komodo_client::entities::{ + sync::SyncFileContents, toml::{ResourceToml, ResourcesToml}, update::Log, - FileContents, }; pub fn read_resources( - path: &Path, + root_path: &Path, + resource_path: &[String], match_tags: &[String], logs: &mut Vec, - files: &mut Vec, - file_errors: &mut Vec, + files: &mut Vec, + file_errors: &mut Vec, ) -> anyhow::Result { - let mut res = ResourcesToml::default(); - let mut log = - format!("{}: reading resources from {path:?}", muted("INFO")); - if let Err(e) = read_resources_recursive( - path, - match_tags, - &mut res, - &mut log, - files, - file_errors, - ) - .with_context(|| format!("failed to read resources from {path:?}")) - { - file_errors.push(FileContents { - path: path.display().to_string(), - contents: format_serror(&e.into()), - }); - logs.push(Log::error("read remote resources", log)); - } else { - logs.push(Log::simple("read remote resources", log)); - }; - Ok(res) + let mut resources = ResourcesToml::default(); + + for resource_path in resource_path { + let resource_path = resource_path + .parse::() + .context("Invalid resource path")?; + let full_path = root_path + .join(&resource_path) + .components() + .collect::(); + + let mut log = format!( + "{}: reading resources from {full_path:?}", + muted("INFO") + ); + + if full_path.is_file() { + if let Err(e) = read_resource_file( + root_path, + None, + &resource_path, + match_tags, + &mut resources, + &mut log, + files, + ) + .with_context(|| { + format!("failed to read resources from {full_path:?}") + }) { + file_errors.push(SyncFileContents { + resource_path: String::new(), + path: resource_path.display().to_string(), + contents: format_serror(&e.into()), + }); + logs.push(Log::error("Read remote resources", log)); + } else { + logs.push(Log::simple("Read remote resources", log)); + }; + } else if full_path.is_dir() { + if let Err(e) = read_resources_directory( + root_path, + &resource_path, + &PathBuf::new(), + match_tags, + &mut resources, + &mut log, + files, + file_errors, + ) + .with_context(|| { + format!("Failed to read resources from {full_path:?}") + }) { + file_errors.push(SyncFileContents { + resource_path: String::new(), + path: resource_path.display().to_string(), + contents: format_serror(&e.into()), + }); + logs.push(Log::error("Read remote resources", log)); + } else { + logs.push(Log::simple("Read remote resources", log)); + }; + } else { + log.push_str(&format!( + "{}: Resoure path {} is neither a file nor a directory.", + colored("WARN", Color::Red), + bold(resource_path.display()) + )); + logs.push(Log::error("Read remote resources", log)); + } + } + + Ok(resources) } -fn read_resources_recursive( - path: &Path, +/// Use when incoming resource path is a file. +fn read_resource_file( + root_path: &Path, + // relative to root path. + resource_path: Option<&Path>, + // relative to resource path if provided, or root path. + file_path: &Path, match_tags: &[String], resources: &mut ResourcesToml, log: &mut String, - files: &mut Vec, - file_errors: &mut Vec, + files: &mut Vec, ) -> anyhow::Result<()> { - let res = - fs::metadata(path).context("failed to get path metadata")?; - if res.is_file() { - if !path - .extension() - .map(|ext| ext == "toml") - .unwrap_or_default() - { - return Ok(()); - } - let contents = std::fs::read_to_string(path) - .context("failed to read file contents")?; - files.push(FileContents { - path: path.display().to_string(), - contents: contents.clone(), - }); - let more = toml::from_str::(&contents) - // the error without this comes through with multiple lines (\n) and looks bad - .map_err(|e| anyhow!("{e:#}")) - .context("failed to parse resource file contents")?; + let full_path = if let Some(resource_path) = resource_path { + root_path.join(resource_path).join(file_path) + } else { + root_path.join(file_path) + }; + if !full_path + .extension() + .map(|ext| ext == "toml") + .unwrap_or_default() + { + return Ok(()); + } + let contents = std::fs::read_to_string(&full_path) + .context("failed to read file contents")?; - log.push('\n'); - log.push_str(&format!( - "{}: {} from {}", - muted("INFO"), - colored("adding resources", Color::Green), - colored(path.display(), Color::Blue) - )); + files.push(SyncFileContents { + resource_path: resource_path + .map(|path| path.display().to_string()) + .unwrap_or_default(), + path: file_path.display().to_string(), + contents: contents.clone(), + }); + let more = toml::from_str::(&contents) + // the error without this comes through with multiple lines (\n) and looks bad + .map_err(|e| anyhow!("{e:#}")) + .context("failed to parse resource file contents")?; + log.push('\n'); + let path_for_view = + if let Some(resource_path) = resource_path.as_ref() { + resource_path.join(&file_path) + } else { + file_path.to_path_buf() + }; + log.push_str(&format!( + "{}: {} from {}", + muted("INFO"), + colored("adding resources", Color::Green), + colored(path_for_view.display(), Color::Blue) + )); - extend_resources(resources, more, match_tags); + extend_resources(resources, more, match_tags); - Ok(()) - } else if res.is_dir() { - let directory = fs::read_dir(path) - .context("failed to read directory contents")?; - for entry in directory.into_iter().flatten() { - let path = entry.path(); - if let Err(e) = read_resources_recursive( - &path, + Ok(()) +} + +/// Reads down into directories. +fn read_resources_directory( + root_path: &Path, + // relative to root path. + resource_path: &Path, + // relative to resource path. start as empty path + curr_path: &Path, + match_tags: &[String], + resources: &mut ResourcesToml, + log: &mut String, + files: &mut Vec, + file_errors: &mut Vec, +) -> anyhow::Result<()> { + let full_resource_path = root_path.join(resource_path); + let full_path = full_resource_path.join(&curr_path); + let directory = fs::read_dir(&full_path).with_context(|| { + format!("Failed to read directory contents at {full_path:?}") + })?; + for entry in directory.into_iter().flatten() { + let path = entry.path(); + let curr_path = + path.strip_prefix(&full_resource_path).unwrap_or(&path); + if path.is_file() { + if let Err(e) = read_resource_file( + root_path, + Some(resource_path), + curr_path, + match_tags, + resources, + log, + files, + ) + .with_context(|| { + format!("failed to read resources from {full_path:?}") + }) { + file_errors.push(SyncFileContents { + resource_path: String::new(), + path: resource_path.display().to_string(), + contents: format_serror(&e.into()), + }); + }; + } else if path.is_dir() { + if let Err(e) = read_resources_directory( + root_path, + resource_path, + curr_path, match_tags, resources, log, @@ -95,8 +205,9 @@ fn read_resources_recursive( .with_context(|| { format!("failed to read resources from {path:?}") }) { - file_errors.push(FileContents { - path: path.display().to_string(), + file_errors.push(SyncFileContents { + resource_path: resource_path.display().to_string(), + path: curr_path.display().to_string(), contents: format_serror(&e.into()), }); log.push('\n'); @@ -108,10 +219,8 @@ fn read_resources_recursive( )); } } - Ok(()) - } else { - Err(anyhow!("resources path is neither file nor directory")) } + Ok(()) } pub fn extend_resources( diff --git a/bin/core/src/sync/mod.rs b/bin/core/src/sync/mod.rs index f18d58e21..b945be183 100644 --- a/bin/core/src/sync/mod.rs +++ b/bin/core/src/sync/mod.rs @@ -5,10 +5,13 @@ use komodo_client::entities::{ deployment::Deployment, procedure::Procedure, repo::Repo, server::Server, server_template::ServerTemplate, stack::Stack, sync::ResourceSync, tag::Tag, toml::ResourceToml, ResourceTarget, + ResourceTargetVariant, }; use mungos::mongodb::bson::oid::ObjectId; use toml::ToToml; +use crate::resource::KomodoResource; + pub mod deploy; pub mod execute; pub mod file; @@ -38,22 +41,44 @@ pub trait ResourceSyncTrait: ToToml + Sized { /// To exclude resource syncs with "file_contents" (they aren't compatible) fn include_resource( + name: &String, _config: &Self::Config, + match_resource_type: Option, + match_resources: Option<&[String]>, resource_tags: &[String], id_to_tags: &HashMap, match_tags: &[String], ) -> bool { - include_resource_by_tags(resource_tags, id_to_tags, match_tags) + include_resource_by_resource_type_and_name::( + match_resource_type, + match_resources, + name, + ) && include_resource_by_tags( + resource_tags, + id_to_tags, + match_tags, + ) } /// To exclude resource syncs with "file_contents" (they aren't compatible) fn include_resource_partial( + name: &String, _config: &Self::PartialConfig, + match_resource_type: Option, + match_resources: Option<&[String]>, resource_tags: &[String], id_to_tags: &HashMap, match_tags: &[String], ) -> bool { - include_resource_by_tags(resource_tags, id_to_tags, match_tags) + include_resource_by_resource_type_and_name::( + match_resource_type, + match_resources, + name, + ) && include_resource_by_tags( + resource_tags, + id_to_tags, + match_tags, + ) } /// Apply any changes to incoming toml partial config @@ -90,6 +115,31 @@ pub fn include_resource_by_tags( match_tags.iter().all(|tag| tag_names.contains(&tag)) } +pub fn include_resource_by_resource_type_and_name< + T: KomodoResource, +>( + resource_type: Option, + resources: Option<&[String]>, + name: &String, +) -> bool { + match (resource_type, resources) { + (Some(resource_type), Some(resources)) => { + if T::resource_type() != resource_type { + return false; + } + resources.contains(name) + } + (Some(resource_type), None) => { + if T::resource_type() != resource_type { + return false; + } + true + } + (None, Some(resources)) => resources.contains(name), + (None, None) => true, + } +} + pub struct AllResourcesById { pub servers: HashMap, pub deployments: HashMap, diff --git a/bin/core/src/sync/remote.rs b/bin/core/src/sync/remote.rs index c41df6191..7eeef06e4 100644 --- a/bin/core/src/sync/remote.rs +++ b/bin/core/src/sync/remote.rs @@ -1,10 +1,11 @@ -use std::path::PathBuf; - use anyhow::{anyhow, Context}; use git::GitRes; use komodo_client::entities::{ - sync::ResourceSync, toml::ResourcesToml, update::Log, CloneArgs, - FileContents, + sync::{ResourceSync, SyncFileContents}, + to_komodo_name, + toml::ResourcesToml, + update::Log, + CloneArgs, }; use crate::{config::core_config, helpers::git_token}; @@ -13,8 +14,8 @@ use super::file::extend_resources; pub struct RemoteResources { pub resources: anyhow::Result, - pub files: Vec, - pub file_errors: Vec, + pub files: Vec, + pub file_errors: Vec, pub logs: Vec, pub hash: Option, pub message: Option, @@ -28,15 +29,14 @@ pub async fn get_remote_resources( // ============= // FILES ON HOST // ============= - let path = sync - .config - .resource_path - .parse::() - .context("Resource path is not valid path")?; + let root_path = core_config() + .sync_directory + .join(to_komodo_name(&sync.name)); let (mut logs, mut files, mut file_errors) = (Vec::new(), Vec::new(), Vec::new()); let resources = super::file::read_resources( - &path, + &root_path, + &sync.config.resource_path, &sync.config.match_tags, &mut logs, &mut files, @@ -50,9 +50,7 @@ pub async fn get_remote_resources( hash: None, message: None, }); - } else if sync.config.managed - || !sync.config.file_contents.is_empty() - { + } else if sync.config.repo.is_empty() { // ========== // UI DEFINED // ========== @@ -72,10 +70,10 @@ pub async fn get_remote_resources( Ok(resources) }; - // filter_by_ return Ok(RemoteResources { resources, - files: vec![FileContents { + files: vec![SyncFileContents { + resource_path: String::new(), path: "database file".to_string(), contents: sync.config.file_contents.clone(), }], @@ -101,10 +99,10 @@ pub async fn get_remote_resources( let access_token = if let Some(account) = &clone_args.account { git_token(&clone_args.provider, account, |https| clone_args.https = https) - .await - .with_context( - || format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider), - )? + .await + .with_context( + || format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider), + )? } else { None }; @@ -139,11 +137,10 @@ pub async fn get_remote_resources( let message = message.context("failed to get commit hash message")?; - let resource_path = repo_path.join(&sync.config.resource_path); - let (mut files, mut file_errors) = (Vec::new(), Vec::new()); let resources = super::file::read_resources( - &resource_path, + &repo_path, + &sync.config.resource_path, &sync.config.match_tags, &mut logs, &mut files, diff --git a/bin/core/src/sync/resources.rs b/bin/core/src/sync/resources.rs index b4d702d8d..af1f13828 100644 --- a/bin/core/src/sync/resources.rs +++ b/bin/core/src/sync/resources.rs @@ -17,7 +17,7 @@ use komodo_client::{ tag::Tag, update::Log, user::sync_user, - ResourceTarget, + ResourceTarget, ResourceTargetVariant, }, }; use partial_derive2::{MaybeNone, PartialDiff}; @@ -31,8 +31,10 @@ use crate::{ }; use super::{ - execute::ExecuteResourceSync, include_resource_by_tags, - AllResourcesById, ResourceSyncTrait, ToCreate, ToDelete, ToUpdate, + execute::ExecuteResourceSync, + include_resource_by_resource_type_and_name, + include_resource_by_tags, AllResourcesById, ResourceSyncTrait, + ToCreate, ToDelete, ToUpdate, }; impl ResourceSyncTrait for Server { @@ -237,12 +239,19 @@ impl ResourceSyncTrait for ResourceSync { } fn include_resource( + name: &String, config: &Self::Config, + match_resource_type: Option, + match_resources: Option<&[String]>, resource_tags: &[String], id_to_tags: &HashMap, match_tags: &[String], ) -> bool { - if !include_resource_by_tags( + if !include_resource_by_resource_type_and_name::( + match_resource_type, + match_resources, + name, + ) || !include_resource_by_tags( resource_tags, id_to_tags, match_tags, @@ -259,17 +268,24 @@ impl ResourceSyncTrait for ResourceSync { } // The file contents MUST be empty contents_empty && - // The sync must be files on host mode OR NOT managed - (config.files_on_host || !config.managed) + // The sync must be files on host mode OR git repo mode + (config.files_on_host || !config.repo.is_empty()) } fn include_resource_partial( + name: &String, config: &Self::PartialConfig, + match_resource_type: Option, + match_resources: Option<&[String]>, resource_tags: &[String], id_to_tags: &HashMap, match_tags: &[String], ) -> bool { - if !include_resource_by_tags( + if !include_resource_by_resource_type_and_name::( + match_resource_type, + match_resources, + name, + ) || !include_resource_by_tags( resource_tags, id_to_tags, match_tags, @@ -291,8 +307,8 @@ impl ResourceSyncTrait for ResourceSync { } // The file contents MUST be empty contents_empty && - // The sync must be files on host mode OR NOT managed - (files_on_host || !config.managed.unwrap_or_default()) + // The sync must be files on host mode OR git repo mode + (files_on_host || !config.repo.as_deref().unwrap_or_default().is_empty()) } fn get_diff( @@ -579,6 +595,13 @@ impl ResourceSyncTrait for Procedure { .map(|s| s.name.clone()) .unwrap_or_default(); } + Execution::DeployStackIfChanged(config) => { + config.stack = resources + .stacks + .get(&config.stack) + .map(|s| s.name.clone()) + .unwrap_or_default(); + } Execution::StartStack(config) => { config.stack = resources .stacks diff --git a/bin/core/src/sync/toml.rs b/bin/core/src/sync/toml.rs index cb339788f..bbdf03ae3 100644 --- a/bin/core/src/sync/toml.rs +++ b/bin/core/src/sync/toml.rs @@ -105,6 +105,8 @@ pub fn resource_toml_to_toml_string( pub fn resource_push_to_toml( mut resource: Resource, + deploy: bool, + after: Vec, toml: &mut String, all: &AllResourcesById, all_tags: &HashMap, @@ -116,7 +118,7 @@ pub fn resource_push_to_toml( toml .push_str(&format!("[[{}]]\n", R::resource_type().toml_header())); R::push_to_toml_string( - convert_resource::(resource, all_tags), + convert_resource::(resource, deploy, after, all_tags), toml, )?; Ok(()) @@ -124,16 +126,22 @@ pub fn resource_push_to_toml( pub fn resource_to_toml( resource: Resource, + deploy: bool, + after: Vec, all: &AllResourcesById, all_tags: &HashMap, ) -> anyhow::Result { let mut toml = String::new(); - resource_push_to_toml::(resource, &mut toml, all, all_tags)?; + resource_push_to_toml::( + resource, deploy, after, &mut toml, all, all_tags, + )?; Ok(toml) } pub fn convert_resource( resource: Resource, + deploy: bool, + after: Vec, all_tags: &HashMap, ) -> ResourceToml { ResourceToml { @@ -144,9 +152,8 @@ pub fn convert_resource( .filter_map(|t| all_tags.get(t).map(|t| t.name.clone())) .collect(), description: resource.description, - deploy: false, - after: Default::default(), - latest_hash: false, + deploy, + after, // The config still needs to be minimized. // This happens in ToToml::push_to_toml config: resource.config.into(), @@ -687,6 +694,15 @@ impl ToToml for Procedure { .map(|r| &r.name) .unwrap_or(&String::new()), ), + Execution::DeployStackIfChanged(exec) => { + exec.stack.clone_from( + all + .stacks + .get(&exec.stack) + .map(|r| &r.name) + .unwrap_or(&String::new()), + ) + } Execution::StartStack(exec) => exec.stack.clone_from( all .stacks diff --git a/bin/core/src/sync/user_groups.rs b/bin/core/src/sync/user_groups.rs index 58c1edca4..636701f11 100644 --- a/bin/core/src/sync/user_groups.rs +++ b/bin/core/src/sync/user_groups.rs @@ -94,6 +94,7 @@ pub async fn get_updates_for_view( Some(original) => original, None => { diffs.push(DiffData::Create { + name: user_group.name.clone(), proposed: format!( "[[user_group]]\n{}", toml_pretty::to_string(&user_group, TOML_PRETTY_OPTIONS) diff --git a/bin/core/src/sync/variables.rs b/bin/core/src/sync/variables.rs index 37382b68b..a251808f2 100644 --- a/bin/core/src/sync/variables.rs +++ b/bin/core/src/sync/variables.rs @@ -75,6 +75,7 @@ pub async fn get_updates_for_view( } None => { diffs.push(DiffData::Create { + name: variable.name.clone(), proposed: format!( "[[variable]]\n{}", toml_pretty::to_string(variable, TOML_PRETTY_OPTIONS) diff --git a/bin/core/src/sync/view.rs b/bin/core/src/sync/view.rs index 7d3dfc498..6452c3b6c 100644 --- a/bin/core/src/sync/view.rs +++ b/bin/core/src/sync/view.rs @@ -5,6 +5,7 @@ use komodo_client::entities::{ sync::{DiffData, ResourceDiff}, tag::Tag, toml::ResourceToml, + ResourceTargetVariant, }; use mungos::find::find_collect; use partial_derive2::MaybeNone; @@ -15,7 +16,9 @@ pub async fn push_updates_for_view( resources: Vec>, delete: bool, all_resources: &AllResourcesById, - all_tags: &HashMap, + match_resource_type: Option, + match_resources: Option<&[String]>, + id_to_tags: &HashMap, match_tags: &[String], diffs: &mut Vec, ) -> anyhow::Result<()> { @@ -25,12 +28,33 @@ pub async fn push_updates_for_view( .into_iter() .filter(|r| { Resource::include_resource( - &r.config, &r.tags, all_tags, match_tags, + &r.name, + &r.config, + match_resource_type, + match_resources, + &r.tags, + id_to_tags, + match_tags, ) }) .map(|r| (r.name.clone(), r)) .collect::>(); + let resources = resources + .into_iter() + .filter(|r| { + Resource::include_resource_partial( + &r.name, + &r.config, + match_resource_type, + match_resources, + &r.tags, + id_to_tags, + match_tags, + ) + }) + .collect::>(); + if delete { for current_resource in current_map.values() { if !resources.iter().any(|r| r.name == current_resource.name) { @@ -41,8 +65,10 @@ pub async fn push_updates_for_view( data: DiffData::Delete { current: super::toml::resource_to_toml::( current_resource.clone(), + false, + vec![], all_resources, - all_tags, + id_to_tags, )?, }, }); @@ -51,15 +77,6 @@ pub async fn push_updates_for_view( } for mut proposed_resource in resources { - // only resource that might not be included is resource sync - if !Resource::include_resource_partial( - &proposed_resource.config, - &proposed_resource.tags, - all_tags, - match_tags, - ) { - continue; - } match current_map.get(&proposed_resource.name) { Some(current_resource) => { // First merge toml resource config (partial) onto default resource config. @@ -87,7 +104,7 @@ pub async fn push_updates_for_view( let current_tags = current_resource .tags .iter() - .filter_map(|id| all_tags.get(id).map(|t| t.name.clone())) + .filter_map(|id| id_to_tags.get(id).map(|t| t.name.clone())) .collect::>(); // Only proceed if there are any fields to update, @@ -105,12 +122,14 @@ pub async fn push_updates_for_view( current_resource.id.clone(), ), data: DiffData::Update { - proposed, current: super::toml::resource_to_toml::( current_resource.clone(), + proposed_resource.deploy, + proposed_resource.after, all_resources, - all_tags, + id_to_tags, )?, + proposed, }, }); } @@ -120,6 +139,7 @@ pub async fn push_updates_for_view( target: Resource::resource_target(String::new()), data: DiffData::Create { + name: proposed_resource.name.clone(), proposed: super::toml::resource_toml_to_toml_string::< Resource, >(proposed_resource)?, diff --git a/bin/periphery/src/api/compose.rs b/bin/periphery/src/api/compose.rs index 3b21c3bc1..3323612f6 100644 --- a/bin/periphery/src/api/compose.rs +++ b/bin/periphery/src/api/compose.rs @@ -3,10 +3,15 @@ use std::path::PathBuf; use anyhow::{anyhow, Context}; use command::run_komodo_command; use formatting::format_serror; +use git::{write_commit_file, GitRes}; use komodo_client::entities::{ - stack::ComposeProject, to_komodo_name, update::Log, FileContents, + stack::ComposeProject, to_komodo_name, update::Log, CloneArgs, + FileContents, +}; +use periphery_client::api::{ + compose::*, + git::{PullOrCloneRepo, RepoActionResponse}, }; -use periphery_client::api::compose::*; use resolver_api::Resolve; use serde::{Deserialize, Serialize}; use tokio::fs; @@ -248,6 +253,103 @@ impl Resolve for State { // +impl Resolve for State { + #[instrument(name = "WriteCommitComposeContents", skip(self))] + async fn resolve( + &self, + WriteCommitComposeContents { + stack, + file_path, + contents, + git_token, + }: WriteCommitComposeContents, + _: (), + ) -> anyhow::Result { + if stack.config.files_on_host { + return Err(anyhow!( + "Wrong method called for files on host stack" + )); + } + if stack.config.repo.is_empty() { + return Err(anyhow!("Repo is not configured")); + } + + let root = periphery_config() + .stack_dir + .join(to_komodo_name(&stack.name)); + + let mut args: CloneArgs = (&stack).into(); + // Set the clone destination to the one created for this run + args.destination = Some(root.display().to_string()); + + let git_token = match git_token { + Some(token) => Some(token), + None => { + if !stack.config.git_account.is_empty() { + match crate::helpers::git_token( + &stack.config.git_provider, + &stack.config.git_account, + ) { + Ok(token) => Some(token.to_string()), + Err(e) => { + return Err( + e.context("Failed to find required git token"), + ); + } + } + } else { + None + } + } + }; + + State + .resolve( + PullOrCloneRepo { + args, + git_token, + environment: vec![], + 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 file_path = stack + .config + .run_directory + .parse::() + .context("Run directory is not a valid path")? + .join(&file_path); + + let GitRes { + logs, + hash, + message, + .. + } = write_commit_file( + "Write Compose File", + &root, + &file_path, + &contents, + ) + .await?; + + Ok(RepoActionResponse { + logs, + commit_hash: hash, + commit_message: message, + env_file_path: None, + }) + } +} + +// + impl Resolve for State { #[instrument( name = "ComposeUp", diff --git a/bin/periphery/src/api/container.rs b/bin/periphery/src/api/container.rs index bc5f76946..91dd38cbc 100644 --- a/bin/periphery/src/api/container.rs +++ b/bin/periphery/src/api/container.rs @@ -317,13 +317,17 @@ impl Resolve for State { .list_containers() .await .context("failed to list all containers on host")?; - let futures = - containers.iter().map(|ContainerListItem { name, .. }| { - let command = format!("docker start {name}"); - async move { - run_komodo_command(&command.clone(), None, command).await + let futures = containers.iter().filter_map( + |ContainerListItem { name, labels, .. }| { + if labels.contains_key("komodo.skip") { + return None; } - }); + let command = format!("docker start {name}"); + Some(async move { + run_komodo_command(&command.clone(), None, command).await + }) + }, + ); Ok(join_all(futures).await) } } @@ -341,13 +345,17 @@ impl Resolve for State { .list_containers() .await .context("failed to list all containers on host")?; - let futures = - containers.iter().map(|ContainerListItem { name, .. }| { - let command = format!("docker restart {name}"); - async move { - run_komodo_command(&command.clone(), None, command).await + let futures = containers.iter().filter_map( + |ContainerListItem { name, labels, .. }| { + if labels.contains_key("komodo.skip") { + return None; } - }); + let command = format!("docker restart {name}"); + Some(async move { + run_komodo_command(&command.clone(), None, command).await + }) + }, + ); Ok(join_all(futures).await) } } @@ -365,13 +373,17 @@ impl Resolve for State { .list_containers() .await .context("failed to list all containers on host")?; - let futures = - containers.iter().map(|ContainerListItem { name, .. }| { - let command = format!("docker pause {name}"); - async move { - run_komodo_command(&command.clone(), None, command).await + let futures = containers.iter().filter_map( + |ContainerListItem { name, labels, .. }| { + if labels.contains_key("komodo.skip") { + return None; } - }); + let command = format!("docker pause {name}"); + Some(async move { + run_komodo_command(&command.clone(), None, command).await + }) + }, + ); Ok(join_all(futures).await) } } @@ -389,13 +401,17 @@ impl Resolve for State { .list_containers() .await .context("failed to list all containers on host")?; - let futures = - containers.iter().map(|ContainerListItem { name, .. }| { - let command = format!("docker unpause {name}"); - async move { - run_komodo_command(&command.clone(), None, command).await + let futures = containers.iter().filter_map( + |ContainerListItem { name, labels, .. }| { + if labels.contains_key("komodo.skip") { + return None; } - }); + let command = format!("docker unpause {name}"); + Some(async move { + run_komodo_command(&command.clone(), None, command).await + }) + }, + ); Ok(join_all(futures).await) } } @@ -413,14 +429,19 @@ impl Resolve for State { .list_containers() .await .context("failed to list all containers on host")?; - let futures = containers.iter().map( - |ContainerListItem { name, .. }| async move { - run_komodo_command( - &format!("docker stop {name}"), - None, - stop_container_command(name, None, None), - ) - .await + let futures = containers.iter().filter_map( + |ContainerListItem { name, labels, .. }| { + if labels.contains_key("komodo.skip") { + return None; + } + Some(async move { + run_komodo_command( + &format!("docker stop {name}"), + None, + stop_container_command(name, None, None), + ) + .await + }) }, ); Ok(join_all(futures).await) diff --git a/bin/periphery/src/api/mod.rs b/bin/periphery/src/api/mod.rs index 22cd7e7b8..ff334837c 100644 --- a/bin/periphery/src/api/mod.rs +++ b/bin/periphery/src/api/mod.rs @@ -80,6 +80,7 @@ pub enum PeripheryRequest { // Compose (Write) WriteComposeContentsToHost(WriteComposeContentsToHost), + WriteCommitComposeContents(WriteCommitComposeContents), ComposeUp(ComposeUp), ComposeExecution(ComposeExecution), diff --git a/bin/periphery/src/compose.rs b/bin/periphery/src/compose.rs index 8c69e576d..cb81c84b7 100644 --- a/bin/periphery/src/compose.rs +++ b/bin/periphery/src/compose.rs @@ -449,6 +449,15 @@ async fn write_stack<'a>( } }; + let env_file_path = stack + .config + .run_directory + .parse::() + .context("Invalid run_directory")? + .join(&stack.config.env_file_path) + .display() + .to_string(); + let clone_or_pull_res = if stack.config.reclone { State .resolve( @@ -456,7 +465,7 @@ async fn write_stack<'a>( args, git_token, environment: env_vars, - env_file_path: stack.config.env_file_path.clone(), + env_file_path, skip_secret_interp: stack.config.skip_secret_interp, // repo replacer only needed for on_clone / on_pull, // which aren't available for stacks @@ -472,7 +481,7 @@ async fn write_stack<'a>( args, git_token, environment: env_vars, - env_file_path: stack.config.env_file_path.clone(), + env_file_path, skip_secret_interp: stack.config.skip_secret_interp, // repo replacer only needed for on_clone / on_pull, // which aren't available for stacks diff --git a/bin/periphery/src/docker.rs b/bin/periphery/src/docker.rs index d022c3923..826f2e2cf 100644 --- a/bin/periphery/src/docker.rs +++ b/bin/periphery/src/docker.rs @@ -50,6 +50,7 @@ impl DockerClient { .into_iter() .map(|container| { Ok(ContainerListItem { + server_id: None, name: container .names .context("no names on container")? @@ -88,6 +89,7 @@ impl DockerClient { .collect() }) .unwrap_or_default(), + labels: container.labels.unwrap_or_default(), }) }) .collect() diff --git a/client/core/rs/src/api/execute/mod.rs b/client/core/rs/src/api/execute/mod.rs index d0530d91a..f1d6adbf0 100644 --- a/client/core/rs/src/api/execute/mod.rs +++ b/client/core/rs/src/api/execute/mod.rs @@ -104,6 +104,7 @@ pub enum Execution { // STACK DeployStack(DeployStack), + DeployStackIfChanged(DeployStackIfChanged), StartStack(StartStack), RestartStack(RestartStack), PauseStack(PauseStack), diff --git a/client/core/rs/src/api/execute/stack.rs b/client/core/rs/src/api/execute/stack.rs index 0a1d9f599..15779cdc4 100644 --- a/client/core/rs/src/api/execute/stack.rs +++ b/client/core/rs/src/api/execute/stack.rs @@ -9,8 +9,6 @@ use crate::entities::update::Update; use super::KomodoExecuteRequest; /// Deploys the target stack. `docker compose up`. Response: [Update] -/// -/// Note. If the stack is already deployed, it will be destroyed first. #[typeshare] #[derive( Debug, @@ -32,6 +30,30 @@ pub struct DeployStack { pub stop_time: Option, } +/// Checks deployed contents vs latest contents, +/// and only if any changes found +/// will `docker compose up`. Response: [Update] +#[typeshare] +#[derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + Request, + EmptyTraits, + Parser, +)] +#[empty_traits(KomodoExecuteRequest)] +#[response(Update)] +pub struct DeployStackIfChanged { + /// Id or name + pub stack: String, + /// Override the default termination max time. + /// Only used if the stack needs to be taken down first. + pub stop_time: Option, +} + // /// Starts the target stack. `docker compose start`. Response: [Update] diff --git a/client/core/rs/src/api/execute/sync.rs b/client/core/rs/src/api/execute/sync.rs index db00e1c1d..a7e6d7f97 100644 --- a/client/core/rs/src/api/execute/sync.rs +++ b/client/core/rs/src/api/execute/sync.rs @@ -4,7 +4,7 @@ use resolver_api::derive::Request; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::entities::update::Update; +use crate::entities::{update::Update, ResourceTargetVariant}; use super::KomodoExecuteRequest; @@ -25,4 +25,11 @@ use super::KomodoExecuteRequest; pub struct RunSync { /// Id or name pub sync: String, + /// Only execute sync on a specific resource type. + /// Combine with `resource_id` to specify resource. + pub resource_type: Option, + /// Only execute sync on a specific resources. + /// Combine with `resource_type` to specify resources. + /// Supports name or id. + pub resources: Option>, } diff --git a/client/core/rs/src/api/read/server.rs b/client/core/rs/src/api/read/server.rs index 6f2712fd9..04c46c0dc 100644 --- a/client/core/rs/src/api/read/server.rs +++ b/client/core/rs/src/api/read/server.rs @@ -262,6 +262,25 @@ pub type ListDockerContainersResponse = Vec; // +/// List all docker containers on the target server. +/// Response: [ListDockerContainersResponse]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(KomodoReadRequest)] +#[response(ListAllDockerContainersResponse)] +pub struct ListAllDockerContainers { + /// Filter by server id or name. + #[serde(default)] + pub servers: Vec, +} + +#[typeshare] +pub type ListAllDockerContainersResponse = Vec; + +// + /// Inspect a docker container on the server. Response: [Container]. #[typeshare] #[derive( diff --git a/client/core/rs/src/api/write/stack.rs b/client/core/rs/src/api/write/stack.rs index 13d6fdef6..8ef5672b0 100644 --- a/client/core/rs/src/api/write/stack.rs +++ b/client/core/rs/src/api/write/stack.rs @@ -103,7 +103,7 @@ pub struct RenameStack { // -/// Rename the stack at id to the given name. Response: [Update]. +/// Update file contents in Files on Server or Git Repo mode. Response: [Update]. #[typeshare] #[derive( Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, @@ -111,7 +111,7 @@ pub struct RenameStack { #[empty_traits(KomodoWriteRequest)] #[response(Update)] pub struct WriteStackFileContents { - /// The name or id of the Stack to write the contents to. + /// The name or id of the target Stack. #[serde(alias = "id", alias = "name")] pub stack: String, /// The file path relative to the stack run directory, diff --git a/client/core/rs/src/api/write/sync.rs b/client/core/rs/src/api/write/sync.rs index aa92028b0..2d89f590e 100644 --- a/client/core/rs/src/api/write/sync.rs +++ b/client/core/rs/src/api/write/sync.rs @@ -5,6 +5,7 @@ use typeshare::typeshare; use crate::entities::{ sync::{ResourceSync, _PartialResourceSyncConfig}, + update::Update, NoData, }; @@ -98,6 +99,28 @@ pub struct RefreshResourceSyncPending { // +/// Rename the stack at id to the given name. Response: [Update]. +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(KomodoWriteRequest)] +#[response(Update)] +pub struct WriteSyncFileContents { + /// The name or id of the target Sync. + #[serde(alias = "id", alias = "name")] + pub sync: String, + /// If this file was under a resource folder, this will be the folder. + /// Otherwise, it should be empty string. + pub resource_path: String, + /// The file path relative to the resource path. + pub file_path: String, + /// The contents to write. + pub contents: String, +} + +// + /// Commits matching resources updated configuration to the target resource sync. Response: [Update] /// /// Note. Will fail if the Sync is not `managed`. diff --git a/client/core/rs/src/entities/config/core.rs b/client/core/rs/src/entities/config/core.rs index 2fe3e1275..c430b5fe1 100644 --- a/client/core/rs/src/entities/config/core.rs +++ b/client/core/rs/src/entities/config/core.rs @@ -58,6 +58,8 @@ pub struct Env { pub komodo_jwt_secret_file: Option, /// Override `jwt_ttl` pub komodo_jwt_ttl: Option, + /// Override `sync_directory` + pub komodo_sync_directory: Option, /// Override `repo_directory` pub komodo_repo_directory: Option, /// Override `resource_poll_interval` @@ -459,15 +461,9 @@ pub struct CoreConfig { #[serde(default)] pub secrets: HashMap, - // ========= - // = Other = - // ========= - /// Specify the directory used to clone stack / repo / build repos, for latest hash / contents. - /// The default is fine when using a container. - /// Default: `/repo-cache` - #[serde(default = "default_repo_directory")] - pub repo_directory: PathBuf, - + // ======= + // = SSL = + // ======= /// Whether to enable ssl. #[serde(default)] pub ssl_enabled: bool, @@ -481,6 +477,20 @@ pub struct CoreConfig { /// Default: `/config/ssl/cert.pem`. #[serde(default = "default_ssl_cert_file")] pub ssl_cert_file: PathBuf, + + // ========= + // = Other = + // ========= + /// Configure directory to store sync files. + /// Default: `/syncs` + #[serde(default = "default_sync_directory")] + pub sync_directory: PathBuf, + + /// Specify the directory used to clone stack / repo / build repos, for latest hash / contents. + /// The default is fine when using a container. + /// Default: `/repo-cache` + #[serde(default = "default_repo_directory")] + pub repo_directory: PathBuf, } fn default_title() -> String { @@ -504,6 +514,11 @@ fn default_repo_directory() -> PathBuf { PathBuf::from_str("/repo-cache").unwrap() } +fn default_sync_directory() -> PathBuf { + // unwrap ok: `/syncs` will always be valid path + PathBuf::from_str("/syncs").unwrap() +} + fn default_prune_days() -> u64 { 14 } @@ -537,6 +552,7 @@ impl CoreConfig { jwt_secret: empty_or_redacted(&config.jwt_secret), jwt_ttl: config.jwt_ttl, repo_directory: config.repo_directory, + sync_directory: config.sync_directory, resource_poll_interval: config.resource_poll_interval, monitoring_interval: config.monitoring_interval, keep_stats_for_days: config.keep_stats_for_days, diff --git a/client/core/rs/src/entities/docker/container.rs b/client/core/rs/src/entities/docker/container.rs index e5551cba2..0e7e09a0d 100644 --- a/client/core/rs/src/entities/docker/container.rs +++ b/client/core/rs/src/entities/docker/container.rs @@ -13,6 +13,8 @@ use super::{ContainerConfig, GraphDriverData, PortBinding}; Debug, Clone, Default, PartialEq, Serialize, Deserialize, )] pub struct ContainerListItem { + /// The Server which holds the container. + pub server_id: Option, /// The first name in Names, not including the initial '/' pub name: String, /// The ID of this container @@ -37,6 +39,11 @@ pub struct ContainerListItem { pub networks: Vec, /// The volume names attached to container pub volumes: Vec, + /// The labels attached to container. + /// It's too big to send with container list, + /// can get it using InspectContainer + #[serde(default, skip_serializing)] + pub labels: HashMap, } #[typeshare] diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index 3bbbbbc6e..1228d8797 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -197,7 +197,7 @@ pub struct __Serror { pub struct SystemCommand { #[serde(default)] pub path: String, - #[serde(default)] + #[serde(default, deserialize_with = "file_contents_deserializer")] pub command: String, } @@ -949,6 +949,7 @@ pub enum Operation { CreateResourceSync, UpdateResourceSync, DeleteResourceSync, + WriteSyncContents, CommitSync, RunSync, @@ -1204,8 +1205,8 @@ impl<'de> Visitor<'de> for FileContentsVisitor { where E: serde::de::Error, { - let out = v.to_string(); - if out.is_empty() || out.ends_with('\n') { + let out = v.trim_end().to_string(); + if out.is_empty() { Ok(out) } else { Ok(out + "\n") diff --git a/client/core/rs/src/entities/stack.rs b/client/core/rs/src/entities/stack.rs index 7f9b2eaf4..f9f4adea1 100644 --- a/client/core/rs/src/entities/stack.rs +++ b/client/core/rs/src/entities/stack.rs @@ -307,6 +307,12 @@ pub struct StackConfig { #[builder(default)] pub webhook_secret: String, + /// By default, the Stack will `DeployStackIfChanged`. + /// If this option is enabled, will always run `DeployStack` without diffing. + #[serde(default)] + #[builder(default)] + pub webhook_force_deploy: bool, + /// Whether to send StackStateChange alerts for this stack. #[serde(default = "default_send_alerts")] #[builder(default = "default_send_alerts()")] @@ -443,6 +449,7 @@ impl Default for StackConfig { git_account: Default::default(), webhook_enabled: default_webhook_enabled(), webhook_secret: Default::default(), + webhook_force_deploy: Default::default(), send_alerts: default_send_alerts(), links: Default::default(), } diff --git a/client/core/rs/src/entities/sync.rs b/client/core/rs/src/entities/sync.rs index 26db03d01..e239c1f41 100644 --- a/client/core/rs/src/entities/sync.rs +++ b/client/core/rs/src/entities/sync.rs @@ -2,13 +2,16 @@ use bson::{doc, Document}; use derive_builder::Builder; use derive_default_builder::DefaultBuilder; use partial_derive2::Partial; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{value::SeqAccessDeserializer, Visitor}, + Deserialize, Deserializer, Serialize, +}; use strum::Display; use typeshare::typeshare; use super::{ resource::{Resource, ResourceListItem, ResourceQuery}, - FileContents, ResourceTarget, I64, + ResourceTarget, I64, }; #[typeshare] @@ -26,8 +29,8 @@ pub struct ResourceSyncListItemInfo { pub file_contents: bool, /// Whether sync has `managed` mode enabled. pub managed: bool, - /// Resource path to the files. - pub resource_path: String, + /// Resource paths to the files. + pub resource_path: Vec, /// The git provider domain. pub git_provider: String, /// The Github repo used as the source of the sync resources @@ -96,10 +99,10 @@ pub struct ResourceSyncInfo { /// The current sync files #[serde(default)] - pub remote_contents: Vec, + pub remote_contents: Vec, /// Any read errors in files by path #[serde(default)] - pub remote_errors: Vec, + pub remote_errors: Vec, } #[typeshare] @@ -118,6 +121,9 @@ pub struct ResourceDiff { pub enum DiffData { /// Resource will be created Create { + /// The name of resource to create + #[serde(default)] + name: String, /// The proposed resource to create in TOML proposed: String, }, @@ -213,10 +219,13 @@ pub struct ResourceSyncConfig { /// - If Git Repo based, this is relative to the root of the repo. /// Can be a specific file, or a directory containing multiple files / folders. /// See [https://komo.do/docs/sync-resources](https://komo.do/docs/sync-resources) for more information. - #[serde(default = "default_resource_path")] - #[builder(default = "default_resource_path()")] - #[partial_default(default_resource_path())] - pub resource_path: String, + #[serde(default, deserialize_with = "resource_path_deserializer")] + #[partial_attr(serde( + default, + deserialize_with = "option_resource_path_deserializer" + ))] + #[builder(default)] + pub resource_path: Vec, /// Enable "pushes" to the file, /// which exports resources matching tags to single file. @@ -270,10 +279,6 @@ fn default_branch() -> String { String::from("main") } -fn default_resource_path() -> String { - String::from("./resources.toml") -} - fn default_webhook_enabled() -> bool { true } @@ -287,7 +292,7 @@ impl Default for ResourceSyncConfig { branch: default_branch(), commit: Default::default(), git_account: Default::default(), - resource_path: default_resource_path(), + resource_path: Default::default(), files_on_host: Default::default(), file_contents: Default::default(), managed: Default::default(), @@ -299,6 +304,106 @@ impl Default for ResourceSyncConfig { } } +#[typeshare] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct SyncFileContents { + /// The base resource path. + #[serde(default)] + pub resource_path: String, + /// The path of the file / error path relative to the resource path. + pub path: String, + /// The contents of the file + pub contents: String, +} + +/// Using this ensures the resource path deserialized to Vec +pub fn resource_path_deserializer<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(ResourcePathVisitor) +} + +/// Using this ensures the resource path deserialized to Vec +pub fn option_resource_path_deserializer<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(OptionResourcePathVisitor) +} + +struct ResourcePathVisitor; + +impl<'de> Visitor<'de> for ResourcePathVisitor { + type Value = Vec; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + write!(formatter, "string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(vec![v.to_string()]) + } + + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + Vec::deserialize(SeqAccessDeserializer::new(seq)) + } +} + +struct OptionResourcePathVisitor; + +impl<'de> Visitor<'de> for OptionResourcePathVisitor { + type Value = Option>; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + write!(formatter, "null or string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + ResourcePathVisitor.visit_str(v).map(Some) + } + + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + ResourcePathVisitor.visit_seq(seq).map(Some) + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } +} + #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] pub struct ResourceSyncActionState { diff --git a/client/core/rs/src/entities/toml.rs b/client/core/rs/src/entities/toml.rs index 2b713121c..27f66721c 100644 --- a/client/core/rs/src/entities/toml.rs +++ b/client/core/rs/src/entities/toml.rs @@ -129,14 +129,6 @@ pub struct ResourceToml { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub after: Vec, - /// Optional. Only relevant for stacks / repos. - /// - /// Will ensure stacks with 'deploy = true' are running with the latest commit hash for the repo. - /// Deploy actions to achieve this will be included in the sync. - /// Default is false. - #[serde(default, skip_serializing_if = "is_false")] - pub latest_hash: bool, - /// Resource specific configuration. #[serde(default)] pub config: PartialConfig, diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts index e767d239b..248ee9a0f 100644 --- a/client/core/ts/src/responses.ts +++ b/client/core/ts/src/responses.ts @@ -58,6 +58,7 @@ export type ReadResponses = { GetServerState: Types.GetServerStateResponse; GetPeripheryVersion: Types.GetPeripheryVersionResponse; ListDockerContainers: Types.ListDockerContainersResponse; + ListAllDockerContainers: Types.ListAllDockerContainersResponse; InspectDockerContainer: Types.InspectDockerContainerResponse; GetResourceMatchingContainer: Types.GetResourceMatchingContainerResponse; GetContainerLog: Types.GetContainerLogResponse; @@ -262,8 +263,9 @@ export type WriteResponses = { CopyResourceSync: Types.ResourceSync; DeleteResourceSync: Types.ResourceSync; UpdateResourceSync: Types.ResourceSync; - RefreshResourceSyncPending: Types.ResourceSync; CommitSync: Types.ResourceSync; + WriteSyncFileContents: Types.Update; + RefreshResourceSyncPending: Types.ResourceSync; CreateSyncWebhook: Types.CreateSyncWebhookResponse; DeleteSyncWebhook: Types.DeleteSyncWebhookResponse; @@ -354,6 +356,7 @@ export type ExecuteResponses = { // ==== STACK ==== DeployStack: Types.Update; + DeployStackIfChanged: Types.Update; StartStack: Types.Update; RestartStack: Types.Update; StopStack: Types.Update; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 3e12c12b3..003d62f75 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -297,7 +297,7 @@ export type AlerterEndpoint = export interface AlerterConfig { /** Whether the alerter is enabled */ - enabled: boolean; + enabled?: boolean; /** * Where to route the alert messages. * @@ -865,6 +865,7 @@ export type Execution = | { type: "PruneSystem", params: PruneSystem } | { type: "RunSync", params: RunSync } | { type: "DeployStack", params: DeployStack } + | { type: "DeployStackIfChanged", params: DeployStackIfChanged } | { type: "StartStack", params: StartStack } | { type: "RestartStack", params: RestartStack } | { type: "PauseStack", params: PauseStack } @@ -1498,6 +1499,8 @@ export enum ContainerStateStatusEnum { } export interface ContainerListItem { + /** The Server which holds the container. */ + server_id?: string; /** The first name in Names, not including the initial '/' */ name: string; /** The ID of this container */ @@ -1522,10 +1525,18 @@ export interface ContainerListItem { networks: string[]; /** The volume names attached to container */ volumes: string[]; + /** + * The labels attached to container. + * It's too big to send with container list, + * can get it using InspectContainer + */ + labels?: Record; } export type ListDockerContainersResponse = ContainerListItem[]; +export type ListAllDockerContainersResponse = ContainerListItem[]; + export enum HealthStatusEnum { Empty = "", None = "none", @@ -2381,6 +2392,11 @@ export interface StackConfig { * If its an empty string, use the default secret from the config. */ webhook_secret?: string; + /** + * By default, the Stack will `DeployStackIfChanged`. + * If this option is enabled, will always run `DeployStack` without diffing. + */ + webhook_force_deploy?: boolean; /** Whether to send StackStateChange alerts for this stack. */ send_alerts: boolean; /** Used with `registry_account` to login to a registry before docker compose up. */ @@ -2641,7 +2657,7 @@ export interface ResourceSyncConfig { * Can be a specific file, or a directory containing multiple files / folders. * See [https://komo.do/docs/sync-resources](https://komo.do/docs/sync-resources) for more information. */ - resource_path: string; + resource_path?: string[]; /** * Enable "pushes" to the file, * which exports resources matching tags to single file. @@ -2667,6 +2683,8 @@ export interface ResourceSyncConfig { export type DiffData = /** Resource will be created */ | { type: "Create", data: { + /** The name of resource to create */ + name?: string; /** The proposed resource to create in TOML */ proposed: string; }} @@ -2698,6 +2716,15 @@ export interface SyncDeployUpdate { log: string; } +export interface SyncFileContents { + /** The base resource path. */ + resource_path?: string; + /** The path of the file / error path relative to the resource path. */ + path: string; + /** The contents of the file */ + contents: string; +} + export interface ResourceSyncInfo { /** Unix timestamp of last applied sync */ last_sync_ts?: I64; @@ -2720,9 +2747,9 @@ export interface ResourceSyncInfo { /** The commit message which produced these pending updates. */ pending_message?: string; /** The current sync files */ - remote_contents?: FileContents[]; + remote_contents?: SyncFileContents[]; /** Any read errors in files by path */ - remote_errors?: FileContents[]; + remote_errors?: SyncFileContents[]; } export type ResourceSync = Resource; @@ -2751,8 +2778,8 @@ export interface ResourceSyncListItemInfo { file_contents: boolean; /** Whether sync has `managed` mode enabled. */ managed: boolean; - /** Resource path to the files. */ - resource_path: string; + /** Resource paths to the files. */ + resource_path: string[]; /** The git provider domain. */ git_provider: string; /** The Github repo used as the source of the sync resources */ @@ -2890,6 +2917,7 @@ export enum Operation { CreateResourceSync = "CreateResourceSync", UpdateResourceSync = "UpdateResourceSync", DeleteResourceSync = "DeleteResourceSync", + WriteSyncContents = "WriteSyncContents", CommitSync = "CommitSync", RunSync = "RunSync", CreateVariable = "CreateVariable", @@ -3754,11 +3782,7 @@ export interface LaunchServer { server_template: string; } -/** - * Deploys the target stack. `docker compose up`. Response: [Update] - * - * Note. If the stack is already deployed, it will be destroyed first. - */ +/** Deploys the target stack. `docker compose up`. Response: [Update] */ export interface DeployStack { /** Id or name */ stack: string; @@ -3769,6 +3793,21 @@ export interface DeployStack { stop_time?: number; } +/** + * Checks deployed contents vs latest contents, + * and only if any changes found + * will `docker compose up`. Response: [Update] + */ +export interface DeployStackIfChanged { + /** Id or name */ + stack: string; + /** + * Override the default termination max time. + * Only used if the stack needs to be taken down first. + */ + stop_time?: number; +} + /** Starts the target stack. `docker compose start`. Response: [Update] */ export interface StartStack { /** Id or name */ @@ -3829,6 +3868,17 @@ export interface DestroyStack { export interface RunSync { /** Id or name */ sync: string; + /** + * Only execute sync on a specific resource type. + * Combine with `resource_id` to specify resource. + */ + resource_type?: ResourceTarget["type"]; + /** + * Only execute sync on a specific resources. + * Combine with `resource_type` to specify resources. + * Supports name or id. + */ + resources?: string[]; } /** @@ -4592,6 +4642,15 @@ export interface ListDockerContainers { server: string; } +/** + * List all docker containers on the target server. + * Response: [ListDockerContainersResponse]. + */ +export interface ListAllDockerContainers { + /** Filter by server id or name. */ + servers?: string[]; +} + /** Inspect a docker container on the server. Response: [Container]. */ export interface InspectDockerContainer { /** Id or name */ @@ -5907,9 +5966,9 @@ export interface RenameStack { name: string; } -/** Rename the stack at id to the given name. Response: [Update]. */ +/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */ export interface WriteStackFileContents { - /** The name or id of the Stack to write the contents to. */ + /** The name or id of the target Stack. */ stack: string; /** * The file path relative to the stack run directory, @@ -6009,6 +6068,21 @@ export interface RefreshResourceSyncPending { sync: string; } +/** Rename the stack at id to the given name. Response: [Update]. */ +export interface WriteSyncFileContents { + /** The name or id of the target Sync. */ + sync: string; + /** + * If this file was under a resource folder, this will be the folder. + * Otherwise, it should be empty string. + */ + resource_path: string; + /** The file path relative to the resource path. */ + file_path: string; + /** The contents to write. */ + contents: string; +} + /** * Commits matching resources updated configuration to the target resource sync. Response: [Update] * @@ -6591,6 +6665,7 @@ export type ExecuteRequest = | { type: "StopDeployment", params: StopDeployment } | { type: "DestroyDeployment", params: DestroyDeployment } | { type: "DeployStack", params: DeployStack } + | { type: "DeployStackIfChanged", params: DeployStackIfChanged } | { type: "StartStack", params: StartStack } | { type: "RestartStack", params: RestartStack } | { type: "StopStack", params: StopStack } @@ -6649,6 +6724,7 @@ export type ReadRequest = | { type: "InspectDockerImage", params: InspectDockerImage } | { type: "ListDockerImageHistory", params: ListDockerImageHistory } | { type: "InspectDockerVolume", params: InspectDockerVolume } + | { type: "ListAllDockerContainers", params: ListAllDockerContainers } | { type: "ListDockerContainers", params: ListDockerContainers } | { type: "ListDockerNetworks", params: ListDockerNetworks } | { type: "ListDockerImages", params: ListDockerImages } @@ -6791,8 +6867,9 @@ export type WriteRequest = | { type: "CopyResourceSync", params: CopyResourceSync } | { type: "DeleteResourceSync", params: DeleteResourceSync } | { type: "UpdateResourceSync", params: UpdateResourceSync } - | { type: "RefreshResourceSyncPending", params: RefreshResourceSyncPending } + | { type: "WriteSyncFileContents", params: WriteSyncFileContents } | { type: "CommitSync", params: CommitSync } + | { type: "RefreshResourceSyncPending", params: RefreshResourceSyncPending } | { type: "CreateSyncWebhook", params: CreateSyncWebhook } | { type: "DeleteSyncWebhook", params: DeleteSyncWebhook } | { type: "CreateStack", params: CreateStack } diff --git a/client/periphery/rs/src/api/compose.rs b/client/periphery/rs/src/api/compose.rs index f13d9a87f..4f31ec0a1 100644 --- a/client/periphery/rs/src/api/compose.rs +++ b/client/periphery/rs/src/api/compose.rs @@ -6,6 +6,8 @@ use komodo_client::entities::{ use resolver_api::derive::Request; use serde::{Deserialize, Serialize}; +use super::git::RepoActionResponse; + /// List the compose project names that are on the host. /// List running `docker compose ls` /// @@ -100,6 +102,23 @@ pub struct WriteComposeContentsToHost { // +/// Write and commit compose contents. +/// Only works with git repo based stacks. +#[derive(Debug, Clone, Serialize, Deserialize, Request)] +#[response(RepoActionResponse)] +pub struct WriteCommitComposeContents { + /// The stack to write to. + pub stack: Stack, + /// Relative to the stack folder + run directory. + pub file_path: String, + /// The contents to write. + pub contents: String, + /// If provided, use it to login in. Otherwise check periphery local git providers. + pub git_token: Option, +} + +// + /// Rewrites the compose directory, pulls any images, takes down existing containers, /// and runs docker compose up. #[derive(Debug, Clone, Serialize, Deserialize, Request)] @@ -111,7 +130,7 @@ pub struct ComposeUp { pub service: Option, /// If provided, use it to login in. Otherwise check periphery local registries. pub git_token: Option, - /// If provided, use it to login in. Otherwise check periphery local registries. + /// If provided, use it to login in. Otherwise check periphery local git providers. pub registry_token: Option, /// Propogate any secret replacers from core interpolation. #[serde(default)] diff --git a/client/periphery/rs/src/api/git.rs b/client/periphery/rs/src/api/git.rs index d18233481..94a00b80b 100644 --- a/client/periphery/rs/src/api/git.rs +++ b/client/periphery/rs/src/api/git.rs @@ -74,7 +74,7 @@ pub struct PullOrCloneRepo { // -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct RepoActionResponse { pub logs: Vec, pub commit_hash: Option, diff --git a/compose/caddy.compose.yaml b/compose/caddy.compose.yaml index 62f148daa..33e604819 100644 --- a/compose/caddy.compose.yaml +++ b/compose/caddy.compose.yaml @@ -1,6 +1,8 @@ services: caddy: image: caddy + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped command: caddy reverse-proxy --from ${KOMODO_HOST} --to core:9120 cap_add: diff --git a/compose/compose.env b/compose/compose.env index 9a2f5a165..417ea7ec8 100644 --- a/compose/compose.env +++ b/compose/compose.env @@ -18,6 +18,9 @@ COMPOSE_LOGGING_DRIVER=local # Enable log rotation with the local driver. DB_USERNAME=admin DB_PASSWORD=admin +## Configure a secure passkey to authenticate between Core / Periphery. +PASSKEY=a_random_passkey + #=-------------------------=# #= Komodo Core Environment =# #=-------------------------=# @@ -50,7 +53,7 @@ KOMODO_MONITORING_INTERVAL="15-sec" KOMODO_RESOURCE_POLL_INTERVAL="5-min" ## Used to auth against periphery. Alt: KOMODO_PASSKEY_FILE -KOMODO_PASSKEY=a_random_passkey +KOMODO_PASSKEY=${PASSKEY} ## Used to auth incoming webhooks. Alt: KOMODO_WEBHOOK_SECRET_FILE KOMODO_WEBHOOK_SECRET=a_random_secret ## Used to generate jwt. Alt: KOMODO_JWT_SECRET_FILE @@ -111,6 +114,9 @@ KOMODO_HETZNER_TOKEN= # Alt: KOMODO_HETZNER_TOKEN_FILE ## Full variable list + descriptions are available here: ## 🦎 https://github.com/mbecker20/komodo/blob/main/config/periphery.config.toml 🦎 +## Periphery passkeys must include KOMODO_PASSKEY to authenticate +PERIPHERY_PASSKEYS=${PASSKEY} + ## Enable SSL using self signed certificates. ## Connect to Periphery at https://address:8120. PERIPHERY_SSL_ENABLED=true diff --git a/compose/mongo.compose.yaml b/compose/mongo.compose.yaml index 15fd6fe8d..97f0508bd 100644 --- a/compose/mongo.compose.yaml +++ b/compose/mongo.compose.yaml @@ -10,7 +10,7 @@ services: mongo: image: mongo - command: --quiet # suppress mongo logs a bit + command: --quiet --wiredTigerCacheSizeGB 0.25 restart: unless-stopped logging: driver: ${COMPOSE_LOGGING_DRIVER:-local} @@ -41,19 +41,25 @@ services: KOMODO_DATABASE_ADDRESS: mongo:27017 KOMODO_DATABASE_USERNAME: ${DB_USERNAME} KOMODO_DATABASE_PASSWORD: ${DB_PASSWORD} - ## allows for systemd Periphery connection at + volumes: + ## Core cache for repos for latest commit hash / contents + - repo-cache:/repo-cache + ## Store sync files on server + # - /path/to/syncs:/syncs + ## Optionally mount a custom core.config.toml + # - /path/to/core.config.toml:/config/config.toml + ## Allows for systemd Periphery connection at ## "http://host.docker.internal:8120" # extra_hosts: # - host.docker.internal:host-gateway - ## Optionally mount a custom core.config.toml - # volumes: - # - /path/to/core.config.toml:/config/config.toml ## Deploy Periphery container using this block, ## or deploy the Periphery binary with systemd using ## https://github.com/mbecker20/komodo/tree/main/scripts periphery: image: ghcr.io/mbecker20/periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest} + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped logging: driver: ${COMPOSE_LOGGING_DRIVER:-local} @@ -78,8 +84,13 @@ services: # - /path/to/compose:/host/compose volumes: + # Mongo mongo-data: mongo-config: + # Core + syncs: + repo-cache: + # Periphery ssl-certs: repos: stacks: diff --git a/compose/postgres.compose.yaml b/compose/postgres.compose.yaml index fce75f600..d7f556626 100644 --- a/compose/postgres.compose.yaml +++ b/compose/postgres.compose.yaml @@ -10,6 +10,8 @@ services: postgres: image: postgres + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped logging: driver: ${COMPOSE_LOGGING_DRIVER:-local} @@ -26,6 +28,8 @@ services: ferretdb: image: ghcr.io/ferretdb/ferretdb + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped depends_on: - postgres @@ -40,6 +44,8 @@ services: core: image: ghcr.io/mbecker20/komodo:${COMPOSE_KOMODO_IMAGE_TAG:-latest} + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped depends_on: - ferretdb @@ -52,20 +58,25 @@ services: env_file: ./compose.env environment: KOMODO_DATABASE_URI: mongodb://${DB_USERNAME}:${DB_PASSWORD}@ferretdb:27017/${KOMODO_DATABASE_DB_NAME:-komodo}?authMechanism=PLAIN - # KOMODO_DATABASE_URI_FILE: - ## allows for systemd Periphery connection at + volumes: + ## Core cache for repos for latest commit hash / contents + - repo-cache:/repo-cache + ## Store sync files on server + # - /path/to/syncs:/syncs + ## Optionally mount a custom core.config.toml + # - /path/to/core.config.toml:/config/config.toml + ## Allows for systemd Periphery connection at ## "http://host.docker.internal:8120" # extra_hosts: # - host.docker.internal:host-gateway - ## Optionally mount a custom core.config.toml - # volumes: - # - /path/to/core.config.toml:/config/config.toml ## Deploy Periphery container using this block, ## or deploy the Periphery binary with systemd using ## https://github.com/mbecker20/komodo/tree/main/scripts periphery: image: ghcr.io/mbecker20/periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest} + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped logging: driver: ${COMPOSE_LOGGING_DRIVER:-local} @@ -90,7 +101,12 @@ services: # - /path/to/compose:/host/compose volumes: + # Postgres pg-data: + # Core + syncs: + repo-cache: + # Periphery ssl-certs: repos: stacks: diff --git a/compose/sqlite.compose.yaml b/compose/sqlite.compose.yaml index 6adbaa157..586122fde 100644 --- a/compose/sqlite.compose.yaml +++ b/compose/sqlite.compose.yaml @@ -10,6 +10,8 @@ services: ferretdb: image: ghcr.io/ferretdb/ferretdb + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped logging: driver: ${COMPOSE_LOGGING_DRIVER:-local} @@ -24,6 +26,8 @@ services: core: image: ghcr.io/mbecker20/komodo:${COMPOSE_KOMODO_IMAGE_TAG:-latest} + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped depends_on: - ferretdb @@ -36,19 +40,25 @@ services: env_file: ./compose.env environment: KOMODO_DATABASE_ADDRESS: ferretdb - ## allows for systemd Periphery connection at + volumes: + ## Core cache for repos for latest commit hash / contents + - repo-cache:/repo-cache + ## Store sync files on server + # - /path/to/syncs:/syncs + ## Optionally mount a custom core.config.toml + # - /path/to/core.config.toml:/config/config.toml + ## Allows for systemd Periphery connection at ## "http://host.docker.internal:8120" # extra_hosts: # - host.docker.internal:host-gateway - ## Optionally mount a custom core.config.toml - # volumes: - # - /path/to/core.config.toml:/config/config.toml ## Deploy Periphery container using this block, ## or deploy the Periphery binary with systemd using ## https://github.com/mbecker20/komodo/tree/main/scripts periphery: image: ghcr.io/mbecker20/periphery:${COMPOSE_KOMODO_IMAGE_TAG:-latest} + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers restart: unless-stopped logging: driver: ${COMPOSE_LOGGING_DRIVER:-local} @@ -73,7 +83,12 @@ services: # - /path/to/compose:/host/compose volumes: + # Sqlite sqlite-data: + # Core + syncs: + repo-cache: + # Periphery ssl-certs: repos: stacks: diff --git a/config/core.config.toml b/config/core.config.toml index 30c063c48..beab82ac4 100644 --- a/config/core.config.toml +++ b/config/core.config.toml @@ -59,6 +59,12 @@ ui_write_disabled = false ## Default: false disable_confirm_dialog = false +## Configure the directory for sync files (inside the container). +## There shouldn't be a need to change this, just mount a volume. +## Env: KOMODO_SYNC_DIRECTORY +## Default: /syncs +sync_directory = "/syncs" + ## Configure the repo directory (inside the container). ## There shouldn't be a need to change this, just mount a volume. ## Env: KOMODO_REPO_DIRECTORY diff --git a/docsite/docs/connect-servers.mdx b/docsite/docs/connect-servers.mdx index eca74605d..8012dd5c8 100644 --- a/docsite/docs/connect-servers.mdx +++ b/docsite/docs/connect-servers.mdx @@ -43,6 +43,8 @@ services: periphery: image: ghcr.io/mbecker20/periphery:latest # image: ghcr.io/mbecker20/periphery:latest-aarch64 # use for arm support + labels: + komodo.skip: # Prevent Komodo from stopping with StopAllContainers logging: driver: local ports: @@ -65,12 +67,16 @@ services: # - /path/to/compose:/host/compose environment: ## Full list: `https://github.com/mbecker20/komodo/blob/main/config/periphery.config.toml` + ## Configure the same passkey given to Komodo Core (KOMODO_PASSKEY) + PERIPHERY_PASSKEYS: your_core_passkey # Alt: PERIPHERY_PASSKEYS_FILE + ## Adding IP here will ensure calling IP is in the list. (optional) + PERIPHERY_ALLOWED_IPS: ## Enable HTTPS server PERIPHERY_SSL_ENABLED: true ## If the disk size is overreporting, can use one of these to ## whitelist / blacklist the disks to filter them, whichever is easier. ## Accepts comma separated list of paths. - ## Usually whitelisting just /etc/hostname gives correct size. + ## Usually whitelisting /etc/hostname gives correct size. PERIPHERY_INCLUDE_DISK_MOUNTS: /etc/hostname # PERIPHERY_EXCLUDE_DISK_MOUNTS: /snap,/etc/repos @@ -112,6 +118,10 @@ periphery \ --merge-nested-config true ``` +:::info +You can run `periphery --help` to see the manual. +::: + ### Passing config files Either file paths or directory paths can be passed to `--config-path`. diff --git a/frontend/package.json b/frontend/package.json index ef5aab53c..187a3e22e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -35,10 +35,10 @@ "clsx": "2.1.1", "cmdk": "1.0.0", "jotai": "2.9.2", - "lightweight-charts": "4.2.0", "lucide-react": "0.437.0", "monaco-editor": "^0.52.0", "react": "18.3.1", + "react-charts": "^3.0.0-beta.57", "react-dom": "18.3.1", "react-minimal-pie-chart": "8.4.0", "react-router-dom": "6.26.0", diff --git a/frontend/src/components/alert/topbar.tsx b/frontend/src/components/alert/topbar.tsx index c97cd093f..79bc0695c 100644 --- a/frontend/src/components/alert/topbar.tsx +++ b/frontend/src/components/alert/topbar.tsx @@ -19,7 +19,7 @@ export const TopbarAlerts = () => { const { data } = useRead( "ListAlerts", { query: { resolved: false } }, - { refetchInterval: 3000 } + { refetchInterval: 10_000 } ); const [open, setOpen] = useState(false); diff --git a/frontend/src/components/config/index.tsx b/frontend/src/components/config/index.tsx index 2663df615..9b01e9b36 100644 --- a/frontend/src/components/config/index.tsx +++ b/frontend/src/components/config/index.tsx @@ -75,7 +75,7 @@ export const ConfigLayout = < onConfirm()} disabled={disabled} file_contents_language={file_contents_language} /> diff --git a/frontend/src/components/config/util.tsx b/frontend/src/components/config/util.tsx index ddee994f4..e709ff66d 100644 --- a/frontend/src/components/config/util.tsx +++ b/frontend/src/components/config/util.tsx @@ -577,7 +577,8 @@ export const InputList = ({ interface ConfirmUpdateProps { previous: T; content: Partial; - onConfirm: () => void; + onConfirm: () => Promise; + loading?: boolean; disabled: boolean; language?: MonacoLanguage; file_contents_language?: MonacoLanguage; @@ -587,6 +588,7 @@ export function ConfirmUpdate({ previous, content, onConfirm, + loading, disabled, language, file_contents_language, @@ -631,10 +633,11 @@ export function ConfirmUpdate({ } - onClick={() => { - onConfirm(); + onClick={async () => { + await onConfirm(); set(false); }} + loading={loading} /> @@ -749,7 +752,10 @@ export const SystemCommand = ({ /> set({ ...(value || {}), command })} readOnly={disabled} diff --git a/frontend/src/components/resources/build/actions.tsx b/frontend/src/components/resources/build/actions.tsx index aa7e0bbb1..e507719f8 100644 --- a/frontend/src/components/resources/build/actions.tsx +++ b/frontend/src/components/resources/build/actions.tsx @@ -11,7 +11,7 @@ export const RunBuild = ({ id }: { id: string }) => { const building = useRead( "GetBuildActionState", { build: id }, - { refetchInterval: 5000 } + { refetchInterval: 5_000 } ).data?.building; const updates = useRead("ListUpdates", { query: { @@ -22,7 +22,7 @@ export const RunBuild = ({ id }: { id: string }) => { const { mutate: run_mutate, isPending: runPending } = useExecute("RunBuild"); const { mutate: cancel_mutate, isPending: cancelPending } = useExecute("CancelBuild"); - const build = useRead("ListBuilds", {}, { refetchInterval: 5000 }).data?.find( + const build = useRead("ListBuilds", {}).data?.find( (d) => d.id === id ); const builder = useBuilder(build?.info.builder_id); diff --git a/frontend/src/components/resources/build/chart.tsx b/frontend/src/components/resources/build/chart.tsx index 9a45ad483..3c48d5088 100644 --- a/frontend/src/components/resources/build/chart.tsx +++ b/frontend/src/components/resources/build/chart.tsx @@ -1,105 +1,105 @@ -import { - ColorType, - IChartApi, - ISeriesApi, - Time, - createChart, -} from "lightweight-charts"; -import { useEffect, useRef } from "react"; -import { useRead } from "@lib/hooks"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@ui/card"; -import { Hammer } from "lucide-react"; -import { Link } from "react-router-dom"; -import { convertTsMsToLocalUnixTsInSecs } from "@lib/utils"; +// import { +// ColorType, +// IChartApi, +// ISeriesApi, +// Time, +// createChart, +// } from "lightweight-charts"; +// import { useEffect, useRef } from "react"; +// import { useRead } from "@lib/hooks"; +// import { +// Card, +// CardContent, +// CardDescription, +// CardHeader, +// CardTitle, +// } from "@ui/card"; +// import { Hammer } from "lucide-react"; +// import { Link } from "react-router-dom"; +// import { convertTsMsToLocalUnixTsInSecs } from "@lib/utils"; -export const BuildChart = () => { - const container_ref = useRef(null); - const line_ref = useRef(); - const series_ref = useRef>(); - const build_stats = useRead("GetBuildMonthlyStats", {}).data; - const summary = useRead("GetBuildsSummary", {}).data; +// export const BuildChart = () => { +// const container_ref = useRef(null); +// const line_ref = useRef(); +// const series_ref = useRef>(); +// const build_stats = useRead("GetBuildMonthlyStats", {}).data; +// const summary = useRead("GetBuildsSummary", {}).data; - const handleResize = () => - line_ref.current?.applyOptions({ - width: container_ref.current?.clientWidth, - }); +// const handleResize = () => +// line_ref.current?.applyOptions({ +// width: container_ref.current?.clientWidth, +// }); - useEffect(() => { - if (!build_stats) return; - if (line_ref.current) line_ref.current.remove(); - const init = () => { - if (!container_ref.current) return; +// useEffect(() => { +// if (!build_stats) return; +// if (line_ref.current) line_ref.current.remove(); +// const init = () => { +// if (!container_ref.current) return; - // INIT LINE - line_ref.current = createChart(container_ref.current, { - width: container_ref.current.clientWidth, - height: container_ref.current.clientHeight, - layout: { - background: { type: ColorType.Solid, color: "transparent" }, - textColor: "grey", - fontSize: 12, - }, - grid: { - horzLines: { color: "transparent" }, - vertLines: { color: "transparent" }, - }, - handleScale: false, - handleScroll: false, - }); - line_ref.current.timeScale().fitContent(); +// // INIT LINE +// line_ref.current = createChart(container_ref.current, { +// width: container_ref.current.clientWidth, +// height: container_ref.current.clientHeight, +// layout: { +// background: { type: ColorType.Solid, color: "transparent" }, +// textColor: "grey", +// fontSize: 12, +// }, +// grid: { +// horzLines: { color: "transparent" }, +// vertLines: { color: "transparent" }, +// }, +// handleScale: false, +// handleScroll: false, +// }); +// line_ref.current.timeScale().fitContent(); - // INIT SERIES - series_ref.current = line_ref.current.addHistogramSeries({ - priceLineVisible: false, - }); - const max = build_stats.days.reduce((m, c) => Math.max(m, c.time), 0); - series_ref.current.setData( - build_stats.days.map((d) => ({ - time: convertTsMsToLocalUnixTsInSecs(d.ts) as Time, - value: d.count, - color: - d.time > max * 0.7 - ? "darkred" - : d.time > max * 0.35 - ? "darkorange" - : "darkgreen", - })) ?? [] - ); - }; +// // INIT SERIES +// series_ref.current = line_ref.current.addHistogramSeries({ +// priceLineVisible: false, +// }); +// const max = build_stats.days.reduce((m, c) => Math.max(m, c.time), 0); +// series_ref.current.setData( +// build_stats.days.map((d) => ({ +// time: convertTsMsToLocalUnixTsInSecs(d.ts) as Time, +// value: d.count, +// color: +// d.time > max * 0.7 +// ? "darkred" +// : d.time > max * 0.35 +// ? "darkorange" +// : "darkgreen", +// })) ?? [] +// ); +// }; - // Run the effect - init(); - window.addEventListener("resize", handleResize); - return () => { - window.removeEventListener("resize", handleResize); - }; - }, [build_stats]); +// // Run the effect +// init(); +// window.addEventListener("resize", handleResize); +// return () => { +// window.removeEventListener("resize", handleResize); +// }; +// }, [build_stats]); - return ( - - - -
-
- Builds - -
{summary?.total} Total
|{" "} -
{build_stats?.total_time.toFixed(2)} Hours
-
-
- -
-
- -
- - - - ); -}; +// return ( +// +// +// +//
+//
+// Builds +// +//
{summary?.total} Total
|{" "} +//
{build_stats?.total_time.toFixed(2)} Hours
+//
+//
+// +//
+//
+// +//
+// +// +// +// ); +// }; diff --git a/frontend/src/components/resources/build/config.tsx b/frontend/src/components/resources/build/config.tsx index 67398d982..7ab83a0fa 100644 --- a/frontend/src/components/resources/build/config.tsx +++ b/frontend/src/components/resources/build/config.tsx @@ -153,9 +153,10 @@ export const BuildConfig = ({ description: "Select a custom branch, or default to 'main'.", }, commit: { - placeholder: "Enter commit hash", + label: "Commit Hash", + placeholder: "Input commit hash", description: - "Switch to a specific hash after cloning the branch.", + "Optional. Switch to a specific commit hash after cloning the branch.", }, }, }, diff --git a/frontend/src/components/resources/build/index.tsx b/frontend/src/components/resources/build/index.tsx index 915e47a22..640cfdaa7 100644 --- a/frontend/src/components/resources/build/index.tsx +++ b/frontend/src/components/resources/build/index.tsx @@ -27,12 +27,12 @@ import { Button } from "@ui/button"; import { useBuilder } from "../builder"; export const useBuild = (id?: string) => - useRead("ListBuilds", {}, { refetchInterval: 5000 }).data?.find( + useRead("ListBuilds", {}, { refetchInterval: 10_000 }).data?.find( (d) => d.id === id ); export const useFullBuild = (id: string) => - useRead("GetBuild", { build: id }, { refetchInterval: 5000 }).data; + useRead("GetBuild", { build: id }, { refetchInterval: 10_000 }).data; const BuildIcon = ({ id, size }: { id?: string; size: number }) => { const state = useBuild(id)?.info.state; diff --git a/frontend/src/components/resources/builder/index.tsx b/frontend/src/components/resources/builder/index.tsx index ba1a6f739..e2754f564 100644 --- a/frontend/src/components/resources/builder/index.tsx +++ b/frontend/src/components/resources/builder/index.tsx @@ -21,7 +21,7 @@ import { BuilderTable } from "./table"; import { ResourcePageHeader } from "@components/util"; export const useBuilder = (id?: string) => - useRead("ListBuilders", {}, { refetchInterval: 5000 }).data?.find( + useRead("ListBuilders", {}, { refetchInterval: 10_000 }).data?.find( (d) => d.id === id ); diff --git a/frontend/src/components/resources/deployment/actions.tsx b/frontend/src/components/resources/deployment/actions.tsx index 023f562ce..4c4a185a9 100644 --- a/frontend/src/components/resources/deployment/actions.tsx +++ b/frontend/src/components/resources/deployment/actions.tsx @@ -38,17 +38,13 @@ export const DeployDeployment = ({ id }: DeploymentId) => { const { mutate: deploy, isPending } = useExecute("Deploy"); - const deployments = useRead( - "ListDeployments", - {}, - { refetchInterval: 5000 } - ).data; + const deployments = useRead("ListDeployments", {}).data; const deployment_item = deployments?.find((d) => d.id === id); const deploying = useRead( "GetDeploymentActionState", { deployment: id }, - { refetchInterval: 5000 } + { refetchInterval: 5_000 } ).data?.deploying; const pending = isPending || deploying; diff --git a/frontend/src/components/resources/deployment/index.tsx b/frontend/src/components/resources/deployment/index.tsx index bea6fd795..66d4ef66f 100644 --- a/frontend/src/components/resources/deployment/index.tsx +++ b/frontend/src/components/resources/deployment/index.tsx @@ -28,12 +28,12 @@ import { ResourcePageHeader, StatusBadge } from "@components/util"; // const configOrLog = atomWithStorage("config-or-log-v1", "Config"); export const useDeployment = (id?: string) => - useRead("ListDeployments", {}, { refetchInterval: 5000 }).data?.find( + useRead("ListDeployments", {}, { refetchInterval: 10_000 }).data?.find( (d) => d.id === id ); export const useFullDeployment = (id: string) => - useRead("GetDeployment", { deployment: id }, { refetchInterval: 5000 }).data; + useRead("GetDeployment", { deployment: id }, { refetchInterval: 10_000 }).data; const ConfigOrLog = ({ id }: { id: string }) => { // const [view, setView] = useAtom(configOrLog); diff --git a/frontend/src/components/resources/index.tsx b/frontend/src/components/resources/index.tsx index d032ade01..58ef921e2 100644 --- a/frontend/src/components/resources/index.tsx +++ b/frontend/src/components/resources/index.tsx @@ -14,13 +14,13 @@ export const ResourceComponents: { [key in UsableResource]: RequiredResourceComponents; } = { Server: ServerComponents, - Deployment: DeploymentComponents, Stack: StackComponents, + Deployment: DeploymentComponents, Build: BuildComponents, Repo: RepoComponents, Procedure: ProcedureComponents, - Builder: BuilderComponents, - ServerTemplate: ServerTemplateComponents, - Alerter: AlerterComponents, ResourceSync: ResourceSyncComponents, + Builder: BuilderComponents, + Alerter: AlerterComponents, + ServerTemplate: ServerTemplateComponents, }; diff --git a/frontend/src/components/resources/procedure/config.tsx b/frontend/src/components/resources/procedure/config.tsx index 9fcbc563f..e41dc6891 100644 --- a/frontend/src/components/resources/procedure/config.tsx +++ b/frontend/src/components/resources/procedure/config.tsx @@ -42,6 +42,7 @@ import { import { DotsHorizontalIcon } from "@radix-ui/react-icons"; import { filterBySplit } from "@lib/utils"; import { useToast } from "@ui/use-toast"; +import { fmt_upper_camelcase } from "@lib/formatting"; export const ProcedureConfig = ({ id }: { id: string }) => { const procedure = useRead("GetProcedure", { procedure: id }).data; @@ -502,36 +503,6 @@ const default_enabled_execution: () => Types.EnabledExecution = () => ({ }, }); -const EXECUTION_TYPES: Types.Execution["type"][] = [ - "RunBuild", - "Deploy", - "RestartDeployment", - "StartDeployment", - "PauseDeployment", - "UnpauseDeployment", - "StopDeployment", - "DestroyDeployment", - "DeployStack", - "StartStack", - "RestartStack", - "PauseStack", - "UnpauseStack", - "StopStack", - "DestroyStack", - "CloneRepo", - "PullRepo", - "BuildRepo", - "RunProcedure", - "RunSync", - "StopAllContainers", - "PruneContainers", - "PruneNetworks", - "PruneImages", - "PruneVolumes", - "PruneSystem", - "Sleep", -]; - const ExecutionTypeSelector = ({ type, onSelect, @@ -541,15 +512,19 @@ const ExecutionTypeSelector = ({ onSelect: (type: Types.Execution["type"]) => void; disabled: boolean; }) => { + const execution_types = Object.keys(TARGET_COMPONENTS).filter( + (c) => !["None"].includes(c) + ); + const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); - const filtered = filterBySplit(EXECUTION_TYPES, search, (item) => item); + const filtered = filterBySplit(execution_types, search, (item) => item); return ( @@ -573,7 +548,7 @@ const ExecutionTypeSelector = ({ onSelect={() => onSelect(type as Types.Execution["type"])} className="flex items-center justify-between" > -
{type}
+
{fmt_upper_camelcase(type)}
))} @@ -752,6 +727,17 @@ const TARGET_COMPONENTS: ExecutionConfigs = { /> ), }, + DeployStackIfChanged: { + params: { stack: "" }, + Component: ({ params, setParams, disabled }) => ( + setParams({ stack: id })} + disabled={disabled} + /> + ), + }, StartStack: { params: { stack: "" }, Component: ({ params, setParams, disabled }) => ( diff --git a/frontend/src/components/resources/repo/config.tsx b/frontend/src/components/resources/repo/config.tsx index d6100046a..43c517d01 100644 --- a/frontend/src/components/resources/repo/config.tsx +++ b/frontend/src/components/resources/repo/config.tsx @@ -9,11 +9,7 @@ import { import { useInvalidate, useRead, useWrite } from "@lib/hooks"; import { Types } from "@komodo/client"; import { useState } from "react"; -import { - CopyGithubWebhook, - ResourceLink, - ResourceSelector, -} from "../common"; +import { CopyGithubWebhook, ResourceLink, ResourceSelector } from "../common"; import { useToast } from "@ui/use-toast"; import { text_color_class_by_intention } from "@lib/color"; import { ConfirmButton } from "@components/util"; @@ -149,12 +145,20 @@ export const RepoConfig = ({ id }: { id: string }) => { description: "Select a custom branch, or default to 'main'.", }, commit: { - placeholder: "Enter commit hash", + label: "Commit Hash", + placeholder: "Input commit hash", description: - "Switch to a specific hash after cloning the branch.", + "Optional. Switch to a specific commit hash after cloning the branch.", }, + }, + }, + { + label: "Path", + labelHidden: true, + components: { path: { label: "Clone Path", + boldLabel: true, placeholder: "/clone/path/on/host", description: "Explicitly specify the folder on the host to clone the repo in.", diff --git a/frontend/src/components/resources/repo/index.tsx b/frontend/src/components/resources/repo/index.tsx index 663db93a2..975b843a0 100644 --- a/frontend/src/components/resources/repo/index.tsx +++ b/frontend/src/components/resources/repo/index.tsx @@ -27,12 +27,12 @@ import { Button } from "@ui/button"; import { useBuilder } from "../builder"; export const useRepo = (id?: string) => - useRead("ListRepos", {}, { refetchInterval: 5000 }).data?.find( + useRead("ListRepos", {}, { refetchInterval: 10_000 }).data?.find( (d) => d.id === id ); export const useFullRepo = (id: string) => - useRead("GetRepo", { repo: id }, { refetchInterval: 5000 }).data; + useRead("GetRepo", { repo: id }, { refetchInterval: 10_000 }).data; const RepoIcon = ({ id, size }: { id?: string; size: number }) => { const state = useRepo(id)?.info.state; diff --git a/frontend/src/components/resources/resource-sync/config.tsx b/frontend/src/components/resources/resource-sync/config.tsx index 6e0fa5243..55d4f6da2 100644 --- a/frontend/src/components/resources/resource-sync/config.tsx +++ b/frontend/src/components/resources/resource-sync/config.tsx @@ -1,4 +1,4 @@ -import { Config } from "@components/config"; +import { Config, ConfigComponent } from "@components/config"; import { AccountSelectorConfig, ConfigItem, @@ -14,6 +14,26 @@ import { text_color_class_by_intention } from "@lib/color"; import { ConfirmButton, ShowHideButton } from "@components/util"; import { Ban, CirclePlus } from "lucide-react"; import { MonacoEditor } from "@components/monaco"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@ui/select"; + +type SyncMode = "UI Defined" | "Files On Server" | "Git Repo" | undefined; +const SYNC_MODES: SyncMode[] = ["UI Defined", "Files On Server", "Git Repo"]; + +function getSyncMode( + update: Partial, + config: Types.ResourceSyncConfig +): SyncMode { + if (update.files_on_host ?? config.files_on_host) return "Files On Server"; + if (update.repo ?? config.repo) return "Git Repo"; + if (update.file_contents ?? config.file_contents) return "UI Defined"; + return undefined; +} export const ResourceSyncConfig = ({ id, @@ -22,7 +42,11 @@ export const ResourceSyncConfig = ({ id: string; titleOther: ReactNode; }) => { - const [showFile, setShowFile] = useLocalStorage(`sync-${id}-show-file`, true); + const [show, setShow] = useLocalStorage(`sync-${id}-show`, { + file: true, + git: true, + webhooks: true, + }); const perms = useRead("GetPermissionLevel", { target: { type: "ResourceSync", id }, }).data; @@ -36,14 +60,435 @@ export const ResourceSyncConfig = ({ if (!config) return null; const disabled = global_disabled || perms !== Types.PermissionLevel.Write; - const files_on_host = update.files_on_host ?? config.files_on_host; - const file_contents = - update.file_contents ?? config.file_contents ? true : false; - const ui_defined = !files_on_host && file_contents; - const repo_selected = update.repo ?? config.repo ? true : false; + + const mode = getSyncMode(update, config); const managed = update.managed ?? config.managed; - const show_git = !managed && !files_on_host && !ui_defined; + const setMode = (mode: SyncMode) => { + if (mode === "Files On Server") { + set({ ...update, files_on_host: true }); + } else if (mode === "Git Repo") { + set({ + ...update, + files_on_host: false, + repo: update.repo || config.repo || "namespace/repo", + }); + } else if (mode === "UI Defined") { + set({ + ...update, + files_on_host: false, + repo: "", + file_contents: + update.file_contents || + config.file_contents || + "# Initialize the sync to import your current resources.\n", + }); + } else if (mode === undefined) { + set({ + ...update, + files_on_host: false, + repo: "", + file_contents: "", + }); + } + }; + + let components: Record< + string, + false | ConfigComponent[] | undefined + > = {}; + + const choose_mode: ConfigComponent = { + label: "Choose Mode", + labelHidden: true, + components: { + file_contents: () => { + return ( + + + + ); + }, + }, + }; + + const match_tags: ConfigComponent = { + label: "Match Tags", + components: { + match_tags: (values, set) => ( + + ), + }, + }; + + if (mode === undefined) { + components = { + "": [choose_mode], + }; + } else if (mode === "Files On Server") { + components = { + "": [ + { + label: "General", + components: { + resource_path: (values, set) => ( + + ), + delete: !managed && { + label: "Delete Unmatched Resources", + description: + "Executions will delete any resources not found in the resource files. Only use this when using one sync for everything.", + }, + managed: { + label: "Managed", + description: + "Enabled managed mode / the 'Commit' button. Commit is the 'reverse' of Execute, and will update the sync file with your configs updated in the UI.", + }, + }, + }, + match_tags, + ], + }; + } else if (mode === "Git Repo") { + components = { + "": [ + { + label: "Source", + contentHidden: !show.git, + actions: ( + setShow({ ...show, git })} + /> + ), + components: { + git_provider: (provider: string | undefined, set) => { + const https = update.git_https ?? config.git_https; + return ( + set({ git_provider })} + https={https} + onHttpsSwitch={() => set({ git_https: !https })} + /> + ); + }, + git_account: (value: string | undefined, set) => { + return ( + set({ git_account })} + disabled={disabled} + placeholder="None" + /> + ); + }, + repo: { + placeholder: "Enter repo", + description: + "The repo path on the provider. {namespace}/{repo_name}", + }, + branch: { + placeholder: "Enter branch", + description: "Select a custom branch, or default to 'main'.", + }, + commit: { + label: "Commit Hash", + placeholder: "Input commit hash", + description: + "Optional. Switch to a specific commit hash after cloning the branch.", + }, + }, + }, + { + label: "General", + components: { + resource_path: (values, set) => ( + + ), + delete: !managed && { + label: "Delete Unmatched Resources", + description: + "Executions will delete any resources not found in the resource files. Only use this when using one sync for everything.", + }, + managed: { + label: "Managed", + description: + "Enabled managed mode / the 'Commit' button. Commit is the 'reverse' of Execute, and will update the sync file with your configs updated in the UI.", + }, + }, + }, + match_tags, + { + label: "Git Webhooks", + description: + "Configure your repo provider to send webhooks to Komodo", + contentHidden: !show.webhooks, + actions: ( + setShow({ ...show, webhooks })} + /> + ), + components: { + ["Guard" as any]: () => { + if (update.branch ?? config.branch) { + return null; + } + return ( + +
Must configure Branch before webhooks will work.
+
+ ); + }, + ["refresh" as any]: () => ( + + + + ), + ["sync" as any]: () => ( + + + + ), + webhook_enabled: webhooks !== undefined && !webhooks.managed, + webhook_secret: { + description: + "Provide a custom webhook secret for this resource, or use the global default.", + placeholder: "Input custom secret", + }, + ["managed" as any]: () => { + const inv = useInvalidate(); + const { toast } = useToast(); + const { mutate: createWebhook, isPending: createPending } = + useWrite("CreateSyncWebhook", { + onSuccess: () => { + toast({ title: "Webhook Created" }); + inv(["GetSyncWebhooksEnabled", { sync: id }]); + }, + }); + const { mutate: deleteWebhook, isPending: deletePending } = + useWrite("DeleteSyncWebhook", { + onSuccess: () => { + toast({ title: "Webhook Deleted" }); + inv(["GetSyncWebhooksEnabled", { sync: id }]); + }, + }); + if (!webhooks || !webhooks.managed) return; + return ( + + {webhooks.sync_enabled && ( +
+
+ Incoming webhook is{" "} +
+ ENABLED +
+ and will trigger +
+ SYNC EXECUTION +
+
+ } + variant="destructive" + onClick={() => + deleteWebhook({ + sync: id, + action: Types.SyncWebhookAction.Sync, + }) + } + loading={deletePending} + disabled={disabled || deletePending} + /> +
+ )} + {!webhooks.sync_enabled && webhooks.refresh_enabled && ( +
+
+ Incoming webhook is{" "} +
+ ENABLED +
+ and will trigger +
+ PENDING REFRESH +
+
+ } + variant="destructive" + onClick={() => + deleteWebhook({ + sync: id, + action: Types.SyncWebhookAction.Refresh, + }) + } + loading={deletePending} + disabled={disabled || deletePending} + /> +
+ )} + {!webhooks.sync_enabled && !webhooks.refresh_enabled && ( +
+
+ Incoming webhook is{" "} +
+ DISABLED +
+
+ } + onClick={() => + createWebhook({ + sync: id, + action: Types.SyncWebhookAction.Refresh, + }) + } + loading={createPending} + disabled={disabled || createPending} + /> + } + onClick={() => + createWebhook({ + sync: id, + action: Types.SyncWebhookAction.Sync, + }) + } + loading={createPending} + disabled={disabled || createPending} + /> +
+ )} +
+ ); + }, + }, + }, + ], + }; + } else if (mode === "UI Defined") { + components = { + "": [ + { + label: "Resource File", + description: + "Manage the resource file contents here, or use a git repo / the files on host option.", + actions: ( + setShow((show) => ({ ...show, file }))} + /> + ), + contentHidden: !show.file, + components: { + file_contents: (file_contents, set) => { + return ( + set({ file_contents })} + language="toml" + /> + ); + }, + }, + }, + { + label: "General", + components: { + delete: !managed && { + label: "Delete Unmatched Resources", + description: + "Executions will delete any resources not found in the resource files. Only use this when using one sync for everything.", + }, + managed: { + label: "Managed", + description: + "Enabled managed mode / the 'Commit' button. Commit is the 'reverse' of Execute, and will update the sync file with your configs updated in the UI.", + }, + }, + }, + match_tags, + ], + }; + } return ( { await mutateAsync({ id, config: update }); }} + components={components} file_contents_language="toml" - components={{ - "": [ - { - label: "Resource File", - hidden: files_on_host || repo_selected, - description: - "Manage the resource file contents here, or use a git repo / the files on host option.", - actions: , - contentHidden: !showFile, - components: { - file_contents: (file_contents, set) => { - return ( - set({ file_contents })} - language="toml" - /> - ); - }, - }, - }, - { - label: "General", - components: { - files_on_host: { - label: "Files on Server", - description: - "Manage the sync files on server yourself. Just configure the path to your folder / file.", - }, - delete: !managed && { - label: "Delete Unmatched Resources", - description: - "Executions will delete any resources not found in the resource files. Only use this when using one sync for everything.", - }, - resource_path: (files_on_host || - (!ui_defined && repo_selected)) && { - placeholder: "./resources", - description: - "Provide the path to resource file / folder, in the container filesystem or from the root of the repo", - }, - managed: (managed || - files_on_host || - ui_defined || - !repo_selected) && { - label: "Managed", - description: - "Enabled managed mode / the 'Commit' button. Commit is the 'reverse' of Execute, and will update the sync file with your configs updated in the UI.", - }, - }, - }, - { - label: "Match Tags", - components: { - match_tags: (values, set) => ( - - ), - }, - }, - { - label: "Source", - hidden: !show_git, - components: { - git_provider: (provider: string | undefined, set) => { - const https = update.git_https ?? config.git_https; - return ( - set({ git_provider })} - https={https} - onHttpsSwitch={() => set({ git_https: !https })} - /> - ); - }, - git_account: (value: string | undefined, set) => { - return ( - set({ git_account })} - disabled={disabled} - placeholder="None" - /> - ); - }, - repo: { - placeholder: "Enter repo", - description: - "The repo path on the provider. {namespace}/{repo_name}", - }, - branch: { - placeholder: "Enter branch", - description: "Select a custom branch, or default to 'main'.", - }, - commit: { - placeholder: "Enter a specific commit hash. Optional.", - description: - "Switch to a specific hash after cloning the branch.", - }, - }, - }, - { - label: "Git Webhooks", - hidden: !show_git, - description: - "Configure your repo provider to send webhooks to Komodo", - components: { - ["Guard" as any]: () => { - if (update.branch ?? config.branch) { - return null; - } - return ( - -
Must configure Branch before webhooks will work.
-
- ); - }, - ["refresh" as any]: () => ( - - - - ), - ["sync" as any]: () => ( - - - - ), - webhook_enabled: webhooks !== undefined && !webhooks.managed, - webhook_secret: { - description: - "Provide a custom webhook secret for this resource, or use the global default.", - placeholder: "Input custom secret", - }, - ["managed" as any]: () => { - const inv = useInvalidate(); - const { toast } = useToast(); - const { mutate: createWebhook, isPending: createPending } = - useWrite("CreateSyncWebhook", { - onSuccess: () => { - toast({ title: "Webhook Created" }); - inv(["GetSyncWebhooksEnabled", { sync: id }]); - }, - }); - const { mutate: deleteWebhook, isPending: deletePending } = - useWrite("DeleteSyncWebhook", { - onSuccess: () => { - toast({ title: "Webhook Deleted" }); - inv(["GetSyncWebhooksEnabled", { sync: id }]); - }, - }); - if (!webhooks || !webhooks.managed) return; - return ( - - {webhooks.sync_enabled && ( -
-
- Incoming webhook is{" "} -
- ENABLED -
- and will trigger -
- SYNC EXECUTION -
-
- } - variant="destructive" - onClick={() => - deleteWebhook({ - sync: id, - action: Types.SyncWebhookAction.Sync, - }) - } - loading={deletePending} - disabled={disabled || deletePending} - /> -
- )} - {!webhooks.sync_enabled && webhooks.refresh_enabled && ( -
-
- Incoming webhook is{" "} -
- ENABLED -
- and will trigger -
- PENDING REFRESH -
-
- } - variant="destructive" - onClick={() => - deleteWebhook({ - sync: id, - action: Types.SyncWebhookAction.Refresh, - }) - } - loading={deletePending} - disabled={disabled || deletePending} - /> -
- )} - {!webhooks.sync_enabled && !webhooks.refresh_enabled && ( -
-
- Incoming webhook is{" "} -
- DISABLED -
-
- } - onClick={() => - createWebhook({ - sync: id, - action: Types.SyncWebhookAction.Refresh, - }) - } - loading={createPending} - disabled={disabled || createPending} - /> - } - onClick={() => - createWebhook({ - sync: id, - action: Types.SyncWebhookAction.Sync, - }) - } - loading={createPending} - disabled={disabled || createPending} - /> -
- )} -
- ); - }, - }, - }, - ], - }} /> ); }; diff --git a/frontend/src/components/resources/resource-sync/index.tsx b/frontend/src/components/resources/resource-sync/index.tsx index c2aa5971f..6fbb0e592 100644 --- a/frontend/src/components/resources/resource-sync/index.tsx +++ b/frontend/src/components/resources/resource-sync/index.tsx @@ -23,12 +23,12 @@ import { ResourceSyncPending } from "./pending"; import { Badge } from "@ui/badge"; export const useResourceSync = (id?: string) => - useRead("ListResourceSyncs", {}, { refetchInterval: 5000 }).data?.find( + useRead("ListResourceSyncs", {}, { refetchInterval: 10_000 }).data?.find( (d) => d.id === id ); export const useFullResourceSync = (id: string) => - useRead("GetResourceSync", { sync: id }, { refetchInterval: 5000 }).data; + useRead("GetResourceSync", { sync: id }, { refetchInterval: 10_000 }).data; const ResourceSyncIcon = ({ id, size }: { id?: string; size: number }) => { const state = useResourceSync(id)?.info.state; @@ -55,12 +55,14 @@ const ConfigInfoPending = ({ id }: { id: string }) => { sync && (!sync_no_changes(sync) || sync.info?.pending_error); const view = - (_view === "Info" && hideInfo) || (_view === "Pending" && !showPending) + _view === "Info" && hideInfo ? "Config" + : _view === "Pending" && !showPending + ? sync?.config?.files_on_host || sync?.config?.repo + ? "Info" + : "Config" : _view; - // useEffect(() => setView(view), [view]); - const title = ( diff --git a/frontend/src/components/resources/resource-sync/info.tsx b/frontend/src/components/resources/resource-sync/info.tsx index f9b7d7dbf..92c67a2c9 100644 --- a/frontend/src/components/resources/resource-sync/info.tsx +++ b/frontend/src/components/resources/resource-sync/info.tsx @@ -1,9 +1,16 @@ import { Section } from "@components/layouts"; -import { ReactNode } from "react"; -import { Card, CardHeader } from "@ui/card"; +import { ReactNode, useState } from "react"; +import { Card, CardContent, CardHeader } from "@ui/card"; import { useFullResourceSync } from "."; import { updateLogToHtml } from "@lib/utils"; import { MonacoEditor } from "@components/monaco"; +import { useEditPermissions } from "@pages/resource"; +import { useWrite } from "@lib/hooks"; +import { useToast } from "@ui/use-toast"; +import { Button } from "@ui/button"; +import { FilePlus, History } from "lucide-react"; +import { ConfirmUpdate } from "@components/config/util"; +import { ConfirmButton } from "@components/util"; export const ResourceSyncInfo = ({ id, @@ -12,45 +19,145 @@ export const ResourceSyncInfo = ({ id: string; titleOther: ReactNode; }) => { + const [edits, setEdits] = useState>({}); + const { canWrite } = useEditPermissions({ type: "ResourceSync", id }); + const { toast } = useToast(); + const { mutateAsync, isPending } = useWrite("WriteSyncFileContents", { + onSuccess: (res) => { + toast({ + title: res.success ? "Contents written." : "Failed to write contents.", + variant: res.success ? undefined : "destructive", + }); + }, + }); const sync = useFullResourceSync(id); + const file_on_host = sync?.config?.files_on_host ?? false; + const git_repo = sync?.config?.repo ? true : false; + const canEdit = canWrite && (file_on_host || git_repo); + const editFileCallback = (path: string) => (contents: string) => + setEdits({ ...edits, [path]: contents }); + + const latest_contents = sync?.info?.remote_contents; + const latest_errors = sync?.info?.remote_errors; + return (
- {sync?.info?.remote_errors && sync?.info?.remote_errors.length > 0 && ( - - - remote errors:{" "} - {sync?.info?.remote_errors?.map((content, i) => ( -
-                path: {content.path}
-                
 0 &&
+        latest_errors.map((error) => (
+          
+            
+              
+ {error.resource_path && ( + <> +
+
Folder:
+ {error.resource_path} +
+
|
+ + )} +
+
Path:
+ {error.path} +
+
+ {canEdit && ( + } + onClick={() => { + if (sync) { + mutateAsync({ + sync: sync.name, + resource_path: error.resource_path ?? "", + file_path: error.path, + contents: "## Add resources to get started\n", + }); + } }} - className="max-h-[500px] overflow-y-auto" + loading={isPending} /> -
- ))} - - - )} - {sync?.info?.remote_contents && - sync?.info?.remote_contents.length > 0 && ( - - - latest contents:{" "} - {sync?.info?.remote_contents?.map((content, i) => ( -
-                  path: {content.path}
-                  
-                
- ))} + )}
+ +
+            
           
-        )}
+        ))}
+
+      {/* Update latest contents */}
+      {latest_contents &&
+        latest_contents.length > 0 &&
+        latest_contents.map((content) => (
+          
+            
+              
+ {content.resource_path && ( + <> +
+
Folder:
+ {content.resource_path} +
+
|
+ + )} +
+
File:
+ {content.path} +
+
+ {canEdit && ( +
+ + { + if (sync) { + return await mutateAsync({ + sync: sync.name, + resource_path: content.resource_path ?? "", + file_path: content.path, + contents: edits[content.path]!, + }).then(() => + setEdits({ ...edits, [content.path]: undefined }) + ); + } + }} + disabled={!edits[content.path]} + language="toml" + loading={isPending} + /> +
+ )} +
+ + + +
+ ))}
); }; diff --git a/frontend/src/components/resources/resource-sync/pending.tsx b/frontend/src/components/resources/resource-sync/pending.tsx index 572ad2e31..9e186780d 100644 --- a/frontend/src/components/resources/resource-sync/pending.tsx +++ b/frontend/src/components/resources/resource-sync/pending.tsx @@ -1,12 +1,16 @@ import { Section } from "@components/layouts"; import { MonacoDiffEditor, MonacoEditor } from "@components/monaco"; -import { useRead } from "@lib/hooks"; +import { useExecute, useRead } from "@lib/hooks"; import { Card, CardContent, CardHeader } from "@ui/card"; import { ReactNode } from "react"; import { ResourceLink } from "../common"; import { UsableResource } from "@types"; import { diff_type_intention, text_color_class_by_intention } from "@lib/color"; import { cn, sanitizeOnlySpan } from "@lib/utils"; +import { ConfirmButton } from "@components/util"; +import { SquarePlay } from "lucide-react"; +import { useEditPermissions } from "@pages/resource"; +import { useFullResourceSync } from "."; export const ResourceSyncPending = ({ id, @@ -15,11 +19,12 @@ export const ResourceSyncPending = ({ id: string; titleOther: ReactNode; }) => { - const sync = useRead( - "GetResourceSync", - { sync: id }, - { refetchInterval: 5000 } - ).data; + const syncing = useRead("GetResourceSyncActionState", { sync: id }).data + ?.syncing; + const sync = useFullResourceSync(id); + const { canExecute } = useEditPermissions({ type: "ResourceSync", id }); + const { mutate, isPending } = useExecute("RunSync"); + const loading = isPending || syncing; return (
{/* Pending Error */} @@ -42,6 +47,7 @@ export const ResourceSyncPending = ({ ) : undefined} + {/* Pending Deploy */} {sync?.info?.pending_deploy?.to_deploy ? ( @@ -63,11 +69,12 @@ export const ResourceSyncPending = ({ ) : undefined} + {/* Pending Resource Update */} {sync?.info?.resource_updates?.map((update) => { return ( - +
|
- + {update.data.type === "Create" ? ( +
{update.data.data.name}
+ ) : ( + + )}
+ {canExecute && ( + } + onClick={() => + mutate({ + sync: id, + resource_type: update.target.type, + resources: [ + update.data.type === "Create" + ? update.data.data.name! + : update.target.id, + ], + }) + } + loading={loading} + /> + )} {update.data.type === "Create" && ( diff --git a/frontend/src/components/resources/server/hooks.ts b/frontend/src/components/resources/server/hooks.ts index 76da759c1..9b6434c1e 100644 --- a/frontend/src/components/resources/server/hooks.ts +++ b/frontend/src/components/resources/server/hooks.ts @@ -2,7 +2,7 @@ import { atomWithStorage } from "@lib/hooks"; import { Types } from "@komodo/client"; import { useAtom } from "jotai"; -const statsGranularityAtom = atomWithStorage( +const statsGranularityAtom = atomWithStorage( "stats-granularity-v0", Types.Timelength.FiveMinutes ); diff --git a/frontend/src/components/resources/server/index.tsx b/frontend/src/components/resources/server/index.tsx index 18f052475..2af28e959 100644 --- a/frontend/src/components/resources/server/index.tsx +++ b/frontend/src/components/resources/server/index.tsx @@ -38,12 +38,12 @@ import { ServerInfo } from "./info"; import { ServerStats } from "./stats"; export const useServer = (id?: string) => - useRead("ListServers", {}, { refetchInterval: 5000 }).data?.find( + useRead("ListServers", {}, { refetchInterval: 10_000 }).data?.find( (d) => d.id === id ); export const useFullServer = (id: string) => - useRead("GetServer", { server: id }, { refetchInterval: 5000 }).data; + useRead("GetServer", { server: id }, { refetchInterval: 10_000 }).data; const Icon = ({ id, size }: { id?: string; size: number }) => { const state = useServer(id)?.info.state; diff --git a/frontend/src/components/resources/server/info/containers.tsx b/frontend/src/components/resources/server/info/containers.tsx index 0f9b776a4..924fb22a8 100644 --- a/frontend/src/components/resources/server/info/containers.tsx +++ b/frontend/src/components/resources/server/info/containers.tsx @@ -11,7 +11,7 @@ export const Containers = ({ setShow: (show: boolean) => void; }) => { const containers = - useRead("ListDockerContainers", { server: id }, { refetchInterval: 5000 }) + useRead("ListDockerContainers", { server: id }, { refetchInterval: 10_000 }) .data ?? []; return ( void; }) => { const images = - useRead("ListDockerImages", { server: id }, { refetchInterval: 5000 }) + useRead("ListDockerImages", { server: id }, { refetchInterval: 10_000 }) .data ?? []; const allInUse = images.every((image) => image.in_use); diff --git a/frontend/src/components/resources/server/info/networks.tsx b/frontend/src/components/resources/server/info/networks.tsx index e0a3f4354..e430f0484 100644 --- a/frontend/src/components/resources/server/info/networks.tsx +++ b/frontend/src/components/resources/server/info/networks.tsx @@ -16,7 +16,7 @@ export const Networks = ({ setShow: (show: boolean) => void; }) => { const networks = - useRead("ListDockerNetworks", { server: id }, { refetchInterval: 5000 }) + useRead("ListDockerNetworks", { server: id }, { refetchInterval: 10_000 }) .data ?? []; const allInUse = networks.every((network) => diff --git a/frontend/src/components/resources/server/info/volumes.tsx b/frontend/src/components/resources/server/info/volumes.tsx index 2d2d20395..c0154eb34 100644 --- a/frontend/src/components/resources/server/info/volumes.tsx +++ b/frontend/src/components/resources/server/info/volumes.tsx @@ -16,7 +16,7 @@ export const Volumes = ({ setShow: (show: boolean) => void; }) => { const volumes = - useRead("ListDockerVolumes", { server: id }, { refetchInterval: 5000 }) + useRead("ListDockerVolumes", { server: id }, { refetchInterval: 10_000 }) .data ?? []; const allInUse = volumes.every((volume) => volume.in_use); diff --git a/frontend/src/components/resources/server/stat-chart.tsx b/frontend/src/components/resources/server/stat-chart.tsx index 386ad3e9f..74e3ea765 100644 --- a/frontend/src/components/resources/server/stat-chart.tsx +++ b/frontend/src/components/resources/server/stat-chart.tsx @@ -1,20 +1,18 @@ import { hex_color_by_intention } from "@lib/color"; import { useRead } from "@lib/hooks"; -import { convertTsMsToLocalUnixTsInSecs } from "@lib/utils"; import { Types } from "@komodo/client"; -import { - ColorType, - IChartApi, - ISeriesApi, - Time, - createChart, -} from "lightweight-charts"; -import { useEffect, useRef } from "react"; +import { useMemo } from "react"; import { useStatsGranularity } from "./hooks"; import { Loader2 } from "lucide-react"; +import { AxisOptions, Chart } from "react-charts"; +import { convertTsMsToLocalUnixTsInMs } from "@lib/utils"; +import { useTheme } from "@ui/theme"; +import { fmt_utc_date } from "@lib/formatting"; type StatType = "cpu" | "mem" | "disk"; +type StatDatapoint = { date: number; value: number }; + export const StatChart = ({ server_id, type, @@ -31,15 +29,19 @@ export const StatChart = ({ granularity, }); - const stats = data?.stats - .map((stat) => { - return { - time: convertTsMsToLocalUnixTsInSecs(stat.ts) as Time, - value: getStat(stat, type), - }; - }) - .reverse(); - + const stats = useMemo( + () => + data?.stats + .map((stat) => { + return { + date: convertTsMsToLocalUnixTsInMs(stat.ts), + value: getStat(stat, type), + }; + }) + .reverse(), + [data] + ); + return (
{isPending ? ( @@ -47,7 +49,8 @@ export const StatChart = ({
) : ( - + stats && + stats.length > 0 && )}
); @@ -58,69 +61,134 @@ export const InnerStatChart = ({ stats, }: { type: StatType; - stats: - | { - time: Time; - value: number; - }[] - | undefined; + stats: StatDatapoint[] | undefined; }) => { - const container_ref = useRef(null); - const line_ref = useRef(); - const series_ref = useRef>(); - const lineColor = getColor(type); - - const handleResize = () => - line_ref.current?.applyOptions({ - width: container_ref.current?.clientWidth, - }); - - useEffect(() => { - if (!stats) return; - if (line_ref.current) line_ref.current.remove(); - - const init = () => { - if (!container_ref.current) return; - - // INIT LINE - line_ref.current = createChart(container_ref.current, { - width: container_ref.current.clientWidth, - height: container_ref.current.clientHeight, - layout: { - background: { type: ColorType.Solid, color: "transparent" }, - // textColor: "grey", - fontSize: 12, - }, - grid: { - horzLines: { color: "#3f454d25" }, - vertLines: { color: "#3f454d25" }, - }, - timeScale: { timeVisible: true }, - handleScale: false, - handleScroll: false, - }); - line_ref.current.timeScale().fitContent(); - - // INIT SERIES - series_ref.current = line_ref.current.addAreaSeries({ - priceLineVisible: false, - title: `${type} %`, - lineColor, - topColor: `${lineColor}B3`, - bottomColor: `${lineColor}0D`, - }); - series_ref.current.setData(stats); + const { theme: _theme } = useTheme(); + const theme = + _theme === "system" + ? window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light" + : _theme; + const min = stats?.[0]?.date ?? 0; + const max = stats?.[stats.length - 1]?.date ?? 0; + const diff = max - min; + const timeAxis = useMemo((): AxisOptions => { + return { + getValue: (datum) => new Date(datum.date), + hardMax: new Date(max + diff * 0.02), + hardMin: new Date(min - diff * 0.02), + tickCount: 6, + formatters: { + // scale: (value?: Date) => fmt_date(value ?? new Date()), + tooltip: (value?: Date) => ( +
+ {fmt_utc_date(value ?? new Date())} +
+ ), + cursor: (_value?: Date) => false, + }, }; + }, []); + const valueAxis = useMemo( + (): AxisOptions[] => [ + { + getValue: (datum) => datum.value, + elementType: "area", + min: 0, + max: 100, + formatters: { + tooltip: (value?: number) => ( +
+ {(value ?? 0) >= 10 ? value?.toFixed(2) : "0" + value?.toFixed(2)} + % +
+ ), + }, + }, + ], + [] + ); + return ( + false, + // }, + }} + /> + ); - // Run the effect - init(); - window.addEventListener("resize", handleResize); - return () => { - window.removeEventListener("resize", handleResize); - }; - }, [stats]); + // const container_ref = useRef(null); + // const line_ref = useRef(); + // const series_ref = useRef>(); + // const lineColor = getColor(type); - return
; + // const handleResize = () => + // line_ref.current?.applyOptions({ + // width: container_ref.current?.clientWidth, + // }); + + // useEffect(() => { + // if (!stats) return; + // if (line_ref.current) line_ref.current.remove(); + + // const init = () => { + // if (!container_ref.current) return; + + // // INIT LINE + // line_ref.current = createChart(container_ref.current, { + // width: container_ref.current.clientWidth, + // height: container_ref.current.clientHeight, + // layout: { + // background: { type: ColorType.Solid, color: "transparent" }, + // // textColor: "grey", + // fontSize: 12, + // }, + // grid: { + // horzLines: { color: "#3f454d25" }, + // vertLines: { color: "#3f454d25" }, + // }, + // timeScale: { timeVisible: true }, + // handleScale: false, + // handleScroll: false, + // }); + // line_ref.current.timeScale().fitContent(); + + // // INIT SERIES + // series_ref.current = line_ref.current.addAreaSeries({ + // priceLineVisible: false, + // title: `${type} %`, + // lineColor, + // topColor: `${lineColor}B3`, + // bottomColor: `${lineColor}0D`, + // }); + // series_ref.current.setData(stats); + // }; + + // // Run the effect + // init(); + // window.addEventListener("resize", handleResize); + // return () => { + // window.removeEventListener("resize", handleResize); + // }; + // }, [stats]); + + // return
; }; const getStat = (stat: Types.SystemStatsRecord, type: StatType) => { diff --git a/frontend/src/components/resources/server/stats.tsx b/frontend/src/components/resources/server/stats.tsx index af196a366..a776f2dff 100644 --- a/frontend/src/components/resources/server/stats.tsx +++ b/frontend/src/components/resources/server/stats.tsx @@ -7,11 +7,11 @@ import { CardTitle, } from "@ui/card"; import { Progress } from "@ui/progress"; -import { Cpu, Database, MemoryStick } from "lucide-react"; +import { Cpu, Database, Loader2, MemoryStick } from "lucide-react"; import { useRead } from "@lib/hooks"; import { Types } from "@komodo/client"; import { DataTable, SortableHeader } from "@ui/data-table"; -import { ReactNode, useState } from "react"; +import { ReactNode, useMemo, useState } from "react"; import { Input } from "@ui/input"; import { StatChart } from "./stat-chart"; import { useStatsGranularity } from "./hooks"; @@ -22,6 +22,7 @@ import { SelectTrigger, SelectValue, } from "@ui/select"; +import { ShowHideButton } from "@components/util"; export const ServerStats = ({ id, @@ -34,7 +35,7 @@ export const ServerStats = ({ const stats = useRead( "GetSystemStats", { server: id }, - { refetchInterval: 5000 } + { refetchInterval: 10_000 } ).data; const info = useRead("GetSystemInformation", { server: id }).data; @@ -49,174 +50,178 @@ export const ServerStats = ({ return (
- {/* Stats */} -
- - `${core_count} Core${(core_count || 0) > 1 ? "s" : ""}`, - }, - { - header: "Total Memory", - accessorFn: ({ mem_total }) => `${mem_total?.toFixed(2)} GB`, - }, - { - header: "Total Disk Size", - accessorFn: ({ disk_total }) => `${disk_total?.toFixed(2)} GB`, - }, - ]} - /> -
+
+ {/* Stats */} +
+ + `${core_count} Core${(core_count || 0) > 1 ? "s" : ""}`, + }, + { + header: "Total Memory", + accessorFn: ({ mem_total }) => `${mem_total?.toFixed(2)} GB`, + }, + { + header: "Total Disk Size", + accessorFn: ({ disk_total }) => `${disk_total?.toFixed(2)} GB`, + }, + ]} + /> +
-
-
- - - -
-
- -
-
Interval:
- +
+
+ + +
- } - > -
- - - -
-
+
-
+
-
Used:
- {disk_used?.toFixed(2)} GB +
Interval:
+
-
-
Total:
- {disk_total?.toFixed(2)} GB -
-
- } - > - ({ - ...disk, - percentage: 100 * (disk.used_gb / disk.total_gb), - })) ?? [] } - columns={[ - { - header: "Path", - cell: ({ row }) => ( -
- {row.original.mount} -
- ), - }, - { - accessorKey: "used_gb", - header: ({ column }) => ( - - ), - cell: ({ row }) => <>{row.original.used_gb.toFixed(2)} GB, - }, - { - accessorKey: "total_gb", - header: ({ column }) => ( - - ), - cell: ({ row }) => <>{row.original.total_gb.toFixed(2)} GB, - }, - { - accessorKey: "percentage", - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - <>{row.original.percentage.toFixed(2)}% Full - ), - }, - ]} - /> -
+ > +
+ + + +
+ - +
+
+
Used:
+ {disk_used?.toFixed(2)} GB +
+
+
Total:
+ {disk_total?.toFixed(2)} GB +
+
+ } + > + ({ + ...disk, + percentage: 100 * (disk.used_gb / disk.total_gb), + })) ?? [] + } + columns={[ + { + header: "Path", + cell: ({ row }) => ( +
+ {row.original.mount} +
+ ), + }, + { + accessorKey: "used_gb", + header: ({ column }) => ( + + ), + cell: ({ row }) => <>{row.original.used_gb.toFixed(2)} GB, + }, + { + accessorKey: "total_gb", + header: ({ column }) => ( + + ), + cell: ({ row }) => <>{row.original.total_gb.toFixed(2)} GB, + }, + { + accessorKey: "percentage", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + <>{row.original.percentage.toFixed(2)}% Full + ), + }, + ]} + /> + + + +
); }; const Processes = ({ id }: { id: string }) => { + const [show, setShow] = useState(false); const [search, setSearch] = useState(""); - const searchSplit = search.split(" "); - - const { data: processes } = useRead("ListSystemProcesses", { server: id }); - if (!processes || processes.length === 0) return; - + const searchSplit = search.toLowerCase().split(" "); return (
} actions={ { /> } > - - searchSplit.every((search) => process.name.includes(search)) - )} - columns={[ - { - header: "Name", - accessorKey: "name", - }, - { - header: "Exe", - accessorKey: "exe", - cell: ({ row }) => ( -
- {row.original.exe} -
- ), - }, - { - accessorKey: "cpu_perc", - header: ({ column }) => ( - - ), - cell: ({ row }) => <>{row.original.cpu_perc.toFixed(2)}%, - }, - { - accessorKey: "mem_mb", - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - <> - {row.original.mem_mb > 1000 - ? `${(row.original.mem_mb / 1024).toFixed(2)} GB` - : `${row.original.mem_mb.toFixed(2)} MB`} - - ), - }, - ]} - /> + {show && }
); }; +const ProcessesInner = ({ + id, + searchSplit, +}: { + id: string; + searchSplit: string[]; +}) => { + const { data: processes, isPending } = useRead("ListSystemProcesses", { + server: id, + }); + const filtered = useMemo( + () => + processes?.filter((process) => { + if (searchSplit.length === 0) return true; + const name = process.name.toLowerCase(); + return searchSplit.every((search) => name.includes(search)); + }), + [processes, searchSplit] + ); + if (isPending) + return ( +
+ +
+ ); + if (!processes) return null; + return ( + ( +
+ {row.original.exe} +
+ ), + }, + { + accessorKey: "cpu_perc", + header: ({ column }) => ( + + ), + cell: ({ row }) => <>{row.original.cpu_perc.toFixed(2)}%, + }, + { + accessorKey: "mem_mb", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + <> + {row.original.mem_mb > 1000 + ? `${(row.original.mem_mb / 1024).toFixed(2)} GB` + : `${row.original.mem_mb.toFixed(2)} MB`} + + ), + }, + ]} + /> + ); +}; + const CPU = ({ stats }: { stats: Types.SystemStats | undefined }) => { const perc = stats?.cpu_perc; diff --git a/frontend/src/components/resources/stack/config.tsx b/frontend/src/components/resources/stack/config.tsx index 11860fd14..03385865d 100644 --- a/frontend/src/components/resources/stack/config.tsx +++ b/frontend/src/components/resources/stack/config.tsx @@ -198,7 +198,7 @@ export const StackConfig = ({ ), env_file_path: { description: - "The path to write the file to, relative to the root of the repo.", + "The path to write the file to, relative to the run directory", placeholder: ".env", }, // skip_secret_interp: true, @@ -489,9 +489,10 @@ export const StackConfig = ({ description: "Select a custom branch, or default to 'main'.", }, commit: { - placeholder: "Enter a specific commit hash. Optional.", + label: "Commit Hash", + placeholder: "Input commit hash", description: - "Switch to a specific hash after cloning the branch.", + "Optional. Switch to a specific commit hash after cloning the branch.", }, reclone: { description: @@ -561,6 +562,10 @@ export const StackConfig = ({ !!(update.branch ?? config.branch) && webhooks !== undefined && !webhooks.managed, + webhook_force_deploy: { + description: + "Usually the Stack won't deploy unless there are changes to the files. Use this to force deploy.", + }, webhook_secret: { description: "Provide a custom webhook secret for this resource, or use the global default.", @@ -753,7 +758,7 @@ export const StackConfig = ({ ); }; -const DEFAULT_STACK_FILE_CONTENTS = `## 🦎 Hello Komodo 🦎 +export const DEFAULT_STACK_FILE_CONTENTS = `## 🦎 Hello Komodo 🦎 services: hello_world: image: hello-world diff --git a/frontend/src/components/resources/stack/index.tsx b/frontend/src/components/resources/stack/index.tsx index fe241929d..ddeaf43e3 100644 --- a/frontend/src/components/resources/stack/index.tsx +++ b/frontend/src/components/resources/stack/index.tsx @@ -39,12 +39,12 @@ import { ResourcePageHeader, StatusBadge } from "@components/util"; import { StackConfig } from "./config"; export const useStack = (id?: string) => - useRead("ListStacks", {}, { refetchInterval: 5000 }).data?.find( + useRead("ListStacks", {}, { refetchInterval: 10_000 }).data?.find( (d) => d.id === id ); export const useFullStack = (id: string) => - useRead("GetStack", { stack: id }, { refetchInterval: 5000 }).data; + useRead("GetStack", { stack: id }, { refetchInterval: 10_000 }).data; const StackIcon = ({ id, size }: { id?: string; size: number }) => { const state = useStack(id)?.info.state; diff --git a/frontend/src/components/resources/stack/info.tsx b/frontend/src/components/resources/stack/info.tsx index 272f74ac1..12496ef2f 100644 --- a/frontend/src/components/resources/stack/info.tsx +++ b/frontend/src/components/resources/stack/info.tsx @@ -8,7 +8,10 @@ import { useEditPermissions } from "@pages/resource"; import { ConfirmUpdate } from "@components/config/util"; import { useWrite } from "@lib/hooks"; import { Button } from "@ui/button"; -import { History } from "lucide-react"; +import { FilePlus, History } from "lucide-react"; +import { useToast } from "@ui/use-toast"; +import { ConfirmButton } from "@components/util"; +import { DEFAULT_STACK_FILE_CONTENTS } from "./config"; export const StackInfo = ({ id, @@ -19,7 +22,15 @@ export const StackInfo = ({ }) => { const [edits, setEdits] = useState>({}); const { canWrite } = useEditPermissions({ type: "Stack", id }); - const { mutateAsync } = useWrite("WriteStackFileContents"); + const { toast } = useToast(); + const { mutateAsync, isPending } = useWrite("WriteStackFileContents", { + onSuccess: (res) => { + toast({ + title: res.success ? "Contents written." : "Failed to write contents.", + variant: res.success ? undefined : "destructive", + }); + }, + }); const stack = useFullStack(id); // const state = useStack(id)?.info.state ?? Types.StackState.Unknown; @@ -27,8 +38,9 @@ export const StackInfo = ({ // state // ); - const file_on_host = stack?.config?.files_on_host; - const canEdit = canWrite && file_on_host; + const file_on_host = stack?.config?.files_on_host ?? false; + const git_repo = stack?.config?.repo ? true : false; + const canEdit = canWrite && (file_on_host || git_repo); const editFileCallback = (path: string) => (contents: string) => setEdits({ ...edits, [path]: contents }); @@ -58,28 +70,47 @@ export const StackInfo = ({ // } const latest_contents = stack?.info?.remote_contents; + const latest_errors = stack?.info?.remote_errors; return (
- {/* ERRORS */} - {stack?.info?.remote_errors && stack?.info?.remote_errors.length > 0 && ( - - - remote errors:{" "} - {stack?.info?.remote_errors?.map((content, i) => ( -
-                path: {content.path}
-                
 0 &&
+        latest_errors.map((error) => (
+          
+            
+              
+
Path:
+ {error.path} +
+ {canEdit && ( + } + onClick={() => { + if (stack) { + mutateAsync({ + stack: stack.name, + file_path: error.path, + contents: DEFAULT_STACK_FILE_CONTENTS, + }); + } }} - className="max-h-[500px] overflow-y-auto" + loading={isPending} /> -
- ))} - - - )} + )} + + +
+            
+          
+        ))}
 
       {/* Update deployed contents with diff */}
       {/* {!is_down && deployed_contents.length > 0 && (
@@ -158,9 +189,12 @@ export const StackInfo = ({
       {latest_contents &&
         latest_contents.length > 0 &&
         latest_contents.map((content) => (
-          
+          
             
-              
{content.path}
+
+
File:
+ {content.path} +
{canEdit && (
)} diff --git a/frontend/src/components/resources/stack/services.tsx b/frontend/src/components/resources/stack/services.tsx index 1d1f84cf0..fd2d20457 100644 --- a/frontend/src/components/resources/stack/services.tsx +++ b/frontend/src/components/resources/stack/services.tsx @@ -25,7 +25,7 @@ export const StackServices = ({ const services = useRead( "ListStackServices", { stack: id }, - { refetchInterval: 5000 } + { refetchInterval: 10_000 } ).data; if ( !services || diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index 572e64de8..0ef85b5a8 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -1,6 +1,13 @@ import { RESOURCE_TARGETS, cn, usableResourcePath } from "@lib/utils"; import { Button } from "@ui/button"; -import { AlertTriangle, Bell, Box, Boxes, Settings } from "lucide-react"; +import { + AlertTriangle, + Bell, + Box, + Boxes, + LayoutDashboard, + Settings, +} from "lucide-react"; import { Link, useLocation } from "react-router-dom"; import { ResourceComponents } from "./resources"; import { Separator } from "@ui/separator"; @@ -17,7 +24,7 @@ export const Sidebar = () => { } + icon={} onClick={() => setView("Dashboard")} highlighted={view === "Dashboard"} /> @@ -28,13 +35,12 @@ export const Sidebar = () => { onClick={() => setView("Resources")} highlighted={view === "Resources"} /> - {/* } - onClick={() => setView("Tree")} - highlighted={view === "Tree"} - /> */} + } + /> +

Resources

diff --git a/frontend/src/lib/formatting.ts b/frontend/src/lib/formatting.ts index 780d1824b..3af811db4 100644 --- a/frontend/src/lib/formatting.ts +++ b/frontend/src/lib/formatting.ts @@ -8,6 +8,14 @@ export const fmt_date = (d: Date) => { }:${minutes > 9 ? minutes : "0" + minutes}`; }; +export const fmt_utc_date = (d: Date) => { + const hours = d.getUTCHours(); + const minutes = d.getUTCMinutes(); + return `${fmt_month(d.getUTCMonth())} ${d.getUTCDate()} ${ + hours > 9 ? hours : "0" + hours + }:${minutes > 9 ? minutes : "0" + minutes}`; +}; + const fmt_month = (month: number) => { switch (month) { case 0: @@ -65,6 +73,10 @@ export const fmt_operation = (operation: Types.Operation) => { return operation.match(/[A-Z][a-z]+|[0-9]+/g)?.join(" ")!; }; +export const fmt_upper_camelcase = (input: string) => { + return input.match(/[A-Z][a-z]+|[0-9]+/g)?.join(" ")!; +}; + /// list_all_items => List All Items export function snake_case_to_upper_space_case(snake: string) { if (snake.length === 0) return ""; diff --git a/frontend/src/lib/hooks.ts b/frontend/src/lib/hooks.ts index 07e6282d0..b05129037 100644 --- a/frontend/src/lib/hooks.ts +++ b/frontend/src/lib/hooks.ts @@ -33,12 +33,20 @@ export const useLoginOptions = () => queryFn: () => client().auth({ type: "GetLoginOptions", params: {} }), }); -export const useUser = () => - useQuery({ +export const useUser = () => { + const userInvalidate = useUserInvalidate(); + const query = useQuery({ queryKey: ["GetUser"], queryFn: () => client().auth({ type: "GetUser", params: {} }), refetchInterval: 30_000, }); + useEffect(() => { + if (query.data && query.error) { + userInvalidate(); + } + }, [query.data, query.error]); + return query; +}; export const useUserInvalidate = () => { const qc = useQueryClient(); diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 4a5860873..dbd83c16a 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -127,10 +127,9 @@ export const has_minimum_permissions = ( return level_to_number(level) >= level_to_number(greater_than); }; -const tzOffset = new Date().getTimezoneOffset() * 60; +const tzOffsetMs = new Date().getTimezoneOffset() * 60 * 1000; -export const convertTsMsToLocalUnixTsInSecs = (ts: number) => - ts / 1000 - tzOffset; +export const convertTsMsToLocalUnixTsInMs = (ts: number) => ts - tzOffsetMs; export const usableResourcePath = (resource: UsableResource) => { if (resource === "ServerTemplate") return "server-templates"; @@ -249,8 +248,8 @@ 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 + return maybe_domain; } else { - return "docker.io" + return "docker.io"; } -} \ No newline at end of file +}; diff --git a/frontend/src/pages/containers.tsx b/frontend/src/pages/containers.tsx new file mode 100644 index 000000000..704ff3ae2 --- /dev/null +++ b/frontend/src/pages/containers.tsx @@ -0,0 +1,137 @@ +import { Page } from "@components/layouts"; +import { ResourceLink } from "@components/resources/common"; +import { DockerResourceLink, StatusBadge } from "@components/util"; +import { container_state_intention } from "@lib/color"; +import { useRead } from "@lib/hooks"; +import { DataTable, SortableHeader } from "@ui/data-table"; +import { Input } from "@ui/input"; +import { Box, Search } from "lucide-react"; +import { useCallback, useMemo, useState } from "react"; + +export const ContainersPage = () => { + const [search, setSearch] = useState(""); + const searchSplit = search.toLowerCase().split(" "); + const servers = useRead("ListServers", {}).data; + const serverName = useCallback( + (id: string) => servers?.find((server) => server.id === id)?.name, + [servers] + ); + const _containers = useRead("ListAllDockerContainers", {}).data; + const containers = useMemo( + () => + _containers?.filter((c) => { + if (searchSplit.length === 0) return true; + return searchSplit.every((search) => c.name.includes(search)); + }), + [_containers, searchSplit] + ); + return ( + + See all containers across all servers + + } + icon={} + > +
+
+
+
+ + setSearch(e.target.value)} + placeholder="search..." + className="pl-8 w-[200px] lg:w-[300px]" + /> +
+
+ ( + + ), + cell: ({ row }) => ( + + ), + size: 200, + }, + { + accessorKey: "server_id", + sortingFn: (a, b) => { + const sa = serverName(a.original.server_id!); + const sb = serverName(b.original.server_id!); + + if (!sa && !sb) return 0; + if (!sa) return -1; + if (!sb) return 1; + + if (sa > sb) return 1; + else if (sa < sb) return -1; + else return 0; + }, + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "image", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "network_mode", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "state", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const state = row.original?.state; + return ( + + ); + }, + }, + ]} + /> +
+
+ ); +}; diff --git a/frontend/src/pages/home/all_resources.tsx b/frontend/src/pages/home/all_resources.tsx index fe80eb8f2..35ee6da5d 100644 --- a/frontend/src/pages/home/all_resources.tsx +++ b/frontend/src/pages/home/all_resources.tsx @@ -51,14 +51,16 @@ export const AllResources = () => {

)} - {Object.entries(ResourceComponents).map(([type, Components]) => ( - - ))} +
+ {Object.entries(ResourceComponents).map(([type, Components]) => ( + + ))} +
); }; @@ -92,7 +94,7 @@ const TableSection = ({ icon={} actions={} > -
+
{show && }
diff --git a/frontend/src/pages/server-info/container/index.tsx b/frontend/src/pages/server-info/container/index.tsx index 2a6d95a6a..98ff7b62e 100644 --- a/frontend/src/pages/server-info/container/index.tsx +++ b/frontend/src/pages/server-info/container/index.tsx @@ -60,7 +60,7 @@ const ContainerPageInner = ({ { server: id, }, - { refetchInterval: 30_000 } + { refetchInterval: 10_000 } ).data?.find((container) => container.name === container_name); if (isPending) { @@ -183,7 +183,7 @@ const AttachedResource = ({ const { data: attached, isPending } = useRead( "GetResourceMatchingContainer", { server: id, container }, - { refetchInterval: 30_000 } + { refetchInterval: 10_000 } ); if (isPending) { diff --git a/frontend/src/pages/server-info/image.tsx b/frontend/src/pages/server-info/image.tsx index ae2b208eb..ea6fd9420 100644 --- a/frontend/src/pages/server-info/image.tsx +++ b/frontend/src/pages/server-info/image.tsx @@ -59,7 +59,7 @@ const ImagePageInner = ({ { server: id, }, - { refetchInterval: 5000 } + { refetchInterval: 10_000 } ).data?.filter((container) => !image?.Id ? false : container.image_id === image?.Id ); diff --git a/frontend/src/pages/server-info/volume.tsx b/frontend/src/pages/server-info/volume.tsx index 99fe3acf0..efcee2f6b 100644 --- a/frontend/src/pages/server-info/volume.tsx +++ b/frontend/src/pages/server-info/volume.tsx @@ -59,7 +59,7 @@ const VolumePageInner = ({ { server: id, }, - { refetchInterval: 5000 } + { refetchInterval: 10_000 } ).data?.filter((container) => container.volumes?.includes(volume_name)); const { mutate: deleteVolume, isPending: deletePending } = useExecute( diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 79a623add..87f234248 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -18,6 +18,7 @@ import { NetworkPage } from "@pages/server-info/network"; import { ImagePage } from "@pages/server-info/image"; import { VolumePage } from "@pages/server-info/volume"; import { ContainerPage } from "@pages/server-info/container"; +import { ContainersPage } from "@pages/containers"; const ROUTER = createBrowserRouter([ { @@ -26,28 +27,22 @@ const ROUTER = createBrowserRouter([ children: [ { path: "", element: }, { path: "settings", element: }, - // { path: "keys", element: }, - // { path: "tags", element: }, { path: "tree", element: }, { path: "alerts", element: }, { path: "updates", element: }, - // { path: "variables", element: }, + { path: "updates", element: }, + { path: "containers", element: }, { path: "resources", element: }, { path: "user-groups/:id", element: }, { path: "users", - children: [ - // { path: "", element: }, - { path: ":id", element: }, - ], + children: [{ path: ":id", element: }], }, { path: ":type", children: [ { path: "", element: }, { path: ":id", element: }, - // { path: ":id/updates", element: }, - // { path: ":id/alerts", element: }, { path: ":id/service/:service", element: , diff --git a/frontend/yarn.lock b/frontend/yarn.lock index d2d05a655..3fbd82680 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -182,6 +182,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.14.6": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" + integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.25.0": version "7.25.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" @@ -1216,6 +1223,35 @@ dependencies: "@babel/types" "^7.20.7" +"@types/d3-array@^3.0.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-path@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" + integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== + +"@types/d3-scale@^4.0.1": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.0.1": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" + integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" + integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== + "@types/estree@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" @@ -1226,6 +1262,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== +"@types/raf@^3.4.0": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04" + integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw== + "@types/react-dom@18.3.0": version "18.3.0" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" @@ -1233,6 +1274,13 @@ dependencies: "@types/react" "*" +"@types/react-dom@^17.0.9": + version "17.0.25" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.25.tgz#e0e5b3571e1069625b3a3da2b279379aa33a0cb5" + integrity sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA== + dependencies: + "@types/react" "^17" + "@types/react@*": version "18.2.21" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.21.tgz#774c37fd01b522d0b91aed04811b58e4e0514ed9" @@ -1250,6 +1298,15 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/react@^17", "@types/react@^17.0.14": + version "17.0.83" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.83.tgz#b477c56387b74279281149dcf5ba2a1e2216d131" + integrity sha512-l0m4ArKJvmFtR4e8UmKrj1pB4tUgOhJITf+mADyF/p69Ts1YAR/E+G9XEM0mHXKVRa1dQNHseyyDNzeuAXfXQw== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "^0.16" + csstype "^3.0.2" + "@types/sanitize-html@2.11.0": version "2.11.0" resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.11.0.tgz#582d8c72215c0228e3af2be136e40e0b531addf2" @@ -1262,6 +1319,11 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== +"@types/scheduler@^0.16": + version "0.16.8" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" + integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== + "@types/svg-path-parser@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@types/svg-path-parser/-/svg-path-parser-1.1.3.tgz#659aca2e886071e5c593e06206b6254bcb811202" @@ -1629,6 +1691,95 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== +d3-array@2, d3-array@^2.12.1, d3-array@^2.3.0: + version "2.12.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + +"d3-array@2 - 3": + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + +d3-delaunay@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-5.3.0.tgz#b47f05c38f854a4e7b3cea80e0bb12e57398772d" + integrity sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w== + dependencies: + delaunator "4" + +"d3-format@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== + +"d3-interpolate@1.2.0 - 2": + version "2.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + +d3-scale@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" + integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== + dependencies: + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "^2.1.1" + d3-time-format "2 - 3" + +d3-shape@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" + integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== + dependencies: + d3-path "1 - 2" + +"d3-time-format@2 - 3": + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== + dependencies: + d3-time "1 - 2" + +d3-time-format@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 2", d3-time@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" + integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== + dependencies: + d3-array "2" + +"d3-time@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -1646,6 +1797,11 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== +delaunator@4: + version "4.0.1" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-4.0.1.tgz#3d779687f57919a7a418f8ab947d3bddb6846957" + integrity sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag== + detect-node-es@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" @@ -1867,11 +2023,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -fancy-canvas@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fancy-canvas/-/fancy-canvas-2.1.0.tgz#44b40e40419ad8ef8304df365e4276767e918552" - integrity sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -2102,6 +2253,16 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -2217,13 +2378,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lightweight-charts@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lightweight-charts/-/lightweight-charts-4.2.0.tgz#507c44bc6ddbf08b6197736a2821d5b1bd2adc8e" - integrity sha512-TRC61cI6bEKBEHcpqawL0KP8Rwos9b2OcdyeQ4Ri7Zybf8NvrRYmNcJV+2a2rMo7VZp1NhVWURuNEskjc+wm4A== - dependencies: - fancy-canvas "2.1.0" - lilconfig@^2.0.5, lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -2539,6 +2693,26 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +react-charts@^3.0.0-beta.57: + version "3.0.0-beta.57" + resolved "https://registry.yarnpkg.com/react-charts/-/react-charts-3.0.0-beta.57.tgz#e32f7b096c20fe6e306cea9f5bfe1910238a95ba" + integrity sha512-vqas7IQhsnDGcMxreGaWXvSIL3poEMoUBNltJrslz/+m0pI3QejBCszL1QrLNYQfOWXrbZADfedi/a+yWOQ7Hw== + dependencies: + "@babel/runtime" "^7.14.6" + "@types/d3-array" "^3.0.1" + "@types/d3-scale" "^4.0.1" + "@types/d3-shape" "^3.0.1" + "@types/raf" "^3.4.0" + "@types/react" "^17.0.14" + "@types/react-dom" "^17.0.9" + d3-array "^2.12.1" + d3-delaunay "5.3.0" + d3-scale "^3.3.0" + d3-shape "^2.1.0" + d3-time "^2.1.1" + d3-time-format "^4.1.0" + ts-toolbelt "^9.6.0" + react-dom@18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" @@ -2884,6 +3058,11 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== +ts-toolbelt@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz#50a25426cfed500d4a09bd1b3afb6f28879edfd5" + integrity sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w== + tsconfck@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.3.tgz#d9bda0e87d05b1c360e996c9050473c7e6f8084f" diff --git a/lib/git/src/commit.rs b/lib/git/src/commit.rs new file mode 100644 index 000000000..0a874c341 --- /dev/null +++ b/lib/git/src/commit.rs @@ -0,0 +1,149 @@ +use std::path::Path; + +use anyhow::Context; +use command::run_komodo_command; +use formatting::format_serror; +use komodo_client::entities::{all_logs_success, update::Log}; +use tokio::fs; + +use crate::{get_commit_hash_log, GitRes}; + +/// Write file, add, commit, force push. +/// Repo must be cloned. +pub async fn write_commit_file( + commit_msg: &str, + repo_dir: &Path, + // relative to repo root + file: &Path, + contents: &str, +) -> anyhow::Result { + let path = repo_dir.join(file); + + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(&parent).await; + } + + fs::write(&path, contents).await.with_context(|| { + format!("Failed to write contents to {path:?}") + })?; + + let mut res = GitRes::default(); + res.logs.push(Log::simple( + "Write file", + format!("File contents written to {path:?}"), + )); + + commit_file_inner(commit_msg, &mut res, repo_dir, file).await; + + Ok(res) +} + +/// Add file, commit, force push. +/// Repo must be cloned. +pub async fn commit_file( + commit_msg: &str, + repo_dir: &Path, + // relative to repo root + file: &Path, +) -> GitRes { + let mut res = GitRes::default(); + commit_file_inner(commit_msg, &mut res, repo_dir, file).await; + res +} + +pub async fn commit_file_inner( + commit_msg: &str, + res: &mut GitRes, + repo_dir: &Path, + // relative to repo root + file: &Path, +) { + let add_log = run_komodo_command( + "add files", + repo_dir, + format!("git add {}", file.display()), + ) + .await; + res.logs.push(add_log); + if !all_logs_success(&res.logs) { + return; + } + + let commit_log = run_komodo_command( + "commit", + repo_dir, + format!( + "git commit -m \"[Komodo] {commit_msg}: update {file:?}\"" + ), + ) + .await; + res.logs.push(commit_log); + if !all_logs_success(&res.logs) { + return; + } + + match get_commit_hash_log(repo_dir).await { + Ok((log, hash, message)) => { + res.logs.push(log); + res.hash = Some(hash); + res.message = Some(message); + } + Err(e) => { + res.logs.push(Log::error( + "get commit hash", + format_serror(&e.into()), + )); + return; + } + }; + + let push_log = + run_komodo_command("push", repo_dir, "git push -f").await; + res.logs.push(push_log); +} + +/// Add, commit, and force push. +/// Repo must be cloned. +pub async fn commit_all(repo_dir: &Path, message: &str) -> GitRes { + let mut res = GitRes::default(); + + let add_log = + run_komodo_command("add files", repo_dir, "git add -A").await; + res.logs.push(add_log); + if !all_logs_success(&res.logs) { + return res; + } + + let commit_log = run_komodo_command( + "commit", + repo_dir, + format!("git commit -m \"[Komodo] {message}\""), + ) + .await; + res.logs.push(commit_log); + if !all_logs_success(&res.logs) { + return res; + } + + match get_commit_hash_log(repo_dir).await { + Ok((log, hash, message)) => { + res.logs.push(log); + res.hash = Some(hash); + res.message = Some(message); + } + Err(e) => { + res.logs.push(Log::error( + "get commit hash", + format_serror(&e.into()), + )); + return res; + } + }; + + let push_log = + run_komodo_command("push", repo_dir, format!("git push -f")) + .await; + res.logs.push(push_log); + + res +} diff --git a/lib/git/src/lib.rs b/lib/git/src/lib.rs index dc7eac331..26ee2176b 100644 --- a/lib/git/src/lib.rs +++ b/lib/git/src/lib.rs @@ -11,13 +11,16 @@ use tracing::instrument; pub mod environment; mod clone; +mod commit; mod pull; mod pull_or_clone; pub use clone::clone; +pub use commit::{commit_all, commit_file, write_commit_file}; pub use pull::pull; pub use pull_or_clone::pull_or_clone; +#[derive(Debug, Default, Clone)] pub struct GitRes { pub logs: Vec, pub hash: Option, @@ -45,7 +48,6 @@ pub async fn get_commit_hash_info( ); Ok(LatestCommit { hash, message }) } - /// returns (Log, commit hash, commit message) #[instrument(level = "debug")] pub async fn get_commit_hash_log( diff --git a/test.core.config.toml b/test.core.config.toml index 394eb5e7f..9aad808ef 100644 --- a/test.core.config.toml +++ b/test.core.config.toml @@ -5,7 +5,8 @@ title = "Komodo Test" host = "http://localhost:9121" port = 9121 passkey = "a_random_passkey" -repo_directory = ".core-repos" +sync_directory = ".komodo/syncs" +repo_directory = ".komodo/core-repos" frontend_path = "frontend/dist" first_server = "http://localhost:8121" diff --git a/test.periphery.config.toml b/test.periphery.config.toml index 485495afb..d984e252f 100644 --- a/test.periphery.config.toml +++ b/test.periphery.config.toml @@ -2,8 +2,8 @@ # 🦎 KOMODO PERIPHERY CONFIG 🦎 # ################################ port = 8121 -repo_dir = ".repos" -stack_dir = ".stacks" +repo_dir = ".komodo/repos" +stack_dir = ".komodo/stacks" stats_polling_rate = "1-sec" legacy_compose_cli = false include_disk_mounts = [] @@ -12,8 +12,8 @@ include_disk_mounts = [] # Security # ############ ssl_enabled = false -ssl_key_file = ".ssl/key.pem" -ssl_cert_file = ".ssl/cert.pem" +ssl_key_file = ".komodo/ssl/key.pem" +ssl_cert_file = ".komodo/ssl/cert.pem" ########### # LOGGING #