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:
Maxwell Becker
2024-08-18 03:04:47 -04:00
committed by GitHub
parent 418f359492
commit 43593162b0
20 changed files with 569 additions and 164 deletions

View File

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

View File

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

View File

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

View File

@@ -91,6 +91,7 @@ pub enum PeripheryRequest {
// Compose
ListComposeProjects(ListComposeProjects),
GetComposeContentsOnHost(GetComposeContentsOnHost),
GetComposeServiceLog(GetComposeServiceLog),
GetComposeServiceLogSearch(GetComposeServiceLogSearch),
ComposeUp(ComposeUp),

View File

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

View File

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

View File

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

View File

@@ -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[];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
);
};

View File

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

View File

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