Compare commits

..

12 Commits

Author SHA1 Message Date
mbecker20
2463ed3879 1.13.4 Fix periphery image registry login / pull ordering 2024-08-20 12:50:07 -04:00
mbecker20
a2758ce6f4 Stack: login to registry BEFORE pulling image 2024-08-20 12:49:19 -04:00
mbecker20
3f1788dbbb docsite: reindent ports in example 2024-08-19 11:36:17 -04:00
mbecker20
33a0560af6 frontend: Ignore Services correct content hidden logic 2024-08-19 00:44:48 -04:00
mbecker20
610a10c488 frontend: show state when using Stack files-on-host 2024-08-18 21:16:53 -04:00
mbecker20
39b217687d 1.13.3 filter Periphery disks 2024-08-18 18:12:47 -04:00
mbecker20
2f73461979 stack don't show Git Repo config is file contents defined in UI 2024-08-18 18:06:12 -04:00
mbecker20
aae9bb9e51 move image registry to the top of Build Image config 2024-08-18 18:06:12 -04:00
mbecker20
7d011d93fa Fix: Periphery should flter out "overlay" volumes, not include them 2024-08-18 18:05:56 -04:00
mbecker20
bffdea4357 Swarm roadmap 2024-08-18 16:05:58 -04:00
mbecker20
790566bf79 add note about podman support 2024-08-18 13:37:26 -04:00
mbecker20
b17db93f13 quick-fix: fix build version config 2024-08-18 04:57:25 -04:00
10 changed files with 324 additions and 309 deletions

28
Cargo.lock generated
View File

@@ -41,7 +41,7 @@ dependencies = [
[[package]]
name = "alerter"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"anyhow",
"axum",
@@ -922,7 +922,7 @@ dependencies = [
[[package]]
name = "command"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"monitor_client",
"run_command",
@@ -1306,7 +1306,7 @@ dependencies = [
[[package]]
name = "formatting"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"serror",
]
@@ -1437,7 +1437,7 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "git"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"anyhow",
"command",
@@ -2041,7 +2041,7 @@ dependencies = [
[[package]]
name = "logger"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"anyhow",
"monitor_client",
@@ -2111,7 +2111,7 @@ dependencies = [
[[package]]
name = "migrator"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"anyhow",
"chrono",
@@ -2164,9 +2164,9 @@ dependencies = [
[[package]]
name = "mongo_indexed"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2570cf8b41b536db37f64a27bafb814e3f1e58db5266dd295427364d957e974f"
checksum = "556e2883109599e3cc28c7ad0d700c0b5c297e4d17ae810c669d18f79a02df26"
dependencies = [
"mongo_indexed_derive",
"mongodb",
@@ -2246,7 +2246,7 @@ dependencies = [
[[package]]
name = "monitor_cli"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"anyhow",
"clap",
@@ -2262,7 +2262,7 @@ dependencies = [
[[package]]
name = "monitor_client"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2294,7 +2294,7 @@ dependencies = [
[[package]]
name = "monitor_core"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2349,7 +2349,7 @@ dependencies = [
[[package]]
name = "monitor_periphery"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"anyhow",
"async_timing_util",
@@ -2773,7 +2773,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "periphery_client"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"anyhow",
"monitor_client",
@@ -4507,7 +4507,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "update_logger"
version = "1.13.2"
version = "1.13.4"
dependencies = [
"anyhow",
"logger",

View File

@@ -3,7 +3,7 @@ resolver = "2"
members = ["bin/*", "lib/*", "client/core/rs", "client/periphery/rs"]
[workspace.package]
version = "1.13.2"
version = "1.13.4"
edition = "2021"
authors = ["mbecker20 <becker.maxh@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -15,7 +15,7 @@ monitor_client = { path = "client/core/rs" }
[workspace.dependencies]
# LOCAL
monitor_client = "1.13.2"
monitor_client = "1.13.3"
periphery_client = { path = "client/periphery/rs" }
formatting = { path = "lib/formatting" }
command = { path = "lib/command" }
@@ -32,7 +32,7 @@ merge_config_files = "0.1.5"
async_timing_util = "1.0.0"
partial_derive2 = "0.4.3"
derive_variants = "1.0.0"
mongo_indexed = "2.0.0"
mongo_indexed = "2.0.1"
resolver_api = "1.1.1"
toml_pretty = "1.1.2"
mungos = "1.0.1"

View File

@@ -122,22 +122,6 @@ pub async fn compose_up(
let last_project_name = stack.project_name(false);
let project_name = stack.project_name(true);
// Pull images before destroying to minimize downtime.
// If this fails, do not continue.
let log = run_monitor_command(
"compose pull",
format!(
"cd {run_dir} && {docker_compose} -p {project_name} -f {file_args} pull{service_arg}",
),
)
.await;
if !log.success {
res.logs.push(log);
return Err(anyhow!(
"Failed to pull required images, stopping the run."
));
}
// Login to the registry to pull private images, if account is set
if !stack.config.registry_account.is_empty() {
let registry = ImageRegistry::Standard(StandardRegistryConfig {
@@ -157,6 +141,22 @@ pub async fn compose_up(
.context("failed to login to image registry")?;
}
// Pull images before destroying to minimize downtime.
// If this fails, do not continue.
let log = run_monitor_command(
"compose pull",
format!(
"cd {run_dir} && {docker_compose} -p {project_name} -f {file_args} pull{service_arg}",
),
)
.await;
if !log.success {
res.logs.push(log);
return Err(anyhow!(
"Failed to pull required images, stopping the run."
));
}
// Take down the existing containers.
// This one tries to use the previously deployed service name, to ensure the right stack is taken down.
destroy_existing_containers(&last_project_name, service, res)

View File

@@ -107,7 +107,7 @@ impl StatsClient {
.list()
.iter()
.filter(|d| {
if d.file_system() != "overlay" {
if d.file_system() == "overlay" {
return false;
}
let path = d.mount_point();

View File

@@ -43,8 +43,8 @@ services:
image: ghcr.io/mbecker20/periphery:latest # use ghcr.io/mbecker20/periphery:latest-aarch64 for arm support
logging:
driver: local
ports:
- 8120:8120
ports:
- 8120:8120
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- monitor-repos:/etc/monitor/repos # manage repos in a docker volume, or change it to an accessible host directory.

View File

@@ -20,6 +20,11 @@ With Monitor you can:
Monitor is opinionated by design, and uses [docker](https://docs.docker.com/) as the container engine for building and deploying.
:::info
Monitor also supports [**podman**](https://podman.io/) instead of docker by utilizing the `podman` -> `docker` alias.
For Stack / docker compose support with podman, check out [**podman-compose**](https://github.com/containers/podman-compose). Thanks to `u/pup_kit` for checking this.
:::
## Architecture and Components
Monitor is composed of a single core and any amount of connected servers running the periphery application.

View File

@@ -72,7 +72,7 @@ export const BuildConfig = ({
version: (_version, set) => {
const version =
typeof _version === "object"
? `${_version.major}.${_version.major}.${_version.patch}`
? `${_version.major}.${_version.minor}.${_version.patch}`
: _version;
return (
<ConfigInput
@@ -136,6 +136,14 @@ export const BuildConfig = ({
{
label: "Image",
components: {
image_registry: (registry, set) => (
<ImageRegistryConfig
registry={registry}
setRegistry={(image_registry) => set({ image_registry })}
resource_id={update.builder_id ?? config.builder_id}
disabled={disabled}
/>
),
build_path: {
placeholder: ".",
description:
@@ -146,14 +154,6 @@ export const BuildConfig = ({
description:
"The path to the dockerfile, relative to the build path.",
},
image_registry: (registry, set) => (
<ImageRegistryConfig
registry={registry}
setRegistry={(image_registry) => set({ image_registry })}
resource_id={update.builder_id ?? config.builder_id}
disabled={disabled}
/>
),
},
},
{

View File

@@ -47,8 +47,9 @@ export const StackConfig = ({
if (!config) return null;
const disabled = global_disabled || perms !== Types.PermissionLevel.Write;
const files_on_host = update.files_on_host ?? config.files_on_host;
const ui_file_contents =
(update.file_contents ?? config.file_contents ?? "").length > 0;
return (
<Config
@@ -283,7 +284,7 @@ export const StackConfig = ({
description:
"If your compose file has init services that exit early, ignore them here so your stack will report the correct health.",
contentHidden:
((update.extra_args ?? config.extra_args)?.length ?? 0) === 0,
((update.ignore_services ?? config.ignore_services)?.length ?? 0) === 0,
actions: !disabled && (
<Button
variant="secondary"
@@ -317,274 +318,282 @@ export const StackConfig = ({
},
},
],
"Git Repo": !files_on_host && [
{
label: "Git",
description:
"Provide config for repo-based compose files. Not necessary if file contents are configured in UI.",
components: {
git_provider: (provider, set) => {
const https = update.git_https ?? config.git_https;
return (
<ProviderSelectorConfig
account_type="git"
selected={provider}
disabled={disabled}
onSelect={(git_provider) => set({ git_provider })}
https={https}
onHttpsSwitch={() => set({ git_https: !https })}
/>
);
},
git_account: (value, set) => {
const server_id = update.server_id || config.server_id;
return (
<AccountSelectorConfig
id={server_id}
type={server_id ? "Server" : "None"}
account_type="git"
provider={update.git_provider ?? config.git_provider}
selected={value}
onSelect={(git_account) => set({ git_account })}
disabled={disabled}
placeholder="None"
/>
);
},
repo: {
placeholder: "Enter repo",
description:
"The repo path on the provider. {namespace}/{repo_name}",
},
branch: {
placeholder: "Enter branch",
description: "Select a custom branch, or default to 'main'.",
},
commit: {
placeholder: "Enter a specific commit hash. Optional.",
description:
"Switch to a specific hash after cloning the branch.",
"Git Repo": !files_on_host &&
!ui_file_contents && [
{
label: "Git",
description:
"Provide config for repo-based compose files. Not necessary if file contents are configured in UI.",
components: {
git_provider: (provider, set) => {
const https = update.git_https ?? config.git_https;
return (
<ProviderSelectorConfig
account_type="git"
selected={provider}
disabled={disabled}
onSelect={(git_provider) => set({ git_provider })}
https={https}
onHttpsSwitch={() => set({ git_https: !https })}
/>
);
},
git_account: (value, set) => {
const server_id = update.server_id || config.server_id;
return (
<AccountSelectorConfig
id={server_id}
type={server_id ? "Server" : "None"}
account_type="git"
provider={update.git_provider ?? config.git_provider}
selected={value}
onSelect={(git_account) => set({ git_account })}
disabled={disabled}
placeholder="None"
/>
);
},
repo: {
placeholder: "Enter repo",
description:
"The repo path on the provider. {namespace}/{repo_name}",
},
branch: {
placeholder: "Enter branch",
description: "Select a custom branch, or default to 'main'.",
},
commit: {
placeholder: "Enter a specific commit hash. Optional.",
description:
"Switch to a specific hash after cloning the branch.",
},
},
},
},
{
label: "Run Path",
labelHidden: true,
components: {
run_directory: {
placeholder: "./",
description:
"Set the cwd when running compose up command. Relative to the repo root.",
boldLabel: true,
{
label: "Run Path",
labelHidden: true,
components: {
run_directory: {
placeholder: "./",
description:
"Set the cwd when running compose up command. Relative to the repo root.",
boldLabel: true,
},
},
},
},
{
label: "File Paths",
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: "File Paths",
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>
),
},
},
{
label: "Git Webhooks",
description:
"Configure your repo provider to send webhooks to Monitor",
components: {
["Guard" as any]: () => {
if (update.branch ?? config.branch) {
return null;
}
return (
<ConfigItem label="Configure Branch">
<div>Must configure Branch before webhooks will work.</div>
</ConfigItem>
);
},
["Refresh" as any]: () =>
(update.branch ?? config.branch) && (
<ConfigItem label="Refresh Cache">
<CopyGithubWebhook path={`/stack/${id}/refresh`} />
</ConfigItem>
components: {
file_paths: (value, set) => (
<InputList
field="file_paths"
values={value ?? []}
set={set}
disabled={disabled}
placeholder="compose.yaml"
/>
),
["Deploy" as any]: () =>
(update.branch ?? config.branch) && (
<ConfigItem label="Auto Redeploy">
<CopyGithubWebhook path={`/stack/${id}/deploy`} />
</ConfigItem>
),
webhook_enabled:
!!(update.branch ?? config.branch) &&
webhooks !== undefined &&
!webhooks.managed,
webhook_secret: {
description:
"Provide a custom webhook secret for this resource, or use the global default.",
placeholder: "Input custom secret",
},
["managed" as any]: () => {
const inv = useInvalidate();
const { toast } = useToast();
const { mutate: createWebhook, isPending: createPending } =
useWrite("CreateStackWebhook", {
onSuccess: () => {
toast({ title: "Webhook Created" });
inv(["GetStackWebhooksEnabled", { stack: id }]);
},
});
const { mutate: deleteWebhook, isPending: deletePending } =
useWrite("DeleteStackWebhook", {
onSuccess: () => {
toast({ title: "Webhook Deleted" });
inv(["GetStackWebhooksEnabled", { stack: id }]);
},
});
if (
!(update.branch ?? config.branch) ||
!webhooks ||
!webhooks.managed
) {
return null;
}
return (
<ConfigItem label="Manage Webhook">
{webhooks.deploy_enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention("Good")}
>
ENABLED
</div>
and will trigger
<div
className={text_color_class_by_intention("Neutral")}
>
DEPLOY
</div>
</div>
<ConfirmButton
title="Disable"
icon={<Ban className="w-4 h-4" />}
variant="destructive"
onClick={() =>
deleteWebhook({
stack: id,
action: Types.StackWebhookAction.Deploy,
})
}
loading={deletePending}
disabled={disabled || deletePending}
/>
</div>
)}
{!webhooks.deploy_enabled && webhooks.refresh_enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention("Good")}
>
ENABLED
</div>
and will trigger
<div
className={text_color_class_by_intention("Neutral")}
>
REFRESH
</div>
</div>
<ConfirmButton
title="Disable"
icon={<Ban className="w-4 h-4" />}
variant="destructive"
onClick={() =>
deleteWebhook({
stack: id,
action: Types.StackWebhookAction.Refresh,
})
}
loading={deletePending}
disabled={disabled || deletePending}
/>
</div>
)}
{!webhooks.deploy_enabled && !webhooks.refresh_enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention(
"Critical"
)}
>
DISABLED
</div>
</div>
<ConfirmButton
title="Enable Deploy"
icon={<CirclePlus className="w-4 h-4" />}
onClick={() =>
createWebhook({
stack: id,
action: Types.StackWebhookAction.Deploy,
})
}
loading={createPending}
disabled={disabled || createPending}
/>
<ConfirmButton
title="Enable Refresh"
icon={<CirclePlus className="w-4 h-4" />}
onClick={() =>
createWebhook({
stack: id,
action: Types.StackWebhookAction.Refresh,
})
}
loading={createPending}
disabled={disabled || createPending}
/>
</div>
)}
</ConfigItem>
);
},
},
},
],
{
label: "Git Webhooks",
description:
"Configure your repo provider to send webhooks to Monitor",
components: {
["Guard" as any]: () => {
if (update.branch ?? config.branch) {
return null;
}
return (
<ConfigItem label="Configure Branch">
<div>
Must configure Branch before webhooks will work.
</div>
</ConfigItem>
);
},
["Refresh" as any]: () =>
(update.branch ?? config.branch) && (
<ConfigItem label="Refresh Cache">
<CopyGithubWebhook path={`/stack/${id}/refresh`} />
</ConfigItem>
),
["Deploy" as any]: () =>
(update.branch ?? config.branch) && (
<ConfigItem label="Auto Redeploy">
<CopyGithubWebhook path={`/stack/${id}/deploy`} />
</ConfigItem>
),
webhook_enabled:
!!(update.branch ?? config.branch) &&
webhooks !== undefined &&
!webhooks.managed,
webhook_secret: {
description:
"Provide a custom webhook secret for this resource, or use the global default.",
placeholder: "Input custom secret",
},
["managed" as any]: () => {
const inv = useInvalidate();
const { toast } = useToast();
const { mutate: createWebhook, isPending: createPending } =
useWrite("CreateStackWebhook", {
onSuccess: () => {
toast({ title: "Webhook Created" });
inv(["GetStackWebhooksEnabled", { stack: id }]);
},
});
const { mutate: deleteWebhook, isPending: deletePending } =
useWrite("DeleteStackWebhook", {
onSuccess: () => {
toast({ title: "Webhook Deleted" });
inv(["GetStackWebhooksEnabled", { stack: id }]);
},
});
if (
!(update.branch ?? config.branch) ||
!webhooks ||
!webhooks.managed
) {
return null;
}
return (
<ConfigItem label="Manage Webhook">
{webhooks.deploy_enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention("Good")}
>
ENABLED
</div>
and will trigger
<div
className={text_color_class_by_intention(
"Neutral"
)}
>
DEPLOY
</div>
</div>
<ConfirmButton
title="Disable"
icon={<Ban className="w-4 h-4" />}
variant="destructive"
onClick={() =>
deleteWebhook({
stack: id,
action: Types.StackWebhookAction.Deploy,
})
}
loading={deletePending}
disabled={disabled || deletePending}
/>
</div>
)}
{!webhooks.deploy_enabled && webhooks.refresh_enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention("Good")}
>
ENABLED
</div>
and will trigger
<div
className={text_color_class_by_intention(
"Neutral"
)}
>
REFRESH
</div>
</div>
<ConfirmButton
title="Disable"
icon={<Ban className="w-4 h-4" />}
variant="destructive"
onClick={() =>
deleteWebhook({
stack: id,
action: Types.StackWebhookAction.Refresh,
})
}
loading={deletePending}
disabled={disabled || deletePending}
/>
</div>
)}
{!webhooks.deploy_enabled &&
!webhooks.refresh_enabled && (
<div className="flex items-center gap-4 flex-wrap">
<div className="flex items-center gap-2">
Incoming webhook is{" "}
<div
className={text_color_class_by_intention(
"Critical"
)}
>
DISABLED
</div>
</div>
<ConfirmButton
title="Enable Deploy"
icon={<CirclePlus className="w-4 h-4" />}
onClick={() =>
createWebhook({
stack: id,
action: Types.StackWebhookAction.Deploy,
})
}
loading={createPending}
disabled={disabled || createPending}
/>
<ConfirmButton
title="Enable Refresh"
icon={<CirclePlus className="w-4 h-4" />}
onClick={() =>
createWebhook({
stack: id,
action: Types.StackWebhookAction.Refresh,
})
}
loading={createPending}
disabled={disabled || createPending}
/>
</div>
)}
</ConfigItem>
);
},
},
},
],
environment: [
{
label: "Environment",

View File

@@ -135,7 +135,7 @@ export const StackComponents: RequiredResourceComponents = {
State: ({ id }) => {
const state = useStack(id)?.info.state ?? Types.StackState.Unknown;
const config = useFullStack(id)?.config;
if (!config?.file_contents && !config?.repo) {
if (!config?.files_on_host && !config?.file_contents && !config?.repo) {
return null;
}
return <StatusBadge text={state} intent={stack_state_intention(state)} />;

View File

@@ -10,6 +10,7 @@ If you have an idea for Monitor, feel free to open an issue beginning with the `
- **v1.13**: Support "Compose" resource - Paste in a docker compose file and manage it like a Portainer "Stack" ✅
- **v1.14**: Manage docker networks, images, volumes in the UI
- **v1.15**: Support generic OAuth2 providers (including self-hosted).
- **v1.16+**: Support "Cluster" resource - Manage Kubernetes cluster, can attach deployments to "Cluster" (in addition to existing "Server")
- **v1.16**: Support "Swarm" resource - Manage docker swarms, attach Deployments / Stacks to "Swarm".
- **v1.17+**: Support "Cluster" resource - Manage Kubernetes cluster, can attach deployments to "Cluster" (in addition to existing "Server")
**Note. The specific versions associated with these features are not final.**