mirror of
https://github.com/moghtech/komodo.git
synced 2026-03-11 17:44:19 -05:00
1.13.2 local compose (#36)
* stack config files_on_host * refresh stack cache not blocked when using files_on_host * add remote errors status * improve info tab * store the full path in ComposeContents
This commit is contained in:
@@ -3,6 +3,7 @@ use std::time::Instant;
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{middleware, routing::post, Extension, Router};
|
||||
use axum_extra::{headers::ContentType, TypedHeader};
|
||||
use derive_variants::{EnumVariants, ExtractVariant};
|
||||
use monitor_client::{api::write::*, entities::user::User};
|
||||
use resolver_api::{derive::Resolver, Resolver};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -31,7 +32,10 @@ mod user_group;
|
||||
mod variable;
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Resolver)]
|
||||
#[derive(
|
||||
Serialize, Deserialize, Debug, Clone, Resolver, EnumVariants,
|
||||
)]
|
||||
#[variant_derive(Debug)]
|
||||
#[resolver_target(State)]
|
||||
#[resolver_args(User)]
|
||||
#[serde(tag = "type", content = "params")]
|
||||
@@ -178,7 +182,11 @@ async fn handler(
|
||||
Ok((TypedHeader(ContentType::json()), res??))
|
||||
}
|
||||
|
||||
#[instrument(name = "WriteRequest", skip(user), fields(user_id = user.id))]
|
||||
#[instrument(
|
||||
name = "WriteRequest",
|
||||
skip(user, request),
|
||||
fields(user_id = user.id, request = format!("{:?}", request.extract_variant()))
|
||||
)]
|
||||
async fn task(
|
||||
req_id: Uuid,
|
||||
request: WriteRequest,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use formatting::format_serror;
|
||||
use monitor_client::{
|
||||
api::write::*,
|
||||
entities::{
|
||||
config::core::CoreConfig,
|
||||
monitor_timestamp,
|
||||
permission::PermissionLevel,
|
||||
stack::{PartialStackConfig, Stack, StackInfo},
|
||||
server::ServerState,
|
||||
stack::{ComposeContents, PartialStackConfig, Stack, StackInfo},
|
||||
update::Update,
|
||||
user::User,
|
||||
NoData, Operation,
|
||||
@@ -18,11 +20,16 @@ use mungos::{
|
||||
use octorust::types::{
|
||||
ReposCreateWebhookRequest, ReposCreateWebhookRequestConfig,
|
||||
};
|
||||
use periphery_client::api::compose::{
|
||||
GetComposeContentsOnHost, GetComposeContentsOnHostResponse,
|
||||
};
|
||||
use resolver_api::Resolve;
|
||||
|
||||
use crate::{
|
||||
config::core_config,
|
||||
helpers::{
|
||||
periphery_client,
|
||||
query::get_server_with_status,
|
||||
stack::{
|
||||
remote::get_remote_compose_contents,
|
||||
services::extract_services_into_res,
|
||||
@@ -146,7 +153,10 @@ impl Resolve<RefreshStackCache, User> for State {
|
||||
|
||||
let file_contents_empty = stack.config.file_contents.is_empty();
|
||||
|
||||
if file_contents_empty && stack.config.repo.is_empty() {
|
||||
if !stack.config.files_on_host
|
||||
&& file_contents_empty
|
||||
&& stack.config.repo.is_empty()
|
||||
{
|
||||
// Nothing to do without one of these
|
||||
return Ok(NoData {});
|
||||
}
|
||||
@@ -159,8 +169,63 @@ impl Resolve<RefreshStackCache, User> for State {
|
||||
remote_errors,
|
||||
latest_hash,
|
||||
latest_message,
|
||||
) = if file_contents_empty {
|
||||
) = if stack.config.files_on_host {
|
||||
// =============
|
||||
// FILES ON HOST
|
||||
// =============
|
||||
if stack.config.server_id.is_empty() {
|
||||
(vec![], None, None, None, None)
|
||||
} else {
|
||||
let (server, status) =
|
||||
get_server_with_status(&stack.config.server_id).await?;
|
||||
if status != ServerState::Ok {
|
||||
(vec![], None, None, None, None)
|
||||
} else {
|
||||
let GetComposeContentsOnHostResponse { contents, errors } =
|
||||
match periphery_client(&server)?
|
||||
.request(GetComposeContentsOnHost {
|
||||
file_paths: stack.file_paths().to_vec(),
|
||||
name: stack.name.clone(),
|
||||
run_directory: stack.config.run_directory.clone(),
|
||||
})
|
||||
.await
|
||||
.context(
|
||||
"failed to get compose file contents from host",
|
||||
) {
|
||||
Ok(res) => res,
|
||||
Err(e) => GetComposeContentsOnHostResponse {
|
||||
contents: Default::default(),
|
||||
errors: vec![ComposeContents {
|
||||
path: stack.config.run_directory.clone(),
|
||||
contents: format_serror(&e.into()),
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
let project_name = stack.project_name(true);
|
||||
|
||||
let mut services = Vec::new();
|
||||
|
||||
for contents in &contents {
|
||||
if let Err(e) = extract_services_into_res(
|
||||
&project_name,
|
||||
&contents.contents,
|
||||
&mut services,
|
||||
) {
|
||||
warn!(
|
||||
"failed to extract stack services, things won't works correctly. stack: {} | {e:#}",
|
||||
stack.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
(services, Some(contents), Some(errors), None, None)
|
||||
}
|
||||
}
|
||||
} else if file_contents_empty {
|
||||
// ================
|
||||
// REPO BASED STACK
|
||||
// ================
|
||||
let (
|
||||
remote_contents,
|
||||
remote_errors,
|
||||
@@ -196,6 +261,9 @@ impl Resolve<RefreshStackCache, User> for State {
|
||||
latest_message,
|
||||
)
|
||||
} else {
|
||||
// =============
|
||||
// UI BASED FILE
|
||||
// =============
|
||||
let mut services = Vec::new();
|
||||
if let Err(e) = extract_services_into_res(
|
||||
// this should latest (not deployed), so make the project name fresh.
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use command::run_monitor_command;
|
||||
use formatting::format_serror;
|
||||
use monitor_client::entities::{stack::ComposeProject, update::Log};
|
||||
use monitor_client::entities::{
|
||||
stack::{ComposeContents, ComposeProject},
|
||||
to_monitor_name,
|
||||
update::Log,
|
||||
};
|
||||
use periphery_client::api::compose::*;
|
||||
use resolver_api::Resolve;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::{
|
||||
compose::{compose_up, docker_compose},
|
||||
config::periphery_config,
|
||||
helpers::log_grep,
|
||||
State,
|
||||
};
|
||||
@@ -70,6 +78,58 @@ pub struct DockerComposeLsItem {
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<GetComposeContentsOnHost, ()> for State {
|
||||
async fn resolve(
|
||||
&self,
|
||||
GetComposeContentsOnHost {
|
||||
name,
|
||||
run_directory,
|
||||
file_paths,
|
||||
}: GetComposeContentsOnHost,
|
||||
_: (),
|
||||
) -> anyhow::Result<GetComposeContentsOnHostResponse> {
|
||||
let root =
|
||||
periphery_config().stack_dir.join(to_monitor_name(&name));
|
||||
let run_directory = root.join(&run_directory);
|
||||
let run_directory = run_directory.canonicalize().context(
|
||||
"failed to validate run directory on host (canonicalize error)",
|
||||
)?;
|
||||
|
||||
let file_paths = file_paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
run_directory.join(path).components().collect::<PathBuf>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut res = GetComposeContentsOnHostResponse::default();
|
||||
|
||||
for full_path in &file_paths {
|
||||
match fs::read_to_string(&full_path).await.with_context(|| {
|
||||
format!(
|
||||
"failed to read compose file contents at {full_path:?}"
|
||||
)
|
||||
}) {
|
||||
Ok(contents) => {
|
||||
res.contents.push(ComposeContents {
|
||||
path: full_path.display().to_string(),
|
||||
contents,
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
res.errors.push(ComposeContents {
|
||||
path: full_path.display().to_string(),
|
||||
contents: format_serror(&e.into()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
impl Resolve<GetComposeServiceLog> for State {
|
||||
#[instrument(
|
||||
name = "GetComposeServiceLog",
|
||||
|
||||
@@ -91,6 +91,7 @@ pub enum PeripheryRequest {
|
||||
|
||||
// Compose
|
||||
ListComposeProjects(ListComposeProjects),
|
||||
GetComposeContentsOnHost(GetComposeContentsOnHost),
|
||||
GetComposeServiceLog(GetComposeServiceLog),
|
||||
GetComposeServiceLogSearch(GetComposeServiceLogSearch),
|
||||
ComposeUp(ComposeUp),
|
||||
|
||||
@@ -58,7 +58,13 @@ pub async fn compose_up(
|
||||
let file_paths = stack
|
||||
.file_paths()
|
||||
.iter()
|
||||
.map(|path| (path, run_directory.join(path)))
|
||||
.map(|path| {
|
||||
(
|
||||
path,
|
||||
// This will remove any intermediate uneeded '/./' in the path
|
||||
run_directory.join(path).components().collect::<PathBuf>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (path, full_path) in &file_paths {
|
||||
@@ -70,7 +76,7 @@ pub async fn compose_up(
|
||||
return Err(anyhow!("A compose file doesn't exist after writing stack. Ensure the run_directory and file_paths are correct."));
|
||||
}
|
||||
|
||||
for (path, full_path) in &file_paths {
|
||||
for (_, full_path) in &file_paths {
|
||||
let file_contents =
|
||||
match fs::read_to_string(&full_path).await.with_context(|| {
|
||||
format!(
|
||||
@@ -85,7 +91,7 @@ pub async fn compose_up(
|
||||
.push(Log::error("read compose file", error.clone()));
|
||||
// This should only happen for repo stacks, ie remote error
|
||||
res.remote_errors.push(ComposeContents {
|
||||
path: path.to_string(),
|
||||
path: full_path.display().to_string(),
|
||||
contents: error,
|
||||
});
|
||||
return Err(anyhow!(
|
||||
@@ -172,12 +178,17 @@ pub async fn compose_up(
|
||||
res.deployed = log.success;
|
||||
res.logs.push(log);
|
||||
|
||||
if let Err(e) = fs::remove_dir_all(&root).await.with_context(|| {
|
||||
format!("failed to clean up files after deploy | path: {root:?} | ensure all volumes are mounted outside the repo directory (preferably use absolute path for mounts)")
|
||||
}) {
|
||||
res
|
||||
.logs
|
||||
.push(Log::error("clean up files", format_serror(&e.into())))
|
||||
// Unless the files are supposed to be managed on the host,
|
||||
// clean up here, which will also let user know immediately if there will be a problem
|
||||
// with any accidental volumes mounted inside repo directory (just use absolute volumes anyways)
|
||||
if !stack.config.files_on_host {
|
||||
if let Err(e) = fs::remove_dir_all(&root).await.with_context(|| {
|
||||
format!("failed to clean up files after deploy | path: {root:?} | ensure all volumes are mounted outside the repo directory (preferably use absolute path for mounts)")
|
||||
}) {
|
||||
res
|
||||
.logs
|
||||
.push(Log::error("clean up files", format_serror(&e.into())))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -198,8 +209,33 @@ async fn write_stack(
|
||||
// Cannot use canonicalize yet as directory may not exist.
|
||||
let run_directory = run_directory.components().collect::<PathBuf>();
|
||||
|
||||
if stack.config.file_contents.is_empty() {
|
||||
// Clone the repo
|
||||
if stack.config.files_on_host {
|
||||
// =============
|
||||
// FILES ON HOST
|
||||
// =============
|
||||
// Only need to write environment file here (which does nothing if not using this feature)
|
||||
let env_file_path = match write_environment_file(
|
||||
&stack.config.environment,
|
||||
&stack.config.env_file_path,
|
||||
stack
|
||||
.config
|
||||
.skip_secret_interp
|
||||
.then_some(&periphery_config().secrets),
|
||||
&run_directory,
|
||||
&mut res.logs,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
return Err(anyhow!("failed to write environment file"));
|
||||
}
|
||||
};
|
||||
Ok(env_file_path)
|
||||
} else if stack.config.file_contents.is_empty() {
|
||||
// ================
|
||||
// REPO BASED FILES
|
||||
// ================
|
||||
if stack.config.repo.is_empty() {
|
||||
// Err response will be written to return, no need to add it to log here
|
||||
return Err(anyhow!("Must either input compose file contents directly or provide a repo. Got neither."));
|
||||
@@ -284,6 +320,9 @@ async fn write_stack(
|
||||
|
||||
Ok(env_file_path)
|
||||
} else {
|
||||
// ==============
|
||||
// UI BASED FILES
|
||||
// ==============
|
||||
// Ensure run directory exists
|
||||
fs::create_dir_all(&run_directory).await.with_context(|| {
|
||||
format!(
|
||||
|
||||
@@ -153,7 +153,8 @@ pub struct StackInfo {
|
||||
#[serde(default)]
|
||||
pub latest_services: Vec<StackServiceNames>,
|
||||
|
||||
/// The remote compose file contents. This is updated whenever Monitor refreshes the stack cache.
|
||||
/// The remote compose file contents, whether on host or in repo.
|
||||
/// This is updated whenever Monitor refreshes the stack cache.
|
||||
/// It will be empty if the file is defined directly in the stack config.
|
||||
pub remote_contents: Option<Vec<ComposeContents>>,
|
||||
/// If there was an error in getting the remote contents, it will be here.
|
||||
@@ -165,59 +166,6 @@ pub struct StackInfo {
|
||||
pub latest_message: Option<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ComposeProject {
|
||||
/// The compose project name.
|
||||
pub name: String,
|
||||
/// The status of the project, as returned by docker.
|
||||
pub status: Option<String>,
|
||||
/// The compose files included in the project.
|
||||
pub compose_files: Vec<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ComposeContents {
|
||||
/// The path of the file on the host
|
||||
pub path: String,
|
||||
/// The contents of the file
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct StackServiceNames {
|
||||
/// The name of the service
|
||||
pub service_name: String,
|
||||
/// Will either be the declared container_name in the compose file,
|
||||
/// or a pattern to match auto named containers.
|
||||
///
|
||||
/// Auto named containers are composed of three parts:
|
||||
///
|
||||
/// 1. The name of the compose project (top level name field of compose file).
|
||||
/// This defaults to the name of the parent folder of the compose file.
|
||||
/// Monitor will always set it to be the name of the stack, but imported stacks
|
||||
/// will have a different name.
|
||||
/// 2. The service name
|
||||
/// 3. The replica number
|
||||
///
|
||||
/// Example: stacko-mongo-1.
|
||||
///
|
||||
/// This stores only 1. and 2., ie stacko-mongo.
|
||||
/// Containers will be matched via regex like `^container_name-?[0-9]*$``
|
||||
pub container_name: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct StackService {
|
||||
/// The service name
|
||||
pub service: String,
|
||||
/// The container
|
||||
pub container: Option<ContainerSummary>,
|
||||
}
|
||||
|
||||
#[typeshare(serialized_as = "Partial<StackConfig>")]
|
||||
pub type _PartialStackConfig = PartialStackConfig;
|
||||
|
||||
@@ -254,6 +202,14 @@ pub struct StackConfig {
|
||||
#[builder(default)]
|
||||
pub file_paths: Vec<String>,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// Used with `registry_account` to login to a registry before docker compose up.
|
||||
#[serde(default)]
|
||||
#[builder(default)]
|
||||
@@ -412,6 +368,7 @@ impl Default for StackConfig {
|
||||
project_name: Default::default(),
|
||||
run_directory: default_run_directory(),
|
||||
file_paths: Default::default(),
|
||||
files_on_host: Default::default(),
|
||||
registry_provider: Default::default(),
|
||||
registry_account: Default::default(),
|
||||
file_contents: Default::default(),
|
||||
@@ -433,6 +390,59 @@ impl Default for StackConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ComposeProject {
|
||||
/// The compose project name.
|
||||
pub name: String,
|
||||
/// The status of the project, as returned by docker.
|
||||
pub status: Option<String>,
|
||||
/// The compose files included in the project.
|
||||
pub compose_files: Vec<String>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ComposeContents {
|
||||
/// The path of the file on the host
|
||||
pub path: String,
|
||||
/// The contents of the file
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct StackServiceNames {
|
||||
/// The name of the service
|
||||
pub service_name: String,
|
||||
/// Will either be the declared container_name in the compose file,
|
||||
/// or a pattern to match auto named containers.
|
||||
///
|
||||
/// Auto named containers are composed of three parts:
|
||||
///
|
||||
/// 1. The name of the compose project (top level name field of compose file).
|
||||
/// This defaults to the name of the parent folder of the compose file.
|
||||
/// Monitor will always set it to be the name of the stack, but imported stacks
|
||||
/// will have a different name.
|
||||
/// 2. The service name
|
||||
/// 3. The replica number
|
||||
///
|
||||
/// Example: stacko-mongo-1.
|
||||
///
|
||||
/// This stores only 1. and 2., ie stacko-mongo.
|
||||
/// Containers will be matched via regex like `^container_name-?[0-9]*$``
|
||||
pub container_name: String,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct StackService {
|
||||
/// The service name
|
||||
pub service: String,
|
||||
/// The container
|
||||
pub container: Option<ContainerSummary>,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub struct StackActionState {
|
||||
|
||||
@@ -78,6 +78,9 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether user is an inbuilt service user
|
||||
///
|
||||
/// NOTE: ALSO UPDATE `frontend/src/lib/utils/is_service_user` to match
|
||||
pub fn is_service_user(user_id: &str) -> bool {
|
||||
matches!(
|
||||
user_id,
|
||||
|
||||
@@ -1481,6 +1481,13 @@ export interface StackConfig {
|
||||
* If this is empty, will use file `compose.yaml`.
|
||||
*/
|
||||
file_paths?: string[];
|
||||
/**
|
||||
* 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;
|
||||
/** Used with `registry_account` to login to a registry before docker compose up. */
|
||||
registry_provider?: string;
|
||||
/** Used with `registry_provider` to login to a registry before docker compose up. */
|
||||
@@ -1612,7 +1619,8 @@ export interface StackInfo {
|
||||
*/
|
||||
latest_services?: StackServiceNames[];
|
||||
/**
|
||||
* The remote compose file contents. This is updated whenever Monitor refreshes the stack cache.
|
||||
* The remote compose file contents, whether on host or in repo.
|
||||
* This is updated whenever Monitor refreshes the stack cache.
|
||||
* It will be empty if the file is defined directly in the stack config.
|
||||
*/
|
||||
remote_contents?: ComposeContents[];
|
||||
|
||||
@@ -17,6 +17,25 @@ pub struct ListComposeProjects {}
|
||||
|
||||
//
|
||||
|
||||
/// Get the compose contents on the host, for stacks using
|
||||
/// `files_on_host`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Request)]
|
||||
#[response(GetComposeContentsOnHostResponse)]
|
||||
pub struct GetComposeContentsOnHost {
|
||||
/// The name of the stack
|
||||
pub name: String,
|
||||
pub run_directory: String,
|
||||
pub file_paths: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct GetComposeContentsOnHostResponse {
|
||||
pub contents: Vec<ComposeContents>,
|
||||
pub errors: Vec<ComposeContents>,
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/// The stack folder must already exist for this to work
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Request)]
|
||||
#[response(Log)]
|
||||
|
||||
@@ -125,7 +125,7 @@ export const Config = <T,>({
|
||||
titleOther?: ReactNode;
|
||||
components: Record<
|
||||
string, // sidebar key
|
||||
ConfigComponent<T>[]
|
||||
ConfigComponent<T>[] | false | undefined
|
||||
>;
|
||||
}) => {
|
||||
const [show, setShow] = useState(keys(components)[0]);
|
||||
@@ -163,80 +163,84 @@ export const Config = <T,>({
|
||||
<div className="flex gap-4">
|
||||
{/** The sidebar when large */}
|
||||
<div className="hidden xl:flex flex-col gap-4 w-[300px]">
|
||||
{keys(components).map((tab) => (
|
||||
<Button
|
||||
key={tab}
|
||||
variant={show === tab ? "secondary" : "outline"}
|
||||
onClick={() => setShow(tab)}
|
||||
className="capitalize"
|
||||
>
|
||||
{tab}
|
||||
</Button>
|
||||
))}
|
||||
{Object.entries(components)
|
||||
.filter(([_, val]) => val)
|
||||
.map(([tab, _]) => (
|
||||
<Button
|
||||
key={tab}
|
||||
variant={show === tab ? "secondary" : "outline"}
|
||||
onClick={() => setShow(tab)}
|
||||
className="capitalize"
|
||||
>
|
||||
{tab}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 min-h-[500px] w-full">
|
||||
{components[show].map(
|
||||
({
|
||||
label,
|
||||
labelHidden,
|
||||
icon,
|
||||
actions,
|
||||
description,
|
||||
hidden,
|
||||
contentHidden,
|
||||
components,
|
||||
}) =>
|
||||
!hidden && (
|
||||
<Card className="w-full grid gap-2" key={label}>
|
||||
{!labelHidden && (
|
||||
<CardHeader
|
||||
className={cn(
|
||||
"flex-row items-center justify-between w-full py-0 h-[60px] space-y-0",
|
||||
!contentHidden && "border-b"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<CardTitle className="flex gap-4">
|
||||
{icon}
|
||||
{label}
|
||||
</CardTitle>
|
||||
{description && (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Card className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer">
|
||||
<Info className="w-4 h-4" />
|
||||
</Card>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="start" side="right">
|
||||
{description}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
{components[show] && (
|
||||
<div className="flex flex-col gap-6 min-h-[500px] w-full">
|
||||
{components[show].map(
|
||||
({
|
||||
label,
|
||||
labelHidden,
|
||||
icon,
|
||||
actions,
|
||||
description,
|
||||
hidden,
|
||||
contentHidden,
|
||||
components,
|
||||
}) =>
|
||||
!hidden && (
|
||||
<Card className="w-full grid gap-2" key={label}>
|
||||
{!labelHidden && (
|
||||
<CardHeader
|
||||
className={cn(
|
||||
"flex-row items-center justify-between w-full py-0 h-[60px] space-y-0",
|
||||
!contentHidden && "border-b"
|
||||
)}
|
||||
</div>
|
||||
{actions}
|
||||
</CardHeader>
|
||||
)}
|
||||
{!contentHidden && (
|
||||
<CardContent
|
||||
className={cn(
|
||||
"flex flex-col gap-1 pb-3",
|
||||
labelHidden && "pt-3"
|
||||
)}
|
||||
>
|
||||
<ConfigAgain
|
||||
config={config}
|
||||
update={update}
|
||||
set={(u) => set((p) => ({ ...p, ...u }))}
|
||||
components={components}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<CardTitle className="flex gap-4">
|
||||
{icon}
|
||||
{label}
|
||||
</CardTitle>
|
||||
{description && (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Card className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer">
|
||||
<Info className="w-4 h-4" />
|
||||
</Card>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="start" side="right">
|
||||
{description}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)}
|
||||
</div>
|
||||
{actions}
|
||||
</CardHeader>
|
||||
)}
|
||||
{!contentHidden && (
|
||||
<CardContent
|
||||
className={cn(
|
||||
"flex flex-col gap-1 pb-3",
|
||||
labelHidden && "pt-3"
|
||||
)}
|
||||
>
|
||||
<ConfigAgain
|
||||
config={config}
|
||||
update={update}
|
||||
set={(u) => set((p) => ({ ...p, ...u }))}
|
||||
components={components}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ConfigLayout>
|
||||
);
|
||||
|
||||
@@ -48,6 +48,8 @@ export const StackConfig = ({
|
||||
|
||||
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
|
||||
|
||||
const files_on_host = update.files_on_host ?? config.files_on_host;
|
||||
|
||||
return (
|
||||
<Config
|
||||
titleOther={titleOther}
|
||||
@@ -85,8 +87,71 @@ export const StackConfig = ({
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Files on Server",
|
||||
labelHidden: true,
|
||||
components: {
|
||||
files_on_host: {
|
||||
label: "Files on Server",
|
||||
boldLabel: true,
|
||||
description:
|
||||
"Manage the compose files on server yourself. Just configure the Run Directory and File Paths to your files.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Run Path",
|
||||
labelHidden: true,
|
||||
hidden: !files_on_host,
|
||||
components: {
|
||||
run_directory: {
|
||||
placeholder: "/path/to/folder",
|
||||
description:
|
||||
"Set the cwd when running compose up command. Should usually be the parent folder of the compose files.",
|
||||
boldLabel: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "File Paths",
|
||||
hidden: !files_on_host,
|
||||
description:
|
||||
"Add files to include using 'docker compose -f'. If empty, uses 'compose.yaml'. Relative to 'Run Directory'.",
|
||||
contentHidden:
|
||||
(update.file_paths ?? config.file_paths)?.length === 0,
|
||||
actions: !disabled && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
set((update) => ({
|
||||
...update,
|
||||
file_paths: [
|
||||
...(update.file_paths ?? config.file_paths ?? []),
|
||||
"",
|
||||
],
|
||||
}))
|
||||
}
|
||||
className="flex items-center gap-2 w-[200px]"
|
||||
>
|
||||
<PlusCircle className="w-4 h-4" />
|
||||
Add File
|
||||
</Button>
|
||||
),
|
||||
components: {
|
||||
file_paths: (value, set) => (
|
||||
<InputList
|
||||
field="file_paths"
|
||||
values={value ?? []}
|
||||
set={set}
|
||||
disabled={disabled}
|
||||
placeholder="compose.yaml"
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Compose File",
|
||||
hidden: files_on_host,
|
||||
description:
|
||||
"Paste the file contents here, or configure a git repo.",
|
||||
actions: (
|
||||
@@ -252,7 +317,7 @@ export const StackConfig = ({
|
||||
},
|
||||
},
|
||||
],
|
||||
"Git Repo": [
|
||||
"Git Repo": !files_on_host && [
|
||||
{
|
||||
label: "Git",
|
||||
description:
|
||||
@@ -307,7 +372,7 @@ export const StackConfig = ({
|
||||
labelHidden: true,
|
||||
components: {
|
||||
run_directory: {
|
||||
placeholder: "Eg. './'",
|
||||
placeholder: "./",
|
||||
description:
|
||||
"Set the cwd when running compose up command. Relative to the repo root.",
|
||||
boldLabel: true,
|
||||
@@ -317,7 +382,7 @@ export const StackConfig = ({
|
||||
{
|
||||
label: "File Paths",
|
||||
description:
|
||||
"Add files to include using 'docker compose -f'. If empty, uses 'compose.yaml'.",
|
||||
"Add files to include using 'docker compose -f'. If empty, uses 'compose.yaml'. Relative to 'Run Directory'.",
|
||||
contentHidden:
|
||||
(update.file_paths ?? config.file_paths)?.length === 0,
|
||||
actions: !disabled && (
|
||||
|
||||
@@ -53,7 +53,8 @@ const StackIcon = ({ id, size }: { id?: string; size: number }) => {
|
||||
|
||||
const ConfigInfoServices = ({ id }: { id: string }) => {
|
||||
const [view, setView] = useLocalStorage("stack-tabs-v1", "Config");
|
||||
const state = useStack(id)?.info.state;
|
||||
const info = useStack(id)?.info;
|
||||
const state = info?.state;
|
||||
const stackDown =
|
||||
state === undefined ||
|
||||
state === Types.StackState.Unknown ||
|
||||
@@ -150,7 +151,7 @@ export const StackComponents: RequiredResourceComponents = {
|
||||
},
|
||||
NoConfig: ({ id }) => {
|
||||
const config = useFullStack(id)?.config;
|
||||
if (config?.file_contents || config?.repo) {
|
||||
if (config?.files_on_host || config?.file_contents || config?.repo) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
@@ -200,6 +201,30 @@ export const StackComponents: RequiredResourceComponents = {
|
||||
</HoverCard>
|
||||
);
|
||||
},
|
||||
RemoteErrors: ({ id }) => {
|
||||
const info = useFullStack(id)?.info;
|
||||
const errors = info?.remote_errors;
|
||||
if (!errors || errors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Card className="px-3 py-2 bg-destructive/75 hover:bg-destructive transition-colors cursor-pointer">
|
||||
<div className="text-sm text-nowrap overflow-hidden overflow-ellipsis">
|
||||
Remote Error
|
||||
</div>
|
||||
</Card>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="start">
|
||||
<div>
|
||||
There are errors reading the remote file contents. See{" "}
|
||||
<span className="font-bold">Info</span> tab for details.
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
},
|
||||
Deployed: ({ id }) => {
|
||||
const info = useStack(id)?.info;
|
||||
const fullInfo = useFullStack(id)?.info;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ReactNode } from "react";
|
||||
import { Card, CardHeader } from "@ui/card";
|
||||
import { useFullStack, useStack } from ".";
|
||||
import { Types } from "@monitor/client";
|
||||
import { updateLogToHtml } from "@lib/utils";
|
||||
|
||||
export const StackInfo = ({
|
||||
id,
|
||||
@@ -65,7 +66,12 @@ export const StackInfo = ({
|
||||
{stack?.info?.remote_errors?.map((content, i) => (
|
||||
<pre key={i} className="flex flex-col gap-2">
|
||||
path: {content.path}
|
||||
<pre>{content.contents}</pre>
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: updateLogToHtml(content.contents),
|
||||
}}
|
||||
className="max-h-[500px] overflow-y-auto"
|
||||
/>
|
||||
</pre>
|
||||
))}
|
||||
</CardHeader>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Link } from "react-router-dom";
|
||||
import { fmt_duration, fmt_operation, fmt_version } from "@lib/formatting";
|
||||
import {
|
||||
cn,
|
||||
is_service_user,
|
||||
updateLogToHtml,
|
||||
usableResourcePath,
|
||||
version_is_none,
|
||||
@@ -50,12 +51,7 @@ export const UpdateUser = ({
|
||||
defaultAvatar?: boolean;
|
||||
muted?: boolean;
|
||||
}) => {
|
||||
if (
|
||||
user_id === "Procedure" ||
|
||||
user_id === "Github" ||
|
||||
user_id === "Auto Redeploy" ||
|
||||
user_id === "Resource Sync"
|
||||
) {
|
||||
if (is_service_user(user_id)) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: Inter;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
pre {
|
||||
@apply bg-card text-card-foreground border rounded-xl min-h-full text-xs p-4 whitespace-pre-wrap scroll-m-4 break-all;
|
||||
|
||||
@@ -207,3 +207,17 @@ export const sync_no_changes = (sync: Types.ResourceSync) => {
|
||||
!pending.data.user_group_updates
|
||||
);
|
||||
};
|
||||
|
||||
export const is_service_user = (user_id: string) => {
|
||||
return (
|
||||
user_id === "System" ||
|
||||
user_id === "Procedure" ||
|
||||
user_id === "Github" ||
|
||||
user_id === "Git Webhook" ||
|
||||
user_id === "Auto Redeploy" ||
|
||||
user_id === "Resource Sync" ||
|
||||
user_id === "Stack Wizard" ||
|
||||
user_id === "Build Manager" ||
|
||||
user_id === "Repo Manager"
|
||||
);
|
||||
};
|
||||
|
||||
@@ -129,7 +129,7 @@ const ResourceHeader = ({ type, id }: { type: UsableResource; id: string }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm text-muted-foreground">Description: </p>
|
||||
{/* <p className="text-sm text-muted-foreground">Description: </p> */}
|
||||
<ResourceDescription type={type} id={id} disabled={!canWrite} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
20
runfile.toml
20
runfile.toml
@@ -15,6 +15,16 @@ cmd = "yarn dev"
|
||||
path = "frontend"
|
||||
cmd = "yarn build"
|
||||
|
||||
[test-compose]
|
||||
description = "deploys test.compose.yaml"
|
||||
cmd = """
|
||||
docker compose -f test.compose.yaml down && \
|
||||
docker compose -f test.compose.yaml up --attach monitor-periphery"""
|
||||
|
||||
[test-core]
|
||||
description = "runs core --release pointing to test.core.config.toml"
|
||||
cmd = "MONITOR_CONFIG_PATH=test.core.config.toml cargo run -p monitor_core --release"
|
||||
|
||||
[update-periphery]
|
||||
path = "."
|
||||
cmd = """
|
||||
@@ -24,13 +34,6 @@ cp ./target/release/periphery /usr/local/bin/periphery && \
|
||||
chmod +x /usr/local/bin/periphery && \
|
||||
systemctl start periphery"""
|
||||
|
||||
[monitor-cli-test]
|
||||
path = "bin/cli"
|
||||
cmd = "cargo run -- execute --help"
|
||||
|
||||
[rustdoc-server]
|
||||
cmd = "cargo watch -s 'cargo doc --no-deps -p monitor_client' & http --quiet target/doc"
|
||||
|
||||
[docsite-start]
|
||||
path = "docsite"
|
||||
cmd = "yarn start"
|
||||
@@ -38,3 +41,6 @@ cmd = "yarn start"
|
||||
[docsite-deploy]
|
||||
path = "docsite"
|
||||
cmd = "yarn deploy"
|
||||
|
||||
[rustdoc-server]
|
||||
cmd = "cargo watch -s 'cargo doc --no-deps -p monitor_client' & http --quiet target/doc"
|
||||
38
test.compose.yaml
Normal file
38
test.compose.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
services:
|
||||
monitor-periphery:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: bin/periphery/Dockerfile
|
||||
logging:
|
||||
driver: local
|
||||
networks:
|
||||
- monitor-network
|
||||
ports:
|
||||
- 8120:8120
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- monitor-repos:/etc/monitor/repos
|
||||
# environment:
|
||||
# # If the disk size is overreporting, can use one of these to
|
||||
# # whitelist / blacklist the disks to filter them, whichever is easier.
|
||||
# PERIPHERY_INCLUDE_DISK_MOUNTS: /etc/monitor/repos
|
||||
# PERIPHERY_EXCLUDE_DISK_MOUNTS: /snap
|
||||
|
||||
monitor-mongo:
|
||||
image: mongo
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: none
|
||||
networks:
|
||||
- monitor-network
|
||||
ports:
|
||||
- 27017:27017
|
||||
volumes:
|
||||
- db-data:/data/db
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
monitor-repos:
|
||||
|
||||
networks:
|
||||
monitor-network: {}
|
||||
34
test.core.config.toml
Normal file
34
test.core.config.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
title = "Test"
|
||||
host = "http://localhost.9120"
|
||||
passkey = "a_random_passkey"
|
||||
ensure_server = "http://localhost:8120"
|
||||
|
||||
############
|
||||
# DATABASE #
|
||||
############
|
||||
|
||||
mongo.address = "localhost:27017"
|
||||
|
||||
################
|
||||
# AUTH / LOGIN #
|
||||
################
|
||||
|
||||
local_auth = true
|
||||
jwt_secret = "your_random_secret"
|
||||
jwt_ttl = "2-wk"
|
||||
|
||||
############
|
||||
# WEBHOOKS #
|
||||
############
|
||||
|
||||
webhook_secret = "a_random_webhook_secret"
|
||||
|
||||
##################
|
||||
# POLL INTERVALS #
|
||||
##################
|
||||
|
||||
stack_poll_interval = "1-min"
|
||||
sync_poll_interval = "1-min"
|
||||
build_poll_interval = "1-min"
|
||||
repo_poll_interval = "1-min"
|
||||
monitoring_interval = "5-sec"
|
||||
Reference in New Issue
Block a user