Compare commits

...

7 Commits

Author SHA1 Message Date
Maxwell Becker
2cfae525e9 1.15.3 (#109)
* fix parser support single quote '

* add stack reclone toggle

* git clone with token uses token:<TOKEN> for gitlab compatability

* support stack pre deploy shell command

* rename compose down update log stage

* deployment configure registry login account

* local testing setup

* bump version to 1.15.3

* new resources auto assign server if only one

* better error log when try to create resource with duplicate name

* end description with .

* ConfirmUpdate multi language

* fix compose write to host logic

* improve instrumentation

* improve update diff when small array

improve 2

* fix compose env file passing when repo_dir is not absolute
2024-10-08 23:07:38 -07:00
mbecker20
80e5d2a972 frontend dev setup guide 2024-10-08 16:55:24 -04:00
mbecker20
6f22c011a6 builder / server template add correct additional line if empty params 2024-10-07 22:55:48 -04:00
mbecker20
401cccee79 config nav buttons secondary 2024-10-07 21:55:14 -04:00
mbecker20
654b923f98 fix broken link to periphery setup 2024-10-07 18:56:14 -04:00
mbecker20
61261be70f update docs, split connecting servers out of Core Setup 2024-10-07 18:54:00 -04:00
mbecker20
46418125e3 update docs for periphery systemd --user install 2024-10-07 18:53:43 -04:00
46 changed files with 675 additions and 321 deletions

View File

@@ -5,4 +5,10 @@ LICENSE
*.code-workspace
*/node_modules
*/dist
*/dist
creds.toml
.core-repos
.repos
.stacks
.ssl

9
.gitignore vendored
View File

@@ -1,11 +1,14 @@
target
/frontend/build
node_modules
/lib/ts_client/build
node_modules
dist
.env
.env.development
.DS_Store
creds.toml
.syncs
.core-repos
.repos
.stacks
.DS_Store
.ssl

27
Cargo.lock generated
View File

@@ -41,7 +41,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"anyhow",
"axum",
@@ -943,7 +943,7 @@ dependencies = [
[[package]]
name = "command"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"komodo_client",
"run_command",
@@ -1355,7 +1355,7 @@ dependencies = [
[[package]]
name = "environment_file"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"thiserror",
]
@@ -1439,7 +1439,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"serror",
]
@@ -1571,7 +1571,7 @@ checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
[[package]]
name = "git"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"anyhow",
"command",
@@ -2192,7 +2192,7 @@ dependencies = [
[[package]]
name = "komodo_cli"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"anyhow",
"clap",
@@ -2208,7 +2208,7 @@ dependencies = [
[[package]]
name = "komodo_client"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2239,7 +2239,7 @@ dependencies = [
[[package]]
name = "komodo_core"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2296,7 +2296,7 @@ dependencies = [
[[package]]
name = "komodo_periphery"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2306,6 +2306,7 @@ dependencies = [
"bollard",
"clap",
"command",
"derive_variants",
"dotenvy",
"environment_file",
"envy",
@@ -2382,7 +2383,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"anyhow",
"komodo_client",
@@ -2446,7 +2447,7 @@ dependencies = [
[[package]]
name = "migrator"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"anyhow",
"dotenvy",
@@ -3101,7 +3102,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"anyhow",
"komodo_client",
@@ -4879,7 +4880,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.15.2"
version = "1.15.3"
dependencies = [
"anyhow",
"komodo_client",

View File

@@ -3,7 +3,7 @@ resolver = "2"
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
[workspace.package]
version = "1.15.2"
version = "1.15.3"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"

View File

@@ -10,36 +10,42 @@ use komodo_client::entities::{
ResourceTargetVariant,
};
use mungos::{find::find_collect, mongodb::bson::doc};
use tracing::Instrument;
use crate::{config::core_config, state::db_client};
mod discord;
mod slack;
#[instrument]
pub async fn send_alerts(alerts: &[Alert]) {
if alerts.is_empty() {
return;
}
let Ok(alerters) = find_collect(
&db_client().alerters,
doc! { "config.enabled": true },
None,
)
.await
.inspect_err(|e| {
error!(
let span =
info_span!("send_alerts", alerts = format!("{alerts:?}"));
async {
let Ok(alerters) = find_collect(
&db_client().alerters,
doc! { "config.enabled": true },
None,
)
.await
.inspect_err(|e| {
error!(
"ERROR sending alerts | failed to get alerters from db | {e:#}"
)
}) else {
return;
};
}) else {
return;
};
let handles =
alerts.iter().map(|alert| send_alert(&alerters, alert));
let handles =
alerts.iter().map(|alert| send_alert(&alerters, alert));
join_all(handles).await;
join_all(handles).await;
}
.instrument(span)
.await
}
#[instrument(level = "debug")]

View File

@@ -2,6 +2,7 @@ use std::time::Instant;
use anyhow::{anyhow, Context};
use axum::{middleware, routing::post, Extension, Router};
use derive_variants::{EnumVariants, ExtractVariant};
use formatting::format_serror;
use komodo_client::{
api::execute::*,
@@ -33,7 +34,10 @@ mod stack;
mod sync;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
)]
#[variant_derive(Debug)]
#[resolver_target(State)]
#[resolver_args((User, Update))]
#[serde(tag = "type", content = "params")]
@@ -154,7 +158,15 @@ async fn handler(
Ok(Json(update))
}
#[instrument(name = "ExecuteRequest", skip(user, update), fields(user_id = user.id, update_id = update.id))]
#[instrument(
name = "ExecuteRequest",
skip(user, update),
fields(
user_id = user.id,
update_id = update.id,
request = format!("{:?}", request.extract_variant()))
)
]
async fn task(
req_id: Uuid,
request: ExecuteRequest,

View File

@@ -19,6 +19,7 @@ use crate::{
add_interp_update_log,
interpolate_variables_secrets_into_extra_args,
interpolate_variables_secrets_into_string,
interpolate_variables_secrets_into_system_command,
},
periphery_client,
query::get_variables_and_secrets,
@@ -102,6 +103,13 @@ impl Resolve<DeployStack, (User, Update)> for State {
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_system_command(
&vars_and_secrets,
&mut stack.config.pre_deploy,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,

View File

@@ -188,7 +188,10 @@ async fn handler(
#[instrument(
name = "WriteRequest",
skip(user, request),
fields(user_id = user.id, request = format!("{:?}", request.extract_variant()))
fields(
user_id = user.id,
request = format!("{:?}", request.extract_variant())
)
)]
async fn task(
req_id: Uuid,

View File

@@ -57,6 +57,7 @@ impl super::KomodoResource for Build {
version: build.config.version,
builder_id: build.config.builder_id,
git_provider: build.config.git_provider,
image_registry_domain: build.config.image_registry.domain,
repo: build.config.repo,
branch: build.config.branch,
built_hash: build.info.built_hash,

View File

@@ -15,7 +15,7 @@ use komodo_client::{
tag::Tag,
to_komodo_name,
update::Update,
user::User,
user::{system_user, User},
Operation, ResourceTarget, ResourceTargetVariant,
},
};
@@ -508,6 +508,18 @@ pub async fn create<T: KomodoResource>(
return Err(anyhow!("valid ObjectIds cannot be used as names."));
}
// Ensure an existing resource with same name doesn't already exist
// The database indexing also ensures this but doesn't give a good error message.
if list_full_for_user::<T>(Default::default(), system_user())
.await
.context("Failed to list all resources for duplicate name check")?
.into_iter()
.find(|r| r.name == name)
.is_some()
{
return Err(anyhow!("Must provide unique name for resource."));
}
let start_ts = komodo_timestamp();
T::validate_create_config(&mut config, user).await?;

View File

@@ -353,7 +353,7 @@ impl ToToml for ServerTemplate {
if empty_params {
// toml_pretty will remove empty map
// but in this case its needed to deserialize the enums.
toml.push_str("\nconfig.params = {}");
toml.push_str("\nparams = {}");
}
}
}
@@ -385,7 +385,7 @@ impl ToToml for Builder {
if empty_params {
// toml_pretty will remove empty map
// but in this case its needed to deserialize the enums.
toml.push_str("\nconfig.params = {}");
toml.push_str("\nparams = {}");
}
}
}

View File

@@ -26,6 +26,7 @@ git.workspace = true
serror = { workspace = true, features = ["axum"] }
merge_config_files.workspace = true
async_timing_util.workspace = true
derive_variants.workspace = true
resolver_api.workspace = true
run_command.workspace = true
svi.workspace = true

View File

@@ -76,85 +76,6 @@ pub struct DockerComposeLsItem {
//
const DEFAULT_COMPOSE_CONTENTS: &str = "## 🦎 Hello Komodo 🦎
services:
hello_world:
image: hello-world
# networks:
# - default
# ports:
# - 3000:3000
# volumes:
# - data:/data
# networks:
# default: {}
# volumes:
# data:
";
impl Resolve<GetComposeContentsOnHost, ()> for State {
async fn resolve(
&self,
GetComposeContentsOnHost {
name,
run_directory,
file_paths,
}: GetComposeContentsOnHost,
_: (),
) -> anyhow::Result<GetComposeContentsOnHostResponse> {
let root =
periphery_config().stack_dir.join(to_komodo_name(&name));
let run_directory =
root.join(&run_directory).components().collect::<PathBuf>();
if !run_directory.exists() {
fs::create_dir_all(&run_directory)
.await
.context("Failed to initialize run directory")?;
}
let file_paths = file_paths
.iter()
.map(|path| {
run_directory.join(path).components().collect::<PathBuf>()
})
.collect::<Vec<_>>();
let mut res = GetComposeContentsOnHostResponse::default();
for full_path in &file_paths {
if !full_path.exists() {
fs::write(&full_path, DEFAULT_COMPOSE_CONTENTS)
.await
.context("Failed to init missing compose file on host")?;
}
match fs::read_to_string(&full_path).await.with_context(|| {
format!(
"Failed to read compose file contents at {full_path:?}"
)
}) {
Ok(contents) => {
res.contents.push(FileContents {
path: full_path.display().to_string(),
contents,
});
}
Err(e) => {
res.errors.push(FileContents {
path: full_path.display().to_string(),
contents: format_serror(&e.into()),
});
}
}
}
Ok(res)
}
}
//
impl Resolve<GetComposeServiceLog> for State {
#[instrument(
name = "GetComposeServiceLog",
@@ -204,6 +125,85 @@ impl Resolve<GetComposeServiceLogSearch> for State {
//
const DEFAULT_COMPOSE_CONTENTS: &str = "## 🦎 Hello Komodo 🦎
services:
hello_world:
image: hello-world
# networks:
# - default
# ports:
# - 3000:3000
# volumes:
# - data:/data
# networks:
# default: {}
# volumes:
# data:
";
impl Resolve<GetComposeContentsOnHost, ()> for State {
#[instrument(
name = "GetComposeContentsOnHost",
level = "debug",
skip(self)
)]
async fn resolve(
&self,
GetComposeContentsOnHost {
name,
run_directory,
file_paths,
}: GetComposeContentsOnHost,
_: (),
) -> anyhow::Result<GetComposeContentsOnHostResponse> {
let root =
periphery_config().stack_dir.join(to_komodo_name(&name));
let run_directory =
root.join(&run_directory).components().collect::<PathBuf>();
if !run_directory.exists() {
fs::create_dir_all(&run_directory)
.await
.context("Failed to initialize run directory")?;
}
let mut res = GetComposeContentsOnHostResponse::default();
for path in file_paths {
let full_path =
run_directory.join(&path).components().collect::<PathBuf>();
if !full_path.exists() {
fs::write(&full_path, DEFAULT_COMPOSE_CONTENTS)
.await
.context("Failed to init missing compose file on host")?;
}
match fs::read_to_string(&full_path).await.with_context(|| {
format!(
"Failed to read compose file contents at {full_path:?}"
)
}) {
Ok(contents) => {
// The path we store here has to be the same as incoming file path in the array,
// in order for WriteComposeContentsToHost to write to the correct path.
res.contents.push(FileContents { path, contents });
}
Err(e) => {
res.errors.push(FileContents {
path,
contents: format_serror(&e.into()),
});
}
}
}
Ok(res)
}
}
//
impl Resolve<WriteComposeContentsToHost> for State {
#[instrument(name = "WriteComposeContentsToHost", skip(self))]
async fn resolve(
@@ -216,13 +216,10 @@ impl Resolve<WriteComposeContentsToHost> for State {
}: WriteComposeContentsToHost,
_: (),
) -> anyhow::Result<Log> {
let root =
periphery_config().stack_dir.join(to_komodo_name(&name));
let run_directory = root.join(&run_directory);
let run_directory = run_directory.canonicalize().context(
"failed to validate run directory on host (canonicalize error)",
)?;
let file_path = run_directory
let file_path = periphery_config()
.stack_dir
.join(to_komodo_name(&name))
.join(&run_directory)
.join(file_path)
.components()
.collect::<PathBuf>();

View File

@@ -1,5 +1,6 @@
use anyhow::Context;
use command::run_komodo_command;
use derive_variants::EnumVariants;
use futures::TryFutureExt;
use komodo_client::entities::{update::Log, SystemCommand};
use periphery_client::api::{
@@ -30,7 +31,10 @@ mod network;
mod stats;
mod volume;
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
)]
#[variant_derive(Debug)]
#[serde(tag = "type", content = "params")]
#[resolver_target(State)]
#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]
@@ -206,7 +210,7 @@ impl ResolveToString<ListSecrets> for State {
}
impl Resolve<GetDockerLists> for State {
#[instrument(name = "GetDockerLists", skip(self))]
#[instrument(name = "GetDockerLists", level = "debug", skip(self))]
async fn resolve(
&self,
GetDockerLists {}: GetDockerLists,

View File

@@ -10,7 +10,7 @@ use komodo_client::entities::{
};
use periphery_client::api::{
compose::ComposeUpResponse,
git::{PullOrCloneRepo, RepoActionResponse},
git::{CloneRepo, PullOrCloneRepo, RepoActionResponse},
};
use resolver_api::Resolve;
use tokio::fs;
@@ -71,7 +71,7 @@ pub async fn compose_up(
return Err(anyhow!("A compose file doesn't exist after writing stack. Ensure the run_directory and file_paths are correct."));
}
for (_, full_path) in &file_paths {
for (path, full_path) in &file_paths {
let file_contents =
match fs::read_to_string(&full_path).await.with_context(|| {
format!(
@@ -86,7 +86,7 @@ pub async fn compose_up(
.push(Log::error("read compose file", error.clone()));
// This should only happen for repo stacks, ie remote error
res.remote_errors.push(FileContents {
path: full_path.display().to_string(),
path: path.to_string(),
contents: error,
});
return Err(anyhow!(
@@ -95,7 +95,7 @@ pub async fn compose_up(
}
};
res.file_contents.push(FileContents {
path: full_path.display().to_string(),
path: path.to_string(),
contents: file_contents,
});
}
@@ -137,7 +137,7 @@ pub async fn compose_up(
}
let env_file = env_file_path
.map(|path| format!(" --env-file {}", path.display()))
.map(|path| format!(" --env-file {path}"))
.unwrap_or_default();
// Build images before destroying to minimize downtime.
@@ -197,6 +197,64 @@ pub async fn compose_up(
}
}
if !stack.config.pre_deploy.command.is_empty() {
let pre_deploy_path =
run_directory.join(&stack.config.pre_deploy.path);
if !stack.config.skip_secret_interp {
let (full_command, mut replacers) = svi::interpolate_variables(
&stack.config.pre_deploy.command,
&periphery_config().secrets,
svi::Interpolator::DoubleBrackets,
true,
)
.context(
"failed to interpolate secrets into pre_deploy command",
)?;
replacers.extend(core_replacers.to_owned());
let mut pre_deploy_log = run_komodo_command(
"pre deploy",
format!("cd {} && {full_command}", pre_deploy_path.display()),
)
.await;
pre_deploy_log.command =
svi::replace_in_string(&pre_deploy_log.command, &replacers);
pre_deploy_log.stdout =
svi::replace_in_string(&pre_deploy_log.stdout, &replacers);
pre_deploy_log.stderr =
svi::replace_in_string(&pre_deploy_log.stderr, &replacers);
tracing::debug!(
"run Stack pre_deploy command | command: {} | cwd: {:?}",
pre_deploy_log.command,
pre_deploy_path
);
res.logs.push(pre_deploy_log);
} else {
let pre_deploy_log = run_komodo_command(
"pre deploy",
format!(
"cd {} && {}",
pre_deploy_path.display(),
stack.config.pre_deploy.command
),
)
.await;
tracing::debug!(
"run Stack pre_deploy command | command: {} | cwd: {:?}",
&stack.config.pre_deploy.command,
pre_deploy_path
);
res.logs.push(pre_deploy_log);
}
if !all_logs_success(&res.logs) {
return Err(anyhow!(
"Failed at running pre_deploy command, stopping the run."
));
}
}
// Take down the existing containers.
// This one tries to use the previously deployed service name, to ensure the right stack is taken down.
compose_down(&last_project_name, service, res)
@@ -237,11 +295,11 @@ pub async fn compose_up(
/// Either writes the stack file_contents to a file, or clones the repo.
/// Returns (run_directory, env_file_path)
async fn write_stack(
stack: &Stack,
async fn write_stack<'a>(
stack: &'a Stack,
git_token: Option<String>,
res: &mut ComposeUpResponse,
) -> anyhow::Result<(PathBuf, Option<PathBuf>)> {
) -> anyhow::Result<(PathBuf, Option<&'a str>)> {
let root = periphery_config()
.stack_dir
.join(to_komodo_name(&stack.name));
@@ -275,7 +333,14 @@ async fn write_stack(
return Err(anyhow!("failed to write environment file"));
}
};
Ok((run_directory, env_file_path))
Ok((
run_directory,
// Env file paths are already relative to run directory,
// so need to pass original env_file_path here.
env_file_path
.is_some()
.then_some(&stack.config.env_file_path),
))
} else if stack.config.repo.is_empty() {
if stack.config.file_contents.trim().is_empty() {
return Err(anyhow!("Must either input compose file contents directly, or use file one host / git repo options."));
@@ -324,7 +389,12 @@ async fn write_stack(
format!("failed to write compose file to {file_path:?}")
})?;
Ok((run_directory, env_file_path))
Ok((
run_directory,
env_file_path
.is_some()
.then_some(&stack.config.env_file_path),
))
} else {
// ================
// REPO BASED FILES
@@ -362,27 +432,46 @@ async fn write_stack(
}
};
let clone_or_pull_res = if stack.config.reclone {
State
.resolve(
CloneRepo {
args,
git_token,
environment: env_vars,
env_file_path: stack.config.env_file_path.clone(),
skip_secret_interp: stack.config.skip_secret_interp,
// repo replacer only needed for on_clone / on_pull,
// which aren't available for stacks
replacers: Default::default(),
},
(),
)
.await
} else {
State
.resolve(
PullOrCloneRepo {
args,
git_token,
environment: env_vars,
env_file_path: stack.config.env_file_path.clone(),
skip_secret_interp: stack.config.skip_secret_interp,
// repo replacer only needed for on_clone / on_pull,
// which aren't available for stacks
replacers: Default::default(),
},
(),
)
.await
};
let RepoActionResponse {
logs,
commit_hash,
commit_message,
env_file_path,
} = match State
.resolve(
PullOrCloneRepo {
args,
git_token,
environment: env_vars,
env_file_path: stack.config.env_file_path.clone(),
skip_secret_interp: stack.config.skip_secret_interp,
// repo replacer only needed for on_clone / on_pull,
// which aren't available for stacks
replacers: Default::default(),
},
(),
)
.await
{
} = match clone_or_pull_res {
Ok(res) => res,
Err(e) => {
let error = format_serror(
@@ -407,7 +496,12 @@ async fn write_stack(
return Err(anyhow!("Stopped after repo pull failure"));
}
Ok((run_directory, env_file_path))
Ok((
run_directory,
env_file_path
.is_some()
.then_some(&stack.config.env_file_path),
))
}
}
@@ -422,7 +516,7 @@ async fn compose_down(
.map(|service| format!(" {service}"))
.unwrap_or_default();
let log = run_komodo_command(
"destroy container",
"compose down",
format!("{docker_compose} -p {project} down{service_arg}"),
)
.await;

View File

@@ -1,4 +1,4 @@
use std::{net::SocketAddr, time::Instant};
use std::net::SocketAddr;
use anyhow::{anyhow, Context};
use axum::{
@@ -11,6 +11,7 @@ use axum::{
Router,
};
use axum_extra::{headers::ContentType, TypedHeader};
use derive_variants::ExtractVariant;
use resolver_api::Resolver;
use serror::{AddStatusCode, AddStatusCodeError, Json};
use uuid::Uuid;
@@ -40,13 +41,12 @@ async fn handler(
Ok((TypedHeader(ContentType::json()), res??))
}
#[instrument(name = "PeripheryHandler")]
async fn task(
req_id: Uuid,
request: crate::api::PeripheryRequest,
) -> anyhow::Result<String> {
let timer = Instant::now();
let variant = request.extract_variant();
let res =
State
.resolve_request(request, ())
@@ -59,16 +59,12 @@ async fn task(
});
if let Err(e) = &res {
warn!("request {req_id} error: {e:#}");
warn!("request {req_id} | type: {variant:?} | error: {e:#}");
}
let elapsed = timer.elapsed();
debug!("request {req_id} | resolve time: {elapsed:?}");
res
}
#[instrument(level = "debug")]
async fn guard_request_by_passkey(
req: Request<Body>,
next: Next,
@@ -100,7 +96,6 @@ async fn guard_request_by_passkey(
}
}
#[instrument(level = "debug")]
async fn guard_request_by_ip(
req: Request<Body>,
next: Next,

View File

@@ -33,6 +33,8 @@ pub struct BuildListItemInfo {
pub builder_id: String,
/// The git provider domain
pub git_provider: String,
/// The image registry domain
pub image_registry_domain: String,
/// The repo used as the source of the build
pub repo: String,
/// The branch of the repo

View File

@@ -87,7 +87,7 @@ pub struct Env {
/// If not provided, will use Default config.
///
/// Note. This is overridden if the equivalent arg is passed in [CliArgs].
#[serde(default)]
#[serde(default, alias = "periphery_config_path")]
pub periphery_config_paths: Vec<String>,
/// If specifying folders, use this to narrow down which
/// files will be matched to parse into the final [PeripheryConfig].
@@ -95,7 +95,7 @@ pub struct Env {
/// provided to `config_keywords` will be included.
///
/// Note. This is overridden if the equivalent arg is passed in [CliArgs].
#[serde(default)]
#[serde(default, alias = "periphery_config_keyword")]
pub periphery_config_keywords: Vec<String>,
/// Will merge nested config object (eg. secrets, providers) across multiple

View File

@@ -164,7 +164,11 @@ pub fn get_image_name(
}
pub fn to_komodo_name(name: &str) -> String {
name.to_lowercase().replace([' ', '.'], "_")
name
.to_lowercase()
.replace([' ', '.'], "_")
.trim()
.to_string()
}
/// Unix timestamp in milliseconds as i64
@@ -620,7 +624,7 @@ impl CloneArgs {
access_token: Option<&str>,
) -> anyhow::Result<String> {
let access_token_at = match &access_token {
Some(token) => format!("{token}@"),
Some(token) => format!("token:{token}@"),
None => String::new(),
};
let protocol = if self.https { "https" } else { "http" };

View File

@@ -11,7 +11,7 @@ use typeshare::typeshare;
use super::{
docker::container::ContainerListItem,
resource::{Resource, ResourceListItem, ResourceQuery},
to_komodo_name, FileContents,
to_komodo_name, FileContents, SystemCommand,
};
#[typeshare]
@@ -284,6 +284,12 @@ pub struct StackConfig {
#[builder(default)]
pub commit: String,
/// By default, the Stack will `git pull` the repo after it is first cloned.
/// If this option is enabled, the repo folder will be deleted and recloned instead.
#[serde(default)]
#[builder(default)]
pub reclone: bool,
/// Whether incoming webhooks actually trigger action.
#[serde(default = "default_webhook_enabled")]
#[builder(default = "default_webhook_enabled()")]
@@ -312,6 +318,11 @@ pub struct StackConfig {
#[builder(default)]
pub registry_account: String,
/// The optional command to run before the Stack is deployed.
#[serde(default)]
#[builder(default)]
pub pre_deploy: SystemCommand,
/// The extra arguments to pass after `docker compose up -d`.
/// If empty, no extra arguments will be passed.
#[serde(default)]
@@ -410,6 +421,7 @@ impl Default for StackConfig {
file_contents: Default::default(),
auto_pull: default_auto_pull(),
ignore_services: Default::default(),
pre_deploy: Default::default(),
extra_args: Default::default(),
environment: Default::default(),
env_file_path: default_env_file_path(),
@@ -421,6 +433,7 @@ impl Default for StackConfig {
repo: Default::default(),
branch: default_branch(),
commit: Default::default(),
reclone: Default::default(),
git_account: Default::default(),
webhook_enabled: default_webhook_enabled(),
webhook_secret: Default::default(),

View File

@@ -27,8 +27,8 @@ pub fn parse_key_value_list(
.trim_start_matches('-')
.trim();
// Remove wrapping quotes (from yaml list)
let line = if let Some(line) = line.strip_prefix('"') {
line.strip_suffix('"').unwrap_or(line)
let line = if let Some(line) = line.strip_prefix(['"', '\'']) {
line.strip_suffix(['"', '\'']).unwrap_or(line)
} else {
line
};
@@ -43,11 +43,12 @@ pub fn parse_key_value_list(
.map(|(key, value)| {
let value = value.trim();
// Remove wrapping quotes around value
if let Some(value) = value.strip_prefix('"') {
value.strip_suffix('"').unwrap_or(value)
} else {
value
};
let value =
if let Some(value) = value.strip_prefix(['"', '\'']) {
value.strip_suffix(['"', '\'']).unwrap_or(value)
} else {
value
};
(key.trim().to_string(), value.trim().to_string())
})?;
anyhow::Ok((key, value))

View File

@@ -438,8 +438,6 @@ export interface BuildConfig {
webhook_secret?: string;
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/** Configuration for the registry to push the built image to. */
image_registry?: ImageRegistryConfig;
/**
* The path of the docker build context relative to the root of the repo.
* Default: "." (the root of the repo).
@@ -447,6 +445,8 @@ export interface BuildConfig {
build_path: string;
/** The path of the dockerfile relative to the build path. */
dockerfile_path: string;
/** Configuration for the registry to push the built image to. */
image_registry?: ImageRegistryConfig;
/** Whether to skip secret interpolation in the build_args. */
skip_secret_interp?: boolean;
/** Whether to use buildx to build (eg `docker buildx build ...`) */
@@ -512,6 +512,8 @@ export interface BuildListItemInfo {
builder_id: string;
/** The git provider domain */
git_provider: string;
/** The image registry domain */
image_registry_domain: string;
/** The repo used as the source of the build */
repo: string;
/** The branch of the repo */
@@ -2365,6 +2367,11 @@ export interface StackConfig {
branch: string;
/** Optionally set a specific commit hash. */
commit?: string;
/**
* By default, the Stack will `git pull` the repo after it is first cloned.
* If this option is enabled, the repo folder will be deleted and recloned instead.
*/
reclone?: boolean;
/** Whether incoming webhooks actually trigger action. */
webhook_enabled: boolean;
/**
@@ -2378,6 +2385,8 @@ export interface StackConfig {
registry_provider?: string;
/** Used with `registry_provider` to login to a registry before docker compose up. */
registry_account?: string;
/** The optional command to run before the Stack is deployed. */
pre_deploy?: SystemCommand;
/**
* The extra arguments to pass after `docker compose up -d`.
* If empty, no extra arguments will be passed.

View File

@@ -6,7 +6,7 @@ Komodo just needs a bit of information in order to build your image.
Komodo supports cloning repos over http/s, from any provider that supports cloning private repos using `git clone https://<Token>@git-provider.net/<Owner>/<Repo>`.
Accounts / access tokens can be configured in either the [core config](../setup/advanced.mdx#mount-a-config-file)
or in the [periphery config](../setup/connect-servers.mdx#manual-install-steps---binaries).
or in the [periphery config](../connect-servers.mdx#manual-install-steps---binaries).
### Repo configuration
To specify the git repo to build, just give it the name of the repo and the branch under *repo config*. The name is given like `mbecker20/komodo`, it includes the username / organization that owns the repo.

View File

@@ -2,12 +2,10 @@
Connecting a server to Komodo has 2 steps:
1. Install the Periphery agent on the server
2. Adding the server to Komodo via the core API
1. Install the Periphery agent on the server (either binary or container).
2. Add the server to Komodo via the Core API / UI.
Once step 1. is complete, you can just connect the server to Komodo Core from the UI.
## Install
## Install Periphery
You can install Periphery as a systemd managed process, run it as a [docker container](https://github.com/mbecker20/komodo/pkgs/container/periphery), or do whatever you want with the binary.

View File

@@ -16,7 +16,7 @@ const sidebars: SidebarsConfig = {
"resources",
{
type: "category",
label: "Setup Komodo",
label: "Setup Komodo Core",
link: {
type: "doc",
id: "setup/index",
@@ -26,9 +26,9 @@ const sidebars: SidebarsConfig = {
"setup/postgres",
"setup/sqlite",
"setup/advanced",
"setup/connect-servers",
],
},
"connect-servers",
{
type: "category",
label: "Build Images",

View File

@@ -1,27 +1,26 @@
# React + TypeScript + Vite
# Komodo Frontend
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Komodo JS stack uses Yarn + Vite + React + Tailwind + shadcn/ui
Currently, two official plugins are available:
## Setup Dev Environment
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
The frontend depends on the local package `@komodo/client` located at `/client/core/ts`.
This must first be built and prepared for yarn link.
## Expanding the ESLint configuration
The following command should setup everything up (run with /frontend as working directory):
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
```sh
cd ../client/core/ts && yarn && yarn build && yarn link && \
cd ../../../frontend && yarn link @komodo/client && yarn
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
You can make a new file `.env.development` (gitignored) which holds:
```sh
VITE_KOMODO_HOST=https://demo.komo.do
```
You can point it to any Komodo host you like, including the demo.
Now you can start the dev frontend server:
```sh
yarn dev
```

View File

@@ -4,6 +4,7 @@ import {
ConfirmUpdate,
} from "@components/config/util";
import { Section } from "@components/layouts";
import { MonacoLanguage } from "@components/monaco";
import { Types } from "@komodo/client";
import { cn } from "@lib/utils";
import { Button } from "@ui/button";
@@ -31,6 +32,7 @@ export const ConfigLayout = <
onReset,
selector,
titleOther,
file_contents_language,
}: {
original: T;
config: Partial<T>;
@@ -40,6 +42,7 @@ export const ConfigLayout = <
onReset: () => void;
selector?: ReactNode;
titleOther?: ReactNode;
file_contents_language?: MonacoLanguage;
}) => {
const titleProps = titleOther
? { titleOther }
@@ -74,6 +77,7 @@ export const ConfigLayout = <
content={config}
onConfirm={onConfirm}
disabled={disabled}
file_contents_language={file_contents_language}
/>
)}
</div>
@@ -120,6 +124,7 @@ export const Config = <T,>({
components,
selector,
titleOther,
file_contents_language,
}: {
resource_id: string;
resource_type: Types.ResourceTarget["type"];
@@ -134,6 +139,7 @@ export const Config = <T,>({
string, // sidebar key
ConfigComponent<T>[] | false | undefined
>;
file_contents_language?: MonacoLanguage;
}) => {
// let component_keys = keys(components);
// const [_show, setShow] = useLocalStorage(
@@ -164,6 +170,7 @@ export const Config = <T,>({
}}
onReset={() => set({})}
selector={selector}
file_contents_language={file_contents_language}
>
<div className="flex gap-6">
<div className="hidden xl:block relative pr-6 border-r">
@@ -186,7 +193,7 @@ export const Config = <T,>({
key={section + item.label}
>
<Button
variant="outline"
variant="secondary"
className="justify-end w-full"
size="sm"
>

View File

@@ -46,7 +46,11 @@ import {
soft_text_color_class_by_intention,
text_color_class_by_intention,
} from "@lib/color";
import { MonacoDiffEditor, MonacoEditor } from "@components/monaco";
import {
MonacoDiffEditor,
MonacoEditor,
MonacoLanguage,
} from "@components/monaco";
export const ConfigItem = ({
label,
@@ -398,6 +402,7 @@ export const AccountSelector = ({
provider,
selected,
onSelect,
placeholder = "Select Account",
}: {
disabled: boolean;
type: "Server" | "Builder" | "None";
@@ -406,6 +411,7 @@ export const AccountSelector = ({
provider: string;
selected: string | undefined;
onSelect: (id: string) => void;
placeholder?: string;
}) => {
const [db_request, config_request]:
| ["ListGitProviderAccounts", "ListGitProvidersFromConfig"]
@@ -447,7 +453,7 @@ export const AccountSelector = ({
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectValue placeholder="Select Account" />
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
<SelectItem value={"Empty"}>None</SelectItem>
@@ -471,12 +477,16 @@ export const AccountSelectorConfig = (params: {
provider: string;
selected: string | undefined;
onSelect: (id: string) => void;
placeholder: string;
placeholder?: string;
description?: string;
}) => {
return (
<ConfigItem
label="Account"
description="Select the account used to log in to the provider"
description={
params.description ??
"Select the account used to log in to the provider"
}
>
<AccountSelector {...params} />
</ConfigItem>
@@ -569,6 +579,8 @@ interface ConfirmUpdateProps<T> {
content: Partial<T>;
onConfirm: () => void;
disabled: boolean;
language?: MonacoLanguage;
file_contents_language?: MonacoLanguage;
}
export function ConfirmUpdate<T>({
@@ -576,6 +588,8 @@ export function ConfirmUpdate<T>({
content,
onConfirm,
disabled,
language,
file_contents_language,
}: ConfirmUpdateProps<T>) {
const [open, set] = useState(false);
useCtrlKeyListener("s", () => {
@@ -608,6 +622,8 @@ export function ConfirmUpdate<T>({
_key={key as any}
val={val as any}
previous={previous}
language={language}
file_contents_language={file_contents_language}
/>
))}
</div>
@@ -629,23 +645,24 @@ function ConfirmUpdateItem<T>({
_key,
val: _val,
previous,
language,
file_contents_language,
}: {
_key: keyof T;
val: T[keyof T];
previous: T;
language?: MonacoLanguage;
file_contents_language?: MonacoLanguage;
}) {
const [show, setShow] = useState(true);
const val =
typeof _val === "string"
? _key === "environment" ||
_key === "build_args" ||
_key === "secret_args"
? _val
.split("\n")
.filter((line) => !line.startsWith("#"))
.map((line) => line.split(" #")[0])
.join("\n")
: _val
? _val
: Array.isArray(_val)
? _val.length > 0 &&
["string", "number", "boolean"].includes(typeof _val[0])
? JSON.stringify(_val)
: JSON.stringify(_val, null, 2)
: JSON.stringify(_val, null, 2);
const prev_val =
typeof previous[_key] === "string"
@@ -653,7 +670,12 @@ function ConfirmUpdateItem<T>({
: _key === "environment" ||
_key === "build_args" ||
_key === "secret_args"
? env_to_text(previous[_key] as any) ?? ""
? env_to_text(previous[_key] as any) ?? "" // For backward compat with 1.14
: Array.isArray(previous[_key])
? previous[_key].length > 0 &&
["string", "number", "boolean"].includes(typeof previous[_key][0])
? JSON.stringify(previous[_key])
: JSON.stringify(previous[_key], null, 2)
: JSON.stringify(previous[_key], null, 2);
const showDiff =
val?.includes("\n") ||
@@ -676,7 +698,16 @@ function ConfirmUpdateItem<T>({
<MonacoDiffEditor
original={prev_val}
modified={val}
language="toml"
language={
language ??
(["environment", "build_args", "secret_args"].includes(
_key as string
)
? "key_value"
: _key === "file_contents"
? file_contents_language
: "json")
}
/>
) : (
<pre style={{ minHeight: 0 }}>

View File

@@ -203,13 +203,13 @@ export const NewLayout = ({
entityType,
children,
enabled,
onSuccess,
onConfirm,
onOpenChange,
}: {
entityType: string;
children: ReactNode;
enabled: boolean;
onSuccess: () => Promise<unknown>;
onConfirm: () => Promise<unknown>;
onOpenChange?: (open: boolean) => void;
}) => {
const [open, set] = useState(false);
@@ -237,7 +237,7 @@ export const NewLayout = ({
variant="outline"
onClick={async () => {
setLoading(true);
await onSuccess();
await onConfirm();
setLoading(false);
set(false);
}}

View File

@@ -115,9 +115,17 @@ export const BuildComponents: RequiredResourceComponents = {
New: () => {
const user = useUser().data;
const builders = useRead("ListBuilders", {}).data;
if (!user) return null;
if (!user.admin && !user.create_build_permissions) return null;
return <NewResource type="Build" />;
return (
<NewResource
type="Build"
builder_id={
builders && builders.length === 1 ? builders[0].id : undefined
}
/>
);
},
Table: ({ resources }) => (

View File

@@ -74,7 +74,7 @@ export const BuilderComponents: RequiredResourceComponents = {
return (
<NewLayout
entityType="Builder"
onSuccess={async () => {
onConfirm={async () => {
if (!type) return;
const id = (await mutateAsync({ name, config: { type, params: {} } }))
._id?.$oid!;

View File

@@ -119,7 +119,11 @@ export const ResourceSelector = ({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="secondary" className="flex justify-start gap-2 w-fit max-w-[200px]" disabled={disabled}>
<Button
variant="secondary"
className="flex justify-start gap-2 w-fit max-w-[200px]"
disabled={disabled}
>
{name || `Select ${type}`}
{!disabled && <ChevronsUpDown className="w-3 h-3" />}
</Button>
@@ -258,16 +262,19 @@ export const NewResource = ({
type,
readable_type,
server_id,
builder_id,
build_id,
name: _name = "",
}: {
type: UsableResource;
readable_type?: string;
server_id?: string;
builder_id?: string;
build_id?: string;
name?: string;
}) => {
const nav = useNavigate();
const { toast } = useToast();
const { mutateAsync } = useWrite(`Create${type}`);
const [name, setName] = useState(_name);
const type_display =
@@ -276,7 +283,7 @@ export const NewResource = ({
: type === "ResourceSync"
? "resource-sync"
: type.toLowerCase();
const config: Types._PartialDeploymentConfig =
const config: Types._PartialDeploymentConfig | Types._PartialRepoConfig =
type === "Deployment"
? {
server_id,
@@ -287,15 +294,19 @@ export const NewResource = ({
: type === "Stack"
? { server_id }
: type === "Repo"
? { server_id }
? { server_id, builder_id }
: type === "Build"
? { builder_id }
: {};
const onConfirm = async () => {
if (!name) toast({ title: "Name cannot be empty" });
const id = (await mutateAsync({ name, config }))._id?.$oid!;
nav(`/${usableResourcePath(type)}/${id}`);
};
return (
<NewLayout
entityType={readable_type ?? type}
onSuccess={async () => {
const id = (await mutateAsync({ name, config }))._id?.$oid!;
nav(`/${usableResourcePath(type)}/${id}`);
}}
onConfirm={onConfirm}
enabled={!!name}
onOpenChange={() => setName("")}
>
@@ -305,6 +316,12 @@ export const NewResource = ({
placeholder={`${type_display}-name`}
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (!name) return;
if (e.key === "Enter") {
onConfirm();
}
}}
/>
</div>
</NewLayout>

View File

@@ -2,6 +2,7 @@ import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@komodo/client";
import { ReactNode, useState } from "react";
import {
AccountSelectorConfig,
AddExtraArgMenu,
ConfigItem,
ConfigList,
@@ -19,6 +20,7 @@ import {
DefaultTerminationSignal,
TerminationTimeout,
} from "./components/term-signal";
import { extract_registry_domain } from "@lib/utils";
export const DeploymentConfig = ({
id,
@@ -31,6 +33,7 @@ export const DeploymentConfig = ({
target: { type: "Deployment", id },
}).data;
const config = useRead("GetDeployment", { deployment: id }).data?.config;
const builds = useRead("ListBuilds", {}).data;
const global_disabled =
useRead("GetCoreInfo", {}).data?.ui_write_disabled ?? false;
const [update, set] = useState<Partial<Types.DeploymentConfig>>({});
@@ -100,6 +103,37 @@ export const DeploymentConfig = ({
image: (value, set) => (
<ImageConfig image={value} set={set} disabled={disabled} />
),
image_registry_account: (account, set) => {
const image = update.image ?? config.image;
const provider =
image?.type === "Image" && image.params.image
? extract_registry_domain(image.params.image)
: image?.type === "Build" && image.params.build_id
? builds?.find((b) => b.id === image.params.build_id)?.info
.image_registry_domain
: undefined;
return (
<AccountSelectorConfig
id={update.server_id ?? config.server_id ?? undefined}
type="Server"
account_type="docker"
provider={provider ?? "docker.io"}
selected={account}
onSelect={(image_registry_account) =>
set({ image_registry_account })
}
disabled={disabled}
placeholder={
image?.type === "Build" ? "Same as Build" : undefined
}
description={
image?.type === "Build"
? "Select an alternate account used to log in to the provider"
: undefined
}
/>
);
},
redeploy_on_build: (update.image?.type ?? config.image?.type) ===
"Build" && {
description: "Automatically redeploy when the image is built.",
@@ -182,6 +216,53 @@ export const DeploymentConfig = ({
),
},
},
{
label: "Restart",
labelHidden: true,
components: {
restart: (value, set) => (
<RestartModeSelector
selected={value}
set={set}
disabled={disabled}
/>
),
},
},
],
advanced: [
{
label: "Command",
labelHidden: true,
components: {
command: (value, set) => (
<ConfigItem
label="Command"
description={
<div className="flex flex-row flex-wrap gap-2">
<div>Replace the CMD, or extend the ENTRYPOINT.</div>
<Link
to="https://docs.docker.com/engine/reference/run/#commands-and-arguments"
target="_blank"
className="text-primary"
>
See docker docs.
{/* <Button variant="link" className="p-0">
</Button> */}
</Link>
</div>
}
>
<MonacoEditor
value={value}
language="shell"
onValueChange={(command) => set({ command })}
readOnly={disabled}
/>
</ConfigItem>
),
},
},
{
label: "Labels",
description: "Attach --labels to the container.",
@@ -197,19 +278,6 @@ export const DeploymentConfig = ({
),
},
},
{
label: "Restart",
labelHidden: true,
components: {
restart: (value, set) => (
<RestartModeSelector
selected={value}
set={set}
disabled={disabled}
/>
),
},
},
{
label: "Extra Args",
labelHidden: true,
@@ -255,38 +323,6 @@ export const DeploymentConfig = ({
),
},
},
{
label: "Command",
labelHidden: true,
components: {
command: (value, set) => (
<ConfigItem
label="Command"
description={
<div className="flex flex-row flex-wrap gap-2">
<div>Replace the CMD, or extend the ENTRYPOINT.</div>
<Link
to="https://docs.docker.com/engine/reference/run/#commands-and-arguments"
target="_blank"
className="text-primary"
>
See docker docs.
{/* <Button variant="link" className="p-0">
</Button> */}
</Link>
</div>
}
>
<MonacoEditor
value={value}
language="shell"
onValueChange={(command) => set({ command })}
readOnly={disabled}
/>
</ConfigItem>
),
},
},
{
label: "Termination",
boldLabel: false,

View File

@@ -1,7 +1,7 @@
import { useLocalStorage, useRead } from "@lib/hooks";
import { Types } from "@komodo/client";
import { RequiredResourceComponents } from "@types";
import { AlertTriangle, HardDrive, Rocket, Server } from "lucide-react";
import { HardDrive, Rocket, Server } from "lucide-react";
import { cn } from "@lib/utils";
import { useServer } from "../server";
import {
@@ -22,7 +22,6 @@ import { DeleteResource, NewResource, ResourceLink } from "../common";
import { RunBuild } from "../build/actions";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
import { DeploymentConfig } from "./config";
import { Link } from "react-router-dom";
import { DashboardPieChart } from "@pages/home/dashboard";
import { ResourcePageHeader, StatusBadge } from "@components/util";
@@ -145,9 +144,21 @@ export const DeploymentComponents: RequiredResourceComponents = {
);
},
New: ({ server_id, build_id }) => (
<NewResource type="Deployment" server_id={server_id} build_id={build_id} />
),
New: ({ server_id: _server_id, build_id }) => {
const servers = useRead("ListServers", {}).data;
const server_id = _server_id
? _server_id
: servers && servers.length === 1
? servers[0].id
: undefined;
return (
<NewResource
type="Deployment"
server_id={server_id}
build_id={build_id}
/>
);
},
Table: ({ resources }) => {
return (
@@ -192,17 +203,6 @@ export const DeploymentComponents: RequiredResourceComponents = {
</div>
);
},
Alerts: ({ id }) => {
return (
<Link
to={`/deployments/${id}/alerts`}
className="flex gap-2 items-center"
>
<AlertTriangle className="w-4 h-4" />
Alerts
</Link>
);
},
},
Actions: {

View File

@@ -57,6 +57,7 @@ export const ResourceSyncConfig = ({
onSave={async () => {
await mutateAsync({ id, config: update });
}}
file_contents_language="toml"
components={{
"": [
{

View File

@@ -58,7 +58,7 @@ export const ServerTemplateComponents: RequiredResourceComponents = {
return (
<NewLayout
entityType="Server Template"
onSuccess={async () => {
onConfirm={async () => {
if (!type) return;
const id = (await mutateAsync({ name, config: { type, params: {} } }))
._id?.$oid!;

View File

@@ -6,6 +6,7 @@ import {
ConfigList,
InputList,
ProviderSelectorConfig,
SystemCommand,
} from "@components/config/util";
import { Types } from "@komodo/client";
import { useInvalidate, useLocalStorage, useRead, useWrite } from "@lib/hooks";
@@ -233,10 +234,24 @@ export const StackConfig = ({
placeholder: "Compose project name",
boldLabel: true,
description:
"Optionally set a different compose project name. It should match the compose project name on your host.",
"Optionally set a different compose project name. If importing existing stack, this should match the compose project name on your host.",
},
},
},
{
label: "Pre Deploy",
description:
"Execute a shell command before running docker compose up. The 'path' is relative to the Run Directory",
components: {
pre_deploy: (value, set) => (
<SystemCommand
value={value}
set={(value) => set({ pre_deploy: value })}
disabled={disabled}
/>
),
},
},
{
label: "Extra Args",
labelHidden: true,
@@ -467,6 +482,10 @@ export const StackConfig = ({
description:
"Switch to a specific hash after cloning the branch.",
},
reclone: {
description:
"Delete the repo folder and clone it again, instead of using 'git pull'.",
},
},
},
{
@@ -718,6 +737,7 @@ export const StackConfig = ({
await mutateAsync({ id, config: update });
}}
components={components}
file_contents_language="yaml"
/>
);
};

View File

@@ -151,7 +151,15 @@ export const StackComponents: RequiredResourceComponents = {
);
},
New: ({ server_id }) => <NewResource type="Stack" server_id={server_id} />,
New: ({ server_id: _server_id }) => {
const servers = useRead("ListServers", {}).data;
const server_id = _server_id
? _server_id
: servers && servers.length === 1
? servers[0].id
: undefined;
return <NewResource type="Stack" server_id={server_id} />;
},
Table: ({ resources }) => (
<StackTable stacks={resources as Types.StackListItem[]} />

View File

@@ -80,7 +80,7 @@ export const StackInfo = ({
</CardHeader>
</Card>
)}
{/* Update deployed contents with diff */}
{/* {!is_down && deployed_contents.length > 0 && (
<Card>
@@ -189,6 +189,7 @@ export const StackInfo = ({
}
}}
disabled={!edits[content.path]}
language="yaml"
/>
</div>
)}

View File

@@ -17,7 +17,7 @@ export const NewUserGroup = () => {
return (
<NewLayout
entityType="User Group"
onSuccess={() => mutateAsync({ name })}
onConfirm={() => mutateAsync({ name })}
enabled={!!name}
onOpenChange={() => setName("")}
>
@@ -46,7 +46,7 @@ export const NewServiceUser = () => {
return (
<NewLayout
entityType="Service User"
onSuccess={() => mutateAsync({ username, description: "" })}
onConfirm={() => mutateAsync({ username, description: "" })}
enabled={!!username}
onOpenChange={() => setUsername("")}
>

View File

@@ -244,3 +244,13 @@ export const is_service_user = (user_id: string) => {
user_id === "Repo Manager"
);
};
export const extract_registry_domain = (image_name: string) => {
if (!image_name) return "docker.io";
const maybe_domain = image_name.split("/")[0];
if (maybe_domain.includes(".")) {
return maybe_domain
} else {
return "docker.io"
}
}

View File

@@ -100,7 +100,7 @@ where
};
if let Some(command) = args.on_clone {
if !command.path.is_empty() && !command.command.is_empty() {
if !command.command.is_empty() {
let on_clone_path = repo_dir.join(&command.path);
if let Some(secrets) = secrets {
let (full_command, mut replacers) =
@@ -154,7 +154,7 @@ where
}
}
if let Some(command) = args.on_pull {
if !command.path.is_empty() && !command.command.is_empty() {
if !command.command.is_empty() {
let on_pull_path = repo_dir.join(&command.path);
if let Some(secrets) = secrets {
let (full_command, mut replacers) =

View File

@@ -30,6 +30,10 @@ docker compose -p komodo-dev -f test.compose.yaml build"""
description = "runs core --release pointing to test.core.config.toml"
cmd = "KOMODO_CONFIG_PATH=test.core.config.toml cargo run -p komodo_core --release"
[test-periphery]
description = "runs periphery --release pointing to test.periphery.config.toml"
cmd = "PERIPHERY_CONFIG_PATH=test.periphery.config.toml cargo run -p komodo_periphery --release"
[docsite-start]
path = "docsite"
cmd = "yarn start"
@@ -38,5 +42,5 @@ cmd = "yarn start"
path = "docsite"
cmd = "yarn deploy"
[rustdoc-server]
cmd = "cargo watch -s 'cargo doc --no-deps -p komodo_client' & http --quiet target/doc"
# [rustdoc-server]
# cmd = "cargo watch -s 'cargo doc --no-deps -p komodo_client' & http --quiet target/doc"

View File

@@ -25,9 +25,6 @@ Will install to paths:
*Note*. The user running periphery must be a member of the docker group, in order to use the docker cli without sudo.
*Note*. Ensure the user running periphery has write access to the configure [repo directory](https://github.com/mbecker20/komodo/blob/main/config/periphery.config.toml).
This allows periphery to clone repos and write compose files.
```sh
curl -sSL https://raw.githubusercontent.com/mbecker20/komodo/main/scripts/setup-periphery.py | python3 - --user
```
@@ -35,4 +32,17 @@ curl -sSL https://raw.githubusercontent.com/mbecker20/komodo/main/scripts/setup-
Will install to paths:
- periphery (binary) -> `$HOME/.local/bin`
- periphery.service -> `$HOME/.config/systemd/user/periphery.service`
- periphery.config.toml -> `$HOME/.config/komodo/periphery.config.toml`
- periphery.config.toml -> `$HOME/.config/komodo/periphery.config.toml`
*Note*. Ensure the user running periphery has write permissions to the configured folders `repo_dir`, `stack_dir`, and `ssl_key_file` / `ssl_cert_file` parent folder.
This allows periphery to clone repos, write compose files, and generate ssl certs.
For example in `periphery.config.toml`, running under `ubuntu` user:
```toml
repo_dir = "/home/ubuntu/.komodo/repos"
stack_dir = "/home/ubuntu/.komodo/stacks"
ssl_enabled = true
ssl_key_file = "/home/ubuntu/.komodo/ssl/key.pem"
ssl_cert_file = "/home/ubuntu/.komodo/ssl/cert.pem"
```

View File

@@ -1,14 +1,18 @@
###########################
# 🦎 KOMODO CORE CONFIG 🦎 #
###########################
title = "Komodo Test"
host = "http://localhost:9121"
port = 9121
passkey = "a_random_passkey"
repo_directory = "/Users/max/komodo-repos"
repo_directory = ".core-repos"
frontend_path = "frontend/dist"
first_server = "http://localhost:8121"
disable_confirm_dialog = true
enable_new_users = true
# database.address = "localhost:27017"
# database.address = "ferretdb.komodo.orb.local"
# database.address = "ferretdb.komodo-dev.orb.local"
database.address = "test-ferretdb.orb.local"
# database.username = "admin"
# database.password = "admin"
@@ -18,16 +22,20 @@ local_auth = true
jwt_secret = "your_random_secret"
jwt_ttl = "2-wk"
oidc_enabled = true
oidc_provider = "http://server.authentik2.orb.local:9000/application/o/komodo"
oidc_client_id = "komodo"
oidc_client_secret = "komodo"
oidc_enabled = false
# oidc_provider = "http://server.authentik2.orb.local:9000/application/o/komodo"
# oidc_client_id = "komodo"
# oidc_client_secret = "komodo"
webhook_secret = "a_random_webhook_secret"
stack_poll_interval = "1-min"
sync_poll_interval = "1-min"
build_poll_interval = "1-min"
repo_poll_interval = "1-min"
monitoring_interval = "5-sec"
resource_poll_interval = "1-min"
###########
# LOGGING #
###########
logging.level = "info"
logging.stdio = "standard"
logging.otlp_endpoint = "http://tempo.grafana.orb.local:4317"
logging.opentelemetry_service_name = "Komodo"

View File

@@ -0,0 +1,24 @@
################################
# 🦎 KOMODO PERIPHERY CONFIG 🦎 #
################################
port = 8121
repo_dir = ".repos"
stack_dir = ".stacks"
stats_polling_rate = "1-sec"
legacy_compose_cli = false
include_disk_mounts = []
############
# Security #
############
ssl_enabled = false
ssl_key_file = ".ssl/key.pem"
ssl_cert_file = ".ssl/cert.pem"
###########
# LOGGING #
###########
logging.level = "info"
logging.stdio = "standard"
logging.otlp_endpoint = "http://tempo.grafana.orb.local:4317"
logging.opentelemetry_service_name = "Periphery"