Compare commits

...
4 Commits
Author SHA1 Message Date
EtienneandGitHub f45205011e Fix compose.env (#415)
Flip "stacks" and "repos" paths
2025-04-15 12:11:48 -07:00
5b211fb8f0 1.17.1 (#383)
* interpolate into slack / discord url

* fix js client docs

* js client should be type: module

* click table tags to toggle tag filter

* git token helper early return when empty provider

* reorder Stack fields

* action support interpolation doc

* Fix for the {account} login fails when the account name contains '$' (#385)

* GetDeploymentsSummary (#386)

* added GetDockerContainersSummary endpoint in rust api

* typescript stuff

* more autogenned typescript stuff

* fixed comments to be in line with actual behaviour

* fixed ReadResponse for GetDockerContainersSummary

* I64 -> u32 for response types

* more accurate error context

* backend for build files on host / ui defined

* core api supports non repo based build

* Ntfy as Alerter (#404)

* add ntfy alerter

* add ntfy alerter

* add ntfy alerter

---------

Co-authored-by: GFXSpeed <github@schafhaupt.com>

* Improve docs around running periphery in a container (#402)

* Add ports that need to be exposed if periphery is remote from core

* Spelling: overide -> override

* Add info about using a custom config file when running periphery in docker

* clean up ntfy alerter

* clean up ResourceSyncConfig

* update build cache after create / update

* refresh stack cache log

* Build UI Defined / file on host frontend

* update clap + rustls

* don't cleanup build files

* clean up dockerfile full path

* update BuildListItemInfo + UI table

* add Other Resources page

* add 5 second ws reconnection timeout

* Make listener address configurable & Add support for IPv6 (#405)

* Make listener address configurable

* Make listener address configurable for periphery

* rename listener_address -> bind_ip

---------

Co-authored-by: Jacky Fong <hello@huzky.dev>
Co-authored-by: Alex Shore <kntrllr@gmail.com>
Co-authored-by: Niklas <108990063+GFXSpeed@users.noreply.github.com>
Co-authored-by: GFXSpeed <github@schafhaupt.com>
Co-authored-by: theRAAPster <theraapster@gmail.com>
Co-authored-by: Daniel Vos <45820840+vosdev@users.noreply.github.com>
2025-04-14 14:48:12 -07:00
Justin GoetteandGitHub 3f04614303 docs: Typo (#392)
FerretDB vs. FerritDB
2025-04-02 00:05:29 -07:00
mbecker20 4c3210f70a update dashboard screenshots with colored tags 2025-03-23 23:17:33 -07:00
93 changed files with 3149 additions and 1916 deletions
Generated
+531 -511
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
version = "1.17.0"
version = "1.17.1"
edition = "2024"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -30,7 +30,7 @@ git = { path = "lib/git" }
# MOGH
run_command = { version = "0.0.6", features = ["async_tokio"] }
serror = { version = "0.5.0", default-features = false }
slack = { version = "0.3.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
slack = { version = "0.4.0", package = "slack_client_rs", default-features = false, features = ["rustls"] }
derive_default_builder = "0.1.8"
derive_empty_traits = "0.1.0"
merge_config_files = "0.1.5"
@@ -80,7 +80,7 @@ opentelemetry = "0.29.0"
tracing = "0.1.41"
# CONFIG
clap = { version = "4.5.32", features = ["derive"] }
clap = { version = "4.5.36", features = ["derive"] }
dotenvy = "0.15.7"
envy = "0.4.2"
@@ -92,7 +92,7 @@ urlencoding = "2.1.3"
nom_pem = "4.0.0"
bcrypt = "0.17.0"
base64 = "0.22.1"
rustls = "0.23.25"
rustls = "0.23.26"
hmac = "0.12.1"
sha2 = "0.10.8"
rand = "0.9.0"
@@ -100,11 +100,11 @@ hex = "0.4.3"
# SYSTEM
bollard = "0.18.1"
sysinfo = "0.33.1"
sysinfo = "0.34.2"
# CLOUD
aws-config = "1.6.0"
aws-sdk-ec2 = "1.118.1"
aws-config = "1.6.1"
aws-sdk-ec2 = "1.121.1"
aws-credential-types = "1.2.2"
# MISC
+25 -1
View File
@@ -192,7 +192,31 @@ pub async fn send_alert(
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
send_message(url, &content).await?;
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
let mut url_interpolated = url.to_string();
// interpolate variables and secrets into the url
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut url_interpolated,
&mut global_replacers,
&mut secret_replacers,
)?;
send_message(&url_interpolated, &content)
.await
.map_err(|e| {
let replacers =
secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {}",
sanitized_error
))
})?;
}
Ok(())
}
+6
View File
@@ -19,6 +19,7 @@ use crate::{config::core_config, state::db_client};
mod discord;
mod slack;
mod ntfy;
#[instrument(level = "debug")]
pub async fn send_alerts(alerts: &[Alert]) {
@@ -128,6 +129,11 @@ pub async fn send_alert_to_alerter(
)
})
}
AlerterEndpoint::Ntfy(NtfyAlerterEndpoint { url }) => {
ntfy::send_alert(url, alert).await.with_context(|| {
format!("Failed to send alert to ntfy Alerter {}", alerter.name)
})
}
}
}
+248
View File
@@ -0,0 +1,248 @@
use std::sync::OnceLock;
use super::*;
#[instrument(level = "debug")]
pub async fn send_alert(
url: &str,
alert: &Alert,
) -> anyhow::Result<()> {
let level = fmt_level(alert.level);
let content = match &alert.data {
AlertData::Test { id, name } => {
let link = resource_link(ResourceTargetVariant::Alerter, id);
format!(
"{level} | If you see this message, then Alerter {} is working\n{link}",
name,
)
}
AlertData::ServerUnreachable {
id,
name,
region,
err,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
match alert.level {
SeverityLevel::Ok => {
format!(
"{level} | {}{} is now reachable\n{link}",
name, region
)
}
SeverityLevel::Critical => {
let err = err
.as_ref()
.map(|e| format!("\nerror: {:#?}", e))
.unwrap_or_default();
format!(
"{level} | {}{} is unreachable ❌\n{link}{err}",
name, region
)
}
_ => unreachable!(),
}
}
AlertData::ServerCpu {
id,
name,
region,
percentage,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
format!(
"{level} | {}{} cpu usage at {percentage:.1}%\n{link}",
name, region,
)
}
AlertData::ServerMem {
id,
name,
region,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} memory usage at {percentage:.1}%💾\n\nUsing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region,
)
}
AlertData::ServerDisk {
id,
name,
region,
path,
used_gb,
total_gb,
} => {
let region = fmt_region(region);
let link = resource_link(ResourceTargetVariant::Server, id);
let percentage = 100.0 * used_gb / total_gb;
format!(
"{level} | {}{} disk usage at {percentage:.1}%💿\nmount point: {:?}\nusing {used_gb:.1} GiB / {total_gb:.1} GiB\n{link}",
name, region, path,
)
}
AlertData::ContainerStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
let to_state = fmt_docker_container_state(to);
format!(
"📦Deployment {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::DeploymentImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} has an update available\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::DeploymentAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
image,
} => {
let link = resource_link(ResourceTargetVariant::Deployment, id);
format!(
"⬆ Deployment {} was updated automatically\nserver: {}\nimage: {}\n{link}",
name, server_name, image,
)
}
AlertData::StackStateChange {
id,
name,
server_id: _server_id,
server_name,
from,
to,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let to_state = fmt_stack_state(to);
format!(
"🥞 Stack {} is now {}\nserver: {}\nprevious: {}\n{link}",
name, to_state, server_name, from,
)
}
AlertData::StackImageUpdateAvailable {
id,
name,
server_id: _server_id,
server_name,
service,
image,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
format!(
"⬆ Stack {} has an update available\nserver: {}\nservice: {}\nimage: {}\n{link}",
name, server_name, service, image,
)
}
AlertData::StackAutoUpdated {
id,
name,
server_id: _server_id,
server_name,
images,
} => {
let link = resource_link(ResourceTargetVariant::Stack, id);
let images_label =
if images.len() > 1 { "images" } else { "image" };
let images_str = images.join(", ");
format!(
"⬆ Stack {} was updated automatically ⏫\nserver: {}\n{}: {}\n{link}",
name, server_name, images_label, images_str,
)
}
AlertData::AwsBuilderTerminationFailed {
instance_id,
message,
} => {
format!(
"{level} | Failed to terminate AWS builder instance\ninstance id: {}\n{}",
instance_id, message,
)
}
AlertData::ResourceSyncPendingUpdates { id, name } => {
let link =
resource_link(ResourceTargetVariant::ResourceSync, id);
format!(
"{level} | Pending resource sync updates on {}\n{link}",
name,
)
}
AlertData::BuildFailed { id, name, version } => {
let link = resource_link(ResourceTargetVariant::Build, id);
format!(
"{level} | Build {} failed\nversion: v{}\n{link}",
name, version,
)
}
AlertData::RepoBuildFailed { id, name } => {
let link = resource_link(ResourceTargetVariant::Repo, id);
format!("{level} | Repo build for {} failed\n{link}", name,)
}
AlertData::None {} => Default::default(),
};
if !content.is_empty() {
send_message(url, content).await?;
}
Ok(())
}
async fn send_message(
url: &str,
content: String,
) -> anyhow::Result<()> {
let response = http_client()
.post(url)
.header("Title", "ntfy Alert")
.body(content)
.send()
.await
.context("Failed to send message")?;
let status = response.status();
if status.is_success() {
debug!("ntfy alert sent successfully: {}", status);
Ok(())
} else {
let text = response.text().await.with_context(|| {
format!(
"Failed to send message to ntfy | {} | failed to get response text",
status
)
})?;
Err(anyhow!(
"Failed to send message to ntfy | {} | {}",
status,
text
))
}
}
fn http_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(reqwest::Client::new)
}
+24 -2
View File
@@ -399,8 +399,30 @@ pub async fn send_alert(
AlertData::None {} => Default::default(),
};
if !text.is_empty() {
let slack = ::slack::Client::new(url);
slack.send_message(text, blocks).await?;
let vars_and_secrets = get_variables_and_secrets().await?;
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
let mut url_interpolated = url.to_string();
// interpolate variables and secrets into the url
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut url_interpolated,
&mut global_replacers,
&mut secret_replacers,
)?;
let slack = ::slack::Client::new(url_interpolated);
slack.send_message(text, blocks).await.map_err(|e| {
let replacers =
secret_replacers.into_iter().collect::<Vec<_>>();
let sanitized_error =
svi::replace_in_string(&format!("{e:?}"), &replacers);
anyhow::Error::msg(format!(
"Error with slack request: {}",
sanitized_error
))
})?;
}
Ok(())
}
+88 -90
View File
@@ -86,7 +86,13 @@ impl Resolve<ExecuteArgs> for RunBuild {
PermissionLevel::Execute,
)
.await?;
let mut vars_and_secrets = get_variables_and_secrets().await?;
// Add the $VERSION to variables. Use with [[$VERSION]]
vars_and_secrets.variables.insert(
String::from("$VERSION"),
build.config.version.to_string(),
);
if build.config.builder_id.is_empty() {
return Err(anyhow!("Must attach builder to RunBuild").into());
@@ -110,14 +116,6 @@ impl Resolve<ExecuteArgs> for RunBuild {
update.version = build.config.version;
update_update(update.clone()).await?;
// Add the $VERSION to variables. Use with [[$VERSION]]
if !vars_and_secrets.variables.contains_key("$VERSION") {
vars_and_secrets.variables.insert(
String::from("$VERSION"),
build.config.version.to_string(),
);
}
let git_token = git_token(
&build.config.git_provider,
&build.config.git_account,
@@ -177,7 +175,6 @@ impl Resolve<ExecuteArgs> for RunBuild {
});
// GET BUILDER PERIPHERY
let (periphery, cleanup_data) = match get_builder_periphery(
build.name.clone(),
Some(build.config.version),
@@ -203,9 +200,8 @@ impl Resolve<ExecuteArgs> for RunBuild {
}
};
// CLONE REPO
// INTERPOLATE VARIABLES
let secret_replacers = if !build.config.skip_secret_interp {
// Interpolate variables / secrets into pre build command
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
@@ -216,6 +212,34 @@ impl Resolve<ExecuteArgs> for RunBuild {
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.build_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.secret_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.dockerfile,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut build.config.extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
@@ -227,84 +251,57 @@ impl Resolve<ExecuteArgs> for RunBuild {
Default::default()
};
let res = tokio::select! {
res = periphery
.request(api::git::CloneRepo {
args: (&build).into(),
git_token,
environment: Default::default(),
env_file_path: Default::default(),
skip_secret_interp: Default::default(),
replacers: secret_replacers.into_iter().collect(),
}) => res,
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
let commit_message = match res {
Ok(res) => {
debug!("finished repo clone");
update.logs.extend(res.logs);
update.commit_hash =
res.commit_hash.unwrap_or_default().to_string();
res.commit_message.unwrap_or_default()
}
Err(e) => {
warn!("failed build at clone repo | {e:#}");
update.push_error_log(
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
);
Default::default()
}
};
update_update(update.clone()).await?;
if all_logs_success(&update.logs) {
let secret_replacers = if !build.config.skip_secret_interp {
// Interpolate variables / secrets into build args
let mut global_replacers = HashSet::new();
let mut secret_replacers = HashSet::new();
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.build_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_string(
&vars_and_secrets,
&mut build.config.secret_args,
&mut global_replacers,
&mut secret_replacers,
)?;
interpolate_variables_secrets_into_extra_args(
&vars_and_secrets,
&mut build.config.extra_args,
&mut global_replacers,
&mut secret_replacers,
)?;
add_interp_update_log(
&mut update,
&global_replacers,
&secret_replacers,
);
secret_replacers
} else {
Default::default()
let commit_message = if !build.config.files_on_host
&& !build.config.repo.is_empty()
{
// CLONE REPO
let res = tokio::select! {
res = periphery
.request(api::git::CloneRepo {
args: (&build).into(),
git_token,
environment: Default::default(),
env_file_path: Default::default(),
skip_secret_interp: Default::default(),
replacers: Default::default(),
}) => res,
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_early_return(update, build.id, build.name, true).await
},
};
let commit_message = match res {
Ok(res) => {
debug!("finished repo clone");
update.logs.extend(res.logs);
update.commit_hash =
res.commit_hash.unwrap_or_default().to_string();
res.commit_message.unwrap_or_default()
}
Err(e) => {
warn!("failed build at clone repo | {e:#}");
update.push_error_log(
"clone repo",
format_serror(&e.context("failed to clone repo").into()),
);
Default::default()
}
};
update_update(update.clone()).await?;
Some(commit_message)
} else {
None
};
if all_logs_success(&update.logs) {
// RUN BUILD
let res = tokio::select! {
res = periphery
.request(api::build::Build {
@@ -321,7 +318,7 @@ impl Resolve<ExecuteArgs> for RunBuild {
_ = cancel.cancelled() => {
info!("build cancelled during build, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during docker build"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
cleanup_builder_instance(cleanup_data, &mut update)
.await;
return handle_early_return(update, build.id, build.name, true).await
},
@@ -365,8 +362,9 @@ impl Resolve<ExecuteArgs> for RunBuild {
// stop the cancel listening task from going forever
cancel.cancel();
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
+4 -3
View File
@@ -462,7 +462,7 @@ impl Resolve<ExecuteArgs> for BuildRepo {
_ = cancel.cancelled() => {
debug!("build cancelled during clone, cleaning up builder");
update.push_error_log("build cancelled", String::from("user cancelled build during repo clone"));
cleanup_builder_instance(periphery, cleanup_data, &mut update)
cleanup_builder_instance(cleanup_data, &mut update)
.await;
info!("builder cleaned up");
return handle_builder_early_return(update, repo.id, repo.name, true).await
@@ -506,8 +506,9 @@ impl Resolve<ExecuteArgs> for BuildRepo {
// stop the cancel listening task from going forever
cancel.cancel();
cleanup_builder_instance(periphery, cleanup_data, &mut update)
.await;
// If building on temporary cloud server (AWS),
// this will terminate the server.
cleanup_builder_instance(cleanup_data, &mut update).await;
// Need to manually update the update before cache refresh,
// and before broadcast with add_update.
+1
View File
@@ -116,6 +116,7 @@ enum ReadRequest {
InspectDockerImage(InspectDockerImage),
ListDockerImageHistory(ListDockerImageHistory),
InspectDockerVolume(InspectDockerVolume),
GetDockerContainersSummary(GetDockerContainersSummary),
ListAllDockerContainers(ListAllDockerContainers),
ListDockerContainers(ListDockerContainers),
ListDockerNetworks(ListDockerNetworks),
+42 -1
View File
@@ -14,7 +14,9 @@ use komodo_client::{
ResourceTarget,
deployment::Deployment,
docker::{
container::{Container, ContainerListItem},
container::{
Container, ContainerListItem, ContainerStateStatusEnum,
},
image::{Image, ImageHistoryResponseItem},
network::Network,
volume::Volume,
@@ -413,6 +415,45 @@ impl Resolve<ReadArgs> for ListAllDockerContainers {
}
}
impl Resolve<ReadArgs> for GetDockerContainersSummary {
async fn resolve(
self,
ReadArgs { user }: &ReadArgs,
) -> serror::Result<GetDockerContainersSummaryResponse> {
let servers = resource::list_full_for_user::<Server>(
Default::default(),
user,
&[],
)
.await
.context("failed to get servers from db")?;
let mut res = GetDockerContainersSummaryResponse::default();
for server in servers {
let cache = server_status_cache()
.get_or_insert_default(&server.id)
.await;
if let Some(containers) = &cache.containers {
for container in containers {
res.total += 1;
match container.state {
ContainerStateStatusEnum::Created
| ContainerStateStatusEnum::Paused
| ContainerStateStatusEnum::Exited => res.stopped += 1,
ContainerStateStatusEnum::Running => res.running += 1,
ContainerStateStatusEnum::Empty => res.unknown += 1,
_ => res.unhealthy += 1,
}
}
}
}
Ok(res)
}
}
impl Resolve<ReadArgs> for InspectDockerContainer {
async fn resolve(
self,
+339 -37
View File
@@ -1,12 +1,17 @@
use std::{path::PathBuf, str::FromStr, time::Duration};
use anyhow::{Context, anyhow};
use formatting::format_serror;
use git::GitRes;
use komodo_client::{
api::write::*,
entities::{
CloneArgs, NoData,
CloneArgs, FileContents, NoData, Operation, all_logs_success,
build::{Build, BuildInfo, PartialBuildConfig},
builder::{Builder, BuilderConfig},
config::core::CoreConfig,
permission::PermissionLevel,
server::ServerState,
update::Update,
},
};
@@ -15,11 +20,22 @@ use mungos::mongodb::bson::to_document;
use octorust::types::{
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
};
use periphery_client::{
PeripheryClient,
api::build::{
GetDockerfileContentsOnHost, WriteDockerfileContentsToHost,
},
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
config::core_config,
helpers::git_token,
helpers::{
git_token, periphery_client,
query::get_server_with_state,
update::{add_update, make_update},
},
resource,
state::{db_client, github_client},
};
@@ -88,6 +104,184 @@ impl Resolve<WriteArgs> for RenameBuild {
}
}
impl Resolve<WriteArgs> for WriteBuildFileContents {
#[instrument(name = "WriteBuildFileContents", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let build = resource::get_check_permissions::<Build>(
&self.build,
&args.user,
PermissionLevel::Write,
)
.await?;
if !build.config.files_on_host && build.config.repo.is_empty() {
return Err(anyhow!(
"Build is not configured to use Files on Host or Git Repo, can't write dockerfile contents"
).into());
}
let mut update =
make_update(&build, Operation::WriteDockerfile, &args.user);
update.push_simple_log("Dockerfile to write", &self.contents);
if build.config.files_on_host {
match get_on_host_periphery(&build)
.await?
.request(WriteDockerfileContentsToHost {
name: build.name,
build_path: build.config.build_path,
dockerfile_path: build.config.dockerfile_path,
contents: self.contents,
})
.await
.context("Failed to write dockerfile contents to host")
{
Ok(log) => {
update.logs.push(log);
}
Err(e) => {
update.push_error_log(
"Write Dockerfile Contents",
format_serror(&e.into()),
);
}
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
if let Err(e) =
(RefreshBuildCache { build: build.id }).resolve(args).await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
} else {
write_dockerfile_contents_git(self, args, build, update).await
}
}
}
async fn write_dockerfile_contents_git(
req: WriteBuildFileContents,
args: &WriteArgs,
build: Build,
mut update: Update,
) -> serror::Result<Update> {
let WriteBuildFileContents { build: _, contents } = req;
let mut clone_args: CloneArgs = (&build).into();
let root = clone_args.unique_path(&core_config().repo_directory)?;
let build_path = build
.config
.build_path
.parse::<PathBuf>()
.context("Invalid build path")?;
let dockerfile_path = build
.config
.dockerfile_path
.parse::<PathBuf>()
.context("Invalid dockerfile path")?;
let full_path = root.join(&build_path).join(&dockerfile_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).await.with_context(|| {
format!(
"Failed to initialize dockerfile parent directory {parent:?}"
)
})?;
}
// Ensure the folder is initialized as git repo.
// This allows a new file to be committed on a branch that may not exist.
if !root.join(".git").exists() {
let access_token = if let Some(account) = &clone_args.account {
git_token(&clone_args.provider, account, |https| clone_args.https = https)
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {account}", clone_args.provider),
)?
} else {
None
};
git::init_folder_as_repo(
&root,
&clone_args,
access_token.as_deref(),
&mut update.logs,
)
.await;
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
}
if let Err(e) =
fs::write(&full_path, &contents).await.with_context(|| {
format!("Failed to write dockerfile contents to {full_path:?}")
})
{
update
.push_error_log("Write Dockerfile", format_serror(&e.into()));
} else {
update.push_simple_log(
"Write Dockerfile",
format!("File written to {full_path:?}"),
);
};
if !all_logs_success(&update.logs) {
update.finalize();
update.id = add_update(update.clone()).await?;
return Ok(update);
}
let commit_res = git::commit_file(
&format!("{}: Commit Dockerfile", args.user.username),
&root,
&build_path.join(&dockerfile_path),
&build.config.branch,
)
.await;
update.logs.extend(commit_res.logs);
if let Err(e) = (RefreshBuildCache { build: build.name })
.resolve(args)
.await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.into()),
);
}
update.finalize();
update.id = add_update(update.clone()).await?;
Ok(update)
}
impl Resolve<WriteArgs> for RefreshBuildCache {
#[instrument(
name = "RefreshBuildCache",
@@ -107,55 +301,104 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
)
.await?;
if build.config.repo.is_empty()
|| build.config.git_provider.is_empty()
{
// Nothing to do here
return Ok(NoData {});
}
let (
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
) = if build.config.files_on_host {
// =============
// FILES ON HOST
// =============
match get_on_host_dockerfile(&build).await {
Ok(FileContents { path, contents }) => {
(Some(path), Some(contents), None, None, None)
}
Err(e) => {
(None, None, Some(format_serror(&e.into())), None, None)
}
}
} else if !build.config.repo.is_empty() {
// ================
// REPO BASED BUILD
// ================
if build.config.git_provider.is_empty() {
// Nothing to do here
return Ok(NoData {});
}
let config = core_config();
let config = core_config();
let mut clone_args: CloneArgs = (&build).into();
let repo_path =
clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(repo_path.display().to_string());
// Don't want to run these on core.
clone_args.on_clone = None;
clone_args.on_pull = None;
let mut clone_args: CloneArgs = (&build).into();
let repo_path =
clone_args.unique_path(&core_config().repo_directory)?;
clone_args.destination = Some(repo_path.display().to_string());
// Don't want to run these on core.
clone_args.on_clone = None;
clone_args.on_pull = None;
let access_token = if let Some(username) = &clone_args.account {
git_token(&clone_args.provider, username, |https| {
let access_token = if let Some(username) = &clone_args.account {
git_token(&clone_args.provider, username, |https| {
clone_args.https = https
})
.await
.with_context(
|| format!("Failed to get git token in call to db. Stopping run. | {} | {username}", clone_args.provider),
)?
} else {
None
};
} else {
None
};
let GitRes {
hash: latest_hash,
message: latest_message,
..
} = git::pull_or_clone(
clone_args,
&config.repo_directory,
access_token,
&[],
"",
None,
&[],
)
.await
.context("failed to clone build repo")?;
let GitRes { hash, message, .. } = git::pull_or_clone(
clone_args,
&config.repo_directory,
access_token,
&[],
"",
None,
&[],
)
.await
.context("failed to clone build repo")?;
let relative_path = PathBuf::from_str(&build.config.build_path)
.context("Invalid build path")?
.join(&build.config.dockerfile_path);
let full_path = repo_path.join(&relative_path);
let (contents, error) = match fs::read_to_string(&full_path)
.await
.with_context(|| {
format!(
"Failed to read dockerfile contents at {full_path:?}"
)
}) {
Ok(contents) => (Some(contents), None),
Err(e) => (None, Some(format_serror(&e.into()))),
};
(
Some(relative_path.display().to_string()),
contents,
error,
hash,
message,
)
} else {
// =============
// UI BASED FILE
// =============
(None, None, None, None, None)
};
let info = BuildInfo {
last_built_at: build.info.last_built_at,
built_hash: build.info.built_hash,
built_message: build.info.built_message,
built_contents: build.info.built_contents,
remote_path,
remote_contents,
remote_error,
latest_hash,
latest_message,
};
@@ -176,6 +419,65 @@ impl Resolve<WriteArgs> for RefreshBuildCache {
}
}
async fn get_on_host_periphery(
build: &Build,
) -> anyhow::Result<PeripheryClient> {
if build.config.builder_id.is_empty() {
return Err(anyhow!("No builder associated with build"));
}
let builder = resource::get::<Builder>(&build.config.builder_id)
.await
.context("Failed to get builder")?;
match builder.config {
BuilderConfig::Aws(_) => {
return Err(anyhow!(
"Files on host doesn't work with AWS builder"
));
}
BuilderConfig::Url(config) => {
let periphery = PeripheryClient::new(
config.address,
config.passkey,
Duration::from_secs(3),
);
periphery.health_check().await?;
Ok(periphery)
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
return Err(anyhow!(
"Builder is type server, but has no server attached"
));
}
let (server, state) =
get_server_with_state(&config.server_id).await?;
if state != ServerState::Ok {
return Err(anyhow!(
"Builder server is disabled or not reachable"
));
};
periphery_client(&server)
}
}
}
/// The successful case will be included as Some(remote_contents).
/// The error case will be included as Some(remote_error)
async fn get_on_host_dockerfile(
build: &Build,
) -> anyhow::Result<FileContents> {
get_on_host_periphery(build)
.await?
.request(GetDockerfileContentsOnHost {
name: build.name.clone(),
build_path: build.config.build_path.clone(),
dockerfile_path: build.config.dockerfile_path.clone(),
})
.await
}
impl Resolve<WriteArgs> for CreateBuildWebhook {
#[instrument(name = "CreateBuildWebhook", skip(args))]
async fn resolve(
+1
View File
@@ -96,6 +96,7 @@ pub enum WriteRequest {
DeleteBuild(DeleteBuild),
UpdateBuild(UpdateBuild),
RenameBuild(RenameBuild),
WriteBuildFileContents(WriteBuildFileContents),
RefreshBuildCache(RefreshBuildCache),
CreateBuildWebhook(CreateBuildWebhook),
DeleteBuildWebhook(DeleteBuildWebhook),
+1 -1
View File
@@ -211,7 +211,7 @@ impl Resolve<WriteArgs> for WriteStackFileContents {
}
update.finalize();
add_update(update.clone()).await?;
update.id = add_update(update.clone()).await?;
Ok(update)
}
+33 -32
View File
@@ -132,6 +132,38 @@ impl Resolve<WriteArgs> for RenameResourceSync {
}
}
impl Resolve<WriteArgs> for WriteSyncFileContents {
#[instrument(name = "WriteSyncFileContents", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let sync = resource::get_check_permissions::<ResourceSync>(
&self.sync,
&args.user,
PermissionLevel::Write,
)
.await?;
if !sync.config.files_on_host && sync.config.repo.is_empty() {
return Err(
anyhow!(
"This method is only for 'files on host' or 'repo' based syncs."
)
.into(),
);
}
let mut update =
make_update(&sync, Operation::WriteSyncContents, &args.user);
update.push_simple_log("File contents", &self.contents);
if sync.config.files_on_host {
write_sync_file_contents_on_host(self, args, sync, update).await
} else {
write_sync_file_contents_git(self, args, sync, update).await
}
}
}
async fn write_sync_file_contents_on_host(
req: WriteSyncFileContents,
args: &WriteArgs,
@@ -298,7 +330,7 @@ async fn write_sync_file_contents_git(
.await
{
update.push_error_log(
"Refresh failed",
"Refresh sync pending",
format_serror(&e.error.into()),
);
}
@@ -309,37 +341,6 @@ async fn write_sync_file_contents_git(
Ok(update)
}
impl Resolve<WriteArgs> for WriteSyncFileContents {
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
let sync = resource::get_check_permissions::<ResourceSync>(
&self.sync,
&args.user,
PermissionLevel::Write,
)
.await?;
if !sync.config.files_on_host && sync.config.repo.is_empty() {
return Err(
anyhow!(
"This method is only for 'files on host' or 'repo' based syncs."
)
.into(),
);
}
let mut update =
make_update(&sync, Operation::WriteSyncContents, &args.user);
update.push_simple_log("File contents", &self.contents);
if sync.config.files_on_host {
write_sync_file_contents_on_host(self, args, sync, update).await
} else {
write_sync_file_contents_git(self, args, sync, update).await
}
}
}
impl Resolve<WriteArgs> for CommitSync {
#[instrument(name = "CommitSync", skip(args))]
async fn resolve(self, args: &WriteArgs) -> serror::Result<Update> {
+3 -1
View File
@@ -5,6 +5,8 @@ pub mod hetzner;
#[derive(Debug)]
pub enum BuildCleanupData {
Server { repo_name: String },
/// Nothing to clean up
Server,
/// Clean up AWS instance
Aws { instance_id: String, region: String },
}
+1
View File
@@ -139,6 +139,7 @@ pub fn core_config() -> &'static CoreConfig {
title: env.komodo_title.unwrap_or(config.title),
host: env.komodo_host.unwrap_or(config.host),
port: env.komodo_port.unwrap_or(config.port),
bind_ip: env.komodo_bind_ip.unwrap_or(config.bind_ip),
first_server: env.komodo_first_server.unwrap_or(config.first_server),
frontend_path: env.komodo_frontend_path.unwrap_or(config.frontend_path),
jwt_ttl: env
+5 -18
View File
@@ -61,12 +61,7 @@ pub async fn get_builder_periphery(
.health_check()
.await
.context("Url Builder failed health check")?;
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
Ok((periphery, BuildCleanupData::Server))
}
BuilderConfig::Server(config) => {
if config.server_id.is_empty() {
@@ -74,12 +69,7 @@ pub async fn get_builder_periphery(
}
let server = resource::get::<Server>(&config.server_id).await?;
let periphery = periphery_client(&server)?;
Ok((
periphery,
BuildCleanupData::Server {
repo_name: resource_name,
},
))
Ok((periphery, BuildCleanupData::Server))
}
BuilderConfig::Aws(config) => {
get_aws_builder(&resource_name, version, config, update).await
@@ -179,17 +169,14 @@ async fn get_aws_builder(
)
}
#[instrument(skip(periphery, update))]
#[instrument(skip(update))]
pub async fn cleanup_builder_instance(
periphery: PeripheryClient,
cleanup_data: BuildCleanupData,
update: &mut Update,
) {
match cleanup_data {
BuildCleanupData::Server { repo_name } => {
let _ = periphery
.request(api::git::DeleteRepo { name: repo_name })
.await;
BuildCleanupData::Server => {
// Nothing to clean up
}
BuildCleanupData::Aws {
instance_id,
+9 -2
View File
@@ -78,6 +78,9 @@ pub async fn git_token(
account_username: &str,
mut on_https_found: impl FnMut(bool),
) -> anyhow::Result<Option<String>> {
if provider_domain.is_empty() || account_username.is_empty() {
return Ok(None);
}
let db_provider = db_client()
.git_accounts
.find_one(doc! { "domain": provider_domain, "username": account_username })
@@ -143,7 +146,11 @@ pub fn periphery_client(
let client = PeripheryClient::new(
&server.config.address,
&core_config().passkey,
if server.config.passkey.is_empty() {
&core_config().passkey
} else {
&server.config.passkey
},
Duration::from_secs(server.config.timeout_seconds as u64),
);
@@ -352,4 +359,4 @@ pub async fn ensure_first_server_and_builder() {
e.error
);
}
}
}
+3 -2
View File
@@ -86,9 +86,10 @@ async fn app() -> anyhow::Result<()> {
)
.into_make_service();
let addr = format!("{}:{}", core_config().bind_ip, core_config().port);
let socket_addr =
SocketAddr::from_str(&format!("0.0.0.0:{}", core_config().port))
.context("failed to parse socket addr")?;
SocketAddr::from_str(&addr)
.context("failed to parse listen address")?;
if config.ssl_enabled {
info!("🔒 Core SSL Enabled");
+40 -18
View File
@@ -1,26 +1,32 @@
use std::time::Duration;
use anyhow::Context;
use komodo_client::entities::{
Operation, ResourceTargetVariant,
build::{
Build, BuildConfig, BuildConfigDiff, BuildInfo, BuildListItem,
BuildListItemInfo, BuildQuerySpecifics, BuildState,
PartialBuildConfig,
use formatting::format_serror;
use komodo_client::{
api::write::RefreshBuildCache,
entities::{
Operation, ResourceTargetVariant,
build::{
Build, BuildConfig, BuildConfigDiff, BuildInfo, BuildListItem,
BuildListItemInfo, BuildQuerySpecifics, BuildState,
PartialBuildConfig,
},
builder::Builder,
environment_vars_from_str, optional_string,
permission::PermissionLevel,
resource::Resource,
update::Update,
user::{User, build_user},
},
builder::Builder,
environment_vars_from_str,
permission::PermissionLevel,
resource::Resource,
update::Update,
user::User,
};
use mungos::{
find::find_collect,
mongodb::{Collection, bson::doc, options::FindOptions},
};
use resolver_api::Resolve;
use crate::{
api::write::WriteArgs,
config::core_config,
helpers::{empty_or_only_spaces, query::get_latest_update},
state::{action_states, build_state_cache, db_client},
@@ -56,10 +62,13 @@ impl super::KomodoResource for Build {
last_built_at: build.info.last_built_at,
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,
files_on_host: build.config.files_on_host,
git_provider: optional_string(build.config.git_provider),
repo: optional_string(build.config.repo),
branch: optional_string(build.config.branch),
image_registry_domain: optional_string(
build.config.image_registry.domain,
),
built_hash: build.info.built_hash,
latest_hash: build.info.latest_hash,
state,
@@ -96,10 +105,23 @@ impl super::KomodoResource for Build {
}
async fn post_create(
_created: &Resource<Self::Config, Self::Info>,
_update: &mut Update,
created: &Resource<Self::Config, Self::Info>,
update: &mut Update,
) -> anyhow::Result<()> {
refresh_build_state_cache().await;
if let Err(e) = (RefreshBuildCache {
build: created.name.clone(),
})
.resolve(&WriteArgs {
user: build_user().to_owned(),
})
.await
{
update.push_error_log(
"Refresh build cache",
format_serror(&e.error.context("The build cache has failed to refresh. This may be due to a misconfiguration of the Build").into())
);
};
Ok(())
}
+1
View File
@@ -170,6 +170,7 @@ impl super::KomodoResource for Repo {
} else {
repo.config.path.clone()
},
is_build: false,
})
.await
{
+1 -1
View File
@@ -162,7 +162,7 @@ impl super::KomodoResource for Stack {
{
update.push_error_log(
"Refresh stack cache",
format_serror(&e.error.context("The stack cache has failed to refresh. This is likely due to a misconfiguration of the Stack").into())
format_serror(&e.error.context("The stack cache has failed to refresh. This may be due to a misconfiguration of the Stack").into())
);
};
if created.config.server_id.is_empty() {
+3 -3
View File
@@ -26,7 +26,7 @@ pub async fn get_repo_compose_contents(
let (repo_path, _logs, hash, message) =
ensure_remote_repo(clone_args)
.await
.context("failed to clone stack repo")?;
.context("Failed to clone stack repo")?;
let run_directory = repo_path.join(&stack.config.run_directory);
// This will remove any intermediate '/./' which can be a problem for some OS.
@@ -44,7 +44,7 @@ pub async fn get_repo_compose_contents(
}
// If file does not exist, will show up in err case so the log is handled
match fs::read_to_string(&file_path).with_context(|| {
format!("failed to read file contents from {file_path:?}")
format!("Failed to read file contents from {file_path:?}")
}) {
Ok(contents) => successful.push(FileContents {
path: path.to_string(),
@@ -102,6 +102,6 @@ pub async fn ensure_remote_repo(
&[],
)
.await
.context("failed to clone stack repo")
.context("Failed to clone stack repo")
.map(|res| (repo_path, res.logs, res.hash, res.message))
}
+9 -12
View File
@@ -8,18 +8,15 @@ pub fn extract_services_from_stack(
stack: &Stack,
) -> Vec<StackServiceNames> {
if let Some(mut services) = stack.info.deployed_services.clone() {
if services.iter().any(|service| service.image.is_empty()) {
for service in
services.iter_mut().filter(|s| s.image.is_empty())
{
service.image = stack
.info
.latest_services
.iter()
.find(|s| s.service_name == service.service_name)
.map(|s| s.image.clone())
.unwrap_or_default();
}
for service in services.iter_mut().filter(|s| s.image.is_empty())
{
service.image = stack
.info
.latest_services
.iter()
.find(|s| s.service_name == service.service_name)
.map(|s| s.image.clone())
.unwrap_or_default();
}
services
} else {
+177 -13
View File
@@ -1,8 +1,12 @@
use std::{fmt::Write, path::Path};
use std::{
fmt::Write,
path::{Path, PathBuf},
};
use anyhow::{Context, anyhow};
use command::{
run_komodo_command, run_komodo_command_with_interpolation,
run_komodo_command, run_komodo_command_multiline,
run_komodo_command_with_interpolation,
};
use formatting::format_serror;
use komodo_client::{
@@ -16,9 +20,12 @@ use komodo_client::{
parsers::QUOTE_PATTERN,
};
use periphery_client::api::build::{
self, PruneBuilders, PruneBuildx,
self, GetDockerfileContentsOnHost,
GetDockerfileContentsOnHostResponse, PruneBuilders, PruneBuildx,
WriteDockerfileContentsToHost,
};
use resolver_api::Resolve;
use tokio::fs;
use crate::{
config::periphery_config,
@@ -26,6 +33,88 @@ use crate::{
helpers::{parse_extra_args, parse_labels},
};
impl Resolve<super::Args> for GetDockerfileContentsOnHost {
#[instrument(name = "GetDockerfileContentsOnHost", level = "debug")]
async fn resolve(
self,
_: &super::Args,
) -> serror::Result<GetDockerfileContentsOnHostResponse> {
let GetDockerfileContentsOnHost {
name,
build_path,
dockerfile_path,
} = self;
let root =
periphery_config().build_dir.join(to_komodo_name(&name));
let build_dir =
root.join(&build_path).components().collect::<PathBuf>();
if !build_dir.exists() {
fs::create_dir_all(&build_dir)
.await
.context("Failed to initialize build directory")?;
}
let full_path = build_dir
.join(&dockerfile_path)
.components()
.collect::<PathBuf>();
let contents =
fs::read_to_string(&full_path).await.with_context(|| {
format!("Failed to read dockerfile contents at {full_path:?}")
})?;
Ok(GetDockerfileContentsOnHostResponse {
contents,
path: full_path.display().to_string(),
})
}
}
impl Resolve<super::Args> for WriteDockerfileContentsToHost {
#[instrument(
name = "WriteDockerfileContentsToHost",
skip_all,
fields(
stack = &self.name,
build_path = &self.build_path,
dockerfile_path = &self.dockerfile_path,
)
)]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let WriteDockerfileContentsToHost {
name,
build_path,
dockerfile_path,
contents,
} = self;
let full_path = periphery_config()
.build_dir
.join(to_komodo_name(&name))
.join(&build_path)
.join(dockerfile_path)
.components()
.collect::<PathBuf>();
// Ensure parent directory exists
if let Some(parent) = full_path.parent() {
if !parent.exists() {
tokio::fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize dockerfile parent directory {parent:?}"))?;
}
}
fs::write(&full_path, contents).await.with_context(|| {
format!("Failed to write dockerfile contents to {full_path:?}")
})?;
Ok(Log::simple(
"Write dockerfile to host",
format!("dockerfile contents written to {full_path:?}"),
))
}
}
impl Resolve<super::Args> for build::Build {
#[instrument(name = "Build", skip_all, fields(build = self.build.name.to_string()))]
async fn resolve(
@@ -36,7 +125,7 @@ impl Resolve<super::Args> for build::Build {
build,
registry_token,
additional_tags,
replacers: core_replacers,
replacers: mut core_replacers,
} = self;
let Build {
name,
@@ -53,11 +142,19 @@ impl Resolve<super::Args> for build::Build {
extra_args,
use_buildx,
image_registry,
repo,
files_on_host,
dockerfile,
pre_build,
..
},
..
} = &build;
if !*files_on_host && repo.is_empty() && dockerfile.is_empty() {
return Err(anyhow!("Build must be files on host mode, have a repo attached, or have dockerfile contents set to build").into());
}
let mut logs = Vec::new();
// Maybe docker login
@@ -82,14 +179,81 @@ impl Resolve<super::Args> for build::Build {
let name = to_komodo_name(name);
// Get paths
let build_dir =
periphery_config().repo_dir.join(&name).join(build_path);
let dockerfile_path = match optional_string(dockerfile_path) {
Some(dockerfile_path) => dockerfile_path.to_owned(),
None => "Dockerfile".to_owned(),
let build_path =
periphery_config().build_dir.join(&name).join(build_path);
let dockerfile_path = optional_string(dockerfile_path)
.unwrap_or("Dockerfile".to_owned());
// Write UI defined Dockerfile to host
if !*files_on_host && repo.is_empty() && !dockerfile.is_empty() {
let dockerfile = if *skip_secret_interp {
dockerfile.to_string()
} else {
let (dockerfile, replacers) = svi::interpolate_variables(
dockerfile,
&periphery_config().secrets,
svi::Interpolator::DoubleBrackets,
true,
).context("Failed to interpolate variables into UI defined dockerfile")?;
core_replacers.extend(replacers);
dockerfile
};
let full_path = build_path
.join(&dockerfile_path)
.components()
.collect::<PathBuf>();
// Ensure parent directory exists
if let Some(parent) = full_path.parent() {
if !parent.exists() {
tokio::fs::create_dir_all(parent)
.await
.with_context(|| format!("Failed to initialize dockerfile parent directory {parent:?}"))?;
}
}
fs::write(&full_path, dockerfile).await.with_context(|| {
format!(
"Failed to write dockerfile contents to {full_path:?}"
)
})?;
logs.push(Log::simple(
"Write Dockerfile",
format!("Dockerfile contents written to {full_path:?}"),
));
};
// Pre Build
if !pre_build.is_none() {
let pre_build_path = build_path.join(&pre_build.path);
if let Some(log) = if !skip_secret_interp {
run_komodo_command_with_interpolation(
"Pre Build",
Some(pre_build_path.as_path()),
&pre_build.command,
true,
&periphery_config().secrets,
&core_replacers,
)
.await
} else {
run_komodo_command_multiline(
"Pre Build",
Some(pre_build_path.as_path()),
&pre_build.command,
)
.await
} {
let success = log.success;
logs.push(log);
if success {
return Ok(logs);
}
};
}
// Get command parts
let image_name =
get_image_name(&build).context("failed to make image name")?;
@@ -109,7 +273,7 @@ impl Resolve<super::Args> for build::Build {
.context("Invalid secret_args")?;
let command_secret_args = parse_secret_args(
&secret_args,
&build_dir,
&build_path,
*skip_secret_interp,
)
.await?;
@@ -131,14 +295,14 @@ impl Resolve<super::Args> for build::Build {
if *skip_secret_interp {
let build_log = run_komodo_command(
"Docker Build",
build_dir.as_ref(),
build_path.as_ref(),
command,
)
.await;
logs.push(build_log);
} else if let Some(log) = run_komodo_command_with_interpolation(
"Docker Build",
build_dir.as_ref(),
build_path.as_ref(),
command,
false,
&periphery_config().secrets,
+1 -1
View File
@@ -206,7 +206,7 @@ impl Resolve<super::Args> for WriteComposeContentsToHost {
if let Some(parent) = file_path.parent() {
fs::create_dir_all(&parent)
.await
.with_context(|| format!("Failed to initialize environment file parent directory {parent:?}"))?;
.with_context(|| format!("Failed to initialize compose file parent directory {parent:?}"))?;
}
fs::write(&file_path, contents).await.with_context(|| {
format!(
+37 -12
View File
@@ -1,4 +1,5 @@
use anyhow::{Context, anyhow};
use formatting::format_serror;
use git::GitRes;
use komodo_client::entities::{CloneArgs, LatestCommit, update::Log};
use periphery_client::api::git::{
@@ -12,7 +13,7 @@ use tokio::fs;
use crate::config::periphery_config;
impl Resolve<super::Args> for GetLatestCommit {
#[instrument(name = "CloneRepo", level = "debug")]
#[instrument(name = "GetLatestCommit", level = "debug")]
async fn resolve(
self,
_: &super::Args,
@@ -68,9 +69,14 @@ impl Resolve<super::Args> for CloneRepo {
)?,
),
};
let parent_dir = if args.is_build {
&periphery_config().build_dir
} else {
&periphery_config().repo_dir
};
git::clone(
args,
&periphery_config().repo_dir,
parent_dir,
token,
&environment,
&env_file_path,
@@ -133,9 +139,14 @@ impl Resolve<super::Args> for PullRepo {
)?,
),
};
let parent_dir = if args.is_build {
&periphery_config().build_dir
} else {
&periphery_config().repo_dir
};
git::pull(
args,
&periphery_config().repo_dir,
parent_dir,
token,
&environment,
&env_file_path,
@@ -198,9 +209,14 @@ impl Resolve<super::Args> for PullOrCloneRepo {
)?,
),
};
let parent_dir = if args.is_build {
&periphery_config().build_dir
} else {
&periphery_config().repo_dir
};
git::pull_or_clone(
args,
&periphery_config().repo_dir,
parent_dir,
token,
&environment,
&env_file_path,
@@ -254,16 +270,25 @@ impl Resolve<super::Args> for RenameRepo {
impl Resolve<super::Args> for DeleteRepo {
#[instrument(name = "DeleteRepo")]
async fn resolve(self, _: &super::Args) -> serror::Result<Log> {
let DeleteRepo { name } = self;
let DeleteRepo { name, is_build } = self;
// If using custom clone path, it will be passed by core instead of name.
// So the join will resolve to just the absolute path.
let deleted =
fs::remove_dir_all(periphery_config().repo_dir.join(&name))
.await;
let msg = match deleted {
Ok(_) => format!("Deleted Repo {name}"),
Err(_) => format!("No Repo at {name} to delete"),
let root = if is_build {
&periphery_config().build_dir
} else {
&periphery_config().repo_dir
};
Ok(Log::simple("Delete Repo on Host", msg))
let full_path = root.join(&name);
let deleted =
fs::remove_dir_all(&full_path).await.with_context(|| {
format!("Failed to delete repo at {full_path:?}")
});
let log = match deleted {
Ok(_) => {
Log::simple("Delete repo", format!("Deleted Repo {name}"))
}
Err(e) => Log::error("Delete repo", format_serror(&e.into())),
};
Ok(log)
}
}
+2
View File
@@ -66,6 +66,8 @@ pub enum PeripheryRequest {
DeleteRepo(DeleteRepo),
// Build
GetDockerfileContentsOnHost(GetDockerfileContentsOnHost),
WriteDockerfileContentsToHost(WriteDockerfileContentsToHost),
Build(Build),
PruneBuilders(PruneBuilders),
PruneBuildx(PruneBuildx),
+2
View File
@@ -35,8 +35,10 @@ pub fn periphery_config() -> &'static PeripheryConfig {
PeripheryConfig {
port: env.periphery_port.unwrap_or(config.port),
bind_ip: env.periphery_bind_ip.unwrap_or(config.bind_ip),
repo_dir: env.periphery_repo_dir.unwrap_or(config.repo_dir),
stack_dir: env.periphery_stack_dir.unwrap_or(config.stack_dir),
build_dir: env.periphery_build_dir.unwrap_or(config.build_dir),
stats_polling_rate: env
.periphery_stats_polling_rate
.unwrap_or(config.stats_polling_rate),
+1 -1
View File
@@ -945,7 +945,7 @@ pub async fn docker_login(
None => crate::helpers::registry_token(domain, account)?,
};
let log = async_run_command(&format!(
"echo {registry_token} | docker login {domain} --username {account} --password-stdin",
"echo {registry_token} | docker login {domain} --username '{account}' --password-stdin",
))
.await;
if log.success() {
+5 -3
View File
@@ -26,9 +26,11 @@ async fn app() -> anyhow::Result<()> {
stats::spawn_system_stats_polling_thread();
let socket_addr =
SocketAddr::from_str(&format!("0.0.0.0:{}", config.port))
.context("failed to parse socket addr")?;
let addr = format!("{}:{}", config::periphery_config().bind_ip, config::periphery_config().port);
let socket_addr =
SocketAddr::from_str(&addr)
.context("failed to parse listen address")?;
let app = router::router()
.into_make_service_with_connect_info::<SocketAddr>();
+1 -1
View File
@@ -194,7 +194,7 @@ fn get_system_information(
os: System::long_os_version(),
kernel: System::kernel_version(),
host_name: System::host_name(),
core_count: sys.physical_core_count().map(|c| c as u32),
core_count: System::physical_core_count().map(|c| c as u32),
cpu_brand: sys
.cpus()
.iter()
+29
View File
@@ -294,6 +294,35 @@ pub type ListAllDockerContainersResponse = Vec<ContainerListItem>;
//
/// Gets a summary of data relating to all containers.
/// Response: [GetDockerContainersSummaryResponse].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoReadRequest)]
#[response(GetDockerContainersSummaryResponse)]
#[error(serror::Error)]
pub struct GetDockerContainersSummary {}
/// Response for [GetDockerContainersSummary]
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct GetDockerContainersSummaryResponse {
/// The total number of Containers
pub total: u32,
/// The number of Containers with Running state
pub running: u32,
/// The number of Containers with Stopped or Paused or Created state
pub stopped: u32,
/// The number of Containers with Restarting or Dead state
pub unhealthy: u32,
/// The number of Containers with Unknown state
pub unknown: u32,
}
//
/// Inspect a docker container on the server. Response: [Container].
#[typeshare]
#[derive(
-1
View File
@@ -98,7 +98,6 @@ pub struct UpdateAction {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameAction {
/// The id or name of the Action to rename.
pub id: String,
-1
View File
@@ -95,7 +95,6 @@ pub struct UpdateAlerter {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameAlerter {
/// The id or name of the Alerter to rename.
pub id: String,
+18 -1
View File
@@ -98,7 +98,6 @@ pub struct UpdateBuild {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameBuild {
/// The id or name of the Build to rename.
pub id: String,
@@ -108,6 +107,24 @@ pub struct RenameBuild {
//
/// Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update].
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, Resolve, EmptyTraits,
)]
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
pub struct WriteBuildFileContents {
/// The name or id of the target Build.
#[serde(alias = "id", alias = "name")]
pub build: String,
/// The dockerfile contents to write.
pub contents: String,
}
//
/// Trigger a refresh of the cached latest hash and message.
#[typeshare]
#[derive(
-1
View File
@@ -97,7 +97,6 @@ pub struct UpdateBuilder {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameBuilder {
/// The id or name of the Builder to rename.
pub id: String,
@@ -122,7 +122,6 @@ pub struct UpdateDeployment {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameDeployment {
/// The id of the deployment to rename.
pub id: String,
@@ -109,7 +109,6 @@ pub type UpdateProcedureResponse = Procedure;
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameProcedure {
/// The id or name of the Procedure to rename.
pub id: String,
-1
View File
@@ -101,7 +101,6 @@ pub struct UpdateRepo {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameRepo {
/// The id or name of the Repo to rename.
pub id: String,
-2
View File
@@ -79,7 +79,6 @@ pub struct UpdateServer {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameServer {
/// The id or name of the Server to rename.
pub id: String,
@@ -100,7 +99,6 @@ pub struct RenameServer {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct CreateNetwork {
/// Server Id or name
pub server: String,
@@ -97,7 +97,6 @@ pub struct UpdateServerTemplate {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameServerTemplate {
/// The id or name of the ServerTemplate to rename.
pub id: String,
-2
View File
@@ -100,7 +100,6 @@ pub struct UpdateStack {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameStack {
/// The id of the stack to rename.
pub id: String,
@@ -118,7 +117,6 @@ pub struct RenameStack {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct WriteStackFileContents {
/// The name or id of the target Stack.
#[serde(alias = "id", alias = "name")]
-3
View File
@@ -99,7 +99,6 @@ pub struct UpdateResourceSync {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct RenameResourceSync {
/// The id or name of the ResourceSync to rename.
pub id: String,
@@ -133,7 +132,6 @@ pub struct RefreshResourceSyncPending {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct WriteSyncFileContents {
/// The name or id of the target Sync.
#[serde(alias = "id", alias = "name")]
@@ -166,7 +164,6 @@ pub struct WriteSyncFileContents {
#[empty_traits(KomodoWriteRequest)]
#[response(Update)]
#[error(serror::Error)]
#[error(serror::Error)]
pub struct CommitSync {
/// Id or name
#[serde(alias = "id", alias = "name")]
+1
View File
@@ -63,6 +63,7 @@ pub type _PartialActionConfig = PartialActionConfig;
#[partial(skip_serializing_none, from, diff)]
pub struct ActionConfig {
/// Typescript file contents using pre-initialized `komodo` client.
/// Supports variable / secret interpolation.
#[serde(default, deserialize_with = "file_contents_deserializer")]
#[partial_attr(serde(
default,
+28
View File
@@ -111,6 +111,9 @@ pub enum AlerterEndpoint {
/// Send alert to a Discord app
Discord(DiscordAlerterEndpoint),
/// Send alert to Ntfy
Ntfy(NtfyAlerterEndpoint),
}
impl Default for AlerterEndpoint {
@@ -195,6 +198,31 @@ fn default_discord_url() -> String {
)
}
/// Configuration for a Ntfy alerter.
#[typeshare]
#[derive(
Debug, Clone, PartialEq, Serialize, Deserialize, Builder,
)]
pub struct NtfyAlerterEndpoint {
/// The ntfy topic URL
#[serde(default = "default_ntfy_url")]
#[builder(default = "default_ntfy_url()")]
pub url: String,
}
impl Default for NtfyAlerterEndpoint {
fn default() -> Self {
Self {
url: default_ntfy_url(),
}
}
}
fn default_ntfy_url() -> String {
String::from("http://localhost:8080/komodo")
}
// QUERY
#[typeshare]
+45 -9
View File
@@ -32,26 +32,31 @@ pub type BuildListItem = ResourceListItem<BuildListItemInfo>;
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildListItemInfo {
/// State of the build. Reflects whether most recent build successful.
pub state: BuildState,
/// Unix timestamp in milliseconds of last build
pub last_built_at: I64,
/// The current version of the build
pub version: Version,
/// The builder attached to build.
pub builder_id: String,
/// Whether build is in files on host mode.
pub files_on_host: bool,
/// The git provider domain
pub git_provider: String,
/// The image registry domain
pub image_registry_domain: String,
pub git_provider: Option<String>,
/// The repo used as the source of the build
pub repo: String,
pub repo: Option<String>,
/// The branch of the repo
pub branch: String,
/// State of the build. Reflects whether most recent build successful.
pub state: BuildState,
pub branch: Option<String>,
/// Latest built short commit hash, or null.
pub built_hash: Option<String>,
/// Latest short commit hash, or null. Only for repo based stacks
pub latest_hash: Option<String>,
/// The image registry domain
pub image_registry_domain: Option<String>,
}
#[typeshare]
@@ -73,11 +78,26 @@ pub enum BuildState {
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct BuildInfo {
/// The timestamp build was last built.
pub last_built_at: I64,
/// Latest built short commit hash, or null.
pub built_hash: Option<String>,
/// Latest built commit message, or null. Only for repo based stacks
pub built_message: Option<String>,
/// The last built dockerfile contents.
/// This is updated whenever Komodo successfully runs the build.
pub built_contents: Option<String>,
/// The absolute path to the file
pub remote_path: Option<String>,
/// The remote dockerfile contents, whether on host or in repo.
/// This is updated whenever Komodo refreshes the build cache.
/// It will be empty if the dockerfile is defined directly in the build config.
pub remote_contents: Option<String>,
/// If there was an error in getting the remote contents, it will be here.
pub remote_error: Option<String>,
/// Latest remote short commit hash, or null.
pub latest_hash: Option<String>,
/// Latest remote commit message, or null
@@ -192,10 +212,13 @@ pub struct BuildConfig {
#[builder(default)]
pub webhook_secret: String,
/// The optional command run after repo clone and before docker build.
/// If this is checked, the build will source the files on the host.
/// Use `build_path` and `dockerfile_path` to specify the path on the host.
/// This is useful for those who wish to setup their files on the host,
/// rather than defining the contents in UI or in a git repo.
#[serde(default)]
#[builder(default)]
pub pre_build: SystemCommand,
pub files_on_host: bool,
/// The path of the docker build context relative to the root of the repo.
/// Default: "." (the root of the repo).
@@ -238,6 +261,17 @@ pub struct BuildConfig {
#[builder(default)]
pub extra_args: Vec<String>,
/// The optional command run after repo clone and before docker build.
#[serde(default)]
#[builder(default)]
pub pre_build: SystemCommand,
/// UI defined dockerfile contents.
/// Supports variable / secret interpolation.
#[serde(default)]
#[builder(default)]
pub dockerfile: String,
/// Docker build arguments.
///
/// These values are visible in the final image by running `docker inspect`.
@@ -338,6 +372,8 @@ impl Default for BuildConfig {
image_registry: Default::default(),
webhook_enabled: default_webhook_enabled(),
webhook_secret: Default::default(),
dockerfile: Default::default(),
files_on_host: Default::default(),
}
}
}
@@ -44,6 +44,8 @@ pub struct Env {
pub komodo_host: Option<String>,
/// Override `port`
pub komodo_port: Option<u16>,
/// Override `bind_ip`
pub komodo_bind_ip: Option<String>,
/// Override `passkey`
pub komodo_passkey: Option<String>,
/// Override `passkey` with file
@@ -256,6 +258,11 @@ pub struct CoreConfig {
#[serde(default = "default_core_port")]
pub port: u16,
/// IP address the core server binds to.
/// Default: [::].
#[serde(default = "default_core_bind_ip")]
pub bind_ip: String,
/// Sent in auth header with req to periphery.
/// Should be some secure hash, maybe 20-40 chars.
pub passkey: String,
@@ -521,6 +528,10 @@ fn default_core_port() -> u16 {
9120
}
fn default_core_bind_ip() -> String {
"[::]".to_string()
}
fn default_frontend_path() -> String {
"/app/frontend".to_string()
}
@@ -571,6 +582,7 @@ impl CoreConfig {
title: config.title,
host: config.host,
port: config.port,
bind_ip: config.bind_ip,
passkey: empty_or_redacted(&config.passkey),
first_server: config.first_server,
frontend_path: config.frontend_path,
@@ -114,10 +114,14 @@ pub struct Env {
/// Override `port`
pub periphery_port: Option<u16>,
/// Override `bind_ip`
pub periphery_bind_ip: Option<String>,
/// Override `repo_dir`
pub periphery_repo_dir: Option<PathBuf>,
/// Override `stack_dir`
pub periphery_stack_dir: Option<PathBuf>,
/// Override `build_dir`
pub periphery_build_dir: Option<PathBuf>,
/// Override `stats_polling_rate`
pub periphery_stats_polling_rate: Option<Timelength>,
/// Override `legacy_compose_cli`
@@ -162,6 +166,11 @@ pub struct PeripheryConfig {
#[serde(default = "default_periphery_port")]
pub port: u16,
/// IP address the periphery server binds to.
/// Default: [::].
#[serde(default = "default_periphery_bind_ip")]
pub bind_ip: String,
/// The system directory where Komodo managed repos will be cloned.
/// Default: `/etc/komodo/repos`
#[serde(default = "default_repo_dir")]
@@ -172,6 +181,11 @@ pub struct PeripheryConfig {
#[serde(default = "default_stack_dir")]
pub stack_dir: PathBuf,
/// The system directory where builds will managed.
/// Default: `/etc/komodo/builds`
#[serde(default = "default_build_dir")]
pub build_dir: PathBuf,
/// The rate at which the system stats will be polled to update the cache.
/// Default: `5-sec`
#[serde(default = "default_stats_polling_rate")]
@@ -244,6 +258,10 @@ fn default_periphery_port() -> u16 {
8120
}
fn default_periphery_bind_ip() -> String {
"[::]".to_string()
}
fn default_repo_dir() -> PathBuf {
"/etc/komodo/repos".parse().unwrap()
}
@@ -252,6 +270,10 @@ fn default_stack_dir() -> PathBuf {
"/etc/komodo/stacks".parse().unwrap()
}
fn default_build_dir() -> PathBuf {
"/etc/komodo/builds".parse().unwrap()
}
fn default_stats_polling_rate() -> Timelength {
Timelength::FiveSeconds
}
@@ -272,8 +294,10 @@ impl Default for PeripheryConfig {
fn default() -> Self {
Self {
port: default_periphery_port(),
bind_ip: default_periphery_bind_ip(),
repo_dir: default_repo_dir(),
stack_dir: default_stack_dir(),
build_dir: default_build_dir(),
stats_polling_rate: default_stats_polling_rate(),
legacy_compose_cli: Default::default(),
logging: Default::default(),
@@ -295,8 +319,10 @@ impl PeripheryConfig {
pub fn sanitized(&self) -> PeripheryConfig {
PeripheryConfig {
port: self.port,
bind_ip: self.bind_ip.clone(),
repo_dir: self.repo_dir.clone(),
stack_dir: self.stack_dir.clone(),
build_dir: self.build_dir.clone(),
stats_polling_rate: self.stats_polling_rate,
legacy_compose_cli: self.legacy_compose_cli,
logging: self.logging.clone(),
+11 -3
View File
@@ -118,11 +118,12 @@ pub fn all_logs_success(logs: &[update::Log]) -> bool {
true
}
pub fn optional_string(string: &str) -> Option<String> {
pub fn optional_string(string: impl Into<String>) -> Option<String> {
let string = string.into();
if string.is_empty() {
None
} else {
Some(string.to_string())
Some(string)
}
}
@@ -394,6 +395,8 @@ pub struct CloneArgs {
pub branch: String,
/// Specific commit hash. Optional
pub commit: Option<String>,
/// Use PERIPHERY_BUILD_DIR as the parent folder for the clone.
pub is_build: bool,
/// The clone destination path
pub destination: Option<String>,
/// Command to run after the repo has been cloned
@@ -459,8 +462,9 @@ impl From<&self::build::Build> for CloneArgs {
branch: optional_string(&build.config.branch)
.unwrap_or_else(|| String::from("main")),
commit: optional_string(&build.config.commit),
is_build: true,
destination: None,
on_clone: build.config.pre_build.clone().into_option(),
on_clone: None,
on_pull: None,
https: build.config.git_https,
account: optional_string(&build.config.git_account),
@@ -478,6 +482,7 @@ impl From<&self::repo::Repo> for CloneArgs {
branch: optional_string(&repo.config.branch)
.unwrap_or_else(|| String::from("main")),
commit: optional_string(&repo.config.commit),
is_build: false,
destination: optional_string(&repo.config.path),
on_clone: repo.config.on_clone.clone().into_option(),
on_pull: repo.config.on_pull.clone().into_option(),
@@ -497,6 +502,7 @@ impl From<&self::sync::ResourceSync> for CloneArgs {
branch: optional_string(&sync.config.branch)
.unwrap_or_else(|| String::from("main")),
commit: optional_string(&sync.config.commit),
is_build: false,
destination: None,
on_clone: None,
on_pull: None,
@@ -516,6 +522,7 @@ impl From<&self::stack::Stack> for CloneArgs {
branch: optional_string(&stack.config.branch)
.unwrap_or_else(|| String::from("main")),
commit: optional_string(&stack.config.commit),
is_build: false,
destination: None,
on_clone: None,
on_pull: None,
@@ -713,6 +720,7 @@ pub enum Operation {
DeleteBuild,
RunBuild,
CancelBuild,
WriteDockerfile,
// repo
CreateRepo,
+15
View File
@@ -62,6 +62,21 @@ pub struct Resource<Config: Default, Info: Default = ()> {
pub base_permission: PermissionLevel,
}
impl<C: Default, I: Default> Default for Resource<C, I> {
fn default() -> Self {
Self {
id: String::new(),
name: String::from("temp-resource"),
description: String::new(),
updated_at: 0,
tags: Vec::new(),
info: I::default(),
config: C::default(),
base_permission: PermissionLevel::None,
}
}
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ResourceListItem<Info> {
+8
View File
@@ -77,6 +77,13 @@ pub struct ServerConfig {
#[partial_default(default_timeout_seconds())]
pub timeout_seconds: I64,
/// An optional override passkey to use
/// to authenticate with periphery agent.
/// If this is empty, will use passkey in core config.
#[serde(default)]
#[builder(default)]
pub passkey: String,
/// Sometimes the system stats reports a mount path that is not desired.
/// Use this field to filter it out from the report.
#[serde(default, deserialize_with = "string_list_deserializer")]
@@ -240,6 +247,7 @@ impl Default for ServerConfig {
send_mem_alerts: default_send_alerts(),
send_disk_alerts: default_send_alerts(),
region: Default::default(),
passkey: Default::default(),
cpu_warning: default_cpu_warning(),
cpu_critical: default_cpu_critical(),
mem_warning: default_mem_warning(),
+42 -41
View File
@@ -269,47 +269,6 @@ pub struct StackConfig {
#[builder(default)]
pub skip_secret_interp: bool,
/// If this is checked, the stack will source the files on the host.
/// Use `run_directory` and `file_paths` to specify the path on the host.
/// This is useful for those who wish to setup their files on the host using SSH or similar,
/// rather than defining the contents in UI or in a git repo.
#[serde(default)]
#[builder(default)]
pub files_on_host: bool,
/// Directory to change to (`cd`) before running `docker compose up -d`.
#[serde(default)]
#[builder(default)]
pub run_directory: String,
/// Add paths to compose files, relative to the run path.
/// If this is empty, will use file `compose.yaml`.
#[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 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()")]
@@ -373,6 +332,47 @@ pub struct StackConfig {
#[builder(default)]
pub webhook_force_deploy: bool,
/// If this is checked, the stack will source the files on the host.
/// Use `run_directory` and `file_paths` to specify the path on the host.
/// This is useful for those who wish to setup their files on the host,
/// rather than defining the contents in UI or in a git repo.
#[serde(default)]
#[builder(default)]
pub files_on_host: bool,
/// Directory to change to (`cd`) before running `docker compose up -d`.
#[serde(default)]
#[builder(default)]
pub run_directory: String,
/// Add paths to compose files, relative to the run path.
/// If this is empty, will use file `compose.yaml`.
#[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 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>,
/// Whether to send StackStateChange alerts for this stack.
#[serde(default = "default_send_alerts")]
#[builder(default = "default_send_alerts()")]
@@ -434,6 +434,7 @@ pub struct StackConfig {
/// 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.
/// Supports variable / secret interpolation.
#[serde(default, deserialize_with = "file_contents_deserializer")]
#[partial_attr(serde(
default,
+1 -1
View File
@@ -18,7 +18,7 @@ import { KomodoClient, Types } from "komodo_client";
const komodo = KomodoClient("https://demo.komo.do", {
type: "api-key",
params: {
api_key: "your_key",
key: "your_key",
secret: "your secret",
},
});
+2 -1
View File
@@ -1,9 +1,10 @@
{
"name": "komodo_client",
"version": "1.17.0",
"version": "1.17.1",
"description": "Komodo client package",
"homepage": "https://komo.do",
"main": "dist/lib.js",
"type": "module",
"license": "GPL-3.0",
"publishConfig": {
"access": "public"
+3
View File
@@ -220,6 +220,9 @@ export function KomodoClient(url: string, options: InitOptions) {
// Sleep for a bit before checking for websocket closed
await new Promise((resolve) => setTimeout(resolve, 500));
}
// Sleep for a bit before retrying connection to avoid spam.
await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));
} catch (error) {
console.error(error);
// Sleep for a bit before retrying, maybe Komodo Core is down temporarily.
+2
View File
@@ -61,6 +61,7 @@ export type ReadResponses = {
GetServer: Types.GetServerResponse;
GetServerState: Types.GetServerStateResponse;
GetPeripheryVersion: Types.GetPeripheryVersionResponse;
GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse;
ListDockerContainers: Types.ListDockerContainersResponse;
ListAllDockerContainers: Types.ListAllDockerContainersResponse;
InspectDockerContainer: Types.InspectDockerContainerResponse;
@@ -227,6 +228,7 @@ export type WriteResponses = {
DeleteBuild: Types.Build;
UpdateBuild: Types.Build;
RenameBuild: Types.Update;
WriteBuildFileContents: Types.Update;
RefreshBuildCache: Types.NoData;
CreateBuildWebhook: Types.CreateBuildWebhookResponse;
DeleteBuildWebhook: Types.DeleteBuildWebhookResponse;
+123 -41
View File
@@ -52,7 +52,10 @@ export interface Resource<Config, Info> {
}
export interface ActionConfig {
/** Typescript file contents using pre-initialized `komodo` client. */
/**
* Typescript file contents using pre-initialized `komodo` client.
* Supports variable / secret interpolation.
*/
file_contents?: string;
/** Whether incoming webhooks actually trigger action. */
webhook_enabled: boolean;
@@ -130,7 +133,9 @@ export type AlerterEndpoint =
/** Send alert to a Slack app */
| { type: "Slack", params: SlackAlerterEndpoint }
/** Send alert to a Discord app */
| { type: "Discord", params: DiscordAlerterEndpoint };
| { type: "Discord", params: DiscordAlerterEndpoint }
/** Send alert to Ntfy */
| { type: "Ntfy", params: NtfyAlerterEndpoint };
/** Used to reference a specific resource across all resource types */
export type ResourceTarget =
@@ -210,11 +215,6 @@ export interface Version {
patch: number;
}
export interface SystemCommand {
path?: string;
command?: string;
}
/** Configuration for an image registry */
export interface ImageRegistryConfig {
/**
@@ -231,6 +231,11 @@ export interface ImageRegistryConfig {
organization?: string;
}
export interface SystemCommand {
path?: string;
command?: string;
}
/** The build configuration. */
export interface BuildConfig {
/** Which builder is used to build the image. */
@@ -292,8 +297,13 @@ export interface BuildConfig {
* If its an empty string, use the default secret from the config.
*/
webhook_secret?: string;
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/**
* If this is checked, the build will source the files on the host.
* Use `build_path` and `dockerfile_path` to specify the path on the host.
* This is useful for those who wish to setup their files on the host,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/**
* The path of the docker build context relative to the root of the repo.
* Default: "." (the root of the repo).
@@ -309,6 +319,13 @@ export interface BuildConfig {
use_buildx?: boolean;
/** Any extra docker cli arguments to be included in the build command */
extra_args?: string[];
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/**
* UI defined dockerfile contents.
* Supports variable / secret interpolation.
*/
dockerfile?: string;
/**
* Docker build arguments.
*
@@ -333,11 +350,27 @@ export interface BuildConfig {
}
export interface BuildInfo {
/** The timestamp build was last built. */
last_built_at: I64;
/** Latest built short commit hash, or null. */
built_hash?: string;
/** Latest built commit message, or null. Only for repo based stacks */
built_message?: string;
/**
* The last built dockerfile contents.
* This is updated whenever Komodo successfully runs the build.
*/
built_contents?: string;
/** The absolute path to the file */
remote_path?: string;
/**
* The remote dockerfile contents, whether on host or in repo.
* This is updated whenever Komodo refreshes the build cache.
* It will be empty if the dockerfile is defined directly in the build config.
*/
remote_contents?: string;
/** If there was an error in getting the remote contents, it will be here. */
remote_error?: string;
/** Latest remote short commit hash, or null. */
latest_hash?: string;
/** Latest remote commit message, or null */
@@ -358,26 +391,28 @@ export enum BuildState {
}
export interface BuildListItemInfo {
/** State of the build. Reflects whether most recent build successful. */
state: BuildState;
/** Unix timestamp in milliseconds of last build */
last_built_at: I64;
/** The current version of the build */
version: Version;
/** The builder attached to build. */
builder_id: string;
/** Whether build is in files on host mode. */
files_on_host: boolean;
/** The git provider domain */
git_provider: string;
/** The image registry domain */
image_registry_domain: string;
git_provider?: string;
/** The repo used as the source of the build */
repo: string;
repo?: string;
/** The branch of the repo */
branch: string;
/** State of the build. Reflects whether most recent build successful. */
state: BuildState;
branch?: string;
/** Latest built short commit hash, or null. */
built_hash?: string;
/** Latest short commit hash, or null. Only for repo based stacks */
latest_hash?: string;
/** The image registry domain */
image_registry_domain?: string;
}
export type BuildListItem = ResourceListItem<BuildListItemInfo>;
@@ -1520,6 +1555,12 @@ export interface ServerConfig {
* default: 2
*/
timeout_seconds: I64;
/**
* An optional override passkey to use
* to authenticate with periphery agent.
* If this is empty, will use passkey in core config.
*/
passkey?: string;
/**
* Sometimes the system stats reports a mount path that is not desired.
* Use this field to filter it out from the report.
@@ -1633,31 +1674,6 @@ export interface StackConfig {
destroy_before_deploy?: boolean;
/** Whether to skip secret interpolation into the stack environment variables. */
skip_secret_interp?: boolean;
/**
* If this is checked, the stack will source the files on the host.
* Use `run_directory` and `file_paths` to specify the path on the host.
* This is useful for those who wish to setup their files on the host using SSH or similar,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/** Directory to change to (`cd`) before running `docker compose up -d`. */
run_directory?: string;
/**
* Add paths to compose files, relative to the run path.
* If this is empty, will use file `compose.yaml`.
*/
file_paths?: string[];
/**
* The name of the written environment file before `docker compose up`.
* Relative to the run directory root.
* Default: .env
*/
env_file_path: string;
/**
* Add additional env files to attach with `--env-file`.
* Relative to the run directory root.
*/
additional_env_files?: string[];
/** The git provider domain. Default: github.com */
git_provider: string;
/**
@@ -1697,6 +1713,31 @@ export interface StackConfig {
* If this option is enabled, will always run `DeployStack` without diffing.
*/
webhook_force_deploy?: boolean;
/**
* If this is checked, the stack will source the files on the host.
* Use `run_directory` and `file_paths` to specify the path on the host.
* This is useful for those who wish to setup their files on the host,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/** Directory to change to (`cd`) before running `docker compose up -d`. */
run_directory?: string;
/**
* Add paths to compose files, relative to the run path.
* If this is empty, will use file `compose.yaml`.
*/
file_paths?: string[];
/**
* The name of the written environment file before `docker compose up`.
* Relative to the run directory root.
* Default: .env
*/
env_file_path: string;
/**
* Add additional env files to attach with `--env-file`.
* Relative to the run directory root.
*/
additional_env_files?: string[];
/** Whether to send StackStateChange alerts for this stack. */
send_alerts: boolean;
/** Used with `registry_account` to login to a registry before docker compose up. */
@@ -1728,6 +1769,7 @@ export interface StackConfig {
* 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.
* Supports variable / secret interpolation.
*/
file_contents?: string;
/**
@@ -2053,6 +2095,7 @@ export enum Operation {
DeleteBuild = "DeleteBuild",
RunBuild = "RunBuild",
CancelBuild = "CancelBuild",
WriteDockerfile = "WriteDockerfile",
CreateRepo = "CreateRepo",
UpdateRepo = "UpdateRepo",
RenameRepo = "RenameRepo",
@@ -4007,6 +4050,8 @@ export interface CloneArgs {
branch: string;
/** Specific commit hash. Optional */
commit?: string;
/** Use PERIPHERY_BUILD_DIR as the parent folder for the clone. */
is_build: boolean;
/** The clone destination path */
destination?: string;
/** Command to run after the repo has been cloned */
@@ -5169,6 +5214,27 @@ export interface GetDeploymentsSummaryResponse {
unknown: I64;
}
/**
* Gets a summary of data relating to all containers.
* Response: [GetDockerContainersSummaryResponse].
*/
export interface GetDockerContainersSummary {
}
/** Response for [GetDockerContainersSummary] */
export interface GetDockerContainersSummaryResponse {
/** The total number of Containers */
total: number;
/** The number of Containers with Running state */
running: number;
/** The number of Containers with Stopped or Paused or Created state */
stopped: number;
/** The number of Containers with Restarting or Dead state */
unhealthy: number;
/** The number of Containers with Unknown state */
unknown: number;
}
/**
* Get a specific docker registry account.
* Response: [GetDockerRegistryAccountResponse].
@@ -6354,6 +6420,12 @@ export interface NameAndId {
id: string;
}
/** Configuration for a Ntfy alerter. */
export interface NtfyAlerterEndpoint {
/** The ntfy topic URL */
url: string;
}
/** Pauses all containers on the target server. Response: [Update] */
export interface PauseAllContainers {
/** Name or id */
@@ -7498,6 +7570,14 @@ export interface UrlBuilderConfig {
passkey?: string;
}
/** Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update]. */
export interface WriteBuildFileContents {
/** The name or id of the target Build. */
build: string;
/** The dockerfile contents to write. */
contents: string;
}
/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */
export interface WriteStackFileContents {
/** The name or id of the target Stack. */
@@ -7648,6 +7728,7 @@ export type ReadRequest =
| { type: "InspectDockerImage", params: InspectDockerImage }
| { type: "ListDockerImageHistory", params: ListDockerImageHistory }
| { type: "InspectDockerVolume", params: InspectDockerVolume }
| { type: "GetDockerContainersSummary", params: GetDockerContainersSummary }
| { type: "ListAllDockerContainers", params: ListAllDockerContainers }
| { type: "ListDockerContainers", params: ListDockerContainers }
| { type: "ListDockerNetworks", params: ListDockerNetworks }
@@ -7763,6 +7844,7 @@ export type WriteRequest =
| { type: "DeleteBuild", params: DeleteBuild }
| { type: "UpdateBuild", params: UpdateBuild }
| { type: "RenameBuild", params: RenameBuild }
| { type: "WriteBuildFileContents", params: WriteBuildFileContents }
| { type: "RefreshBuildCache", params: RefreshBuildCache }
| { type: "CreateBuildWebhook", params: CreateBuildWebhook }
| { type: "DeleteBuildWebhook", params: DeleteBuildWebhook }
+37 -1
View File
@@ -1,4 +1,4 @@
use komodo_client::entities::update::Log;
use komodo_client::entities::{FileContents, update::Log};
use resolver_api::Resolve;
use serde::{Deserialize, Serialize};
@@ -21,6 +21,42 @@ pub type BuildResponse = Vec<Log>;
//
/// Get the dockerfile contents on the host, for builds using
/// `files_on_host`.
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
#[response(GetDockerfileContentsOnHostResponse)]
#[error(serror::Error)]
pub struct GetDockerfileContentsOnHost {
/// The name of the build
pub name: String,
/// The build path for the build.
pub build_path: String,
/// The dockerfile path for the build, relative to the build_path
pub dockerfile_path: String,
}
pub type GetDockerfileContentsOnHostResponse = FileContents;
//
/// Write the dockerfile contents to the file on the host, for build using
/// `files_on_host`.
#[derive(Debug, Clone, Serialize, Deserialize, Resolve)]
#[response(Log)]
#[error(serror::Error)]
pub struct WriteDockerfileContentsToHost {
/// The name of the build
pub name: String,
/// The build path for the build.
pub build_path: String,
/// The dockerfile path for the build, relative to the build_path
pub dockerfile_path: String,
/// The contents to write.
pub contents: String,
}
//
#[derive(Serialize, Deserialize, Debug, Clone, Resolve)]
#[response(Log)]
#[error(serror::Error)]
+2
View File
@@ -106,4 +106,6 @@ pub struct RenameRepo {
#[error(serror::Error)]
pub struct DeleteRepo {
pub name: String,
/// Clears
pub is_build: bool,
}
+7 -1
View File
@@ -122,13 +122,19 @@ PERIPHERY_PASSKEYS=${KOMODO_PASSKEY}
## Specify the root directory used by Periphery agent.
PERIPHERY_ROOT_DIRECTORY=/etc/komodo
PERIPHERY_REPO_DIR=$PERIPHERY_ROOT_DIRECTORY/repos
PERIPHERY_STACK_DIR=$PERIPHERY_ROOT_DIRECTORY/stacks
PERIPHERY_BUILD_DIR=$PERIPHERY_ROOT_DIRECTORY/builds
## Enable SSL using self signed certificates.
## Connect to Periphery at https://address:8120.
PERIPHERY_SSL_ENABLED=true
PERIPHERY_SSL_KEY_FILE=$PERIPHERY_ROOT_DIRECTORY/ssl/key.pem
PERIPHERY_SSL_CERT_FILE=$PERIPHERY_ROOT_DIRECTORY/ssl/cert.pem
## If the disk size is overreporting, can use one of these to
## whitelist / blacklist the disks to filter them, whichever is easier.
## Accepts comma separated list of paths.
## Usually whitelisting just /etc/hostname gives correct size.
PERIPHERY_INCLUDE_DISK_MOUNTS=/etc/hostname
# PERIPHERY_EXCLUDE_DISK_MOUNTS=/snap,/etc/repos
# PERIPHERY_EXCLUDE_DISK_MOUNTS=/snap,/etc/repos
-5
View File
@@ -64,11 +64,6 @@ services:
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
env_file: ./compose.env
environment:
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
volumes:
## Mount external docker socket
- /var/run/docker.sock:/var/run/docker.sock
+8 -1
View File
@@ -17,6 +17,7 @@ services:
environment:
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
PERIPHERY_BUILD_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/builds
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
## Pass the same passkey as used by the Komodo Core connecting to this Periphery agent.
@@ -36,4 +37,10 @@ services:
## Must be the same inside and outside the container,
## or docker will get confused. See https://github.com/moghtech/komodo/discussions/180.
## Default: /etc/komodo.
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
- ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}:${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}
## If periphery is being run remote from the core server, ports need
## to be exposed on the host.
# ports:
# - 8120:8120
## If you want to use a custom periphery config file, use command to pass it to periphery.
# command: periphery --config-path ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/periphery.config.toml
-5
View File
@@ -75,11 +75,6 @@ services:
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
env_file: ./compose.env
environment:
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
volumes:
## Mount external docker socket
- /var/run/docker.sock:/var/run/docker.sock
-5
View File
@@ -59,11 +59,6 @@ services:
logging:
driver: ${COMPOSE_LOGGING_DRIVER:-local}
env_file: ./compose.env
environment:
PERIPHERY_REPO_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/repos
PERIPHERY_STACK_DIR: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/stacks
PERIPHERY_SSL_KEY_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/key.pem
PERIPHERY_SSL_CERT_FILE: ${PERIPHERY_ROOT_DIRECTORY:-/etc/komodo}/ssl/cert.pem
volumes:
## Mount external docker socket
- /var/run/docker.sock:/var/run/docker.sock
+6
View File
@@ -33,6 +33,12 @@ host = "https://demo.komo.do"
## Default: 9120
port = 9120
## The IP address the core server will bind to.
## The default will allow it to accept external IPv4 and IPv6 connections.
## Env: KOMODO_BIND_IP
## Default: [::]
bind_ip = "[::]"
## This is the token used to authenticate core requests to periphery.
## Ensure this matches a passkey in the connected periphery configs.
## If the periphery servers don't have passkeys configured, this doesn't need to be changed.
+12
View File
@@ -18,6 +18,12 @@
## Default: 8120
port = 8120
## The IP address the periphery server will bind to.
## The default will allow it to accept external IPv4 and IPv6 connections.
## Env: PERIPHERY_BIND_IP
## Default: [::]
bind_ip = "[::]"
## The directory periphery will use to manage repos.
## The periphery user must have write access to this directory.
## Env: PERIPHERY_REPO_DIR
@@ -30,6 +36,12 @@ repo_dir = "/etc/komodo/repos"
## Default: /etc/komodo/stacks
stack_dir = "/etc/komodo/stacks"
## The directory periphery will use to manage builds.
## The periphery user must have write access to this directory.
## Env: PERIPHERY_BUILD_DIR
## Default: /etc/komodo/builds
build_dir = "/etc/komodo/builds"
## How often Periphery polls the host for system stats,
## like CPU / memory usage.
## Env: PERIPHERY_STATS_POLLING_RATE
+10 -5
View File
@@ -77,7 +77,7 @@ Ensure that the user which periphery is run as has access to the docker group wi
```
periphery \
--config-path /path/to/periphery.config.base.toml \
--config-path /other_path/to/overide-periphery-config-directory \
--config-path /other_path/to/override-periphery-config-directory \
--config-keyword periphery \
--config-keyword config \
--merge-nested-config true
@@ -87,21 +87,26 @@ periphery \
You can run `periphery --help` to see the manual.
:::
When running periphery in docker, use [command](https://docs.docker.com/reference/compose-file/services/#command) to pass in additional arguments.
```
command: periphery --config-path /path/in/container/to/periphery.config.base.toml
```
### Passing config files
Either file paths or directory paths can be passed to `--config-path`.
When using directories, the file entries can be filtered by name with the `--config-keyword` argument, which can be passed multiple times to add more keywords. If passed, then only config files with file names that contain all keywords will be merged.
When passing multiple config files, later --config-path given in the command will always overide previous ones. Directory config files are merged in alphabetical order by name, so `config_b.toml` will overide `config_a.toml`.
When passing multiple config files, later --config-path given in the command will always override previous ones. Directory config files are merged in alphabetical order by name, so `config_b.toml` will override `config_a.toml`.
There are two ways to merge config files. The default behavior is to completely replace any base fields with whatever fields are present in the overide config. So if you pass `allowed_ips = []` in your overide config, the final allowed_ips will be an empty list as well.
There are two ways to merge config files. The default behavior is to completely replace any base fields with whatever fields are present in the override config. So if you pass `allowed_ips = []` in your override config, the final allowed_ips will be an empty list as well.
`--merge-nested-config true` will merge config fields recursively and extend config array fields.
For example, with `--merge-nested-config true` you can specify an allowed ip in the base config, and another in the overide config, they will both be present in the final config.
For example, with `--merge-nested-config true` you can specify an allowed ip in the base config, and another in the override config, they will both be present in the final config.
Similarly, you can specify a base docker / github account pair, and extend them with additional accounts in the overide config.
Similarly, you can specify a base docker / github account pair, and extend them with additional accounts in the override config.
## Configuration
+15
View File
@@ -0,0 +1,15 @@
# Other Resources
### Posts and Guides
- [Migrating to Komodo](https://blog.foxxmd.dev/posts/migrating-to-komodo) by [FoxxMD](https://github.com/FoxxMD)
- [FAQ, Tips, and Tricks](https://blog.foxxmd.dev/posts/komodo-tips-tricks) by [FoxxMD](https://github.com/FoxxMD)
- [Compose Environments Explained](https://blog.foxxmd.dev/posts/compose-envs-explained) by [FoxxMD](https://github.com/FoxxMD)
- [How To: Automate version updates for your self-hosted Docker containers with Gitea, Renovate, and Komodo](https://nickcunningh.am/blog/how-to-automate-version-updates-for-your-self-hosted-docker-containers-with-gitea-renovate-and-komodo) by [TheNickOfTime](https://github.com/TheNickOfTime)
### Community Alerters
These provide alerting implementations which can be used with the `Custom` Alerter type.
- [Discord](https://github.com/FoxxMD/deploy-discord-alerter) by [FoxxMD](https://github.com/FoxxMD)
- [Telegram](https://github.com/mattsmallman/komodo-alert-to-telgram) by [mattsmallman](https://github.com/mattsmallman)
- [Ntfy](https://github.com/FoxxMD/deploy-ntfy-alerter) by [FoxxMD](https://github.com/FoxxMD)
- [Gotify](https://github.com/FoxxMD/deploy-gotify-alerter) by [FoxxMD](https://github.com/FoxxMD)
- [Apprise](https://github.com/FoxxMD/deploy-apprise-alerter) by [FoxxMD](https://github.com/FoxxMD)
+2 -2
View File
@@ -1,7 +1,7 @@
# Sqlite
:::info
FerritDB + Sqlite authentication support is [still experimental](https://docs.ferretdb.io/security/authentication/#experimental-authentication-mode),
FerretDB + Sqlite authentication support is [still experimental](https://docs.ferretdb.io/security/authentication/#experimental-authentication-mode),
and is not enabled in this database configuration. Because of this, the values for `DB_USERNAME` and `DB_PASSWORD`
are ignored when using Sqlite, and the database port is not exposed publicly by default.
:::
@@ -22,4 +22,4 @@ docker compose -p komodo -f komodo/sqlite.compose.yaml --env-file komodo/compose
import ComposeAndEnv from "@site/src/components/ComposeAndEnv";
<ComposeAndEnv file_name="sqlite.compose.yaml" />
```
```
+2 -1
View File
@@ -65,7 +65,8 @@ const sidebars: SidebarsConfig = {
"webhooks",
"version-upgrades",
"api",
"development"
"development",
"other-resources"
],
};
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:20.12-alpine as builder
FROM node:20.12-alpine AS builder
WORKDIR /builder
@@ -7,7 +7,7 @@ COPY ./client/core/ts ./client
# Optionally specify a specific Komodo host.
ARG VITE_KOMODO_HOST=""
ENV VITE_KOMODO_HOST=${VITE_KOMODO_HOST}
ENV VITE_KOMODO_HOST=$VITE_KOMODO_HOST
# Build and link the client
RUN cd client && yarn && yarn build && yarn link
+2
View File
@@ -117,6 +117,8 @@ export function KomodoClient(url, options) {
// Sleep for a bit before checking for websocket closed
await new Promise((resolve) => setTimeout(resolve, 500));
}
// Sleep for a bit before retrying to avoid spam.
await new Promise((resolve) => setTimeout(resolve, retry_timeout_ms));
}
catch (error) {
console.error(error);
+2
View File
@@ -46,6 +46,7 @@ export type ReadResponses = {
GetServer: Types.GetServerResponse;
GetServerState: Types.GetServerStateResponse;
GetPeripheryVersion: Types.GetPeripheryVersionResponse;
GetDockerContainersSummary: Types.GetDockerContainersSummaryResponse;
ListDockerContainers: Types.ListDockerContainersResponse;
ListAllDockerContainers: Types.ListAllDockerContainersResponse;
InspectDockerContainer: Types.InspectDockerContainerResponse;
@@ -168,6 +169,7 @@ export type WriteResponses = {
DeleteBuild: Types.Build;
UpdateBuild: Types.Build;
RenameBuild: Types.Update;
WriteBuildFileContents: Types.Update;
RefreshBuildCache: Types.NoData;
CreateBuildWebhook: Types.CreateBuildWebhookResponse;
DeleteBuildWebhook: Types.DeleteBuildWebhookResponse;
+124 -39
View File
@@ -43,7 +43,10 @@ export interface Resource<Config, Info> {
base_permission?: PermissionLevel;
}
export interface ActionConfig {
/** Typescript file contents using pre-initialized `komodo` client. */
/**
* Typescript file contents using pre-initialized `komodo` client.
* Supports variable / secret interpolation.
*/
file_contents?: string;
/** Whether incoming webhooks actually trigger action. */
webhook_enabled: boolean;
@@ -119,6 +122,11 @@ export type AlerterEndpoint =
| {
type: "Discord";
params: DiscordAlerterEndpoint;
}
/** Send alert to Ntfy */
| {
type: "Ntfy";
params: NtfyAlerterEndpoint;
};
/** Used to reference a specific resource across all resource types */
export type ResourceTarget = {
@@ -216,10 +224,6 @@ export interface Version {
minor: number;
patch: number;
}
export interface SystemCommand {
path?: string;
command?: string;
}
/** Configuration for an image registry */
export interface ImageRegistryConfig {
/**
@@ -235,6 +239,10 @@ export interface ImageRegistryConfig {
*/
organization?: string;
}
export interface SystemCommand {
path?: string;
command?: string;
}
/** The build configuration. */
export interface BuildConfig {
/** Which builder is used to build the image. */
@@ -296,8 +304,13 @@ export interface BuildConfig {
* If its an empty string, use the default secret from the config.
*/
webhook_secret?: string;
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/**
* If this is checked, the build will source the files on the host.
* Use `build_path` and `dockerfile_path` to specify the path on the host.
* This is useful for those who wish to setup their files on the host,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/**
* The path of the docker build context relative to the root of the repo.
* Default: "." (the root of the repo).
@@ -313,6 +326,13 @@ export interface BuildConfig {
use_buildx?: boolean;
/** Any extra docker cli arguments to be included in the build command */
extra_args?: string[];
/** The optional command run after repo clone and before docker build. */
pre_build?: SystemCommand;
/**
* UI defined dockerfile contents.
* Supports variable / secret interpolation.
*/
dockerfile?: string;
/**
* Docker build arguments.
*
@@ -336,11 +356,27 @@ export interface BuildConfig {
labels?: string;
}
export interface BuildInfo {
/** The timestamp build was last built. */
last_built_at: I64;
/** Latest built short commit hash, or null. */
built_hash?: string;
/** Latest built commit message, or null. Only for repo based stacks */
built_message?: string;
/**
* The last built dockerfile contents.
* This is updated whenever Komodo successfully runs the build.
*/
built_contents?: string;
/** The absolute path to the file */
remote_path?: string;
/**
* The remote dockerfile contents, whether on host or in repo.
* This is updated whenever Komodo refreshes the build cache.
* It will be empty if the dockerfile is defined directly in the build config.
*/
remote_contents?: string;
/** If there was an error in getting the remote contents, it will be here. */
remote_error?: string;
/** Latest remote short commit hash, or null. */
latest_hash?: string;
/** Latest remote commit message, or null */
@@ -358,26 +394,28 @@ export declare enum BuildState {
Unknown = "Unknown"
}
export interface BuildListItemInfo {
/** State of the build. Reflects whether most recent build successful. */
state: BuildState;
/** Unix timestamp in milliseconds of last build */
last_built_at: I64;
/** The current version of the build */
version: Version;
/** The builder attached to build. */
builder_id: string;
/** Whether build is in files on host mode. */
files_on_host: boolean;
/** The git provider domain */
git_provider: string;
/** The image registry domain */
image_registry_domain: string;
git_provider?: string;
/** The repo used as the source of the build */
repo: string;
repo?: string;
/** The branch of the repo */
branch: string;
/** State of the build. Reflects whether most recent build successful. */
state: BuildState;
branch?: string;
/** Latest built short commit hash, or null. */
built_hash?: string;
/** Latest short commit hash, or null. Only for repo based stacks */
latest_hash?: string;
/** The image registry domain */
image_registry_domain?: string;
}
export type BuildListItem = ResourceListItem<BuildListItemInfo>;
export interface BuildQuerySpecifics {
@@ -1622,6 +1660,12 @@ export interface ServerConfig {
* default: 2
*/
timeout_seconds: I64;
/**
* An optional override passkey to use
* to authenticate with periphery agent.
* If this is empty, will use passkey in core config.
*/
passkey?: string;
/**
* Sometimes the system stats reports a mount path that is not desired.
* Use this field to filter it out from the report.
@@ -1732,31 +1776,6 @@ export interface StackConfig {
destroy_before_deploy?: boolean;
/** Whether to skip secret interpolation into the stack environment variables. */
skip_secret_interp?: boolean;
/**
* If this is checked, the stack will source the files on the host.
* Use `run_directory` and `file_paths` to specify the path on the host.
* This is useful for those who wish to setup their files on the host using SSH or similar,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/** Directory to change to (`cd`) before running `docker compose up -d`. */
run_directory?: string;
/**
* Add paths to compose files, relative to the run path.
* If this is empty, will use file `compose.yaml`.
*/
file_paths?: string[];
/**
* The name of the written environment file before `docker compose up`.
* Relative to the run directory root.
* Default: .env
*/
env_file_path: string;
/**
* Add additional env files to attach with `--env-file`.
* Relative to the run directory root.
*/
additional_env_files?: string[];
/** The git provider domain. Default: github.com */
git_provider: string;
/**
@@ -1796,6 +1815,31 @@ export interface StackConfig {
* If this option is enabled, will always run `DeployStack` without diffing.
*/
webhook_force_deploy?: boolean;
/**
* If this is checked, the stack will source the files on the host.
* Use `run_directory` and `file_paths` to specify the path on the host.
* This is useful for those who wish to setup their files on the host,
* rather than defining the contents in UI or in a git repo.
*/
files_on_host?: boolean;
/** Directory to change to (`cd`) before running `docker compose up -d`. */
run_directory?: string;
/**
* Add paths to compose files, relative to the run path.
* If this is empty, will use file `compose.yaml`.
*/
file_paths?: string[];
/**
* The name of the written environment file before `docker compose up`.
* Relative to the run directory root.
* Default: .env
*/
env_file_path: string;
/**
* Add additional env files to attach with `--env-file`.
* Relative to the run directory root.
*/
additional_env_files?: string[];
/** Whether to send StackStateChange alerts for this stack. */
send_alerts: boolean;
/** Used with `registry_account` to login to a registry before docker compose up. */
@@ -1827,6 +1871,7 @@ export interface StackConfig {
* 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.
* Supports variable / secret interpolation.
*/
file_contents?: string;
/**
@@ -2137,6 +2182,7 @@ export declare enum Operation {
DeleteBuild = "DeleteBuild",
RunBuild = "RunBuild",
CancelBuild = "CancelBuild",
WriteDockerfile = "WriteDockerfile",
CreateRepo = "CreateRepo",
UpdateRepo = "UpdateRepo",
RenameRepo = "RenameRepo",
@@ -3869,6 +3915,8 @@ export interface CloneArgs {
branch: string;
/** Specific commit hash. Optional */
commit?: string;
/** Use PERIPHERY_BUILD_DIR as the parent folder for the clone. */
is_build: boolean;
/** The clone destination path */
destination?: string;
/** Command to run after the repo has been cloned */
@@ -4925,6 +4973,25 @@ export interface GetDeploymentsSummaryResponse {
/** The number of Deployments with Unknown state */
unknown: I64;
}
/**
* Gets a summary of data relating to all containers.
* Response: [GetDockerContainersSummaryResponse].
*/
export interface GetDockerContainersSummary {
}
/** Response for [GetDockerContainersSummary] */
export interface GetDockerContainersSummaryResponse {
/** The total number of Containers */
total: number;
/** The number of Containers with Running state */
running: number;
/** The number of Containers with Stopped or Paused or Created state */
stopped: number;
/** The number of Containers with Restarting or Dead state */
unhealthy: number;
/** The number of Containers with Unknown state */
unknown: number;
}
/**
* Get a specific docker registry account.
* Response: [GetDockerRegistryAccountResponse].
@@ -5988,6 +6055,11 @@ export interface NameAndId {
name: string;
id: string;
}
/** Configuration for a Ntfy alerter. */
export interface NtfyAlerterEndpoint {
/** The ntfy topic URL */
url: string;
}
/** Pauses all containers on the target server. Response: [Update] */
export interface PauseAllContainers {
/** Name or id */
@@ -7031,6 +7103,13 @@ export interface UrlBuilderConfig {
/** A custom passkey to use. Otherwise, use the default passkey. */
passkey?: string;
}
/** Update dockerfile contents in Files on Server or Git Repo mode. Response: [Update]. */
export interface WriteBuildFileContents {
/** The name or id of the target Build. */
build: string;
/** The dockerfile contents to write. */
contents: string;
}
/** Update file contents in Files on Server or Git Repo mode. Response: [Update]. */
export interface WriteStackFileContents {
/** The name or id of the target Stack. */
@@ -7401,6 +7480,9 @@ export type ReadRequest = {
} | {
type: "InspectDockerVolume";
params: InspectDockerVolume;
} | {
type: "GetDockerContainersSummary";
params: GetDockerContainersSummary;
} | {
type: "ListAllDockerContainers";
params: ListAllDockerContainers;
@@ -7736,6 +7818,9 @@ export type WriteRequest = {
} | {
type: "RenameBuild";
params: RenameBuild;
} | {
type: "WriteBuildFileContents";
params: WriteBuildFileContents;
} | {
type: "RefreshBuildCache";
params: RefreshBuildCache;
+1
View File
@@ -236,6 +236,7 @@ export var Operation;
Operation["DeleteBuild"] = "DeleteBuild";
Operation["RunBuild"] = "RunBuild";
Operation["CancelBuild"] = "CancelBuild";
Operation["WriteDockerfile"] = "WriteDockerfile";
Operation["CreateRepo"] = "CreateRepo";
Operation["UpdateRepo"] = "UpdateRepo";
Operation["RenameRepo"] = "RenameRepo";
@@ -105,7 +105,7 @@ export const ActionConfig = ({ id }: { id: string }) => {
},
{
label: "Webhook",
description: `Configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
components: {
["Builder" as any]: () => (
<WebhookBuilder git_provider={ACTION_GIT_PROVIDER}>
@@ -13,6 +13,7 @@ const ENDPOINT_TYPES: Types.AlerterEndpoint["type"][] = [
"Custom",
"Discord",
"Slack",
"Ntfy",
];
export const EndpointConfig = ({
@@ -67,5 +68,7 @@ const default_url = (type: Types.AlerterEndpoint["type"]) => {
? "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
: type === "Discord"
? "https://discord.com/api/webhooks/XXXXXXXXXXXX/XXXX-XXXXXXXXXX"
: type === "Ntfy"
? "https://ntfy.sh/komodo"
: "";
};
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,11 @@
import { Section } from "@components/layouts";
import { useInvalidate, useRead, useUser, useWrite } from "@lib/hooks";
import {
useInvalidate,
useLocalStorage,
useRead,
useUser,
useWrite,
} from "@lib/hooks";
import { RequiredResourceComponents } from "@types";
import { Factory, FolderGit, Hammer, Loader2, RefreshCcw } from "lucide-react";
import { BuildConfig } from "./config";
@@ -13,7 +19,6 @@ import {
stroke_color_class_by_intention,
} from "@lib/color";
import { cn } from "@lib/utils";
import { useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ui/tabs";
import { ResourceComponents } from "..";
import { Types } from "komodo_client";
@@ -27,6 +32,7 @@ import { useBuilder } from "../builder";
import { RenameResource } from "@components/config/util";
import { GroupActions } from "@components/group-actions";
import { Tooltip, TooltipContent, TooltipTrigger } from "@ui/tooltip";
import { BuildInfo } from "./info";
export const useBuild = (id?: string) =>
useRead("ListBuilds", {}, { refetchInterval: 10_000 }).data?.find(
@@ -42,8 +48,11 @@ const BuildIcon = ({ id, size }: { id?: string; size: number }) => {
return <Hammer className={cn(`w-${size} h-${size}`, state && color)} />;
};
const ConfigOrDeployments = ({ id }: { id: string }) => {
const [view, setView] = useState("Config");
const ConfigInfoDeployments = ({ id }: { id: string }) => {
const [view, setView] = useLocalStorage<"Config" | "Info" | "Deployments">(
"build-tabs-v1",
"Config"
);
const deployments = useRead("ListDeployments", {}).data?.filter(
(deployment) => deployment.info.build_id === id
);
@@ -53,6 +62,9 @@ const ConfigOrDeployments = ({ id }: { id: string }) => {
<TabsTrigger value="Config" className="w-[110px]">
Config
</TabsTrigger>
<TabsTrigger value="Info" className="w-[110px]">
Info
</TabsTrigger>
<TabsTrigger
value="Deployments"
className="w-[110px]"
@@ -64,13 +76,16 @@ const ConfigOrDeployments = ({ id }: { id: string }) => {
);
return (
<Tabs
value={deploymentsDisabled ? "Config" : view}
onValueChange={setView}
value={deploymentsDisabled && view === "Deployments" ? "Config" : view}
onValueChange={setView as any}
className="grid gap-4"
>
<TabsContent value="Config">
<BuildConfig id={id} titleOther={titleOther} />
</TabsContent>
<TabsContent value="Info">
<BuildInfo id={id} titleOther={titleOther} />
</TabsContent>
<TabsContent value="Deployments">
<Section
titleOther={titleOther}
@@ -265,7 +280,7 @@ export const BuildComponents: RequiredResourceComponents = {
Page: {},
Config: ConfigOrDeployments,
Config: ConfigInfoDeployments,
DangerZone: ({ id }) => (
<>
@@ -0,0 +1,226 @@
import { Section } from "@components/layouts";
import { ReactNode, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@ui/card";
import { useFullBuild } from ".";
import { cn, updateLogToHtml } from "@lib/utils";
import { MonacoEditor } from "@components/monaco";
import { useEditPermissions } from "@pages/resource";
import { ConfirmUpdate } from "@components/config/util";
import { useLocalStorage, useRead, useWrite } from "@lib/hooks";
import { Button } from "@ui/button";
import { Clock, FilePlus, History } from "lucide-react";
import { useToast } from "@ui/use-toast";
import { ConfirmButton, ShowHideButton } from "@components/util";
import { DEFAULT_BUILD_DOCKERFILE_CONTENTS } from "./config";
import { fmt_duration } from "@lib/formatting";
export const BuildInfo = ({
id,
titleOther,
}: {
id: string;
titleOther: ReactNode;
}) => {
const [edits, setEdits] = useLocalStorage<{ contents: string | undefined }>(
`build-${id}-edits`,
{ contents: undefined }
);
const [showContents, setShowContents] = useState(true);
const { canWrite } = useEditPermissions({ type: "Build", id });
const { toast } = useToast();
const { mutateAsync, isPending } = useWrite("WriteBuildFileContents", {
onSuccess: (res) => {
toast({
title: res.success ? "Contents written." : "Failed to write contents.",
variant: res.success ? undefined : "destructive",
});
},
});
const build = useFullBuild(id);
const recent_builds = useRead("ListUpdates", {
query: { "target.type": "Build", "target.id": id, operation: "RunBuild" },
}).data;
const _last_build = recent_builds?.updates[0];
const last_build = useRead(
"GetUpdate",
{
id: _last_build?.id!,
},
{ enabled: !!_last_build }
).data;
const file_on_host = build?.config?.files_on_host ?? false;
const git_repo = build?.config?.repo ? true : false;
const canEdit = canWrite && (file_on_host || git_repo);
const remote_path = build?.info?.remote_path;
const remote_contents = build?.info?.remote_contents;
const remote_error = build?.info?.remote_error;
return (
<Section titleOther={titleOther}>
{/* Errors */}
{remote_error && remote_error.length > 0 && (
<Card className="flex flex-col gap-4">
<CardHeader className="flex flex-row justify-between items-center pb-0">
<div className="font-mono flex gap-2">
{remote_path && (
<>
<div className="text-muted-foreground">Path:</div>
{remote_path}
</>
)}
</div>
{canEdit && (
<ConfirmButton
title="Initialize File"
icon={<FilePlus className="w-4 h-4" />}
onClick={() => {
if (build) {
mutateAsync({
build: build.name,
contents: DEFAULT_BUILD_DOCKERFILE_CONTENTS,
});
}
}}
loading={isPending}
/>
)}
</CardHeader>
<CardContent className="pr-8">
<pre
dangerouslySetInnerHTML={{
__html: updateLogToHtml(remote_error),
}}
className="max-h-[500px] overflow-y-auto"
/>
</CardContent>
</Card>
)}
{/* Update latest contents */}
{remote_contents && remote_contents.length > 0 && (
<Card className="flex flex-col gap-4">
<CardHeader
className={cn(
"flex flex-row justify-between items-center",
showContents && "pb-0"
)}
>
{remote_path && (
<CardTitle className="font-mono flex gap-2">
<div className="text-muted-foreground">Path:</div>
{remote_path}
</CardTitle>
)}
<div className="flex items-center gap-2">
{canEdit && (
<>
<Button
variant="outline"
onClick={() => setEdits({ contents: undefined })}
className="flex items-center gap-2"
disabled={!edits.contents}
>
<History className="w-4 h-4" />
Reset
</Button>
<ConfirmUpdate
previous={{ contents: remote_contents }}
content={{ contents: edits.contents }}
onConfirm={async () => {
if (build) {
return await mutateAsync({
build: build.name,
contents: edits.contents!,
}).then(() => setEdits({ contents: undefined }));
}
}}
disabled={!edits.contents}
language="dockerfile"
loading={isPending}
/>
</>
)}
<ShowHideButton show={showContents} setShow={setShowContents} />
</div>
</CardHeader>
{showContents && (
<CardContent className="pr-8">
<MonacoEditor
value={edits.contents ?? remote_contents}
language="dockerfile"
readOnly={!canEdit}
onValueChange={(contents) => setEdits({ contents })}
/>
</CardContent>
)}
</Card>
)}
{/* Last build output */}
{last_build && last_build.logs.length > 0 && (
<code className="font-bold">Last Build Logs</code>
)}
{last_build &&
last_build.logs.length > 0 &&
last_build.logs?.map((log, i) => (
<Card key={i}>
<CardHeader className="flex-col">
<CardTitle>{log.stage}</CardTitle>
<CardDescription className="flex gap-2">
<span>
Stage {i + 1} of {last_build.logs.length}
</span>
<span>|</span>
<span className="flex items-center gap-2">
<Clock className="w-4 h-4" />
{fmt_duration(log.start_ts, log.end_ts)}
</span>
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2">
{log.command && (
<div>
<CardDescription>command</CardDescription>
<pre className="max-h-[500px] overflow-y-auto">
{log.command}
</pre>
</div>
)}
{log.stdout && (
<div>
<CardDescription>stdout</CardDescription>
<pre
dangerouslySetInnerHTML={{
__html: updateLogToHtml(log.stdout),
}}
className="max-h-[500px] overflow-y-auto"
/>
</div>
)}
{log.stderr && (
<div>
<CardDescription>stderr</CardDescription>
<pre
dangerouslySetInnerHTML={{
__html: updateLogToHtml(log.stderr),
}}
className="max-h-[500px] overflow-y-auto"
/>
</div>
)}
</CardContent>
</Card>
))}
</Section>
);
};
@@ -27,10 +27,19 @@ export const BuildTable = ({ builds }: { builds: Types.BuildListItem[] }) => {
size: 200,
},
{
accessorKey: "info.repo",
header: ({ column }) => (
<SortableHeader column={column} title="Repo" />
),
// header: ({ column }) => (
// <SortableHeader column={column} title="Source" />
// ),
header: "Source",
accessorFn: ({ info: { files_on_host, repo } }) => {
if (files_on_host) {
return "Files on Server";
} else if (repo) {
return repo;
} else {
return "UI Defined";
}
},
size: 200,
},
{
@@ -222,7 +222,9 @@ const ProcedureConfigInner = ({
<CardHeader>
<CardTitle>Webhook</CardTitle>
<CardDescription>
Trigger this Procedure with a webhook.
Copy the webhook given here, and configure your
{webhook_integration}-style repo provider to send webhooks to
Komodo
</CardDescription>
</CardHeader>
<CardContent>
@@ -238,8 +238,7 @@ export const RepoConfig = ({ id }: { id: string }) => {
},
{
label: "Webhooks",
description:
"Configure your repo provider to send webhooks to Komodo",
description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
components: {
["Guard" as any]: () => {
if (update.branch ?? config.branch) {
@@ -90,7 +90,7 @@ export const ResourceSyncConfig = ({
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
const git_provider = update.git_provider ?? config.git_provider;
const integration = getWebhookIntegration(integrations, git_provider);
const webhook_integration = getWebhookIntegration(integrations, git_provider);
const mode = getSyncMode(update, config);
const managed = update.managed ?? config.managed ?? false;
@@ -138,6 +138,7 @@ export const ResourceSyncConfig = ({
<ConfigItem
label="Choose Mode"
description="Will the file contents be defined in UI, stored on the server, or pulled from a git repo?"
boldLabel
>
<Select
value={mode}
@@ -331,8 +332,7 @@ export const ResourceSyncConfig = ({
match_tags,
{
label: "Git Webhooks",
description:
"Configure your repo provider to send webhooks to Komodo",
description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
contentHidden: !show.webhooks,
actions: (
<ShowHideButton
@@ -360,7 +360,7 @@ export const ResourceSyncConfig = ({
description="Trigger an update of the pending sync cache, to display the changes in the UI on push."
>
<CopyWebhook
integration={integration}
integration={webhook_integration}
path={`/sync/${id_or_name === "Id" ? id : name}/refresh`}
/>
</ConfigItem>
@@ -371,7 +371,7 @@ export const ResourceSyncConfig = ({
description="Trigger an execution of the sync on push."
>
<CopyWebhook
integration={integration}
integration={webhook_integration}
path={`/sync/${id_or_name === "Id" ? id : name}/sync`}
/>
</ConfigItem>
@@ -164,6 +164,7 @@ export const StackConfig = ({
<ConfigItem
label="Choose Mode"
description="Will the file contents be defined in UI, stored on the server, or pulled from a git repo?"
boldLabel
>
<Select
value={mode}
@@ -493,12 +494,10 @@ export const StackConfig = ({
server_component,
{
label: "Files",
labelHidden: true,
components: {
run_directory: {
label: "Run Directory",
description:
"Set the working directory when running the compose up command. Usually is the parent folder of the compose files.",
description: `Set the working directory when running the 'compose up' command. Can be absolute path, or relative to $PERIPHERY_STACK_DIR/${stack.name}`,
placeholder: "/path/to/folder",
},
file_paths: (value, set) => (
@@ -583,13 +582,11 @@ export const StackConfig = ({
},
{
label: "Files",
labelHidden: true,
components: {
run_directory: {
label: "Run Directory",
description:
"Set the working directory when running the compose up command, relative to the stack root. ($periphery_stack_dir/$stack_name/$run_directory)",
placeholder: "./path/to/folder",
"Set the working directory when running the compose up command, relative to the root of the repo.",
placeholder: "path/to/folder",
},
file_paths: (value, set) => (
<ConfigList
@@ -607,8 +604,7 @@ export const StackConfig = ({
...general_common,
{
label: "Webhooks",
description:
"Configure your repo provider to send webhooks to Komodo",
description: `Copy the webhook given here, and configure your ${webhook_integration}-style repo provider to send webhooks to Komodo`,
actions: (
<ShowHideButton
show={show.webhooks}
@@ -846,7 +842,7 @@ export const StackConfig = ({
);
};
export const DEFAULT_STACK_FILE_CONTENTS = `## 🦎 Hello Komodo 🦎
export const DEFAULT_STACK_FILE_CONTENTS = `## Add your compose file here
services:
hello_world:
image: hello-world
+8 -11
View File
@@ -1,5 +1,5 @@
import {
tagsAtom,
useTags,
useInvalidate,
useRead,
useShiftKeyListener,
@@ -19,7 +19,6 @@ import {
} from "@ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import { useToast } from "@ui/use-toast";
import { useAtom } from "jotai";
import { MinusCircle, PlusCircle, SearchX, Tag, X } from "lucide-react";
import { ReactNode, useEffect, useState } from "react";
import { tag_background_class } from "@lib/color";
@@ -29,27 +28,24 @@ type TargetExcludingSystem = Exclude<Types.ResourceTarget, { type: "System" }>;
export const TagsFilter = () => {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [tags, setTags] = useAtom<string[]>(tagsAtom);
const { tags, add_tag, remove_tag, clear_tags } = useTags();
const all_tags = useRead("ListTags", {}).data;
const filtered = filterBySplit(all_tags, search, (item) => item.name);
useShiftKeyListener("T", () => setOpen(true));
useShiftKeyListener("C", () => setTags([]));
useShiftKeyListener("C", () => clear_tags());
return (
<div className="flex gap-3 items-center">
{tags.length > 0 && (
<Button
variant="destructive"
className="px-2 py-1.5 h-fit"
onClick={() => setTags([])}
onClick={() => clear_tags()}
>
<X className="w-4 h-4" />
</Button>
)}
<TagsFilterTags
tag_ids={tags}
onBadgeClick={(tag_id) => setTags(tags.filter((id) => id !== tag_id))}
/>
<TagsFilterTags tag_ids={tags} onBadgeClick={remove_tag} />
<Popover
open={open}
@@ -89,7 +85,7 @@ export const TagsFilter = () => {
<CommandItem
key={tag.name}
onSelect={() => {
setTags([...tags, tag._id!.$oid]);
add_tag(tag._id!.$oid);
setSearch("");
setOpen(false);
}}
@@ -228,9 +224,10 @@ export const TagsWithBadge = ({
};
export const TableTags = ({ tag_ids }: { tag_ids: string[] }) => {
const { toggle_tag } = useTags();
return (
<div className="flex gap-1 flex-wrap">
<TagsWithBadge tag_ids={tag_ids} />
<TagsWithBadge tag_ids={tag_ids} onBadgeClick={toggle_tag} />
</div>
);
};
+25 -1
View File
@@ -343,7 +343,31 @@ export const atomWithStorage = <T>(key: string, init: T) => {
);
};
export const tagsAtom = atomWithStorage<string[]>("tags-v0", []);
const tagsAtom = atomWithStorage<string[]>("tags-v0", []);
export const useTags = () => {
const [tags, setTags] = useAtom<string[]>(tagsAtom);
const add_tag = (tag_id: string) => setTags([...tags, tag_id]);
const remove_tag = (tag_id: string) =>
setTags(tags.filter((id) => id !== tag_id));
const toggle_tag = (tag_id: string) => {
if (tags.includes(tag_id)) {
remove_tag(tag_id);
} else {
add_tag(tag_id);
}
};
const clear_tags = () => setTags([]);
return {
tags,
add_tag,
remove_tag,
toggle_tag,
clear_tags,
};
};
export const useTagsFilter = () => {
const [tags] = useAtom<string[]>(tagsAtom);
-147
View File
@@ -1,147 +0,0 @@
import * as monaco from "monaco-editor";
export const dockerfile_conf: monaco.languages.LanguageConfiguration = {
brackets: [
["{", "}"],
["[", "]"],
["(", ")"],
],
autoClosingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
surroundingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
};
export const dockerfile_language = <monaco.languages.IMonarchLanguage>{
defaultToken: "",
tokenPostfix: ".dockerfile",
variable: /\${?[\w]+}?/,
tokenizer: {
root: [
{ include: "@whitespace" },
{ include: "@comment" },
[/(ONBUILD)(\s+)/, ["keyword", ""]],
[
/(ENV)(\s+)([\w]+)/,
["keyword", "", { token: "variable", next: "@arguments" }],
],
[
/(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|ARG|VOLUME|LABEL|USER|WORKDIR|COPY|CMD|STOPSIGNAL|SHELL|HEALTHCHECK|ENTRYPOINT)/,
{ token: "keyword", next: "@arguments" },
],
],
arguments: [
{ include: "@whitespace" },
{ include: "@strings" },
[
/(@variable)/,
{
cases: {
"@eos": { token: "variable", next: "@popall" },
"@default": "variable",
},
},
],
[
/\\/,
{
cases: {
"@eos": "",
"@default": "",
},
},
],
[
/./,
{
cases: {
"@eos": { token: "", next: "@popall" },
"@default": "",
},
},
],
],
// Deal with white space, including comments
whitespace: [
[
/\s+/,
{
cases: {
"@eos": { token: "", next: "@popall" },
"@default": "",
},
},
],
],
comment: [[/(^#.*$)/, "comment", "@popall"]],
// Recognize strings, including those broken across lines with \ (but not without)
strings: [
[/\\'$/, "", "@popall"], // \' leaves @arguments at eol
[/\\'/, ""], // \' is not a string
[/'$/, "string", "@popall"],
[/'/, "string", "@stringBody"],
[/"$/, "string", "@popall"],
[/"/, "string", "@dblStringBody"],
],
stringBody: [
[
/[^\\\$']/,
{
cases: {
"@eos": { token: "string", next: "@popall" },
"@default": "string",
},
},
],
[/\\./, "string.escape"],
[/'$/, "string", "@popall"],
[/'/, "string", "@pop"],
[/(@variable)/, "variable"],
[/\\$/, "string"],
[/$/, "string", "@popall"],
],
dblStringBody: [
[
/[^\\\$"]/,
{
cases: {
"@eos": { token: "string", next: "@popall" },
"@default": "string",
},
},
],
[/\\./, "string.escape"],
[/"$/, "string", "@popall"],
[/"/, "string", "@pop"],
[/(@variable)/, "variable"],
[/\\$/, "string"],
[/$/, "string", "@popall"],
],
},
};
monaco.languages.register({ id: "dockerfile" });
monaco.languages.setLanguageConfiguration("dockerfile", dockerfile_conf);
monaco.languages.setMonarchTokensProvider("dockerfile", dockerfile_language);
+1 -3
View File
@@ -31,8 +31,6 @@ import "./theme";
// Load the parsers
import "./yaml";
import "./toml";
import "./shell"
import "./key_value";
import "./string_list";
import "./shell";
import "./dockerfile";
import "./rust";
-370
View File
@@ -1,370 +0,0 @@
import * as monaco from "monaco-editor";
export const rust_conf: monaco.languages.LanguageConfiguration = {
comments: {
lineComment: "//",
blockComment: ["/*", "*/"],
},
brackets: [
["{", "}"],
["[", "]"],
["(", ")"],
],
autoClosingPairs: [
{ open: "[", close: "]" },
{ open: "{", close: "}" },
{ open: "(", close: ")" },
{ open: '"', close: '"', notIn: ["string"] },
],
surroundingPairs: [
{ open: "{", close: "}" },
{ open: "[", close: "]" },
{ open: "(", close: ")" },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
folding: {
markers: {
start: new RegExp("^\\s*#pragma\\s+region\\b"),
end: new RegExp("^\\s*#pragma\\s+endregion\\b"),
},
},
};
export const rust_language = <monaco.languages.IMonarchLanguage>{
tokenPostfix: ".rust",
defaultToken: "invalid",
keywords: [
"as",
"async",
"await",
"box",
"break",
"const",
"continue",
"crate",
"dyn",
"else",
"enum",
"extern",
"false",
"fn",
"for",
"if",
"impl",
"in",
"let",
"loop",
"match",
"mod",
"move",
"mut",
"pub",
"ref",
"return",
"self",
"static",
"struct",
"super",
"trait",
"true",
"try",
"type",
"unsafe",
"use",
"where",
"while",
"catch",
"default",
"union",
"static",
"abstract",
"alignof",
"become",
"do",
"final",
"macro",
"offsetof",
"override",
"priv",
"proc",
"pure",
"sizeof",
"typeof",
"unsized",
"virtual",
"yield",
],
typeKeywords: [
"Self",
"m32",
"m64",
"m128",
"f80",
"f16",
"f128",
"int",
"uint",
"float",
"char",
"bool",
"u8",
"u16",
"u32",
"u64",
"f32",
"f64",
"i8",
"i16",
"i32",
"i64",
"str",
"Option",
"Either",
"c_float",
"c_double",
"c_void",
"FILE",
"fpos_t",
"DIR",
"dirent",
"c_char",
"c_schar",
"c_uchar",
"c_short",
"c_ushort",
"c_int",
"c_uint",
"c_long",
"c_ulong",
"size_t",
"ptrdiff_t",
"clock_t",
"time_t",
"c_longlong",
"c_ulonglong",
"intptr_t",
"uintptr_t",
"off_t",
"dev_t",
"ino_t",
"pid_t",
"mode_t",
"ssize_t",
],
constants: ["true", "false", "Some", "None", "Left", "Right", "Ok", "Err"],
supportConstants: [
"EXIT_FAILURE",
"EXIT_SUCCESS",
"RAND_MAX",
"EOF",
"SEEK_SET",
"SEEK_CUR",
"SEEK_END",
"_IOFBF",
"_IONBF",
"_IOLBF",
"BUFSIZ",
"FOPEN_MAX",
"FILENAME_MAX",
"L_tmpnam",
"TMP_MAX",
"O_RDONLY",
"O_WRONLY",
"O_RDWR",
"O_APPEND",
"O_CREAT",
"O_EXCL",
"O_TRUNC",
"S_IFIFO",
"S_IFCHR",
"S_IFBLK",
"S_IFDIR",
"S_IFREG",
"S_IFMT",
"S_IEXEC",
"S_IWRITE",
"S_IREAD",
"S_IRWXU",
"S_IXUSR",
"S_IWUSR",
"S_IRUSR",
"F_OK",
"R_OK",
"W_OK",
"X_OK",
"STDIN_FILENO",
"STDOUT_FILENO",
"STDERR_FILENO",
],
supportMacros: [
"format!",
"print!",
"println!",
"panic!",
"format_args!",
"unreachable!",
"write!",
"writeln!",
],
operators: [
"!",
"!=",
"%",
"%=",
"&",
"&=",
"&&",
"*",
"*=",
"+",
"+=",
"-",
"-=",
"->",
".",
"..",
"...",
"/",
"/=",
":",
";",
"<<",
"<<=",
"<",
"<=",
"=",
"==",
"=>",
">",
">=",
">>",
">>=",
"@",
"^",
"^=",
"|",
"|=",
"||",
"_",
"?",
"#",
],
escapes: /\\([nrt0\"''\\]|x\h{2}|u\{\h{1,6}\})/,
delimiters: /[,]/,
symbols: /[\#\!\%\&\*\+\-\.\/\:\;\<\=\>\@\^\|_\?]+/,
intSuffixes: /[iu](8|16|32|64|128|size)/,
floatSuffixes: /f(32|64)/,
tokenizer: {
root: [
// Raw string literals
[
/r(#*)"/,
{ token: "string.quote", bracket: "@open", next: "@stringraw.$1" },
],
[
/[a-zA-Z][a-zA-Z0-9_]*!?|_[a-zA-Z0-9_]+/,
{
cases: {
"@typeKeywords": "keyword.type",
"@keywords": "keyword",
"@supportConstants": "keyword",
"@supportMacros": "keyword",
"@constants": "keyword",
"@default": "identifier",
},
},
],
// Designator
[/\$/, "identifier"],
// Lifetime annotations
[/'[a-zA-Z_][a-zA-Z0-9_]*(?=[^\'])/, "identifier"],
// Byte literal
[/'(\S|@escapes)'/, "string.byteliteral"],
// Strings
[/"/, { token: "string.quote", bracket: "@open", next: "@string" }],
{ include: "@numbers" },
// Whitespace + comments
{ include: "@whitespace" },
[
/@delimiters/,
{
cases: {
"@keywords": "keyword",
"@default": "delimiter",
},
},
],
[/[{}()\[\]<>]/, "@brackets"],
[/@symbols/, { cases: { "@operators": "operator", "@default": "" } }],
],
whitespace: [
[/[ \t\r\n]+/, "white"],
[/\/\*/, "comment", "@comment"],
[/\/\/.*$/, "comment"],
],
comment: [
[/[^\/*]+/, "comment"],
[/\/\*/, "comment", "@push"],
["\\*/", "comment", "@pop"],
[/[\/*]/, "comment"],
],
string: [
[/[^\\"]+/, "string"],
[/@escapes/, "string.escape"],
[/\\./, "string.escape.invalid"],
[/"/, { token: "string.quote", bracket: "@close", next: "@pop" }],
],
stringraw: [
[/[^"#]+/, { token: "string" }],
[
/"(#*)/,
{
cases: {
"$1==$S2": {
token: "string.quote",
bracket: "@close",
next: "@pop",
},
"@default": { token: "string" },
},
},
],
[/["#]/, { token: "string" }],
],
numbers: [
//Octal
[/(0o[0-7_]+)(@intSuffixes)?/, { token: "number" }],
//Binary
[/(0b[0-1_]+)(@intSuffixes)?/, { token: "number" }],
//Exponent
[
/[\d][\d_]*(\.[\d][\d_]*)?[eE][+-][\d_]+(@floatSuffixes)?/,
{ token: "number" },
],
//Float
[/\b(\d\.?[\d_]*)(@floatSuffixes)?\b/, { token: "number" }],
//Hexadecimal
[/(0x[\da-fA-F]+)_?(@intSuffixes)?/, { token: "number" }],
//Integer
[/[\d][\d_]*(@intSuffixes?)?/, { token: "number" }],
],
},
};
monaco.languages.register({ id: "rust" });
monaco.languages.setLanguageConfiguration("rust", rust_conf);
monaco.languages.setMonarchTokensProvider("rust", rust_language);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

After

Width:  |  Height:  |  Size: 147 KiB