Compare commits

..

4 Commits

Author SHA1 Message Date
mbecker20
6fe5bc7420 properly host client lib for deno importing (types working) 2024-10-20 03:17:58 -04:00
mbecker20
82324b00ee typescript komodo_client v 1.16.0 2024-10-20 02:35:43 -04:00
Maxwell Becker
5daba3a557 1.16.0 (#140)
* consolidate deserializers

* key value list doc

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

* add additional env files support

* plumbing for Action resource

* js client readme indentation

* regen lock

* add action UI

* action backend

* start on action frontend

* update lock

* get up to speed

* get action started

* clean up default action file

* seems to work

* toml export include action

* action works

* action works part 2

* bump rust version to 1.82.0

* copy deno bin from bin image

* action use local dir

* update not having changes doesn't return error

* format with prettier

* support yaml formatting with prettier

* variable no change is Ok
2024-10-19 23:27:28 -07:00
mbecker20
020cdc06fd remove migrator link in readme 2024-10-18 21:23:02 -04:00
144 changed files with 17590 additions and 6738 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

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

View File

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

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

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

View File

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

View File

@@ -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.16.0",
"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 = {}));

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