mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-29 18:16:43 +00:00
ProxMenux 1.2.6.2-beta: OCI containers in the Monitor, docs and fixes
OCI manager Apps - App tab: containers installed from an OCI image are identified from their installation record; the application and image versions are shown and an update is detected by image digest; repository link; Refresh data. - Updates tab for OCI containers: Update and Recreate run the same flow as the OCI menu in the Monitor terminal; the pre-update backup can be kept in a backup storage; scheduled image updates with an optional minimum age. - Logs tab: console output of the application, kept on the host (lxc.console.logfile + logrotate) and followed live. - The Proxmox console opens a shell (cmode: shell) when the image has one. - A damaged image download is fetched again before failing. - Multi-container applications open at their LAN address; volume mount points on block storage report their usage. Monitor - Proxmox notifications are delivered to a loopback-only HTTP listener when HTTPS is enabled, so they no longer fail certificate verification. - Log persistence counts recurring patterns only; an ended burst is not reported as persistent and its warning clears on its own (#386). - Proxmox notification config backups are deduplicated and capped at three. - The update icon on the Apps page opens the container on its Updates tab. - Version 1.2.6.2-beta and its release notes in every Monitor language. Docs - OCI manager Apps and Audit & Report rebuilt as per-page message files, with a new page for OCI containers in the Monitor. - Seven pages fixed where rich-text tags were missing from t.rich. Translations - Spanish fixes across the OCI engine, the Monitor and the TUI menus. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
386d33df6e
commit
4437a671d2
@@ -80,6 +80,19 @@ export const sidebarItems: MenuItem[] = [
|
||||
],
|
||||
},
|
||||
{ title: "Health Monitor", i18nKey: "healthMonitor", href: "/docs/monitor/health-monitor" },
|
||||
{
|
||||
title: "Audit & Report",
|
||||
i18nKey: "auditReport",
|
||||
href: "/docs/monitor/audit-report",
|
||||
submenu: [
|
||||
{ title: "Overview", i18nKey: "auditReportOverview", href: "/docs/monitor/audit-report" },
|
||||
{ title: "Assessment & inventory", i18nKey: "auditReportAssessment", href: "/docs/monitor/audit-report/assessment" },
|
||||
{ title: "Changes", i18nKey: "auditReportChanges", href: "/docs/monitor/audit-report/changes" },
|
||||
{ title: "Policy", i18nKey: "auditReportPolicy", href: "/docs/monitor/audit-report/policy" },
|
||||
{ title: "Reports & comparisons", i18nKey: "auditReportReports", href: "/docs/monitor/audit-report/reports" },
|
||||
{ title: "Scope & guarantees", i18nKey: "auditReportScope", href: "/docs/monitor/audit-report/scope" },
|
||||
],
|
||||
},
|
||||
{ title: "Notifications", i18nKey: "notifications", href: "/docs/monitor/notifications" },
|
||||
{ title: "AI Assistant", i18nKey: "aiAssistant", href: "/docs/monitor/ai-assistant" },
|
||||
{ title: "API Reference", i18nKey: "apiReference", href: "/docs/monitor/api" },
|
||||
@@ -87,6 +100,22 @@ export const sidebarItems: MenuItem[] = [
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: "OCI manager Apps",
|
||||
i18nKey: "ociManager",
|
||||
href: "/docs/oci-manager",
|
||||
submenu: [
|
||||
{ title: "Overview", i18nKey: "ociOverview", href: "/docs/oci-manager" },
|
||||
{ title: "How an OCI image is translated", i18nKey: "ociArchitecture", href: "/docs/oci-manager/architecture" },
|
||||
{ title: "Image not in the catalog", i18nKey: "ociCustomImage", href: "/docs/oci-manager/custom-image" },
|
||||
{ title: "Data, paths and networking", i18nKey: "ociStorageNetwork", href: "/docs/oci-manager/storage-network" },
|
||||
{ title: "Devices and acceleration", i18nKey: "ociHardware", href: "/docs/oci-manager/hardware" },
|
||||
{ title: "Multi-container applications", i18nKey: "ociStacks", href: "/docs/oci-manager/stacks" },
|
||||
{ title: "Install, update and recreate", i18nKey: "ociLifecycle", href: "/docs/oci-manager/lifecycle" },
|
||||
{ title: "In ProxMenux Monitor", i18nKey: "ociMonitor", href: "/docs/oci-manager/monitor" },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: "ProxMenux Scripts",
|
||||
i18nKey: "proxmenuxScripts",
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
import type { ReactNode } from "react"
|
||||
import Image from "next/image"
|
||||
import { getMessages, getTranslations } from "next-intl/server"
|
||||
import {
|
||||
Archive,
|
||||
Boxes,
|
||||
Braces,
|
||||
CheckCircle2,
|
||||
Cpu,
|
||||
Database,
|
||||
Download,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
FolderTree,
|
||||
HardDrive,
|
||||
Layers3,
|
||||
Network,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
SquareTerminal,
|
||||
Waypoints,
|
||||
} from "lucide-react"
|
||||
import { Link } from "@/i18n/navigation"
|
||||
import { Callout } from "@/components/ui/callout"
|
||||
import { DataFlowDiagram } from "@/components/ui/data-flow-diagram"
|
||||
import { DocHeader } from "@/components/ui/doc-header"
|
||||
import { Mermaid } from "@/components/ui/mermaid"
|
||||
import { STACK_DEPENDENCY_HOOK_SOURCE } from "@/components/oci/stackDependencyHookSource"
|
||||
|
||||
/*
|
||||
* A documentation page described entirely by its message file.
|
||||
*
|
||||
* The page JSON holds `header` and an ordered list of `sections`, each with
|
||||
* an optional title and intro and a list of blocks. A block is an object
|
||||
* with a single key naming its kind ({"p": "..."}, {"table": {...}}), so no
|
||||
* structural value is ever a translatable string. Values the translation
|
||||
* pipeline must leave alone live under keys it copies verbatim: `code`,
|
||||
* `chartCode`, `icon`, `src`, `href`, `id`.
|
||||
*
|
||||
* Prose goes through t.rich, so it may use <code>, <strong>, <em> and the
|
||||
* link tags the page declares.
|
||||
*/
|
||||
|
||||
type Rich = (chunks: ReactNode) => ReactNode
|
||||
type Json = Record<string, any>
|
||||
|
||||
const ICONS: Record<string, ReactNode> = {
|
||||
archive: <Archive className="h-5 w-5" />,
|
||||
boxes: <Boxes className="h-5 w-5" />,
|
||||
braces: <Braces className="h-5 w-5" />,
|
||||
cpu: <Cpu className="h-5 w-5" />,
|
||||
database: <Database className="h-5 w-5" />,
|
||||
fileText: <FileText className="h-5 w-5" />,
|
||||
folderTree: <FolderTree className="h-5 w-5" />,
|
||||
hardDrive: <HardDrive className="h-5 w-5" />,
|
||||
layers: <Layers3 className="h-5 w-5" />,
|
||||
network: <Network className="h-5 w-5" />,
|
||||
refresh: <RefreshCw className="h-5 w-5" />,
|
||||
shield: <ShieldCheck className="h-5 w-5" />,
|
||||
terminal: <SquareTerminal className="h-5 w-5" />,
|
||||
waypoints: <Waypoints className="h-5 w-5" />,
|
||||
}
|
||||
|
||||
const CALLOUTS = {
|
||||
calloutInfo: "info",
|
||||
calloutTip: "tip",
|
||||
calloutWarning: "warning",
|
||||
calloutDanger: "danger",
|
||||
} as const
|
||||
|
||||
const SNIPPETS: Record<string, string> = {
|
||||
stackDependencyHook: STACK_DEPENDENCY_HOOK_SOURCE,
|
||||
}
|
||||
|
||||
function at(root: Json, path: string): any {
|
||||
return path.split(".").reduce((node, key) => (node == null ? undefined : node[key]), root as any)
|
||||
}
|
||||
|
||||
export async function DocPage({
|
||||
locale,
|
||||
namespace,
|
||||
minutes,
|
||||
links = {},
|
||||
}: {
|
||||
locale: string
|
||||
namespace: string
|
||||
minutes: number
|
||||
links?: Record<string, string>
|
||||
}) {
|
||||
const t = await getTranslations({ locale, namespace })
|
||||
const messages = (await getMessages({ locale })) as Json
|
||||
const page = at(messages, namespace) as Json
|
||||
|
||||
const tags: Record<string, Rich> = {
|
||||
code: (chunks) => <code>{chunks}</code>,
|
||||
strong: (chunks) => <strong>{chunks}</strong>,
|
||||
em: (chunks) => <em>{chunks}</em>,
|
||||
}
|
||||
for (const [name, href] of Object.entries(links)) {
|
||||
tags[name] = href.startsWith("http")
|
||||
? (chunks) => (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-blue-600 hover:underline">
|
||||
{chunks}
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
)
|
||||
: (chunks) => <Link href={href} className="text-blue-600 hover:underline">{chunks}</Link>
|
||||
}
|
||||
const rich = (key: string) => t.rich(key, tags)
|
||||
|
||||
const block = (key: string, value: Json, index: number): ReactNode => {
|
||||
const [kind] = Object.keys(value)
|
||||
const base = `${key}.${kind}`
|
||||
const data = value[kind]
|
||||
switch (kind) {
|
||||
case "p":
|
||||
return <p key={index} className="mb-4 text-gray-800 leading-relaxed">{rich(base)}</p>
|
||||
|
||||
case "calloutInfo":
|
||||
case "calloutTip":
|
||||
case "calloutWarning":
|
||||
case "calloutDanger":
|
||||
return (
|
||||
<Callout key={index} variant={CALLOUTS[kind]} title={t(`${base}.title`)}>
|
||||
{rich(`${base}.body`)}
|
||||
</Callout>
|
||||
)
|
||||
|
||||
case "list":
|
||||
return (
|
||||
<ul key={index} className="mb-6 space-y-2 text-gray-800">
|
||||
{data.items.map((_: string, i: number) => (
|
||||
<li key={i} className="flex gap-3 leading-relaxed">
|
||||
<CheckCircle2 className="mt-1 h-4 w-4 shrink-0 text-blue-600" />
|
||||
<span>{rich(`${base}.items.${i}`)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
|
||||
case "steps":
|
||||
return (
|
||||
<ol key={index} className="mb-6 space-y-3">
|
||||
{data.items.map((_: Json, i: number) => (
|
||||
<li key={i} className="grid grid-cols-[2.25rem_1fr] gap-4 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-blue-700 text-sm font-bold text-white">{i + 1}</span>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">{t(`${base}.items.${i}.title`)}</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-gray-700">{rich(`${base}.items.${i}.body`)}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)
|
||||
|
||||
case "cards":
|
||||
return (
|
||||
<div key={index} className="mb-6 grid gap-4 md:grid-cols-2">
|
||||
{data.items.map((item: Json, i: number) => (
|
||||
<article key={i} className="rounded-lg border border-gray-200 bg-white p-5">
|
||||
{item.icon && ICONS[item.icon] && (
|
||||
<div className="mb-3 inline-flex rounded-lg border border-blue-100 bg-blue-50 p-2 text-blue-700">{ICONS[item.icon]}</div>
|
||||
)}
|
||||
<h3 className="font-semibold text-gray-900">{t(`${base}.items.${i}.title`)}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-gray-700">{rich(`${base}.items.${i}.body`)}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
case "table":
|
||||
return (
|
||||
<div key={index} className="mb-6 overflow-x-auto">
|
||||
<table className="w-full text-sm border border-gray-200 rounded-md">
|
||||
<thead className="bg-gray-50 text-gray-900">
|
||||
<tr>
|
||||
{data.headers.map((_: string, i: number) => (
|
||||
<th key={i} className="text-left px-3 py-2 border-b border-gray-200 font-semibold">{t(`${base}.headers.${i}`)}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-800">
|
||||
{data.rows.map((row: string[], r: number) => (
|
||||
<tr key={r} className={r < data.rows.length - 1 ? "border-b border-gray-100" : ""}>
|
||||
{row.map((_: string, c: number) => (
|
||||
<td key={c} className={`px-3 py-2 align-top leading-6 ${c === 0 ? "font-medium text-gray-900" : ""}`}>
|
||||
{rich(`${base}.rows.${r}.${c}`)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
|
||||
case "flow":
|
||||
return (
|
||||
<DataFlowDiagram
|
||||
key={index}
|
||||
className="mb-6"
|
||||
nodes={data.nodes.map((_: Json, i: number) => ({
|
||||
label: t(`${base}.nodes.${i}.label`),
|
||||
detail: data.nodes[i].detail ? t(`${base}.nodes.${i}.detail`) : undefined,
|
||||
variant: i === 0 ? "source" : i === data.nodes.length - 1 ? "target" : "bridge",
|
||||
}))}
|
||||
caption={data.caption ? t(`${base}.caption`) : undefined}
|
||||
/>
|
||||
)
|
||||
|
||||
case "code":
|
||||
return (
|
||||
<div key={index} className="mb-6 overflow-hidden rounded-lg border border-gray-200">
|
||||
{data.title && <div className="border-b border-gray-200 bg-gray-50 px-4 py-2 text-sm font-semibold text-gray-900">{t(`${base}.title`)}</div>}
|
||||
<pre className="overflow-x-auto bg-white p-4 text-xs leading-6 text-gray-800">{data.code}</pre>
|
||||
</div>
|
||||
)
|
||||
|
||||
case "codeGrid":
|
||||
return (
|
||||
<div key={index} className="mb-6 grid gap-4 lg:grid-cols-2">
|
||||
{data.items.map((item: Json, i: number) => (
|
||||
<div key={i} className="overflow-hidden rounded-lg border border-gray-200">
|
||||
<div className="border-b border-gray-200 bg-gray-50 px-4 py-2 text-sm font-semibold text-gray-900">{t(`${base}.items.${i}.title`)}</div>
|
||||
<pre className="overflow-x-auto bg-white p-4 text-xs leading-6 text-gray-800">{item.code}</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
case "shell":
|
||||
return (
|
||||
<pre key={index} className="mb-6 overflow-x-auto rounded-lg bg-gray-950 p-4 text-xs leading-6 text-gray-100">{data.code}</pre>
|
||||
)
|
||||
|
||||
case "mermaid": {
|
||||
let chart: string = data.chartCode
|
||||
for (const name of Object.keys(data.labels ?? {})) {
|
||||
chart = chart.split(`{{${name}}}`).join(t(`${base}.labels.${name}`).replace(/"/g, "'"))
|
||||
}
|
||||
return <div key={index} className="mb-6"><Mermaid chart={chart} /></div>
|
||||
}
|
||||
|
||||
case "figure":
|
||||
return (
|
||||
<figure key={index} className="my-6">
|
||||
<Image
|
||||
src={data.src}
|
||||
alt={t(`${base}.alt`)}
|
||||
width={data.width ?? 1600}
|
||||
height={data.height ?? 1000}
|
||||
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
|
||||
/>
|
||||
{data.caption && (
|
||||
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{rich(`${base}.caption`)}</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
)
|
||||
|
||||
case "snippet":
|
||||
return (
|
||||
<details key={index} className="group mb-6 overflow-hidden rounded-lg border border-blue-200 bg-white">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 bg-blue-50 px-5 py-3 font-semibold text-gray-900 marker:content-none">
|
||||
<span>{t(`${base}.summary`)}</span>
|
||||
<code className="text-xs font-normal text-gray-600">{data.pathCode}</code>
|
||||
</summary>
|
||||
<pre className="max-h-[42rem] overflow-auto border-t border-blue-200 bg-gray-950 p-5 text-xs leading-6 text-gray-100">
|
||||
<code>{SNIPPETS[data.snippetCode] ?? ""}</code>
|
||||
</pre>
|
||||
</details>
|
||||
)
|
||||
|
||||
case "downloads":
|
||||
return (
|
||||
<div key={index} className="mb-6 space-y-4">
|
||||
{data.items.map((item: Json, i: number) => (
|
||||
<article key={i} className="overflow-hidden rounded-lg border border-gray-200 bg-white">
|
||||
<div className="flex items-start gap-4 border-b border-gray-200 bg-gray-50 px-4 py-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="font-semibold text-gray-900">{t(`${base}.items.${i}.title`)}</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-gray-700">{rich(`${base}.items.${i}.body`)}</p>
|
||||
</div>
|
||||
<a href={item.href} target="_blank" rel="noopener noreferrer" className="inline-flex shrink-0 items-center gap-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-sm font-medium text-blue-800 hover:bg-blue-100">
|
||||
<Download className="h-4 w-4" />PDF
|
||||
</a>
|
||||
</div>
|
||||
<dl className="grid text-sm md:grid-cols-[11rem_1fr]">
|
||||
{(item.facts ?? []).map((_: Json, f: number) => (
|
||||
<div key={f} className="contents">
|
||||
<dt className="border-t border-gray-200 bg-white px-4 py-2 font-semibold text-gray-900">{t(`${base}.items.${i}.facts.${f}.label`)}</dt>
|
||||
<dd className="border-t border-gray-200 bg-white px-4 py-2 leading-6 text-gray-700">{rich(`${base}.items.${i}.facts.${f}.value`)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
case "next":
|
||||
return (
|
||||
<ul key={index} className="mb-6 list-disc pl-6 space-y-1 text-gray-800">
|
||||
{data.items.map((item: Json, i: number) => (
|
||||
<li key={i}>
|
||||
<Link href={item.href} className="text-blue-600 hover:underline">{t(`${base}.items.${i}.label`)}</Link>
|
||||
{item.tail ? <> — {t(`${base}.items.${i}.tail`)}</> : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pb-16">
|
||||
<DocHeader
|
||||
title={t("header.title")}
|
||||
description={t("header.description")}
|
||||
section={t("header.section")}
|
||||
estimatedMinutes={minutes}
|
||||
/>
|
||||
{(page.sections as Json[]).map((section, s) => (
|
||||
<section key={s} id={section.id} className="scroll-mt-24">
|
||||
{section.title && <h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t(`sections.${s}.title`)}</h2>}
|
||||
{section.intro && <p className="mb-4 text-gray-800 leading-relaxed">{rich(`sections.${s}.intro`)}</p>}
|
||||
{(section.blocks as Json[]).map((value, b) => block(`sections.${s}.blocks.${b}`, value, b))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Generated documentation snapshot of oci/remote/stack_dependency_hook.sh.
|
||||
// Keep this file synchronized when the installed hook changes.
|
||||
export const STACK_DEPENDENCY_HOOK_SOURCE = "#!/usr/bin/env bash\nset -Eeuo pipefail\n\nPATH=/usr/sbin:/usr/bin:/sbin:/bin\n\ndie() {\n printf 'ERROR: %s\\n' \"$*\" >&2\n exit 1\n}\n\nfind_snippet_storage() {\n local storage\n if pvesm status --content snippets 2>/dev/null \\\n | awk 'NR > 1 && $1 == \"local\" && $3 == \"active\" {found=1} END {exit !found}'; then\n printf 'local'\n return\n fi\n storage=$(pvesm status --content snippets 2>/dev/null \\\n | awk 'NR > 1 && $3 == \"active\" {print $1; exit}')\n [[ -n $storage ]] || die \"No active storage supports snippets\"\n printf '%s' \"$storage\"\n}\n\ninstall_hook() {\n local main_id=${1:?missing main VMID} source_config=${2:?missing lifecycle JSON}\n local storage hook_volume hook_path target_config\n [[ $main_id =~ ^[0-9]+$ ]] || die \"Invalid main VMID\"\n jq -e '\n .schema == 1 and\n (.dependencies | type == \"array\") and\n (.dependencies | length > 0) and\n (all(.dependencies[];\n (.vmid | type == \"number\") and\n (.label | type == \"string\") and\n (.healthcheck.type | IN(\"exec\", \"http\", \"running\")) and\n (.healthcheck.timeout_seconds | type == \"number\")\n ))\n ' \"$source_config\" >/dev/null || die \"Invalid dependency contract\"\n\n storage=$(find_snippet_storage)\n hook_volume=\"${storage}:snippets/proxmenux-stack-dependencies.sh\"\n hook_path=$(pvesm path \"$hook_volume\")\n install -D -m 0755 \"$0\" \"$hook_path\"\n\n target_config=\"/etc/pve/priv/proxmenux-stack-${main_id}.json\"\n umask 077\n cat \"$source_config\" >\"$target_config\"\n pct set \"$main_id\" --hookscript \"$hook_volume\" >/dev/null\n printf 'Proxmox hookscript installed: CT %s starts its dependencies through %s\\n' \\\n \"$main_id\" \"$hook_volume\"\n}\n\ndependency_is_healthy() {\n local id=$1 healthcheck=$2 type url\n type=$(jq -r '.type' <<<\"$healthcheck\")\n case \"$type\" in\n running)\n [[ $(pct status \"$id\" 2>/dev/null || true) == \"status: running\" ]]\n ;;\n exec)\n local -a command=()\n mapfile -t command < <(jq -r '.argv[]' <<<\"$healthcheck\")\n ((${#command[@]} > 0)) || return 1\n pct exec \"$id\" -- \"${command[@]}\" >/dev/null 2>&1\n ;;\n http)\n url=$(jq -r '.url' <<<\"$healthcheck\")\n curl -fsS --max-time 3 \"$url\" >/dev/null 2>&1\n ;;\n *) return 1 ;;\n esac\n}\n\nstart_dependencies() {\n local main_id=$1 config=\"/etc/pve/priv/proxmenux-stack-${1}.json\"\n local encoded dependency id label healthcheck timeout elapsed\n [[ -r $config ]] || die \"Missing dependency contract for main CT $main_id\"\n\n exec 9>\"/run/lock/proxmenux-stack-${main_id}.lock\"\n flock 9\n while IFS= read -r encoded; do\n [[ -n $encoded ]] || continue\n dependency=$(base64 -d <<<\"$encoded\")\n id=$(jq -r '.vmid' <<<\"$dependency\")\n label=$(jq -r '.label' <<<\"$dependency\")\n healthcheck=$(jq -c '.healthcheck' <<<\"$dependency\")\n timeout=$(jq -r '.healthcheck.timeout_seconds' <<<\"$dependency\")\n [[ $id =~ ^[0-9]+$ && $timeout =~ ^[0-9]+$ && $timeout -gt 0 ]] \\\n || die \"Invalid dependency in $config\"\n pct config \"$id\" >/dev/null 2>&1 \\\n || die \"Dependency $label (CT $id) does not exist\"\n\n if [[ $(pct status \"$id\" 2>/dev/null || true) != \"status: running\" ]]; then\n printf 'Starting dependency %s (CT %s)...\\n' \"$label\" \"$id\"\n pct start \"$id\" || die \"Could not start $label (CT $id)\"\n else\n printf 'Dependency %s (CT %s) was already running.\\n' \"$label\" \"$id\"\n fi\n\n elapsed=0\n while (( elapsed < timeout )); do\n if dependency_is_healthy \"$id\" \"$healthcheck\"; then\n printf 'Dependency %s (CT %s): ready.\\n' \"$label\" \"$id\"\n break\n fi\n [[ $(pct status \"$id\" 2>/dev/null || true) == \"status: running\" ]] \\\n || die \"$label (CT $id) stopped while starting\"\n sleep 2\n elapsed=$((elapsed + 2))\n done\n (( elapsed < timeout )) \\\n || die \"$label (CT $id) did not pass its health check within ${timeout}s\"\n done < <(jq -r '.dependencies[] | @base64' \"$config\")\n}\n\nif [[ ${1:-} == \"--install\" ]]; then\n shift\n install_hook \"$@\"\n exit 0\nfi\n\nvmid=${1:?missing VMID}\nphase=${2:?missing lifecycle phase}\ncase \"$phase\" in\n pre-start) start_dependencies \"$vmid\" ;;\n post-start|pre-stop|post-stop) ;;\n *) die \"Unknown lifecycle phase: $phase\" ;;\nesac\n"
|
||||
|
||||
Reference in New Issue
Block a user