* 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
This commit is contained in:
Maxwell Becker
2024-10-20 02:27:28 -04:00
committed by GitHub
parent 020cdc06fd
commit 5daba3a557
142 changed files with 17524 additions and 6737 deletions

474
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "1.15.12"
version = "1.16.0"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -64,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

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())
}
@@ -168,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
}

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 . .
@@ -23,7 +23,7 @@ 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
@@ -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,206 @@
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

@@ -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

@@ -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),

View File

@@ -6,7 +6,7 @@ use komodo_client::{
ListUserGroups,
},
entities::{
alerter::Alerter, build::Build, builder::Builder,
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,
@@ -124,6 +124,16 @@ impl Resolve<ExportAllResourcesToToml, User> for State {
.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(),
@@ -339,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,
};
}
@@ -442,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,15 +105,15 @@ 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 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)
@@ -165,7 +166,7 @@ impl Resolve<ListUpdates, User> for State {
build_query,
repo_query,
procedure_query,
// action_query,
action_query,
alerter_query,
builder_query,
server_template_query,
@@ -303,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

@@ -0,0 +1,59 @@
use komodo_client::{
api::write::*,
entities::{
action::Action, permission::PermissionLevel, 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<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

@@ -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;
@@ -125,6 +126,12 @@ pub enum WriteRequest {
DeleteProcedure(DeleteProcedure),
UpdateProcedure(UpdateProcedure),
// ==== ACTION ====
CreateAction(CreateAction),
CopyAction(CopyAction),
DeleteAction(DeleteAction),
UpdateAction(UpdateAction),
// ==== SYNC ====
CreateResourceSync(CreateResourceSync),
CopyResourceSync(CopyResourceSync),

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

@@ -5,28 +5,9 @@ use formatting::format_serror;
use komodo_client::{
api::{read::ExportAllResourcesToToml, write::*},
entities::{
self,
alert::{Alert, AlertData, SeverityLevel},
alerter::Alerter,
all_logs_success,
build::Build,
builder::Builder,
config::core::CoreConfig,
deployment::Deployment,
komodo_timestamp,
permission::PermissionLevel,
procedure::Procedure,
repo::Repo,
server::Server,
server_template::ServerTemplate,
stack::Stack,
sync::{
self, action::Action, alert::{Alert, AlertData, SeverityLevel}, alerter::Alerter, all_logs_success, build::Build, builder::Builder, config::core::CoreConfig, deployment::Deployment, komodo_timestamp, permission::PermissionLevel, procedure::Procedure, repo::Repo, server::Server, server_template::ServerTemplate, stack::Stack, sync::{
PartialResourceSyncConfig, ResourceSync, ResourceSyncInfo,
},
to_komodo_name,
update::{Log, Update},
user::{sync_user, User},
CloneArgs, NoData, Operation, ResourceTarget,
}, to_komodo_name, update::{Log, Update}, user::{sync_user, User}, CloneArgs, NoData, Operation, ResourceTarget
},
};
use mungos::{
@@ -535,6 +516,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,

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?;

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,
@@ -291,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

@@ -57,6 +57,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();

View File

@@ -0,0 +1,214 @@
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
}
async 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<()> {
refresh_action_state_cache().await;
Ok(())
}
// 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

@@ -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,
};
@@ -619,7 +623,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");
@@ -687,6 +691,7 @@ fn resource_target<T: KomodoResource>(id: String) -> ResourceTarget {
ResourceTarget::ResourceSync(id)
}
ResourceTargetVariant::Stack => ResourceTarget::Stack(id),
ResourceTargetVariant::Action => ResourceTarget::Action(id),
}
}
@@ -860,6 +865,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,
@@ -172,6 +173,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,
@@ -598,7 +608,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;
@@ -608,7 +618,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:#}")
});
}
@@ -655,7 +665,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

@@ -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

@@ -262,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,11 @@ 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

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

View File

@@ -275,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
@@ -716,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
@@ -875,6 +893,13 @@ pub async fn convert_user_groups(
.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

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

@@ -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

@@ -126,6 +126,7 @@ impl Resolve<build::Build> for State {
"docker build",
build_dir.as_ref(),
command,
false,
)
.await;
logs.push(build_log);
@@ -146,6 +147,7 @@ impl Resolve<build::Build> for State {
"docker build",
build_dir.as_ref(),
command,
false,
)
.await;
build_log.command =
@@ -244,7 +246,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 +263,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,7 +134,10 @@ 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,
)
}
}
@@ -378,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

@@ -87,7 +87,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 +108,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);

View File

@@ -44,7 +44,7 @@ impl Resolve<DeleteImage> for State {
_: (),
) -> anyhow::Result<Log> {
let command = format!("docker image rm {name}");
Ok(run_komodo_command("delete image", None, command).await)
Ok(run_komodo_command("delete image", None, command, false).await)
}
}
@@ -58,6 +58,6 @@ impl Resolve<PruneImages> for State {
_: (),
) -> anyhow::Result<Log> {
let command = String::from("docker image prune -a -f");
Ok(run_komodo_command("prune images", None, command).await)
Ok(run_komodo_command("prune images", None, command, false).await)
}
}

View File

@@ -256,7 +256,7 @@ impl Resolve<RunCommand> for State {
} else {
format!("cd {path} && {command}")
};
run_komodo_command("run command", None, command).await
run_komodo_command("run command", None, command, false).await
})
.await
.context("failure in spawned task")
@@ -271,6 +271,6 @@ impl Resolve<PruneSystem> for State {
_: (),
) -> anyhow::Result<Log> {
let command = String::from("docker system prune -a -f --volumes");
Ok(run_komodo_command("prune system", None, command).await)
Ok(run_komodo_command("prune system", None, command, false).await)
}
}

View File

@@ -34,7 +34,10 @@ impl Resolve<CreateNetwork> for State {
None => String::new(),
};
let command = format!("docker network create{driver} {name}");
Ok(run_komodo_command("create network", None, command).await)
Ok(
run_komodo_command("create network", None, command, false)
.await,
)
}
}
@@ -48,7 +51,10 @@ impl Resolve<DeleteNetwork> for State {
_: (),
) -> anyhow::Result<Log> {
let command = format!("docker network rm {name}");
Ok(run_komodo_command("delete network", None, command).await)
Ok(
run_komodo_command("delete network", None, command, false)
.await,
)
}
}
@@ -62,6 +68,9 @@ impl Resolve<PruneNetworks> for State {
_: (),
) -> anyhow::Result<Log> {
let command = String::from("docker network prune -f");
Ok(run_komodo_command("prune networks", None, command).await)
Ok(
run_komodo_command("prune networks", None, command, false)
.await,
)
}
}

View File

@@ -28,7 +28,9 @@ impl Resolve<DeleteVolume> for State {
_: (),
) -> anyhow::Result<Log> {
let command = format!("docker volume rm {name}");
Ok(run_komodo_command("delete volume", None, command).await)
Ok(
run_komodo_command("delete volume", None, command, false).await,
)
}
}
@@ -42,6 +44,8 @@ impl Resolve<PruneVolumes> for State {
_: (),
) -> anyhow::Result<Log> {
let command = String::from("docker volume prune -a -f");
Ok(run_komodo_command("prune volumes", None, command).await)
Ok(
run_komodo_command("prune volumes", None, command, false).await,
)
}
}

View File

@@ -141,19 +141,27 @@ pub async fn compose_up(
.map(|path| format!(" --env-file {path}"))
.unwrap_or_default();
let additional_env_files = stack
.config
.additional_env_files
.iter()
.map(|file| format!(" --env-file {file}"))
.collect::<String>();
// Build images before destroying to minimize downtime.
// If this fails, do not continue.
if stack.config.run_build {
let build_extra_args =
parse_extra_args(&stack.config.build_extra_args);
let command = format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file} build{build_extra_args}{service_arg}",
"{docker_compose} -p {project_name} -f {file_args}{env_file}{additional_env_files} build{build_extra_args}{service_arg}",
);
if stack.config.skip_secret_interp {
let log = run_komodo_command(
"compose build",
run_directory.as_ref(),
command,
false,
)
.await;
res.logs.push(log);
@@ -170,6 +178,7 @@ pub async fn compose_up(
"compose build",
run_directory.as_ref(),
command,
false,
)
.await;
@@ -197,6 +206,7 @@ pub async fn compose_up(
format!(
"{docker_compose} -p {project_name} -f {file_args}{env_file} pull{service_arg}",
),
false,
)
.await;
@@ -223,6 +233,7 @@ pub async fn compose_up(
"pre deploy",
pre_deploy_path.as_ref(),
&full_command,
true,
)
.await;
@@ -245,6 +256,7 @@ pub async fn compose_up(
"pre deploy",
pre_deploy_path.as_ref(),
&stack.config.pre_deploy.command,
true,
)
.await;
tracing::debug!(
@@ -279,8 +291,13 @@ pub async fn compose_up(
);
let log = if stack.config.skip_secret_interp {
run_komodo_command("compose up", run_directory.as_ref(), command)
.await
run_komodo_command(
"compose up",
run_directory.as_ref(),
command,
false,
)
.await
} else {
let (command, mut replacers) = svi::interpolate_variables(
&command,
@@ -294,6 +311,7 @@ pub async fn compose_up(
"compose up",
run_directory.as_ref(),
command,
false,
)
.await;
@@ -545,6 +563,7 @@ async fn compose_down(
"compose down",
None,
format!("{docker_compose} -p {project} down{service_arg}"),
false,
)
.await;
let success = log.success;

View File

@@ -937,7 +937,7 @@ pub async fn docker_login(
#[instrument]
pub async fn pull_image(image: &str) -> Log {
let command = format!("docker pull {image}");
run_komodo_command("docker pull", None, command).await
run_komodo_command("docker pull", None, command, false).await
}
pub fn stop_container_command(

View File

@@ -0,0 +1,28 @@
use clap::Parser;
use derive_empty_traits::EmptyTraits;
use resolver_api::derive::Request;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::update::Update;
use super::KomodoExecuteRequest;
/// Runs the target Action. Response: [Update]
#[typeshare]
#[derive(
Debug,
Clone,
PartialEq,
Serialize,
Deserialize,
Request,
EmptyTraits,
Parser,
)]
#[empty_traits(KomodoExecuteRequest)]
#[response(Update)]
pub struct RunAction {
/// Id or name
pub action: String,
}

View File

@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use typeshare::typeshare;
mod action;
mod build;
mod deployment;
mod procedure;
@@ -14,6 +15,7 @@ mod server_template;
mod stack;
mod sync;
pub use action::*;
pub use build::*;
pub use deployment::*;
pub use procedure::*;
@@ -55,6 +57,9 @@ pub enum Execution {
/// The "null" execution. Does nothing.
None(NoData),
// ACTION
RunAction(RunAction),
// PROCEDURE
RunProcedure(RunProcedure),

View File

@@ -8,7 +8,7 @@ use crate::entities::update::Update;
use super::KomodoExecuteRequest;
/// Runs the target procedure. Response: [Update]
/// Runs the target Procedure. Response: [Update]
#[typeshare]
#[derive(
Debug,

View File

@@ -38,10 +38,10 @@
//!
//! ```text
//! curl --header "Content-Type: application/json" \
//! --header "X-Api-Key: your_api_key" \
//! --header "X-Api-Secret: your_api_secret" \
//! --data '{ "type": "UpdateBuild", "params": { "id": "67076689ed600cfdd52ac637", "config": { "version": "1.15.9" } } }' \
//! https://komodo.example.com/write
//! --header "X-Api-Key: your_api_key" \
//! --header "X-Api-Secret: your_api_secret" \
//! --data '{ "type": "UpdateBuild", "params": { "id": "67076689ed600cfdd52ac637", "config": { "version": "1.15.9" } } }' \
//! https://komodo.example.com/write
//! ```
//!
//! ## Modules

View File

@@ -0,0 +1,110 @@
use derive_empty_traits::EmptyTraits;
use resolver_api::derive::Request;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::action::{
Action, ActionActionState, ActionListItem, ActionQuery,
};
use super::KomodoReadRequest;
//
/// Get a specific action. Response: [Action].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(GetActionResponse)]
pub struct GetAction {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub action: String,
}
#[typeshare]
pub type GetActionResponse = Action;
//
/// List actions matching optional query. Response: [ListActionsResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListActionsResponse)]
pub struct ListActions {
/// optional structured query to filter actions.
#[serde(default)]
pub query: ActionQuery,
}
#[typeshare]
pub type ListActionsResponse = Vec<ActionListItem>;
//
/// List actions matching optional query. Response: [ListFullActionsResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, Request, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(ListFullActionsResponse)]
pub struct ListFullActions {
/// optional structured query to filter actions.
#[serde(default)]
pub query: ActionQuery,
}
#[typeshare]
pub type ListFullActionsResponse = Vec<Action>;
//
/// Get current action state for the action. Response: [ActionActionState].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(GetActionActionStateResponse)]
pub struct GetActionActionState {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub action: String,
}
#[typeshare]
pub type GetActionActionStateResponse = ActionActionState;
//
/// Gets a summary of data relating to all actions.
/// Response: [GetActionsSummaryResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(GetActionsSummaryResponse)]
pub struct GetActionsSummary {}
/// Response for [GetActionsSummary].
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct GetActionsSummaryResponse {
/// The total number of actions.
pub total: u32,
/// The number of actions with Ok state.
pub ok: u32,
/// The number of actions currently running.
pub running: u32,
/// The number of actions with failed state.
pub failed: u32,
/// The number of actions with unknown state.
pub unknown: u32,
}

View File

@@ -3,6 +3,7 @@ use resolver_api::{derive::Request, HasResponse};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
mod action;
mod alert;
mod alerter;
mod build;
@@ -24,6 +25,7 @@ mod user;
mod user_group;
mod variable;
pub use action::*;
pub use alert::*;
pub use alerter::*;
pub use build::*;

View File

@@ -0,0 +1,118 @@
use derive_empty_traits::EmptyTraits;
use resolver_api::derive::Request;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{
action::{Action, _PartialActionConfig},
NoData,
};
use super::KomodoWriteRequest;
//
/// Create a action. Response: [Action].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Action)]
pub struct CreateAction {
/// The name given to newly created action.
pub name: String,
/// Optional partial config to initialize the action with.
pub config: _PartialActionConfig,
}
//
/// Creates a new action with given `name` and the configuration
/// of the action at the given `id`. Response: [Action].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Action)]
pub struct CopyAction {
/// The name of the new action.
pub name: String,
/// The id of the action to copy.
pub id: String,
}
//
/// Deletes the action at the given id, and returns the deleted action.
/// Response: [Action]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Action)]
pub struct DeleteAction {
/// The id or name of the action to delete.
pub id: String,
}
//
/// Update the action at the given id, and return the updated action.
/// Response: [Action].
///
/// Note. This method updates only the fields which are set in the [_PartialActionConfig],
/// effectively merging diffs into the final document.
/// This is helpful when multiple users are using
/// the same resources concurrently by ensuring no unintentional
/// field changes occur from out of date local state.
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Action)]
pub struct UpdateAction {
/// The id of the action to update.
pub id: String,
/// The partial config update to apply.
pub config: _PartialActionConfig,
}
/// Create a webhook on the github action attached to the Action resource.
/// passed in request. Response: [CreateActionWebhookResponse]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(CreateActionWebhookResponse)]
pub struct CreateActionWebhook {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub action: String,
}
#[typeshare]
pub type CreateActionWebhookResponse = NoData;
//
/// Delete the webhook on the github action attached to the Action resource.
/// passed in request. Response: [DeleteActionWebhookResponse]
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Request, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(DeleteActionWebhookResponse)]
pub struct DeleteActionWebhook {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub action: String,
}
#[typeshare]
pub type DeleteActionWebhookResponse = NoData;

View File

@@ -1,3 +1,4 @@
mod action;
mod alerter;
mod api_key;
mod build;
@@ -17,6 +18,7 @@ mod user;
mod user_group;
mod variable;
pub use action::*;
pub use alerter::*;
pub use api_key::*;
pub use build::*;

View File

@@ -121,7 +121,7 @@ pub struct CreateRepoWebhook {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub repo: String,
/// "Clone" or "Pull"
/// "Clone" or "Pull" or "Build"
pub action: RepoWebhookAction,
}
@@ -142,7 +142,7 @@ pub struct DeleteRepoWebhook {
/// Id or name
#[serde(alias = "id", alias = "name")]
pub repo: String,
/// "Clone" or "Pull"
/// "Clone" or "Pull" or "Build"
pub action: RepoWebhookAction,
}

View File

@@ -1,8 +1,8 @@
use crate::entities::{
build::BuildActionState, deployment::DeploymentActionState,
procedure::ProcedureActionState, repo::RepoActionState,
server::ServerActionState, stack::StackActionState,
sync::ResourceSyncActionState,
action::ActionActionState, build::BuildActionState,
deployment::DeploymentActionState, procedure::ProcedureActionState,
repo::RepoActionState, server::ServerActionState,
stack::StackActionState, sync::ResourceSyncActionState,
};
pub trait Busy {
@@ -66,6 +66,12 @@ impl Busy for ProcedureActionState {
}
}
impl Busy for ActionActionState {
fn busy(&self) -> bool {
self.running
}
}
impl Busy for ResourceSyncActionState {
fn busy(&self) -> bool {
self.syncing

View File

@@ -0,0 +1,108 @@
use serde::{
de::{value::SeqAccessDeserializer, Visitor},
Deserialize, Deserializer,
};
use crate::entities::deployment::Conversion;
pub fn conversions_deserializer<'de, D>(
deserializer: D,
) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ConversionVisitor)
}
pub fn option_conversions_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionConversionVisitor)
}
struct ConversionVisitor;
impl<'de> Visitor<'de> for ConversionVisitor {
type Value = String;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<Conversion>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let out = v.to_string();
if out.is_empty() || out.ends_with('\n') {
Ok(out)
} else {
Ok(out + "\n")
}
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let res = Vec::<Conversion>::deserialize(
SeqAccessDeserializer::new(seq),
)?;
let res = res
.iter()
.map(|Conversion { local, container }| {
format!(" {local}: {container}")
})
.collect::<Vec<_>>()
.join("\n");
let extra = if res.is_empty() { "" } else { "\n" };
Ok(res + extra)
}
}
struct OptionConversionVisitor;
impl<'de> Visitor<'de> for OptionConversionVisitor {
type Value = Option<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<Conversion>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
ConversionVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
ConversionVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}

View File

@@ -0,0 +1,108 @@
use serde::{
de::{value::SeqAccessDeserializer, Visitor},
Deserialize, Deserializer,
};
use crate::entities::EnvironmentVar;
pub fn env_vars_deserializer<'de, D>(
deserializer: D,
) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(EnvironmentVarVisitor)
}
pub fn option_env_vars_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionEnvVarVisitor)
}
struct EnvironmentVarVisitor;
impl<'de> Visitor<'de> for EnvironmentVarVisitor {
type Value = String;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<EnvironmentVar>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let out = v.to_string();
if out.is_empty() || out.ends_with('\n') {
Ok(out)
} else {
Ok(out + "\n")
}
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let vars = Vec::<EnvironmentVar>::deserialize(
SeqAccessDeserializer::new(seq),
)?;
let vars = vars
.iter()
.map(|EnvironmentVar { variable, value }| {
format!(" {variable} = {value}")
})
.collect::<Vec<_>>()
.join("\n");
let extra = if vars.is_empty() { "" } else { "\n" };
Ok(vars + extra)
}
}
struct OptionEnvVarVisitor;
impl<'de> Visitor<'de> for OptionEnvVarVisitor {
type Value = Option<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<EnvironmentVar>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
EnvironmentVarVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
EnvironmentVarVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}

View File

@@ -0,0 +1,80 @@
use serde::{de::Visitor, Deserializer};
/// Using this ensures the file contents end with trailing '\n'
pub fn file_contents_deserializer<'de, D>(
deserializer: D,
) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(FileContentsVisitor)
}
/// Using this ensures the file contents end with trailing '\n'
pub fn option_file_contents_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionFileContentsVisitor)
}
struct FileContentsVisitor;
impl<'de> Visitor<'de> for FileContentsVisitor {
type Value = String;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let out = v.trim_end().to_string();
if out.is_empty() {
Ok(out)
} else {
Ok(out + "\n")
}
}
}
struct OptionFileContentsVisitor;
impl<'de> Visitor<'de> for OptionFileContentsVisitor {
type Value = Option<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
FileContentsVisitor.visit_str(v).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}

View File

@@ -0,0 +1,108 @@
use serde::{
de::{value::SeqAccessDeserializer, Visitor},
Deserialize, Deserializer,
};
use crate::entities::EnvironmentVar;
pub fn labels_deserializer<'de, D>(
deserializer: D,
) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(LabelVisitor)
}
pub fn option_labels_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionLabelVisitor)
}
struct LabelVisitor;
impl<'de> Visitor<'de> for LabelVisitor {
type Value = String;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<EnvironmentVar>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let out = v.to_string();
if out.is_empty() || out.ends_with('\n') {
Ok(out)
} else {
Ok(out + "\n")
}
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let vars = Vec::<EnvironmentVar>::deserialize(
SeqAccessDeserializer::new(seq),
)?;
let vars = vars
.iter()
.map(|EnvironmentVar { variable, value }| {
format!(" {variable}: {value}")
})
.collect::<Vec<_>>()
.join("\n");
let extra = if vars.is_empty() { "" } else { "\n" };
Ok(vars + extra)
}
}
struct OptionLabelVisitor;
impl<'de> Visitor<'de> for OptionLabelVisitor {
type Value = Option<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<EnvironmentVar>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
LabelVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
LabelVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}

View File

@@ -0,0 +1,15 @@
//! Deserializers for custom behavior and backward compatibility.
mod conversion;
mod environment;
mod file_contents;
mod labels;
mod string_list;
mod term_signal_labels;
pub use conversion::*;
pub use environment::*;
pub use file_contents::*;
pub use labels::*;
pub use string_list::*;
pub use term_signal_labels::*;

View File

@@ -0,0 +1,92 @@
use serde::{
de::{value::SeqAccessDeserializer, SeqAccess, Visitor},
Deserialize, Deserializer,
};
use crate::parsers::parse_string_list;
pub fn string_list_deserializer<'de, D>(
deserializer: D,
) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(StringListVisitor)
}
pub fn option_string_list_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<Vec<String>>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionStringListVisitor)
}
struct StringListVisitor;
impl<'de> Visitor<'de> for StringListVisitor {
type Value = Vec<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<String>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(parse_string_list(v))
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
Vec::<String>::deserialize(SeqAccessDeserializer::new(seq))
}
}
struct OptionStringListVisitor;
impl<'de> Visitor<'de> for OptionStringListVisitor {
type Value = Option<Vec<String>>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<String>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
StringListVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
StringListVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}

View File

@@ -0,0 +1,107 @@
use serde::{
de::{value::SeqAccessDeserializer, Visitor},
Deserialize, Deserializer,
};
use crate::entities::deployment::TerminationSignalLabel;
pub fn term_labels_deserializer<'de, D>(
deserializer: D,
) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(TermSignalLabelVisitor)
}
pub fn option_term_labels_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionTermSignalLabelVisitor)
}
struct TermSignalLabelVisitor;
impl<'de> Visitor<'de> for TermSignalLabelVisitor {
type Value = String;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<TerminationSignalLabel>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let out = v.to_string();
if out.is_empty() || out.ends_with('\n') {
Ok(out)
} else {
Ok(out + "\n")
}
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let res = Vec::<TerminationSignalLabel>::deserialize(
SeqAccessDeserializer::new(seq),
)?
.into_iter()
.map(|TerminationSignalLabel { signal, label }| {
format!(" {signal}: {label}")
})
.collect::<Vec<_>>()
.join("\n");
let extra = if res.is_empty() { "" } else { "\n" };
Ok(res + extra)
}
}
struct OptionTermSignalLabelVisitor;
impl<'de> Visitor<'de> for OptionTermSignalLabelVisitor {
type Value = Option<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<TerminationSignalLabel>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
TermSignalLabelVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
TermSignalLabelVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}

View File

@@ -0,0 +1,107 @@
use bson::{doc, Document};
use derive_builder::Builder;
use derive_default_builder::DefaultBuilder;
use partial_derive2::Partial;
use serde::{Deserialize, Serialize};
use strum::Display;
use typeshare::typeshare;
use crate::{
deserializers::{
file_contents_deserializer, option_file_contents_deserializer,
},
entities::I64,
};
use super::resource::{Resource, ResourceListItem, ResourceQuery};
#[typeshare]
pub type ActionListItem = ResourceListItem<ActionListItemInfo>;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct ActionListItemInfo {
/// Action last run timestamp in ms.
pub last_run_at: I64,
/// Whether last action run successful
pub state: ActionState,
}
#[typeshare]
#[derive(
Debug, Clone, Copy, Default, Serialize, Deserialize, Display,
)]
pub enum ActionState {
/// Unknown case
#[default]
Unknown,
/// Last clone / pull successful (or never cloned)
Ok,
/// Last clone / pull failed
Failed,
/// Currently running
Running,
}
#[typeshare]
pub type Action = Resource<ActionConfig, ActionInfo>;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct ActionInfo {
/// When action was last run
#[serde(default)]
pub last_run_at: I64,
}
#[typeshare(serialized_as = "Partial<ActionConfig>")]
pub type _PartialActionConfig = PartialActionConfig;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]
#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]
#[partial(skip_serializing_none, from, diff)]
pub struct ActionConfig {
/// Typescript file contents using pre-initialized `komodo` client.
#[serde(default, deserialize_with = "file_contents_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_file_contents_deserializer"
))]
#[builder(default)]
pub file_contents: String,
}
impl ActionConfig {
pub fn builder() -> ActionConfigBuilder {
ActionConfigBuilder::default()
}
}
impl Default for ActionConfig {
fn default() -> Self {
Self {
file_contents: Default::default(),
}
}
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
pub struct ActionActionState {
/// Whether the action is currently running.
pub running: bool,
}
#[typeshare]
pub type ActionQuery = ResourceQuery<ActionQuerySpecifics>;
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Default, DefaultBuilder,
)]
pub struct ActionQuerySpecifics {}
impl super::resource::AddFilters for ActionQuerySpecifics {
fn add_filters(&self, _filters: &mut Document) {}
}

View File

@@ -9,7 +9,14 @@ use serde::{
use strum::Display;
use typeshare::typeshare;
use crate::entities::I64;
use crate::{
deserializers::{
env_vars_deserializer, labels_deserializer,
option_env_vars_deserializer, option_labels_deserializer,
option_string_list_deserializer, string_list_deserializer,
},
entities::I64,
};
use super::{
resource::{Resource, ResourceListItem, ResourceQuery},
@@ -126,7 +133,11 @@ pub struct BuildConfig {
pub image_tag: String,
/// Configure quick links that are displayed in the resource header
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub links: Vec<String>,
@@ -219,20 +230,21 @@ pub struct BuildConfig {
pub use_buildx: bool,
/// Any extra docker cli arguments to be included in the build command
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(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 = "super::env_vars_deserializer"
)]
#[serde(default, deserialize_with = "env_vars_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
deserialize_with = "option_env_vars_deserializer"
))]
#[builder(default)]
pub build_args: String,
@@ -247,22 +259,19 @@ pub struct BuildConfig {
/// RUN --mount=type=secret,id=SECRET_KEY \
/// SECRET_KEY=$(cat /run/secrets/SECRET_KEY) ...
/// ```
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[serde(default, deserialize_with = "env_vars_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
deserialize_with = "option_env_vars_deserializer"
))]
#[builder(default)]
pub secret_args: String,
/// Docker labels
#[serde(default, deserialize_with = "super::labels_deserializer")]
#[serde(default, deserialize_with = "labels_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "super::option_labels_deserializer"
deserialize_with = "option_labels_deserializer"
))]
#[builder(default)]
pub labels: String,

View File

@@ -5,6 +5,10 @@ use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use typeshare::typeshare;
use crate::deserializers::{
option_string_list_deserializer, string_list_deserializer,
};
use super::{
config::{DockerRegistry, GitProvider},
resource::{AddFilters, Resource, ResourceListItem, ResourceQuery},
@@ -330,7 +334,11 @@ pub struct AwsBuilderConfig {
pub use_public_ip: bool,
/// The security group ids to attach to the instance.
/// This should include a security group to allow core inbound access to the periphery port.
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub security_group_ids: Vec<String>,
/// The user data to deploy the instance with.
@@ -347,7 +355,11 @@ pub struct AwsBuilderConfig {
#[builder(default)]
pub docker_registries: Vec<DockerRegistry>,
/// Which secrets are available on the AMI.
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub secrets: Vec<String>,
}

View File

@@ -62,6 +62,8 @@ pub struct Env {
pub komodo_sync_directory: Option<PathBuf>,
/// Override `repo_directory`
pub komodo_repo_directory: Option<PathBuf>,
/// Override `action_directory`
pub komodo_action_directory: Option<PathBuf>,
/// Override `resource_poll_interval`
pub komodo_resource_poll_interval: Option<Timelength>,
/// Override `monitoring_interval`
@@ -491,6 +493,11 @@ pub struct CoreConfig {
/// Default: `/repo-cache`
#[serde(default = "default_repo_directory")]
pub repo_directory: PathBuf,
/// Specify the directory used to temporarily write typescript files used with actions.
/// Default: `/action-cache`
#[serde(default = "default_action_directory")]
pub action_directory: PathBuf,
}
fn default_title() -> String {
@@ -509,14 +516,19 @@ fn default_jwt_ttl() -> Timelength {
Timelength::OneDay
}
fn default_sync_directory() -> PathBuf {
// unwrap ok: `/syncs` will always be valid path
PathBuf::from_str("/syncs").unwrap()
}
fn default_repo_directory() -> PathBuf {
// unwrap ok: `/repo-cache` will always be valid path
PathBuf::from_str("/repo-cache").unwrap()
}
fn default_sync_directory() -> PathBuf {
// unwrap ok: `/syncs` will always be valid path
PathBuf::from_str("/syncs").unwrap()
fn default_action_directory() -> PathBuf {
// unwrap ok: `/action-cache` will always be valid path
PathBuf::from_str("/action-cache").unwrap()
}
fn default_prune_days() -> u64 {
@@ -552,6 +564,7 @@ impl CoreConfig {
jwt_secret: empty_or_redacted(&config.jwt_secret),
jwt_ttl: config.jwt_ttl,
repo_directory: config.repo_directory,
action_directory: config.action_directory,
sync_directory: config.sync_directory,
resource_poll_interval: config.resource_poll_interval,
monitoring_interval: config.monitoring_interval,

View File

@@ -4,14 +4,20 @@ use derive_builder::Builder;
use derive_default_builder::DefaultBuilder;
use derive_variants::EnumVariants;
use partial_derive2::Partial;
use serde::{
de::{value::SeqAccessDeserializer, Visitor},
Deserialize, Deserializer, Serialize,
};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use typeshare::typeshare;
use crate::parser::parse_key_value_list;
use crate::{
deserializers::{
conversions_deserializer, env_vars_deserializer,
labels_deserializer, option_conversions_deserializer,
option_env_vars_deserializer, option_labels_deserializer,
option_string_list_deserializer, option_term_labels_deserializer,
string_list_deserializer, term_labels_deserializer,
},
parsers::parse_key_value_list,
};
use super::{
docker::container::ContainerStateStatusEnum,
@@ -125,7 +131,11 @@ pub struct DeploymentConfig {
/// Extra args which are interpolated into the `docker run` command,
/// and affect the container configuration.
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub extra_args: Vec<String>,
@@ -161,25 +171,19 @@ pub struct DeploymentConfig {
pub volumes: String,
/// The environment variables passed to the container.
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[serde(default, deserialize_with = "env_vars_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
deserialize_with = "option_env_vars_deserializer"
))]
#[builder(default)]
pub environment: String,
/// The docker labels given to the container.
#[serde(
default,
deserialize_with = "super::labels_deserializer"
)]
#[serde(default, deserialize_with = "labels_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "super::option_labels_deserializer"
deserialize_with = "option_labels_deserializer"
))]
#[builder(default)]
pub labels: String,
@@ -294,108 +298,6 @@ pub fn conversions_from_str(
})
}
pub fn conversions_deserializer<'de, D>(
deserializer: D,
) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ConversionVisitor)
}
pub fn option_conversions_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionConversionVisitor)
}
struct ConversionVisitor;
impl<'de> Visitor<'de> for ConversionVisitor {
type Value = String;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<Conversion>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let out = v.to_string();
if out.is_empty() || out.ends_with('\n') {
Ok(out)
} else {
Ok(out + "\n")
}
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let res = Vec::<Conversion>::deserialize(
SeqAccessDeserializer::new(seq),
)?;
let res = res
.iter()
.map(|Conversion { local, container }| {
format!(" {local}: {container}")
})
.collect::<Vec<_>>()
.join("\n");
let extra = if res.is_empty() { "" } else { "\n" };
Ok(res + extra)
}
}
struct OptionConversionVisitor;
impl<'de> Visitor<'de> for OptionConversionVisitor {
type Value = Option<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<Conversion>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
ConversionVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
ConversionVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}
/// Variants de/serialized from/to snake_case.
///
/// Eg.
@@ -512,107 +414,6 @@ pub fn term_signal_labels_from_str(
})
}
pub fn term_labels_deserializer<'de, D>(
deserializer: D,
) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(TermSignalLabelVisitor)
}
pub fn option_term_labels_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionTermSignalLabelVisitor)
}
struct TermSignalLabelVisitor;
impl<'de> Visitor<'de> for TermSignalLabelVisitor {
type Value = String;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<TerminationSignalLabel>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let out = v.to_string();
if out.is_empty() || out.ends_with('\n') {
Ok(out)
} else {
Ok(out + "\n")
}
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let res = Vec::<TerminationSignalLabel>::deserialize(
SeqAccessDeserializer::new(seq),
)?
.into_iter()
.map(|TerminationSignalLabel { signal, label }| {
format!(" {signal}: {label}")
})
.collect::<Vec<_>>()
.join("\n");
let extra = if res.is_empty() { "" } else { "\n" };
Ok(res + extra)
}
}
struct OptionTermSignalLabelVisitor;
impl<'de> Visitor<'de> for OptionTermSignalLabelVisitor {
type Value = Option<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<TerminationSignalLabel>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
TermSignalLabelVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
TermSignalLabelVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}
#[typeshare]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct DeploymentActionState {

View File

@@ -10,18 +10,20 @@ use clap::Parser;
use derive_empty_traits::EmptyTraits;
use derive_variants::{EnumVariants, ExtractVariant};
use serde::{
de::{
value::{MapAccessDeserializer, SeqAccessDeserializer},
Visitor,
},
Deserialize, Deserializer, Serialize,
de::{value::MapAccessDeserializer, Visitor},
Deserialize, Serialize,
};
use serror::Serror;
use strum::{AsRefStr, Display, EnumString};
use typeshare::typeshare;
use crate::parser::parse_key_value_list;
use crate::{
deserializers::file_contents_deserializer,
parsers::parse_key_value_list,
};
/// Subtypes of [Action][action::Action].
pub mod action;
/// Subtypes of [Alert][alert::Alert].
pub mod alert;
/// Subtypes of [Alerter][alerter::Alerter].
@@ -365,210 +367,6 @@ pub fn environment_vars_from_str(
})
}
pub fn env_vars_deserializer<'de, D>(
deserializer: D,
) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(EnvironmentVarVisitor)
}
pub fn option_env_vars_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionEnvVarVisitor)
}
struct EnvironmentVarVisitor;
impl<'de> Visitor<'de> for EnvironmentVarVisitor {
type Value = String;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<EnvironmentVar>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let out = v.to_string();
if out.is_empty() || out.ends_with('\n') {
Ok(out)
} else {
Ok(out + "\n")
}
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let vars = Vec::<EnvironmentVar>::deserialize(
SeqAccessDeserializer::new(seq),
)?;
let vars = vars
.iter()
.map(|EnvironmentVar { variable, value }| {
format!(" {variable} = {value}")
})
.collect::<Vec<_>>()
.join("\n");
let extra = if vars.is_empty() { "" } else { "\n" };
Ok(vars + extra)
}
}
struct OptionEnvVarVisitor;
impl<'de> Visitor<'de> for OptionEnvVarVisitor {
type Value = Option<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<EnvironmentVar>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
EnvironmentVarVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
EnvironmentVarVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}
pub fn labels_deserializer<'de, D>(
deserializer: D,
) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(LabelVisitor)
}
pub fn option_labels_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionLabelVisitor)
}
struct LabelVisitor;
impl<'de> Visitor<'de> for LabelVisitor {
type Value = String;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string or Vec<EnvironmentVar>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let out = v.to_string();
if out.is_empty() || out.ends_with('\n') {
Ok(out)
} else {
Ok(out + "\n")
}
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let vars = Vec::<EnvironmentVar>::deserialize(
SeqAccessDeserializer::new(seq),
)?;
let vars = vars
.iter()
.map(|EnvironmentVar { variable, value }| {
format!(" {variable}: {value}")
})
.collect::<Vec<_>>()
.join("\n");
let extra = if vars.is_empty() { "" } else { "\n" };
Ok(vars + extra)
}
}
struct OptionLabelVisitor;
impl<'de> Visitor<'de> for OptionLabelVisitor {
type Value = Option<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string or Vec<EnvironmentVar>")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
LabelVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
LabelVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LatestCommit {
@@ -929,6 +727,12 @@ pub enum Operation {
DeleteProcedure,
RunProcedure,
// action
CreateAction,
UpdateAction,
DeleteAction,
RunAction,
// builder
CreateBuilder,
UpdateBuilder,
@@ -1053,6 +857,7 @@ pub enum ResourceTarget {
Build(String),
Repo(String),
Procedure(String),
Action(String),
Builder(String),
Alerter(String),
ServerTemplate(String),
@@ -1073,6 +878,7 @@ impl ResourceTarget {
ResourceTarget::Repo(id) => id,
ResourceTarget::Alerter(id) => id,
ResourceTarget::Procedure(id) => id,
ResourceTarget::Action(id) => id,
ResourceTarget::ServerTemplate(id) => id,
ResourceTarget::ResourceSync(id) => id,
};
@@ -1145,8 +951,14 @@ impl From<&sync::ResourceSync> for ResourceTarget {
}
impl From<&stack::Stack> for ResourceTarget {
fn from(resource_sync: &stack::Stack) -> Self {
Self::Stack(resource_sync.id.clone())
fn from(stack: &stack::Stack) -> Self {
Self::Stack(stack.id.clone())
}
}
impl From<&action::Action> for ResourceTarget {
fn from(action: &action::Action) -> Self {
Self::Action(action.id.clone())
}
}
@@ -1165,85 +977,7 @@ impl ResourceTargetVariant {
ResourceTargetVariant::ServerTemplate => "server_template",
ResourceTargetVariant::ResourceSync => "resource_sync",
ResourceTargetVariant::Stack => "stack",
ResourceTargetVariant::Action => "action",
}
}
}
/// Using this ensures the file contents end with trailing '\n'
pub fn file_contents_deserializer<'de, D>(
deserializer: D,
) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(FileContentsVisitor)
}
/// Using this ensures the file contents end with trailing '\n'
pub fn option_file_contents_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionFileContentsVisitor)
}
struct FileContentsVisitor;
impl<'de> Visitor<'de> for FileContentsVisitor {
type Value = String;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let out = v.trim_end().to_string();
if out.is_empty() {
Ok(out)
} else {
Ok(out + "\n")
}
}
}
struct OptionFileContentsVisitor;
impl<'de> Visitor<'de> for OptionFileContentsVisitor {
type Value = Option<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
FileContentsVisitor.visit_str(v).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}

View File

@@ -7,7 +7,13 @@ use serde::{Deserialize, Serialize};
use strum::Display;
use typeshare::typeshare;
use crate::entities::I64;
use crate::{
deserializers::{
env_vars_deserializer, option_env_vars_deserializer,
option_string_list_deserializer, string_list_deserializer,
},
entities::I64,
};
use super::{
environment_vars_from_str,
@@ -178,7 +184,11 @@ pub struct RepoConfig {
pub on_pull: SystemCommand,
/// Configure quick links that are displayed in the resource header
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub links: Vec<String>,
@@ -187,13 +197,10 @@ pub struct RepoConfig {
/// which is given relative to the run directory.
///
/// If it is empty, no file will be written.
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[serde(default, deserialize_with = "env_vars_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
deserialize_with = "option_env_vars_deserializer"
))]
#[builder(default)]
pub environment: String,

View File

@@ -4,7 +4,10 @@ use derive_default_builder::DefaultBuilder;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::entities::{MongoId, I64};
use crate::{
deserializers::string_list_deserializer,
entities::{MongoId, I64},
};
use super::{permission::PermissionLevel, ResourceTargetVariant};
@@ -38,7 +41,7 @@ pub struct Resource<Config: Default, Info: Default = ()> {
pub updated_at: I64,
/// Tag Ids
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[builder(default)]
pub tags: Vec<String>,
@@ -84,7 +87,7 @@ pub struct ResourceQuery<T: Default> {
#[serde(default)]
pub names: Vec<String>,
/// Pass Vec of tag ids or tag names
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
pub tags: Vec<String>,
#[serde(default)]
pub tag_behavior: TagBehavior,

View File

@@ -5,6 +5,10 @@ use partial_derive2::Partial;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::deserializers::{
option_string_list_deserializer, string_list_deserializer,
};
use super::{
alert::SeverityLevel,
resource::{AddFilters, Resource, ResourceListItem, ResourceQuery},
@@ -65,7 +69,11 @@ pub struct ServerConfig {
/// Sometimes the system stats reports a mount path that is not desired.
/// Use this field to filter it out from the report.
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub ignore_mounts: Vec<String>,
@@ -84,7 +92,11 @@ pub struct ServerConfig {
pub auto_prune: bool,
/// Configure quick links that are displayed in the resource header
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub links: Vec<String>,

View File

@@ -4,7 +4,12 @@ use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display};
use typeshare::typeshare;
use crate::entities::builder::AwsBuilderConfig;
use crate::{
deserializers::{
option_string_list_deserializer, string_list_deserializer,
},
entities::builder::AwsBuilderConfig,
};
#[typeshare(serialized_as = "Partial<AwsServerTemplateConfig>")]
pub type _PartialAwsServerTemplateConfig =
@@ -56,7 +61,11 @@ pub struct AwsServerTemplateConfig {
#[partial_default(default_use_https())]
pub use_https: bool,
/// The security groups to give to the instance.
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub security_group_ids: Vec<String>,
/// Specify the EBS volumes to attach.

View File

@@ -6,7 +6,12 @@ use serde::{Deserialize, Serialize};
use strum::AsRefStr;
use typeshare::typeshare;
use crate::entities::I64;
use crate::{
deserializers::{
option_string_list_deserializer, string_list_deserializer,
},
entities::I64,
};
#[typeshare(serialized_as = "Partial<HetznerServerTemplateConfig>")]
pub type _PartialHetznerServerTemplateConfig =
@@ -37,7 +42,11 @@ pub struct HetznerServerTemplateConfig {
#[builder(default)]
pub server_type: HetznerServerType,
/// SSH key IDs ( integer ) or names ( string ) which should be injected into the Server at creation time
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub ssh_keys: Vec<String>,
/// Network IDs which should be attached to the Server private network interface at the creation time

View File

@@ -8,6 +8,12 @@ use serde::{Deserialize, Serialize};
use strum::Display;
use typeshare::typeshare;
use crate::deserializers::{
env_vars_deserializer, file_contents_deserializer,
option_env_vars_deserializer, option_file_contents_deserializer,
option_string_list_deserializer, string_list_deserializer,
};
use super::{
docker::container::ContainerListItem,
resource::{Resource, ResourceListItem, ResourceQuery},
@@ -186,7 +192,11 @@ pub struct StackConfig {
pub server_id: String,
/// Configure quick links that are displayed in the resource header
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub links: Vec<String>,
@@ -238,18 +248,32 @@ pub struct StackConfig {
/// Add paths to compose files, relative to the run path.
/// If this is empty, will use file `compose.yaml`.
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub file_paths: Vec<String>,
/// The name of the written environment file before `docker compose up`.
/// Relative to the repo root.
/// Relative to the run directory root.
/// Default: .env
#[serde(default = "default_env_file_path")]
#[builder(default = "default_env_file_path()")]
#[partial_default(default_env_file_path())]
pub env_file_path: String,
/// Add additional env files to attach with `--env-file`.
/// Relative to the run directory root.
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub additional_env_files: Vec<String>,
/// The git provider domain. Default: github.com
#[serde(default = "default_git_provider")]
#[builder(default = "default_git_provider()")]
@@ -336,34 +360,43 @@ pub struct StackConfig {
/// The extra arguments to pass after `docker compose up -d`.
/// If empty, no extra arguments will be passed.
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub extra_args: Vec<String>,
/// The extra arguments to pass after `docker compose build`.
/// If empty, no extra build arguments will be passed.
/// Only used if `run_build: true`
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub build_extra_args: Vec<String>,
/// Ignore certain services declared in the compose file when checking
/// the stack status. For example, an init service might be exited, but the
/// stack should be healthy. This init service should be in `ignore_services`
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub ignore_services: Vec<String>,
/// The contents of the file directly, for management in the UI.
/// If this is empty, it will fall back to checking git config for
/// repo based compose file.
#[serde(
default,
deserialize_with = "super::file_contents_deserializer"
)]
#[serde(default, deserialize_with = "file_contents_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "super::option_file_contents_deserializer"
deserialize_with = "option_file_contents_deserializer"
))]
#[builder(default)]
pub file_contents: String,
@@ -373,13 +406,10 @@ pub struct StackConfig {
/// which is given relative to the run directory.
///
/// If it is empty, no file will be written.
#[serde(
default,
deserialize_with = "super::env_vars_deserializer"
)]
#[serde(default, deserialize_with = "env_vars_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "super::option_env_vars_deserializer"
deserialize_with = "option_env_vars_deserializer"
))]
#[builder(default)]
pub environment: String,
@@ -436,6 +466,7 @@ impl Default for StackConfig {
extra_args: Default::default(),
environment: Default::default(),
env_file_path: default_env_file_path(),
additional_env_files: Default::default(),
run_build: Default::default(),
destroy_before_deploy: Default::default(),
build_extra_args: Default::default(),

View File

@@ -2,13 +2,15 @@ use bson::{doc, Document};
use derive_builder::Builder;
use derive_default_builder::DefaultBuilder;
use partial_derive2::Partial;
use serde::{
de::{value::SeqAccessDeserializer, Visitor},
Deserialize, Deserializer, Serialize,
};
use serde::{Deserialize, Serialize};
use strum::Display;
use typeshare::typeshare;
use crate::deserializers::{
file_contents_deserializer, option_file_contents_deserializer,
option_string_list_deserializer, string_list_deserializer,
};
use super::{
resource::{Resource, ResourceListItem, ResourceQuery},
ResourceTarget, I64,
@@ -219,10 +221,10 @@ pub struct ResourceSyncConfig {
/// - If Git Repo based, this is relative to the root of the repo.
/// Can be a specific file, or a directory containing multiple files / folders.
/// See [https://komo.do/docs/sync-resources](https://komo.do/docs/sync-resources) for more information.
#[serde(default, deserialize_with = "resource_path_deserializer")]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_resource_path_deserializer"
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub resource_path: Vec<String>,
@@ -244,18 +246,19 @@ pub struct ResourceSyncConfig {
/// When using `managed` resource sync, will only export resources
/// matching all of the given tags. If none, will match all resources.
#[serde(default)]
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub match_tags: Vec<String>,
/// Manage the file contents in the UI.
#[serde(
default,
deserialize_with = "super::file_contents_deserializer"
)]
#[serde(default, deserialize_with = "file_contents_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "super::option_file_contents_deserializer"
deserialize_with = "option_file_contents_deserializer"
))]
#[builder(default)]
pub file_contents: String,
@@ -327,94 +330,6 @@ pub struct SyncFileContents {
pub contents: String,
}
/// Using this ensures the resource path deserialized to Vec<String>
pub fn resource_path_deserializer<'de, D>(
deserializer: D,
) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ResourcePathVisitor)
}
/// Using this ensures the resource path deserialized to Vec<String>
pub fn option_resource_path_deserializer<'de, D>(
deserializer: D,
) -> Result<Option<Vec<String>>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(OptionResourcePathVisitor)
}
struct ResourcePathVisitor;
impl<'de> Visitor<'de> for ResourcePathVisitor {
type Value = Vec<String>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(vec![v.to_string()])
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
Vec::deserialize(SeqAccessDeserializer::new(seq))
}
}
struct OptionResourcePathVisitor;
impl<'de> Visitor<'de> for OptionResourcePathVisitor {
type Value = Option<Vec<String>>;
fn expecting(
&self,
formatter: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(formatter, "null or string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
ResourcePathVisitor.visit_str(v).map(Some)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
ResourcePathVisitor.visit_seq(seq).map(Some)
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
pub struct ResourceSyncActionState {

View File

@@ -3,10 +3,11 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::{
alerter::PartialAlerterConfig, build::PartialBuildConfig,
builder::PartialBuilderConfig, deployment::PartialDeploymentConfig,
permission::PermissionLevel, procedure::PartialProcedureConfig,
repo::PartialRepoConfig, server::PartialServerConfig,
action::PartialActionConfig, alerter::PartialAlerterConfig,
build::PartialBuildConfig, builder::PartialBuilderConfig,
deployment::PartialDeploymentConfig, permission::PermissionLevel,
procedure::PartialProcedureConfig, repo::PartialRepoConfig,
server::PartialServerConfig,
server_template::PartialServerTemplateConfig,
stack::PartialStackConfig, sync::PartialResourceSyncConfig,
variable::Variable, ResourceTarget, ResourceTargetVariant,
@@ -57,6 +58,13 @@ pub struct ResourcesToml {
)]
pub procedures: Vec<ResourceToml<PartialProcedureConfig>>,
#[serde(
default,
rename = "action",
skip_serializing_if = "Vec::is_empty"
)]
pub actions: Vec<ResourceToml<PartialActionConfig>>,
#[serde(
default,
rename = "alerter",

View File

@@ -87,30 +87,56 @@ impl User {
matches!(
user_id,
"System"
| "000000000000000000000000"
| "Procedure"
| "Github" // Github can be removed later, just keeping for backward compat.
| "000000000000000000000001"
| "Action"
| "000000000000000000000002"
| "Git Webhook"
| "000000000000000000000003"
| "Auto Redeploy"
| "000000000000000000000004"
| "Resource Sync"
| "000000000000000000000005"
| "Stack Wizard"
| "000000000000000000000006"
| "Build Manager"
| "000000000000000000000007"
| "Repo Manager"
| "000000000000000000000008"
)
}
}
pub fn admin_service_user(user_id: &str) -> Option<User> {
match user_id {
"System" => system_user().to_owned().into(),
"Procedure" => procedure_user().to_owned().into(),
// Github should be removed later, replaced by Git Webhook, just keeping for backward compat.
"Github" => git_webhook_user().to_owned().into(),
"Git Webhook" => git_webhook_user().to_owned().into(),
"Auto Redeploy" => auto_redeploy_user().to_owned().into(),
"Resource Sync" => sync_user().to_owned().into(),
"Stack Wizard" => stack_user().to_owned().into(),
"Build Manager" => build_user().to_owned().into(),
"Repo Manager" => repo_user().to_owned().into(),
"000000000000000000000000" | "System" => {
system_user().to_owned().into()
}
"000000000000000000000001" | "Procedure" => {
procedure_user().to_owned().into()
}
"000000000000000000000002" | "Action" => {
action_user().to_owned().into()
}
"000000000000000000000003" | "Git Webhook" => {
git_webhook_user().to_owned().into()
}
"000000000000000000000004" | "Auto Redeploy" => {
auto_redeploy_user().to_owned().into()
}
"000000000000000000000005" | "Resource Sync" => {
sync_user().to_owned().into()
}
"000000000000000000000006" | "Stack Wizard" => {
stack_user().to_owned().into()
}
"000000000000000000000007" | "Build Manager" => {
build_user().to_owned().into()
}
"000000000000000000000008" | "Repo Manager" => {
repo_user().to_owned().into()
}
_ => None,
}
}
@@ -120,8 +146,9 @@ pub fn system_user() -> &'static User {
SYSTEM_USER.get_or_init(|| {
let id_name = String::from("System");
User {
id: id_name.clone(),
id: "000000000000000000000000".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
@@ -133,8 +160,23 @@ pub fn procedure_user() -> &'static User {
PROCEDURE_USER.get_or_init(|| {
let id_name = String::from("Procedure");
User {
id: id_name.clone(),
id: "000000000000000000000001".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
})
}
pub fn action_user() -> &'static User {
static ACTION_USER: OnceLock<User> = OnceLock::new();
ACTION_USER.get_or_init(|| {
let id_name = String::from("Action");
User {
id: "000000000000000000000002".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
@@ -146,8 +188,9 @@ pub fn git_webhook_user() -> &'static User {
GIT_WEBHOOK_USER.get_or_init(|| {
let id_name = String::from("Git Webhook");
User {
id: id_name.clone(),
id: "000000000000000000000003".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
@@ -159,8 +202,9 @@ pub fn auto_redeploy_user() -> &'static User {
AUTO_REDEPLOY_USER.get_or_init(|| {
let id_name = String::from("Auto Redeploy");
User {
id: id_name.clone(),
id: "000000000000000000000004".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
@@ -172,8 +216,9 @@ pub fn sync_user() -> &'static User {
SYNC_USER.get_or_init(|| {
let id_name = String::from("Resource Sync");
User {
id: id_name.clone(),
id: "000000000000000000000005".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
@@ -185,8 +230,9 @@ pub fn stack_user() -> &'static User {
STACK_USER.get_or_init(|| {
let id_name = String::from("Stack Wizard");
User {
id: id_name.clone(),
id: "000000000000000000000006".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
@@ -198,8 +244,9 @@ pub fn build_user() -> &'static User {
BUILD_USER.get_or_init(|| {
let id_name = String::from("Build Manager");
User {
id: id_name.clone(),
id: "000000000000000000000007".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}
@@ -211,8 +258,9 @@ pub fn repo_user() -> &'static User {
REPO_USER.get_or_init(|| {
let id_name = String::from("Repo Manager");
User {
id: id_name.clone(),
id: "000000000000000000000008".to_string(),
username: id_name,
enabled: true,
admin: true,
..Default::default()
}

View File

@@ -3,6 +3,8 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
use crate::deserializers::string_list_deserializer;
use super::{
permission::PermissionLevel, MongoId, ResourceTargetVariant, I64,
};
@@ -37,6 +39,7 @@ pub struct UserGroup {
/// User ids of group members
#[cfg_attr(feature = "mongo", index)]
#[serde(default, deserialize_with = "string_list_deserializer")]
pub users: Vec<String>,
/// Give the user group elevated permissions on all resources of a certain type

View File

@@ -35,8 +35,9 @@ use serde::Deserialize;
pub mod api;
pub mod busy;
pub mod entities;
pub mod parser;
pub mod parsers;
pub mod ws;
pub mod deserializers;
mod request;

View File

@@ -1,5 +1,25 @@
use anyhow::Context;
/// Parses a list of key value pairs from a multiline string
///
/// Example source:
/// ```text
/// # Supports comments
/// KEY_1 = value_1 # end of line comments
///
/// # Supports string wrapped values
/// KEY_2="value_2"
/// 'KEY_3 = value_3'
///
/// # Also supports yaml list formats
/// - KEY_4: 'value_4'
/// - "KEY_5=value_5"
/// ```
///
/// Returns:
/// ```text
/// [("KEY_1", "value_1"), ("KEY_2", "value_2"), ("KEY_3", "value_3"), ("KEY_4", "value_4"), ("KEY_5", "value_5")]
/// ```
pub fn parse_key_value_list(
input: &str,
) -> anyhow::Result<Vec<(String, String)>> {
@@ -103,3 +123,33 @@ pub fn parse_multiline_command(command: impl AsRef<str>) -> String {
.collect::<Vec<_>>()
.join(" && ")
}
/// Parses a list of strings from a comment seperated and multiline string
///
/// Example source:
/// ```text
/// # supports comments
/// path/to/file1 # comment1
/// path/to/file2
///
/// # also supports comma seperated values
/// path/to/file3,path/to/file4
/// ```
///
/// Returns:
/// ```text
/// ["path/to/file1", "path/to/file2", "path/to/file3", "path/to/file4"]
/// ```
pub fn parse_string_list(source: impl AsRef<str>) -> Vec<String> {
source
.as_ref()
.split('\n')
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| line.split(" #").next())
.flat_map(|line| line.split(','))
.map(str::trim)
.filter(|entry| !entry.is_empty())
.map(str::to_string)
.collect()
}

View File

@@ -24,14 +24,14 @@ const komodo = KomodoClient("https://demo.komo.do", {
});
const stacks: Types.StackListItem[] = await komodo.read({
type: "ListStacks",
params: {},
type: "ListStacks",
params: {},
});
const stack: Types.Stack = await komodo.read({
type: "GetStack",
params: {
stack: stacks[0].name,
}
type: "GetStack",
params: {
stack: stacks[0].name,
}
});
```

View File

@@ -1,6 +1,6 @@
{
"name": "komodo_client",
"version": "1.15.9",
"version": "1.15.15",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",
@@ -11,9 +11,7 @@
"scripts": {
"build": "tsc"
},
"dependencies": {
"axios": "^1.7.7"
},
"dependencies": {},
"devDependencies": {
"typescript": "^5.6.3"
},

View File

@@ -1,4 +1,3 @@
import axios from "axios";
import {
AuthResponses,
ExecuteResponses,
@@ -20,6 +19,7 @@ type InitOptions =
| { type: "jwt"; params: { jwt: string } }
| { type: "api-key"; params: { key: string; secret: string } };
/** Initialize a new client for Komodo */
export function KomodoClient(url: string, options: InitOptions) {
const state = {
jwt: options.type === "jwt" ? options.params.jwt : undefined,
@@ -27,31 +27,190 @@ export function KomodoClient(url: string, options: InitOptions) {
secret: options.type === "api-key" ? options.params.secret : undefined,
};
const request = async <Req, Res>(path: string, request: Req) =>
await axios
.post<Res>(url + path, request, {
headers: {
Authorization: state.jwt,
"X-API-KEY": state.key,
"X-API-SECRET": state.secret,
},
})
.then(({ data }) => data);
const request = async <Req, Res>(
path: "/auth" | "/user" | "/read" | "/execute" | "/write",
request: Req
): Promise<Res> =>
new Promise(async (res, rej) => {
try {
let response = await fetch(url + path, {
method: "POST",
body: JSON.stringify(request),
headers: {
...(state.jwt
? {
authorization: state.jwt,
}
: state.key && state.secret
? {
"x-api-key": state.key,
"x-api-secret": state.secret,
}
: {}),
"content-type": "application/json",
},
});
if (response.status === 200) {
const body: Res = await response.json();
res(body);
} else {
try {
const result = await response.json();
rej({ status: response.status, result });
} catch (error) {
rej({
status: response.status,
result: {
error: "Failed to get response body",
trace: [JSON.stringify(error)],
},
error,
});
}
}
} catch (error) {
rej({
status: 1,
result: {
error: "Request failed with error",
trace: [JSON.stringify(error)],
},
error,
});
}
});
const auth = async <Req extends AuthRequest>(req: Req) =>
await request<Req, AuthResponses[Req["type"]]>("/auth", req);
const auth = async <
T extends AuthRequest["type"],
Req extends Extract<AuthRequest, { type: T }>
>(
type: T,
params: Req["params"]
) =>
await request<
{ type: T; params: Req["params"] },
AuthResponses[Req["type"]]
>("/auth", {
type,
params,
});
const user = async <Req extends UserRequest>(req: Req) =>
await request<Req, UserResponses[Req["type"]]>("/user", req);
const user = async <
T extends UserRequest["type"],
Req extends Extract<UserRequest, { type: T }>
>(
type: T,
params: Req["params"]
) =>
await request<
{ type: T; params: Req["params"] },
UserResponses[Req["type"]]
>("/user", { type, params });
const read = async <Req extends ReadRequest>(req: Req) =>
await request<Req, ReadResponses[Req["type"]]>("/read", req);
const read = async <
T extends ReadRequest["type"],
Req extends Extract<ReadRequest, { type: T }>
>(
type: T,
params: Req["params"]
) =>
await request<
{ type: T; params: Req["params"] },
ReadResponses[Req["type"]]
>("/read", { type, params });
const write = async <Req extends WriteRequest>(req: Req) =>
await request<Req, WriteResponses[Req["type"]]>("/write", req);
const write = async <
T extends WriteRequest["type"],
Req extends Extract<WriteRequest, { type: T }>
>(
type: T,
params: Req["params"]
) =>
await request<
{ type: T; params: Req["params"] },
WriteResponses[Req["type"]]
>("/write", { type, params });
const execute = async <Req extends ExecuteRequest>(req: Req) =>
await request<Req, ExecuteResponses[Req["type"]]>("/execute", req);
const execute = async <
T extends ExecuteRequest["type"],
Req extends Extract<ExecuteRequest, { type: T }>
>(
type: T,
params: Req["params"]
) =>
await request<
{ type: T; params: Req["params"] },
ExecuteResponses[Req["type"]]
>("/execute", { type, params });
return { request, auth, user, read, write, execute };
const core_version = () => read("GetVersion", {}).then((res) => res.version);
return {
/**
* Call the `/auth` api.
*
* ```
* const login_options = await komodo.auth("GetLoginOptions", {});
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/auth/index.html
*/
auth,
/**
* Call the `/user` api.
*
* ```
* const { key, secret } = await komodo.user("CreateApiKey", {
* name: "my-api-key"
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/user/index.html
*/
user,
/**
* Call the `/read` api.
*
* ```
* const stack = await komodo.read("GetStack", {
* stack: "my-stack"
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/read/index.html
*/
read,
/**
* Call the `/write` api.
*
* ```
* const build = await komodo.write("UpdateBuild", {
* id: "my-build",
* config: {
* version: "1.0.4"
* }
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/write/index.html
*/
write,
/**
* Call the `/execute` api.
*
* ```
* const update = await komodo.execute("DeployStack", {
* stack: "my-stack"
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html
*/
execute,
/** Returns the version of Komodo Core the client is calling to. */
core_version,
};
}

View File

@@ -46,6 +46,13 @@ export type ReadResponses = {
ListProcedures: Types.ListProceduresResponse;
ListFullProcedures: Types.ListFullProceduresResponse;
// ==== ACTION ====
GetActionsSummary: Types.GetActionsSummaryResponse;
GetAction: Types.GetActionResponse;
GetActionActionState: Types.GetActionActionStateResponse;
ListActions: Types.ListActionsResponse;
ListFullActions: Types.ListFullActionsResponse;
// ==== SERVER TEMPLATE ====
GetServerTemplate: Types.GetServerTemplateResponse;
GetServerTemplatesSummary: Types.GetServerTemplatesSummaryResponse;
@@ -258,6 +265,12 @@ export type WriteResponses = {
DeleteProcedure: Types.Procedure;
UpdateProcedure: Types.Procedure;
// ==== ACTION ====
CreateAction: Types.Action;
CopyAction: Types.Action;
DeleteAction: Types.Action;
UpdateAction: Types.Action;
// ==== SYNC ====
CreateResourceSync: Types.ResourceSync;
CopyResourceSync: Types.ResourceSync;
@@ -347,6 +360,9 @@ export type ExecuteResponses = {
// ==== PROCEDURE ====
RunProcedure: Types.Update;
// ==== ACTION ====
RunAction: Types.Update;
// ==== SERVER TEMPLATE ====
LaunchServer: Types.Update;

File diff suppressed because it is too large Load Diff

View File

@@ -2,63 +2,6 @@
# yarn lockfile v1
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.7.7:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
typescript@^5.6.3:
version "5.6.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"

View File

@@ -71,6 +71,12 @@ sync_directory = "/syncs"
## Default: /repo-cache
repo_directory = "/repo-cache"
## Configure the action directory (inside the container).
## There shouldn't be a need to change this, or even mount a volume.
## Env: KOMODO_ACTION_DIRECTORY
## Default: /action-cache
action_directory = "/action-cache"
################
# AUTH / LOGIN #
################

View File

@@ -12,55 +12,56 @@
},
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-hover-card": "^1.1.2",
"@radix-ui/react-icons": "1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@tanstack/react-query": "5.51.23",
"@tanstack/react-table": "8.20.1",
"@tanstack/react-query": "5.59.15",
"@tanstack/react-table": "8.20.5",
"ansi-to-html": "0.7.2",
"class-variance-authority": "0.7.0",
"clsx": "2.1.1",
"cmdk": "1.0.0",
"jotai": "2.9.2",
"lucide-react": "0.437.0",
"jotai": "2.10.1",
"lucide-react": "0.453.0",
"monaco-editor": "^0.52.0",
"prettier": "3.3.3",
"react": "18.3.1",
"react-charts": "^3.0.0-beta.57",
"react-dom": "18.3.1",
"react-minimal-pie-chart": "8.4.0",
"react-router-dom": "6.26.0",
"sanitize-html": "2.13.0",
"tailwind-merge": "2.4.0",
"react-router-dom": "6.27.0",
"sanitize-html": "2.13.1",
"tailwind-merge": "2.5.4",
"tailwindcss-animate": "1.0.7"
},
"devDependencies": {
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/sanitize-html": "2.11.0",
"@typescript-eslint/eslint-plugin": "8.0.1",
"@types/react": "18.3.11",
"@types/react-dom": "18.3.1",
"@types/sanitize-html": "2.13.0",
"@typescript-eslint/eslint-plugin": "8.10.0",
"@typescript-eslint/parser": "8.0.1",
"@vitejs/plugin-react": "4.3.1",
"@vitejs/plugin-react": "4.3.3",
"autoprefixer": "10.4.20",
"eslint": "9.9.0",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-react-refresh": "0.4.9",
"postcss": "8.4.41",
"tailwindcss": "3.4.9",
"typescript": "5.5.4",
"vite": "5.4.0",
"eslint": "9.13.0",
"eslint-plugin-react-hooks": "5.0.0",
"eslint-plugin-react-refresh": "0.4.13",
"postcss": "8.4.47",
"tailwindcss": "3.4.14",
"typescript": "5.6.3",
"vite": "5.4.9",
"vite-tsconfig-paths": "5.0.1"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"

91
frontend/public/client/lib.d.ts vendored Normal file
View File

@@ -0,0 +1,91 @@
import { AuthResponses, ExecuteResponses, ReadResponses, UserResponses, WriteResponses } from "./responses.js";
import { AuthRequest, ExecuteRequest, ReadRequest, UserRequest, WriteRequest } from "./types.js";
export * as Types from "./types.js";
type InitOptions = {
type: "jwt";
params: {
jwt: string;
};
} | {
type: "api-key";
params: {
key: string;
secret: string;
};
};
/** Initialize a new client for Komodo */
export declare function KomodoClient(url: string, options: InitOptions): {
/**
* Call the `/auth` api.
*
* ```
* const login_options = await komodo.auth("GetLoginOptions", {});
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/auth/index.html
*/
auth: <T extends AuthRequest["type"], Req extends Extract<AuthRequest, {
type: T;
}>>(type: T, params: Req["params"]) => Promise<AuthResponses[Req["type"]]>;
/**
* Call the `/user` api.
*
* ```
* const { key, secret } = await komodo.user("CreateApiKey", {
* name: "my-api-key"
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/user/index.html
*/
user: <T extends UserRequest["type"], Req extends Extract<UserRequest, {
type: T;
}>>(type: T, params: Req["params"]) => Promise<UserResponses[Req["type"]]>;
/**
* Call the `/read` api.
*
* ```
* const stack = await komodo.read("GetStack", {
* stack: "my-stack"
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/read/index.html
*/
read: <T extends ReadRequest["type"], Req extends Extract<ReadRequest, {
type: T;
}>>(type: T, params: Req["params"]) => Promise<ReadResponses[Req["type"]]>;
/**
* Call the `/write` api.
*
* ```
* const build = await komodo.write("UpdateBuild", {
* id: "my-build",
* config: {
* version: "1.0.4"
* }
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/write/index.html
*/
write: <T extends WriteRequest["type"], Req extends Extract<WriteRequest, {
type: T;
}>>(type: T, params: Req["params"]) => Promise<WriteResponses[Req["type"]]>;
/**
* Call the `/execute` api.
*
* ```
* const update = await komodo.execute("DeployStack", {
* stack: "my-stack"
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html
*/
execute: <T extends ExecuteRequest["type"], Req extends Extract<ExecuteRequest, {
type: T;
}>>(type: T, params: Req["params"]) => Promise<ExecuteResponses[Req["type"]]>;
/** Returns the version of Komodo Core the client is calling to. */
core_version: () => Promise<string>;
};

View File

@@ -0,0 +1,134 @@
export * as Types from "./types.js";
/** Initialize a new client for Komodo */
export function KomodoClient(url, options) {
const state = {
jwt: options.type === "jwt" ? options.params.jwt : undefined,
key: options.type === "api-key" ? options.params.key : undefined,
secret: options.type === "api-key" ? options.params.secret : undefined,
};
const request = async (path, request) => new Promise(async (res, rej) => {
try {
let response = await fetch(url + path, {
method: "POST",
body: JSON.stringify(request),
headers: {
...(state.jwt
? {
authorization: state.jwt,
}
: state.key && state.secret
? {
"x-api-key": state.key,
"x-api-secret": state.secret,
}
: {}),
"content-type": "application/json",
},
});
if (response.status === 200) {
const body = await response.json();
res(body);
}
else {
try {
const result = await response.json();
rej({ status: response.status, result });
}
catch (error) {
rej({
status: response.status,
result: {
error: "Failed to get response body",
trace: [JSON.stringify(error)],
},
error,
});
}
}
}
catch (error) {
rej({
status: 1,
result: {
error: "Request failed with error",
trace: [JSON.stringify(error)],
},
error,
});
}
});
const auth = async (type, params) => await request("/auth", {
type,
params,
});
const user = async (type, params) => await request("/user", { type, params });
const read = async (type, params) => await request("/read", { type, params });
const write = async (type, params) => await request("/write", { type, params });
const execute = async (type, params) => await request("/execute", { type, params });
const core_version = () => read("GetVersion", {}).then((res) => res.version);
return {
/**
* Call the `/auth` api.
*
* ```
* const login_options = await komodo.auth("GetLoginOptions", {});
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/auth/index.html
*/
auth,
/**
* Call the `/user` api.
*
* ```
* const { key, secret } = await komodo.user("CreateApiKey", {
* name: "my-api-key"
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/user/index.html
*/
user,
/**
* Call the `/read` api.
*
* ```
* const stack = await komodo.read("GetStack", {
* stack: "my-stack"
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/read/index.html
*/
read,
/**
* Call the `/write` api.
*
* ```
* const build = await komodo.write("UpdateBuild", {
* id: "my-build",
* config: {
* version: "1.0.4"
* }
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/write/index.html
*/
write,
/**
* Call the `/execute` api.
*
* ```
* const update = await komodo.execute("DeployStack", {
* stack: "my-stack"
* });
* ```
*
* https://docs.rs/komodo_client/latest/komodo_client/api/execute/index.html
*/
execute,
/** Returns the version of Komodo Core the client is calling to. */
core_version,
};
}

288
frontend/public/client/responses.d.ts vendored Normal file
View File

@@ -0,0 +1,288 @@
import * as Types from "./types.js";
export type AuthResponses = {
GetLoginOptions: Types.GetLoginOptionsResponse;
CreateLocalUser: Types.CreateLocalUserResponse;
LoginLocalUser: Types.LoginLocalUserResponse;
ExchangeForJwt: Types.ExchangeForJwtResponse;
GetUser: Types.GetUserResponse;
};
export type UserResponses = {
PushRecentlyViewed: Types.PushRecentlyViewedResponse;
SetLastSeenUpdate: Types.SetLastSeenUpdateResponse;
CreateApiKey: Types.CreateApiKeyResponse;
DeleteApiKey: Types.DeleteApiKeyResponse;
};
export type ReadResponses = {
GetVersion: Types.GetVersionResponse;
GetCoreInfo: Types.GetCoreInfoResponse;
ListSecrets: Types.ListSecretsResponse;
ListGitProvidersFromConfig: Types.ListGitProvidersFromConfigResponse;
ListDockerRegistriesFromConfig: Types.ListDockerRegistriesFromConfigResponse;
GetUsername: Types.GetUsernameResponse;
GetPermissionLevel: Types.GetPermissionLevelResponse;
FindUser: Types.FindUserResponse;
ListUsers: Types.ListUsersResponse;
ListApiKeys: Types.ListApiKeysResponse;
ListApiKeysForServiceUser: Types.ListApiKeysForServiceUserResponse;
ListPermissions: Types.ListPermissionsResponse;
ListUserTargetPermissions: Types.ListUserTargetPermissionsResponse;
GetUserGroup: Types.GetUserGroupResponse;
ListUserGroups: Types.ListUserGroupsResponse;
FindResources: Types.FindResourcesResponse;
GetProceduresSummary: Types.GetProceduresSummaryResponse;
GetProcedure: Types.GetProcedureResponse;
GetProcedureActionState: Types.GetProcedureActionStateResponse;
ListProcedures: Types.ListProceduresResponse;
ListFullProcedures: Types.ListFullProceduresResponse;
GetActionsSummary: Types.GetActionsSummaryResponse;
GetAction: Types.GetActionResponse;
GetActionActionState: Types.GetActionActionStateResponse;
ListActions: Types.ListActionsResponse;
ListFullActions: Types.ListFullActionsResponse;
GetServerTemplate: Types.GetServerTemplateResponse;
GetServerTemplatesSummary: Types.GetServerTemplatesSummaryResponse;
ListServerTemplates: Types.ListServerTemplatesResponse;
ListFullServerTemplates: Types.ListFullServerTemplatesResponse;
GetServersSummary: Types.GetServersSummaryResponse;
GetServer: Types.GetServerResponse;
GetServerState: Types.GetServerStateResponse;
GetPeripheryVersion: Types.GetPeripheryVersionResponse;
ListDockerContainers: Types.ListDockerContainersResponse;
ListAllDockerContainers: Types.ListAllDockerContainersResponse;
InspectDockerContainer: Types.InspectDockerContainerResponse;
GetResourceMatchingContainer: Types.GetResourceMatchingContainerResponse;
GetContainerLog: Types.GetContainerLogResponse;
SearchContainerLog: Types.SearchContainerLogResponse;
ListDockerNetworks: Types.ListDockerNetworksResponse;
InspectDockerNetwork: Types.InspectDockerNetworkResponse;
ListDockerImages: Types.ListDockerImagesResponse;
InspectDockerImage: Types.InspectDockerImageResponse;
ListDockerImageHistory: Types.ListDockerImageHistoryResponse;
ListDockerVolumes: Types.ListDockerVolumesResponse;
InspectDockerVolume: Types.InspectDockerVolumeResponse;
ListComposeProjects: Types.ListComposeProjectsResponse;
GetServerActionState: Types.GetServerActionStateResponse;
GetHistoricalServerStats: Types.GetHistoricalServerStatsResponse;
ListServers: Types.ListServersResponse;
ListFullServers: Types.ListFullServersResponse;
GetDeploymentsSummary: Types.GetDeploymentsSummaryResponse;
GetDeployment: Types.GetDeploymentResponse;
GetDeploymentContainer: Types.GetDeploymentContainerResponse;
GetDeploymentActionState: Types.GetDeploymentActionStateResponse;
GetDeploymentStats: Types.GetDeploymentStatsResponse;
GetDeploymentLog: Types.GetDeploymentLogResponse;
SearchDeploymentLog: Types.SearchDeploymentLogResponse;
ListDeployments: Types.ListDeploymentsResponse;
ListFullDeployments: Types.ListFullDeploymentsResponse;
ListCommonDeploymentExtraArgs: Types.ListCommonDeploymentExtraArgsResponse;
GetBuildsSummary: Types.GetBuildsSummaryResponse;
GetBuild: Types.GetBuildResponse;
GetBuildActionState: Types.GetBuildActionStateResponse;
GetBuildMonthlyStats: Types.GetBuildMonthlyStatsResponse;
GetBuildWebhookEnabled: Types.GetBuildWebhookEnabledResponse;
ListBuilds: Types.ListBuildsResponse;
ListFullBuilds: Types.ListFullBuildsResponse;
ListBuildVersions: Types.ListBuildVersionsResponse;
ListCommonBuildExtraArgs: Types.ListCommonBuildExtraArgsResponse;
GetReposSummary: Types.GetReposSummaryResponse;
GetRepo: Types.GetRepoResponse;
GetRepoActionState: Types.GetRepoActionStateResponse;
GetRepoWebhooksEnabled: Types.GetRepoWebhooksEnabledResponse;
ListRepos: Types.ListReposResponse;
ListFullRepos: Types.ListFullReposResponse;
GetResourceSyncsSummary: Types.GetResourceSyncsSummaryResponse;
GetResourceSync: Types.GetResourceSyncResponse;
GetResourceSyncActionState: Types.GetResourceSyncActionStateResponse;
GetSyncWebhooksEnabled: Types.GetSyncWebhooksEnabledResponse;
ListResourceSyncs: Types.ListResourceSyncsResponse;
ListFullResourceSyncs: Types.ListFullResourceSyncsResponse;
GetStacksSummary: Types.GetStacksSummaryResponse;
GetStack: Types.GetStackResponse;
GetStackActionState: Types.GetStackActionStateResponse;
GetStackWebhooksEnabled: Types.GetStackWebhooksEnabledResponse;
GetStackServiceLog: Types.GetStackServiceLogResponse;
SearchStackServiceLog: Types.SearchStackServiceLogResponse;
ListStacks: Types.ListStacksResponse;
ListFullStacks: Types.ListFullStacksResponse;
ListStackServices: Types.ListStackServicesResponse;
ListCommonStackExtraArgs: Types.ListCommonStackExtraArgsResponse;
ListCommonStackBuildExtraArgs: Types.ListCommonStackBuildExtraArgsResponse;
GetBuildersSummary: Types.GetBuildersSummaryResponse;
GetBuilder: Types.GetBuilderResponse;
ListBuilders: Types.ListBuildersResponse;
ListFullBuilders: Types.ListFullBuildersResponse;
GetAlertersSummary: Types.GetAlertersSummaryResponse;
GetAlerter: Types.GetAlerterResponse;
ListAlerters: Types.ListAlertersResponse;
ListFullAlerters: Types.ListFullAlertersResponse;
ExportAllResourcesToToml: Types.ExportAllResourcesToTomlResponse;
ExportResourcesToToml: Types.ExportResourcesToTomlResponse;
GetTag: Types.GetTagResponse;
ListTags: Types.ListTagsResponse;
GetUpdate: Types.GetUpdateResponse;
ListUpdates: Types.ListUpdatesResponse;
ListAlerts: Types.ListAlertsResponse;
GetAlert: Types.GetAlertResponse;
GetSystemInformation: Types.GetSystemInformationResponse;
GetSystemStats: Types.GetSystemStatsResponse;
ListSystemProcesses: Types.ListSystemProcessesResponse;
GetVariable: Types.GetVariableResponse;
ListVariables: Types.ListVariablesResponse;
GetGitProviderAccount: Types.GetGitProviderAccountResponse;
ListGitProviderAccounts: Types.ListGitProviderAccountsResponse;
GetDockerRegistryAccount: Types.GetDockerRegistryAccountResponse;
ListDockerRegistryAccounts: Types.ListDockerRegistryAccountsResponse;
};
export type WriteResponses = {
UpdateUserUsername: Types.UpdateUserUsername;
UpdateUserPassword: Types.UpdateUserPassword;
DeleteUser: Types.DeleteUser;
CreateServiceUser: Types.CreateServiceUserResponse;
UpdateServiceUserDescription: Types.UpdateServiceUserDescriptionResponse;
CreateApiKeyForServiceUser: Types.CreateApiKeyForServiceUserResponse;
DeleteApiKeyForServiceUser: Types.DeleteApiKeyForServiceUserResponse;
CreateUserGroup: Types.UserGroup;
RenameUserGroup: Types.UserGroup;
DeleteUserGroup: Types.UserGroup;
AddUserToUserGroup: Types.UserGroup;
RemoveUserFromUserGroup: Types.UserGroup;
SetUsersInUserGroup: Types.UserGroup;
UpdateUserAdmin: Types.UpdateUserAdminResponse;
UpdateUserBasePermissions: Types.UpdateUserBasePermissionsResponse;
UpdatePermissionOnResourceType: Types.UpdatePermissionOnResourceTypeResponse;
UpdatePermissionOnTarget: Types.UpdatePermissionOnTargetResponse;
UpdateDescription: Types.UpdateDescriptionResponse;
LaunchServer: Types.Update;
CreateServer: Types.Server;
DeleteServer: Types.Server;
UpdateServer: Types.Server;
RenameServer: Types.Update;
CreateNetwork: Types.Update;
CreateDeployment: Types.Deployment;
CopyDeployment: Types.Deployment;
DeleteDeployment: Types.Deployment;
UpdateDeployment: Types.Deployment;
RenameDeployment: Types.Update;
CreateBuild: Types.Build;
CopyBuild: Types.Build;
DeleteBuild: Types.Build;
UpdateBuild: Types.Build;
RefreshBuildCache: Types.NoData;
CreateBuildWebhook: Types.CreateBuildWebhookResponse;
DeleteBuildWebhook: Types.DeleteBuildWebhookResponse;
CreateBuilder: Types.Builder;
CopyBuilder: Types.Builder;
DeleteBuilder: Types.Builder;
UpdateBuilder: Types.Builder;
CreateServerTemplate: Types.ServerTemplate;
CopyServerTemplate: Types.ServerTemplate;
DeleteServerTemplate: Types.ServerTemplate;
UpdateServerTemplate: Types.ServerTemplate;
CreateRepo: Types.Repo;
CopyRepo: Types.Repo;
DeleteRepo: Types.Repo;
UpdateRepo: Types.Repo;
RefreshRepoCache: Types.NoData;
CreateRepoWebhook: Types.CreateRepoWebhookResponse;
DeleteRepoWebhook: Types.DeleteRepoWebhookResponse;
CreateAlerter: Types.Alerter;
CopyAlerter: Types.Alerter;
DeleteAlerter: Types.Alerter;
UpdateAlerter: Types.Alerter;
CreateProcedure: Types.Procedure;
CopyProcedure: Types.Procedure;
DeleteProcedure: Types.Procedure;
UpdateProcedure: Types.Procedure;
CreateAction: Types.Action;
CopyAction: Types.Action;
DeleteAction: Types.Action;
UpdateAction: Types.Action;
CreateResourceSync: Types.ResourceSync;
CopyResourceSync: Types.ResourceSync;
DeleteResourceSync: Types.ResourceSync;
UpdateResourceSync: Types.ResourceSync;
CommitSync: Types.ResourceSync;
WriteSyncFileContents: Types.Update;
RefreshResourceSyncPending: Types.ResourceSync;
CreateSyncWebhook: Types.CreateSyncWebhookResponse;
DeleteSyncWebhook: Types.DeleteSyncWebhookResponse;
CreateStack: Types.Stack;
CopyStack: Types.Stack;
DeleteStack: Types.Stack;
UpdateStack: Types.Stack;
RenameStack: Types.Update;
WriteStackFileContents: Types.Update;
RefreshStackCache: Types.NoData;
CreateStackWebhook: Types.CreateStackWebhookResponse;
DeleteStackWebhook: Types.DeleteStackWebhookResponse;
CreateTag: Types.Tag;
DeleteTag: Types.Tag;
RenameTag: Types.Tag;
UpdateTagsOnResource: Types.UpdateTagsOnResourceResponse;
CreateVariable: Types.CreateVariableResponse;
UpdateVariableValue: Types.UpdateVariableValueResponse;
UpdateVariableDescription: Types.UpdateVariableDescriptionResponse;
UpdateVariableIsSecret: Types.UpdateVariableIsSecretResponse;
DeleteVariable: Types.DeleteVariableResponse;
CreateGitProviderAccount: Types.CreateGitProviderAccountResponse;
UpdateGitProviderAccount: Types.UpdateGitProviderAccountResponse;
DeleteGitProviderAccount: Types.DeleteGitProviderAccountResponse;
CreateDockerRegistryAccount: Types.CreateDockerRegistryAccountResponse;
UpdateDockerRegistryAccount: Types.UpdateDockerRegistryAccountResponse;
DeleteDockerRegistryAccount: Types.DeleteDockerRegistryAccountResponse;
};
export type ExecuteResponses = {
StartContainer: Types.Update;
RestartContainer: Types.Update;
PauseContainer: Types.Update;
UnpauseContainer: Types.Update;
StopContainer: Types.Update;
DestroyContainer: Types.Update;
StartAllContainers: Types.Update;
RestartAllContainers: Types.Update;
PauseAllContainers: Types.Update;
UnpauseAllContainers: Types.Update;
StopAllContainers: Types.Update;
PruneContainers: Types.Update;
DeleteNetwork: Types.Update;
PruneNetworks: Types.Update;
DeleteImage: Types.Update;
PruneImages: Types.Update;
DeleteVolume: Types.Update;
PruneVolumes: Types.Update;
PruneDockerBuilders: Types.Update;
PruneBuildx: Types.Update;
PruneSystem: Types.Update;
Deploy: Types.Update;
StartDeployment: Types.Update;
RestartDeployment: Types.Update;
PauseDeployment: Types.Update;
UnpauseDeployment: Types.Update;
StopDeployment: Types.Update;
DestroyDeployment: Types.Update;
RunBuild: Types.Update;
CancelBuild: Types.Update;
CloneRepo: Types.Update;
PullRepo: Types.Update;
BuildRepo: Types.Update;
CancelRepoBuild: Types.Update;
RunProcedure: Types.Update;
RunAction: Types.Update;
LaunchServer: Types.Update;
RunSync: Types.Update;
DeployStack: Types.Update;
DeployStackIfChanged: Types.Update;
StartStack: Types.Update;
RestartStack: Types.Update;
StopStack: Types.Update;
PauseStack: Types.Update;
UnpauseStack: Types.Update;
DestroyStack: Types.Update;
DeployStackService: Types.Update;
StartStackService: Types.Update;
RestartStackService: Types.Update;
StopStackService: Types.Update;
PauseStackService: Types.Update;
UnpauseStackService: Types.Update;
DestroyStackService: Types.Update;
};

View File

@@ -0,0 +1 @@
export {};

7173
frontend/public/client/types.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,481 @@
/*
Generated by typeshare 1.11.0
*/
/** The levels of permission that a User or UserGroup can have on a resource. */
export var PermissionLevel;
(function (PermissionLevel) {
/** No permissions. */
PermissionLevel["None"] = "None";
/** Can see the rousource */
PermissionLevel["Read"] = "Read";
/** Can execute actions on the resource */
PermissionLevel["Execute"] = "Execute";
/** Can update the resource configuration */
PermissionLevel["Write"] = "Write";
})(PermissionLevel || (PermissionLevel = {}));
export var ActionState;
(function (ActionState) {
/** Unknown case */
ActionState["Unknown"] = "Unknown";
/** Last clone / pull successful (or never cloned) */
ActionState["Ok"] = "Ok";
/** Last clone / pull failed */
ActionState["Failed"] = "Failed";
/** Currently running */
ActionState["Running"] = "Running";
})(ActionState || (ActionState = {}));
export var TagBehavior;
(function (TagBehavior) {
/** Returns resources which have strictly all the tags */
TagBehavior["All"] = "All";
/** Returns resources which have one or more of the tags */
TagBehavior["Any"] = "Any";
})(TagBehavior || (TagBehavior = {}));
export var BuildState;
(function (BuildState) {
/** Last build successful (or never built) */
BuildState["Ok"] = "Ok";
/** Last build failed */
BuildState["Failed"] = "Failed";
/** Currently building */
BuildState["Building"] = "Building";
/** Other case */
BuildState["Unknown"] = "Unknown";
})(BuildState || (BuildState = {}));
export var RestartMode;
(function (RestartMode) {
RestartMode["NoRestart"] = "no";
RestartMode["OnFailure"] = "on-failure";
RestartMode["Always"] = "always";
RestartMode["UnlessStopped"] = "unless-stopped";
})(RestartMode || (RestartMode = {}));
export var TerminationSignal;
(function (TerminationSignal) {
TerminationSignal["SigHup"] = "SIGHUP";
TerminationSignal["SigInt"] = "SIGINT";
TerminationSignal["SigQuit"] = "SIGQUIT";
TerminationSignal["SigTerm"] = "SIGTERM";
})(TerminationSignal || (TerminationSignal = {}));
/**
* Variants de/serialized from/to snake_case.
*
* Eg.
* - NotDeployed -> not_deployed
* - Restarting -> restarting
* - Running -> running.
*/
export var DeploymentState;
(function (DeploymentState) {
DeploymentState["Unknown"] = "unknown";
DeploymentState["NotDeployed"] = "not_deployed";
DeploymentState["Created"] = "created";
DeploymentState["Restarting"] = "restarting";
DeploymentState["Running"] = "running";
DeploymentState["Removing"] = "removing";
DeploymentState["Paused"] = "paused";
DeploymentState["Exited"] = "exited";
DeploymentState["Dead"] = "dead";
})(DeploymentState || (DeploymentState = {}));
/** Severity level of problem. */
export var SeverityLevel;
(function (SeverityLevel) {
/** No problem. */
SeverityLevel["Ok"] = "OK";
/** Problem is imminent. */
SeverityLevel["Warning"] = "WARNING";
/** Problem fully realized. */
SeverityLevel["Critical"] = "CRITICAL";
})(SeverityLevel || (SeverityLevel = {}));
export var Timelength;
(function (Timelength) {
Timelength["OneSecond"] = "1-sec";
Timelength["FiveSeconds"] = "5-sec";
Timelength["TenSeconds"] = "10-sec";
Timelength["FifteenSeconds"] = "15-sec";
Timelength["ThirtySeconds"] = "30-sec";
Timelength["OneMinute"] = "1-min";
Timelength["TwoMinutes"] = "2-min";
Timelength["FiveMinutes"] = "5-min";
Timelength["TenMinutes"] = "10-min";
Timelength["FifteenMinutes"] = "15-min";
Timelength["ThirtyMinutes"] = "30-min";
Timelength["OneHour"] = "1-hr";
Timelength["TwoHours"] = "2-hr";
Timelength["SixHours"] = "6-hr";
Timelength["EightHours"] = "8-hr";
Timelength["TwelveHours"] = "12-hr";
Timelength["OneDay"] = "1-day";
Timelength["ThreeDay"] = "3-day";
Timelength["OneWeek"] = "1-wk";
Timelength["TwoWeeks"] = "2-wk";
Timelength["ThirtyDays"] = "30-day";
})(Timelength || (Timelength = {}));
export var Operation;
(function (Operation) {
Operation["None"] = "None";
Operation["CreateServer"] = "CreateServer";
Operation["UpdateServer"] = "UpdateServer";
Operation["DeleteServer"] = "DeleteServer";
Operation["RenameServer"] = "RenameServer";
Operation["StartContainer"] = "StartContainer";
Operation["RestartContainer"] = "RestartContainer";
Operation["PauseContainer"] = "PauseContainer";
Operation["UnpauseContainer"] = "UnpauseContainer";
Operation["StopContainer"] = "StopContainer";
Operation["DestroyContainer"] = "DestroyContainer";
Operation["StartAllContainers"] = "StartAllContainers";
Operation["RestartAllContainers"] = "RestartAllContainers";
Operation["PauseAllContainers"] = "PauseAllContainers";
Operation["UnpauseAllContainers"] = "UnpauseAllContainers";
Operation["StopAllContainers"] = "StopAllContainers";
Operation["PruneContainers"] = "PruneContainers";
Operation["CreateNetwork"] = "CreateNetwork";
Operation["DeleteNetwork"] = "DeleteNetwork";
Operation["PruneNetworks"] = "PruneNetworks";
Operation["DeleteImage"] = "DeleteImage";
Operation["PruneImages"] = "PruneImages";
Operation["DeleteVolume"] = "DeleteVolume";
Operation["PruneVolumes"] = "PruneVolumes";
Operation["PruneDockerBuilders"] = "PruneDockerBuilders";
Operation["PruneBuildx"] = "PruneBuildx";
Operation["PruneSystem"] = "PruneSystem";
Operation["CreateStack"] = "CreateStack";
Operation["UpdateStack"] = "UpdateStack";
Operation["RenameStack"] = "RenameStack";
Operation["DeleteStack"] = "DeleteStack";
Operation["WriteStackContents"] = "WriteStackContents";
Operation["RefreshStackCache"] = "RefreshStackCache";
Operation["DeployStack"] = "DeployStack";
Operation["StartStack"] = "StartStack";
Operation["RestartStack"] = "RestartStack";
Operation["PauseStack"] = "PauseStack";
Operation["UnpauseStack"] = "UnpauseStack";
Operation["StopStack"] = "StopStack";
Operation["DestroyStack"] = "DestroyStack";
Operation["StartStackService"] = "StartStackService";
Operation["RestartStackService"] = "RestartStackService";
Operation["PauseStackService"] = "PauseStackService";
Operation["UnpauseStackService"] = "UnpauseStackService";
Operation["StopStackService"] = "StopStackService";
Operation["CreateDeployment"] = "CreateDeployment";
Operation["UpdateDeployment"] = "UpdateDeployment";
Operation["DeleteDeployment"] = "DeleteDeployment";
Operation["Deploy"] = "Deploy";
Operation["StartDeployment"] = "StartDeployment";
Operation["RestartDeployment"] = "RestartDeployment";
Operation["PauseDeployment"] = "PauseDeployment";
Operation["UnpauseDeployment"] = "UnpauseDeployment";
Operation["StopDeployment"] = "StopDeployment";
Operation["DestroyDeployment"] = "DestroyDeployment";
Operation["RenameDeployment"] = "RenameDeployment";
Operation["CreateBuild"] = "CreateBuild";
Operation["UpdateBuild"] = "UpdateBuild";
Operation["DeleteBuild"] = "DeleteBuild";
Operation["RunBuild"] = "RunBuild";
Operation["CancelBuild"] = "CancelBuild";
Operation["CreateRepo"] = "CreateRepo";
Operation["UpdateRepo"] = "UpdateRepo";
Operation["DeleteRepo"] = "DeleteRepo";
Operation["CloneRepo"] = "CloneRepo";
Operation["PullRepo"] = "PullRepo";
Operation["BuildRepo"] = "BuildRepo";
Operation["CancelRepoBuild"] = "CancelRepoBuild";
Operation["CreateProcedure"] = "CreateProcedure";
Operation["UpdateProcedure"] = "UpdateProcedure";
Operation["DeleteProcedure"] = "DeleteProcedure";
Operation["RunProcedure"] = "RunProcedure";
Operation["CreateAction"] = "CreateAction";
Operation["UpdateAction"] = "UpdateAction";
Operation["DeleteAction"] = "DeleteAction";
Operation["RunAction"] = "RunAction";
Operation["CreateBuilder"] = "CreateBuilder";
Operation["UpdateBuilder"] = "UpdateBuilder";
Operation["DeleteBuilder"] = "DeleteBuilder";
Operation["CreateAlerter"] = "CreateAlerter";
Operation["UpdateAlerter"] = "UpdateAlerter";
Operation["DeleteAlerter"] = "DeleteAlerter";
Operation["CreateServerTemplate"] = "CreateServerTemplate";
Operation["UpdateServerTemplate"] = "UpdateServerTemplate";
Operation["DeleteServerTemplate"] = "DeleteServerTemplate";
Operation["LaunchServer"] = "LaunchServer";
Operation["CreateResourceSync"] = "CreateResourceSync";
Operation["UpdateResourceSync"] = "UpdateResourceSync";
Operation["DeleteResourceSync"] = "DeleteResourceSync";
Operation["WriteSyncContents"] = "WriteSyncContents";
Operation["CommitSync"] = "CommitSync";
Operation["RunSync"] = "RunSync";
Operation["CreateVariable"] = "CreateVariable";
Operation["UpdateVariableValue"] = "UpdateVariableValue";
Operation["DeleteVariable"] = "DeleteVariable";
Operation["CreateGitProviderAccount"] = "CreateGitProviderAccount";
Operation["UpdateGitProviderAccount"] = "UpdateGitProviderAccount";
Operation["DeleteGitProviderAccount"] = "DeleteGitProviderAccount";
Operation["CreateDockerRegistryAccount"] = "CreateDockerRegistryAccount";
Operation["UpdateDockerRegistryAccount"] = "UpdateDockerRegistryAccount";
Operation["DeleteDockerRegistryAccount"] = "DeleteDockerRegistryAccount";
})(Operation || (Operation = {}));
/** An update's status */
export var UpdateStatus;
(function (UpdateStatus) {
/** The run is in the system but hasn't started yet */
UpdateStatus["Queued"] = "Queued";
/** The run is currently running */
UpdateStatus["InProgress"] = "InProgress";
/** The run is complete */
UpdateStatus["Complete"] = "Complete";
})(UpdateStatus || (UpdateStatus = {}));
export var ContainerStateStatusEnum;
(function (ContainerStateStatusEnum) {
ContainerStateStatusEnum["Empty"] = "";
ContainerStateStatusEnum["Created"] = "created";
ContainerStateStatusEnum["Running"] = "running";
ContainerStateStatusEnum["Paused"] = "paused";
ContainerStateStatusEnum["Restarting"] = "restarting";
ContainerStateStatusEnum["Removing"] = "removing";
ContainerStateStatusEnum["Exited"] = "exited";
ContainerStateStatusEnum["Dead"] = "dead";
})(ContainerStateStatusEnum || (ContainerStateStatusEnum = {}));
export var HealthStatusEnum;
(function (HealthStatusEnum) {
HealthStatusEnum["Empty"] = "";
HealthStatusEnum["None"] = "none";
HealthStatusEnum["Starting"] = "starting";
HealthStatusEnum["Healthy"] = "healthy";
HealthStatusEnum["Unhealthy"] = "unhealthy";
})(HealthStatusEnum || (HealthStatusEnum = {}));
export var RestartPolicyNameEnum;
(function (RestartPolicyNameEnum) {
RestartPolicyNameEnum["Empty"] = "";
RestartPolicyNameEnum["No"] = "no";
RestartPolicyNameEnum["Always"] = "always";
RestartPolicyNameEnum["UnlessStopped"] = "unless-stopped";
RestartPolicyNameEnum["OnFailure"] = "on-failure";
})(RestartPolicyNameEnum || (RestartPolicyNameEnum = {}));
export var MountTypeEnum;
(function (MountTypeEnum) {
MountTypeEnum["Empty"] = "";
MountTypeEnum["Bind"] = "bind";
MountTypeEnum["Volume"] = "volume";
MountTypeEnum["Tmpfs"] = "tmpfs";
MountTypeEnum["Npipe"] = "npipe";
MountTypeEnum["Cluster"] = "cluster";
})(MountTypeEnum || (MountTypeEnum = {}));
export var MountBindOptionsPropagationEnum;
(function (MountBindOptionsPropagationEnum) {
MountBindOptionsPropagationEnum["Empty"] = "";
MountBindOptionsPropagationEnum["Private"] = "private";
MountBindOptionsPropagationEnum["Rprivate"] = "rprivate";
MountBindOptionsPropagationEnum["Shared"] = "shared";
MountBindOptionsPropagationEnum["Rshared"] = "rshared";
MountBindOptionsPropagationEnum["Slave"] = "slave";
MountBindOptionsPropagationEnum["Rslave"] = "rslave";
})(MountBindOptionsPropagationEnum || (MountBindOptionsPropagationEnum = {}));
export var HostConfigCgroupnsModeEnum;
(function (HostConfigCgroupnsModeEnum) {
HostConfigCgroupnsModeEnum["Empty"] = "";
HostConfigCgroupnsModeEnum["Private"] = "private";
HostConfigCgroupnsModeEnum["Host"] = "host";
})(HostConfigCgroupnsModeEnum || (HostConfigCgroupnsModeEnum = {}));
export var HostConfigIsolationEnum;
(function (HostConfigIsolationEnum) {
HostConfigIsolationEnum["Empty"] = "";
HostConfigIsolationEnum["Default"] = "default";
HostConfigIsolationEnum["Process"] = "process";
HostConfigIsolationEnum["Hyperv"] = "hyperv";
})(HostConfigIsolationEnum || (HostConfigIsolationEnum = {}));
export var VolumeScopeEnum;
(function (VolumeScopeEnum) {
VolumeScopeEnum["Empty"] = "";
VolumeScopeEnum["Local"] = "local";
VolumeScopeEnum["Global"] = "global";
})(VolumeScopeEnum || (VolumeScopeEnum = {}));
export var ClusterVolumeSpecAccessModeScopeEnum;
(function (ClusterVolumeSpecAccessModeScopeEnum) {
ClusterVolumeSpecAccessModeScopeEnum["Empty"] = "";
ClusterVolumeSpecAccessModeScopeEnum["Single"] = "single";
ClusterVolumeSpecAccessModeScopeEnum["Multi"] = "multi";
})(ClusterVolumeSpecAccessModeScopeEnum || (ClusterVolumeSpecAccessModeScopeEnum = {}));
export var ClusterVolumeSpecAccessModeSharingEnum;
(function (ClusterVolumeSpecAccessModeSharingEnum) {
ClusterVolumeSpecAccessModeSharingEnum["Empty"] = "";
ClusterVolumeSpecAccessModeSharingEnum["None"] = "none";
ClusterVolumeSpecAccessModeSharingEnum["Readonly"] = "readonly";
ClusterVolumeSpecAccessModeSharingEnum["Onewriter"] = "onewriter";
ClusterVolumeSpecAccessModeSharingEnum["All"] = "all";
})(ClusterVolumeSpecAccessModeSharingEnum || (ClusterVolumeSpecAccessModeSharingEnum = {}));
export var ClusterVolumeSpecAccessModeAvailabilityEnum;
(function (ClusterVolumeSpecAccessModeAvailabilityEnum) {
ClusterVolumeSpecAccessModeAvailabilityEnum["Empty"] = "";
ClusterVolumeSpecAccessModeAvailabilityEnum["Active"] = "active";
ClusterVolumeSpecAccessModeAvailabilityEnum["Pause"] = "pause";
ClusterVolumeSpecAccessModeAvailabilityEnum["Drain"] = "drain";
})(ClusterVolumeSpecAccessModeAvailabilityEnum || (ClusterVolumeSpecAccessModeAvailabilityEnum = {}));
export var ClusterVolumePublishStatusStateEnum;
(function (ClusterVolumePublishStatusStateEnum) {
ClusterVolumePublishStatusStateEnum["Empty"] = "";
ClusterVolumePublishStatusStateEnum["PendingPublish"] = "pending-publish";
ClusterVolumePublishStatusStateEnum["Published"] = "published";
ClusterVolumePublishStatusStateEnum["PendingNodeUnpublish"] = "pending-node-unpublish";
ClusterVolumePublishStatusStateEnum["PendingControllerUnpublish"] = "pending-controller-unpublish";
})(ClusterVolumePublishStatusStateEnum || (ClusterVolumePublishStatusStateEnum = {}));
export var ProcedureState;
(function (ProcedureState) {
/** Last run successful */
ProcedureState["Ok"] = "Ok";
/** Last run failed */
ProcedureState["Failed"] = "Failed";
/** Currently running */
ProcedureState["Running"] = "Running";
/** Other case (never run) */
ProcedureState["Unknown"] = "Unknown";
})(ProcedureState || (ProcedureState = {}));
export var RepoState;
(function (RepoState) {
/** Unknown case */
RepoState["Unknown"] = "Unknown";
/** Last clone / pull successful (or never cloned) */
RepoState["Ok"] = "Ok";
/** Last clone / pull failed */
RepoState["Failed"] = "Failed";
/** Currently cloning */
RepoState["Cloning"] = "Cloning";
/** Currently pulling */
RepoState["Pulling"] = "Pulling";
/** Currently building */
RepoState["Building"] = "Building";
})(RepoState || (RepoState = {}));
export var ResourceSyncState;
(function (ResourceSyncState) {
/** Last sync successful (or never synced). No Changes pending */
ResourceSyncState["Ok"] = "Ok";
/** Last sync failed */
ResourceSyncState["Failed"] = "Failed";
/** Currently syncing */
ResourceSyncState["Syncing"] = "Syncing";
/** Updates pending */
ResourceSyncState["Pending"] = "Pending";
/** Other case */
ResourceSyncState["Unknown"] = "Unknown";
})(ResourceSyncState || (ResourceSyncState = {}));
export var ServerState;
(function (ServerState) {
/** Server is unreachable. */
ServerState["NotOk"] = "NotOk";
/** Server health check passing. */
ServerState["Ok"] = "Ok";
/** Server is disabled. */
ServerState["Disabled"] = "Disabled";
})(ServerState || (ServerState = {}));
export var StackState;
(function (StackState) {
/** All containers are running. */
StackState["Running"] = "running";
/** All containers are paused */
StackState["Paused"] = "paused";
/** All contianers are stopped */
StackState["Stopped"] = "stopped";
/** All containers are created */
StackState["Created"] = "created";
/** All containers are restarting */
StackState["Restarting"] = "restarting";
/** All containers are dead */
StackState["Dead"] = "dead";
/** All containers are removing */
StackState["Removing"] = "removing";
/** The containers are in a mix of states */
StackState["Unhealthy"] = "unhealthy";
/** The stack is not deployed */
StackState["Down"] = "down";
/** Server not reachable */
StackState["Unknown"] = "unknown";
})(StackState || (StackState = {}));
export var AwsVolumeType;
(function (AwsVolumeType) {
AwsVolumeType["Gp2"] = "gp2";
AwsVolumeType["Gp3"] = "gp3";
AwsVolumeType["Io1"] = "io1";
AwsVolumeType["Io2"] = "io2";
})(AwsVolumeType || (AwsVolumeType = {}));
export var RepoWebhookAction;
(function (RepoWebhookAction) {
RepoWebhookAction["Clone"] = "Clone";
RepoWebhookAction["Pull"] = "Pull";
RepoWebhookAction["Build"] = "Build";
})(RepoWebhookAction || (RepoWebhookAction = {}));
export var StackWebhookAction;
(function (StackWebhookAction) {
StackWebhookAction["Refresh"] = "Refresh";
StackWebhookAction["Deploy"] = "Deploy";
})(StackWebhookAction || (StackWebhookAction = {}));
export var SyncWebhookAction;
(function (SyncWebhookAction) {
SyncWebhookAction["Refresh"] = "Refresh";
SyncWebhookAction["Sync"] = "Sync";
})(SyncWebhookAction || (SyncWebhookAction = {}));
export var HetznerDatacenter;
(function (HetznerDatacenter) {
HetznerDatacenter["Nuremberg1Dc3"] = "Nuremberg1Dc3";
HetznerDatacenter["Helsinki1Dc2"] = "Helsinki1Dc2";
HetznerDatacenter["Falkenstein1Dc14"] = "Falkenstein1Dc14";
HetznerDatacenter["AshburnDc1"] = "AshburnDc1";
HetznerDatacenter["HillsboroDc1"] = "HillsboroDc1";
HetznerDatacenter["SingaporeDc1"] = "SingaporeDc1";
})(HetznerDatacenter || (HetznerDatacenter = {}));
export var HetznerServerType;
(function (HetznerServerType) {
/** CPX11 - AMD 2 Cores, 2 Gb Ram, 40 Gb disk */
HetznerServerType["SharedAmd2Core2Ram40Disk"] = "SharedAmd2Core2Ram40Disk";
/** CAX11 - Arm 2 Cores, 4 Gb Ram, 40 Gb disk */
HetznerServerType["SharedArm2Core4Ram40Disk"] = "SharedArm2Core4Ram40Disk";
/** CX22 - Intel 2 Cores, 4 Gb Ram, 40 Gb disk */
HetznerServerType["SharedIntel2Core4Ram40Disk"] = "SharedIntel2Core4Ram40Disk";
/** CPX21 - AMD 3 Cores, 4 Gb Ram, 80 Gb disk */
HetznerServerType["SharedAmd3Core4Ram80Disk"] = "SharedAmd3Core4Ram80Disk";
/** CAX21 - Arm 4 Cores, 8 Gb Ram, 80 Gb disk */
HetznerServerType["SharedArm4Core8Ram80Disk"] = "SharedArm4Core8Ram80Disk";
/** CX32 - Intel 4 Cores, 8 Gb Ram, 80 Gb disk */
HetznerServerType["SharedIntel4Core8Ram80Disk"] = "SharedIntel4Core8Ram80Disk";
/** CPX31 - AMD 4 Cores, 8 Gb Ram, 160 Gb disk */
HetznerServerType["SharedAmd4Core8Ram160Disk"] = "SharedAmd4Core8Ram160Disk";
/** CAX31 - Arm 8 Cores, 16 Gb Ram, 160 Gb disk */
HetznerServerType["SharedArm8Core16Ram160Disk"] = "SharedArm8Core16Ram160Disk";
/** CX42 - Intel 8 Cores, 16 Gb Ram, 160 Gb disk */
HetznerServerType["SharedIntel8Core16Ram160Disk"] = "SharedIntel8Core16Ram160Disk";
/** CPX41 - AMD 8 Cores, 16 Gb Ram, 240 Gb disk */
HetznerServerType["SharedAmd8Core16Ram240Disk"] = "SharedAmd8Core16Ram240Disk";
/** CAX41 - Arm 16 Cores, 32 Gb Ram, 320 Gb disk */
HetznerServerType["SharedArm16Core32Ram320Disk"] = "SharedArm16Core32Ram320Disk";
/** CX52 - Intel 16 Cores, 32 Gb Ram, 320 Gb disk */
HetznerServerType["SharedIntel16Core32Ram320Disk"] = "SharedIntel16Core32Ram320Disk";
/** CPX51 - AMD 16 Cores, 32 Gb Ram, 360 Gb disk */
HetznerServerType["SharedAmd16Core32Ram360Disk"] = "SharedAmd16Core32Ram360Disk";
/** CCX13 - AMD 2 Cores, 8 Gb Ram, 80 Gb disk */
HetznerServerType["DedicatedAmd2Core8Ram80Disk"] = "DedicatedAmd2Core8Ram80Disk";
/** CCX23 - AMD 4 Cores, 16 Gb Ram, 160 Gb disk */
HetznerServerType["DedicatedAmd4Core16Ram160Disk"] = "DedicatedAmd4Core16Ram160Disk";
/** CCX33 - AMD 8 Cores, 32 Gb Ram, 240 Gb disk */
HetznerServerType["DedicatedAmd8Core32Ram240Disk"] = "DedicatedAmd8Core32Ram240Disk";
/** CCX43 - AMD 16 Cores, 64 Gb Ram, 360 Gb disk */
HetznerServerType["DedicatedAmd16Core64Ram360Disk"] = "DedicatedAmd16Core64Ram360Disk";
/** CCX53 - AMD 32 Cores, 128 Gb Ram, 600 Gb disk */
HetznerServerType["DedicatedAmd32Core128Ram600Disk"] = "DedicatedAmd32Core128Ram600Disk";
/** CCX63 - AMD 48 Cores, 192 Gb Ram, 960 Gb disk */
HetznerServerType["DedicatedAmd48Core192Ram960Disk"] = "DedicatedAmd48Core192Ram960Disk";
})(HetznerServerType || (HetznerServerType = {}));
export var HetznerVolumeFormat;
(function (HetznerVolumeFormat) {
HetznerVolumeFormat["Xfs"] = "Xfs";
HetznerVolumeFormat["Ext4"] = "Ext4";
})(HetznerVolumeFormat || (HetznerVolumeFormat = {}));
export var PortTypeEnum;
(function (PortTypeEnum) {
PortTypeEnum["EMPTY"] = "";
PortTypeEnum["TCP"] = "tcp";
PortTypeEnum["UDP"] = "udp";
PortTypeEnum["SCTP"] = "sctp";
})(PortTypeEnum || (PortTypeEnum = {}));
export var SearchCombinator;
(function (SearchCombinator) {
SearchCombinator["Or"] = "Or";
SearchCombinator["And"] = "And";
})(SearchCombinator || (SearchCombinator = {}));

8
frontend/public/index.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
import { KomodoClient, Types as KomodoTypes } from "./client/lib.js";
declare global {
var komodo: ReturnType<typeof KomodoClient>;
export import Types = KomodoTypes;
}
export {}

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