Compare commits

..
Author SHA1 Message Date
mbecker20 51b16434a5 docs new organization 2025-02-08 19:21:31 -08:00
mbecker20 f49d73dba3 fix new compose images 2025-02-08 19:21:19 -08:00
mbecker20 62723fa32f more legible favicon 2025-02-08 19:18:04 -08:00
mbecker20 d1d17626b2 fix login screen logo 2025-02-08 19:00:08 -08:00
mbecker20 312f51e50b dev-1 2025-02-08 18:45:32 -08:00
mbecker20 d68ea5cf45 remove example from cargo toml workspace 2025-02-08 18:45:32 -08:00
mbecker20 71fc0bba97 mbecker20 -> moghtech 2025-02-08 18:45:32 -08:00
1a7a0299de Remove .git from remote_url (#299)
Remove .git from remote_url

Co-authored-by: Deon Marshall <dmarshall@ccp.com.au>
2025-02-08 18:45:32 -08:00
unsyncandmbecker20 cd59da100f feature: interpolate secrets in custom alerter (#289)
* feature: interpolate secrets in custom alerter

* fix rust warning

* review: sanitize errors

* review: sanitize error message
2025-02-08 18:45:32 -08:00
unsyncandmbecker20 5a8ed8b81d feature: add post_deploy command (#288)
* feature: add post_deploy command

* review: do not run post_deploy if deploy failed
2025-02-08 18:45:32 -08:00
mbecker20 4c9479a8bc 1.17.0-dev 2025-02-08 18:45:32 -08:00
unsyncandmbecker20 19aa5fb260 feature: use the repo path instead of name in GetLatestCommit (#282)
* Update repo path handling in commit fetching

- Changed `name` to `path` for repository identification.
- Updated cache update function to use the new path field.
- Improved error message for non-directory repo paths.

* feat: use optional name and path in GetLatestCommit

* review: don't use optional for name

* review: use helper

* review: remove redundant to_string()
2025-02-08 18:45:32 -08:00
mbecker20 75c0c967ac update available deployment table 2025-02-08 18:45:32 -08:00
mbecker20 8832a05ffe show update available stack table 2025-02-08 18:45:32 -08:00
mbecker20 7598978cd1 finish oidc comment 2025-02-08 18:45:32 -08:00
mbecker20 b868ad5794 clean up rust client websocket subscription 2025-02-08 18:45:32 -08:00
mbecker20 be6a0db511 escape incoming sync backslashes (BREAKING) 2025-02-08 18:45:32 -08:00
mbecker20 c360289984 rename Test Alerter button 2025-02-08 18:45:32 -08:00
mbecker20 23abdb85b0 simplify network stats 2025-02-08 18:45:32 -08:00
mbecker20 b510d6dc41 komodo-logo 2025-02-08 18:45:31 -08:00
mbecker20 f29c60a6e9 higher quality / colored icons 2025-02-08 18:45:31 -08:00
mbecker20 ee33fc98d9 Add test alerter button 2025-02-08 18:45:31 -08:00
mbecker20 729ffc9c3c fix last axum updates 2025-02-08 18:45:31 -08:00
mbecker20 395930094d axum update :param to {param} syntax 2025-02-08 18:45:31 -08:00
mbecker20 79a16c6f22 rust 1.84.0 2025-02-08 18:45:31 -08:00
mbecker20 4e4aa8c567 test alert implementation 2025-02-08 18:45:31 -08:00
mbecker20 bfbcd33c1a add entities / message for test alerter 2025-02-08 18:45:31 -08:00
mbecker20 01e2e0be11 the komodo env file should be highest priority over additional files 2025-02-08 18:45:31 -08:00
mbecker20 2e6e409b85 clean up cors 2025-02-08 18:45:31 -08:00
mbecker20 30e87b6aff just make it 1.17.0 2025-02-08 18:45:31 -08:00
mbecker20 8423a53106 bump aws deps 2025-02-08 18:45:31 -08:00
mbecker20 9e6c122313 axum to 0.8 2025-02-08 18:45:31 -08:00
mbecker20 d03d0b3b78 resource2 not really a benefit 2025-02-08 18:45:31 -08:00
mbecker20 1bf76f1b57 format 2025-02-08 18:45:31 -08:00
mbecker20 7815639aeb fmt 2025-02-08 18:45:31 -08:00
mbecker20 af9fbf9667 resolver v3
add new ec2 instance types

clean up testing config

document the libraries a bit

clean up main

update sysinfo and otel

update client resolver 3.0

resolver v3 prog

clean up gitignore

implement periphery resolver v3

clean up

core read api v3

more prog

execute api

missing apis

compiling

1.16.13

work on more granular traits

prog on crud
2025-02-08 18:45:31 -08:00
143 changed files with 2794 additions and 3344 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20.12.2"
"version": "18.18.0"
},
"ghcr.io/devcontainers-community/features/deno:1": {
-2
View File
@@ -1,2 +0,0 @@
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
open_collective: komodo
Generated
+362 -402
View File
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "1.17.0-dev-4"
version = "1.17.0-dev-1"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -40,11 +40,11 @@ derive_variants = "1.0.0"
mongo_indexed = "2.0.1"
resolver_api = "3.0.0"
toml_pretty = "1.1.2"
mungos = "3.2.0"
mungos = "1.1.0"
svi = "1.0.1"
# ASYNC
reqwest = { version = "0.12.12", default-features = false, features = ["json", "rustls-tls-native-roots"] }
reqwest = { version = "0.12.12", default-features = false, features = ["json", "rustls-tls"] }
tokio = { version = "1.43.0", features = ["full"] }
tokio-util = "0.7.13"
futures = "0.3.31"
@@ -55,45 +55,45 @@ axum-extra = { version = "0.10.0", features = ["typed-header"] }
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
axum-server = { version = "0.7.1", features = ["tls-rustls"] }
axum = { version = "0.8.1", features = ["ws", "json", "macros"] }
tokio-tungstenite = "0.26.2"
tokio-tungstenite = "0.26.1"
# SER/DE
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
serde = { version = "1.0.218", features = ["derive"] }
strum = { version = "0.27.1", features = ["derive"] }
serde_json = "1.0.140"
serde = { version = "1.0.217", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
serde_json = "1.0.135"
serde_yaml = "0.9.34"
toml = "0.8.20"
toml = "0.8.19"
# ERROR
anyhow = "1.0.97"
thiserror = "2.0.12"
anyhow = "1.0.95"
thiserror = "2.0.11"
# LOGGING
opentelemetry-otlp = { version = "0.28.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.28.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.27.0", features = ["tls-roots", "reqwest-rustls"] }
opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio"] }
tracing-subscriber = { version = "0.3.19", features = ["json"] }
opentelemetry-semantic-conventions = "0.28.0"
tracing-opentelemetry = "0.29.0"
opentelemetry = "0.28.0"
opentelemetry-semantic-conventions = "0.27.0"
tracing-opentelemetry = "0.28.0"
opentelemetry = "0.27.1"
tracing = "0.1.41"
# CONFIG
clap = { version = "4.5.31", features = ["derive"] }
clap = { version = "4.5.26", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
# CRYPTO / AUTH
uuid = { version = "1.15.1", features = ["v4", "fast-rng", "serde"] }
openidconnect = "4.0.0"
uuid = { version = "1.12.0", features = ["v4", "fast-rng", "serde"] }
openidconnect = "3.5.0"
urlencoding = "2.1.3"
nom_pem = "4.0.0"
bcrypt = "0.17.0"
bcrypt = "0.16.0"
base64 = "0.22.1"
rustls = "0.23.23"
rustls = "0.23.21"
hmac = "0.12.1"
sha2 = "0.10.8"
rand = "0.9.0"
rand = "0.8.5"
jwt = "0.16.0"
hex = "0.4.3"
@@ -102,8 +102,8 @@ bollard = "0.18.1"
sysinfo = "0.33.1"
# CLOUD
aws-config = "1.5.17"
aws-sdk-ec2 = "1.114.0"
aws-config = "1.5.13"
aws-sdk-ec2 = "1.101.0"
# MISC
derive_builder = "0.20.2"
+3 -4
View File
@@ -67,7 +67,7 @@ impl Resolve<ExecuteArgs> for RunAction {
) -> serror::Result<Update> {
let mut action = resource::get_check_permissions::<Action>(
&self.action,
user,
&user,
PermissionLevel::Execute,
)
.await?;
@@ -111,9 +111,7 @@ impl Resolve<ExecuteArgs> for RunAction {
let path = core_config().action_directory.join(&file);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize Action file parent directory {parent:?}"))?;
let _ = fs::create_dir_all(parent).await;
}
fs::write(&path, contents).await.with_context(|| {
@@ -125,6 +123,7 @@ impl Resolve<ExecuteArgs> for RunAction {
"Execute Action",
None,
format!("deno run --allow-all {}", path.display()),
false,
)
.await;
+2 -2
View File
@@ -82,7 +82,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
) -> serror::Result<Update> {
let mut build = resource::get_check_permissions::<Build>(
&self.build,
user,
&user,
PermissionLevel::Execute,
)
.await?;
@@ -517,7 +517,7 @@ impl Resolve<ExecuteArgs> for CancelBuild {
) -> serror::Result<Update> {
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
&user,
PermissionLevel::Execute,
)
.await?;
+2 -2
View File
@@ -561,7 +561,7 @@ impl Resolve<ExecuteArgs> for UnpauseDeployment {
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
setup_deployment_execution(&self.deployment, &user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
@@ -610,7 +610,7 @@ impl Resolve<ExecuteArgs> for StopDeployment {
ExecuteArgs { user, update }: &ExecuteArgs,
) -> serror::Result<Update> {
let (deployment, server) =
setup_deployment_execution(&self.deployment, user).await?;
setup_deployment_execution(&self.deployment, &user).await?;
// get the action state for the deployment (or insert default).
let action_state = action_states()
+4 -3
View File
@@ -173,7 +173,7 @@ impl Resolve<ExecuteArgs> for BatchPullRepo {
ExecuteArgs { user, .. }: &ExecuteArgs,
) -> serror::Result<BatchExecutionResponse> {
Ok(
super::batch_execute::<BatchPullRepo>(&self.pattern, user)
super::batch_execute::<BatchPullRepo>(&self.pattern, &user)
.await?,
)
}
@@ -187,7 +187,7 @@ impl Resolve<ExecuteArgs> for PullRepo {
) -> serror::Result<Update> {
let mut repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
&user,
PermissionLevel::Execute,
)
.await?;
@@ -438,7 +438,8 @@ impl Resolve<ExecuteArgs> for BuildRepo {
return handle_builder_early_return(
update, repo.id, repo.name, false,
)
.await;
.await
.map_err(Into::into);
}
};
+20 -62
View File
@@ -41,7 +41,7 @@ impl super::BatchExecute for BatchDeployStack {
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DeployStack(DeployStack {
stack,
services: Vec::new(),
service: None,
stop_time: None,
})
}
@@ -87,13 +87,10 @@ impl Resolve<ExecuteArgs> for DeployStack {
update_update(update.clone()).await?;
if !self.services.is_empty() {
if let Some(service) = &self.service {
update.logs.push(Log::simple(
&format!("Service/s",),
format!(
"Execution requested for Stack service/s {}",
self.services.join(", ")
),
&format!("Service: {service}"),
format!("Execution requested for Stack service {service}"),
))
}
@@ -180,13 +177,12 @@ impl Resolve<ExecuteArgs> for DeployStack {
file_contents,
missing_files,
remote_errors,
compose_config,
commit_hash,
commit_message,
} = periphery_client(&server)?
.request(ComposeUp {
stack: stack.clone(),
services: self.services,
service: self.service,
git_token,
registry_token,
replacers: secret_replacers.into_iter().collect(),
@@ -210,14 +206,12 @@ impl Resolve<ExecuteArgs> for DeployStack {
let (
deployed_services,
deployed_contents,
deployed_config,
deployed_hash,
deployed_message,
) = if deployed {
(
Some(latest_services.clone()),
Some(file_contents.clone()),
compose_config,
commit_hash.clone(),
commit_message.clone(),
)
@@ -225,7 +219,6 @@ impl Resolve<ExecuteArgs> for DeployStack {
(
stack.info.deployed_services,
stack.info.deployed_contents,
stack.info.deployed_config,
stack.info.deployed_hash,
stack.info.deployed_message,
)
@@ -236,7 +229,6 @@ impl Resolve<ExecuteArgs> for DeployStack {
deployed_project_name: project_name.into(),
deployed_services,
deployed_contents,
deployed_config,
deployed_hash,
deployed_message,
latest_services,
@@ -374,7 +366,7 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
DeployStack {
stack: stack.name,
services: Vec::new(),
service: None,
stop_time: self.stop_time,
}
.resolve(&ExecuteArgs {
@@ -387,18 +379,14 @@ impl Resolve<ExecuteArgs> for DeployStackIfChanged {
pub async fn pull_stack_inner(
mut stack: Stack,
services: Vec<String>,
service: Option<String>,
server: &Server,
mut update: Option<&mut Update>,
update: Option<&mut Update>,
) -> anyhow::Result<ComposePullResponse> {
if let Some(update) = update.as_mut() {
if !services.is_empty() {}
if let (Some(service), Some(update)) = (&service, update) {
update.logs.push(Log::simple(
&format!("Service/s"),
format!(
"Execution requested for Stack service/s {}",
services.join(", ")
),
&format!("Service: {service}"),
format!("Execution requested for Stack service {service}"),
))
}
@@ -417,40 +405,10 @@ pub async fn pull_stack_inner(
|| format!("Failed to get registry token in call to db. Stopping run. | {} | {}", stack.config.registry_provider, stack.config.registry_account),
)?;
// interpolate variables / secrets
if !stack.config.skip_secret_interp {
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.file_contents,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut stack.config.environment,
&mut global_replacers,
&mut secret_replacers,
)?;
if let Some(update) = update {
add_interp_update_log(
update,
&global_replacers,
&secret_replacers,
);
}
};
let res = periphery_client(server)?
.request(ComposePull {
stack,
services,
service,
git_token,
registry_token,
})
@@ -490,7 +448,7 @@ impl Resolve<ExecuteArgs> for PullStack {
let res = pull_stack_inner(
stack,
self.services,
self.service,
&server,
Some(&mut update),
)
@@ -512,7 +470,7 @@ impl Resolve<ExecuteArgs> for StartStack {
) -> serror::Result<Update> {
execute_compose::<StartStack>(
&self.stack,
self.services,
self.service,
user,
|state| state.starting = true,
update.clone(),
@@ -531,7 +489,7 @@ impl Resolve<ExecuteArgs> for RestartStack {
) -> serror::Result<Update> {
execute_compose::<RestartStack>(
&self.stack,
self.services,
self.service,
user,
|state| {
state.restarting = true;
@@ -552,7 +510,7 @@ impl Resolve<ExecuteArgs> for PauseStack {
) -> serror::Result<Update> {
execute_compose::<PauseStack>(
&self.stack,
self.services,
self.service,
user,
|state| state.pausing = true,
update.clone(),
@@ -571,7 +529,7 @@ impl Resolve<ExecuteArgs> for UnpauseStack {
) -> serror::Result<Update> {
execute_compose::<UnpauseStack>(
&self.stack,
self.services,
self.service,
user,
|state| state.unpausing = true,
update.clone(),
@@ -590,7 +548,7 @@ impl Resolve<ExecuteArgs> for StopStack {
) -> serror::Result<Update> {
execute_compose::<StopStack>(
&self.stack,
self.services,
self.service,
user,
|state| state.stopping = true,
update.clone(),
@@ -606,7 +564,7 @@ impl super::BatchExecute for BatchDestroyStack {
fn single_request(stack: String) -> ExecuteRequest {
ExecuteRequest::DestroyStack(DestroyStack {
stack,
services: Vec::new(),
service: None,
remove_orphans: false,
stop_time: None,
})
@@ -633,7 +591,7 @@ impl Resolve<ExecuteArgs> for DestroyStack {
) -> serror::Result<Update> {
execute_compose::<DestroyStack>(
&self.stack,
self.services,
self.service,
user,
|state| state.destroying = true,
update.clone(),
+21 -3
View File
@@ -25,13 +25,16 @@ use komodo_client::{
},
};
use mongo_indexed::doc;
use mungos::{by_id::update_one_by_id, mongodb::bson::oid::ObjectId};
use mungos::{
by_id::update_one_by_id,
mongodb::bson::{oid::ObjectId, to_document},
};
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
helpers::{query::get_id_to_tags, update::update_update},
resource,
resource::{self, refresh_resource_sync_state_cache},
state::{action_states, db_client},
sync::{
deploy::{
@@ -58,7 +61,7 @@ impl Resolve<ExecuteArgs> for RunSync {
} = self;
let sync = resource::get_check_permissions::<
entities::sync::ResourceSync,
>(&sync, user, PermissionLevel::Execute)
>(&sync, &user, PermissionLevel::Execute)
.await?;
// get the action state for the sync (or insert default).
@@ -606,6 +609,21 @@ impl Resolve<ExecuteArgs> for RunSync {
}
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.updates,
&update.id,
mungos::update::Update::Set(update_doc),
None,
)
.await;
refresh_resource_sync_state_cache().await;
}
update_update(update.clone()).await?;
Ok(update)
+4 -4
View File
@@ -45,7 +45,7 @@ impl Resolve<ReadArgs> for ListActions {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Action>(self.query, user, &all_tags)
resource::list_for_user::<Action>(self.query, &user, &all_tags)
.await?,
)
}
@@ -63,7 +63,7 @@ impl Resolve<ReadArgs> for ListFullActions {
};
Ok(
resource::list_full_for_user::<Action>(
self.query, user, &all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -77,7 +77,7 @@ impl Resolve<ReadArgs> for GetActionActionState {
) -> serror::Result<ActionActionState> {
let action = resource::get_check_permissions::<Action>(
&self.action,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -98,7 +98,7 @@ impl Resolve<ReadArgs> for GetActionsSummary {
) -> serror::Result<GetActionsSummaryResponse> {
let actions = resource::list_full_for_user::<Action>(
Default::default(),
user,
&user,
&[],
)
.await
+2 -2
View File
@@ -61,7 +61,7 @@ impl Resolve<ReadArgs> for ListFullAlerters {
};
Ok(
resource::list_full_for_user::<Alerter>(
self.query, user, &all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -75,7 +75,7 @@ impl Resolve<ReadArgs> for GetAlertersSummary {
) -> serror::Result<GetAlertersSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
Alerter,
>(user)
>(&user)
.await?
{
Some(ids) => doc! {
+1 -1
View File
@@ -75,7 +75,7 @@ impl Resolve<ReadArgs> for GetBuildersSummary {
) -> serror::Result<GetBuildersSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
Builder,
>(user)
>(&user)
.await?
{
Some(ids) => doc! {
+1 -1
View File
@@ -281,7 +281,7 @@ impl Resolve<ReadArgs> for ListCommonDeploymentExtraArgs {
get_all_tags(None).await?
};
let deployments = resource::list_full_for_user::<Deployment>(
self.query, user, &all_tags,
self.query, &user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
+5 -5
View File
@@ -167,8 +167,8 @@ enum ReadRequest {
GetStack(GetStack),
GetStackActionState(GetStackActionState),
GetStackWebhooksEnabled(GetStackWebhooksEnabled),
GetStackLog(GetStackLog),
SearchStackLog(SearchStackLog),
GetStackServiceLog(GetStackServiceLog),
SearchStackServiceLog(SearchStackServiceLog),
ListStacks(ListStacks),
ListFullStacks(ListFullStacks),
ListStackServices(ListStackServices),
@@ -381,17 +381,17 @@ impl Resolve<ReadArgs> for ListGitProvidersFromConfig {
let (builds, repos, syncs) = tokio::try_join!(
resource::list_full_for_user::<Build>(
Default::default(),
user,
&user,
&[]
),
resource::list_full_for_user::<Repo>(
Default::default(),
user,
&user,
&[]
),
resource::list_full_for_user::<ResourceSync>(
Default::default(),
user,
&user,
&[]
),
)?;
+1 -1
View File
@@ -63,7 +63,7 @@ impl Resolve<ReadArgs> for ListFullProcedures {
};
Ok(
resource::list_full_for_user::<Procedure>(
self.query, user, &all_tags,
self.query, &user, &all_tags,
)
.await?,
)
+2 -2
View File
@@ -45,7 +45,7 @@ impl Resolve<ReadArgs> for ListRepos {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Repo>(self.query, user, &all_tags)
resource::list_for_user::<Repo>(self.query, &user, &all_tags)
.await?,
)
}
@@ -63,7 +63,7 @@ impl Resolve<ReadArgs> for ListFullRepos {
};
Ok(
resource::list_full_for_user::<Repo>(
self.query, user, &all_tags,
self.query, &user, &all_tags,
)
.await?,
)
+4 -4
View File
@@ -128,7 +128,7 @@ impl Resolve<ReadArgs> for ListServers {
get_all_tags(None).await?
};
Ok(
resource::list_for_user::<Server>(self.query, user, &all_tags)
resource::list_for_user::<Server>(self.query, &user, &all_tags)
.await?,
)
}
@@ -146,7 +146,7 @@ impl Resolve<ReadArgs> for ListFullServers {
};
Ok(
resource::list_full_for_user::<Server>(
self.query, user, &all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -387,7 +387,7 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
) -> serror::Result<ListAllDockerContainersResponse> {
let servers = resource::list_for_user::<Server>(
Default::default(),
user,
&user,
&[],
)
.await?
@@ -533,7 +533,7 @@ impl Resolve<ReadArgs> for GetResourceMatchingContainer {
let stacks =
resource::list_full_for_user_using_document::<Stack>(
doc! { "config.server_id": &server.id },
user,
&user,
)
.await?;
+1 -1
View File
@@ -76,7 +76,7 @@ impl Resolve<ReadArgs> for GetServerTemplatesSummary {
) -> serror::Result<GetServerTemplatesSummaryResponse> {
let query = match resource::get_resource_object_ids_for_user::<
ServerTemplate,
>(user)
>(&user)
.await?
{
Some(ids) => doc! {
+17 -17
View File
@@ -10,7 +10,7 @@ use komodo_client::{
},
};
use periphery_client::api::compose::{
GetComposeLog, GetComposeLogSearch,
GetComposeServiceLog, GetComposeServiceLogSearch,
};
use resolver_api::Resolve;
@@ -64,14 +64,14 @@ impl Resolve<ReadArgs> for ListStackServices {
}
}
impl Resolve<ReadArgs> for GetStackLog {
impl Resolve<ReadArgs> for GetStackServiceLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetStackLogResponse> {
let GetStackLog {
) -> serror::Result<GetStackServiceLogResponse> {
let GetStackServiceLog {
stack,
services,
service,
tail,
timestamps,
} = self;
@@ -79,26 +79,26 @@ impl Resolve<ReadArgs> for GetStackLog {
get_stack_and_server(&stack, user, PermissionLevel::Read, true)
.await?;
let res = periphery_client(&server)?
.request(GetComposeLog {
.request(GetComposeServiceLog {
project: stack.project_name(false),
services,
service,
tail,
timestamps,
})
.await
.context("Failed to get stack log from periphery")?;
.context("failed to get stack service log from periphery")?;
Ok(res)
}
}
impl Resolve<ReadArgs> for SearchStackLog {
impl Resolve<ReadArgs> for SearchStackServiceLog {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<SearchStackLogResponse> {
let SearchStackLog {
) -> serror::Result<SearchStackServiceLogResponse> {
let SearchStackServiceLog {
stack,
services,
service,
terms,
combinator,
invert,
@@ -108,16 +108,16 @@ impl Resolve<ReadArgs> for SearchStackLog {
get_stack_and_server(&stack, user, PermissionLevel::Read, true)
.await?;
let res = periphery_client(&server)?
.request(GetComposeLogSearch {
.request(GetComposeServiceLogSearch {
project: stack.project_name(false),
services,
service,
terms,
combinator,
invert,
timestamps,
})
.await
.context("Failed to search stack log from periphery")?;
.context("failed to get stack service log from periphery")?;
Ok(res)
}
}
@@ -133,7 +133,7 @@ impl Resolve<ReadArgs> for ListCommonStackExtraArgs {
get_all_tags(None).await?
};
let stacks = resource::list_full_for_user::<Stack>(
self.query, user, &all_tags,
self.query, &user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
@@ -164,7 +164,7 @@ impl Resolve<ReadArgs> for ListCommonStackBuildExtraArgs {
get_all_tags(None).await?
};
let stacks = resource::list_full_for_user::<Stack>(
self.query, user, &all_tags,
self.query, &user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
+29 -13
View File
@@ -6,6 +6,7 @@ use komodo_client::{
permission::PermissionLevel,
sync::{
ResourceSync, ResourceSyncActionState, ResourceSyncListItem,
ResourceSyncState,
},
},
};
@@ -15,7 +16,7 @@ use crate::{
config::core_config,
helpers::query::get_all_tags,
resource,
state::{action_states, github_client},
state::{action_states, github_client, resource_sync_state_cache},
};
use super::ReadArgs;
@@ -28,7 +29,7 @@ impl Resolve<ReadArgs> for GetResourceSync {
Ok(
resource::get_check_permissions::<ResourceSync>(
&self.sync,
user,
&user,
PermissionLevel::Read,
)
.await?,
@@ -48,7 +49,7 @@ impl Resolve<ReadArgs> for ListResourceSyncs {
};
Ok(
resource::list_for_user::<ResourceSync>(
self.query, user, &all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -67,7 +68,7 @@ impl Resolve<ReadArgs> for ListFullResourceSyncs {
};
Ok(
resource::list_full_for_user::<ResourceSync>(
self.query, user, &all_tags,
self.query, &user, &all_tags,
)
.await?,
)
@@ -111,6 +112,7 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
let mut res = GetResourceSyncsSummaryResponse::default();
let cache = resource_sync_state_cache();
let action_states = action_states();
for resource_sync in resource_syncs {
@@ -129,15 +131,29 @@ impl Resolve<ReadArgs> for GetResourceSyncsSummary {
res.failed += 1;
continue;
}
if action_states
.resource_sync
.get(&resource_sync.id)
.await
.unwrap_or_default()
.get()?
.syncing
{
res.syncing += 1;
match (
cache.get(&resource_sync.id).await.unwrap_or_default(),
action_states
.resource_sync
.get(&resource_sync.id)
.await
.unwrap_or_default()
.get()?,
) {
(_, action_states) if action_states.syncing => {
res.syncing += 1;
}
(ResourceSyncState::Ok, _) => res.ok += 1,
(ResourceSyncState::Failed, _) => res.failed += 1,
(ResourceSyncState::Unknown, _) => res.unknown += 1,
// will never come off the cache in the building state, since that comes from action states
(ResourceSyncState::Syncing, _) => {
unreachable!()
}
(ResourceSyncState::Pending, _) => {
unreachable!()
}
}
}
+24 -22
View File
@@ -49,7 +49,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_for_user::<Alerter>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -59,7 +59,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_for_user::<Builder>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -69,7 +69,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_for_user::<Server>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -79,7 +79,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_for_user::<Stack>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -89,7 +89,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_for_user::<Deployment>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -99,7 +99,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_for_user::<Build>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -109,7 +109,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_for_user::<Repo>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -119,7 +119,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_for_user::<Procedure>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -129,7 +129,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_for_user::<Action>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -139,7 +139,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_for_user::<ServerTemplate>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -149,7 +149,7 @@ impl Resolve<ReadArgs> for ExportAllResourcesToToml {
targets.extend(
resource::list_full_for_user::<ResourceSync>(
ResourceQuery::builder().tags(self.tags.clone()).build(),
user,
&user,
&all_tags,
)
.await?
@@ -198,7 +198,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
ResourceTarget::Alerter(id) => {
let alerter = resource::get_check_permissions::<Alerter>(
&id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -212,7 +212,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
ResourceTarget::ResourceSync(id) => {
let sync = resource::get_check_permissions::<ResourceSync>(
&id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -231,7 +231,9 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
ResourceTarget::ServerTemplate(id) => {
let template = resource::get_check_permissions::<
ServerTemplate,
>(&id, user, PermissionLevel::Read)
>(
&id, &user, PermissionLevel::Read
)
.await?;
res.server_templates.push(
convert_resource::<ServerTemplate>(
@@ -245,7 +247,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
ResourceTarget::Server(id) => {
let server = resource::get_check_permissions::<Server>(
&id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -260,7 +262,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
let mut builder =
resource::get_check_permissions::<Builder>(
&id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -275,7 +277,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
ResourceTarget::Build(id) => {
let mut build = resource::get_check_permissions::<Build>(
&id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -291,7 +293,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
let mut deployment = resource::get_check_permissions::<
Deployment,
>(
&id, user, PermissionLevel::Read
&id, &user, PermissionLevel::Read
)
.await?;
Deployment::replace_ids(&mut deployment, &all);
@@ -305,7 +307,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
ResourceTarget::Repo(id) => {
let mut repo = resource::get_check_permissions::<Repo>(
&id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -320,7 +322,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
ResourceTarget::Stack(id) => {
let mut stack = resource::get_check_permissions::<Stack>(
&id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -336,7 +338,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
let mut procedure = resource::get_check_permissions::<
Procedure,
>(
&id, user, PermissionLevel::Read
&id, &user, PermissionLevel::Read
)
.await?;
Procedure::replace_ids(&mut procedure, &all);
@@ -350,7 +352,7 @@ impl Resolve<ReadArgs> for ExportResourcesToToml {
ResourceTarget::Action(id) => {
let mut action = resource::get_check_permissions::<Action>(
&id,
user,
&user,
PermissionLevel::Read,
)
.await?;
+22 -22
View File
@@ -43,7 +43,7 @@ impl Resolve<ReadArgs> for ListUpdates {
self.query
} else {
let server_query =
resource::get_resource_ids_for_user::<Server>(user)
resource::get_resource_ids_for_user::<Server>(&user)
.await?
.map(|ids| {
doc! {
@@ -53,7 +53,7 @@ impl Resolve<ReadArgs> for ListUpdates {
.unwrap_or_else(|| doc! { "target.type": "Server" });
let deployment_query =
resource::get_resource_ids_for_user::<Deployment>(user)
resource::get_resource_ids_for_user::<Deployment>(&user)
.await?
.map(|ids| {
doc! {
@@ -63,7 +63,7 @@ impl Resolve<ReadArgs> for ListUpdates {
.unwrap_or_else(|| doc! { "target.type": "Deployment" });
let stack_query =
resource::get_resource_ids_for_user::<Stack>(user)
resource::get_resource_ids_for_user::<Stack>(&user)
.await?
.map(|ids| {
doc! {
@@ -73,7 +73,7 @@ impl Resolve<ReadArgs> for ListUpdates {
.unwrap_or_else(|| doc! { "target.type": "Stack" });
let build_query =
resource::get_resource_ids_for_user::<Build>(user)
resource::get_resource_ids_for_user::<Build>(&user)
.await?
.map(|ids| {
doc! {
@@ -83,7 +83,7 @@ impl Resolve<ReadArgs> for ListUpdates {
.unwrap_or_else(|| doc! { "target.type": "Build" });
let repo_query =
resource::get_resource_ids_for_user::<Repo>(user)
resource::get_resource_ids_for_user::<Repo>(&user)
.await?
.map(|ids| {
doc! {
@@ -93,7 +93,7 @@ impl Resolve<ReadArgs> for ListUpdates {
.unwrap_or_else(|| doc! { "target.type": "Repo" });
let procedure_query =
resource::get_resource_ids_for_user::<Procedure>(user)
resource::get_resource_ids_for_user::<Procedure>(&user)
.await?
.map(|ids| {
doc! {
@@ -103,7 +103,7 @@ impl Resolve<ReadArgs> for ListUpdates {
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
let action_query =
resource::get_resource_ids_for_user::<Action>(user)
resource::get_resource_ids_for_user::<Action>(&user)
.await?
.map(|ids| {
doc! {
@@ -113,7 +113,7 @@ impl Resolve<ReadArgs> for ListUpdates {
.unwrap_or_else(|| doc! { "target.type": "Action" });
let builder_query =
resource::get_resource_ids_for_user::<Builder>(user)
resource::get_resource_ids_for_user::<Builder>(&user)
.await?
.map(|ids| {
doc! {
@@ -123,7 +123,7 @@ impl Resolve<ReadArgs> for ListUpdates {
.unwrap_or_else(|| doc! { "target.type": "Builder" });
let alerter_query =
resource::get_resource_ids_for_user::<Alerter>(user)
resource::get_resource_ids_for_user::<Alerter>(&user)
.await?
.map(|ids| {
doc! {
@@ -133,7 +133,7 @@ impl Resolve<ReadArgs> for ListUpdates {
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
let server_template_query =
resource::get_resource_ids_for_user::<ServerTemplate>(user)
resource::get_resource_ids_for_user::<ServerTemplate>(&user)
.await?
.map(|ids| {
doc! {
@@ -144,7 +144,7 @@ impl Resolve<ReadArgs> for ListUpdates {
let resource_sync_query =
resource::get_resource_ids_for_user::<ResourceSync>(
user,
&user,
)
.await?
.map(|ids| {
@@ -247,7 +247,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::Server(id) => {
resource::get_check_permissions::<Server>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -255,7 +255,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::Deployment(id) => {
resource::get_check_permissions::<Deployment>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -263,7 +263,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::Build(id) => {
resource::get_check_permissions::<Build>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -271,7 +271,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::Repo(id) => {
resource::get_check_permissions::<Repo>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -279,7 +279,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::Builder(id) => {
resource::get_check_permissions::<Builder>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -287,7 +287,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::Alerter(id) => {
resource::get_check_permissions::<Alerter>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -295,7 +295,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::Procedure(id) => {
resource::get_check_permissions::<Procedure>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -303,7 +303,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::Action(id) => {
resource::get_check_permissions::<Action>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -311,7 +311,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -319,7 +319,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::ResourceSync(id) => {
resource::get_check_permissions::<ResourceSync>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
@@ -327,7 +327,7 @@ impl Resolve<ReadArgs> for GetUpdate {
ResourceTarget::Stack(id) => {
resource::get_check_permissions::<Stack>(
id,
user,
&user,
PermissionLevel::Read,
)
.await?;
+2 -2
View File
@@ -32,12 +32,12 @@ impl Resolve<WriteArgs> for CopyAction {
let Action { config, .. } =
resource::get_check_permissions::<Action>(
&self.id,
user,
&user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Action>(&self.name, config.into(), user)
resource::create::<Action>(&self.name, config.into(), &user)
.await?,
)
}
+1 -1
View File
@@ -32,7 +32,7 @@ impl Resolve<WriteArgs> for CopyAlerter {
let Alerter { config, .. } =
resource::get_check_permissions::<Alerter>(
&self.id,
user,
&user,
PermissionLevel::Write,
)
.await?;
+1 -1
View File
@@ -308,7 +308,7 @@ impl Resolve<WriteArgs> for DeleteBuildWebhook {
let build = resource::get_check_permissions::<Build>(
&self.build,
user,
&user,
PermissionLevel::Write,
)
.await?;
+2 -2
View File
@@ -37,7 +37,7 @@ impl Resolve<WriteArgs> for CopyBuilder {
)
.await?;
Ok(
resource::create::<Builder>(&self.name, config.into(), user)
resource::create::<Builder>(&self.name, config.into(), &user)
.await?,
)
}
@@ -73,7 +73,7 @@ impl Resolve<WriteArgs> for RenameBuilder {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Update> {
Ok(
resource::rename::<Builder>(&self.id, &self.name, user)
resource::rename::<Builder>(&self.id, &self.name, &user)
.await?,
)
}
+4 -4
View File
@@ -61,7 +61,7 @@ impl Resolve<WriteArgs> for CopyDeployment {
resource::create::<Deployment>(
&self.name,
config.into(),
user,
&user,
)
.await?,
)
@@ -157,7 +157,7 @@ impl Resolve<WriteArgs> for CreateDeploymentFromContainer {
}
Ok(
resource::create::<Deployment>(&self.name, config, user)
resource::create::<Deployment>(&self.name, config, &user)
.await?,
)
}
@@ -180,7 +180,7 @@ impl Resolve<WriteArgs> for UpdateDeployment {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Deployment> {
Ok(
resource::update::<Deployment>(&self.id, self.config, user)
resource::update::<Deployment>(&self.id, self.config, &user)
.await?,
)
}
@@ -224,7 +224,7 @@ impl Resolve<WriteArgs> for RenameDeployment {
}
let mut update =
make_update(&deployment, Operation::RenameDeployment, user);
make_update(&deployment, Operation::RenameDeployment, &user);
update_one_by_id(
&db_client().deployments,
+11 -11
View File
@@ -33,7 +33,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Server>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -41,7 +41,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Deployment>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -49,7 +49,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Build>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -57,7 +57,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Repo>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -65,7 +65,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Builder>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -73,7 +73,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Alerter>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -81,7 +81,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Procedure>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -89,7 +89,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Action>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -97,7 +97,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<ServerTemplate>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -105,7 +105,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<ResourceSync>(
&id,
&self.description,
user,
&user,
)
.await?;
}
@@ -113,7 +113,7 @@ impl Resolve<WriteArgs> for UpdateDescription {
resource::update_description::<Stack>(
&id,
&self.description,
user,
&user,
)
.await?;
}
+1 -1
View File
@@ -32,7 +32,7 @@ impl Resolve<WriteArgs> for CopyProcedure {
let Procedure { config, .. } =
resource::get_check_permissions::<Procedure>(
&self.id,
user,
&user,
PermissionLevel::Write,
)
.await?;
+8 -8
View File
@@ -44,7 +44,7 @@ impl Resolve<WriteArgs> for CreateGitProviderAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateGitProviderAccount,
user,
&user,
);
account.id = db_client()
@@ -114,7 +114,7 @@ impl Resolve<WriteArgs> for UpdateGitProviderAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateGitProviderAccount,
user,
&user,
);
let account = to_document(&self.account).context(
@@ -173,7 +173,7 @@ impl Resolve<WriteArgs> for DeleteGitProviderAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateGitProviderAccount,
user,
&user,
);
let db = db_client();
@@ -235,7 +235,7 @@ impl Resolve<WriteArgs> for CreateDockerRegistryAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateDockerRegistryAccount,
user,
&user,
);
account.id = db_client()
@@ -298,8 +298,8 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
if username.is_empty() {
return Err(
anyhow!(
"cannot update docker registry account with empty username"
)
"cannot update docker registry account with empty username"
)
.into(),
);
}
@@ -310,7 +310,7 @@ impl Resolve<WriteArgs> for UpdateDockerRegistryAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateDockerRegistryAccount,
user,
&user,
);
let account = to_document(&self.account).context(
@@ -373,7 +373,7 @@ impl Resolve<WriteArgs> for DeleteDockerRegistryAccount {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateDockerRegistryAccount,
user,
&user,
);
let db = db_client();
+6 -6
View File
@@ -53,12 +53,12 @@ impl Resolve<WriteArgs> for CopyRepo {
let Repo { config, .. } =
resource::get_check_permissions::<Repo>(
&self.id,
user,
&user,
PermissionLevel::Write,
)
.await?;
Ok(
resource::create::<Repo>(&self.name, config.into(), user)
resource::create::<Repo>(&self.name, config.into(), &user)
.await?,
)
}
@@ -89,7 +89,7 @@ impl Resolve<WriteArgs> for RenameRepo {
) -> serror::Result<Update> {
let repo = resource::get_check_permissions::<Repo>(
&self.id,
user,
&user,
PermissionLevel::Write,
)
.await?;
@@ -98,7 +98,7 @@ impl Resolve<WriteArgs> for RenameRepo {
|| !repo.config.path.is_empty()
{
return Ok(
resource::rename::<Repo>(&repo.id, &self.name, user).await?,
resource::rename::<Repo>(&repo.id, &self.name, &user).await?,
);
}
@@ -113,7 +113,7 @@ impl Resolve<WriteArgs> for RenameRepo {
let name = to_komodo_name(&self.name);
let mut update = make_update(&repo, Operation::RenameRepo, user);
let mut update = make_update(&repo, Operation::RenameRepo, &user);
update_one_by_id(
&db_client().repos,
@@ -171,7 +171,7 @@ impl Resolve<WriteArgs> for RefreshRepoCache {
// repo should be able to do this.
let repo = resource::get_check_permissions::<Repo>(
&self.repo,
user,
&user,
PermissionLevel::Execute,
)
.await?;
+1 -1
View File
@@ -77,7 +77,7 @@ impl Resolve<WriteArgs> for CreateNetwork {
let periphery = periphery_client(&server)?;
let mut update =
make_update(&server, Operation::CreateNetwork, user);
make_update(&server, Operation::CreateNetwork, &user);
update.status = UpdateStatus::InProgress;
update.id = add_update(update.clone()).await?;
+3 -3
View File
@@ -24,7 +24,7 @@ impl Resolve<WriteArgs> for CreateServerTemplate {
resource::create::<ServerTemplate>(
&self.name,
self.config,
user,
&user,
)
.await?,
)
@@ -40,7 +40,7 @@ impl Resolve<WriteArgs> for CopyServerTemplate {
let ServerTemplate { config, .. } =
resource::get_check_permissions::<ServerTemplate>(
&self.id,
user,
&user,
PermissionLevel::Write,
)
.await?;
@@ -48,7 +48,7 @@ impl Resolve<WriteArgs> for CopyServerTemplate {
resource::create::<ServerTemplate>(
&self.name,
config.into(),
user,
&user,
)
.await?,
)
+5 -7
View File
@@ -102,7 +102,6 @@ impl Resolve<WriteArgs> for RenameStack {
}
impl Resolve<WriteArgs> for WriteStackFileContents {
#[instrument(name = "WriteStackFileContents", skip(user))]
async fn resolve(
self,
WriteArgs { user }: &WriteArgs,
@@ -114,7 +113,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
} = self;
let (mut stack, server) = get_stack_and_server(
&stack,
user,
&user,
PermissionLevel::Write,
true,
)
@@ -127,7 +126,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
}
let mut update =
make_update(&stack, Operation::WriteStackContents, user);
make_update(&stack, Operation::WriteStackContents, &user);
update.push_simple_log("File contents to write", &contents);
@@ -149,7 +148,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
}
Err(e) => {
update.push_error_log(
"Write File Contents",
"Write file contents",
format_serror(&e.into()),
);
}
@@ -187,7 +186,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
}
Err(e) => {
update.push_error_log(
"Write File Contents",
"Write file contents",
format_serror(&e.into()),
);
}
@@ -372,7 +371,6 @@ impl Resolve<WriteArgs> for RefreshStackCache {
deployed_services: stack.info.deployed_services.clone(),
deployed_project_name: stack.info.deployed_project_name.clone(),
deployed_contents: stack.info.deployed_contents.clone(),
deployed_config: stack.info.deployed_config.clone(),
deployed_hash: stack.info.deployed_hash.clone(),
deployed_message: stack.info.deployed_message.clone(),
latest_services,
@@ -402,7 +400,7 @@ impl Resolve<WriteArgs> for RefreshStackCache {
if state == ServerState::Ok {
let name = stack.name.clone();
if let Err(e) =
pull_stack_inner(stack, Vec::new(), &server, None).await
pull_stack_inner(stack, None, &server, None).await
{
warn!(
"Failed to pull latest images for Stack {name} | {e:#}",
+123 -196
View File
@@ -45,11 +45,10 @@ use crate::{
api::read::ReadArgs,
config::core_config,
helpers::{
git_token,
query::get_id_to_tags,
update::{add_update, make_update, update_update},
},
resource,
resource::{self, refresh_resource_sync_state_cache},
state::{db_client, github_client},
sync::{
deploy::SyncDeployParams, remote::RemoteResources,
@@ -66,8 +65,12 @@ impl Resolve<WriteArgs> for CreateResourceSync {
WriteArgs { user }: &WriteArgs,
) -> serror::Result<ResourceSync> {
Ok(
resource::create::<ResourceSync>(&self.name, self.config, user)
.await?,
resource::create::<ResourceSync>(
&self.name,
self.config,
&user,
)
.await?,
)
}
}
@@ -81,7 +84,7 @@ impl Resolve<WriteArgs> for CopyResourceSync {
let ResourceSync { config, .. } =
resource::get_check_permissions::<ResourceSync>(
&self.id,
user,
&user,
PermissionLevel::Write,
)
.await?;
@@ -132,184 +135,19 @@ impl Resolve<WriteArgs> for RenameResourceSync {
}
}
async fn write_sync_file_contents_on_host(
req: WriteSyncFileContents,
args: &WriteArgs,
sync: ResourceSync,
mut update: Update,
) -> serror::Result<Update> {
let WriteSyncFileContents {
sync: _,
resource_path,
file_path,
contents,
} = req;
let root = core_config()
.sync_directory
.join(to_komodo_name(&sync.name));
let file_path =
file_path.parse::<PathBuf>().context("Invalid file path")?;
let resource_path = resource_path
.parse::<PathBuf>()
.context("Invalid resource path")?;
let full_path = root.join(&resource_path).join(&file_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to initialize resource file parent directory {parent:?}"
)
})?;
}
if let Err(e) =
fs::write(&full_path, &contents).await.with_context(|| {
format!("Failed to write resource 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 let Err(e) = (RefreshResourceSyncPending { sync: sync.name })
.resolve(args)
.await
{
update.push_error_log(
"Refresh failed",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
async fn write_sync_file_contents_git(
req: WriteSyncFileContents,
args: &WriteArgs,
sync: ResourceSync,
mut update: Update,
) -> serror::Result<Update> {
let WriteSyncFileContents {
sync: _,
resource_path,
file_path,
contents,
} = req;
let mut clone_args: CloneArgs = (&sync).into();
let root = clone_args.unique_path(&core_config().repo_directory)?;
let file_path =
file_path.parse::<PathBuf>().context("Invalid file path")?;
let resource_path = resource_path
.parse::<PathBuf>()
.context("Invalid resource path")?;
let full_path = root.join(&resource_path).join(&file_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to initialize resource file parent directory {parent:?}"
)
})?;
}
// Ensure the folder is initialized as git repo.
// This allows a new file to be committed on a branch that may not exist.
if !root.join(".git").exists() {
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),
)?
} else {
None
};
git::init_folder_as_repo(
&root,
&clone_args,
access_token.as_deref(),
&mut update.logs,
)
.await;
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
}
if let Err(e) =
fs::write(&full_path, &contents).await.with_context(|| {
format!("Failed to write resource 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);
}
let commit_res = git::commit_file(
&format!("{}: Commit Resource File", args.user.username),
&root,
&resource_path.join(&file_path),
&sync.config.branch,
)
.await;
update.logs.extend(commit_res.logs);
if let Err(e) = (RefreshResourceSyncPending { sync: sync.name })
.resolve(args)
.await
{
update.push_error_log(
"Refresh failed",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
impl Resolve<WriteArgs> for WriteSyncFileContents {
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let WriteSyncFileContents {
sync,
resource_path,
file_path,
contents,
} = self;
let WriteArgs { user } = args;
let sync = resource::get_check_permissions::<ResourceSync>(
&self.sync,
&args.user,
&sync,
user,
PermissionLevel::Write,
)
.await?;
@@ -317,22 +155,96 @@ impl Resolve<WriteArgs> for WriteSyncFileContents {
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."
)
"This method is only for files on host, or repo based syncs."
)
.into(),
);
}
let mut update =
make_update(&sync, Operation::WriteSyncContents, &args.user);
make_update(&sync, Operation::WriteSyncContents, &user);
update.push_simple_log("File contents", &self.contents);
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::<PathBuf>().context("Invalid file path")?;
let resource_path = resource_path
.parse::<PathBuf>()
.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 {
write_sync_file_contents_on_host(self, args, sync, update).await
} else {
write_sync_file_contents_git(self, args, sync, update).await
if let Err(e) = (RefreshResourceSyncPending { sync: sync.name })
.resolve(args)
.await
{
update.push_error_log(
"Refresh failed",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
let commit_res = git::commit_file(
&format!("{}: Commit Resource File", user.username),
&root,
&resource_path.join(&file_path),
)
.await;
update.logs.extend(commit_res.logs);
if let Err(e) = (RefreshResourceSyncPending { sync: sync.name })
.resolve(args)
.await
{
update.push_error_log(
"Refresh failed",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
}
@@ -343,7 +255,7 @@ impl Resolve<WriteArgs> for CommitSync {
let sync = resource::get_check_permissions::<
entities::sync::ResourceSync,
>(&self.sync, user, PermissionLevel::Write)
>(&self.sync, &user, PermissionLevel::Write)
.await?;
let file_contents_empty = sync.config.file_contents_empty();
@@ -392,7 +304,7 @@ impl Resolve<WriteArgs> for CommitSync {
})
.await?;
let mut update = make_update(&sync, Operation::CommitSync, user);
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()));
@@ -407,9 +319,7 @@ impl Resolve<WriteArgs> for CommitSync {
.join(to_komodo_name(&sync.name))
.join(&resource_path);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize resource file parent directory {parent:?}"))?;
let _ = tokio::fs::create_dir_all(&parent).await;
};
if let Err(e) = tokio::fs::write(&file_path, &res.toml)
.await
@@ -443,7 +353,6 @@ impl Resolve<WriteArgs> for CommitSync {
&root,
&resource_path,
&res.toml,
&sync.config.branch,
)
.await
{
@@ -489,6 +398,21 @@ impl Resolve<WriteArgs> for CommitSync {
};
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.clone()).await?;
Ok(update)
@@ -539,9 +463,12 @@ impl Resolve<WriteArgs> for RefreshResourceSyncPending {
sync.info.pending_message = message;
if !sync.info.remote_errors.is_empty() {
return Err(anyhow!(
"Remote resources have errors. Cannot compute diffs."
));
return Err(
anyhow!(
"Remote resources have errors. Cannot compute diffs."
)
.into(),
);
}
let resources = resources?;
+2 -2
View File
@@ -68,7 +68,7 @@ impl Resolve<WriteArgs> for RenameTag {
return Err(anyhow!("tag name cannot be ObjectId").into());
}
get_tag_check_owner(&self.id, user).await?;
get_tag_check_owner(&self.id, &user).await?;
update_one_by_id(
&db_client().tags,
@@ -89,7 +89,7 @@ impl Resolve<WriteArgs> for DeleteTag {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<Tag> {
let tag = get_tag_check_owner(&self.id, user).await?;
let tag = get_tag_check_owner(&self.id, &user).await?;
tokio::try_join!(
resource::remove_tag_from_all::<Server>(&self.id),
+1 -23
View File
@@ -12,9 +12,7 @@ use komodo_client::{
use mungos::mongodb::bson::{doc, oid::ObjectId};
use resolver_api::Resolve;
use crate::{
config::core_config, helpers::hash_password, state::db_client,
};
use crate::{helpers::hash_password, state::db_client};
use super::WriteArgs;
@@ -25,16 +23,6 @@ impl Resolve<WriteArgs> for UpdateUserUsername {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateUserUsernameResponse> {
for locked_username in &core_config().lock_login_credentials_for {
if locked_username == "__ALL__"
|| *locked_username == user.username
{
return Err(
anyhow!("User not allowed to update their username.")
.into(),
);
}
}
if self.username.is_empty() {
return Err(anyhow!("Username cannot be empty.").into());
}
@@ -68,16 +56,6 @@ impl Resolve<WriteArgs> for UpdateUserPassword {
self,
WriteArgs { user }: &WriteArgs,
) -> serror::Result<UpdateUserPasswordResponse> {
for locked_username in &core_config().lock_login_credentials_for {
if locked_username == "__ALL__"
|| *locked_username == user.username
{
return Err(
anyhow!("User not allowed to update their password.")
.into(),
);
}
}
let UserConfig::Local { .. } = user.config else {
return Err(anyhow!("User is not local user").into());
};
+2 -2
View File
@@ -49,7 +49,7 @@ impl Resolve<WriteArgs> for CreateVariable {
let mut update = make_update(
ResourceTarget::system(),
Operation::CreateVariable,
user,
&user,
);
update
@@ -92,7 +92,7 @@ impl Resolve<WriteArgs> for UpdateVariableValue {
let mut update = make_update(
ResourceTarget::system(),
Operation::UpdateVariableValue,
user,
&user,
);
let log = if variable.is_secret {
+6 -26
View File
@@ -2,37 +2,17 @@ use std::sync::OnceLock;
use anyhow::Context;
use openidconnect::{
core::*, Client, ClientId, ClientSecret, EmptyAdditionalClaims,
EndpointMaybeSet, EndpointNotSet, EndpointSet, IssuerUrl,
RedirectUrl, StandardErrorResponse,
core::{CoreClient, CoreProviderMetadata},
reqwest::async_http_client,
ClientId, ClientSecret, IssuerUrl, RedirectUrl,
};
use crate::config::core_config;
type OidcClient = Client<
EmptyAdditionalClaims,
CoreAuthDisplay,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJsonWebKey,
CoreAuthPrompt,
StandardErrorResponse<CoreErrorResponseType>,
CoreTokenResponse,
CoreTokenIntrospectionResponse,
CoreRevocableToken,
CoreRevocationErrorResponse,
EndpointSet,
EndpointNotSet,
EndpointNotSet,
EndpointNotSet,
EndpointMaybeSet,
EndpointMaybeSet,
>;
static DEFAULT_OIDC_CLIENT: OnceLock<Option<OidcClient>> =
static DEFAULT_OIDC_CLIENT: OnceLock<Option<CoreClient>> =
OnceLock::new();
pub fn default_oidc_client() -> Option<&'static OidcClient> {
pub fn default_oidc_client() -> Option<&'static CoreClient> {
DEFAULT_OIDC_CLIENT
.get()
.expect("OIDC client get before init")
@@ -55,7 +35,7 @@ pub async fn init_default_oidc_client() {
// Use OpenID Connect Discovery to fetch the provider metadata.
let provider_metadata = CoreProviderMetadata::discover_async(
IssuerUrl::new(config.oidc_provider.clone())?,
super::reqwest_client(),
async_http_client,
)
.await
.context(
+36 -19
View File
@@ -12,8 +12,9 @@ use komodo_client::entities::{
};
use mungos::mongodb::bson::{doc, Document};
use openidconnect::{
core::CoreAuthenticationFlow, AuthorizationCode, CsrfToken, Nonce,
Scope, TokenResponse,
core::CoreAuthenticationFlow, AccessTokenHash, AuthorizationCode,
CsrfToken, Nonce, OAuth2TokenResponse, PkceCodeChallenge,
PkceCodeVerifier, Scope, TokenResponse,
};
use reqwest::StatusCode;
use serde::Deserialize;
@@ -28,22 +29,13 @@ use super::RedirectQuery;
pub mod client;
fn reqwest_client() -> &'static reqwest::Client {
static REQWEST: OnceLock<reqwest::Client> = OnceLock::new();
REQWEST.get_or_init(|| {
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.expect("Invalid OIDC reqwest client")
})
}
/// CSRF tokens can only be used once from the callback,
/// and must be used within this timeframe
const CSRF_VALID_FOR_MS: i64 = 120_000; // 2 minutes for user to log in.
type RedirectUrl = Option<String>;
type CsrfMap = DashMap<String, (Nonce, RedirectUrl, i64)>;
type CsrfMap =
DashMap<String, (PkceCodeVerifier, Nonce, RedirectUrl, i64)>;
fn csrf_verifier_tokens() -> &'static CsrfMap {
static CSRF: OnceLock<CsrfMap> = OnceLock::new();
CSRF.get_or_init(Default::default)
@@ -72,6 +64,10 @@ async fn login(
let client =
default_oidc_client().context("OIDC Client not configured")?;
// Generate a PKCE challenge.
let (pkce_challenge, pkce_verifier) =
PkceCodeChallenge::new_random_sha256();
// Generate the authorization URL.
let (auth_url, csrf_token, nonce) = client
.authorize_url(
@@ -81,12 +77,18 @@ async fn login(
)
.add_scope(Scope::new("openid".to_string()))
.add_scope(Scope::new("email".to_string()))
.set_pkce_challenge(pkce_challenge)
.url();
// Data inserted here will be matched on callback side for csrf protection.
csrf_verifier_tokens().insert(
csrf_token.secret().clone(),
(nonce, redirect, komodo_timestamp() + CSRF_VALID_FOR_MS),
(
pkce_verifier,
nonce,
redirect,
komodo_timestamp() + CSRF_VALID_FOR_MS,
),
);
let config = core_config();
@@ -133,9 +135,10 @@ async fn callback(
query.state.context("Provider did not return state")?,
);
let (_, (nonce, redirect, valid_until)) = csrf_verifier_tokens()
.remove(state.secret())
.context("CSRF Token invalid")?;
let (_, (pkce_verifier, nonce, redirect, valid_until)) =
csrf_verifier_tokens()
.remove(state.secret())
.context("CSRF Token invalid")?;
if komodo_timestamp() > valid_until {
return Err(anyhow!(
@@ -145,8 +148,9 @@ async fn callback(
let token_response = client
.exchange_code(AuthorizationCode::new(code))
.context("Failed to get Oauth token at exchange code")?
.request_async(reqwest_client())
// Set the PKCE code verifier.
.set_pkce_verifier(pkce_verifier)
.request_async(openidconnect::reqwest::async_http_client)
.await
.context("Failed to get Oauth token")?;
@@ -171,6 +175,19 @@ async fn callback(
.claims(&verifier, &nonce)
.context("Failed to verify token claims")?;
// Verify the access token hash to ensure that the access token hasn't been substituted for
// another user's.
if let Some(expected_access_token_hash) = claims.access_token_hash()
{
let actual_access_token_hash = AccessTokenHash::from_token(
token_response.access_token(),
&id_token.signing_alg()?,
)?;
if actual_access_token_hash != *expected_access_token_hash {
return Err(anyhow!("Invalid access token"));
}
}
let user_id = claims.subject().as_str();
let db_client = db_client();
-2
View File
@@ -182,8 +182,6 @@ pub fn core_config() -> &'static CoreConfig {
.unwrap_or(config.disable_user_registration),
disable_non_admin_create: env.komodo_disable_non_admin_create
.unwrap_or(config.disable_non_admin_create),
lock_login_credentials_for: env.komodo_lock_login_credentials_for
.unwrap_or(config.lock_login_credentials_for),
local_auth: env.komodo_local_auth
.unwrap_or(config.local_auth),
logging: LogConfig {
+2 -2
View File
@@ -84,8 +84,8 @@ pub struct UpdateGuard<'a, States: Default + Send + 'static>(
&'a Mutex<States>,
);
impl<States: Default + Send + 'static> Drop
for UpdateGuard<'_, States>
impl<'a, States: Default + Send + 'static> Drop
for UpdateGuard<'a, States>
{
fn drop(&mut self) {
let mut lock = match self.0.lock() {
+3 -3
View File
@@ -21,7 +21,7 @@ use mungos::{
mongodb::bson::{doc, oid::ObjectId, to_document, Bson},
};
use periphery_client::PeripheryClient;
use rand::Rng;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use resolver_api::Resolve;
use crate::{
@@ -54,8 +54,8 @@ pub fn empty_or_only_spaces(word: &str) -> bool {
}
pub fn random_string(length: usize) -> String {
rand::rng()
.sample_iter(&rand::distr::Alphanumeric)
thread_rng()
.sample_iter(&Alphanumeric)
.take(length)
.map(char::from)
.collect()
+2 -2
View File
@@ -1259,7 +1259,7 @@ impl ExtendBatch for BatchDeployStack {
fn single_execution(stack: String) -> Execution {
Execution::DeployStack(DeployStack {
stack,
services: Vec::new(),
service: None,
stop_time: None,
})
}
@@ -1280,7 +1280,7 @@ impl ExtendBatch for BatchDestroyStack {
fn single_execution(stack: String) -> Execution {
Execution::DestroyStack(DestroyStack {
stack,
services: Vec::new(),
service: None,
remove_orphans: false,
stop_time: None,
})
+2 -3
View File
@@ -34,9 +34,8 @@ async fn prune_images() -> anyhow::Result<()> {
.await
.context("failed to get servers from db")?
.into_iter()
.filter(|server| {
server.config.enabled && server.config.auto_prune
})
// This could be done in the mongo query, but rather have rust type system guarantee this.
.filter(|server| server.config.auto_prune)
.map(|server| async move {
(
async {
+8 -8
View File
@@ -405,7 +405,7 @@ pub async fn init_execution_update(
// Stack
ExecuteRequest::DeployStack(data) => (
if !data.services.is_empty() {
if data.service.is_some() {
Operation::DeployStackService
} else {
Operation::DeployStack
@@ -427,7 +427,7 @@ pub async fn init_execution_update(
return Ok(Default::default())
}
ExecuteRequest::StartStack(data) => (
if !data.services.is_empty() {
if data.service.is_some() {
Operation::StartStackService
} else {
Operation::StartStack
@@ -437,7 +437,7 @@ pub async fn init_execution_update(
),
),
ExecuteRequest::PullStack(data) => (
if !data.services.is_empty() {
if data.service.is_some() {
Operation::PullStackService
} else {
Operation::PullStack
@@ -447,7 +447,7 @@ pub async fn init_execution_update(
),
),
ExecuteRequest::RestartStack(data) => (
if !data.services.is_empty() {
if data.service.is_some() {
Operation::RestartStackService
} else {
Operation::RestartStack
@@ -457,7 +457,7 @@ pub async fn init_execution_update(
),
),
ExecuteRequest::PauseStack(data) => (
if !data.services.is_empty() {
if data.service.is_some() {
Operation::PauseStackService
} else {
Operation::PauseStack
@@ -467,7 +467,7 @@ pub async fn init_execution_update(
),
),
ExecuteRequest::UnpauseStack(data) => (
if !data.services.is_empty() {
if data.service.is_some() {
Operation::UnpauseStackService
} else {
Operation::UnpauseStack
@@ -477,7 +477,7 @@ pub async fn init_execution_update(
),
),
ExecuteRequest::StopStack(data) => (
if !data.services.is_empty() {
if data.service.is_some() {
Operation::StopStackService
} else {
Operation::StopStack
@@ -487,7 +487,7 @@ pub async fn init_execution_update(
),
),
ExecuteRequest::DestroyStack(data) => (
if !data.services.is_empty() {
if data.service.is_some() {
Operation::DestroyStackService
} else {
Operation::DestroyStack
+1 -1
View File
@@ -231,7 +231,7 @@ impl StackExecution for DeployStack {
if stack.config.webhook_force_deploy {
let req = ExecuteRequest::DeployStack(DeployStack {
stack: stack.id,
services: Vec::new(),
service: None,
stop_time: None,
});
let update = init_execution_update(&req, &user).await?;
+1
View File
@@ -59,6 +59,7 @@ async fn app() -> anyhow::Result<()> {
resource::spawn_repo_state_refresh_loop();
resource::spawn_procedure_state_refresh_loop();
resource::spawn_action_state_refresh_loop();
resource::spawn_resource_sync_state_refresh_loop();
helpers::prune::spawn_prune_loop();
// Setup static frontend services
+4 -9
View File
@@ -322,8 +322,8 @@ pub async fn update_stack_cache(
}
}).collect::<Vec<_>>();
let mut update_available = false;
let mut images_with_update = Vec::new();
let mut services_to_update = Vec::new();
for service in services_with_containers.iter() {
if service.update_available {
@@ -336,7 +336,7 @@ pub async fn update_stack_cache(
.map(|c| c.state == ContainerStateStatusEnum::Running)
.unwrap_or_default()
{
services_to_update.push(service.service.clone());
update_available = true
}
}
}
@@ -346,7 +346,7 @@ pub async fn update_stack_cache(
&services,
containers,
);
if !services_to_update.is_empty()
if update_available
&& stack.config.auto_update
&& state == StackState::Running
&& !action_states()
@@ -358,16 +358,11 @@ pub async fn update_stack_cache(
{
let id = stack.id.clone();
let server_name = server_name.clone();
let services = if stack.config.auto_update_all_services {
Vec::new()
} else {
services_to_update
};
tokio::spawn(async move {
match execute::inner_handler(
ExecuteRequest::DeployStack(DeployStack {
stack: stack.name.clone(),
services,
service: None,
stop_time: None,
}),
auto_redeploy_user().to_owned(),
+4
View File
@@ -73,6 +73,10 @@ pub use refresh::spawn_resource_refresh_loop;
pub use repo::{
refresh_repo_state_cache, spawn_repo_state_refresh_loop,
};
pub use sync::{
refresh_resource_sync_state_cache,
spawn_resource_sync_state_refresh_loop,
};
/// Implement on each Komodo resource for common methods
pub trait KomodoResource {
+85 -8
View File
@@ -1,3 +1,5 @@
use std::time::Duration;
use anyhow::Context;
use formatting::format_serror;
use komodo_client::{
@@ -17,12 +19,15 @@ use komodo_client::{
},
};
use mongo_indexed::doc;
use mungos::mongodb::Collection;
use mungos::{
find::find_collect,
mongodb::{options::FindOneOptions, Collection},
};
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
state::{action_states, db_client},
state::{action_states, db_client, resource_sync_state_cache},
};
impl super::KomodoResource for ResourceSync {
@@ -112,6 +117,7 @@ impl super::KomodoResource for ResourceSync {
format_serror(&e.error.context("The sync pending cache has failed to refresh. This is likely due to a misconfiguration of the sync").into())
);
};
refresh_resource_sync_state_cache().await;
Ok(())
}
@@ -174,6 +180,35 @@ impl super::KomodoResource for ResourceSync {
}
}
pub fn spawn_resource_sync_state_refresh_loop() {
tokio::spawn(async move {
loop {
refresh_resource_sync_state_cache().await;
tokio::time::sleep(Duration::from_secs(60)).await;
}
});
}
pub async fn refresh_resource_sync_state_cache() {
let _ = async {
let resource_syncs =
find_collect(&db_client().resource_syncs, None, None)
.await
.context("failed to get resource_syncs from db")?;
let cache = resource_sync_state_cache();
for resource_sync in resource_syncs {
let state =
get_resource_sync_state_from_db(&resource_sync.id).await;
cache.insert(resource_sync.id, state).await;
}
anyhow::Ok(())
}
.await
.inspect_err(|e| {
error!("failed to refresh resource_sync state cache | {e:#}")
});
}
async fn get_resource_sync_state(
id: &String,
data: &ResourceSyncInfo,
@@ -197,15 +232,57 @@ async fn get_resource_sync_state(
{
return state;
}
if data.pending_error.is_some() || !data.remote_errors.is_empty() {
ResourceSyncState::Failed
} else if !data.resource_updates.is_empty()
if data.pending_error.is_some() {
return ResourceSyncState::Failed;
}
if !data.resource_updates.is_empty()
|| !data.variable_updates.is_empty()
|| !data.user_group_updates.is_empty()
|| data.pending_deploy.to_deploy > 0
{
ResourceSyncState::Pending
} else {
ResourceSyncState::Ok
return ResourceSyncState::Pending;
}
resource_sync_state_cache()
.get(id)
.await
.unwrap_or_default()
}
async fn get_resource_sync_state_from_db(
id: &str,
) -> ResourceSyncState {
async {
let state = db_client()
.updates
.find_one(doc! {
"target.type": "ResourceSync",
"target.id": id,
"$or": [
{ "operation": "RunSync" },
{ "operation": "CommitSync" },
],
})
.with_options(
FindOneOptions::builder()
.sort(doc! { "start_ts": -1 })
.build(),
)
.await?
.map(|u| {
if u.success {
ResourceSyncState::Ok
} else {
ResourceSyncState::Failed
}
})
.unwrap_or(ResourceSyncState::Ok);
anyhow::Ok(state)
}
.await
.inspect_err(|e| {
warn!(
"failed to get resource sync state from db for {id} | {e:#}"
)
})
.unwrap_or(ResourceSyncState::Unknown)
}
+36 -51
View File
@@ -23,14 +23,14 @@ pub trait ExecuteCompose {
async fn execute(
periphery: PeripheryClient,
stack: Stack,
services: Vec<String>,
service: Option<String>,
extras: Self::Extras,
) -> anyhow::Result<Log>;
}
pub async fn execute_compose<T: ExecuteCompose>(
stack: &str,
services: Vec<String>,
service: Option<String>,
user: &User,
set_in_progress: impl Fn(&mut StackActionState),
mut update: Update,
@@ -53,19 +53,16 @@ pub async fn execute_compose<T: ExecuteCompose>(
let periphery = periphery_client(&server)?;
if !services.is_empty() {
if let Some(service) = &service {
update.logs.push(Log::simple(
"Service/s",
format!(
"Execution requested for Stack service/s {}",
services.join(", ")
),
&format!("Service: {service}"),
format!("Execution requested for Stack service {service}"),
))
}
update
.logs
.push(T::execute(periphery, stack, services, extras).await?);
.push(T::execute(periphery, stack, service, extras).await?);
// Ensure cached stack state up to date by updating server cache
update_cache_for_server(&server).await;
@@ -81,18 +78,16 @@ impl ExecuteCompose for StartStack {
async fn execute(
periphery: PeripheryClient,
stack: Stack,
services: Vec<String>,
service: Option<String>,
_: Self::Extras,
) -> anyhow::Result<Log> {
let service_args = if !services.is_empty() {
services.join(" ")
} else {
String::new()
};
let service = service
.map(|service| format!(" {service}"))
.unwrap_or_default();
periphery
.request(ComposeExecution {
project: stack.project_name(false),
command: format!("start{service_args}"),
command: format!("start{service}"),
})
.await
}
@@ -103,18 +98,16 @@ impl ExecuteCompose for RestartStack {
async fn execute(
periphery: PeripheryClient,
stack: Stack,
services: Vec<String>,
service: Option<String>,
_: Self::Extras,
) -> anyhow::Result<Log> {
let service_args = if !services.is_empty() {
services.join(" ")
} else {
String::new()
};
let service = service
.map(|service| format!(" {service}"))
.unwrap_or_default();
periphery
.request(ComposeExecution {
project: stack.project_name(false),
command: format!("restart{service_args}"),
command: format!("restart{service}"),
})
.await
}
@@ -125,18 +118,16 @@ impl ExecuteCompose for PauseStack {
async fn execute(
periphery: PeripheryClient,
stack: Stack,
services: Vec<String>,
service: Option<String>,
_: Self::Extras,
) -> anyhow::Result<Log> {
let service_args = if !services.is_empty() {
services.join(" ")
} else {
String::new()
};
let service = service
.map(|service| format!(" {service}"))
.unwrap_or_default();
periphery
.request(ComposeExecution {
project: stack.project_name(false),
command: format!("pause{service_args}"),
command: format!("pause{service}"),
})
.await
}
@@ -147,18 +138,16 @@ impl ExecuteCompose for UnpauseStack {
async fn execute(
periphery: PeripheryClient,
stack: Stack,
services: Vec<String>,
service: Option<String>,
_: Self::Extras,
) -> anyhow::Result<Log> {
let service_args = if !services.is_empty() {
services.join(" ")
} else {
String::new()
};
let service = service
.map(|service| format!(" {service}"))
.unwrap_or_default();
periphery
.request(ComposeExecution {
project: stack.project_name(false),
command: format!("unpause{service_args}"),
command: format!("unpause{service}"),
})
.await
}
@@ -169,19 +158,17 @@ impl ExecuteCompose for StopStack {
async fn execute(
periphery: PeripheryClient,
stack: Stack,
services: Vec<String>,
service: Option<String>,
timeout: Self::Extras,
) -> anyhow::Result<Log> {
let service_args = if !services.is_empty() {
services.join(" ")
} else {
String::new()
};
let service = service
.map(|service| format!(" {service}"))
.unwrap_or_default();
let maybe_timeout = maybe_timeout(timeout);
periphery
.request(ComposeExecution {
project: stack.project_name(false),
command: format!("stop{maybe_timeout}{service_args}"),
command: format!("stop{maybe_timeout}{service}"),
})
.await
}
@@ -192,14 +179,12 @@ impl ExecuteCompose for DestroyStack {
async fn execute(
periphery: PeripheryClient,
stack: Stack,
services: Vec<String>,
service: Option<String>,
(timeout, remove_orphans): Self::Extras,
) -> anyhow::Result<Log> {
let service_args = if !services.is_empty() {
services.join(" ")
} else {
String::new()
};
let service = service
.map(|service| format!(" {service}"))
.unwrap_or_default();
let maybe_timeout = maybe_timeout(timeout);
let maybe_remove_orphans = if remove_orphans {
" --remove-orphans"
@@ -210,7 +195,7 @@ impl ExecuteCompose for DestroyStack {
.request(ComposeExecution {
project: stack.project_name(false),
command: format!(
"down{maybe_timeout}{maybe_remove_orphans}{service_args}"
"down{maybe_timeout}{maybe_remove_orphans}{service}"
),
})
.await
+10
View File
@@ -12,6 +12,7 @@ use komodo_client::entities::{
procedure::ProcedureState,
repo::RepoState,
stack::StackState,
sync::ResourceSyncState,
};
use octorust::auth::{
Credentials, InstallationTokenGenerator, JWTCredentials,
@@ -196,3 +197,12 @@ pub fn action_state_cache() -> &'static ActionStateCache {
OnceLock::new();
ACTION_STATE_CACHE.get_or_init(Default::default)
}
pub type ResourceSyncStateCache = Cache<String, ResourceSyncState>;
pub fn resource_sync_state_cache() -> &'static ResourceSyncStateCache
{
static RESOURCE_SYNC_STATE_CACHE: OnceLock<ResourceSyncStateCache> =
OnceLock::new();
RESOURCE_SYNC_STATE_CACHE.get_or_init(Default::default)
}
+1 -1
View File
@@ -105,7 +105,7 @@ pub async fn deploy_from_cache(
ResourceTarget::Stack(name) => {
let req = ExecuteRequest::DeployStack(DeployStack {
stack: name.to_string(),
services: Vec::new(),
service: None,
stop_time: None,
});
-1
View File
@@ -165,7 +165,6 @@ fn read_resource_file(
}
/// Reads down into directories.
#[allow(clippy::too_many_arguments)]
fn read_resources_directory(
root_path: &Path,
// relative to root path.
+7 -20
View File
@@ -223,26 +223,13 @@ impl AllResourcesById {
fn deserialize_resources_toml(
toml_str: &str,
) -> anyhow::Result<ResourcesToml> {
::toml::from_str::<ResourcesToml>(&escape_between_triple_string(
toml_str,
))
::toml::from_str::<ResourcesToml>(
&toml_str
// Escape the \ for the user on incoming.
// After deserialized by `toml` crate, the contents
// will match what would be passed on eg command line exactly.
.replace(r#"\"#, r#"\\"#),
)
// the error without this comes through with multiple lines (\n) and looks bad
.map_err(|e| anyhow!("{e:#}"))
}
fn escape_between_triple_string(toml_str: &str) -> String {
toml_str
.split(r#"""""#)
.enumerate()
.map(|(i, section)| {
// The odd entries are between triple string,
// and the \ need to be escaped.
if i % 2 == 0 {
section.to_string()
} else {
section.replace(r#"\"#, r#"\\"#)
}
})
.collect::<Vec<_>>()
.join(r#"""""#)
}
+5 -5
View File
@@ -133,9 +133,9 @@ pub async fn get_remote_resources(
format!("Failed to update resource repo at {repo_path:?}")
})?;
// let hash = hash.context("failed to get commit hash")?;
// let message =
// message.context("failed to get commit hash message")?;
let hash = hash.context("failed to get commit hash")?;
let message =
message.context("failed to get commit hash message")?;
let (mut files, mut file_errors) = (Vec::new(), Vec::new());
let resources = super::file::read_resources(
@@ -152,7 +152,7 @@ pub async fn get_remote_resources(
files,
file_errors,
logs,
hash,
message,
hash: Some(hash),
message: Some(message),
})
}
-1
View File
@@ -188,7 +188,6 @@ impl ToToml for Stack {
config
.into_iter()
.map(|(key, value)| {
#[allow(clippy::single_match)]
match key.as_str() {
"server_id" => return Ok((String::from("server"), value)),
_ => {}
-1
View File
@@ -12,7 +12,6 @@ use partial_derive2::MaybeNone;
use super::{AllResourcesById, ResourceSyncTrait};
#[allow(clippy::too_many_arguments)]
pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
resources: Vec<ResourceToml<Resource::PartialConfig>>,
delete: bool,
-1
View File
@@ -35,7 +35,6 @@ svi.workspace = true
# external
axum-server.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
futures.workspace = true
tracing.workspace = true
bollard.workspace = true
+36 -18
View File
@@ -1,7 +1,5 @@
use anyhow::{anyhow, Context};
use command::{
run_komodo_command, run_komodo_command_with_interpolation,
};
use command::run_komodo_command;
use formatting::format_serror;
use komodo_client::{
entities::{
@@ -69,7 +67,7 @@ impl Resolve<super::Args> for build::Build {
Ok(should_push) => should_push,
Err(e) => {
logs.push(Log::error(
"Docker Login",
"docker login",
format_serror(
&e.context("failed to login to docker registry").into(),
),
@@ -124,23 +122,40 @@ impl Resolve<super::Args> for build::Build {
if *skip_secret_interp {
let build_log = run_komodo_command(
"Docker Build",
"docker build",
build_dir.as_ref(),
command,
false,
)
.await;
logs.push(build_log);
} else if let Some(log) = run_komodo_command_with_interpolation(
"Docker Build",
build_dir.as_ref(),
command,
false,
&periphery_config().secrets,
&core_replacers,
)
.await
{
logs.push(log)
} else {
// Interpolate any missing secrets
let (command, mut replacers) = svi::interpolate_variables(
&command,
&periphery_config().secrets,
svi::Interpolator::DoubleBrackets,
true,
)
.context(
"failed to interpolate secrets into docker build command",
)?;
replacers.extend(core_replacers);
let mut build_log = run_komodo_command(
"docker build",
build_dir.as_ref(),
command,
false,
)
.await;
build_log.command =
svi::replace_in_string(&build_log.command, &replacers);
build_log.stdout =
svi::replace_in_string(&build_log.stdout, &replacers);
build_log.stderr =
svi::replace_in_string(&build_log.stderr, &replacers);
logs.push(build_log);
}
cleanup_secret_env_vars(&secret_args);
@@ -235,7 +250,10 @@ impl Resolve<super::Args> for PruneBuilders {
#[instrument(name = "PruneBuilders", skip_all)]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let command = String::from("docker builder prune -a -f");
Ok(run_komodo_command("Prune Builders", None, command).await)
Ok(
run_komodo_command("prune builders", None, command, false)
.await,
)
}
}
@@ -245,6 +263,6 @@ impl Resolve<super::Args> for PruneBuildx {
#[instrument(name = "PruneBuildx", skip_all)]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let command = String::from("docker buildx prune -a -f");
Ok(run_komodo_command("Prune Buildx", None, command).await)
Ok(run_komodo_command("prune buildx", None, command, false).await)
}
}
+42 -67
View File
@@ -27,9 +27,10 @@ impl Resolve<super::Args> for ListComposeProjects {
) -> serror::Result<Vec<ComposeProject>> {
let docker_compose = docker_compose();
let res = run_komodo_command(
"List Projects",
"list projects",
None,
format!("{docker_compose} ls --all --format json"),
false,
)
.await;
@@ -37,8 +38,8 @@ impl Resolve<super::Args> for ListComposeProjects {
return Err(
anyhow!("{}", res.combined())
.context(format!(
"failed to list compose projects using {docker_compose} ls"
))
"failed to list compose projects using {docker_compose} ls"
))
.into(),
);
}
@@ -81,12 +82,12 @@ pub struct DockerComposeLsItem {
//
impl Resolve<super::Args> for GetComposeLog {
#[instrument(name = "GetComposeLog", level = "debug")]
impl Resolve<super::Args> for GetComposeServiceLog {
#[instrument(name = "GetComposeServiceLog", level = "debug")]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let GetComposeLog {
let GetComposeServiceLog {
project,
services,
service,
tail,
timestamps,
} = self;
@@ -94,19 +95,20 @@ impl Resolve<super::Args> for GetComposeLog {
let timestamps =
timestamps.then_some(" --timestamps").unwrap_or_default();
let command = format!(
"{docker_compose} -p {project} logs --tail {tail}{timestamps} {}",
services.join(" ")
"{docker_compose} -p {project} logs {service} --tail {tail}{timestamps}"
);
Ok(run_komodo_command("get stack log", None, command).await)
Ok(
run_komodo_command("get stack log", None, command, false).await,
)
}
}
impl Resolve<super::Args> for GetComposeLogSearch {
#[instrument(name = "GetComposeLogSearch", level = "debug")]
impl Resolve<super::Args> for GetComposeServiceLogSearch {
#[instrument(name = "GetComposeServiceLogSearch", level = "debug")]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let GetComposeLogSearch {
let GetComposeServiceLogSearch {
project,
services,
service,
terms,
combinator,
invert,
@@ -116,11 +118,11 @@ impl Resolve<super::Args> for GetComposeLogSearch {
let grep = log_grep(&terms, combinator, invert);
let timestamps =
timestamps.then_some(" --timestamps").unwrap_or_default();
let command = format!(
"{docker_compose} -p {project} logs --tail 5000{timestamps} {} 2>&1 | {grep}",
services.join(" ")
);
Ok(run_komodo_command("Get stack log grep", None, command).await)
let command = format!("{docker_compose} -p {project} logs {service} --tail 5000{timestamps} 2>&1 | {grep}");
Ok(
run_komodo_command("get stack log grep", None, command, false)
.await,
)
}
}
@@ -179,15 +181,7 @@ impl Resolve<super::Args> for GetComposeContentsOnHost {
//
impl Resolve<super::Args> for WriteComposeContentsToHost {
#[instrument(
name = "WriteComposeContentsToHost",
skip_all,
fields(
stack = &self.name,
run_directory = &self.run_directory,
file_path = &self.file_path,
)
)]
#[instrument(name = "WriteComposeContentsToHost")]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let WriteComposeContentsToHost {
name,
@@ -204,9 +198,7 @@ impl Resolve<super::Args> for WriteComposeContentsToHost {
.collect::<PathBuf>();
// Ensure parent directory exists
if let Some(parent) = file_path.parent() {
fs::create_dir_all(&parent)
.await
.with_context(|| format!("Failed to initialize environment file parent directory {parent:?}"))?;
let _ = fs::create_dir_all(&parent).await;
}
fs::write(&file_path, contents).await.with_context(|| {
format!(
@@ -264,14 +256,7 @@ impl Resolve<super::Args> for WriteCommitComposeContents {
hash,
message,
..
} = write_commit_file(
&msg,
&root,
&file_path,
&contents,
&stack.config.branch,
)
.await?;
} = write_commit_file(&msg, &root, &file_path, &contents).await?;
Ok(RepoActionResponse {
logs,
@@ -284,7 +269,7 @@ impl Resolve<super::Args> for WriteCommitComposeContents {
//
impl WriteStackRes for &mut ComposePullResponse {
impl<'a> WriteStackRes for &'a mut ComposePullResponse {
fn logs(&mut self) -> &mut Vec<Log> {
&mut self.logs
}
@@ -296,7 +281,7 @@ impl Resolve<super::Args> for ComposePull {
skip_all,
fields(
stack = &self.stack.name,
services = format!("{:?}", self.services),
service = &self.service,
)
)]
async fn resolve(
@@ -305,23 +290,13 @@ impl Resolve<super::Args> for ComposePull {
) -> serror::Result<ComposePullResponse> {
let ComposePull {
stack,
services,
service,
git_token,
registry_token,
} = self;
let mut res = ComposePullResponse::default();
let (run_directory, env_file_path, _replacers) =
match write_stack(&stack, git_token, &mut res).await {
Ok(res) => res,
Err(e) => {
res.logs.push(Log::error(
"Write Stack",
format_serror(&e.into()),
));
return Ok(res);
}
};
let (run_directory, env_file_path) =
write_stack(&stack, git_token, &mut res).await?;
// Canonicalize the path to ensure it exists, and is the cleanest path to the run directory.
let run_directory = run_directory.canonicalize().context(
@@ -347,12 +322,10 @@ impl Resolve<super::Args> for ComposePull {
}
let docker_compose = docker_compose();
let service_args = if services.is_empty() {
String::new()
} else {
format!(" {}", services.join(" "))
};
let service_arg = service
.as_ref()
.map(|service| format!(" {service}"))
.unwrap_or_default();
let file_args = if stack.config.file_paths.is_empty() {
String::from("compose.yaml")
@@ -396,11 +369,12 @@ impl Resolve<super::Args> for ComposePull {
let project_name = stack.project_name(false);
let log = run_komodo_command(
"Compose Pull",
"compose pull",
run_directory.as_ref(),
format!(
"{docker_compose} -p {project_name} -f {file_args}{additional_env_files}{env_file} pull{service_args}",
"{docker_compose} -p {project_name} -f {file_args}{additional_env_files}{env_file} pull{service_arg}",
),
false,
)
.await;
@@ -418,7 +392,7 @@ impl Resolve<super::Args> for ComposeUp {
skip_all,
fields(
stack = &self.stack.name,
services = format!("{:?}", self.services),
service = &self.service,
)
)]
async fn resolve(
@@ -427,7 +401,7 @@ impl Resolve<super::Args> for ComposeUp {
) -> serror::Result<ComposeUpResponse> {
let ComposeUp {
stack,
services,
service,
git_token,
registry_token,
replacers,
@@ -435,7 +409,7 @@ impl Resolve<super::Args> for ComposeUp {
let mut res = ComposeUpResponse::default();
if let Err(e) = compose_up(
stack,
services,
service,
git_token,
registry_token,
&mut res,
@@ -444,7 +418,7 @@ impl Resolve<super::Args> for ComposeUp {
.await
{
res.logs.push(Log::error(
"Compose Up - Failed",
"compose up failed",
format_serror(&e.into()),
));
};
@@ -460,9 +434,10 @@ impl Resolve<super::Args> for ComposeExecution {
let ComposeExecution { project, command } = self;
let docker_compose = docker_compose();
let log = run_komodo_command(
"Compose Command",
"compose command",
None,
format!("{docker_compose} -p {project} {command}"),
false,
)
.await;
Ok(log)
+47 -21
View File
@@ -44,7 +44,10 @@ impl Resolve<super::Args> for GetContainerLog {
timestamps.then_some(" --timestamps").unwrap_or_default();
let command =
format!("docker logs {name} --tail {tail}{timestamps}");
Ok(run_komodo_command("Get container log", None, command).await)
Ok(
run_komodo_command("get container log", None, command, false)
.await,
)
}
}
@@ -67,8 +70,13 @@ impl Resolve<super::Args> for GetContainerLogSearch {
"docker logs {name} --tail 5000{timestamps} 2>&1 | {grep}"
);
Ok(
run_komodo_command("Get container log grep", None, command)
.await,
run_komodo_command(
"get container log grep",
None,
command,
false,
)
.await,
)
}
}
@@ -109,9 +117,10 @@ impl Resolve<super::Args> for StartContainer {
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
Ok(
run_komodo_command(
"Docker Start",
"docker start",
None,
format!("docker start {}", self.name),
false,
)
.await,
)
@@ -125,9 +134,10 @@ impl Resolve<super::Args> for RestartContainer {
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
Ok(
run_komodo_command(
"Docker Restart",
"docker restart",
None,
format!("docker restart {}", self.name),
false,
)
.await,
)
@@ -141,9 +151,10 @@ impl Resolve<super::Args> for PauseContainer {
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
Ok(
run_komodo_command(
"Docker Pause",
"docker pause",
None,
format!("docker pause {}", self.name),
false,
)
.await,
)
@@ -155,9 +166,10 @@ impl Resolve<super::Args> for UnpauseContainer {
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
Ok(
run_komodo_command(
"Docker Unpause",
"docker unpause",
None,
format!("docker unpause {}", self.name),
false,
)
.await,
)
@@ -171,11 +183,12 @@ impl Resolve<super::Args> for StopContainer {
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let StopContainer { name, signal, time } = self;
let command = stop_container_command(&name, signal, time);
let log = run_komodo_command("Docker Stop", None, command).await;
let log =
run_komodo_command("docker stop", None, command, false).await;
if log.stderr.contains("unknown flag: --signal") {
let command = stop_container_command(&name, None, time);
let mut log =
run_komodo_command("Docker Stop", None, command).await;
run_komodo_command("docker stop", None, command, false).await;
log.stderr = format!(
"old docker version: unable to use --signal flag{}",
if !log.stderr.is_empty() {
@@ -200,18 +213,21 @@ impl Resolve<super::Args> for RemoveContainer {
let stop_command = stop_container_command(&name, signal, time);
let command =
format!("{stop_command} && docker container rm {name}");
let log =
run_komodo_command("Docker Stop and Remove", None, command)
.await;
let log = run_komodo_command(
"docker stop and remove",
None,
command,
false,
)
.await;
if log.stderr.contains("unknown flag: --signal") {
let stop_command = stop_container_command(&name, None, time);
let command =
format!("{stop_command} && docker container rm {name}");
let mut log =
run_komodo_command("Docker Stop and Remove", None, command)
.await;
run_komodo_command("docker stop", None, command, false).await;
log.stderr = format!(
"Old docker version: unable to use --signal flag{}",
"old docker version: unable to use --signal flag{}",
if !log.stderr.is_empty() {
format!("\n\n{}", log.stderr)
} else {
@@ -236,7 +252,9 @@ impl Resolve<super::Args> for RenameContainer {
} = self;
let new = to_komodo_name(&new_name);
let command = format!("docker rename {curr_name} {new}");
Ok(run_komodo_command("Docker Rename", None, command).await)
Ok(
run_komodo_command("docker rename", None, command, false).await,
)
}
}
@@ -246,7 +264,10 @@ impl Resolve<super::Args> for PruneContainers {
#[instrument(name = "PruneContainers", skip_all)]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let command = String::from("docker container prune -f");
Ok(run_komodo_command("Prune Containers", None, command).await)
Ok(
run_komodo_command("prune containers", None, command, false)
.await,
)
}
}
@@ -269,7 +290,8 @@ impl Resolve<super::Args> for StartAllContainers {
}
let command = format!("docker start {name}");
Some(async move {
run_komodo_command(&command.clone(), None, command).await
run_komodo_command(&command.clone(), None, command, false)
.await
})
},
);
@@ -296,7 +318,8 @@ impl Resolve<super::Args> for RestartAllContainers {
}
let command = format!("docker restart {name}");
Some(async move {
run_komodo_command(&command.clone(), None, command).await
run_komodo_command(&command.clone(), None, command, false)
.await
})
},
);
@@ -323,7 +346,8 @@ impl Resolve<super::Args> for PauseAllContainers {
}
let command = format!("docker pause {name}");
Some(async move {
run_komodo_command(&command.clone(), None, command).await
run_komodo_command(&command.clone(), None, command, false)
.await
})
},
);
@@ -350,7 +374,8 @@ impl Resolve<super::Args> for UnpauseAllContainers {
}
let command = format!("docker unpause {name}");
Some(async move {
run_komodo_command(&command.clone(), None, command).await
run_komodo_command(&command.clone(), None, command, false)
.await
})
},
);
@@ -380,6 +405,7 @@ impl Resolve<super::Args> for StopAllContainers {
&format!("docker stop {name}"),
None,
stop_container_command(name, None, None),
false,
)
.await
})
+25 -16
View File
@@ -1,7 +1,5 @@
use anyhow::Context;
use command::{
run_komodo_command, run_komodo_command_with_interpolation,
};
use command::run_komodo_command;
use formatting::format_serror;
use komodo_client::{
entities::{
@@ -90,22 +88,33 @@ impl Resolve<super::Args> for Deploy {
debug!("docker run command: {command}");
if deployment.config.skip_secret_interp {
Ok(run_komodo_command("Docker Run", None, command).await)
Ok(run_komodo_command("docker run", None, command, false).await)
} else {
match run_komodo_command_with_interpolation(
"Docker Run",
None,
command,
false,
let command = svi::interpolate_variables(
&command,
&periphery_config().secrets,
&core_replacers,
svi::Interpolator::DoubleBrackets,
true,
)
.await
{
Some(log) => Ok(log),
// The None case can not be reached, as the command is always non-empty
None => unreachable!(),
}
.context(
"failed to interpolate secrets into docker run command",
);
let (command, mut replacers) = match command {
Ok(res) => res,
Err(e) => {
return Ok(Log::error("docker run", format!("{e:?}")));
}
};
replacers.extend(core_replacers);
let mut log =
run_komodo_command("docker run", None, command, false).await;
log.command = svi::replace_in_string(&log.command, &replacers);
log.stdout = svi::replace_in_string(&log.stdout, &replacers);
log.stderr = svi::replace_in_string(&log.stderr, &replacers);
Ok(log)
}
}
}
+4 -3
View File
@@ -75,9 +75,10 @@ impl Resolve<super::Args> for PullImage {
.await?;
anyhow::Ok(
run_komodo_command(
"Docker Pull",
"docker pull",
None,
format!("docker pull {name}"),
false,
)
.await,
)
@@ -98,7 +99,7 @@ impl Resolve<super::Args> for DeleteImage {
#[instrument(name = "DeleteImage")]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let command = format!("docker image rm {}", self.name);
Ok(run_komodo_command("Delete Image", None, command).await)
Ok(run_komodo_command("delete image", None, command, false).await)
}
}
@@ -108,6 +109,6 @@ impl Resolve<super::Args> for PruneImages {
#[instrument(name = "PruneImages")]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let command = String::from("docker image prune -a -f");
Ok(run_komodo_command("Prune Images", None, command).await)
Ok(run_komodo_command("prune images", None, command, false).await)
}
}
+4 -4
View File
@@ -72,8 +72,8 @@ pub enum PeripheryRequest {
// Compose (Read)
GetComposeContentsOnHost(GetComposeContentsOnHost),
GetComposeLog(GetComposeLog),
GetComposeLogSearch(GetComposeLogSearch),
GetComposeServiceLog(GetComposeServiceLog),
GetComposeServiceLogSearch(GetComposeServiceLogSearch),
// Compose (Write)
WriteComposeContentsToHost(WriteComposeContentsToHost),
@@ -247,7 +247,7 @@ impl Resolve<Args> for RunCommand {
} else {
format!("cd {path} && {command}")
};
run_komodo_command("run command", None, command).await
run_komodo_command("run command", None, command, false).await
})
.await
.context("failure in spawned task")?;
@@ -259,6 +259,6 @@ impl Resolve<Args> for PruneSystem {
#[instrument(name = "PruneSystem", skip_all)]
async fn resolve(self, _: &Args) -> serror::Result<Log> {
let command = String::from("docker system prune -a -f --volumes");
Ok(run_komodo_command("Prune System", None, command).await)
Ok(run_komodo_command("prune system", None, command, false).await)
}
}
+12 -3
View File
@@ -27,7 +27,10 @@ impl Resolve<super::Args> for CreateNetwork {
None => String::new(),
};
let command = format!("docker network create{driver} {name}");
Ok(run_komodo_command("Create Network", None, command).await)
Ok(
run_komodo_command("create network", None, command, false)
.await,
)
}
}
@@ -37,7 +40,10 @@ impl Resolve<super::Args> for DeleteNetwork {
#[instrument(name = "DeleteNetwork", skip(self))]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let command = format!("docker network rm {}", self.name);
Ok(run_komodo_command("Delete Network", None, command).await)
Ok(
run_komodo_command("delete network", None, command, false)
.await,
)
}
}
@@ -47,6 +53,9 @@ impl Resolve<super::Args> for PruneNetworks {
#[instrument(name = "PruneNetworks", skip(self))]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let command = String::from("docker network prune -f");
Ok(run_komodo_command("Prune Networks", None, command).await)
Ok(
run_komodo_command("prune networks", None, command, false)
.await,
)
}
}
+6 -2
View File
@@ -20,7 +20,9 @@ impl Resolve<super::Args> for DeleteVolume {
#[instrument(name = "DeleteVolume")]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let command = format!("docker volume rm {}", self.name);
Ok(run_komodo_command("Delete Volume", None, command).await)
Ok(
run_komodo_command("delete volume", None, command, false).await,
)
}
}
@@ -30,6 +32,8 @@ impl Resolve<super::Args> for PruneVolumes {
#[instrument(name = "PruneVolumes")]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let command = String::from("docker volume prune -a -f");
Ok(run_komodo_command("Prune Volumes", None, command).await)
Ok(
run_komodo_command("prune volumes", None, command, false).await,
)
}
}
+227 -194
View File
@@ -1,10 +1,7 @@
use std::{fmt::Write, path::PathBuf};
use anyhow::{anyhow, Context};
use command::{
run_komodo_command, run_komodo_command_multiline,
run_komodo_command_with_interpolation,
};
use command::run_komodo_command;
use formatting::format_serror;
use git::environment;
use komodo_client::entities::{
@@ -25,8 +22,9 @@ use resolver_api::Resolve;
use tokio::fs;
use crate::{
config::periphery_config, docker::docker_login,
helpers::parse_extra_args,
config::periphery_config,
docker::docker_login,
helpers::{interpolate_variables, parse_extra_args},
};
pub fn docker_compose() -> &'static str {
@@ -40,7 +38,7 @@ pub fn docker_compose() -> &'static str {
/// If this fn returns Err, the caller of `compose_up` has to write result to the log before return.
pub async fn compose_up(
stack: Stack,
services: Vec<String>,
service: Option<String>,
git_token: Option<String>,
registry_token: Option<String>,
res: &mut ComposeUpResponse,
@@ -49,21 +47,11 @@ pub async fn compose_up(
// Write the stack to local disk. For repos, will first delete any existing folder to ensure fresh deploy.
// Will also set additional fields on the reponse.
// Use the env_file_path in the compose command.
let (run_directory, env_file_path, periphery_replacers) =
let (run_directory, env_file_path) =
write_stack(&stack, git_token, &mut *res)
.await
.context("Failed to write / clone compose file")?;
let replacers =
if let Some(periphery_replacers) = periphery_replacers {
core_replacers
.into_iter()
.chain(periphery_replacers)
.collect()
} else {
core_replacers
};
// Canonicalize the path to ensure it exists, and is the cleanest path to the run directory.
let run_directory = run_directory.canonicalize().context(
"Failed to validate run directory on host after stack write (canonicalize error)",
@@ -91,29 +79,28 @@ pub async fn compose_up(
}
for (path, full_path) in &file_paths {
let file_contents = match fs::read_to_string(&full_path)
.await
.with_context(|| {
let file_contents =
match fs::read_to_string(&full_path).await.with_context(|| {
format!(
"failed to read compose file contents at {full_path:?}"
)
}) {
Ok(res) => res,
Err(e) => {
let error = format_serror(&e.into());
res
.logs
.push(Log::error("read compose file", error.clone()));
// This should only happen for repo stacks, ie remote error
res.remote_errors.push(FileContents {
path: path.to_string(),
contents: error,
});
return Err(anyhow!(
"failed to read compose file at {full_path:?}, stopping run"
));
}
};
Ok(res) => res,
Err(e) => {
let error = format_serror(&e.into());
res
.logs
.push(Log::error("read compose file", error.clone()));
// This should only happen for repo stacks, ie remote error
res.remote_errors.push(FileContents {
path: path.to_string(),
contents: error,
});
return Err(anyhow!(
"failed to read compose file at {full_path:?}, stopping run"
));
}
};
res.file_contents.push(FileContents {
path: path.to_string(),
contents: file_contents,
@@ -121,12 +108,10 @@ pub async fn compose_up(
}
let docker_compose = docker_compose();
let service_args = if services.is_empty() {
String::new()
} else {
format!(" {}", services.join(" "))
};
let service_arg = service
.as_ref()
.map(|service| format!(" {service}"))
.unwrap_or_default();
let file_args = if stack.config.file_paths.is_empty() {
String::from("compose.yaml")
@@ -174,12 +159,13 @@ pub async fn compose_up(
// after performing interpolation
{
let command = format!(
"{docker_compose} -p {project_name} -f {file_args}{additional_env_files}{env_file} config",
"{docker_compose} -p {project_name} -f {file_args}{additional_env_files}{env_file} config --format json",
);
let config_log = run_komodo_command(
"Compose Config",
"compose build",
run_directory.as_ref(),
command,
false,
)
.await;
if !config_log.success {
@@ -188,11 +174,8 @@ pub async fn compose_up(
"Failed to validate compose files, stopping the run."
));
}
// Record sanitized compose config output
res.compose_config =
Some(svi::replace_in_string(&config_log.stdout, &replacers));
let compose =
serde_yaml::from_str::<ComposeFile>(&config_log.stdout)
serde_json::from_str::<ComposeFile>(&config_log.stdout)
.context("Failed to parse compose contents")?;
for (
service_name,
@@ -237,26 +220,38 @@ pub async fn compose_up(
let build_extra_args =
parse_extra_args(&stack.config.build_extra_args);
let command = format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} build{build_extra_args}{service_args}",
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} build{build_extra_args}{service_arg}",
);
if stack.config.skip_secret_interp {
let log = run_komodo_command(
"Compose Build",
"compose build",
run_directory.as_ref(),
command,
false,
)
.await;
res.logs.push(log);
} else if let Some(log) = run_komodo_command_with_interpolation(
"Compose Build",
run_directory.as_ref(),
command,
false,
&periphery_config().secrets,
&replacers,
)
.await
{
} else {
let (command, mut replacers) = svi::interpolate_variables(
&command,
&periphery_config().secrets,
svi::Interpolator::DoubleBrackets,
true,
).context("failed to interpolate periphery secrets into stack build command")?;
replacers.extend(core_replacers.clone());
let mut log = run_komodo_command(
"compose build",
run_directory.as_ref(),
command,
false,
)
.await;
log.command = svi::replace_in_string(&log.command, &replacers);
log.stdout = svi::replace_in_string(&log.stdout, &replacers);
log.stderr = svi::replace_in_string(&log.stderr, &replacers);
res.logs.push(log);
}
@@ -272,11 +267,12 @@ pub async fn compose_up(
// Pull images before destroying to minimize downtime.
// If this fails, do not continue.
let log = run_komodo_command(
"Compose Pull",
"compose pull",
run_directory.as_ref(),
format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} pull{service_args}",
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} pull{service_arg}",
),
false,
)
.await;
@@ -289,33 +285,58 @@ pub async fn compose_up(
}
}
// Pre deploy command
let pre_deploy_path =
run_directory.join(&stack.config.pre_deploy.path);
if let Some(log) = if stack.config.skip_secret_interp {
run_komodo_command_multiline(
"Pre Deploy",
pre_deploy_path.as_ref(),
&stack.config.pre_deploy.command,
)
.await
} else {
run_komodo_command_with_interpolation(
"Pre Deploy",
pre_deploy_path.as_ref(),
&stack.config.pre_deploy.command,
true,
&periphery_config().secrets,
&replacers,
)
.await
} {
res.logs.push(log);
}
if !all_logs_success(&res.logs) {
return Err(anyhow!(
"Failed at running pre_deploy command, stopping the run."
));
if !stack.config.pre_deploy.command.is_empty() {
let pre_deploy_path =
run_directory.join(&stack.config.pre_deploy.path);
if !stack.config.skip_secret_interp {
let (full_command, mut replacers) =
interpolate_variables(&stack.config.pre_deploy.command)
.context(
"failed to interpolate secrets into pre_deploy command",
)?;
replacers.extend(core_replacers.to_owned());
let mut pre_deploy_log = run_komodo_command(
"pre deploy",
pre_deploy_path.as_ref(),
&full_command,
true,
)
.await;
pre_deploy_log.command =
svi::replace_in_string(&pre_deploy_log.command, &replacers);
pre_deploy_log.stdout =
svi::replace_in_string(&pre_deploy_log.stdout, &replacers);
pre_deploy_log.stderr =
svi::replace_in_string(&pre_deploy_log.stderr, &replacers);
tracing::debug!(
"run Stack pre_deploy command | command: {} | cwd: {:?}",
pre_deploy_log.command,
pre_deploy_path
);
res.logs.push(pre_deploy_log);
} else {
let pre_deploy_log = run_komodo_command(
"pre deploy",
pre_deploy_path.as_ref(),
&stack.config.pre_deploy.command,
true,
)
.await;
tracing::debug!(
"run Stack pre_deploy command | command: {} | cwd: {:?}",
&stack.config.pre_deploy.command,
pre_deploy_path
);
res.logs.push(pre_deploy_log);
}
if !all_logs_success(&res.logs) {
return Err(anyhow!(
"Failed at running pre_deploy command, stopping the run."
));
}
}
if stack.config.destroy_before_deploy
@@ -324,7 +345,7 @@ pub async fn compose_up(
{
// Take down the existing containers.
// This one tries to use the previously deployed service name, to ensure the right stack is taken down.
compose_down(&last_project_name, &services, res)
compose_down(&last_project_name, service, res)
.await
.context("failed to destroy existing containers")?;
}
@@ -332,27 +353,39 @@ pub async fn compose_up(
// Run compose up
let extra_args = parse_extra_args(&stack.config.extra_args);
let command = format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} up -d{extra_args}{service_args}",
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} up -d{extra_args}{service_arg}",
);
let log = if stack.config.skip_secret_interp {
run_komodo_command("Compose Up", run_directory.as_ref(), command)
.await
} else {
match run_komodo_command_with_interpolation(
"Compose Up",
run_komodo_command(
"compose up",
run_directory.as_ref(),
command,
false,
&periphery_config().secrets,
&replacers,
)
.await
{
Some(log) => log,
// The command is definitely non-empty, the result will never be None.
None => unreachable!(),
}
} else {
let (command, mut replacers) = svi::interpolate_variables(
&command,
&periphery_config().secrets,
svi::Interpolator::DoubleBrackets,
true,
).context("failed to interpolate periphery secrets into stack run command")?;
replacers.extend(core_replacers.clone());
let mut log = run_komodo_command(
"compose up",
run_directory.as_ref(),
command,
false,
)
.await;
log.command = svi::replace_in_string(&log.command, &replacers);
log.stdout = svi::replace_in_string(&log.stdout, &replacers);
log.stderr = svi::replace_in_string(&log.stderr, &replacers);
log
};
res.deployed = log.success;
@@ -360,28 +393,52 @@ pub async fn compose_up(
// push the compose up command logs to keep the correct order
res.logs.push(log);
if res.deployed {
if res.deployed && !stack.config.post_deploy.command.is_empty() {
let post_deploy_path =
run_directory.join(&stack.config.post_deploy.path);
if let Some(log) = if stack.config.skip_secret_interp {
run_komodo_command_multiline(
"Post Deploy",
run_directory.join(&stack.config.post_deploy.path);
if !stack.config.skip_secret_interp {
let (full_command, mut replacers) =
interpolate_variables(&stack.config.post_deploy.command)
.context(
"failed to interpolate secrets into post_deploy command",
)?;
replacers.extend(core_replacers);
let mut post_deploy_log = run_komodo_command(
"post deploy",
post_deploy_path.as_ref(),
&stack.config.post_deploy.command,
&full_command,
true,
)
.await
.await;
post_deploy_log.command =
svi::replace_in_string(&post_deploy_log.command, &replacers);
post_deploy_log.stdout =
svi::replace_in_string(&post_deploy_log.stdout, &replacers);
post_deploy_log.stderr =
svi::replace_in_string(&post_deploy_log.stderr, &replacers);
tracing::debug!(
"run Stack post_deploy command | command: {} | cwd: {:?}",
post_deploy_log.command,
post_deploy_path
);
res.logs.push(post_deploy_log);
} else {
run_komodo_command_with_interpolation(
"Post Deploy",
let post_deploy_log = run_komodo_command(
"post deploy",
post_deploy_path.as_ref(),
&stack.config.post_deploy.command,
true,
&periphery_config().secrets,
&replacers,
)
.await
} {
res.logs.push(log)
.await;
tracing::debug!(
"run Stack post_deploy command | command: {} | cwd: {:?}",
&stack.config.post_deploy.command,
post_deploy_path
);
res.logs.push(post_deploy_log);
}
if !all_logs_success(&res.logs) {
return Err(anyhow!(
@@ -400,7 +457,7 @@ pub trait WriteStackRes {
fn set_commit_message(&mut self, _message: Option<String>) {}
}
impl WriteStackRes for &mut ComposeUpResponse {
impl<'a> WriteStackRes for &'a mut ComposeUpResponse {
fn logs(&mut self) -> &mut Vec<Log> {
&mut self.logs
}
@@ -416,17 +473,12 @@ impl WriteStackRes for &mut ComposeUpResponse {
}
/// Either writes the stack file_contents to a file, or clones the repo.
/// Performs variable replacement on env and writes file.
/// Returns (run_directory, env_file_path, periphery_replacers)
/// Returns (run_directory, env_file_path)
pub async fn write_stack(
stack: &Stack,
git_token: Option<String>,
mut res: impl WriteStackRes,
) -> anyhow::Result<(
PathBuf,
Option<&str>,
Option<Vec<(String, String)>>,
)> {
) -> anyhow::Result<(PathBuf, Option<&str>)> {
let root = periphery_config()
.stack_dir
.join(to_komodo_name(&stack.name));
@@ -435,61 +487,42 @@ pub async fn write_stack(
// Cannot use 'canonicalize' yet as directory may not exist.
let run_directory = run_directory.components().collect::<PathBuf>();
let (env_interpolated, env_replacers) =
if stack.config.skip_secret_interp {
(stack.config.environment.clone(), None)
} else {
let (environment, replacers) = svi::interpolate_variables(
&stack.config.environment,
&periphery_config().secrets,
svi::Interpolator::DoubleBrackets,
true,
)
.context(
"Failed to interpolate Periphery secrets into Environment",
)?;
(environment, Some(replacers))
};
match &env_replacers {
Some(replacers) if !replacers.is_empty() => {
res.logs().push(Log::simple(
"Interpolate - Environment (Periphery)",
replacers
.iter()
.map(|(_, variable)| format!("<span class=\"text-muted-foreground\">replaced:</span> {variable}"))
.collect::<Vec<_>>()
.join("\n"),
))
}
_ => {}
}
let env_vars = environment_vars_from_str(&env_interpolated)
let env_vars = environment_vars_from_str(&stack.config.environment)
.context("Invalid environment variables")?;
if stack.config.files_on_host {
// =============
// FILES ON HOST
// =============
let env_file_path = environment::write_file_simple(
// Only need to write environment file here (which does nothing if not using this feature)
let env_file_path = match environment::write_file(
&env_vars,
&stack.config.env_file_path,
stack
.config
.skip_secret_interp
.then_some(&periphery_config().secrets),
run_directory.as_ref(),
res.logs(),
)
.await?;
.await
{
Ok(path) => path,
Err(_) => {
return Err(anyhow!("failed to write environment file"));
}
};
Ok((
run_directory,
// Env file paths are expected to be already relative to run directory,
// Env file paths are already relative to run directory,
// so need to pass original env_file_path here.
env_file_path
.is_some()
.then_some(&stack.config.env_file_path),
env_replacers,
))
} else if stack.config.repo.is_empty() {
if stack.config.file_contents.trim().is_empty() {
return Err(anyhow!("Must either input compose file contents directly, or use files on host / git repo options."));
return Err(anyhow!("Must either input compose file contents directly, or use file one host / git repo options."));
}
// ==============
// UI BASED FILES
@@ -500,13 +533,23 @@ pub async fn write_stack(
"failed to create stack run directory at {run_directory:?}"
)
})?;
let env_file_path = environment::write_file_simple(
let env_file_path = match environment::write_file(
&env_vars,
&stack.config.env_file_path,
stack
.config
.skip_secret_interp
.then_some(&periphery_config().secrets),
run_directory.as_ref(),
res.logs(),
)
.await?;
.await
{
Ok(path) => path,
Err(_) => {
return Err(anyhow!("failed to write environment file"));
}
};
let file_path = run_directory
.join(
stack
@@ -520,10 +563,7 @@ pub async fn write_stack(
.components()
.collect::<PathBuf>();
let (file_contents, file_replacers) = if !stack
.config
.skip_secret_interp
{
let file_contents = if !stack.config.skip_secret_interp {
let (contents, replacers) = svi::interpolate_variables(
&stack.config.file_contents,
&periphery_config().secrets,
@@ -533,7 +573,7 @@ pub async fn write_stack(
.context("failed to interpolate secrets into file contents")?;
if !replacers.is_empty() {
res.logs().push(Log::simple(
"Interpolate - Compose file (Periphery)",
"Interpolate - Compose file",
replacers
.iter()
.map(|(_, variable)| format!("<span class=\"text-muted-foreground\">replaced:</span> {variable}"))
@@ -541,13 +581,13 @@ pub async fn write_stack(
.join("\n"),
));
}
(contents, Some(replacers))
contents
} else {
(stack.config.file_contents.clone(), None)
stack.config.file_contents.clone()
};
fs::write(&file_path, &file_contents).await.with_context(
|| format!("Failed to write compose file to {file_path:?}"),
|| format!("failed to write compose file to {file_path:?}"),
)?;
Ok((
@@ -555,14 +595,6 @@ pub async fn write_stack(
env_file_path
.is_some()
.then_some(&stack.config.env_file_path),
match (env_replacers, file_replacers) {
(Some(env_replacers), Some(file_replacers)) => Some(
env_replacers.into_iter().chain(file_replacers).collect(),
),
(Some(env_replacers), None) => Some(env_replacers),
(None, Some(file_replacers)) => Some(file_replacers),
(None, None) => None,
},
))
} else {
// ================
@@ -616,8 +648,9 @@ pub async fn write_stack(
git_token,
environment: env_vars,
env_file_path,
// Env has already been interpolated above
skip_secret_interp: true,
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(),
}
.resolve(&crate::api::Args)
@@ -628,8 +661,9 @@ pub async fn write_stack(
git_token,
environment: env_vars,
env_file_path,
// Env has already been interpolated above
skip_secret_interp: true,
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(),
}
.resolve(&crate::api::Args)
@@ -645,17 +679,17 @@ pub async fn write_stack(
Ok(res) => res,
Err(e) => {
let error = format_serror(
&e.error.context("Failed to pull stack repo").into(),
&e.error.context("failed to pull stack repo").into(),
);
res
.logs()
.push(Log::error("Pull Stack Repo", error.clone()));
.push(Log::error("pull stack repo", error.clone()));
res.add_remote_error(FileContents {
path: Default::default(),
contents: error,
});
return Err(anyhow!(
"Failed to pull stack repo, stopping run"
"failed to pull stack repo, stopping run"
));
}
};
@@ -673,26 +707,25 @@ pub async fn write_stack(
env_file_path
.is_some()
.then_some(&stack.config.env_file_path),
env_replacers,
))
}
}
async fn compose_down(
project: &str,
services: &[String],
service: Option<String>,
res: &mut ComposeUpResponse,
) -> anyhow::Result<()> {
let docker_compose = docker_compose();
let service_args = if services.is_empty() {
String::new()
} else {
format!(" {}", services.join(" "))
};
let service_arg = service
.as_ref()
.map(|service| format!(" {service}"))
.unwrap_or_default();
let log = run_komodo_command(
"compose down",
None,
format!("{docker_compose} -p {project} down{service_args}"),
format!("{docker_compose} -p {project} down{service_arg}"),
false,
)
.await;
let success = log.success;
+1 -1
View File
@@ -969,7 +969,7 @@ pub async fn docker_login(
#[instrument]
pub async fn pull_image(image: &str) -> Log {
let command = format!("docker pull {image}");
run_komodo_command("Docker Pull", None, command).await
run_komodo_command("docker pull", None, command, false).await
}
pub fn stop_container_command(
+11
View File
@@ -86,6 +86,17 @@ pub fn log_grep(
}
}
pub fn interpolate_variables(
input: &str,
) -> svi::Result<(String, Vec<(String, String)>)> {
svi::interpolate_variables(
input,
&periphery_config().secrets,
svi::Interpolator::DoubleBrackets,
true,
)
}
/// Returns path to root directory of the stack repo.
pub async fn pull_or_clone_stack(
stack: &Stack,
+16 -32
View File
@@ -26,10 +26,8 @@ use super::{BatchExecutionResponse, KomodoExecuteRequest};
pub struct DeployStack {
/// Id or name
pub stack: String,
/// Filter to only deploy specific services.
/// If empty, will deploy all services.
#[serde(default)]
pub services: Vec<String>,
/// Optionally specify a specific service to "compose up"
pub service: Option<String>,
/// Override the default termination max time.
/// Only used if the stack needs to be taken down first.
pub stop_time: Option<i32>,
@@ -144,10 +142,8 @@ pub struct BatchDeployStackIfChanged {
pub struct PullStack {
/// Id or name
pub stack: String,
/// Filter to only pull specific services.
/// If empty, will pull all services.
#[serde(default)]
pub services: Vec<String>,
/// Optionally specify a specific service to start
pub service: Option<String>,
}
//
@@ -170,10 +166,8 @@ pub struct PullStack {
pub struct StartStack {
/// Id or name
pub stack: String,
/// Filter to only start specific services.
/// If empty, will start all services.
#[serde(default)]
pub services: Vec<String>,
/// Optionally specify a specific service to start
pub service: Option<String>,
}
//
@@ -196,10 +190,8 @@ pub struct StartStack {
pub struct RestartStack {
/// Id or name
pub stack: String,
/// Filter to only restart specific services.
/// If empty, will restart all services.
#[serde(default)]
pub services: Vec<String>,
/// Optionally specify a specific service to restart
pub service: Option<String>,
}
//
@@ -222,10 +214,8 @@ pub struct RestartStack {
pub struct PauseStack {
/// Id or name
pub stack: String,
/// Filter to only pause specific services.
/// If empty, will pause all services.
#[serde(default)]
pub services: Vec<String>,
/// Optionally specify a specific service to pause
pub service: Option<String>,
}
//
@@ -250,10 +240,8 @@ pub struct PauseStack {
pub struct UnpauseStack {
/// Id or name
pub stack: String,
/// Filter to only unpause specific services.
/// If empty, will unpause all services.
#[serde(default)]
pub services: Vec<String>,
/// Optionally specify a specific service to unpause
pub service: Option<String>,
}
//
@@ -278,10 +266,8 @@ pub struct StopStack {
pub stack: String,
/// Override the default termination max time.
pub stop_time: Option<i32>,
/// Filter to only stop specific services.
/// If empty, will stop all services.
#[serde(default)]
pub services: Vec<String>,
/// Optionally specify a specific service to stop
pub service: Option<String>,
}
//
@@ -304,10 +290,8 @@ pub struct StopStack {
pub struct DestroyStack {
/// Id or name
pub stack: String,
/// Filter to only destroy specific services.
/// If empty, will destroy all services.
#[serde(default)]
pub services: Vec<String>,
/// Optionally specify a specific service to destroy
pub service: Option<String>,
/// Pass `--remove-orphans`
#[serde(default)]
pub remove_orphans: bool,
+13 -17
View File
@@ -53,23 +53,20 @@ pub type ListStackServicesResponse = Vec<StackService>;
//
/// Get a stack's logs. Filter down included services. Response: [GetStackLogResponse].
///
/// Note. This call will hit the underlying server directly for most up to date log.
/// Get a stack service's log. Response: [GetStackServiceLogResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(GetStackLogResponse)]
#[response(GetStackServiceLogResponse)]
#[error(serror::Error)]
pub struct GetStackLog {
pub struct GetStackServiceLog {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub stack: String,
/// Filter the logs to only ones from specific services.
/// If empty, will include logs from all services.
pub services: Vec<String>,
/// The service to get the log for.
pub service: String,
/// The number of lines of the log tail to include.
/// Default: 100.
/// Max: 5000.
@@ -85,12 +82,12 @@ fn default_tail() -> u64 {
}
#[typeshare]
pub type GetStackLogResponse = Log;
pub type GetStackServiceLogResponse = Log;
//
/// Search the stack log's tail using `grep`. All lines go to stdout.
/// Response: [SearchStackLogResponse].
/// Search the deployment log's tail using `grep`. All lines go to stdout.
/// Response: [Log].
///
/// Note. This call will hit the underlying server directly for most up to date log.
#[typeshare]
@@ -98,15 +95,14 @@ pub type GetStackLogResponse = Log;
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(SearchStackLogResponse)]
#[response(SearchStackServiceLogResponse)]
#[error(serror::Error)]
pub struct SearchStackLog {
pub struct SearchStackServiceLog {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub stack: String,
/// Filter the logs to only ones from specific services.
/// If empty, will include logs from all services.
pub services: Vec<String>,
/// The service to get the log for.
pub service: String,
/// The terms to search for.
pub terms: Vec<String>,
/// When searching for multiple terms, can use `AND` or `OR` combinator.
@@ -124,7 +120,7 @@ pub struct SearchStackLog {
}
#[typeshare]
pub type SearchStackLogResponse = Log;
pub type SearchStackServiceLogResponse = Log;
//
@@ -22,7 +22,7 @@ where
struct FileContentsVisitor;
impl Visitor<'_> for FileContentsVisitor {
impl<'de> Visitor<'de> for FileContentsVisitor {
type Value = String;
fn expecting(
@@ -47,7 +47,7 @@ impl Visitor<'_> for FileContentsVisitor {
struct OptionFileContentsVisitor;
impl Visitor<'_> for OptionFileContentsVisitor {
impl<'de> Visitor<'de> for OptionFileContentsVisitor {
type Value = Option<String>;
fn expecting(
@@ -20,7 +20,7 @@ where
struct MaybeStringI64Visitor;
impl Visitor<'_> for MaybeStringI64Visitor {
impl<'de> Visitor<'de> for MaybeStringI64Visitor {
type Value = i64;
fn expecting(
@@ -110,7 +110,7 @@ impl Visitor<'_> for MaybeStringI64Visitor {
struct OptionMaybeStringI64Visitor;
impl Visitor<'_> for OptionMaybeStringI64Visitor {
impl<'de> Visitor<'de> for OptionMaybeStringI64Visitor {
type Value = Option<i64>;
fn expecting(
-1
View File
@@ -72,7 +72,6 @@ impl AlerterConfig {
}
}
#[allow(clippy::derivable_impls)]
impl Default for AlerterConfig {
fn default() -> Self {
Self {
@@ -96,8 +96,6 @@ pub struct Env {
pub komodo_enable_new_users: Option<bool>,
/// Override `disable_user_registration`
pub komodo_disable_user_registration: Option<bool>,
/// Override `lock_login_credentials_for`
pub komodo_lock_login_credentials_for: Option<Vec<String>>,
/// Override `disable_confirm_dialog`
pub komodo_disable_confirm_dialog: Option<bool>,
/// Override `disable_non_admin_create`
@@ -302,13 +300,6 @@ pub struct CoreConfig {
#[serde(default)]
pub disable_user_registration: bool,
/// List of usernames for which the update username / password
/// APIs are disabled. Used by demo to lock the 'demo' : 'demo' login.
///
/// To lock the api for all users, use `lock_login_credentials_for = ["__ALL__"]`
#[serde(default)]
pub lock_login_credentials_for: Vec<String>,
/// Normally all users can create resources.
/// If `disable_non_admin_create = true`, only admins will be able to create resources.
#[serde(default)]
@@ -590,7 +581,6 @@ impl CoreConfig {
enable_new_users: config.enable_new_users,
disable_user_registration: config.disable_user_registration,
disable_non_admin_create: config.disable_non_admin_create,
lock_login_credentials_for: config.lock_login_credentials_for,
local_auth: config.local_auth,
oidc_enabled: config.oidc_enabled,
oidc_provider: config.oidc_provider,
+3 -3
View File
@@ -23,7 +23,7 @@ pub struct GitProvider {
/// Whether to use https. Default: true.
#[serde(default = "default_git_https")]
pub https: bool,
/// The accounts on the git provider. Required.
/// The account username. Required.
#[serde(alias = "account")]
pub accounts: Vec<ProviderAccount>,
}
@@ -52,8 +52,8 @@ pub struct DockerRegistry {
/// The docker provider domain. Default: `docker.io`.
#[serde(default = "default_docker_provider")]
pub domain: String,
/// The accounts on the registry. Required.
#[serde(alias = "account")]
/// The account username. Required.
#[serde(default, alias = "account")]
pub accounts: Vec<ProviderAccount>,
/// Available organizations on the registry provider.
/// Used to push an image under an organization's repo rather than an account's repo.
+2 -14
View File
@@ -163,15 +163,12 @@ pub struct StackInfo {
pub deployed_hash: Option<String>,
/// Deployed commit message, or null. Only for repo based stacks
pub deployed_message: Option<String>,
/// The deployed compose file contents.
/// This is updated whenever Komodo successfully deploys the stack.
/// The deployed compose file contents. This is updated whenever Komodo successfully deploys the stack.
pub deployed_contents: Option<Vec<FileContents>>,
/// The deployed service names.
/// This is updated whenever it is empty, or deployed contents is updated.
pub deployed_services: Option<Vec<StackServiceNames>>,
/// The output of `docker compose config`.
/// This is updated whenever Komodo successfully deploys the stack.
pub deployed_config: Option<String>,
/// The latest service names.
/// This is updated whenever the stack cache refreshes, using the latest file contents (either db defined or remote).
#[serde(default)]
@@ -250,14 +247,6 @@ pub struct StackConfig {
#[builder(default)]
pub auto_update: bool,
/// If auto update is enabled, Komodo will
/// by default only update the specific services
/// with image updates. If this parameter is set to true,
/// Komodo will redeploy the whole Stack (all services).
#[serde(default)]
#[builder(default)]
pub auto_update_all_services: bool,
/// Whether to run `docker compose down` before `compose up`.
#[serde(default)]
#[builder(default)]
@@ -503,7 +492,6 @@ impl Default for StackConfig {
auto_pull: default_auto_pull(),
poll_for_updates: Default::default(),
auto_update: Default::default(),
auto_update_all_services: Default::default(),
ignore_services: Default::default(),
pre_deploy: Default::default(),
post_deploy: Default::default(),
-7
View File
@@ -229,12 +229,6 @@ pub struct ResourceSyncConfig {
#[builder(default)]
pub resource_path: Vec<String>,
/// Excluse Komodo Resources (Servers / Stacks / Builds)
/// from the sync. Will be variable / user group only sync.
#[serde(default)]
#[builder(default)]
pub exclude_resources: bool,
/// Enable "pushes" to the file,
/// which exports resources matching tags to single file.
/// - If using `files_on_host`, it is stored in the file_contents, which must point to a .toml file path (it will be created if it doesn't exist).
@@ -315,7 +309,6 @@ impl Default for ResourceSyncConfig {
resource_path: Default::default(),
files_on_host: Default::default(),
file_contents: Default::default(),
exclude_resources: Default::default(),
managed: Default::default(),
match_tags: Default::default(),
delete: Default::default(),
+3 -89
View File
@@ -9,10 +9,8 @@ import {
AuthRequest,
ExecuteRequest,
ReadRequest,
UpdateListItem,
UserRequest,
WriteRequest,
WsLoginMessage,
} from "./types.js";
export * as Types from "./types.js";
@@ -21,16 +19,6 @@ type InitOptions =
| { type: "jwt"; params: { jwt: string } }
| { type: "api-key"; params: { key: string; secret: string } };
export class CancelToken {
cancelled: boolean;
constructor() {
this.cancelled = false;
}
cancel() {
this.cancelled = true;
}
}
/** Initialize a new client for Komodo */
export function KomodoClient(url: string, options: InitOptions) {
const state = {
@@ -119,6 +107,7 @@ export function KomodoClient(url: string, options: InitOptions) {
UserResponses[Req["type"]]
>("/user", { type, params });
const read = async <
T extends ReadRequest["type"],
Req extends Extract<ReadRequest, { type: T }>
@@ -131,6 +120,7 @@ export function KomodoClient(url: string, options: InitOptions) {
ReadResponses[Req["type"]]
>("/read", { type, params });
const write = async <
T extends WriteRequest["type"],
Req extends Extract<WriteRequest, { type: T }>
@@ -143,6 +133,7 @@ export function KomodoClient(url: string, options: InitOptions) {
WriteResponses[Req["type"]]
>("/write", { type, params });
const execute = async <
T extends ExecuteRequest["type"],
Req extends Extract<ExecuteRequest, { type: T }>
@@ -157,77 +148,6 @@ export function KomodoClient(url: string, options: InitOptions) {
const core_version = () => read("GetVersion", {}).then((res) => res.version);
const subscribe_to_update_websocket = async ({
on_update,
on_login,
on_close,
retry_timeout_ms = 5_000,
cancel = new CancelToken(),
on_cancel,
}: {
on_update: (update: UpdateListItem) => void;
on_login?: () => void;
on_open?: () => void;
on_close?: () => void;
retry_timeout_ms?: number;
cancel?: CancelToken;
on_cancel?: () => void;
}) => {
while (true) {
if (cancel.cancelled) {
on_cancel?.();
return;
}
try {
const ws = new WebSocket(url.replace("http", "ws") + "/ws/update");
// Handle login on websocket open
ws.addEventListener("open", () => {
const login_msg: WsLoginMessage =
options.type === "jwt"
? {
type: "Jwt",
params: {
jwt: options.params.jwt,
},
}
: {
type: "ApiKeys",
params: {
key: options.params.key,
secret: options.params.secret,
},
};
ws.send(JSON.stringify(login_msg));
});
ws.addEventListener("message", ({ data }: MessageEvent) => {
if (data == "LOGGED_IN") return on_login?.();
on_update(JSON.parse(data));
});
if (on_close) {
ws.addEventListener("close", on_close);
}
// This while loop will end when the socket is closed
while (
ws.readyState !== WebSocket.CLOSING &&
ws.readyState !== WebSocket.CLOSED
) {
if (cancel.cancelled) ws.close();
// Sleep for a bit before checking for websocket closed
await new Promise((resolve) => setTimeout(resolve, 500));
}
} catch (error) {
console.error(error);
// Sleep for a bit before retrying, maybe Komodo Core is down temporarily.
await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));
}
}
};
return {
/**
* Call the `/auth` api.
@@ -292,11 +212,5 @@ export function KomodoClient(url: string, options: InitOptions) {
execute,
/** Returns the version of Komodo Core the client is calling to. */
core_version,
/**
* Subscribes to the update websocket with automatic reconnect loop.
*
* Note. Awaiting this method will never finish.
*/
subscribe_to_update_websocket,
};
}
+2 -2
View File
@@ -124,8 +124,8 @@ export type ReadResponses = {
GetStack: Types.GetStackResponse;
GetStackActionState: Types.GetStackActionStateResponse;
GetStackWebhooksEnabled: Types.GetStackWebhooksEnabledResponse;
GetStackLog: Types.GetStackLogResponse;
SearchStackLog: Types.SearchStackLogResponse;
GetStackServiceLog: Types.GetStackServiceLogResponse;
SearchStackServiceLog: Types.SearchStackServiceLogResponse;
ListStacks: Types.ListStacksResponse;
ListFullStacks: Types.ListFullStacksResponse;
ListStackServices: Types.ListStackServicesResponse;
File diff suppressed because it is too large Load Diff
+10 -20
View File
@@ -44,13 +44,11 @@ pub struct GetComposeContentsOnHostResponse {
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
#[response(Log)]
#[error(serror::Error)]
pub struct GetComposeLog {
pub struct GetComposeServiceLog {
/// The name of the project
pub project: String,
/// Filter the logs to only ones from specific services.
/// If empty, will include logs from all services.
#[serde(default)]
pub services: Vec<String>,
/// The service name
pub service: String,
/// Pass `--tail` for only recent log contents. Max of 5000
#[serde(default = "default_tail")]
pub tail: u64,
@@ -69,13 +67,11 @@ fn default_tail() -> u64 {
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
#[response(Log)]
#[error(serror::Error)]
pub struct GetComposeLogSearch {
pub struct GetComposeServiceLogSearch {
/// The name of the project
pub project: String,
/// Filter the logs to only ones from specific services.
/// If empty, will include logs from all services.
#[serde(default)]
pub services: Vec<String>,
/// The service name
pub service: String,
/// The search terms.
pub terms: Vec<String>,
/// And: Only lines matching all terms
@@ -139,10 +135,8 @@ pub struct WriteCommitComposeContents {
pub struct ComposePull {
/// The stack to deploy
pub stack: Stack,
/// Filter to only pull specific services.
/// If empty, will pull all services.
#[serde(default)]
pub services: Vec<String>,
/// Only deploy one service
pub service: Option<String>,
/// If provided, use it to login in. Otherwise check periphery local registries.
pub git_token: Option<String>,
/// If provided, use it to login in. Otherwise check periphery local git providers.
@@ -164,10 +158,8 @@ pub struct ComposePullResponse {
pub struct ComposeUp {
/// The stack to deploy
pub stack: Stack,
/// Filter to only deploy specific services.
/// If empty, will deploy all services.
#[serde(default)]
pub services: Vec<String>,
/// Only deploy one service
pub service: Option<String>,
/// If provided, use it to login in. Otherwise check periphery local registries.
pub git_token: Option<String>,
/// If provided, use it to login in. Otherwise check periphery local git providers.
@@ -194,8 +186,6 @@ pub struct ComposeUpResponse {
pub file_contents: Vec<FileContents>,
/// The error in getting remote file contents at the path, or null
pub remote_errors: Vec<FileContents>,
/// The output of `docker compose config` at deploy time
pub compose_config: Option<String>,
/// If its a repo based stack, will include the latest commit hash
pub commit_hash: Option<String>,
/// If its a repo based stack, will include the latest commit message
+2 -2
View File
@@ -9,7 +9,7 @@
services:
postgres:
image: postgres:17
image: postgres
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
@@ -27,7 +27,7 @@ services:
- POSTGRES_DB=${KOMODO_DATABASE_DB_NAME:-komodo}
ferretdb:
image: ghcr.io/ferretdb/ferretdb:1
image: ghcr.io/ferretdb/ferretdb
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
+1 -1
View File
@@ -9,7 +9,7 @@
services:
ferretdb:
image: ghcr.io/ferretdb/ferretdb:1
image: ghcr.io/ferretdb/ferretdb
labels:
komodo.skip: # Prevent Komodo from stopping with StopAllContainers
restart: unless-stopped
-9
View File
@@ -118,15 +118,6 @@ transparent_mode = false
## Default: false
disable_non_admin_create = false
## Normally users can update their username / password using the API.
## This will disable this ability for specific users or all users.
## Example:
## - `lock_login_credentials_for = []` will allow all users to update username / password.
## - `lock_login_credentials_for = ["demo"]` will block the demo user from doing so.
## - `lock_login_credentials_for = ["__ALL__"]` will block all users.
## Default: empty list
lock_login_credentials_for = []
## Optionally provide a specific jwt secret.
## Passing nothing or an empty string will cause one to be generated on every startup.
## This means users will have to log in again if Komodo restarts.
+2 -2
View File
@@ -15,8 +15,8 @@ Komodo is able to support Postgres and Sqlite by utilizing the [FerretDB Mongo A
### First login
Core should now be accessible on the specified port, so navigating to `http://<address>:<port>` will display the login page.
Enter your preferred admin username and password, and click **"Sign Up"**, _not_ "Log In", to create your admin user for Komodo.
Any additional users to create accounts will be disabled by default, and must be enabled by an admin.
The first user to log in will be auto enabled and made an admin. Any additional users to create accounts will be disabled by default, and must be enabled by an admin.
### Https
+5 -8
View File
@@ -41,11 +41,13 @@ const config: Config = {
{
docs: {
sidebarPath: "./sidebars.ts",
editUrl: "https://github.com/moghtech/komodo/tree/main/docsite",
editUrl:
"https://github.com/moghtech/komodo/tree/main/docsite",
},
blog: {
showReadingTime: true,
editUrl: "https://github.com/moghtech/komodo/tree/main/docsite",
editUrl:
"https://github.com/moghtech/komodo/tree/main/docsite",
},
theme: {
customCss: "./src/css/custom.css",
@@ -76,12 +78,7 @@ const config: Config = {
label: "docs",
},
{
href: "https://opencollective.com/komodo",
label: "Donate",
position: "right",
},
{
href: "https://docs.rs/komodo_client/latest/komodo_client",
href: "https://docs.rs/komodo_client/latest/komodo_client/",
label: "Docs.rs",
position: "right",
},
+1 -20
View File
@@ -1,5 +1,5 @@
import { AuthResponses, ExecuteResponses, ReadResponses, UserResponses, WriteResponses } from "./responses.js";
import { AuthRequest, ExecuteRequest, ReadRequest, UpdateListItem, UserRequest, WriteRequest } from "./types.js";
import { AuthRequest, ExecuteRequest, ReadRequest, UserRequest, WriteRequest } from "./types.js";
export * as Types from "./types.js";
type InitOptions = {
type: "jwt";
@@ -13,11 +13,6 @@ type InitOptions = {
secret: string;
};
};
export declare class CancelToken {
cancelled: boolean;
constructor();
cancel(): void;
}
/** Initialize a new client for Komodo */
export declare function KomodoClient(url: string, options: InitOptions): {
/**
@@ -93,18 +88,4 @@ export declare function KomodoClient(url: string, options: InitOptions): {
}>>(type: T, params: Req["params"]) => Promise<ExecuteResponses[Req["type"]]>;
/** Returns the version of Komodo Core the client is calling to. */
core_version: () => Promise<string>;
/**
* Subscribes to the update websocket with automatic reconnect loop.
*
* Note. Awaiting this method will never finish.
*/
subscribe_to_update_websocket: ({ on_update, on_login, on_close, retry_timeout_ms, cancel, on_cancel, }: {
on_update: (update: UpdateListItem) => void;
on_login?: () => void;
on_open?: () => void;
on_close?: () => void;
retry_timeout_ms?: number;
cancel?: CancelToken;
on_cancel?: () => void;
}) => Promise<void>;
};
-65
View File
@@ -1,13 +1,4 @@
export * as Types from "./types.js";
export class CancelToken {
cancelled;
constructor() {
this.cancelled = false;
}
cancel() {
this.cancelled = true;
}
}
/** Initialize a new client for Komodo */
export function KomodoClient(url, options) {
const state = {
@@ -75,56 +66,6 @@ export function KomodoClient(url, options) {
const write = async (type, params) => await request("/write", { type, params });
const execute = async (type, params) => await request("/execute", { type, params });
const core_version = () => read("GetVersion", {}).then((res) => res.version);
const subscribe_to_update_websocket = async ({ on_update, on_login, on_close, retry_timeout_ms = 5_000, cancel = new CancelToken(), on_cancel, }) => {
while (true) {
if (cancel.cancelled) {
on_cancel?.();
return;
}
try {
const ws = new WebSocket(url.replace("http", "ws") + "/ws/update");
// Handle login on websocket open
ws.addEventListener("open", () => {
const login_msg = options.type === "jwt"
? {
type: "Jwt",
params: {
jwt: options.params.jwt,
},
}
: {
type: "ApiKeys",
params: {
key: options.params.key,
secret: options.params.secret,
},
};
ws.send(JSON.stringify(login_msg));
});
ws.addEventListener("message", ({ data }) => {
if (data == "LOGGED_IN")
return on_login?.();
on_update(JSON.parse(data));
});
if (on_close) {
ws.addEventListener("close", on_close);
}
// This while loop will end when the socket is closed
while (ws.readyState !== WebSocket.CLOSING &&
ws.readyState !== WebSocket.CLOSED) {
if (cancel.cancelled)
ws.close();
// Sleep for a bit before checking for websocket closed
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
catch (error) {
console.error(error);
// Sleep for a bit before retrying, maybe Komodo Core is down temporarily.
await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));
}
}
};
return {
/**
* Call the `/auth` api.
@@ -189,11 +130,5 @@ export function KomodoClient(url, options) {
execute,
/** Returns the version of Komodo Core the client is calling to. */
core_version,
/**
* Subscribes to the update websocket with automatic reconnect loop.
*
* Note. Awaiting this method will never finish.
*/
subscribe_to_update_websocket,
};
}
+2 -2
View File
@@ -99,8 +99,8 @@ export type ReadResponses = {
GetStack: Types.GetStackResponse;
GetStackActionState: Types.GetStackActionStateResponse;
GetStackWebhooksEnabled: Types.GetStackWebhooksEnabledResponse;
GetStackLog: Types.GetStackLogResponse;
SearchStackLog: Types.SearchStackLogResponse;
GetStackServiceLog: Types.GetStackServiceLogResponse;
SearchStackServiceLog: Types.SearchStackServiceLogResponse;
ListStacks: Types.ListStacksResponse;
ListFullStacks: Types.ListFullStacksResponse;
ListStackServices: Types.ListStackServicesResponse;
+47 -101
View File
@@ -104,7 +104,7 @@ export interface ResourceQuery<T> {
export interface ActionQuerySpecifics {
}
export type ActionQuery = ResourceQuery<ActionQuerySpecifics>;
export type AlerterEndpoint =
export type AlerterEndpoint =
/** Send alert serialized to JSON to an http endpoint. */
{
type: "Custom";
@@ -390,7 +390,7 @@ export interface BuildQuerySpecifics {
built_since?: I64;
}
export type BuildQuery = ResourceQuery<BuildQuerySpecifics>;
export type BuilderConfig =
export type BuilderConfig =
/** Use a Periphery address as a Builder. */
{
type: "Url";
@@ -421,7 +421,7 @@ export interface BuilderQuerySpecifics {
}
export type BuilderQuery = ResourceQuery<BuilderQuerySpecifics>;
/** A wrapper for all Komodo exections. */
export type Execution =
export type Execution =
/** The "null" execution. Does nothing. */
{
type: "None";
@@ -725,7 +725,7 @@ export interface JwtResponse {
export type CreateLocalUserResponse = JwtResponse;
export type CreateProcedureResponse = Procedure;
export type CreateRepoWebhookResponse = NoData;
export type UserConfig =
export type UserConfig =
/** User that logs in with username / password */
{
type: "Local";
@@ -833,7 +833,7 @@ export type DeleteStackWebhookResponse = NoData;
export type DeleteSyncWebhookResponse = NoData;
export type DeleteUserResponse = User;
export type DeleteVariableResponse = Variable;
export type DeploymentImage =
export type DeploymentImage =
/** Deploy any external image. */
{
type: "Image";
@@ -1010,7 +1010,7 @@ export declare enum SeverityLevel {
Critical = "CRITICAL"
}
/** The variants of data related to the alert. */
export type AlertData =
export type AlertData =
/** A null alert */
{
type: "None";
@@ -1455,11 +1455,6 @@ export interface ResourceSyncConfig {
* See [https://komo.do/docs/sync-resources](https://komo.do/docs/sync-resources) for more information.
*/
resource_path?: string[];
/**
* Excluse Komodo Resources (Servers / Stacks / Builds)
* from the sync. Will be variable / user group only sync.
*/
exclude_resources?: boolean;
/**
* Enable "pushes" to the file,
* which exports resources matching tags to single file.
@@ -1481,7 +1476,7 @@ export interface ResourceSyncConfig {
/** Manage the file contents in the UI. */
file_contents?: string;
}
export type DiffData =
export type DiffData =
/** Resource will be created */
{
type: "Create";
@@ -1646,7 +1641,7 @@ export interface ServerConfig {
}
export type Server = Resource<ServerConfig, undefined>;
export type GetServerResponse = Server;
export type ServerTemplateConfig =
export type ServerTemplateConfig =
/** Template to launch an AWS EC2 instance */
{
type: "Aws";
@@ -1670,7 +1665,6 @@ export interface StackActionState {
destroying: boolean;
}
export type GetStackActionStateResponse = StackActionState;
export type GetStackLogResponse = Log;
/** The compose file configuration. */
export interface StackConfig {
/** The server to deploy the stack on. */
@@ -1705,13 +1699,6 @@ export interface StackConfig {
* enable both.
*/
auto_update?: boolean;
/**
* If auto update is enabled, Komodo will
* by default only update the specific services
* with image updates. If this parameter is set to true,
* Komodo will redeploy the whole Stack (all services).
*/
auto_update_all_services?: boolean;
/** Whether to run `docker compose down` before `compose up`. */
destroy_before_deploy?: boolean;
/** Whether to skip secret interpolation into the stack environment variables. */
@@ -1788,7 +1775,7 @@ export interface StackConfig {
registry_account?: string;
/** The optional command to run before the Stack is deployed. */
pre_deploy?: SystemCommand;
/** The optional command to run after the Stack is deployed. */
/** The optional command to run before the Stack is deployed. */
post_deploy?: SystemCommand;
/**
* The extra arguments to pass after `docker compose up -d`.
@@ -1870,21 +1857,13 @@ export interface StackInfo {
deployed_hash?: string;
/** Deployed commit message, or null. Only for repo based stacks */
deployed_message?: string;
/**
* The deployed compose file contents.
* This is updated whenever Komodo successfully deploys the stack.
*/
/** The deployed compose file contents. This is updated whenever Komodo successfully deploys the stack. */
deployed_contents?: FileContents[];
/**
* The deployed service names.
* This is updated whenever it is empty, or deployed contents is updated.
*/
deployed_services?: StackServiceNames[];
/**
* The output of `docker compose config`.
* This is updated whenever Komodo successfully deploys the stack.
*/
deployed_config?: string;
/**
* The latest service names.
* This is updated whenever the stack cache refreshes, using the latest file contents (either db defined or remote).
@@ -1905,6 +1884,7 @@ export interface StackInfo {
}
export type Stack = Resource<StackConfig, StackInfo>;
export type GetStackResponse = Stack;
export type GetStackServiceLogResponse = Log;
/** System information of a server */
export interface SystemInformation {
/** The system name */
@@ -3057,8 +3037,8 @@ export interface ProviderAccount {
export interface DockerRegistry {
/** The docker provider domain. Default: `docker.io`. */
domain: string;
/** The accounts on the registry. Required. */
accounts: ProviderAccount[];
/** The account username. Required. */
accounts?: ProviderAccount[];
/**
* Available organizations on the registry provider.
* Used to push an image under an organization's repo rather than an account's repo.
@@ -3097,11 +3077,11 @@ export interface GitProvider {
domain: string;
/** Whether to use https. Default: true. */
https: boolean;
/** The accounts on the git provider. Required. */
/** The account username. Required. */
accounts: ProviderAccount[];
}
export type ListGitProvidersFromConfigResponse = GitProvider[];
export type UserTarget =
export type UserTarget =
/** User Id */
{
type: "User";
@@ -3388,7 +3368,7 @@ export interface ResourceSyncQuerySpecifics {
export type ResourceSyncQuery = ResourceQuery<ResourceSyncQuerySpecifics>;
export type SearchContainerLogResponse = Log;
export type SearchDeploymentLogResponse = Log;
export type SearchStackLogResponse = Log;
export type SearchStackServiceLogResponse = Log;
export interface ServerQuerySpecifics {
}
/** Server-specific query */
@@ -4455,11 +4435,8 @@ export interface Deploy {
export interface DeployStack {
/** Id or name */
stack: string;
/**
* Filter to only deploy specific services.
* If empty, will deploy all services.
*/
services?: string[];
/** Optionally specify a specific service to "compose up" */
service?: string;
/**
* Override the default termination max time.
* Only used if the stack needs to be taken down first.
@@ -4514,11 +4491,8 @@ export interface DestroyDeployment {
export interface DestroyStack {
/** Id or name */
stack: string;
/**
* Filter to only destroy specific services.
* If empty, will destroy all services.
*/
services?: string[];
/** Optionally specify a specific service to destroy */
service?: string;
/** Pass `--remove-orphans` */
remove_orphans?: boolean;
/** Override the default termination max time. */
@@ -5111,19 +5085,12 @@ export interface GetStackActionState {
/** Id or name */
stack: string;
}
/**
* Get a stack's logs. Filter down included services. Response: [GetStackLogResponse].
*
* Note. This call will hit the underlying server directly for most up to date log.
*/
export interface GetStackLog {
/** Get a stack service's log. Response: [GetStackServiceLogResponse]. */
export interface GetStackServiceLog {
/** Id or name */
stack: string;
/**
* Filter the logs to only ones from specific services.
* If empty, will include logs from all services.
*/
services: string[];
/** The service to get the log for. */
service: string;
/**
* The number of lines of the log tail to include.
* Default: 100.
@@ -5762,7 +5729,7 @@ export interface ListStackServices {
}
/** List stacks matching optional query. Response: [ListStacksResponse]. */
export interface ListStacks {
/** optional structured query to filter stacks. */
/** optional structured query to filter syncs. */
query?: StackQuery;
}
/**
@@ -5916,11 +5883,8 @@ export interface PauseDeployment {
export interface PauseStack {
/** Id or name */
stack: string;
/**
* Filter to only pause specific services.
* If empty, will pause all services.
*/
services?: string[];
/** Optionally specify a specific service to pause */
service?: string;
}
export interface PermissionToml {
/**
@@ -6038,11 +6002,8 @@ export interface PullRepo {
export interface PullStack {
/** Id or name */
stack: string;
/**
* Filter to only pull specific services.
* If empty, will pull all services.
*/
services?: string[];
/** Optionally specify a specific service to start */
service?: string;
}
/**
* Push a resource to the front of the users 10 most recently viewed resources.
@@ -6287,11 +6248,8 @@ export interface RestartDeployment {
export interface RestartStack {
/** Id or name */
stack: string;
/**
* Filter to only restart specific services.
* If empty, will restart all services.
*/
services?: string[];
/** Optionally specify a specific service to restart */
service?: string;
}
/** Runs the target Action. Response: [Update] */
export interface RunAction {
@@ -6384,19 +6342,16 @@ export interface SearchDeploymentLog {
timestamps?: boolean;
}
/**
* Search the stack log's tail using `grep`. All lines go to stdout.
* Response: [SearchStackLogResponse].
* Search the deployment log's tail using `grep`. All lines go to stdout.
* Response: [Log].
*
* Note. This call will hit the underlying server directly for most up to date log.
*/
export interface SearchStackLog {
export interface SearchStackServiceLog {
/** Id or name */
stack: string;
/**
* Filter the logs to only ones from specific services.
* If empty, will include logs from all services.
*/
services: string[];
/** The service to get the log for. */
service: string;
/** The terms to search for. */
terms: string[];
/**
@@ -6491,11 +6446,8 @@ export interface StartDeployment {
export interface StartStack {
/** Id or name */
stack: string;
/**
* Filter to only start specific services.
* If empty, will start all services.
*/
services?: string[];
/** Optionally specify a specific service to start */
service?: string;
}
/** Stops all containers on the target server. Response: [Update] */
export interface StopAllContainers {
@@ -6536,11 +6488,8 @@ export interface StopStack {
stack: string;
/** Override the default termination max time. */
stop_time?: number;
/**
* Filter to only stop specific services.
* If empty, will stop all services.
*/
services?: string[];
/** Optionally specify a specific service to stop */
service?: string;
}
export interface TerminationSignalLabel {
signal: TerminationSignal;
@@ -6595,11 +6544,8 @@ export interface UnpauseDeployment {
export interface UnpauseStack {
/** Id or name */
stack: string;
/**
* Filter to only unpause specific services.
* If empty, will unpause all services.
*/
services?: string[];
/** Optionally specify a specific service to unpause */
service?: string;
}
/**
* Update the action at the given id, and return the updated action.
@@ -7147,7 +7093,7 @@ export type ExecuteRequest = {
params: RunSync;
};
/** Configuration for the registry to push the built image to. */
export type ImageRegistryLegacy1_14 =
export type ImageRegistryLegacy1_14 =
/** Don't push the image to any registry */
{
type: "None";
@@ -7417,11 +7363,11 @@ export type ReadRequest = {
type: "GetStackWebhooksEnabled";
params: GetStackWebhooksEnabled;
} | {
type: "GetStackLog";
params: GetStackLog;
type: "GetStackServiceLog";
params: GetStackServiceLog;
} | {
type: "SearchStackLog";
params: SearchStackLog;
type: "SearchStackServiceLog";
params: SearchStackServiceLog;
} | {
type: "ListStacks";
params: ListStacks;
+1 -1
View File
@@ -19,7 +19,7 @@ export const TopbarAlerts = () => {
const { data } = useRead(
"ListAlerts",
{ query: { resolved: false } },
{ refetchInterval: 3_000 }
{ refetchInterval: 10_000 }
);
const [open, setOpen] = useState(false);
+32 -77
View File
@@ -25,7 +25,7 @@ export const ConfigLayout = <
T extends Types.Resource<unknown, unknown>["config"],
>({
original,
update,
config,
children,
disabled,
onConfirm,
@@ -35,7 +35,7 @@ export const ConfigLayout = <
file_contents_language,
}: {
original: T;
update: Partial<T>;
config: Partial<T>;
children: ReactNode;
disabled: boolean;
onConfirm: () => void;
@@ -47,7 +47,7 @@ export const ConfigLayout = <
const titleProps = titleOther
? { titleOther }
: { title: "Config", icon: <Settings className="w-4 h-4" /> };
const changesMade = Object.keys(update).length ? true : false;
const changesMade = Object.keys(config).length ? true : false;
return (
<Section
{...titleProps}
@@ -61,25 +61,24 @@ export const ConfigLayout = <
)}
{selector}
{changesMade && (
<>
<Button
variant="outline"
onClick={onReset}
disabled={disabled || !changesMade}
className="flex items-center gap-2"
>
<History className="w-4 h-4" />
Reset
</Button>
<ConfirmUpdate
previous={original}
content={update}
onConfirm={async () => onConfirm()}
disabled={disabled}
file_contents_language={file_contents_language}
key_listener
/>
</>
<Button
variant="outline"
onClick={onReset}
disabled={disabled || !changesMade}
className="flex items-center gap-2"
>
<History className="w-4 h-4" />
Reset
</Button>
)}
{changesMade && (
<ConfirmUpdate
previous={original}
content={config}
onConfirm={async () => onConfirm()}
disabled={disabled}
file_contents_language={file_contents_language}
/>
)}
</div>
}
@@ -115,7 +114,7 @@ export type ConfigComponent<T> = {
};
export const Config = <T,>({
original,
config,
update,
disabled,
disableSidebar,
@@ -126,7 +125,7 @@ export const Config = <T,>({
titleOther,
file_contents_language,
}: {
original: T;
config: T;
update: Partial<T>;
disabled: boolean;
disableSidebar?: boolean;
@@ -141,20 +140,18 @@ export const Config = <T,>({
file_contents_language?: MonacoLanguage;
}) => {
const sections = keys(components).filter((section) => !!components[section]);
const changesMade = Object.keys(update).length ? true : false;
const onConfirm = async () => {
await onSave();
set({});
};
const onReset = () => set({});
return (
<ConfigLayout
original={original}
original={config}
titleOther={titleOther}
update={update}
config={update}
disabled={disabled}
onConfirm={onConfirm}
onReset={onReset}
onConfirm={async () => {
await onSave();
set({});
}}
onReset={() => set({})}
selector={selector}
file_contents_language={file_contents_language}
>
@@ -191,24 +188,6 @@ export const Config = <T,>({
</div>
</div>
))}
<div className="flex flex-col gap-2">
<ConfirmUpdate
previous={original}
content={update}
onConfirm={onConfirm}
disabled={disabled || !changesMade}
file_contents_language={file_contents_language}
/>
<Button
variant="outline"
onClick={onReset}
disabled={disabled || !changesMade}
className="flex items-center gap-2"
>
<History className="w-4 h-4" />
Reset
</Button>
</div>
</div>
</div>
)}
@@ -296,7 +275,7 @@ export const Config = <T,>({
)}
{!contentHidden && (
<ConfigAgain
config={original}
config={config}
update={update}
set={(u) => set((p) => ({ ...p, ...u }))}
components={components}
@@ -310,30 +289,6 @@ export const Config = <T,>({
</div>
)
)}
{changesMade && (
<div className="flex gap-2 justify-end">
<div className="text-muted-foreground flex items-center gap-2">
<AlertTriangle className="w-4 h-4" /> Unsaved changes
<AlertTriangle className="w-4 h-4" />
</div>
<Button
variant="outline"
onClick={onReset}
disabled={disabled}
className="flex items-center gap-2"
>
<History className="w-4 h-4" />
Reset
</Button>
<ConfirmUpdate
previous={original}
content={update}
onConfirm={onConfirm}
disabled={disabled}
file_contents_language={file_contents_language}
/>
</div>
)}
</div>
</div>
</ConfigLayout>
+1 -4
View File
@@ -600,7 +600,6 @@ interface ConfirmUpdateProps<T> {
disabled: boolean;
language?: MonacoLanguage;
file_contents_language?: MonacoLanguage;
key_listener?: boolean;
}
export function ConfirmUpdate<T>({
@@ -611,11 +610,9 @@ export function ConfirmUpdate<T>({
disabled,
language,
file_contents_language,
key_listener = false,
}: ConfirmUpdateProps<T>) {
const [open, set] = useState(false);
useCtrlKeyListener("Enter", () => {
if (!key_listener) return;
if (open) {
onConfirm();
} else {
@@ -628,7 +625,7 @@ export function ConfirmUpdate<T>({
<Button
onClick={() => set(true)}
disabled={disabled}
className="flex items-center gap-2 w-100"
className="flex items-center gap-2"
>
<Save className="w-4 h-4" />
Save
+3 -168
View File
@@ -1,174 +1,9 @@
import { logToHtml } from "@lib/utils";
import { Types } from "komodo_client";
import { Button } from "@ui/button";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@ui/select";
import {
AlertOctagon,
ChevronDown,
RefreshCw,
ScrollText,
X,
} from "lucide-react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { Section } from "./layouts";
import { Switch } from "@ui/switch";
import { Input } from "@ui/input";
import { ToggleGroup, ToggleGroupItem } from "@ui/toggle-group";
import { useToast } from "@ui/use-toast";
import { useLocalStorage } from "@lib/hooks";
export type LogStream = "stdout" | "stderr";
export const LogSection = ({
regular_logs,
search_logs,
titleOther,
extraParams,
}: {
regular_logs: (
timestamps: boolean,
stream: LogStream,
tail: number
) => {
Log: ReactNode;
refetch: () => void;
stderr: boolean;
};
search_logs: (
timestamps: boolean,
terms: string[],
invert: boolean
) => { Log: ReactNode; refetch: () => void; stderr: boolean };
titleOther?: ReactNode;
extraParams?: ReactNode;
}) => {
const { toast } = useToast();
const [timestamps, setTimestamps] = useLocalStorage(
"log-timestamps-v1",
false
);
const [stream, setStream] = useState<LogStream>("stdout");
const [tail, set] = useState("100");
const [terms, setTerms] = useState<string[]>([]);
const [invert, setInvert] = useState(false);
const [search, setSearch] = useState("");
const [poll, setPoll] = useLocalStorage("log-poll-v1", false);
const addTerm = () => {
if (!search.length) return;
if (terms.includes(search)) {
toast({ title: "Search term is already present" });
setSearch("");
return;
}
setTerms([...terms, search]);
setSearch("");
};
const clearSearch = () => {
setSearch("");
setTerms([]);
};
const { Log, refetch, stderr } = terms.length
? search_logs(timestamps, terms, invert)
: regular_logs(timestamps, stream, Number(tail));
return (
<Section
title={titleOther ? undefined : "Log"}
icon={titleOther ? undefined : <ScrollText className="w-4 h-4" />}
titleOther={titleOther}
itemsCenterTitleRow
actions={
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
<div className="text-muted-foreground flex gap-1 text-sm">
Invert
</div>
<Switch checked={invert} onCheckedChange={setInvert} />
</div>
{terms.map((term, index) => (
<Button
key={term}
variant="destructive"
onClick={() => setTerms(terms.filter((_, i) => i !== index))}
className="flex gap-2 items-center py-0 px-2"
>
{term}
<X className="w-4 h-h" />
</Button>
))}
<div className="relative">
<Input
placeholder="Search Logs"
value={search}
onChange={(e) => setSearch(e.target.value)}
onBlur={addTerm}
onKeyDown={(e) => {
if (e.key === "Enter") addTerm();
}}
className="w-[180px] xl:w-[240px]"
/>
<Button
variant="ghost"
size="icon"
onClick={clearSearch}
className="absolute right-0 top-1/2 -translate-y-1/2"
>
<X className="w-4 h-4" />
</Button>
</div>
<ToggleGroup
type="single"
value={stream}
onValueChange={setStream as any}
>
<ToggleGroupItem value="stdout">stdout</ToggleGroupItem>
<ToggleGroupItem value="stderr">
stderr
{stderr && (
<AlertOctagon className="w-4 h-4 ml-2 stroke-red-500" />
)}
</ToggleGroupItem>
</ToggleGroup>
<Button variant="secondary" size="icon" onClick={() => refetch()}>
<RefreshCw className="w-4 h-4" />
</Button>
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => setTimestamps((t) => !t)}
>
<div className="text-muted-foreground text-sm">Timestamps</div>
<Switch checked={timestamps} />
</div>
<div
className="flex items-center gap-2 cursor-pointer"
onClick={() => setPoll((p) => !p)}
>
<div className="text-muted-foreground text-sm">Poll</div>
<Switch checked={poll} />
</div>
<TailLengthSelector
selected={tail}
onSelect={set}
disabled={search.length > 0}
/>
{extraParams}
</div>
}
>
{Log}
</Section>
);
};
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@ui/select";
import { ChevronDown } from "lucide-react";
import { useEffect, useRef } from "react";
export const Log = ({
log,
@@ -46,7 +46,7 @@ export const ActionConfig = ({ id }: { id: string }) => {
<Config
disabled={disabled}
disableSidebar
original={config}
config={config}
update={update}
set={set}
onSave={async () => {
@@ -24,7 +24,7 @@ export const AlerterConfig = ({ id }: { id: string }) => {
return (
<Config
disabled={disabled}
original={config}
config={config}
update={update}
set={set}
onSave={async () => {

Some files were not shown because too many files have changed in this diff Show More