Compare commits

..

32 Commits

Author SHA1 Message Date
Maxwell Becker
f9b2994d44 1.16.2 (#145)
* Env vars written using same quotes (single vs double) as the user passes

* fmt

* trim start matches '-'

* ts client version
2024-10-22 11:41:17 -07:00
mbecker20
c0d6d96b64 get username works for service users 2024-10-22 03:36:20 -04:00
mbecker20
34496b948a bump ts client to 1.16.1 2024-10-22 02:58:42 -04:00
mbecker20
90c6adf923 fix periphery installer force file recreation command 2024-10-22 02:55:39 -04:00
mbecker20
3b72dc65cc remove "Overviews" label from sidebar 2024-10-22 02:27:22 -04:00
mbecker20
05f38d02be bump version to 1.16.1 2024-10-22 02:21:16 -04:00
Maxwell Becker
ea5506c202 1.16.1 (#143)
* ensure sync state cache is refreshed on sync create / copy

* clean up resources post_create

* show sidebar if element length > 1

* update `run_komodo_command` command

* rename all resources

* refresh repo cache after clone / pull

* improve rename repo log
2024-10-21 23:19:40 -07:00
mbecker20
64b0a5c9d2 delete unrelated caddy compose 2024-10-21 00:30:54 -04:00
mbecker20
93cc6a3a6e Add running Action to dashboard "Active" 2024-10-21 00:21:17 -04:00
mbecker20
7ae69cf33b ignore top level return linting 2024-10-20 05:10:28 -04:00
mbecker20
404e00cc64 move action info inline 2024-10-20 04:43:29 -04:00
mbecker20
6fe5bc7420 properly host client lib for deno importing (types working) 2024-10-20 03:17:58 -04:00
mbecker20
82324b00ee typescript komodo_client v 1.16.0 2024-10-20 02:35:43 -04:00
Maxwell Becker
5daba3a557 1.16.0 (#140)
* consolidate deserializers

* key value list doc

* use string list deserializers for all entity Vec<String>

* add additional env files support

* plumbing for Action resource

* js client readme indentation

* regen lock

* add action UI

* action backend

* start on action frontend

* update lock

* get up to speed

* get action started

* clean up default action file

* seems to work

* toml export include action

* action works

* action works part 2

* bump rust version to 1.82.0

* copy deno bin from bin image

* action use local dir

* update not having changes doesn't return error

* format with prettier

* support yaml formatting with prettier

* variable no change is Ok
2024-10-19 23:27:28 -07:00
mbecker20
020cdc06fd remove migrator link in readme 2024-10-18 21:23:02 -04:00
Maxwell Becker
cb270f4dff 1.15.12 (#139)
* add containers link to mobile dropdown

* fix update / alert not showing permission issue

* prevent disk alert back and forth

* improve user group pending toml
2024-10-18 17:14:22 -07:00
Matt Foxx
21666cf9b3 feat: Add docs link to topbar (#134) 2024-10-18 16:10:01 -07:00
mbecker20
a417926690 1.15.11 allow adding stack to user group 2024-10-16 23:09:06 -04:00
mbecker20
293b36fae4 Allow adding Stack to User Group 2024-10-16 23:08:44 -04:00
mbecker20
dca37e9ba8 1.15.10 connect with http using DOCKER_HOST 2024-10-16 22:16:07 -04:00
Morgan Wyatt
1cc302fcbf Update docker.rs to allow http docker socket connection (#131)
* Update docker.rs to allow http docker socket connection

Add or_else to allow attempt to connect to docker socket proxy via http if local connection fails

* Update docker.rs

Change two part connection to use connect_with_defaults instead, per review on PR.
2024-10-16 19:13:19 -07:00
mbecker20
febcf739d0 Remove Comma from installer: thanks @PiotrBzdrega 2024-10-16 10:43:54 -04:00
mbecker20
cb79e00794 update systemd service file 2024-10-15 17:35:54 -04:00
mbecker20
869b397596 force service file recreation docs 2024-10-15 17:25:29 -04:00
Maxwell Becker
41d1ff9760 1.15.9 (#127)
* add close alert threshold to prevent Ok - Warning back and forth

* remove part about repo being deleted, no longer behavior

* resource sync share general common

* remove this changelog. use releases

* remove changelog from readme

* write commit file clean up path

* docs: supports any git provider repo

* fix docs: authorization

* multiline command supports escaped newlines

* move webhook to build config advanced

* parser comments with escaped newline

* improve parser

* save use Enter. escape monaco using escape

* improve logic when deployment / stack action buttons shown

* used_mem = total - available

* Fix unrecognized path have 404

* webhooks will 404 if misconfigured

* move update logger / alerter

* delete migrator

* update examples

* publish typescript client komodo_client
2024-10-14 23:04:49 -07:00
mbecker20
dfafadf57b demo / build username pw 2024-10-14 11:49:44 -04:00
mbecker20
538a79b8b5 fix upausing all container action state 2024-10-13 18:11:09 -04:00
Maxwell Becker
5088dc5c3c 1.15.8 (#124)
* fix all containers restart and unpause

* add CommitSync to Procedure

* validate resource query tags causes failure on non exist

* files on host init working. match tags fail if tag doesnt exist

* intelligent sync match tag selector

* fix linting

* Wait for user initialize file on host
2024-10-13 15:03:16 -07:00
mbecker20
581d7e0b2c fix Procedure sync log 2024-10-13 04:21:03 -04:00
mbecker20
657298041f remove unneeded syncs volume 2024-10-13 04:03:09 -04:00
mbecker20
d71e9dca11 fix version 2024-10-13 03:21:56 -04:00
Maxwell Becker
165131bdf8 1.15.7 (#119)
* 1.15.7-dev ensure git config set

* add username to commit msg
2024-10-13 00:01:14 -07:00
315 changed files with 20574 additions and 9160 deletions

487
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,15 @@
[workspace]
resolver = "2"
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
members = [
"bin/*",
"lib/*",
"example/*",
"client/core/rs",
"client/periphery/rs",
]
[workspace.package]
version = "1.15.6"
version = "1.16.2"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -15,7 +21,7 @@ homepage = "https://komo.do"
[workspace.dependencies]
# LOCAL
# komodo_client = "1.14.3"
# komodo_client = "1.15.6"
komodo_client = { path = "client/core/rs" }
periphery_client = { path = "client/periphery/rs" }
environment_file = { path = "lib/environment_file" }
@@ -58,12 +64,12 @@ tokio-tungstenite = "0.24.0"
ordered_hash_map = { version = "0.4.0", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }
strum = { version = "0.26.3", features = ["derive"] }
serde_json = "1.0.128"
serde_json = "1.0.132"
serde_yaml = "0.9.34"
toml = "0.8.19"
# ERROR
anyhow = "1.0.89"
anyhow = "1.0.90"
thiserror = "1.0.64"
# LOGGING
@@ -108,4 +114,4 @@ octorust = "0.7.0"
dashmap = "6.1.0"
colored = "2.1.0"
regex = "1.11.0"
bson = "2.13.0"
bson = "2.13.0"

View File

@@ -21,6 +21,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::None(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunAction(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::RunProcedure(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -129,6 +132,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::RunSync(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::CommitSync(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
Execution::DeployStack(data) => {
println!("{}: {data:?}", "Data".dimmed())
}
@@ -165,6 +171,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
info!("Running Execution...");
let res = match execution {
Execution::RunAction(request) => {
komodo_client().execute(request).await
}
Execution::RunProcedure(request) => {
komodo_client().execute(request).await
}
@@ -273,6 +282,9 @@ pub async fn run(execution: Execution) -> anyhow::Result<()> {
Execution::RunSync(request) => {
komodo_client().execute(request).await
}
Execution::CommitSync(request) => {
komodo_client().write(request).await
}
Execution::DeployStack(request) => {
komodo_client().execute(request).await
}

View File

@@ -19,6 +19,7 @@ komodo_client = { workspace = true, features = ["mongo"] }
periphery_client.workspace = true
environment_file.workspace = true
formatting.workspace = true
command.workspace = true
logger.workspace = true
git.workspace = true
# mogh

View File

@@ -4,7 +4,7 @@
## and may negatively affect runtime performance.
# Build Core
FROM rust:1.81.0-alpine AS core-builder
FROM rust:1.82.0-alpine AS core-builder
WORKDIR /builder
RUN apk update && apk --no-cache add musl-dev openssl-dev openssl-libs-static
COPY . .
@@ -16,14 +16,14 @@ WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link @komodo/client && yarn && yarn build
RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM alpine:3.20
# Install Deps
RUN apk update && apk add --no-cache --virtual .build-deps \
openssl ca-certificates git git-lfs
openssl ca-certificates git git-lfs curl
# Setup an application directory
WORKDIR /app
@@ -32,6 +32,7 @@ WORKDIR /app
COPY ./config/core.config.toml /config/config.toml
COPY --from=core-builder /builder/target/release/core /app
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
# Hint at the port
EXPOSE 9120

View File

@@ -1,5 +1,5 @@
# Build Core
FROM rust:1.81.0-bullseye AS core-builder
FROM rust:1.82.0-bullseye AS core-builder
WORKDIR /builder
COPY . .
RUN cargo build -p komodo_core --release
@@ -10,7 +10,7 @@ WORKDIR /builder
COPY ./frontend ./frontend
COPY ./client/core/ts ./client
RUN cd client && yarn && yarn build && yarn link
RUN cd frontend && yarn link @komodo/client && yarn && yarn build
RUN cd frontend && yarn link komodo_client && yarn && yarn build
# Final Image
FROM debian:bullseye-slim
@@ -27,6 +27,7 @@ WORKDIR /app
COPY ./config/core.config.toml /config/config.toml
COPY --from=core-builder /builder/target/release/core /app
COPY --from=frontend-builder /builder/frontend/dist /app/frontend
COPY --from=denoland/deno:bin /deno /usr/local/bin/deno
# Hint at the port
EXPOSE 9120

View File

@@ -201,6 +201,9 @@ fn resource_link(
ResourceTargetVariant::Procedure => {
format!("/procedures/{id}")
}
ResourceTargetVariant::Action => {
format!("/actions/{id}")
}
ResourceTargetVariant::ServerTemplate => {
format!("/server-templates/{id}")
}

View File

@@ -0,0 +1,209 @@
use std::collections::HashSet;
use anyhow::Context;
use command::run_komodo_command;
use komodo_client::{
api::{
execute::RunAction,
user::{CreateApiKey, CreateApiKeyResponse, DeleteApiKey},
},
entities::{
action::Action,
config::core::CoreConfig,
permission::PermissionLevel,
update::Update,
user::{action_user, User},
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
config::core_config,
helpers::{
interpolate::{
add_interp_update_log,
interpolate_variables_secrets_into_string,
},
query::get_variables_and_secrets,
random_string,
update::update_update,
},
resource::{self, refresh_action_state_cache},
state::{action_states, db_client, State},
};
impl Resolve<RunAction, (User, Update)> for State {
async fn resolve(
&self,
RunAction { action }: RunAction,
(user, mut update): (User, Update),
) -> anyhow::Result<Update> {
let mut action = resource::get_check_permissions::<Action>(
&action,
&user,
PermissionLevel::Execute,
)
.await?;
// get the action state for the action (or insert default).
let action_state = action_states()
.action
.get_or_insert_default(&action.id)
.await;
// This will set action state back to default when dropped.
// Will also check to ensure action not already busy before updating.
let _action_guard =
action_state.update(|state| state.running = true)?;
update_update(update.clone()).await?;
let CreateApiKeyResponse { key, secret } = State
.resolve(
CreateApiKey {
name: update.id.clone(),
expires: 0,
},
action_user().to_owned(),
)
.await?;
let contents = &mut action.config.file_contents;
// Wrap the file contents in the execution context.
*contents = full_contents(contents, &key, &secret);
let replacers =
interpolate(contents, &mut update, key.clone(), secret.clone())
.await?
.into_iter()
.collect::<Vec<_>>();
let path = core_config()
.action_directory
.join(format!("{}.ts", random_string(10)));
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent).await;
}
fs::write(&path, contents).await.with_context(|| {
format!("Faild to write action file to {path:?}")
})?;
let mut res = run_komodo_command(
// Keep this stage name as is, the UI will find the latest update log by matching the stage name
"Execute Action",
None,
format!(
"deno run --allow-read --allow-net --allow-import {}",
path.display()
),
false,
)
.await;
res.stdout = svi::replace_in_string(&res.stdout, &replacers)
.replace(&key, "<ACTION_API_KEY>");
res.stderr = svi::replace_in_string(&res.stderr, &replacers)
.replace(&secret, "<ACTION_API_SECRET>");
if let Err(e) = fs::remove_file(path).await {
warn!(
"Failed to delete action file after action execution | {e:#}"
);
}
if let Err(e) = State
.resolve(DeleteApiKey { key }, action_user().to_owned())
.await
{
warn!(
"Failed to delete API key after action execution | {e:#}"
);
};
update.logs.push(res);
update.finalize();
// Need to manually update the update before cache refresh,
// and before broadcast with update_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_action_state_cache().await;
}
update_update(update.clone()).await?;
Ok(update)
}
}
async fn interpolate(
contents: &mut String,
update: &mut Update,
key: String,
secret: String,
) -> anyhow::Result<HashSet<(String, String)>> {
let mut vars_and_secrets = get_variables_and_secrets().await?;
vars_and_secrets
.secrets
.insert(String::from("ACTION_API_KEY"), key);
vars_and_secrets
.secrets
.insert(String::from("ACTION_API_SECRET"), secret);
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
contents,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(update, &global_replacers, &secret_replacers);
Ok(secret_replacers)
}
fn full_contents(contents: &str, key: &str, secret: &str) -> String {
let CoreConfig {
port, ssl_enabled, ..
} = core_config();
let protocol = if *ssl_enabled { "https" } else { "http" };
let base_url = format!("{protocol}://localhost:{port}");
format!(
"import {{ KomodoClient }} from '{base_url}/client/lib.js';
const komodo = KomodoClient('{base_url}', {{
type: 'api-key',
params: {{ key: '{key}', secret: '{secret}' }}
}});
async function main() {{{contents}}}
main().catch(error => {{
console.error('🚨 Action exited early with errors 🚨')
if (error.status !== undefined && error.result !== undefined) {{
console.error('Status:', error.status);
console.error(JSON.stringify(error.result, null, 2));
}} else {{
console.error(JSON.stringify(error, null, 2));
}}
Deno.exit(1)
}}).then(() => console.log('🦎 Action completed successfully 🦎'));"
)
}

View File

@@ -24,6 +24,7 @@ use crate::{
state::{db_client, State},
};
mod action;
mod build;
mod deployment;
mod procedure;
@@ -97,6 +98,9 @@ pub enum ExecuteRequest {
// ==== PROCEDURE ====
RunProcedure(RunProcedure),
// ==== ACTION ====
RunAction(RunAction),
// ==== SERVER TEMPLATE ====
LaunchServer(LaunchServer),

View File

@@ -3,7 +3,7 @@ use std::{collections::HashSet, future::IntoFuture, time::Duration};
use anyhow::{anyhow, Context};
use formatting::format_serror;
use komodo_client::{
api::execute::*,
api::{execute::*, write::RefreshRepoCache},
entities::{
alert::{Alert, AlertData, SeverityLevel},
builder::{Builder, BuilderConfig},
@@ -123,6 +123,17 @@ impl Resolve<CloneRepo, (User, Update)> for State {
update_last_pulled_time(&repo.name).await;
}
if let Err(e) = State
.resolve(RefreshRepoCache { repo: repo.id }, user)
.await
.context("Failed to refresh repo cache")
{
update.push_error_log(
"Refresh Repo cache",
format_serror(&e.into()),
);
};
handle_server_update_return(update).await
}
}
@@ -207,6 +218,17 @@ impl Resolve<PullRepo, (User, Update)> for State {
update_last_pulled_time(&repo.name).await;
}
if let Err(e) = State
.resolve(RefreshRepoCache { repo: repo.id }, user)
.await
.context("Failed to refresh repo cache")
{
update.push_error_log(
"Refresh Repo cache",
format_serror(&e.into()),
);
};
handle_server_update_return(update).await
}
}

View File

@@ -425,7 +425,7 @@ impl Resolve<RestartAllContainers, (User, Update)> for State {
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
.request(api::container::StartAllContainers {})
.request(api::container::RestartAllContainers {})
.await
.context("failed to restart all containers on host")?;
@@ -520,12 +520,12 @@ impl Resolve<UnpauseAllContainers, (User, Update)> for State {
// Will check to ensure server not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard = action_state
.update(|state| state.starting_containers = true)?;
.update(|state| state.unpausing_containers = true)?;
update_update(update.clone()).await?;
let logs = periphery_client(&server)?
.request(api::container::StartAllContainers {})
.request(api::container::UnpauseAllContainers {})
.await
.context("failed to unpause all containers on host")?;

View File

@@ -6,6 +6,7 @@ use komodo_client::{
api::{execute::RunSync, write::RefreshResourceSyncPending},
entities::{
self,
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
@@ -126,6 +127,10 @@ impl Resolve<RunSync, (User, Update)> for State {
.procedures
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Action => all_resources
.actions
.get(&name_or_id)
.map(|p| p.name.clone()),
ResourceTargetVariant::Repo => all_resources
.repos
.get(&name_or_id)
@@ -270,6 +275,17 @@ impl Resolve<RunSync, (User, Update)> for State {
&sync.config.match_tags,
)
.await?;
let (actions_to_create, actions_to_update, actions_to_delete) =
get_updates_for_execution::<Action>(
resources.actions,
delete,
&all_resources,
match_resource_type,
match_resources.as_deref(),
&id_to_tags,
&sync.config.match_tags,
)
.await?;
let (builders_to_create, builders_to_update, builders_to_delete) =
get_updates_for_execution::<Builder>(
resources.builders,
@@ -388,6 +404,9 @@ impl Resolve<RunSync, (User, Update)> for State {
&& procedures_to_create.is_empty()
&& procedures_to_update.is_empty()
&& procedures_to_delete.is_empty()
&& actions_to_create.is_empty()
&& actions_to_update.is_empty()
&& actions_to_delete.is_empty()
&& user_groups_to_create.is_empty()
&& user_groups_to_update.is_empty()
&& user_groups_to_delete.is_empty()
@@ -464,6 +483,15 @@ impl Resolve<RunSync, (User, Update)> for State {
)
.await,
);
maybe_extend(
&mut update.logs,
Action::execute_sync_updates(
actions_to_create,
actions_to_update,
actions_to_delete,
)
.await,
);
// Dependent on server
maybe_extend(

View File

@@ -0,0 +1,132 @@
use anyhow::Context;
use komodo_client::{
api::read::*,
entities::{
action::{
Action, ActionActionState, ActionListItem, ActionState,
},
permission::PermissionLevel,
user::User,
},
};
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{action_state_cache, action_states, State},
};
impl Resolve<GetAction, User> for State {
async fn resolve(
&self,
GetAction { action }: GetAction,
user: User,
) -> anyhow::Result<Action> {
resource::get_check_permissions::<Action>(
&action,
&user,
PermissionLevel::Read,
)
.await
}
}
impl Resolve<ListActions, User> for State {
async fn resolve(
&self,
ListActions { query }: ListActions,
user: User,
) -> anyhow::Result<Vec<ActionListItem>> {
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Action>(query, &user, &all_tags).await
}
}
impl Resolve<ListFullActions, User> for State {
async fn resolve(
&self,
ListFullActions { query }: ListFullActions,
user: User,
) -> anyhow::Result<ListFullActionsResponse> {
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Action>(query, &user, &all_tags)
.await
}
}
impl Resolve<GetActionActionState, User> for State {
async fn resolve(
&self,
GetActionActionState { action }: GetActionActionState,
user: User,
) -> anyhow::Result<ActionActionState> {
let action = resource::get_check_permissions::<Action>(
&action,
&user,
PermissionLevel::Read,
)
.await?;
let action_state = action_states()
.action
.get(&action.id)
.await
.unwrap_or_default()
.get()?;
Ok(action_state)
}
}
impl Resolve<GetActionsSummary, User> for State {
async fn resolve(
&self,
GetActionsSummary {}: GetActionsSummary,
user: User,
) -> anyhow::Result<GetActionsSummaryResponse> {
let actions = resource::list_full_for_user::<Action>(
Default::default(),
&user,
&[],
)
.await
.context("failed to get actions from db")?;
let mut res = GetActionsSummaryResponse::default();
let cache = action_state_cache();
let action_states = action_states();
for action in actions {
res.total += 1;
match (
cache.get(&action.id).await.unwrap_or_default(),
action_states
.action
.get(&action.id)
.await
.unwrap_or_default()
.get()?,
) {
(_, action_states) if action_states.running => {
res.running += 1;
}
(ActionState::Ok, _) => res.ok += 1,
(ActionState::Failed, _) => res.failed += 1,
(ActionState::Unknown, _) => res.unknown += 1,
// will never come off the cache in the running state, since that comes from action states
(ActionState::Running, _) => unreachable!(),
}
}
Ok(res)
}
}

View File

@@ -3,7 +3,10 @@ use komodo_client::{
api::read::{
GetAlert, GetAlertResponse, ListAlerts, ListAlertsResponse,
},
entities::{deployment::Deployment, server::Server, user::User},
entities::{
deployment::Deployment, server::Server, stack::Stack,
sync::ResourceSync, user::User,
},
};
use mungos::{
by_id::find_one_by_id,
@@ -30,12 +33,18 @@ impl Resolve<ListAlerts, User> for State {
if !user.admin && !core_config().transparent_mode {
let server_ids =
get_resource_ids_for_user::<Server>(&user).await?;
let stack_ids =
get_resource_ids_for_user::<Stack>(&user).await?;
let deployment_ids =
get_resource_ids_for_user::<Deployment>(&user).await?;
let sync_ids =
get_resource_ids_for_user::<ResourceSync>(&user).await?;
query.extend(doc! {
"$or": [
{ "target.type": "Server", "target.id": { "$in": &server_ids } },
{ "target.type": "Stack", "target.id": { "$in": &stack_ids } },
{ "target.type": "Deployment", "target.id": { "$in": &deployment_ids } },
{ "target.type": "ResourceSync", "target.id": { "$in": &sync_ids } },
]
});
}

View File

@@ -12,6 +12,7 @@ use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{db_client, State},
};
@@ -37,7 +38,12 @@ impl Resolve<ListAlerters, User> for State {
ListAlerters { query }: ListAlerters,
user: User,
) -> anyhow::Result<Vec<AlerterListItem>> {
resource::list_for_user::<Alerter>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Alerter>(query, &user, &all_tags).await
}
}
@@ -47,7 +53,13 @@ impl Resolve<ListFullAlerters, User> for State {
ListFullAlerters { query }: ListFullAlerters,
user: User,
) -> anyhow::Result<ListFullAlertersResponse> {
resource::list_full_for_user::<Alerter>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Alerter>(query, &user, &all_tags)
.await
}
}
@@ -57,15 +69,16 @@ impl Resolve<GetAlertersSummary, User> for State {
GetAlertersSummary {}: GetAlertersSummary,
user: User,
) -> anyhow::Result<GetAlertersSummaryResponse> {
let query =
match resource::get_resource_ids_for_user::<Alerter>(&user)
.await?
{
Some(ids) => doc! {
"_id": { "$in": ids }
},
None => Document::new(),
};
let query = match resource::get_resource_object_ids_for_user::<
Alerter,
>(&user)
.await?
{
Some(ids) => doc! {
"_id": { "$in": ids }
},
None => Document::new(),
};
let total = db_client()
.alerters
.count_documents(query)

View File

@@ -22,6 +22,7 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
resource,
state::{
action_states, build_state_cache, db_client, github_client, State,
@@ -49,7 +50,12 @@ impl Resolve<ListBuilds, User> for State {
ListBuilds { query }: ListBuilds,
user: User,
) -> anyhow::Result<Vec<BuildListItem>> {
resource::list_for_user::<Build>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Build>(query, &user, &all_tags).await
}
}
@@ -59,7 +65,13 @@ impl Resolve<ListFullBuilds, User> for State {
ListFullBuilds { query }: ListFullBuilds,
user: User,
) -> anyhow::Result<ListFullBuildsResponse> {
resource::list_full_for_user::<Build>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Build>(query, &user, &all_tags)
.await
}
}
@@ -94,6 +106,7 @@ impl Resolve<GetBuildsSummary, User> for State {
let builds = resource::list_full_for_user::<Build>(
Default::default(),
&user,
&[],
)
.await
.context("failed to get all builds")?;
@@ -252,9 +265,15 @@ impl Resolve<ListCommonBuildExtraArgs, User> for State {
ListCommonBuildExtraArgs { query }: ListCommonBuildExtraArgs,
user: User,
) -> anyhow::Result<ListCommonBuildExtraArgsResponse> {
let builds = resource::list_full_for_user::<Build>(query, &user)
.await
.context("failed to get resources matching query")?;
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let builds =
resource::list_full_for_user::<Build>(query, &user, &all_tags)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();

View File

@@ -12,6 +12,7 @@ use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{db_client, State},
};
@@ -37,7 +38,12 @@ impl Resolve<ListBuilders, User> for State {
ListBuilders { query }: ListBuilders,
user: User,
) -> anyhow::Result<Vec<BuilderListItem>> {
resource::list_for_user::<Builder>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Builder>(query, &user, &all_tags).await
}
}
@@ -47,7 +53,13 @@ impl Resolve<ListFullBuilders, User> for State {
ListFullBuilders { query }: ListFullBuilders,
user: User,
) -> anyhow::Result<ListFullBuildersResponse> {
resource::list_full_for_user::<Builder>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Builder>(query, &user, &all_tags)
.await
}
}
@@ -57,15 +69,16 @@ impl Resolve<GetBuildersSummary, User> for State {
GetBuildersSummary {}: GetBuildersSummary,
user: User,
) -> anyhow::Result<GetBuildersSummaryResponse> {
let query =
match resource::get_resource_ids_for_user::<Builder>(&user)
.await?
{
Some(ids) => doc! {
"_id": { "$in": ids }
},
None => Document::new(),
};
let query = match resource::get_resource_object_ids_for_user::<
Builder,
>(&user)
.await?
{
Some(ids) => doc! {
"_id": { "$in": ids }
},
None => Document::new(),
};
let total = db_client()
.builders
.count_documents(query)

View File

@@ -19,7 +19,7 @@ use periphery_client::api;
use resolver_api::Resolve;
use crate::{
helpers::periphery_client,
helpers::{periphery_client, query::get_all_tags},
resource,
state::{action_states, deployment_status_cache, State},
};
@@ -45,7 +45,13 @@ impl Resolve<ListDeployments, User> for State {
ListDeployments { query }: ListDeployments,
user: User,
) -> anyhow::Result<Vec<DeploymentListItem>> {
resource::list_for_user::<Deployment>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Deployment>(query, &user, &all_tags)
.await
}
}
@@ -55,7 +61,15 @@ impl Resolve<ListFullDeployments, User> for State {
ListFullDeployments { query }: ListFullDeployments,
user: User,
) -> anyhow::Result<ListFullDeploymentsResponse> {
resource::list_full_for_user::<Deployment>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Deployment>(
query, &user, &all_tags,
)
.await
}
}
@@ -217,6 +231,7 @@ impl Resolve<GetDeploymentsSummary, User> for State {
let deployments = resource::list_full_for_user::<Deployment>(
Default::default(),
&user,
&[],
)
.await
.context("failed to get deployments from db")?;
@@ -254,10 +269,16 @@ impl Resolve<ListCommonDeploymentExtraArgs, User> for State {
ListCommonDeploymentExtraArgs { query }: ListCommonDeploymentExtraArgs,
user: User,
) -> anyhow::Result<ListCommonDeploymentExtraArgsResponse> {
let deployments =
resource::list_full_for_user::<Deployment>(query, &user)
.await
.context("failed to get resources matching query")?;
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let deployments = resource::list_full_for_user::<Deployment>(
query, &user, &all_tags,
)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();

View File

@@ -29,6 +29,7 @@ use crate::{
resource, state::State,
};
mod action;
mod alert;
mod alerter;
mod build;
@@ -88,6 +89,13 @@ enum ReadRequest {
ListProcedures(ListProcedures),
ListFullProcedures(ListFullProcedures),
// ==== ACTION ====
GetActionsSummary(GetActionsSummary),
GetAction(GetAction),
GetActionActionState(GetActionActionState),
ListActions(ListActions),
ListFullActions(ListFullActions),
// ==== SERVER TEMPLATE ====
GetServerTemplate(GetServerTemplate),
GetServerTemplatesSummary(GetServerTemplatesSummary),
@@ -403,12 +411,18 @@ impl Resolve<ListGitProvidersFromConfig, User> for State {
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,
&[]
),
resource::list_full_for_user::<Repo>(Default::default(), &user),
resource::list_full_for_user::<ResourceSync>(
Default::default(),
&user
&user,
&[]
),
)?;

View File

@@ -10,6 +10,7 @@ use komodo_client::{
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{action_states, procedure_state_cache, State},
};
@@ -35,7 +36,13 @@ impl Resolve<ListProcedures, User> for State {
ListProcedures { query }: ListProcedures,
user: User,
) -> anyhow::Result<ListProceduresResponse> {
resource::list_for_user::<Procedure>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Procedure>(query, &user, &all_tags)
.await
}
}
@@ -45,7 +52,13 @@ impl Resolve<ListFullProcedures, User> for State {
ListFullProcedures { query }: ListFullProcedures,
user: User,
) -> anyhow::Result<ListFullProceduresResponse> {
resource::list_full_for_user::<Procedure>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Procedure>(query, &user, &all_tags)
.await
}
}
@@ -58,6 +71,7 @@ impl Resolve<GetProceduresSummary, User> for State {
let procedures = resource::list_full_for_user::<Procedure>(
Default::default(),
&user,
&[],
)
.await
.context("failed to get procedures from db")?;

View File

@@ -12,6 +12,7 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
resource,
state::{action_states, github_client, repo_state_cache, State},
};
@@ -37,7 +38,12 @@ impl Resolve<ListRepos, User> for State {
ListRepos { query }: ListRepos,
user: User,
) -> anyhow::Result<Vec<RepoListItem>> {
resource::list_for_user::<Repo>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Repo>(query, &user, &all_tags).await
}
}
@@ -47,7 +53,13 @@ impl Resolve<ListFullRepos, User> for State {
ListFullRepos { query }: ListFullRepos,
user: User,
) -> anyhow::Result<ListFullReposResponse> {
resource::list_full_for_user::<Repo>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Repo>(query, &user, &all_tags)
.await
}
}
@@ -79,10 +91,13 @@ impl Resolve<GetReposSummary, User> for State {
GetReposSummary {}: GetReposSummary,
user: User,
) -> anyhow::Result<GetReposSummaryResponse> {
let repos =
resource::list_full_for_user::<Repo>(Default::default(), &user)
.await
.context("failed to get repos from db")?;
let repos = resource::list_full_for_user::<Repo>(
Default::default(),
&user,
&[],
)
.await
.context("failed to get repos from db")?;
let mut res = GetReposSummaryResponse::default();

View File

@@ -43,7 +43,7 @@ use resolver_api::{Resolve, ResolveToString};
use tokio::sync::Mutex;
use crate::{
helpers::periphery_client,
helpers::{periphery_client, query::get_all_tags},
resource,
stack::compose_container_match_regex,
state::{action_states, db_client, server_status_cache, State},
@@ -55,9 +55,12 @@ impl Resolve<GetServersSummary, User> for State {
GetServersSummary {}: GetServersSummary,
user: User,
) -> anyhow::Result<GetServersSummaryResponse> {
let servers =
resource::list_for_user::<Server>(Default::default(), &user)
.await?;
let servers = resource::list_for_user::<Server>(
Default::default(),
&user,
&[],
)
.await?;
let mut res = GetServersSummaryResponse::default();
for server in servers {
res.total += 1;
@@ -119,7 +122,12 @@ impl Resolve<ListServers, User> for State {
ListServers { query }: ListServers,
user: User,
) -> anyhow::Result<Vec<ServerListItem>> {
resource::list_for_user::<Server>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Server>(query, &user, &all_tags).await
}
}
@@ -129,7 +137,13 @@ impl Resolve<ListFullServers, User> for State {
ListFullServers { query }: ListFullServers,
user: User,
) -> anyhow::Result<ListFullServersResponse> {
resource::list_full_for_user::<Server>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Server>(query, &user, &all_tags)
.await
}
}
@@ -374,15 +388,18 @@ impl Resolve<ListAllDockerContainers, User> for State {
ListAllDockerContainers { servers }: ListAllDockerContainers,
user: User,
) -> anyhow::Result<Vec<ContainerListItem>> {
let servers =
resource::list_for_user::<Server>(Default::default(), &user)
.await?
.into_iter()
.filter(|server| {
servers.is_empty()
|| servers.contains(&server.id)
|| servers.contains(&server.name)
});
let servers = resource::list_for_user::<Server>(
Default::default(),
&user,
&[],
)
.await?
.into_iter()
.filter(|server| {
servers.is_empty()
|| servers.contains(&server.id)
|| servers.contains(&server.name)
});
let mut containers = Vec::<ContainerListItem>::new();

View File

@@ -11,6 +11,7 @@ use mungos::mongodb::bson::doc;
use resolver_api::Resolve;
use crate::{
helpers::query::get_all_tags,
resource,
state::{db_client, State},
};
@@ -36,7 +37,13 @@ impl Resolve<ListServerTemplates, User> for State {
ListServerTemplates { query }: ListServerTemplates,
user: User,
) -> anyhow::Result<ListServerTemplatesResponse> {
resource::list_for_user::<ServerTemplate>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<ServerTemplate>(query, &user, &all_tags)
.await
}
}
@@ -46,7 +53,15 @@ impl Resolve<ListFullServerTemplates, User> for State {
ListFullServerTemplates { query }: ListFullServerTemplates,
user: User,
) -> anyhow::Result<ListFullServerTemplatesResponse> {
resource::list_full_for_user::<ServerTemplate>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<ServerTemplate>(
query, &user, &all_tags,
)
.await
}
}
@@ -56,7 +71,7 @@ impl Resolve<GetServerTemplatesSummary, User> for State {
GetServerTemplatesSummary {}: GetServerTemplatesSummary,
user: User,
) -> anyhow::Result<GetServerTemplatesSummaryResponse> {
let query = match resource::get_resource_ids_for_user::<
let query = match resource::get_resource_object_ids_for_user::<
ServerTemplate,
>(&user)
.await?

View File

@@ -17,7 +17,7 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::periphery_client,
helpers::{periphery_client, query::get_all_tags},
resource,
stack::get_stack_and_server,
state::{action_states, github_client, stack_status_cache, State},
@@ -133,9 +133,15 @@ impl Resolve<ListCommonStackExtraArgs, User> for State {
ListCommonStackExtraArgs { query }: ListCommonStackExtraArgs,
user: User,
) -> anyhow::Result<ListCommonStackExtraArgsResponse> {
let stacks = resource::list_full_for_user::<Stack>(query, &user)
.await
.context("failed to get resources matching query")?;
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks =
resource::list_full_for_user::<Stack>(query, &user, &all_tags)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
@@ -158,9 +164,15 @@ impl Resolve<ListCommonStackBuildExtraArgs, User> for State {
ListCommonStackBuildExtraArgs { query }: ListCommonStackBuildExtraArgs,
user: User,
) -> anyhow::Result<ListCommonStackBuildExtraArgsResponse> {
let stacks = resource::list_full_for_user::<Stack>(query, &user)
.await
.context("failed to get resources matching query")?;
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
let stacks =
resource::list_full_for_user::<Stack>(query, &user, &all_tags)
.await
.context("failed to get resources matching query")?;
// first collect with guaranteed uniqueness
let mut res = HashSet::<String>::new();
@@ -183,7 +195,12 @@ impl Resolve<ListStacks, User> for State {
ListStacks { query }: ListStacks,
user: User,
) -> anyhow::Result<Vec<StackListItem>> {
resource::list_for_user::<Stack>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<Stack>(query, &user, &all_tags).await
}
}
@@ -193,7 +210,13 @@ impl Resolve<ListFullStacks, User> for State {
ListFullStacks { query }: ListFullStacks,
user: User,
) -> anyhow::Result<ListFullStacksResponse> {
resource::list_full_for_user::<Stack>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<Stack>(query, &user, &all_tags)
.await
}
}
@@ -228,6 +251,7 @@ impl Resolve<GetStacksSummary, User> for State {
let stacks = resource::list_full_for_user::<Stack>(
Default::default(),
&user,
&[],
)
.await
.context("failed to get stacks from db")?;

View File

@@ -15,6 +15,7 @@ use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::query::get_all_tags,
resource,
state::{
action_states, github_client, resource_sync_state_cache, State,
@@ -42,7 +43,13 @@ impl Resolve<ListResourceSyncs, User> for State {
ListResourceSyncs { query }: ListResourceSyncs,
user: User,
) -> anyhow::Result<Vec<ResourceSyncListItem>> {
resource::list_for_user::<ResourceSync>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_for_user::<ResourceSync>(query, &user, &all_tags)
.await
}
}
@@ -52,7 +59,15 @@ impl Resolve<ListFullResourceSyncs, User> for State {
ListFullResourceSyncs { query }: ListFullResourceSyncs,
user: User,
) -> anyhow::Result<ListFullResourceSyncsResponse> {
resource::list_full_for_user::<ResourceSync>(query, &user).await
let all_tags = if query.tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
resource::list_full_for_user::<ResourceSync>(
query, &user, &all_tags,
)
.await
}
}
@@ -88,6 +103,7 @@ impl Resolve<GetResourceSyncsSummary, User> for State {
resource::list_full_for_user::<ResourceSync>(
Default::default(),
&user,
&[],
)
.await
.context("failed to get resource_syncs from db")?;

View File

@@ -1,27 +1,16 @@
use std::collections::HashMap;
use anyhow::Context;
use komodo_client::{
api::read::{
ExportAllResourcesToToml, ExportAllResourcesToTomlResponse,
ExportResourcesToToml, ExportResourcesToTomlResponse,
GetUserGroup, ListUserTargetPermissions,
ListUserGroups,
},
entities::{
alerter::Alerter,
build::Build,
builder::Builder,
deployment::Deployment,
permission::{PermissionLevel, UserTarget},
procedure::Procedure,
repo::Repo,
resource::ResourceQuery,
server::Server,
server_template::ServerTemplate,
stack::Stack,
sync::ResourceSync,
toml::{PermissionToml, ResourcesToml, UserGroupToml},
user::User,
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, permission::PermissionLevel,
procedure::Procedure, repo::Repo, resource::ResourceQuery,
server::Server, server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, toml::ResourcesToml, user::User,
ResourceTarget,
},
};
@@ -29,11 +18,14 @@ use mungos::find::find_collect;
use resolver_api::Resolve;
use crate::{
helpers::query::{get_id_to_tags, get_user_user_group_ids},
helpers::query::{
get_all_tags, get_id_to_tags, get_user_user_group_ids,
},
resource,
state::{db_client, State},
sync::{
toml::{convert_resource, ToToml, TOML_PRETTY_OPTIONS},
user_groups::convert_user_groups,
AllResourcesById,
},
};
@@ -46,10 +38,17 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
) -> anyhow::Result<ExportAllResourcesToTomlResponse> {
let mut targets = Vec::<ResourceTarget>::new();
let all_tags = if tags.is_empty() {
vec![]
} else {
get_all_tags(None).await?
};
targets.extend(
resource::list_for_user::<Alerter>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -59,6 +58,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
resource::list_for_user::<Builder>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -68,6 +68,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
resource::list_for_user::<Server>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -77,6 +78,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
resource::list_for_user::<Deployment>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -86,6 +88,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
resource::list_for_user::<Stack>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -95,6 +98,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
resource::list_for_user::<Build>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -104,6 +108,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
resource::list_for_user::<Repo>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -113,15 +118,27 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
resource::list_for_user::<Procedure>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Procedure(resource.id)),
);
targets.extend(
resource::list_for_user::<Action>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
.map(|resource| ResourceTarget::Action(resource.id)),
);
targets.extend(
resource::list_for_user::<ServerTemplate>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -131,6 +148,7 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
resource::list_full_for_user::<ResourceSync>(
ResourceQuery::builder().tags(tags.clone()).build(),
&user,
&all_tags,
)
.await?
.into_iter()
@@ -331,6 +349,21 @@ impl Resolve<ExportResourcesToToml, User> for State {
&id_to_tags,
));
}
ResourceTarget::Action(id) => {
let mut action = resource::get_check_permissions::<Action>(
&id,
&user,
PermissionLevel::Read,
)
.await?;
Action::replace_ids(&mut action, &all);
res.actions.push(convert_resource::<Action>(
action,
false,
vec![],
&id_to_tags,
));
}
ResourceTarget::System(_) => continue,
};
}
@@ -367,122 +400,17 @@ async fn add_user_groups(
all: &AllResourcesById,
user: &User,
) -> anyhow::Result<()> {
let db = db_client();
let usernames = find_collect(&db.users, None, None)
let user_groups = State
.resolve(ListUserGroups {}, user.clone())
.await?
.into_iter()
.map(|user| (user.id, user.username))
.collect::<HashMap<_, _>>();
for user_group in user_groups {
let ug = State
.resolve(GetUserGroup { user_group }, user.clone())
.await?;
// this method is admin only, but we already know user can see user group if above does not return Err
let permissions = State
.resolve(
ListUserTargetPermissions {
user_target: UserTarget::UserGroup(ug.id),
},
User {
admin: true,
..Default::default()
},
)
.await?
.into_iter()
.map(|mut permission| {
match &mut permission.resource_target {
ResourceTarget::Build(id) => {
*id = all
.builds
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Builder(id) => {
*id = all
.builders
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Deployment(id) => {
*id = all
.deployments
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Server(id) => {
*id = all
.servers
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Repo(id) => {
*id = all
.repos
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Alerter(id) => {
*id = all
.alerters
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Procedure(id) => {
*id = all
.procedures
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::ServerTemplate(id) => {
*id = all
.templates
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::ResourceSync(id) => {
*id = all
.syncs
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Stack(id) => {
*id = all
.stacks
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::System(_) => {}
}
PermissionToml {
target: permission.resource_target,
level: permission.level,
}
})
.collect();
res.user_groups.push(UserGroupToml {
name: ug.name,
users: ug
.users
.into_iter()
.filter_map(|user_id| usernames.get(&user_id).cloned())
.collect(),
all: ug.all,
permissions,
.filter(|ug| {
user_groups.contains(&ug.name) || user_groups.contains(&ug.id)
});
}
let mut ug = Vec::with_capacity(user_groups.size_hint().0);
convert_user_groups(user_groups, all, &mut ug).await?;
res.user_groups = ug.into_iter().map(|ug| ug.1).collect();
Ok(())
}
@@ -539,6 +467,14 @@ fn serialize_resources_toml(
Procedure::push_to_toml_string(procedure, &mut toml)?;
}
for action in resources.actions {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");
}
toml.push_str("[[action]]\n");
Action::push_to_toml_string(action, &mut toml)?;
}
for alerter in resources.alerters {
if !toml.is_empty() {
toml.push_str("\n\n##\n\n");

View File

@@ -4,6 +4,7 @@ use anyhow::{anyhow, Context};
use komodo_client::{
api::read::{GetUpdate, ListUpdates, ListUpdatesResponse},
entities::{
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
@@ -104,6 +105,16 @@ impl Resolve<ListUpdates, User> for State {
})
.unwrap_or_else(|| doc! { "target.type": "Procedure" });
let action_query =
resource::get_resource_ids_for_user::<Action>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "Action", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "Action" });
let builder_query =
resource::get_resource_ids_for_user::<Builder>(&user)
.await?
@@ -124,27 +135,27 @@ impl Resolve<ListUpdates, User> for State {
})
.unwrap_or_else(|| doc! { "target.type": "Alerter" });
let server_template_query = resource::get_resource_ids_for_user::<ServerTemplate>(
&user,
)
.await?
.map(|ids| {
doc! {
"target.type": "ServerTemplate", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ServerTemplate" });
let server_template_query =
resource::get_resource_ids_for_user::<ServerTemplate>(&user)
.await?
.map(|ids| {
doc! {
"target.type": "ServerTemplate", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ServerTemplate" });
let resource_sync_query = resource::get_resource_ids_for_user::<ResourceSync>(
&user,
)
.await?
.map(|ids| {
doc! {
"target.type": "ResourceSync", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
let resource_sync_query =
resource::get_resource_ids_for_user::<ResourceSync>(
&user,
)
.await?
.map(|ids| {
doc! {
"target.type": "ResourceSync", "target.id": { "$in": ids }
}
})
.unwrap_or_else(|| doc! { "target.type": "ResourceSync" });
let mut query = query.unwrap_or_default();
query.extend(doc! {
@@ -155,6 +166,7 @@ impl Resolve<ListUpdates, User> for State {
build_query,
repo_query,
procedure_query,
action_query,
alerter_query,
builder_query,
server_template_query,
@@ -292,6 +304,14 @@ impl Resolve<GetUpdate, User> for State {
)
.await?;
}
ResourceTarget::Action(id) => {
resource::get_check_permissions::<Action>(
id,
&user,
PermissionLevel::Read,
)
.await?;
}
ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>(
id,

View File

@@ -6,7 +6,7 @@ use komodo_client::{
ListApiKeysForServiceUserResponse, ListApiKeysResponse,
ListUsers, ListUsersResponse,
},
entities::user::{User, UserConfig},
entities::user::{admin_service_user, User, UserConfig},
};
use mungos::{
by_id::find_one_by_id,
@@ -26,6 +26,13 @@ impl Resolve<GetUsername, User> for State {
GetUsername { user_id }: GetUsername,
_: User,
) -> anyhow::Result<GetUsernameResponse> {
if let Some(user) = admin_service_user(&user_id) {
return Ok(GetUsernameResponse {
username: user.username,
avatar: None,
});
}
let user = find_one_by_id(&db_client().users, &user_id)
.await
.context("failed at mongo query for user")?

View File

@@ -0,0 +1,71 @@
use komodo_client::{
api::write::*,
entities::{
action::Action, permission::PermissionLevel, update::Update,
user::User,
},
};
use resolver_api::Resolve;
use crate::{resource, state::State};
impl Resolve<CreateAction, User> for State {
#[instrument(name = "CreateAction", skip(self, user))]
async fn resolve(
&self,
CreateAction { name, config }: CreateAction,
user: User,
) -> anyhow::Result<Action> {
resource::create::<Action>(&name, config, &user).await
}
}
impl Resolve<CopyAction, User> for State {
#[instrument(name = "CopyAction", skip(self, user))]
async fn resolve(
&self,
CopyAction { name, id }: CopyAction,
user: User,
) -> anyhow::Result<Action> {
let Action { config, .. } = resource::get_check_permissions::<
Action,
>(
&id, &user, PermissionLevel::Write
)
.await?;
resource::create::<Action>(&name, config.into(), &user).await
}
}
impl Resolve<UpdateAction, User> for State {
#[instrument(name = "UpdateAction", skip(self, user))]
async fn resolve(
&self,
UpdateAction { id, config }: UpdateAction,
user: User,
) -> anyhow::Result<Action> {
resource::update::<Action>(&id, config, &user).await
}
}
impl Resolve<RenameAction, User> for State {
#[instrument(name = "RenameAction", skip(self, user))]
async fn resolve(
&self,
RenameAction { id, name }: RenameAction,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Action>(&id, &name, &user).await
}
}
impl Resolve<DeleteAction, User> for State {
#[instrument(name = "DeleteAction", skip(self, user))]
async fn resolve(
&self,
DeleteAction { id }: DeleteAction,
user: User,
) -> anyhow::Result<Action> {
resource::delete::<Action>(&id, &user).await
}
}

View File

@@ -1,9 +1,8 @@
use komodo_client::{
api::write::{
CopyAlerter, CreateAlerter, DeleteAlerter, UpdateAlerter,
},
api::write::*,
entities::{
alerter::Alerter, permission::PermissionLevel, user::User,
alerter::Alerter, permission::PermissionLevel, update::Update,
user::User,
},
};
use resolver_api::Resolve;
@@ -59,3 +58,14 @@ impl Resolve<UpdateAlerter, User> for State {
resource::update::<Alerter>(&id, config, &user).await
}
}
impl Resolve<RenameAlerter, User> for State {
#[instrument(name = "RenameAlerter", skip(self, user))]
async fn resolve(
&self,
RenameAlerter { id, name }: RenameAlerter,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Alerter>(&id, &name, &user).await
}
}

View File

@@ -6,6 +6,7 @@ use komodo_client::{
build::{Build, BuildInfo, PartialBuildConfig},
config::core::CoreConfig,
permission::PermissionLevel,
update::Update,
user::User,
CloneArgs, NoData,
},
@@ -77,6 +78,17 @@ impl Resolve<UpdateBuild, User> for State {
}
}
impl Resolve<RenameBuild, User> for State {
#[instrument(name = "RenameBuild", skip(self, user))]
async fn resolve(
&self,
RenameBuild { id, name }: RenameBuild,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Build>(&id, &name, &user).await
}
}
impl Resolve<RefreshBuildCache, User> for State {
#[instrument(
name = "RefreshBuildCache",

View File

@@ -1,7 +1,8 @@
use komodo_client::{
api::write::*,
entities::{
builder::Builder, permission::PermissionLevel, user::User,
builder::Builder, permission::PermissionLevel, update::Update,
user::User,
},
};
use resolver_api::Resolve;
@@ -57,3 +58,14 @@ impl Resolve<UpdateBuilder, User> for State {
resource::update::<Builder>(&id, config, &user).await
}
}
impl Resolve<RenameBuilder, User> for State {
#[instrument(name = "RenameBuilder", skip(self, user))]
async fn resolve(
&self,
RenameBuilder { id, name }: RenameBuilder,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Builder>(&id, &name, &user).await
}
}

View File

@@ -108,7 +108,7 @@ impl Resolve<RenameDeployment, User> for State {
if container_state == DeploymentState::Unknown {
return Err(anyhow!(
"cannot rename deployment when container status is unknown"
"Cannot rename Deployment when container status is unknown"
));
}
@@ -124,7 +124,7 @@ impl Resolve<RenameDeployment, User> for State {
None,
)
.await
.context("failed to update deployment name on db")?;
.context("Failed to update Deployment name on db")?;
if container_state != DeploymentState::NotDeployed {
let server =
@@ -135,20 +135,19 @@ impl Resolve<RenameDeployment, User> for State {
new_name: name.clone(),
})
.await
.context("failed to rename container on server")?;
.context("Failed to rename container on server")?;
update.logs.push(log);
}
update.push_simple_log(
"rename deployment",
"Rename Deployment",
format!(
"renamed deployment from {} to {}",
"Renamed Deployment from {} to {}",
deployment.name, name
),
);
update.finalize();
add_update(update.clone()).await?;
update.id = add_update(update.clone()).await?;
Ok(update)
}

View File

@@ -2,7 +2,7 @@ use anyhow::anyhow;
use komodo_client::{
api::write::{UpdateDescription, UpdateDescriptionResponse},
entities::{
alerter::Alerter, build::Build, builder::Builder,
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, procedure::Procedure, repo::Repo,
server::Server, server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, user::User, ResourceTarget,
@@ -84,6 +84,14 @@ impl Resolve<UpdateDescription, User> for State {
)
.await?;
}
ResourceTarget::Action(id) => {
resource::update_description::<Action>(
&id,
&description,
&user,
)
.await?;
}
ResourceTarget::ServerTemplate(id) => {
resource::update_description::<ServerTemplate>(
&id,

View File

@@ -13,6 +13,7 @@ use uuid::Uuid;
use crate::{auth::auth_request, state::State};
mod action;
mod alerter;
mod build;
mod builder;
@@ -88,6 +89,7 @@ pub enum WriteRequest {
CopyBuild(CopyBuild),
DeleteBuild(DeleteBuild),
UpdateBuild(UpdateBuild),
RenameBuild(RenameBuild),
RefreshBuildCache(RefreshBuildCache),
CreateBuildWebhook(CreateBuildWebhook),
DeleteBuildWebhook(DeleteBuildWebhook),
@@ -97,18 +99,21 @@ pub enum WriteRequest {
CopyBuilder(CopyBuilder),
DeleteBuilder(DeleteBuilder),
UpdateBuilder(UpdateBuilder),
RenameBuilder(RenameBuilder),
// ==== SERVER TEMPLATE ====
CreateServerTemplate(CreateServerTemplate),
CopyServerTemplate(CopyServerTemplate),
DeleteServerTemplate(DeleteServerTemplate),
UpdateServerTemplate(UpdateServerTemplate),
RenameServerTemplate(RenameServerTemplate),
// ==== REPO ====
CreateRepo(CreateRepo),
CopyRepo(CopyRepo),
DeleteRepo(DeleteRepo),
UpdateRepo(UpdateRepo),
RenameRepo(RenameRepo),
RefreshRepoCache(RefreshRepoCache),
CreateRepoWebhook(CreateRepoWebhook),
DeleteRepoWebhook(DeleteRepoWebhook),
@@ -118,18 +123,28 @@ pub enum WriteRequest {
CopyAlerter(CopyAlerter),
DeleteAlerter(DeleteAlerter),
UpdateAlerter(UpdateAlerter),
RenameAlerter(RenameAlerter),
// ==== PROCEDURE ====
CreateProcedure(CreateProcedure),
CopyProcedure(CopyProcedure),
DeleteProcedure(DeleteProcedure),
UpdateProcedure(UpdateProcedure),
RenameProcedure(RenameProcedure),
// ==== ACTION ====
CreateAction(CreateAction),
CopyAction(CopyAction),
DeleteAction(DeleteAction),
UpdateAction(UpdateAction),
RenameAction(RenameAction),
// ==== SYNC ====
CreateResourceSync(CreateResourceSync),
CopyResourceSync(CopyResourceSync),
DeleteResourceSync(DeleteResourceSync),
UpdateResourceSync(UpdateResourceSync),
RenameResourceSync(RenameResourceSync),
WriteSyncFileContents(WriteSyncFileContents),
CommitSync(CommitSync),
RefreshResourceSyncPending(RefreshResourceSyncPending),

View File

@@ -387,6 +387,20 @@ async fn extract_resource_target_with_validation(
.id;
Ok((ResourceTargetVariant::Procedure, id))
}
ResourceTarget::Action(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },
Err(_) => doc! { "name": ident },
};
let id = db_client()
.actions
.find_one(filter)
.await
.context("failed to query db for actions")?
.context("no matching action found")?
.id;
Ok((ResourceTargetVariant::Action, id))
}
ResourceTarget::ServerTemplate(ident) => {
let filter = match ObjectId::from_str(ident) {
Ok(id) => doc! { "_id": id },

View File

@@ -1,7 +1,8 @@
use komodo_client::{
api::write::*,
entities::{
permission::PermissionLevel, procedure::Procedure, user::User,
permission::PermissionLevel, procedure::Procedure,
update::Update, user::User,
},
};
use resolver_api::Resolve;
@@ -48,6 +49,17 @@ impl Resolve<UpdateProcedure, User> for State {
}
}
impl Resolve<RenameProcedure, User> for State {
#[instrument(name = "RenameProcedure", skip(self, user))]
async fn resolve(
&self,
RenameProcedure { id, name }: RenameProcedure,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<Procedure>(&id, &name, &user).await
}
}
impl Resolve<DeleteProcedure, User> for State {
#[instrument(name = "DeleteProcedure", skip(self, user))]
async fn resolve(

View File

@@ -1,27 +1,36 @@
use anyhow::{anyhow, Context};
use formatting::format_serror;
use git::GitRes;
use komodo_client::{
api::write::*,
entities::{
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
repo::{PartialRepoConfig, Repo, RepoInfo},
server::Server,
to_komodo_name,
update::{Log, Update},
user::User,
CloneArgs, NoData,
CloneArgs, NoData, Operation,
},
};
use mongo_indexed::doc;
use mungos::mongodb::bson::to_document;
use mungos::{by_id::update_one_by_id, mongodb::bson::to_document};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use periphery_client::api;
use resolver_api::Resolve;
use crate::{
config::core_config,
helpers::git_token,
helpers::{
git_token, periphery_client,
update::{add_update, make_update},
},
resource,
state::{db_client, github_client, State},
state::{action_states, db_client, github_client, State},
};
impl Resolve<CreateRepo, User> for State {
@@ -75,6 +84,81 @@ impl Resolve<UpdateRepo, User> for State {
}
}
impl Resolve<RenameRepo, User> for State {
#[instrument(name = "RenameRepo", skip(self, user))]
async fn resolve(
&self,
RenameRepo { id, name }: RenameRepo,
user: User,
) -> anyhow::Result<Update> {
let repo = resource::get_check_permissions::<Repo>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
if repo.config.server_id.is_empty()
|| !repo.config.path.is_empty()
{
return resource::rename::<Repo>(&repo.id, &name, &user).await;
}
// get the action state for the repo (or insert default).
let action_state =
action_states().repo.get_or_insert_default(&repo.id).await;
// Will check to ensure repo not already busy before updating, and return Err if so.
// The returned guard will set the action state back to default when dropped.
let _action_guard =
action_state.update(|state| state.renaming = true)?;
let name = to_komodo_name(&name);
let mut update = make_update(&repo, Operation::RenameRepo, &user);
update_one_by_id(
&db_client().repos,
&repo.id,
mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.context("Failed to update Repo name on db")?;
let server =
resource::get::<Server>(&repo.config.server_id).await?;
let log = match periphery_client(&server)?
.request(api::git::RenameRepo {
curr_name: to_komodo_name(&repo.name),
new_name: name.clone(),
})
.await
.context("Failed to rename Repo directory on Server")
{
Ok(log) => log,
Err(e) => Log::error(
"Rename Repo directory failure",
format_serror(&e.into()),
),
};
update.logs.push(log);
update.push_simple_log(
"Rename Repo",
format!("Renamed Repo from {} to {}", repo.name, name),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
}
impl Resolve<RefreshRepoCache, User> for State {
#[instrument(
name = "RefreshRepoCache",

View File

@@ -1,9 +1,7 @@
use anyhow::Context;
use formatting::format_serror;
use komodo_client::{
api::write::*,
entities::{
komodo_timestamp,
permission::PermissionLevel,
server::Server,
update::{Update, UpdateStatus},
@@ -11,7 +9,6 @@ use komodo_client::{
Operation,
},
};
use mungos::{by_id::update_one_by_id, mongodb::bson::doc};
use periphery_client::api;
use resolver_api::Resolve;
@@ -21,7 +18,7 @@ use crate::{
update::{add_update, make_update, update_update},
},
resource,
state::{db_client, State},
state::State,
};
impl Resolve<CreateServer, User> for State {
@@ -64,25 +61,7 @@ impl Resolve<RenameServer, User> for State {
RenameServer { id, name }: RenameServer,
user: User,
) -> anyhow::Result<Update> {
let server = resource::get_check_permissions::<Server>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
let mut update =
make_update(&server, Operation::RenameServer, &user);
update_one_by_id(&db_client().servers, &id, mungos::update::Update::Set(doc! { "name": &name, "updated_at": komodo_timestamp() }), None)
.await
.context("failed to update server on db. this name may already be taken.")?;
update.push_simple_log(
"rename server",
format!("renamed server {id} from {} to {name}", server.name),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
resource::rename::<Server>(&id, &name, &user).await
}
}

View File

@@ -1,11 +1,11 @@
use komodo_client::{
api::write::{
CopyServerTemplate, CreateServerTemplate, DeleteServerTemplate,
UpdateServerTemplate,
RenameServerTemplate, UpdateServerTemplate,
},
entities::{
permission::PermissionLevel, server_template::ServerTemplate,
user::User,
update::Update, user::User,
},
};
use resolver_api::Resolve;
@@ -63,3 +63,14 @@ impl Resolve<UpdateServerTemplate, User> for State {
resource::update::<ServerTemplate>(&id, config, &user).await
}
}
impl Resolve<RenameServerTemplate, User> for State {
#[instrument(name = "RenameServerTemplate", skip(self, user))]
async fn resolve(
&self,
RenameServerTemplate { id, name }: RenameServerTemplate,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<ServerTemplate>(&id, &name, &user).await
}
}

View File

@@ -4,7 +4,6 @@ use komodo_client::{
api::write::*,
entities::{
config::core::CoreConfig,
komodo_timestamp,
permission::PermissionLevel,
server::ServerState,
stack::{PartialStackConfig, Stack, StackInfo},
@@ -13,10 +12,7 @@ use komodo_client::{
FileContents, NoData, Operation,
},
};
use mungos::{
by_id::update_one_by_id,
mongodb::bson::{doc, to_document},
};
use mungos::mongodb::bson::{doc, to_document};
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
@@ -100,36 +96,7 @@ impl Resolve<RenameStack, User> for State {
RenameStack { id, name }: RenameStack,
user: User,
) -> anyhow::Result<Update> {
let stack = resource::get_check_permissions::<Stack>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
let mut update =
make_update(&stack, Operation::RenameStack, &user);
update_one_by_id(
&db_client().stacks,
&stack.id,
mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.context("failed to update stack name on db")?;
update.push_simple_log(
"rename stack",
format!("renamed stack from {} to {}", stack.name, name),
);
update.finalize();
add_update(update.clone()).await?;
Ok(update)
resource::rename::<Stack>(&id, &name, &user).await
}
}
@@ -205,6 +172,7 @@ impl Resolve<WriteStackFileContents, User> for State {
match periphery_client(&server)?
.request(WriteCommitComposeContents {
stack,
username: Some(user.username),
file_path,
contents,
git_token,

View File

@@ -6,6 +6,7 @@ use komodo_client::{
api::{read::ExportAllResourcesToToml, write::*},
entities::{
self,
action::Action,
alert::{Alert, AlertData, SeverityLevel},
alerter::Alerter,
all_logs_success,
@@ -106,6 +107,17 @@ impl Resolve<UpdateResourceSync, User> for State {
}
}
impl Resolve<RenameResourceSync, User> for State {
#[instrument(name = "RenameResourceSync", skip(self, user))]
async fn resolve(
&self,
RenameResourceSync { id, name }: RenameResourceSync,
user: User,
) -> anyhow::Result<Update> {
resource::rename::<ResourceSync>(&id, &name, &user).await
}
}
impl Resolve<WriteSyncFileContents, User> for State {
async fn resolve(
&self,
@@ -190,7 +202,7 @@ impl Resolve<WriteSyncFileContents, User> for State {
}
let commit_res = git::commit_file(
"Commit Resource File",
&format!("{}: Commit Resource File", user.username),
&root,
&resource_path.join(&file_path),
)
@@ -219,15 +231,17 @@ impl Resolve<CommitSync, User> for State {
&self,
CommitSync { sync }: CommitSync,
user: User,
) -> anyhow::Result<ResourceSync> {
) -> anyhow::Result<Update> {
let sync = resource::get_check_permissions::<
entities::sync::ResourceSync,
>(&sync, &user, PermissionLevel::Write)
.await?;
let file_contents_empty = sync.config.file_contents_empty();
let fresh_sync = !sync.config.files_on_host
&& sync.config.file_contents.is_empty()
&& sync.config.repo.is_empty();
&& sync.config.repo.is_empty()
&& file_contents_empty;
if !sync.config.managed && !fresh_sync {
return Err(anyhow!(
@@ -235,21 +249,30 @@ impl Resolve<CommitSync, User> for State {
));
}
let resource_path = sync
.config
.resource_path
.first()
.context("Sync does not have resource path configured.")?
.parse::<PathBuf>()
.context("Invalid resource path")?;
// Get this here so it can fail before update created.
let resource_path =
if sync.config.files_on_host || !sync.config.repo.is_empty() {
let resource_path = sync
.config
.resource_path
.first()
.context("Sync does not have resource path configured.")?
.parse::<PathBuf>()
.context("Invalid resource path")?;
if resource_path
.extension()
.context("Resource path missing '.toml' extension")?
!= "toml"
{
return Err(anyhow!("Resource path missing '.toml' extension"));
}
if resource_path
.extension()
.context("Resource path missing '.toml' extension")?
!= "toml"
{
return Err(anyhow!(
"Resource path missing '.toml' extension"
));
}
Some(resource_path)
} else {
None
};
let res = State
.resolve(
@@ -266,6 +289,10 @@ impl Resolve<CommitSync, User> for State {
update.logs.push(Log::simple("Resources", res.toml.clone()));
if sync.config.files_on_host {
let Some(resource_path) = resource_path else {
// Resource path checked above for files_on_host mode.
unreachable!()
};
let file_path = core_config()
.sync_directory
.join(to_komodo_name(&sync.name))
@@ -284,8 +311,8 @@ impl Resolve<CommitSync, User> for State {
format_serror(&e.into()),
);
update.finalize();
add_update(update).await?;
return resource::get::<ResourceSync>(&sync.name).await;
add_update(update.clone()).await?;
return Ok(update);
} else {
update.push_simple_log(
"Write contents",
@@ -293,6 +320,10 @@ impl Resolve<CommitSync, User> for State {
);
}
} else if !sync.config.repo.is_empty() {
let Some(resource_path) = resource_path else {
// Resource path checked above for repo mode.
unreachable!()
};
// GIT REPO
let args: CloneArgs = (&sync).into();
let root = args.unique_path(&core_config().repo_directory)?;
@@ -311,8 +342,8 @@ impl Resolve<CommitSync, User> for State {
format_serror(&e.into()),
);
update.finalize();
add_update(update).await?;
return resource::get::<ResourceSync>(&sync.name).await;
add_update(update.clone()).await?;
return Ok(update);
}
}
// ===========
@@ -331,22 +362,18 @@ impl Resolve<CommitSync, User> for State {
format_serror(&e.into()),
);
update.finalize();
add_update(update).await?;
return resource::get::<ResourceSync>(&sync.name).await;
add_update(update.clone()).await?;
return Ok(update);
}
let res = match State
if let Err(e) = State
.resolve(RefreshResourceSyncPending { sync: sync.name }, user)
.await
{
Ok(sync) => Ok(sync),
Err(e) => {
update.push_error_log(
"Refresh sync pending",
format_serror(&(&e).into()),
);
Err(e)
}
update.push_error_log(
"Refresh sync pending",
format_serror(&(&e).into()),
);
};
update.finalize();
@@ -365,9 +392,9 @@ impl Resolve<CommitSync, User> for State {
.await;
refresh_resource_sync_state_cache().await;
}
update_update(update).await?;
update_update(update.clone()).await?;
res
Ok(update)
}
}
@@ -520,6 +547,17 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
&mut diffs,
)
.await?;
push_updates_for_view::<Action>(
resources.actions,
delete,
&all_resources,
None,
None,
&id_to_tags,
&sync.config.match_tags,
&mut diffs,
)
.await?;
push_updates_for_view::<Builder>(
resources.builders,
delete,
@@ -569,8 +607,7 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
let variable_updates = if sync.config.match_tags.is_empty() {
crate::sync::variables::get_updates_for_view(
&resources.variables,
// Delete doesn't work with variables when match tags are set
sync.config.match_tags.is_empty() && delete,
delete,
)
.await?
} else {
@@ -580,8 +617,7 @@ impl Resolve<RefreshResourceSyncPending, User> for State {
let user_group_updates = if sync.config.match_tags.is_empty() {
crate::sync::user_groups::get_updates_for_view(
resources.user_groups,
// Delete doesn't work with user groups when match tags are set
sync.config.match_tags.is_empty() && delete,
delete,
&all_resources,
)
.await?

View File

@@ -7,7 +7,7 @@ use komodo_client::{
UpdateTagsOnResourceResponse,
},
entities::{
alerter::Alerter, build::Build, builder::Builder,
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, permission::PermissionLevel,
procedure::Procedure, repo::Repo, server::Server,
server_template::ServerTemplate, stack::Stack,
@@ -182,6 +182,15 @@ impl Resolve<UpdateTagsOnResource, User> for State {
.await?;
resource::update_tags::<Procedure>(&id, tags, user).await?
}
ResourceTarget::Action(id) => {
resource::get_check_permissions::<Action>(
&id,
&user,
PermissionLevel::Write,
)
.await?;
resource::update_tags::<Action>(&id, tags, user).await?
}
ResourceTarget::ServerTemplate(id) => {
resource::get_check_permissions::<ServerTemplate>(
&id,

View File

@@ -81,7 +81,7 @@ impl Resolve<UpdateVariableValue, User> for State {
let variable = get_variable(&name).await?;
if value == variable.value {
return Err(anyhow!("no change"));
return Ok(variable);
}
db_client()

View File

@@ -150,6 +150,9 @@ pub fn core_config() -> &'static CoreConfig {
repo_directory: env
.komodo_repo_directory
.unwrap_or(config.repo_directory),
action_directory: env
.komodo_action_directory
.unwrap_or(config.action_directory),
resource_poll_interval: env
.komodo_resource_poll_interval
.unwrap_or(config.resource_poll_interval),

View File

@@ -1,4 +1,5 @@
use komodo_client::entities::{
action::Action,
alert::Alert,
alerter::Alerter,
api_key::ApiKey,
@@ -47,6 +48,7 @@ pub struct DbClient {
pub builders: Collection<Builder>,
pub repos: Collection<Repo>,
pub procedures: Collection<Procedure>,
pub actions: Collection<Action>,
pub alerters: Collection<Alerter>,
pub server_templates: Collection<ServerTemplate>,
pub resource_syncs: Collection<ResourceSync>,
@@ -115,6 +117,7 @@ impl DbClient {
repos: resource_collection(&db, "Repo").await?,
alerters: resource_collection(&db, "Alerter").await?,
procedures: resource_collection(&db, "Procedure").await?,
actions: resource_collection(&db, "Action").await?,
server_templates: resource_collection(&db, "ServerTemplate")
.await?,
resource_syncs: resource_collection(&db, "ResourceSync")

View File

@@ -4,7 +4,8 @@ use anyhow::anyhow;
use komodo_client::{
busy::Busy,
entities::{
build::BuildActionState, deployment::DeploymentActionState,
action::ActionActionState, build::BuildActionState,
deployment::DeploymentActionState,
procedure::ProcedureActionState, repo::RepoActionState,
server::ServerActionState, stack::StackActionState,
sync::ResourceSyncActionState,
@@ -22,6 +23,7 @@ pub struct ActionStates {
pub repo: Cache<String, Arc<ActionState<RepoActionState>>>,
pub procedure:
Cache<String, Arc<ActionState<ProcedureActionState>>>,
pub action: Cache<String, Arc<ActionState<ActionActionState>>>,
pub resource_sync:
Cache<String, Arc<ActionState<ResourceSyncActionState>>>,
pub stack: Cache<String, Arc<ActionState<StackActionState>>>,

View File

@@ -146,6 +146,22 @@ async fn execute_execution(
)
.await?
}
Execution::RunAction(req) => {
let req = ExecuteRequest::RunAction(req);
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunAction(req) = req else {
unreachable!()
};
let update_id = update.id.clone();
handle_resolve_result(
State
.resolve(req, (user, update))
.await
.context("Failed at RunAction"),
&update_id,
)
.await?
}
Execution::RunBuild(req) => {
let req = ExecuteRequest::RunBuild(req);
let update = init_execution_update(&req, &user).await?;
@@ -706,6 +722,11 @@ async fn execute_execution(
)
.await?
}
// Exception: This is a write operation.
Execution::CommitSync(req) => State
.resolve(req, user)
.await
.context("Failed at CommitSync")?,
Execution::DeployStack(req) => {
let req = ExecuteRequest::DeployStack(req);
let update = init_execution_update(&req, &user).await?;

View File

@@ -2,6 +2,7 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, Context};
use komodo_client::entities::{
action::Action,
alerter::Alerter,
build::Build,
builder::Builder,
@@ -201,6 +202,14 @@ pub async fn get_tag_check_owner(
Err(anyhow!("user must be tag owner or admin"))
}
pub async fn get_all_tags(
filter: impl Into<Option<Document>>,
) -> anyhow::Result<Vec<Tag>> {
find_collect(&db_client().tags, filter, None)
.await
.context("failed to query db for tags")
}
pub async fn get_id_to_tags(
filter: impl Into<Option<Document>>,
) -> anyhow::Result<HashMap<String, Tag>> {
@@ -283,6 +292,9 @@ pub async fn get_user_permission_on_target(
ResourceTarget::Procedure(id) => {
get_user_permission_on_resource::<Procedure>(user, id).await
}
ResourceTarget::Action(id) => {
get_user_permission_on_resource::<Action>(user, id).await
}
ResourceTarget::ServerTemplate(id) => {
get_user_permission_on_resource::<ServerTemplate>(user, id)
.await

View File

@@ -1,5 +1,6 @@
use anyhow::Context;
use komodo_client::entities::{
action::Action,
build::Build,
deployment::Deployment,
komodo_timestamp,
@@ -345,6 +346,14 @@ pub async fn init_execution_update(
),
),
// Action
ExecuteRequest::RunAction(data) => (
Operation::RunAction,
ResourceTarget::Action(
resource::get::<Action>(&data.action).await?.id,
),
),
// Server template
ExecuteRequest::LaunchServer(data) => (
Operation::LaunchServer,

View File

@@ -6,7 +6,9 @@ use komodo_client::{
api::execute::RunBuild,
entities::{build::Build, user::git_webhook_user},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCode;
use crate::{
api::execute::ExecuteRequest,
@@ -20,22 +22,30 @@ fn build_locks() -> &'static ListenerLockCache {
BUILD_LOCKS.get_or_init(Default::default)
}
pub async fn handle_build_webhook(
build_id: String,
pub async fn auth_build_webhook(
build_id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<Build> {
let build = resource::get::<Build>(build_id)
.await
.status_code(StatusCode::NOT_FOUND)?;
verify_gh_signature(headers, body, &build.config.webhook_secret)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(build)
}
pub async fn handle_build_webhook(
build: Build,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = build_locks().get_or_insert_default(&build_id).await;
let lock = build_locks().get_or_insert_default(&build.id).await;
let _lock = lock.lock().await;
let build = resource::get::<Build>(&build_id).await?;
verify_gh_signature(headers, &body, &build.config.webhook_secret)
.await?;
if !build.config.webhook_enabled {
return Err(anyhow!("build does not have webhook enabled"));
}
@@ -46,7 +56,7 @@ pub async fn handle_build_webhook(
}
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunBuild(RunBuild { build: build_id });
let req = ExecuteRequest::RunBuild(RunBuild { build: build.id });
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunBuild(req) = req else {
unreachable!()

View File

@@ -39,10 +39,11 @@ pub fn router() -> Router {
"/build/:id",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let build = build::auth_build_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("build_webhook", id);
async {
let res = build::handle_build_webhook(id.clone(), headers, body).await;
let res = build::handle_build_webhook(build, body).await;
if let Err(e) = res {
warn!("failed to run build webook for build {id} | {e:#}");
}
@@ -50,6 +51,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
),
)
@@ -57,10 +59,11 @@ pub fn router() -> Router {
"/repo/:id/clone",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let repo = repo::auth_repo_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("repo_clone_webhook", id);
async {
let res = repo::handle_repo_clone_webhook(id.clone(), headers, body).await;
let res = repo::handle_repo_clone_webhook(repo, body).await;
if let Err(e) = res {
warn!("failed to run repo clone webook for repo {id} | {e:#}");
}
@@ -68,6 +71,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -75,10 +79,11 @@ pub fn router() -> Router {
"/repo/:id/pull",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let repo = repo::auth_repo_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("repo_pull_webhook", id);
async {
let res = repo::handle_repo_pull_webhook(id.clone(), headers, body).await;
let res = repo::handle_repo_pull_webhook(repo, body).await;
if let Err(e) = res {
warn!("failed to run repo pull webook for repo {id} | {e:#}");
}
@@ -86,6 +91,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -93,10 +99,11 @@ pub fn router() -> Router {
"/repo/:id/build",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let repo = repo::auth_repo_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("repo_build_webhook", id);
async {
let res = repo::handle_repo_build_webhook(id.clone(), headers, body).await;
let res = repo::handle_repo_build_webhook(repo, body).await;
if let Err(e) = res {
warn!("failed to run repo build webook for repo {id} | {e:#}");
}
@@ -104,6 +111,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -111,10 +119,11 @@ pub fn router() -> Router {
"/stack/:id/refresh",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let stack = stack::auth_stack_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("stack_clone_webhook", id);
async {
let res = stack::handle_stack_refresh_webhook(id.clone(), headers, body).await;
let res = stack::handle_stack_refresh_webhook(stack, body).await;
if let Err(e) = res {
warn!("failed to run stack clone webook for stack {id} | {e:#}");
}
@@ -122,6 +131,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -129,10 +139,11 @@ pub fn router() -> Router {
"/stack/:id/deploy",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let stack = stack::auth_stack_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("stack_pull_webhook", id);
async {
let res = stack::handle_stack_deploy_webhook(id.clone(), headers, body).await;
let res = stack::handle_stack_deploy_webhook(stack, body).await;
if let Err(e) = res {
warn!("failed to run stack pull webook for stack {id} | {e:#}");
}
@@ -140,6 +151,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -147,13 +159,13 @@ pub fn router() -> Router {
"/procedure/:id/:branch",
post(
|Path(IdBranch { id, branch }), headers: HeaderMap, body: String| async move {
let procedure = procedure::auth_procedure_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("procedure_webhook", id, branch);
async {
let res = procedure::handle_procedure_webhook(
id.clone(),
procedure,
branch.unwrap_or_else(|| String::from("main")),
headers,
body
).await;
if let Err(e) = res {
@@ -163,6 +175,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -170,12 +183,12 @@ pub fn router() -> Router {
"/sync/:id/refresh",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let sync = sync::auth_sync_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("sync_refresh_webhook", id);
async {
let res = sync::handle_sync_refresh_webhook(
id.clone(),
headers,
sync,
body
).await;
if let Err(e) = res {
@@ -185,6 +198,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)
@@ -192,12 +206,12 @@ pub fn router() -> Router {
"/sync/:id/sync",
post(
|Path(Id { id }), headers: HeaderMap, body: String| async move {
let sync = sync::auth_sync_webhook(&id, headers, &body).await?;
tokio::spawn(async move {
let span = info_span!("sync_execute_webhook", id);
async {
let res = sync::handle_sync_execute_webhook(
id.clone(),
headers,
sync,
body
).await;
if let Err(e) = res {
@@ -207,6 +221,7 @@ pub fn router() -> Router {
.instrument(span)
.await
});
serror::Result::Ok(())
},
)
)

View File

@@ -6,7 +6,9 @@ use komodo_client::{
api::execute::RunProcedure,
entities::{procedure::Procedure, user::git_webhook_user},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCode;
use crate::{
api::execute::ExecuteRequest,
@@ -20,28 +22,36 @@ fn procedure_locks() -> &'static ListenerLockCache {
BUILD_LOCKS.get_or_init(Default::default)
}
pub async fn handle_procedure_webhook(
procedure_id: String,
target_branch: String,
pub async fn auth_procedure_webhook(
procedure_id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<Procedure> {
let procedure = resource::get::<Procedure>(procedure_id)
.await
.status_code(StatusCode::NOT_FOUND)?;
verify_gh_signature(
headers,
body,
&procedure.config.webhook_secret,
)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(procedure)
}
pub async fn handle_procedure_webhook(
procedure: Procedure,
target_branch: String,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock =
procedure_locks().get_or_insert_default(&procedure_id).await;
procedure_locks().get_or_insert_default(&procedure.id).await;
let _lock = lock.lock().await;
let procedure = resource::get::<Procedure>(&procedure_id).await?;
verify_gh_signature(
headers,
&body,
&procedure.config.webhook_secret,
)
.await?;
if !procedure.config.webhook_enabled {
return Err(anyhow!("procedure does not have webhook enabled"));
}
@@ -53,7 +63,7 @@ pub async fn handle_procedure_webhook(
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunProcedure(RunProcedure {
procedure: procedure_id,
procedure: procedure.id,
});
let update = init_execution_update(&req, &user).await?;
let ExecuteRequest::RunProcedure(req) = req else {

View File

@@ -6,7 +6,9 @@ use komodo_client::{
api::execute::{BuildRepo, CloneRepo, PullRepo},
entities::{repo::Repo, user::git_webhook_user},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCode;
use crate::{
helpers::update::init_execution_update, resource, state::State,
@@ -19,22 +21,30 @@ fn repo_locks() -> &'static ListenerLockCache {
REPO_LOCKS.get_or_init(Default::default)
}
pub async fn handle_repo_clone_webhook(
repo_id: String,
pub async fn auth_repo_webhook(
repo_id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<Repo> {
let repo = resource::get::<Repo>(repo_id)
.await
.status_code(StatusCode::NOT_FOUND)?;
verify_gh_signature(headers, body, &repo.config.webhook_secret)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(repo)
}
pub async fn handle_repo_clone_webhook(
repo: Repo,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = repo_locks().get_or_insert_default(&repo_id).await;
let lock = repo_locks().get_or_insert_default(&repo.id).await;
let _lock = lock.lock().await;
let repo = resource::get::<Repo>(&repo_id).await?;
verify_gh_signature(headers, &body, &repo.config.webhook_secret)
.await?;
if !repo.config.webhook_enabled {
return Err(anyhow!("repo does not have webhook enabled"));
}
@@ -47,7 +57,7 @@ pub async fn handle_repo_clone_webhook(
let user = git_webhook_user().to_owned();
let req =
crate::api::execute::ExecuteRequest::CloneRepo(CloneRepo {
repo: repo_id,
repo: repo.id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::CloneRepo(req) = req
@@ -59,21 +69,15 @@ pub async fn handle_repo_clone_webhook(
}
pub async fn handle_repo_pull_webhook(
repo_id: String,
headers: HeaderMap,
repo: Repo,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = repo_locks().get_or_insert_default(&repo_id).await;
let lock = repo_locks().get_or_insert_default(&repo.id).await;
let _lock = lock.lock().await;
let repo = resource::get::<Repo>(&repo_id).await?;
verify_gh_signature(headers, &body, &repo.config.webhook_secret)
.await?;
if !repo.config.webhook_enabled {
return Err(anyhow!("repo does not have webhook enabled"));
}
@@ -85,7 +89,7 @@ pub async fn handle_repo_pull_webhook(
let user = git_webhook_user().to_owned();
let req = crate::api::execute::ExecuteRequest::PullRepo(PullRepo {
repo: repo_id,
repo: repo.id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::PullRepo(req) = req else {
@@ -96,21 +100,15 @@ pub async fn handle_repo_pull_webhook(
}
pub async fn handle_repo_build_webhook(
repo_id: String,
headers: HeaderMap,
repo: Repo,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = repo_locks().get_or_insert_default(&repo_id).await;
let lock = repo_locks().get_or_insert_default(&repo.id).await;
let _lock = lock.lock().await;
let repo = resource::get::<Repo>(&repo_id).await?;
verify_gh_signature(headers, &body, &repo.config.webhook_secret)
.await?;
if !repo.config.webhook_enabled {
return Err(anyhow!("repo does not have webhook enabled"));
}
@@ -123,7 +121,7 @@ pub async fn handle_repo_build_webhook(
let user = git_webhook_user().to_owned();
let req =
crate::api::execute::ExecuteRequest::BuildRepo(BuildRepo {
repo: repo_id,
repo: repo.id,
});
let update = init_execution_update(&req, &user).await?;
let crate::api::execute::ExecuteRequest::BuildRepo(req) = req

View File

@@ -9,7 +9,9 @@ use komodo_client::{
},
entities::{stack::Stack, user::git_webhook_user},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCode;
use crate::{
api::execute::ExecuteRequest,
@@ -23,22 +25,30 @@ fn stack_locks() -> &'static ListenerLockCache {
STACK_LOCKS.get_or_init(Default::default)
}
pub async fn handle_stack_refresh_webhook(
stack_id: String,
pub async fn auth_stack_webhook(
stack_id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<Stack> {
let stack = resource::get::<Stack>(stack_id)
.await
.status_code(StatusCode::NOT_FOUND)?;
verify_gh_signature(headers, body, &stack.config.webhook_secret)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(stack)
}
pub async fn handle_stack_refresh_webhook(
stack: Stack,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through, from "action state busy".
let lock = stack_locks().get_or_insert_default(&stack_id).await;
let lock = stack_locks().get_or_insert_default(&stack.id).await;
let _lock = lock.lock().await;
let stack = resource::get::<Stack>(&stack_id).await?;
verify_gh_signature(headers, &body, &stack.config.webhook_secret)
.await?;
if !stack.config.webhook_enabled {
return Err(anyhow!("stack does not have webhook enabled"));
}
@@ -56,21 +66,15 @@ pub async fn handle_stack_refresh_webhook(
}
pub async fn handle_stack_deploy_webhook(
stack_id: String,
headers: HeaderMap,
stack: Stack,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = stack_locks().get_or_insert_default(&stack_id).await;
let lock = stack_locks().get_or_insert_default(&stack.id).await;
let _lock = lock.lock().await;
let stack = resource::get::<Stack>(&stack_id).await?;
verify_gh_signature(headers, &body, &stack.config.webhook_secret)
.await?;
if !stack.config.webhook_enabled {
return Err(anyhow!("stack does not have webhook enabled"));
}
@@ -83,7 +87,7 @@ pub async fn handle_stack_deploy_webhook(
let user = git_webhook_user().to_owned();
if stack.config.webhook_force_deploy {
let req = ExecuteRequest::DeployStack(DeployStack {
stack: stack_id,
stack: stack.id,
stop_time: None,
});
let update = init_execution_update(&req, &user).await?;
@@ -94,7 +98,7 @@ pub async fn handle_stack_deploy_webhook(
} else {
let req =
ExecuteRequest::DeployStackIfChanged(DeployStackIfChanged {
stack: stack_id,
stack: stack.id,
stop_time: None,
});
let update = init_execution_update(&req, &user).await?;

View File

@@ -6,7 +6,9 @@ use komodo_client::{
api::{execute::RunSync, write::RefreshResourceSyncPending},
entities::{sync::ResourceSync, user::git_webhook_user},
};
use reqwest::StatusCode;
use resolver_api::Resolve;
use serror::AddStatusCode;
use crate::{
api::execute::ExecuteRequest,
@@ -20,22 +22,30 @@ fn sync_locks() -> &'static ListenerLockCache {
SYNC_LOCKS.get_or_init(Default::default)
}
pub async fn handle_sync_refresh_webhook(
sync_id: String,
pub async fn auth_sync_webhook(
sync_id: &str,
headers: HeaderMap,
body: &str,
) -> serror::Result<ResourceSync> {
let sync = resource::get::<ResourceSync>(sync_id)
.await
.status_code(StatusCode::NOT_FOUND)?;
verify_gh_signature(headers, body, &sync.config.webhook_secret)
.await
.status_code(StatusCode::UNAUTHORIZED)?;
Ok(sync)
}
pub async fn handle_sync_refresh_webhook(
sync: ResourceSync,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = sync_locks().get_or_insert_default(&sync_id).await;
let lock = sync_locks().get_or_insert_default(&sync.id).await;
let _lock = lock.lock().await;
let sync = resource::get::<ResourceSync>(&sync_id).await?;
verify_gh_signature(headers, &body, &sync.config.webhook_secret)
.await?;
if !sync.config.webhook_enabled {
return Err(anyhow!("sync does not have webhook enabled"));
}
@@ -47,27 +57,21 @@ pub async fn handle_sync_refresh_webhook(
let user = git_webhook_user().to_owned();
State
.resolve(RefreshResourceSyncPending { sync: sync_id }, user)
.resolve(RefreshResourceSyncPending { sync: sync.id }, user)
.await?;
Ok(())
}
pub async fn handle_sync_execute_webhook(
sync_id: String,
headers: HeaderMap,
sync: ResourceSync,
body: String,
) -> anyhow::Result<()> {
// Acquire and hold lock to make a task queue for
// subsequent listener calls on same resource.
// It would fail if we let it go through from action state busy.
let lock = sync_locks().get_or_insert_default(&sync_id).await;
let lock = sync_locks().get_or_insert_default(&sync.id).await;
let _lock = lock.lock().await;
let sync = resource::get::<ResourceSync>(&sync_id).await?;
verify_gh_signature(headers, &body, &sync.config.webhook_secret)
.await?;
if !sync.config.webhook_enabled {
return Err(anyhow!("sync does not have webhook enabled"));
}
@@ -79,7 +83,7 @@ pub async fn handle_sync_execute_webhook(
let user = git_webhook_user().to_owned();
let req = ExecuteRequest::RunSync(RunSync {
sync: sync_id,
sync: sync.id,
resource_type: None,
resources: None,
});

View File

@@ -26,6 +26,7 @@ mod resource;
mod stack;
mod state;
mod sync;
mod ts_client;
mod ws;
async fn app() -> anyhow::Result<()> {
@@ -57,6 +58,7 @@ async fn app() -> anyhow::Result<()> {
resource::spawn_build_state_refresh_loop();
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();
@@ -75,6 +77,7 @@ async fn app() -> anyhow::Result<()> {
.nest("/execute", api::execute::router())
.nest("/listener", listener::router())
.nest("/ws", ws::router())
.nest("/client", ts_client::router())
.nest_service("/", serve_dir)
.fallback_service(frontend_index)
.layer(cors()?)

View File

@@ -2,9 +2,7 @@ use std::collections::HashMap;
use anyhow::Context;
use komodo_client::entities::{
resource::ResourceQuery,
server::{Server, ServerListItem},
user::User,
resource::ResourceQuery, server::Server, user::User,
};
use crate::resource;
@@ -32,16 +30,16 @@ pub async fn check_alerts(ts: i64) {
}
#[instrument(level = "debug")]
async fn get_all_servers_map() -> anyhow::Result<(
HashMap<String, ServerListItem>,
HashMap<String, String>,
)> {
let servers = resource::list_for_user::<Server>(
async fn get_all_servers_map(
) -> anyhow::Result<(HashMap<String, Server>, HashMap<String, String>)>
{
let servers = resource::list_full_for_user::<Server>(
ResourceQuery::default(),
&User {
admin: true,
..Default::default()
},
&[],
)
.await
.context("failed to get servers from db (in alert_servers)")?;

View File

@@ -5,7 +5,7 @@ use derive_variants::ExtractVariant;
use komodo_client::entities::{
alert::{Alert, AlertData, AlertDataVariant, SeverityLevel},
komodo_timestamp, optional_string,
server::{ServerListItem, ServerState},
server::{Server, ServerState},
ResourceTarget,
};
use mongo_indexed::Indexed;
@@ -28,7 +28,7 @@ type OpenDiskAlertMap = OpenAlertMap<PathBuf>;
#[instrument(level = "debug")]
pub async fn alert_servers(
ts: i64,
mut servers: HashMap<String, ServerListItem>,
mut servers: HashMap<String, Server>,
) {
let server_statuses = server_status_cache().get_list().await;
@@ -70,12 +70,12 @@ pub async fn alert_servers(
data: AlertData::ServerUnreachable {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
err: server_status.err.clone(),
},
};
alerts_to_open
.push((alert, server.info.send_unreachable_alerts))
.push((alert, server.config.send_unreachable_alerts))
}
(ServerState::NotOk, Some(alert)) => {
// update alert err
@@ -102,8 +102,10 @@ pub async fn alert_servers(
// Close an open alert
(ServerState::Ok | ServerState::Disabled, Some(alert)) => {
alert_ids_to_close
.push((alert.clone(), server.info.send_unreachable_alerts));
alert_ids_to_close.push((
alert.clone(),
server.config.send_unreachable_alerts,
));
}
_ => {}
}
@@ -119,20 +121,21 @@ pub async fn alert_servers(
.as_ref()
.and_then(|alerts| alerts.get(&AlertDataVariant::ServerCpu))
.cloned();
match (health.cpu, cpu_alert) {
(SeverityLevel::Warning | SeverityLevel::Critical, None) => {
match (health.cpu.level, cpu_alert, health.cpu.should_close_alert)
{
(SeverityLevel::Warning | SeverityLevel::Critical, None, _) => {
// open alert
let alert = Alert {
id: Default::default(),
ts,
resolved: false,
resolved_ts: None,
level: health.cpu,
level: health.cpu.level,
target: ResourceTarget::Server(server_status.id.clone()),
data: AlertData::ServerCpu {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
percentage: server_status
.stats
.as_ref()
@@ -140,41 +143,44 @@ pub async fn alert_servers(
.unwrap_or(0.0),
},
};
alerts_to_open.push((alert, server.info.send_cpu_alerts));
alerts_to_open.push((alert, server.config.send_cpu_alerts));
}
(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(mut alert),
_,
) => {
// modify alert level only if it has increased
if alert.level < health.cpu {
alert.level = health.cpu;
if alert.level < health.cpu.level {
alert.level = health.cpu.level;
alert.data = AlertData::ServerCpu {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
percentage: server_status
.stats
.as_ref()
.map(|s| s.cpu_perc as f64)
.unwrap_or(0.0),
};
alerts_to_update.push((alert, server.info.send_cpu_alerts));
alerts_to_update
.push((alert, server.config.send_cpu_alerts));
}
}
(SeverityLevel::Ok, Some(alert)) => {
(SeverityLevel::Ok, Some(alert), true) => {
let mut alert = alert.clone();
alert.data = AlertData::ServerCpu {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
percentage: server_status
.stats
.as_ref()
.map(|s| s.cpu_perc as f64)
.unwrap_or(0.0),
};
alert_ids_to_close.push((alert, server.info.send_cpu_alerts))
alert_ids_to_close
.push((alert, server.config.send_cpu_alerts))
}
_ => {}
}
@@ -186,20 +192,21 @@ pub async fn alert_servers(
.as_ref()
.and_then(|alerts| alerts.get(&AlertDataVariant::ServerMem))
.cloned();
match (health.mem, mem_alert) {
(SeverityLevel::Warning | SeverityLevel::Critical, None) => {
match (health.mem.level, mem_alert, health.mem.should_close_alert)
{
(SeverityLevel::Warning | SeverityLevel::Critical, None, _) => {
// open alert
let alert = Alert {
id: Default::default(),
ts,
resolved: false,
resolved_ts: None,
level: health.mem,
level: health.mem.level,
target: ResourceTarget::Server(server_status.id.clone()),
data: AlertData::ServerMem {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
total_gb: server_status
.stats
.as_ref()
@@ -212,19 +219,20 @@ pub async fn alert_servers(
.unwrap_or(0.0),
},
};
alerts_to_open.push((alert, server.info.send_mem_alerts));
alerts_to_open.push((alert, server.config.send_mem_alerts));
}
(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(mut alert),
_,
) => {
// modify alert level only if it has increased
if alert.level < health.mem {
alert.level = health.mem;
if alert.level < health.mem.level {
alert.level = health.mem.level;
alert.data = AlertData::ServerMem {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
total_gb: server_status
.stats
.as_ref()
@@ -236,15 +244,16 @@ pub async fn alert_servers(
.map(|s| s.mem_used_gb)
.unwrap_or(0.0),
};
alerts_to_update.push((alert, server.info.send_mem_alerts));
alerts_to_update
.push((alert, server.config.send_mem_alerts));
}
}
(SeverityLevel::Ok, Some(alert)) => {
(SeverityLevel::Ok, Some(alert), true) => {
let mut alert = alert.clone();
alert.data = AlertData::ServerMem {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
total_gb: server_status
.stats
.as_ref()
@@ -256,7 +265,8 @@ pub async fn alert_servers(
.map(|s| s.mem_used_gb)
.unwrap_or(0.0),
};
alert_ids_to_close.push((alert, server.info.send_mem_alerts))
alert_ids_to_close
.push((alert, server.config.send_mem_alerts))
}
_ => {}
}
@@ -273,8 +283,12 @@ pub async fn alert_servers(
.as_ref()
.and_then(|alerts| alerts.get(path))
.cloned();
match (*health, disk_alert) {
(SeverityLevel::Warning | SeverityLevel::Critical, None) => {
match (health.level, disk_alert, health.should_close_alert) {
(
SeverityLevel::Warning | SeverityLevel::Critical,
None,
_,
) => {
let disk = server_status.stats.as_ref().and_then(|stats| {
stats.disks.iter().find(|disk| disk.mount == *path)
});
@@ -283,58 +297,60 @@ pub async fn alert_servers(
ts,
resolved: false,
resolved_ts: None,
level: *health,
level: health.level,
target: ResourceTarget::Server(server_status.id.clone()),
data: AlertData::ServerDisk {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
path: path.to_owned(),
total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),
used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),
},
};
alerts_to_open.push((alert, server.info.send_disk_alerts));
alerts_to_open
.push((alert, server.config.send_disk_alerts));
}
(
SeverityLevel::Warning | SeverityLevel::Critical,
Some(mut alert),
_,
) => {
// Disk is persistent, update alert if health changes regardless of direction
if *health != alert.level {
// modify alert level only if it has increased
if health.level < alert.level {
let disk =
server_status.stats.as_ref().and_then(|stats| {
stats.disks.iter().find(|disk| disk.mount == *path)
});
alert.level = *health;
alert.level = health.level;
alert.data = AlertData::ServerDisk {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
path: path.to_owned(),
total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),
used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),
};
alerts_to_update
.push((alert, server.info.send_disk_alerts));
.push((alert, server.config.send_disk_alerts));
}
}
(SeverityLevel::Ok, Some(alert)) => {
(SeverityLevel::Ok, Some(alert), true) => {
let mut alert = alert.clone();
let disk = server_status.stats.as_ref().and_then(|stats| {
stats.disks.iter().find(|disk| disk.mount == *path)
});
alert.level = *health;
alert.level = health.level;
alert.data = AlertData::ServerDisk {
id: server_status.id.clone(),
name: server.name.clone(),
region: optional_string(&server.info.region),
region: optional_string(&server.config.region),
path: path.to_owned(),
total_gb: disk.map(|d| d.total_gb).unwrap_or_default(),
used_gb: disk.map(|d| d.used_gb).unwrap_or_default(),
};
alert_ids_to_close
.push((alert, server.info.send_disk_alerts))
.push((alert, server.config.send_disk_alerts))
}
_ => {}
}
@@ -347,7 +363,7 @@ pub async fn alert_servers(
let mut alert = alert.clone();
alert.level = SeverityLevel::Ok;
alert_ids_to_close
.push((alert, server.info.send_disk_alerts));
.push((alert, server.config.send_disk_alerts));
}
}
}

View File

@@ -6,7 +6,10 @@ use komodo_client::entities::{
network::NetworkListItem, volume::VolumeListItem,
},
repo::Repo,
server::{Server, ServerConfig, ServerHealth, ServerState},
server::{
Server, ServerConfig, ServerHealth, ServerHealthState,
ServerState,
},
stack::{ComposeProject, Stack, StackState},
stats::{SingleDiskUsage, SystemStats},
};
@@ -126,6 +129,8 @@ pub async fn insert_server_status(
.await;
}
const ALERT_PERCENTAGE_THRESHOLD: f32 = 5.0;
fn get_server_health(
server: &Server,
SystemStats {
@@ -148,16 +153,22 @@ fn get_server_health(
let mut health = ServerHealth::default();
if cpu_perc >= cpu_critical {
health.cpu = SeverityLevel::Critical
health.cpu.level = SeverityLevel::Critical;
} else if cpu_perc >= cpu_warning {
health.cpu = SeverityLevel::Warning
health.cpu.level = SeverityLevel::Warning
} else if *cpu_perc < cpu_warning - ALERT_PERCENTAGE_THRESHOLD {
health.cpu.should_close_alert = true
}
let mem_perc = 100.0 * mem_used_gb / mem_total_gb;
if mem_perc >= *mem_critical {
health.mem = SeverityLevel::Critical
health.mem.level = SeverityLevel::Critical
} else if mem_perc >= *mem_warning {
health.mem = SeverityLevel::Warning
health.mem.level = SeverityLevel::Warning
} else if mem_perc
< mem_warning - (ALERT_PERCENTAGE_THRESHOLD as f64)
{
health.mem.should_close_alert = true
}
for SingleDiskUsage {
@@ -168,14 +179,17 @@ fn get_server_health(
} in disks
{
let perc = 100.0 * used_gb / total_gb;
let stats_state = if perc >= *disk_critical {
SeverityLevel::Critical
let mut state = ServerHealthState::default();
if perc >= *disk_critical {
state.level = SeverityLevel::Critical;
} else if perc >= *disk_warning {
SeverityLevel::Warning
} else {
SeverityLevel::Ok
state.level = SeverityLevel::Warning;
} else if perc
< disk_warning - (ALERT_PERCENTAGE_THRESHOLD as f64)
{
state.should_close_alert = true;
};
health.disks.insert(mount.clone(), stats_state);
health.disks.insert(mount.clone(), state);
}
health

View File

@@ -0,0 +1,219 @@
use std::time::Duration;
use anyhow::Context;
use komodo_client::entities::{
action::{
Action, ActionConfig, ActionConfigDiff, ActionInfo,
ActionListItem, ActionListItemInfo, ActionQuerySpecifics,
ActionState, PartialActionConfig,
},
resource::Resource,
update::Update,
user::User,
Operation, ResourceTargetVariant,
};
use mungos::{
find::find_collect,
mongodb::{bson::doc, options::FindOneOptions, Collection},
};
use crate::state::{action_state_cache, action_states, db_client};
impl super::KomodoResource for Action {
type Config = ActionConfig;
type PartialConfig = PartialActionConfig;
type ConfigDiff = ActionConfigDiff;
type Info = ActionInfo;
type ListItem = ActionListItem;
type QuerySpecifics = ActionQuerySpecifics;
fn resource_type() -> ResourceTargetVariant {
ResourceTargetVariant::Action
}
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().actions
}
async fn to_list_item(
action: Resource<Self::Config, Self::Info>,
) -> Self::ListItem {
let state = get_action_state(&action.id).await;
ActionListItem {
name: action.name,
id: action.id,
tags: action.tags,
resource_type: ResourceTargetVariant::Action,
info: ActionListItemInfo {
state,
last_run_at: action.info.last_run_at,
},
}
}
async fn busy(id: &String) -> anyhow::Result<bool> {
action_states()
.action
.get(id)
.await
.unwrap_or_default()
.busy()
}
// CREATE
fn create_operation() -> Operation {
Operation::CreateAction
}
fn user_can_create(user: &User) -> bool {
user.admin
}
async fn validate_create_config(
config: &mut Self::PartialConfig,
_user: &User,
) -> anyhow::Result<()> {
if config.file_contents.is_none() {
config.file_contents =
Some(DEFAULT_ACTION_FILE_CONTENTS.to_string());
}
Ok(())
}
async fn post_create(
_created: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
) -> anyhow::Result<()> {
refresh_action_state_cache().await;
Ok(())
}
// UPDATE
fn update_operation() -> Operation {
Operation::UpdateAction
}
async fn validate_update_config(
_id: &str,
_config: &mut Self::PartialConfig,
_user: &User,
) -> anyhow::Result<()> {
Ok(())
}
async fn post_update(
updated: &Self,
update: &mut Update,
) -> anyhow::Result<()> {
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameAction
}
// DELETE
fn delete_operation() -> Operation {
Operation::DeleteAction
}
async fn pre_delete(
_resource: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
) -> anyhow::Result<()> {
Ok(())
}
async fn post_delete(
_resource: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
) -> anyhow::Result<()> {
Ok(())
}
}
pub fn spawn_action_state_refresh_loop() {
tokio::spawn(async move {
loop {
refresh_action_state_cache().await;
tokio::time::sleep(Duration::from_secs(60)).await;
}
});
}
pub async fn refresh_action_state_cache() {
let _ = async {
let actions = find_collect(&db_client().actions, None, None)
.await
.context("Failed to get Actions from db")?;
let cache = action_state_cache();
for action in actions {
let state = get_action_state_from_db(&action.id).await;
cache.insert(action.id, state).await;
}
anyhow::Ok(())
}
.await
.inspect_err(|e| {
error!("Failed to refresh Action state cache | {e:#}")
});
}
async fn get_action_state(id: &String) -> ActionState {
if action_states()
.action
.get(id)
.await
.map(|s| s.get().map(|s| s.running))
.transpose()
.ok()
.flatten()
.unwrap_or_default()
{
return ActionState::Running;
}
action_state_cache().get(id).await.unwrap_or_default()
}
async fn get_action_state_from_db(id: &str) -> ActionState {
async {
let state = db_client()
.updates
.find_one(doc! {
"target.type": "Action",
"target.id": id,
"operation": "RunAction"
})
.with_options(
FindOneOptions::builder()
.sort(doc! { "start_ts": -1 })
.build(),
)
.await?
.map(|u| {
if u.success {
ActionState::Ok
} else {
ActionState::Failed
}
})
.unwrap_or(ActionState::Ok);
anyhow::Ok(state)
}
.await
.inspect_err(|e| {
warn!("Failed to get Action state for {id} | {e:#}")
})
.unwrap_or(ActionState::Unknown)
}
const DEFAULT_ACTION_FILE_CONTENTS: &str =
"// Run actions using the pre initialized 'komodo' client.
const version: Types.GetVersionResponse = await komodo.read('GetVersion', {});
console.log('🦎 Komodo version:', version.version, '🦎\\n');";

View File

@@ -25,8 +25,8 @@ impl super::KomodoResource for Alerter {
ResourceTargetVariant::Alerter
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().alerters
}
@@ -94,6 +94,12 @@ impl super::KomodoResource for Alerter {
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameAlerter
}
// DELETE
fn delete_operation() -> Operation {

View File

@@ -38,8 +38,8 @@ impl super::KomodoResource for Build {
ResourceTargetVariant::Build
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().builds
}
@@ -118,11 +118,16 @@ impl super::KomodoResource for Build {
}
async fn post_update(
_updated: &Self,
_update: &mut Update,
updated: &Self,
update: &mut Update,
) -> anyhow::Result<()> {
refresh_build_state_cache().await;
Ok(())
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameBuild
}
// DELETE

View File

@@ -31,8 +31,8 @@ impl super::KomodoResource for Builder {
ResourceTargetVariant::Builder
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().builders
}
@@ -118,6 +118,12 @@ impl super::KomodoResource for Builder {
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameBuilder
}
// DELETE
fn delete_operation() -> Operation {

View File

@@ -26,7 +26,6 @@ use crate::{
query::get_deployment_state,
},
monitor::update_cache_for_server,
resource,
state::{action_states, db_client, deployment_status_cache},
};
@@ -44,8 +43,8 @@ impl super::KomodoResource for Deployment {
ResourceTargetVariant::Deployment
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().deployments
}
@@ -132,11 +131,21 @@ impl super::KomodoResource for Deployment {
created: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
) -> anyhow::Result<()> {
if !created.config.server_id.is_empty() {
let server =
resource::get::<Server>(&created.config.server_id).await?;
update_cache_for_server(&server).await;
if created.config.server_id.is_empty() {
return Ok(());
}
let Ok(server) = super::get::<Server>(&created.config.server_id)
.await
.inspect_err(|e| {
warn!(
"Failed to get Server for Deployment {} | {e:#}",
created.name
)
})
else {
return Ok(());
};
update_cache_for_server(&server).await;
Ok(())
}
@@ -156,14 +165,15 @@ impl super::KomodoResource for Deployment {
async fn post_update(
updated: &Self,
_update: &mut Update,
update: &mut Update,
) -> anyhow::Result<()> {
if !updated.config.server_id.is_empty() {
let server =
resource::get::<Server>(&updated.config.server_id).await?;
update_cache_for_server(&server).await;
}
Ok(())
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameDeployment
}
// DELETE

View File

@@ -45,6 +45,7 @@ use crate::{
state::{db_client, State},
};
mod action;
mod alerter;
mod build;
mod builder;
@@ -57,6 +58,9 @@ mod server_template;
mod stack;
mod sync;
pub use action::{
refresh_action_state_cache, spawn_action_state_refresh_loop,
};
pub use build::{
refresh_build_state_cache, spawn_build_state_refresh_loop,
};
@@ -106,8 +110,7 @@ pub trait KomodoResource {
fn resource_type() -> ResourceTargetVariant;
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>>;
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>;
async fn to_list_item(
resource: Resource<Self::Config, Self::Info>,
@@ -165,6 +168,12 @@ pub trait KomodoResource {
update: &mut Update,
) -> anyhow::Result<()>;
// =======
// RENAME
// =======
fn rename_operation() -> Operation;
// =======
// DELETE
// =======
@@ -195,7 +204,6 @@ pub async fn get<T: KomodoResource>(
id_or_name: &str,
) -> anyhow::Result<Resource<T::Config, T::Info>> {
T::coll()
.await
.find_one(id_or_name_filter(id_or_name))
.await
.context("failed to query db for resource")?
@@ -240,9 +248,24 @@ pub async fn get_check_permissions<T: KomodoResource>(
/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access).
#[instrument(level = "debug")]
pub async fn get_resource_ids_for_user<T: KomodoResource>(
pub async fn get_resource_object_ids_for_user<T: KomodoResource>(
user: &User,
) -> anyhow::Result<Option<Vec<ObjectId>>> {
get_resource_ids_for_user::<T>(user).await.map(|ids| {
ids.map(|ids| {
ids
.into_iter()
.flat_map(|id| ObjectId::from_str(&id))
.collect()
})
})
}
/// Returns None if still no need to filter by resource id (eg transparent mode, group membership with all access).
#[instrument(level = "debug")]
pub async fn get_resource_ids_for_user<T: KomodoResource>(
user: &User,
) -> anyhow::Result<Option<Vec<String>>> {
// Check admin or transparent mode
if user.admin || core_config().transparent_mode {
return Ok(None);
@@ -270,8 +293,8 @@ pub async fn get_resource_ids_for_user<T: KomodoResource>(
let (base, perms) = tokio::try_join!(
// Get any resources with non-none base permission,
find_collect(
T::coll().await,
doc! { "base_permission": { "$ne": "None" } },
T::coll(),
doc! { "base_permission": { "$exists": true, "$ne": "None" } },
None,
)
.map(|res| res.with_context(|| format!(
@@ -283,7 +306,7 @@ pub async fn get_resource_ids_for_user<T: KomodoResource>(
doc! {
"$or": user_target_query(&user.id, &groups)?,
"resource_target.type": resource_type.as_ref(),
"level": { "$in": ["Read", "Execute", "Write"] }
"level": { "$exists": true, "$ne": "None" }
},
None,
)
@@ -297,9 +320,6 @@ pub async fn get_resource_ids_for_user<T: KomodoResource>(
// Chain in the ones with non-None base permissions
.chain(base.into_iter().map(|res| res.id))
// collect into hashset first to remove any duplicates
.collect::<HashSet<_>>()
.into_iter()
.flat_map(|id| ObjectId::from_str(&id))
.collect::<HashSet<_>>();
Ok(Some(ids.into_iter().collect()))
@@ -382,8 +402,9 @@ pub async fn get_user_permission_on_resource<T: KomodoResource>(
pub async fn list_for_user<T: KomodoResource>(
mut query: ResourceQuery<T::QuerySpecifics>,
user: &User,
all_tags: &[Tag],
) -> anyhow::Result<Vec<T::ListItem>> {
validate_resource_query_tags(&mut query).await;
validate_resource_query_tags(&mut query, all_tags)?;
let mut filters = Document::new();
query.add_filters(&mut filters);
list_for_user_using_document::<T>(filters, user).await
@@ -404,8 +425,9 @@ pub async fn list_for_user_using_document<T: KomodoResource>(
pub async fn list_full_for_user<T: KomodoResource>(
mut query: ResourceQuery<T::QuerySpecifics>,
user: &User,
all_tags: &[Tag],
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {
validate_resource_query_tags(&mut query).await;
validate_resource_query_tags(&mut query, all_tags)?;
let mut filters = Document::new();
query.add_filters(&mut filters);
list_full_for_user_using_document::<T>(filters, user).await
@@ -416,11 +438,13 @@ pub async fn list_full_for_user_using_document<T: KomodoResource>(
mut filters: Document,
user: &User,
) -> anyhow::Result<Vec<Resource<T::Config, T::Info>>> {
if let Some(ids) = get_resource_ids_for_user::<T>(user).await? {
if let Some(ids) =
get_resource_object_ids_for_user::<T>(user).await?
{
filters.insert("_id", doc! { "$in": ids });
}
find_collect(
T::coll().await,
T::coll(),
filters,
FindOptions::builder().sort(doc! { "name": 1 }).build(),
)
@@ -443,7 +467,7 @@ pub async fn get_id_to_resource_map<T: KomodoResource>(
id_to_tags: &HashMap<String, Tag>,
match_tags: &[String],
) -> anyhow::Result<IdResourceMap<T>> {
let res = find_collect(T::coll().await, None, None)
let res = find_collect(T::coll(), None, None)
.await
.with_context(|| {
format!("failed to pull {}s from mongo", T::resource_type())
@@ -510,7 +534,7 @@ pub async fn create<T: KomodoResource>(
// Ensure an existing resource with same name doesn't already exist
// The database indexing also ensures this but doesn't give a good error message.
if list_full_for_user::<T>(Default::default(), system_user())
if list_full_for_user::<T>(Default::default(), system_user(), &[])
.await
.context("Failed to list all resources for duplicate name check")?
.into_iter()
@@ -535,7 +559,6 @@ pub async fn create<T: KomodoResource>(
};
let resource_id = T::coll()
.await
.insert_one(&resource)
.await
.with_context(|| {
@@ -603,7 +626,7 @@ pub async fn update<T: KomodoResource>(
let diff = resource.config.partial_diff(config);
if diff.is_none() {
return Err(anyhow!("update has no changes"));
return Ok(resource);
}
let mut diff_log = String::from("diff");
@@ -624,14 +647,9 @@ pub async fn update<T: KomodoResource>(
let update_doc = flatten_document(doc! { "config": config_doc });
update_one_by_id(
T::coll().await,
&id,
doc! { "$set": update_doc },
None,
)
.await
.context("failed to update resource on database")?;
update_one_by_id(T::coll(), &id, doc! { "$set": update_doc }, None)
.await
.context("failed to update resource on database")?;
let mut update = make_update(
resource_target::<T>(id),
@@ -671,6 +689,7 @@ fn resource_target<T: KomodoResource>(id: String) -> ResourceTarget {
ResourceTarget::ResourceSync(id)
}
ResourceTargetVariant::Stack => ResourceTarget::Stack(id),
ResourceTargetVariant::Action => ResourceTarget::Action(id),
}
}
@@ -686,7 +705,6 @@ pub async fn update_description<T: KomodoResource>(
)
.await?;
T::coll()
.await
.update_one(
id_or_name_filter(id_or_name),
doc! { "$set": { "description": description } },
@@ -720,7 +738,6 @@ pub async fn update_tags<T: KomodoResource>(
.flatten()
.collect::<Vec<_>>();
T::coll()
.await
.update_one(
id_or_name_filter(id_or_name),
doc! { "$set": { "tags": tags } },
@@ -733,13 +750,67 @@ pub async fn remove_tag_from_all<T: KomodoResource>(
tag_id: &str,
) -> anyhow::Result<()> {
T::coll()
.await
.update_many(doc! {}, doc! { "$pull": { "tags": tag_id } })
.await
.context("failed to remove tag from resources")?;
Ok(())
}
// =======
// RENAME
// =======
pub async fn rename<T: KomodoResource>(
id_or_name: &str,
name: &str,
user: &User,
) -> anyhow::Result<Update> {
let resource = get_check_permissions::<T>(
id_or_name,
user,
PermissionLevel::Write,
)
.await?;
let mut update = make_update(
resource_target::<T>(resource.id.clone()),
T::rename_operation(),
user,
);
let name = to_komodo_name(name);
update_one_by_id(
T::coll(),
&resource.id,
mungos::update::Update::Set(
doc! { "name": &name, "updated_at": komodo_timestamp() },
),
None,
)
.await
.with_context(|| {
format!(
"Failed to update {ty} on db. This name may already be taken.",
ty = T::resource_type()
)
})?;
update.push_simple_log(
&format!("Rename {}", T::resource_type()),
format!(
"Renamed {ty} {id} from {prev_name} to {name}",
ty = T::resource_type(),
id = resource.id,
prev_name = resource.name
),
);
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
// =======
// DELETE
// =======
@@ -769,7 +840,7 @@ pub async fn delete<T: KomodoResource>(
delete_all_permissions_on_resource(target.clone()).await;
remove_from_recently_viewed(target.clone()).await;
delete_one_by_id(T::coll().await, &resource.id, None)
delete_one_by_id(T::coll(), &resource.id, None)
.await
.with_context(|| {
format!("failed to delete {} from database", T::resource_type())
@@ -793,14 +864,24 @@ pub async fn delete<T: KomodoResource>(
// =======
#[instrument(level = "debug")]
pub async fn validate_resource_query_tags<
T: Default + std::fmt::Debug,
>(
pub fn validate_resource_query_tags<T: Default + std::fmt::Debug>(
query: &mut ResourceQuery<T>,
) {
let futures = query.tags.iter().map(|tag| get_tag(tag));
let res = join_all(futures).await;
query.tags = res.into_iter().flatten().map(|tag| tag.id).collect();
all_tags: &[Tag],
) -> anyhow::Result<()> {
query.tags = query
.tags
.iter()
.map(|tag| {
all_tags
.iter()
.find(|t| t.name == *tag || t.id == *tag)
.map(|tag| tag.id.clone())
.with_context(|| {
format!("No tag found matching name or id: {}", tag)
})
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(())
}
#[instrument]
@@ -834,6 +915,7 @@ where
ResourceTarget::Build(id) => ("recents.Build", id),
ResourceTarget::Repo(id) => ("recents.Repo", id),
ResourceTarget::Procedure(id) => ("recents.Procedure", id),
ResourceTarget::Action(id) => ("recents.Action", id),
ResourceTarget::Stack(id) => ("recents.Stack", id),
ResourceTarget::Builder(id) => ("recents.Builder", id),
ResourceTarget::Alerter(id) => ("recents.Alerter", id),

View File

@@ -4,6 +4,7 @@ use anyhow::{anyhow, Context};
use komodo_client::{
api::execute::Execution,
entities::{
action::Action,
build::Build,
deployment::Deployment,
permission::PermissionLevel,
@@ -44,8 +45,8 @@ impl super::KomodoResource for Procedure {
ResourceTargetVariant::Procedure
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().procedures
}
@@ -114,11 +115,16 @@ impl super::KomodoResource for Procedure {
}
async fn post_update(
_updated: &Self,
_update: &mut Update,
updated: &Self,
update: &mut Update,
) -> anyhow::Result<()> {
refresh_procedure_state_cache().await;
Ok(())
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameProcedure
}
// DELETE
@@ -172,6 +178,15 @@ async fn validate_config(
}
params.procedure = procedure.id;
}
Execution::RunAction(params) => {
let action = super::get_check_permissions::<Action>(
&params.action,
user,
PermissionLevel::Execute,
)
.await?;
params.action = action.id;
}
Execution::RunBuild(params) => {
let build = super::get_check_permissions::<Build>(
&params.build,
@@ -494,6 +509,16 @@ async fn validate_config(
.await?;
params.sync = sync.id;
}
Execution::CommitSync(params) => {
// This one is actually a write operation.
let sync = super::get_check_permissions::<ResourceSync>(
&params.sync,
user,
PermissionLevel::Write,
)
.await?;
params.sync = sync.id;
}
Execution::DeployStack(params) => {
let stack = super::get_check_permissions::<Stack>(
&params.stack,
@@ -588,7 +613,7 @@ pub async fn refresh_procedure_state_cache() {
let procedures =
find_collect(&db_client().procedures, None, None)
.await
.context("failed to get procedures from db")?;
.context("Failed to get Procedures from db")?;
let cache = procedure_state_cache();
for procedure in procedures {
let state = get_procedure_state_from_db(&procedure.id).await;
@@ -598,7 +623,7 @@ pub async fn refresh_procedure_state_cache() {
}
.await
.inspect_err(|e| {
error!("failed to refresh build state cache | {e:#}")
error!("Failed to refresh Procedure state cache | {e:#}")
});
}
@@ -645,7 +670,7 @@ async fn get_procedure_state_from_db(id: &str) -> ProcedureState {
}
.await
.inspect_err(|e| {
warn!("failed to get procedure state for {id} | {e:#}")
warn!("Failed to get Procedure state for {id} | {e:#}")
})
.unwrap_or(ProcedureState::Unknown)
}

View File

@@ -11,6 +11,7 @@ use komodo_client::entities::{
},
resource::Resource,
server::Server,
to_komodo_name,
update::Update,
user::User,
Operation, ResourceTargetVariant,
@@ -43,8 +44,8 @@ impl super::KomodoResource for Repo {
ResourceTargetVariant::Repo
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().repos
}
@@ -132,6 +133,12 @@ impl super::KomodoResource for Repo {
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameRepo
}
// DELETE
fn delete_operation() -> Operation {
@@ -158,7 +165,11 @@ impl super::KomodoResource for Repo {
match periphery
.request(DeleteRepo {
name: repo.name.clone(),
name: if repo.config.path.is_empty() {
to_komodo_name(&repo.name)
} else {
repo.config.path.clone()
},
})
.await
{

View File

@@ -30,8 +30,8 @@ impl super::KomodoResource for Server {
ResourceTargetVariant::Server
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().servers
}
@@ -115,6 +115,12 @@ impl super::KomodoResource for Server {
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameServer
}
// DELETE
fn delete_operation() -> Operation {

View File

@@ -29,8 +29,8 @@ impl super::KomodoResource for ServerTemplate {
ResourceTargetVariant::ServerTemplate
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().server_templates
}
@@ -117,6 +117,12 @@ impl super::KomodoResource for ServerTemplate {
Ok(())
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameServerTemplate
}
// DELETE
fn delete_operation() -> Operation {

View File

@@ -44,8 +44,8 @@ impl super::KomodoResource for Stack {
ResourceTargetVariant::Stack
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().stacks
}
@@ -172,7 +172,7 @@ impl super::KomodoResource for Stack {
.await
.inspect_err(|e| {
warn!(
"Failed to get server for stack {} | {e:#}",
"Failed to get Server for Stack {} | {e:#}",
created.name
)
})
@@ -204,6 +204,12 @@ impl super::KomodoResource for Stack {
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameStack
}
// DELETE
fn delete_operation() -> Operation {

View File

@@ -41,8 +41,8 @@ impl super::KomodoResource for ResourceSync {
ResourceTargetVariant::ResourceSync
}
async fn coll(
) -> &'static Collection<Resource<Self::Config, Self::Info>> {
fn coll() -> &'static Collection<Resource<Self::Config, Self::Info>>
{
&db_client().resource_syncs
}
@@ -117,6 +117,7 @@ impl super::KomodoResource for ResourceSync {
format_serror(&e.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(())
}
@@ -141,6 +142,12 @@ impl super::KomodoResource for ResourceSync {
Self::post_create(updated, update).await
}
// RENAME
fn rename_operation() -> Operation {
Operation::RenameResourceSync
}
// DELETE
fn delete_operation() -> Operation {

View File

@@ -5,6 +5,7 @@ use std::{
use anyhow::Context;
use komodo_client::entities::{
action::ActionState,
build::BuildState,
config::core::{CoreConfig, GithubWebhookAppConfig},
deployment::DeploymentState,
@@ -191,6 +192,14 @@ pub fn procedure_state_cache() -> &'static ProcedureStateCache {
PROCEDURE_STATE_CACHE.get_or_init(Default::default)
}
pub type ActionStateCache = Cache<String, ActionState>;
pub fn action_state_cache() -> &'static ActionStateCache {
static ACTION_STATE_CACHE: OnceLock<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

View File

@@ -32,7 +32,7 @@ pub async fn get_updates_for_execution<
id_to_tags: &HashMap<String, Tag>,
match_tags: &[String],
) -> anyhow::Result<UpdatesResult<Resource::PartialConfig>> {
let map = find_collect(Resource::coll().await, None, None)
let map = find_collect(Resource::coll(), None, None)
.await
.context("failed to get resources from db")?
.into_iter()

View File

@@ -80,9 +80,25 @@ pub fn read_resources(
} else {
logs.push(Log::simple("Read remote resources", log));
};
} else if !full_path.exists() {
file_errors.push(SyncFileContents {
resource_path: String::new(),
path: resource_path.display().to_string(),
contents: format_serror(
&anyhow!("Initialize the file to proceed.")
.context(format!("Path {full_path:?} does not exist."))
.into(),
),
});
log.push_str(&format!(
"{}: Resoure path {} does not exist.",
colored("ERROR", Color::Red),
bold(resource_path.display())
));
logs.push(Log::error("Read remote resources", log));
} else {
log.push_str(&format!(
"{}: Resoure path {} is neither a file nor a directory.",
"{}: Resoure path {} exists, but is neither a file nor a directory.",
colored("WARN", Color::Red),
bold(resource_path.display())
));
@@ -134,7 +150,7 @@ fn read_resource_file(
log.push('\n');
let path_for_view =
if let Some(resource_path) = resource_path.as_ref() {
resource_path.join(&file_path)
resource_path.join(file_path)
} else {
file_path.to_path_buf()
};
@@ -164,7 +180,7 @@ fn read_resources_directory(
file_errors: &mut Vec<SyncFileContents>,
) -> anyhow::Result<()> {
let full_resource_path = root_path.join(resource_path);
let full_path = full_resource_path.join(&curr_path);
let full_path = full_resource_path.join(curr_path);
let directory = fs::read_dir(&full_path).with_context(|| {
format!("Failed to read directory contents at {full_path:?}")
})?;
@@ -246,6 +262,9 @@ pub fn extend_resources(
resources
.procedures
.extend(filter_by_tag(more.procedures, match_tags));
resources
.actions
.extend(filter_by_tag(more.actions, match_tags));
resources
.alerters
.extend(filter_by_tag(more.alerters, match_tags));

View File

@@ -1,7 +1,7 @@
use std::{collections::HashMap, str::FromStr};
use komodo_client::entities::{
alerter::Alerter, build::Build, builder::Builder,
action::Action, alerter::Alerter, build::Build, builder::Builder,
deployment::Deployment, procedure::Procedure, repo::Repo,
server::Server, server_template::ServerTemplate, stack::Stack,
sync::ResourceSync, tag::Tag, toml::ResourceToml, ResourceTarget,
@@ -147,6 +147,7 @@ pub struct AllResourcesById {
pub builds: HashMap<String, Build>,
pub repos: HashMap<String, Repo>,
pub procedures: HashMap<String, Procedure>,
pub actions: HashMap<String, Action>,
pub builders: HashMap<String, Builder>,
pub alerters: HashMap<String, Alerter>,
pub templates: HashMap<String, ServerTemplate>,
@@ -181,6 +182,10 @@ impl AllResourcesById {
id_to_tags, match_tags,
)
.await?,
actions: crate::resource::get_id_to_resource_map::<Action>(
id_to_tags, match_tags,
)
.await?,
builders: crate::resource::get_id_to_resource_map::<Builder>(
id_to_tags, match_tags,
)

View File

@@ -4,6 +4,7 @@ use formatting::{bold, colored, muted, Color};
use komodo_client::{
api::execute::Execution,
entities::{
action::Action,
alerter::Alerter,
build::Build,
builder::{Builder, BuilderConfig},
@@ -233,6 +234,22 @@ impl ResourceSyncTrait for ServerTemplate {
impl ExecuteResourceSync for ServerTemplate {}
impl ResourceSyncTrait for Action {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::Action(id)
}
fn get_diff(
original: Self::Config,
update: Self::PartialConfig,
_resources: &AllResourcesById,
) -> anyhow::Result<Self::ConfigDiff> {
Ok(original.partial_diff(update))
}
}
impl ExecuteResourceSync for Action {}
impl ResourceSyncTrait for ResourceSync {
fn resource_target(id: String) -> ResourceTarget {
ResourceTarget::ResourceSync(id)
@@ -343,6 +360,13 @@ impl ResourceSyncTrait for Procedure {
.map(|p| p.name.clone())
.unwrap_or_default();
}
Execution::RunAction(config) => {
config.action = resources
.actions
.get(&config.action)
.map(|p| p.name.clone())
.unwrap_or_default();
}
Execution::RunBuild(config) => {
config.build = resources
.builds
@@ -588,6 +612,13 @@ impl ResourceSyncTrait for Procedure {
.map(|s| s.name.clone())
.unwrap_or_default();
}
Execution::CommitSync(config) => {
config.sync = resources
.syncs
.get(&config.sync)
.map(|s| s.name.clone())
.unwrap_or_default();
}
Execution::DeployStack(config) => {
config.stack = resources
.stacks
@@ -675,14 +706,14 @@ impl ExecuteResourceSync for Procedure {
{
has_error = true;
log.push_str(&format!(
"{}: failed to delete {} '{}' | {e:#}",
"\n{}: failed to delete {} '{}' | {e:#}",
colored("ERROR", Color::Red),
Self::resource_type(),
bold(&name),
))
} else {
log.push_str(&format!(
"{}: {} {} '{}'",
"\n{}: {} {} '{}'",
muted("INFO"),
colored("deleted", Color::Red),
Self::resource_type(),

View File

@@ -4,6 +4,7 @@ use anyhow::Context;
use komodo_client::{
api::execute::Execution,
entities::{
action::Action,
alerter::Alerter,
build::Build,
builder::{Builder, BuilderConfig, PartialBuilderConfig},
@@ -164,6 +165,7 @@ pub fn convert_resource<R: KomodoResource>(
impl ToToml for Alerter {}
impl ToToml for Server {}
impl ToToml for ResourceSync {}
impl ToToml for Action {}
impl ToToml for Stack {
fn replace_ids(
@@ -412,6 +414,13 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::RunAction(exec) => exec.action.clone_from(
all
.actions
.get(&exec.action)
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::RunBuild(exec) => exec.build.clone_from(
all
.builds
@@ -687,6 +696,13 @@ impl ToToml for Procedure {
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::CommitSync(exec) => exec.sync.clone_from(
all
.syncs
.get(&exec.sync)
.map(|r| &r.name)
.unwrap_or(&String::new()),
),
Execution::DeployStack(exec) => exec.stack.clone_from(
all
.stacks

View File

@@ -15,7 +15,8 @@ use komodo_client::{
sync::DiffData,
toml::{PermissionToml, UserGroupToml},
update::Log,
user::sync_user,
user::{sync_user, User},
user_group::UserGroup,
ResourceTarget, ResourceTargetVariant,
},
};
@@ -43,17 +44,21 @@ pub async fn get_updates_for_view(
delete: bool,
all_resources: &AllResourcesById,
) -> anyhow::Result<Vec<DiffData>> {
let map = find_collect(&db_client().user_groups, None, None)
let _curr = find_collect(&db_client().user_groups, None, None)
.await
.context("failed to query db for UserGroups")?
.context("failed to query db for UserGroups")?;
let mut curr = Vec::with_capacity(_curr.capacity());
convert_user_groups(_curr.into_iter(), all_resources, &mut curr)
.await?;
let map = curr
.into_iter()
.map(|ug| (ug.name.clone(), ug))
.map(|ug| (ug.1.name.clone(), ug))
.collect::<HashMap<_, _>>();
let mut diffs = Vec::<DiffData>::new();
if delete {
for user_group in map.values() {
for (_id, user_group) in map.values() {
if !user_groups.iter().any(|ug| ug.name == user_group.name) {
diffs.push(DiffData::Delete {
current: format!(
@@ -66,13 +71,6 @@ pub async fn get_updates_for_view(
}
}
let id_to_user = find_collect(&db_client().users, None, None)
.await
.context("failed to query db for Users")?
.into_iter()
.map(|user| (user.id.clone(), user))
.collect::<HashMap<_, _>>();
for mut user_group in user_groups {
user_group
.permissions
@@ -90,7 +88,10 @@ pub async fn get_updates_for_view(
)
})?;
let original = match map.get(&user_group.name).cloned() {
let (_original_id, original) = match map
.get(&user_group.name)
.cloned()
{
Some(original) => original,
None => {
diffs.push(DiffData::Create {
@@ -104,121 +105,16 @@ pub async fn get_updates_for_view(
continue;
}
};
let mut original_users = original
.users
.clone()
.into_iter()
.filter_map(|user_id| {
id_to_user.get(&user_id).map(|u| u.username.clone())
})
.collect::<Vec<_>>();
let mut original_permissions = State
.resolve(
ListUserTargetPermissions {
user_target: UserTarget::UserGroup(original.id.clone()),
},
sync_user().to_owned(),
)
.await
.context("failed to query for existing UserGroup permissions")?
.into_iter()
.filter(|p| p.level > PermissionLevel::None)
.map(|mut p| {
// replace the ids with names
match &mut p.resource_target {
ResourceTarget::System(_) => {}
ResourceTarget::Build(id) => {
*id = all_resources
.builds
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Builder(id) => {
*id = all_resources
.builders
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Deployment(id) => {
*id = all_resources
.deployments
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Server(id) => {
*id = all_resources
.servers
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Repo(id) => {
*id = all_resources
.repos
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Alerter(id) => {
*id = all_resources
.alerters
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Procedure(id) => {
*id = all_resources
.procedures
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::ServerTemplate(id) => {
*id = all_resources
.templates
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::ResourceSync(id) => {
*id = all_resources
.syncs
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Stack(id) => {
*id = all_resources
.stacks
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
}
PermissionToml {
target: p.resource_target,
level: p.level,
}
})
.collect::<Vec<_>>();
original_users.sort();
user_group.users.sort();
let all_diff = diff_group_all(&original.all, &user_group.all);
user_group.permissions.sort_by(sort_permissions);
original_permissions.sort_by(sort_permissions);
let update_users = user_group.users != original_users;
let update_users = user_group.users != original.users;
let update_all = !all_diff.is_empty();
let update_permissions =
user_group.permissions != original_permissions;
user_group.permissions != original.permissions;
// only add log after diff detected
if update_users || update_all || update_permissions {
@@ -379,6 +275,13 @@ pub async fn get_updates_for_execution(
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::Action(id) => {
*id = all_resources
.actions
.get(id)
.map(|b| b.name.clone())
.unwrap_or_default()
}
ResourceTarget::ServerTemplate(id) => {
*id = all_resources
.templates
@@ -820,6 +723,17 @@ async fn expand_user_group_permissions(
});
expanded.extend(permissions);
}
ResourceTargetVariant::Action => {
let permissions = all_resources
.actions
.values()
.filter(|resource| regex.is_match(&resource.name))
.map(|resource| PermissionToml {
target: ResourceTarget::Action(resource.name.clone()),
level: permission.level,
});
expanded.extend(permissions);
}
ResourceTargetVariant::ServerTemplate => {
let permissions = all_resources
.templates
@@ -900,3 +814,139 @@ fn diff_group_all(
to_update
}
pub async fn convert_user_groups(
user_groups: impl Iterator<Item = UserGroup>,
all: &AllResourcesById,
res: &mut Vec<(String, UserGroupToml)>,
) -> anyhow::Result<()> {
let db = db_client();
let usernames = find_collect(&db.users, None, None)
.await?
.into_iter()
.map(|user| (user.id, user.username))
.collect::<HashMap<_, _>>();
for user_group in user_groups {
// this method is admin only, but we already know user can see user group if above does not return Err
let mut permissions = State
.resolve(
ListUserTargetPermissions {
user_target: UserTarget::UserGroup(user_group.id.clone()),
},
User {
admin: true,
..Default::default()
},
)
.await?
.into_iter()
.map(|mut permission| {
match &mut permission.resource_target {
ResourceTarget::Build(id) => {
*id = all
.builds
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Builder(id) => {
*id = all
.builders
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Deployment(id) => {
*id = all
.deployments
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Server(id) => {
*id = all
.servers
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Repo(id) => {
*id = all
.repos
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Alerter(id) => {
*id = all
.alerters
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Procedure(id) => {
*id = all
.procedures
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Action(id) => {
*id = all
.actions
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::ServerTemplate(id) => {
*id = all
.templates
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::ResourceSync(id) => {
*id = all
.syncs
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::Stack(id) => {
*id = all
.stacks
.get(id)
.map(|r| r.name.clone())
.unwrap_or_default()
}
ResourceTarget::System(_) => {}
}
PermissionToml {
target: permission.resource_target,
level: permission.level,
}
})
.collect::<Vec<_>>();
let mut users = user_group
.users
.into_iter()
.filter_map(|user_id| usernames.get(&user_id).cloned())
.collect::<Vec<_>>();
permissions.sort_by(sort_permissions);
users.sort();
res.push((
user_group.id,
UserGroupToml {
name: user_group.name,
users,
all: user_group.all,
permissions,
},
));
}
Ok(())
}

View File

@@ -22,7 +22,7 @@ pub async fn push_updates_for_view<Resource: ResourceSyncTrait>(
match_tags: &[String],
diffs: &mut Vec<ResourceDiff>,
) -> anyhow::Result<()> {
let current_map = find_collect(Resource::coll().await, None, None)
let current_map = find_collect(Resource::coll(), None, None)
.await
.context("failed to get resources from db")?
.into_iter()

64
bin/core/src/ts_client.rs Normal file
View File

@@ -0,0 +1,64 @@
use anyhow::{anyhow, Context};
use axum::{
extract::Path,
http::{HeaderMap, HeaderValue},
routing::get,
Router,
};
use reqwest::StatusCode;
use serde::Deserialize;
use serror::AddStatusCodeError;
use tokio::fs;
use crate::config::core_config;
pub fn router() -> Router {
Router::new().route("/:path", get(serve_client_file))
}
const ALLOWED_FILES: &[&str] = &[
"lib.js",
"lib.d.ts",
"types.js",
"types.d.ts",
"responses.js",
"responses.d.ts",
];
#[derive(Deserialize)]
struct FilePath {
path: String,
}
async fn serve_client_file(
Path(FilePath { path }): Path<FilePath>,
) -> serror::Result<(HeaderMap, String)> {
if !ALLOWED_FILES.contains(&path.as_str()) {
return Err(
anyhow!("File {path} not found.")
.status_code(StatusCode::NOT_FOUND),
);
}
let contents = fs::read_to_string(format!(
"{}/client/{path}",
core_config().frontend_path
))
.await
.with_context(|| format!("Failed to read file: {path}"))?;
let mut headers = HeaderMap::new();
if path.ends_with(".js") {
headers.insert(
"X-TypeScript-Types",
HeaderValue::from_str(&format!(
"/client/{}",
path.replace(".js", ".d.ts")
))
.context("?? Invalid Header Value")?,
);
}
Ok((headers, contents))
}

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, Context};
use anyhow::anyhow;
use axum::{
extract::{
ws::{Message, WebSocket},
@@ -15,7 +15,6 @@ use komodo_client::{
},
ws::WsLoginMessage,
};
use mungos::by_id::find_one_by_id;
use serde_json::json;
use serror::serialize_error;
use tokio::select;
@@ -23,11 +22,10 @@ use tokio_util::sync::CancellationToken;
use crate::{
auth::{auth_api_key_check_enabled, auth_jwt_check_enabled},
db::DbClient,
helpers::{
channel::update_channel, query::get_user_permission_on_target,
channel::update_channel,
query::{get_user, get_user_permission_on_target},
},
state::db_client,
};
pub fn router() -> Router {
@@ -51,7 +49,6 @@ async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
let cancel_clone = cancel.clone();
tokio::spawn(async move {
let db_client = db_client();
loop {
// poll for updates off the receiver / await cancel.
let update = select! {
@@ -61,7 +58,7 @@ async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
// before sending every update, verify user is still valid.
// kill the connection is user if found to be invalid.
let user = check_user_valid(db_client, &user.id).await;
let user = check_user_valid(&user.id).await;
let user = match user {
Err(e) => {
let _ = ws_sender
@@ -183,15 +180,9 @@ enum LoginMessage {
Err(String),
}
#[instrument(level = "debug", skip(db_client))]
async fn check_user_valid(
db_client: &DbClient,
user_id: &str,
) -> anyhow::Result<User> {
let user = find_one_by_id(&db_client.users, user_id)
.await
.context("failed to query mongo for users")?
.context("user not found")?;
#[instrument(level = "debug")]
async fn check_user_valid(user_id: &str) -> anyhow::Result<User> {
let user = get_user(user_id).await?;
if !user.enabled {
return Err(anyhow!("user not enabled"));
}

View File

@@ -1,23 +0,0 @@
[package]
name = "migrator"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# komodo_client.workspace = true
logger.workspace = true
#
# mungos.workspace = true
#
tokio.workspace = true
anyhow.workspace = true
dotenvy.workspace = true
envy.workspace = true
serde.workspace = true
tracing.workspace = true

View File

@@ -1,16 +0,0 @@
FROM rust:1.80.1-bookworm AS builder
WORKDIR /builder
COPY . .
RUN cargo build -p migrator --release
# Final Image
FROM gcr.io/distroless/cc-debian12
COPY --from=builder /builder/target/release/migrator /
# Label for Ghcr
LABEL org.opencontainers.image.source=https://github.com/mbecker20/komodo
LABEL org.opencontainers.image.description="Database migrator for Komodo version upgrades"
LABEL org.opencontainers.image.licenses=GPL-3.0
CMD ["./migrator"]

View File

@@ -1,25 +0,0 @@
# Migrator
Performs schema changes on the Komodo database
## v1.7 - v1.11 migration
Run this before upgrading to latest from versions 1.7 to 1.11.
```sh
docker run --rm --name komodo-migrator \
--network "host" \
--env MIGRATION="v1.11" \
--env TARGET_URI="mongodb://<USERNAME>:<PASSWORD>@<ADDRESS>" \
--env TARGET_DB_NAME="<DB_NAME>" \
ghcr.io/mbecker20/komodo_migrator
```
## v1.0 - v1.6 migration
Run this before upgrading to latest from versions 1.0 to 1.6.
```sh
docker run --rm --name komodo-migrator \
--network "host" \
--env MIGRATION="v1.6" \
--env TARGET_URI="mongodb://<USERNAME>:<PASSWORD>@<ADDRESS>" \
--env TARGET_DB_NAME="<DB_NAME>" \
ghcr.io/mbecker20/komodo_migrator
```

View File

@@ -1,2 +0,0 @@
#[allow(unused)]
pub mod v1_11;

View File

@@ -1,261 +0,0 @@
use komodo_client::entities::{
build::StandardRegistryConfig, EnvironmentVar, NoData,
SystemCommand, Version, I64,
};
use serde::{Deserialize, Serialize};
use super::resource::Resource;
pub type Build = Resource<BuildConfig, BuildInfo>;
impl From<Build> for komodo_client::entities::build::Build {
fn from(value: Build) -> Self {
komodo_client::entities::build::Build {
id: value.id,
name: value.name,
description: value.description,
updated_at: value.updated_at,
tags: value.tags,
info: komodo_client::entities::build::BuildInfo {
last_built_at: value.info.last_built_at,
built_hash: None,
built_message: None,
latest_hash: None,
latest_message: None,
},
config: value.config.into(),
base_permission: Default::default(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct BuildInfo {
pub last_built_at: I64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildConfig {
/// Which builder is used to build the image.
#[serde(default, alias = "builder")]
pub builder_id: String,
/// The current version of the build.
#[serde(default)]
pub version: Version,
/// The Github repo used as the source of the build.
#[serde(default)]
pub repo: String,
/// The branch of the repo.
#[serde(default = "default_branch")]
pub branch: String,
/// Optionally set a specific commit hash.
#[serde(default)]
pub commit: String,
/// The github account used to clone (used to access private repos).
/// Empty string is public clone (only public repos).
#[serde(default)]
pub github_account: String,
/// The optional command run after repo clone and before docker build.
#[serde(default)]
pub pre_build: SystemCommand,
/// Configuration for the registry to push the built image to.
#[serde(default)]
pub image_registry: ImageRegistry,
/// The path of the docker build context relative to the root of the repo.
/// Default: "." (the root of the repo).
#[serde(default = "default_build_path")]
pub build_path: String,
/// The path of the dockerfile relative to the build path.
#[serde(default = "default_dockerfile_path")]
pub dockerfile_path: String,
/// Whether to skip secret interpolation in the build_args.
#[serde(default)]
pub skip_secret_interp: bool,
/// Whether to use buildx to build (eg `docker buildx build ...`)
#[serde(default)]
pub use_buildx: bool,
/// Whether incoming webhooks actually trigger action.
#[serde(default = "default_webhook_enabled")]
pub webhook_enabled: bool,
/// Any extra docker cli arguments to be included in the build command
#[serde(default)]
pub extra_args: Vec<String>,
/// Docker build arguments.
///
/// These values are visible in the final image by running `docker inspect`.
#[serde(
default,
deserialize_with = "komodo_client::entities::env_vars_deserializer"
)]
pub build_args: Vec<EnvironmentVar>,
/// Secret arguments.
///
/// These values remain hidden in the final image by using
/// docker secret mounts. See `<https://docs.docker.com/build/building/secrets>`.
///
/// The values can be used in RUN commands:
/// ```
/// RUN --mount=type=secret,id=SECRET_KEY \
/// SECRET_KEY=$(cat /run/secrets/SECRET_KEY) ...
/// ```
#[serde(
default,
deserialize_with = "komodo_client::entities::env_vars_deserializer"
)]
pub secret_args: Vec<EnvironmentVar>,
/// Docker labels
#[serde(
default,
deserialize_with = "komodo_client::entities::env_vars_deserializer"
)]
pub labels: Vec<EnvironmentVar>,
}
impl From<BuildConfig>
for komodo_client::entities::build::BuildConfig
{
fn from(value: BuildConfig) -> Self {
komodo_client::entities::build::BuildConfig {
builder_id: value.builder_id,
skip_secret_interp: value.skip_secret_interp,
version: komodo_client::entities::Version {
major: value.version.major,
minor: value.version.minor,
patch: value.version.patch,
},
links: Default::default(),
auto_increment_version: true,
image_name: Default::default(),
image_tag: Default::default(),
git_provider: String::from("github.com"),
git_https: true,
repo: value.repo,
branch: value.branch,
commit: value.commit,
git_account: value.github_account,
pre_build: komodo_client::entities::SystemCommand {
path: value.pre_build.path,
command: value.pre_build.command,
},
build_path: value.build_path,
dockerfile_path: value.dockerfile_path,
build_args: value
.build_args
.into_iter()
.map(Into::into)
.collect(),
secret_args: Default::default(),
labels: value.labels.into_iter().map(Into::into).collect(),
extra_args: value.extra_args,
use_buildx: value.use_buildx,
webhook_enabled: value.webhook_enabled,
webhook_secret: Default::default(),
image_registry: value.image_registry.into(),
}
}
}
fn default_branch() -> String {
String::from("main")
}
fn default_build_path() -> String {
String::from(".")
}
fn default_dockerfile_path() -> String {
String::from("Dockerfile")
}
fn default_webhook_enabled() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "params")]
pub enum ImageRegistry {
/// Don't push the image to any registry
None(NoData),
/// Push the image to DockerHub
DockerHub(CloudRegistryConfig),
/// Push the image to the Github Container Registry.
///
/// See [the Github docs](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#pushing-container-images)
/// for information on creating an access token
Ghcr(CloudRegistryConfig),
/// Push the image to Aws Elastic Container Registry
///
/// The string held in 'params' should match a label of an `aws_ecr_registry` in the core config.
AwsEcr(String),
/// Todo. Will point to a custom "Registry" resource by id
Custom(String),
}
impl Default for ImageRegistry {
fn default() -> Self {
Self::None(NoData {})
}
}
impl From<ImageRegistry>
for komodo_client::entities::build::ImageRegistry
{
fn from(value: ImageRegistry) -> Self {
match value {
ImageRegistry::None(_) | ImageRegistry::Custom(_) => {
komodo_client::entities::build::ImageRegistry::None(NoData {})
}
ImageRegistry::DockerHub(params) => {
komodo_client::entities::build::ImageRegistry::Standard(
StandardRegistryConfig {
domain: String::from("docker.io"),
account: params.account,
organization: params.organization,
},
)
}
ImageRegistry::Ghcr(params) => {
komodo_client::entities::build::ImageRegistry::Standard(
StandardRegistryConfig {
domain: String::from("ghcr.io"),
account: params.account,
organization: params.organization,
},
)
}
ImageRegistry::AwsEcr(label) => {
komodo_client::entities::build::ImageRegistry::None(NoData {})
}
}
}
}
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
pub struct CloudRegistryConfig {
/// Specify an account to use with the cloud registry.
#[serde(default)]
pub account: String,
/// Optional. Specify an organization to push the image under.
/// Empty string means no organization.
#[serde(default)]
pub organization: String,
}

View File

@@ -1,168 +0,0 @@
use komodo_client::entities::{
deployment::{
conversions_deserializer, term_labels_deserializer, Conversion,
DeploymentImage, RestartMode, TerminationSignalLabel,
},
env_vars_deserializer, EnvironmentVar, TerminationSignal,
};
use serde::{Deserialize, Serialize};
use super::{build::ImageRegistry, resource::Resource};
pub type Deployment = Resource<DeploymentConfig, ()>;
impl From<Deployment>
for komodo_client::entities::deployment::Deployment
{
fn from(value: Deployment) -> Self {
komodo_client::entities::deployment::Deployment {
id: value.id,
name: value.name,
description: value.description,
updated_at: value.updated_at,
tags: value.tags,
info: (),
config: value.config.into(),
base_permission: Default::default(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DeploymentConfig {
/// The id of server the deployment is deployed on.
#[serde(default, alias = "server")]
pub server_id: String,
/// The image which the deployment deploys.
/// Can either be a user inputted image, or a Komodo build.
#[serde(default)]
pub image: DeploymentImage,
/// Configure the registry used to pull the image from the registry.
/// Used with `docker login`.
///
/// When using attached build as image source:
/// - If the field is `None` variant, will use the same ImageRegistry config as the build.
/// - Otherwise, it must match the variant of the ImageRegistry build config.
/// - Only the account is used, the organization is not needed here
#[serde(default)]
pub image_registry: ImageRegistry,
/// Whether to skip secret interpolation into the deployment environment variables.
#[serde(default)]
pub skip_secret_interp: bool,
/// Whether to redeploy the deployment whenever the attached build finishes.
#[serde(default)]
pub redeploy_on_build: bool,
/// Whether to send ContainerStateChange alerts for this deployment.
#[serde(default = "default_send_alerts")]
pub send_alerts: bool,
/// The network attached to the container.
/// Default is `host`.
#[serde(default = "default_network")]
pub network: String,
/// The restart mode given to the container.
#[serde(default)]
pub restart: RestartMode,
/// This is interpolated at the end of the `docker run` command,
/// which means they are either passed to the containers inner process,
/// or replaces the container command, depending on use of ENTRYPOINT or CMD in dockerfile.
/// Empty is no command.
#[serde(default)]
pub command: String,
/// The default termination signal to use to stop the deployment. Defaults to SigTerm (default docker signal).
#[serde(default)]
pub termination_signal: TerminationSignal,
/// The termination timeout.
#[serde(default = "default_termination_timeout")]
pub termination_timeout: i32,
/// Extra args which are interpolated into the `docker run` command,
/// and affect the container configuration.
#[serde(default)]
pub extra_args: Vec<String>,
/// Labels attached to various termination signal options.
/// Used to specify different shutdown functionality depending on the termination signal.
#[serde(
default = "default_term_signal_labels",
deserialize_with = "term_labels_deserializer"
)]
pub term_signal_labels: Vec<TerminationSignalLabel>,
/// The container port mapping.
/// Irrelevant if container network is `host`.
/// Maps ports on host to ports on container.
#[serde(default, deserialize_with = "conversions_deserializer")]
pub ports: Vec<Conversion>,
/// The container volume mapping.
/// Maps files / folders on host to files / folders in container.
#[serde(default, deserialize_with = "conversions_deserializer")]
pub volumes: Vec<Conversion>,
/// The environment variables passed to the container.
#[serde(default, deserialize_with = "env_vars_deserializer")]
pub environment: Vec<EnvironmentVar>,
/// The docker labels given to the container.
#[serde(default, deserialize_with = "env_vars_deserializer")]
pub labels: Vec<EnvironmentVar>,
}
fn default_send_alerts() -> bool {
true
}
fn default_term_signal_labels() -> Vec<TerminationSignalLabel> {
vec![TerminationSignalLabel::default()]
}
fn default_termination_timeout() -> i32 {
10
}
fn default_network() -> String {
String::from("host")
}
impl From<DeploymentConfig>
for komodo_client::entities::deployment::DeploymentConfig
{
fn from(value: DeploymentConfig) -> Self {
komodo_client::entities::deployment::DeploymentConfig {
server_id: value.server_id,
image: value.image,
image_registry_account: match value.image_registry {
ImageRegistry::None(_)
| ImageRegistry::AwsEcr(_)
| ImageRegistry::Custom(_) => String::new(),
ImageRegistry::DockerHub(params) => params.account,
ImageRegistry::Ghcr(params) => params.account,
},
skip_secret_interp: value.skip_secret_interp,
redeploy_on_build: value.redeploy_on_build,
send_alerts: value.send_alerts,
network: value.network,
restart: value.restart,
command: value.command,
termination_signal: value.termination_signal,
termination_timeout: value.termination_timeout,
extra_args: value.extra_args,
term_signal_labels: value.term_signal_labels,
ports: value.ports,
volumes: value.volumes,
environment: value.environment,
labels: value.labels,
links: Default::default(),
}
}
}

View File

@@ -1,48 +0,0 @@
// use mungos::{init::MongoBuilder, mongodb::Collection};
// use serde::{Deserialize, Serialize};
// pub mod build;
// pub mod deployment;
// pub mod resource;
// pub struct DbClient {
// pub builds: Collection<build::Build>,
// pub deployments: Collection<deployment::Deployment>,
// }
// impl DbClient {
// pub async fn new(
// legacy_uri: &str,
// legacy_db_name: &str,
// ) -> DbClient {
// let client = MongoBuilder::default()
// .uri(legacy_uri)
// .build()
// .await
// .expect("failed to init legacy mongo client");
// let db = client.database(legacy_db_name);
// DbClient {
// builds: db.collection("Build"),
// deployments: db.collection("Deployment"),
// }
// }
// }
// #[derive(
// Serialize, Deserialize, Debug, Clone, Default, PartialEq,
// )]
// pub struct Version {
// pub major: i32,
// pub minor: i32,
// pub patch: i32,
// }
// #[derive(
// Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq,
// )]
// pub struct SystemCommand {
// #[serde(default)]
// pub path: String,
// #[serde(default)]
// pub command: String,
// }

View File

@@ -1,54 +0,0 @@
use mungos::mongodb::bson::serde_helpers::hex_string_as_object_id;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resource<Config, Info: Default = ()> {
/// The Mongo ID of the resource.
/// This field is de/serialized from/to JSON as
/// `{ "_id": { "$oid": "..." }, ...(rest of serialized Resource<T>) }`
#[serde(
default,
rename = "_id",
skip_serializing_if = "String::is_empty",
with = "hex_string_as_object_id"
)]
pub id: String,
/// The resource name.
/// This is guaranteed unique among others of the same resource type.
pub name: String,
/// A description for the resource
#[serde(default)]
pub description: String,
/// When description last updated
#[serde(default)]
pub updated_at: i64,
/// Tag Ids
#[serde(default)]
pub tags: Vec<String>,
/// Resource-specific information (not user configurable).
#[serde(default)]
pub info: Info,
/// Resource-specific configuration.
pub config: Config,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ResourceListItem<Info> {
/// The resource id
pub id: String,
/// The resource type, ie `Server` or `Deployment`
// #[serde(rename = "type")]
// pub resource_type: ResourceTargetVariant,
/// The resource name
pub name: String,
/// Tag Ids
pub tags: Vec<String>,
/// Resource specific info
pub info: Info,
}

View File

@@ -1,46 +0,0 @@
#![allow(unused)]
#[macro_use]
extern crate tracing;
use serde::Deserialize;
mod legacy;
mod migrate;
#[derive(Deserialize)]
enum Migration {
#[serde(alias = "v1.11")]
V1_11,
}
#[derive(Deserialize)]
struct Env {
migration: Migration,
target_uri: String,
target_db_name: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenvy::dotenv().ok();
logger::init(&Default::default())?;
info!("starting migrator");
let env: Env = envy::from_env()?;
match env.migration {
Migration::V1_11 => {
// let db = legacy::v1_11::DbClient::new(
// &env.target_uri,
// &env.target_db_name,
// )
// .await;
// migrate::v1_11::migrate_all_in_place(&db).await?
}
}
info!("finished!");
Ok(())
}

View File

@@ -1 +0,0 @@
pub mod v1_11;

View File

@@ -1,70 +0,0 @@
// use anyhow::Context;
// use komodo_client::entities::{build::Build, deployment::Deployment};
// use mungos::{
// find::find_collect,
// mongodb::bson::{doc, to_document},
// };
// use crate::legacy::v1_11;
// pub async fn migrate_all_in_place(
// db: &v1_11::DbClient,
// ) -> anyhow::Result<()> {
// migrate_builds_in_place(db).await?;
// migrate_deployments_in_place(db).await?;
// Ok(())
// }
// pub async fn migrate_builds_in_place(
// db: &v1_11::DbClient,
// ) -> anyhow::Result<()> {
// let builds = find_collect(&db.builds, None, None)
// .await
// .context("failed to get builds")?
// .into_iter()
// .map(Into::into)
// .collect::<Vec<Build>>();
// info!("migrating {} builds...", builds.len());
// for build in builds {
// db.builds
// .update_one(
// doc! { "name": &build.name },
// doc! { "$set": to_document(&build)? },
// )
// .await
// .context("failed to insert builds on target")?;
// }
// info!("builds have been migrated\n");
// Ok(())
// }
// pub async fn migrate_deployments_in_place(
// db: &v1_11::DbClient,
// ) -> anyhow::Result<()> {
// let deployments = find_collect(&db.deployments, None, None)
// .await
// .context("failed to get deployments")?
// .into_iter()
// .map(Into::into)
// .collect::<Vec<Deployment>>();
// info!("migrating {} deployments...", deployments.len());
// for deployment in deployments {
// db.deployments
// .update_one(
// doc! { "name": &deployment.name },
// doc! { "$set": to_document(&deployment)? },
// )
// .await
// .context("failed to insert deployments on target")?;
// }
// info!("deployments have been migrated\n");
// Ok(())
// }

View File

@@ -4,7 +4,7 @@
## and may negatively affect runtime performance.
# Build Periphery
FROM rust:1.81.0-alpine AS builder
FROM rust:1.82.0-alpine AS builder
WORKDIR /builder
COPY . .
RUN apk update && apk --no-cache add musl-dev openssl-dev openssl-libs-static

View File

@@ -1,5 +1,5 @@
# Build Periphery
FROM rust:1.81.0-bullseye AS builder
FROM rust:1.82.0-bullseye AS builder
WORKDIR /builder
COPY . .
RUN cargo build -p komodo_periphery --release

View File

@@ -1,12 +1,15 @@
use anyhow::{anyhow, Context};
use command::run_komodo_command;
use formatting::format_serror;
use komodo_client::entities::{
build::{Build, BuildConfig},
environment_vars_from_str, get_image_name, optional_string,
to_komodo_name,
update::Log,
EnvironmentVar, Version,
use komodo_client::{
entities::{
build::{Build, BuildConfig},
environment_vars_from_str, get_image_name, optional_string,
to_komodo_name,
update::Log,
EnvironmentVar, Version,
},
parsers::QUOTE_PATTERN,
};
use periphery_client::api::build::{
self, PruneBuilders, PruneBuildx,
@@ -101,8 +104,9 @@ impl Resolve<build::Build> for State {
let secret_args = environment_vars_from_str(secret_args)
.context("Invalid secret_args")?;
let _secret_args =
let command_secret_args =
parse_secret_args(&secret_args, *skip_secret_interp)?;
let labels = parse_labels(
&environment_vars_from_str(labels).context("Invalid labels")?,
);
@@ -118,7 +122,7 @@ impl Resolve<build::Build> for State {
// Construct command
let command = format!(
"docker{buildx} build{build_args}{_secret_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{push_command}",
"docker{buildx} build{build_args}{command_secret_args}{extra_args}{labels}{image_tags} -f {dockerfile_path} .{push_command}",
);
if *skip_secret_interp {
@@ -126,6 +130,7 @@ impl Resolve<build::Build> for State {
"docker build",
build_dir.as_ref(),
command,
false,
)
.await;
logs.push(build_log);
@@ -146,6 +151,7 @@ impl Resolve<build::Build> for State {
"docker build",
build_dir.as_ref(),
command,
false,
)
.await;
build_log.command =
@@ -188,7 +194,16 @@ fn image_tags(
fn parse_build_args(build_args: &[EnvironmentVar]) -> String {
build_args
.iter()
.map(|p| format!(" --build-arg {}=\"{}\"", p.variable, p.value))
.map(|p| {
if p.value.starts_with(QUOTE_PATTERN)
&& p.value.ends_with(QUOTE_PATTERN)
{
// If the value already wrapped in quotes, don't wrap it again
format!(" --build-arg {}={}", p.variable, p.value)
} else {
format!(" --build-arg {}=\"{}\"", p.variable, p.value)
}
})
.collect::<Vec<_>>()
.join("")
}
@@ -244,7 +259,10 @@ impl Resolve<PruneBuilders> for State {
_: (),
) -> anyhow::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,
)
}
}
@@ -258,6 +276,6 @@ impl Resolve<PruneBuildx> for State {
_: (),
) -> anyhow::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)
}
}

View File

@@ -35,6 +35,7 @@ impl Resolve<ListComposeProjects, ()> for State {
"list projects",
None,
format!("{docker_compose} ls --all --format json"),
false,
)
.await;
@@ -104,7 +105,9 @@ impl Resolve<GetComposeServiceLog> for State {
let command = format!(
"{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,
)
}
}
@@ -131,30 +134,15 @@ impl Resolve<GetComposeServiceLogSearch> for State {
let timestamps =
timestamps.then_some(" --timestamps").unwrap_or_default();
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).await)
Ok(
run_komodo_command("get stack log grep", None, command, false)
.await,
)
}
}
//
const DEFAULT_COMPOSE_CONTENTS: &str = "## 🦎 Hello Komodo 🦎
services:
hello_world:
image: hello-world
# networks:
# - default
# ports:
# - 3000:3000
# volumes:
# - data:/data
# networks:
# default: {}
# volumes:
# data:
";
impl Resolve<GetComposeContentsOnHost, ()> for State {
#[instrument(
name = "GetComposeContentsOnHost",
@@ -186,11 +174,6 @@ impl Resolve<GetComposeContentsOnHost, ()> for State {
for path in file_paths {
let full_path =
run_directory.join(&path).components().collect::<PathBuf>();
if !full_path.exists() {
fs::write(&full_path, DEFAULT_COMPOSE_CONTENTS)
.await
.context("Failed to init missing compose file on host")?;
}
match fs::read_to_string(&full_path).await.with_context(|| {
format!(
"Failed to read compose file contents at {full_path:?}"
@@ -259,6 +242,7 @@ impl Resolve<WriteCommitComposeContents> for State {
&self,
WriteCommitComposeContents {
stack,
username,
file_path,
contents,
git_token,
@@ -326,18 +310,18 @@ impl Resolve<WriteCommitComposeContents> for State {
.context("Run directory is not a valid path")?
.join(&file_path);
let msg = if let Some(username) = username {
format!("{}: Write Compose File", username)
} else {
"Write Compose File".to_string()
};
let GitRes {
logs,
hash,
message,
..
} = write_commit_file(
"Write Compose File",
&root,
&file_path,
&contents,
)
.await?;
} = write_commit_file(&msg, &root, &file_path, &contents).await?;
Ok(RepoActionResponse {
logs,
@@ -400,6 +384,7 @@ impl Resolve<ComposeExecution> for State {
"compose command",
None,
format!("{docker_compose} -p {project} {command}"),
false,
)
.await;
Ok(log)

View File

@@ -53,7 +53,10 @@ impl Resolve<GetContainerLog> for State {
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,
)
}
}
@@ -83,8 +86,13 @@ impl Resolve<GetContainerLogSearch> for State {
"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,
)
}
}
@@ -142,6 +150,7 @@ impl Resolve<StartContainer> for State {
"docker start",
None,
format!("docker start {name}"),
false,
)
.await,
)
@@ -162,6 +171,7 @@ impl Resolve<RestartContainer> for State {
"docker restart",
None,
format!("docker restart {name}"),
false,
)
.await,
)
@@ -182,6 +192,7 @@ impl Resolve<PauseContainer> for State {
"docker pause",
None,
format!("docker pause {name}"),
false,
)
.await,
)
@@ -200,6 +211,7 @@ impl Resolve<UnpauseContainer> for State {
"docker unpause",
None,
format!("docker unpause {name}"),
false,
)
.await,
)
@@ -216,11 +228,12 @@ impl Resolve<StopContainer> for State {
_: (),
) -> anyhow::Result<Log> {
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() {
@@ -248,15 +261,19 @@ impl Resolve<RemoveContainer> for State {
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", 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() {
@@ -286,7 +303,9 @@ impl Resolve<RenameContainer> for State {
) -> anyhow::Result<Log> {
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,
)
}
}
@@ -300,7 +319,10 @@ impl Resolve<PruneContainers> for State {
_: (),
) -> anyhow::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,
)
}
}
@@ -324,7 +346,8 @@ impl Resolve<StartAllContainers> for State {
}
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
})
},
);
@@ -352,7 +375,8 @@ impl Resolve<RestartAllContainers> for State {
}
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
})
},
);
@@ -380,7 +404,8 @@ impl Resolve<PauseAllContainers> for State {
}
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
})
},
);
@@ -408,7 +433,8 @@ impl Resolve<UnpauseAllContainers> for State {
}
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
})
},
);
@@ -439,6 +465,7 @@ impl Resolve<StopAllContainers> for State {
&format!("docker stop {name}"),
None,
stop_container_command(name, None, None),
false,
)
.await
})

View File

@@ -1,14 +1,17 @@
use anyhow::Context;
use command::run_komodo_command;
use formatting::format_serror;
use komodo_client::entities::{
deployment::{
conversions_from_str, extract_registry_domain, Conversion,
Deployment, DeploymentConfig, DeploymentImage, RestartMode,
use komodo_client::{
entities::{
deployment::{
conversions_from_str, extract_registry_domain, Conversion,
Deployment, DeploymentConfig, DeploymentImage, RestartMode,
},
environment_vars_from_str, to_komodo_name,
update::Log,
EnvironmentVar,
},
environment_vars_from_str, to_komodo_name,
update::Log,
EnvironmentVar,
parsers::QUOTE_PATTERN,
};
use periphery_client::api::container::{Deploy, RemoveContainer};
use resolver_api::Resolve;
@@ -87,7 +90,7 @@ impl Resolve<Deploy> for State {
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 {
let command = svi::interpolate_variables(
&command,
@@ -108,7 +111,7 @@ impl Resolve<Deploy> for State {
replacers.extend(core_replacers);
let mut log =
run_komodo_command("docker run", None, command).await;
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);
@@ -175,7 +178,16 @@ fn parse_conversions(
fn parse_environment(environment: &[EnvironmentVar]) -> String {
environment
.iter()
.map(|p| format!(" --env {}=\"{}\"", p.variable, p.value))
.map(|p| {
if p.value.starts_with(QUOTE_PATTERN)
&& p.value.ends_with(QUOTE_PATTERN)
{
// If the value already wrapped in quotes, don't wrap it again
format!(" --env {}={}", p.variable, p.value)
} else {
format!(" --env {}=\"{}\"", p.variable, p.value)
}
})
.collect::<Vec<_>>()
.join("")
}

View File

@@ -1,13 +1,12 @@
use anyhow::{anyhow, Context};
use git::GitRes;
use komodo_client::entities::{
to_komodo_name, update::Log, CloneArgs, LatestCommit,
};
use komodo_client::entities::{update::Log, CloneArgs, LatestCommit};
use periphery_client::api::git::{
CloneRepo, DeleteRepo, GetLatestCommit, PullOrCloneRepo, PullRepo,
RepoActionResponse,
RenameRepo, RepoActionResponse,
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{config::periphery_config, State};
@@ -207,6 +206,31 @@ impl Resolve<PullOrCloneRepo> for State {
//
impl Resolve<RenameRepo> for State {
#[instrument(name = "RenameRepo", skip(self))]
async fn resolve(
&self,
RenameRepo {
curr_name,
new_name,
}: RenameRepo,
_: (),
) -> anyhow::Result<Log> {
let renamed = fs::rename(
periphery_config().repo_dir.join(&curr_name),
periphery_config().repo_dir.join(&new_name),
)
.await;
let msg = match renamed {
Ok(_) => format!("Renamed Repo directory on Server"),
Err(_) => format!("No Repo cloned at {curr_name} to rename"),
};
Ok(Log::simple("Rename Repo on Server", msg))
}
}
//
impl Resolve<DeleteRepo> for State {
#[instrument(name = "DeleteRepo", skip(self))]
async fn resolve(
@@ -214,14 +238,15 @@ impl Resolve<DeleteRepo> for State {
DeleteRepo { name }: DeleteRepo,
_: (),
) -> anyhow::Result<Log> {
let name = to_komodo_name(&name);
let deleted = std::fs::remove_dir_all(
periphery_config().repo_dir.join(&name),
);
// If using custom clone path, it will be passed by core instead of name.
// So the join will resolve to just the absolute path.
let deleted =
fs::remove_dir_all(periphery_config().repo_dir.join(&name))
.await;
let msg = match deleted {
Ok(_) => format!("deleted repo {name}"),
Err(_) => format!("no repo at {name} to delete"),
Ok(_) => format!("Deleted Repo {name}"),
Err(_) => format!("No Repo at {name} to delete"),
};
Ok(Log::simple("delete repo", msg))
Ok(Log::simple("Delete Repo on Host", msg))
}
}

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