1.0.29
This commit is contained in:
44 files changed
+1672
-226
No files matched your search
@@ -5,6 +5,8 @@
|
||||
* - dockhand.update=false — Skip this container during auto-updates and batch updates
|
||||
* - dockhand.hidden=true — Hide this container from the Dockhand UI
|
||||
* - dockhand.notify=false — Suppress notifications for this container's events
|
||||
* - dockhand.url=<url> — Custom clickable URL displayed alongside container ports
|
||||
* - dockhand.port.<hostPort>.url=<url> — Override the click URL for a specific published port
|
||||
*
|
||||
* All label values are case-insensitive and accept: true/yes/1 and false/no/0.
|
||||
* The opt-out model means labels override DB settings (label wins).
|
||||
@@ -15,6 +17,7 @@ export const DOCKHAND_LABELS = {
|
||||
UPDATE: 'dockhand.update',
|
||||
HIDDEN: 'dockhand.hidden',
|
||||
NOTIFY: 'dockhand.notify',
|
||||
URL: 'dockhand.url',
|
||||
} as const;
|
||||
|
||||
const TRUTHY_VALUES = new Set(['true', 'yes', '1']);
|
||||
@@ -72,6 +75,15 @@ export function isNotifyDisabledByLabel(labels: Record<string, string> | undefin
|
||||
return value === false; // explicitly disabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom URL from dockhand.url label.
|
||||
* Returns the URL string if set, or undefined.
|
||||
*/
|
||||
export function getCustomUrl(labels: Record<string, string> | undefined | null): string | undefined {
|
||||
const value = getLabel(labels, DOCKHAND_LABELS.URL);
|
||||
return value?.trim() || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all Dockhand label states from a container's labels.
|
||||
* Useful for including in API responses so the frontend knows about label overrides.
|
||||
@@ -80,10 +92,12 @@ export function getDockhandLabels(labels: Record<string, string> | undefined | n
|
||||
updateDisabled: boolean;
|
||||
hidden: boolean;
|
||||
notifyDisabled: boolean;
|
||||
customUrl?: string;
|
||||
} {
|
||||
return {
|
||||
updateDisabled: isUpdateDisabledByLabel(labels),
|
||||
hidden: isHiddenByLabel(labels),
|
||||
notifyDisabled: isNotifyDisabledByLabel(labels),
|
||||
customUrl: getCustomUrl(labels),
|
||||
};
|
||||
}
|
||||
@@ -1046,6 +1046,7 @@ export interface ContainerInfo {
|
||||
networks: { [networkName: string]: { ipAddress: string } };
|
||||
health?: string;
|
||||
restartCount: number;
|
||||
exitCode?: number;
|
||||
mounts: Array<{ type: string; source: string; destination: string; mode: string; rw: boolean }>;
|
||||
labels: { [key: string]: string };
|
||||
command: string;
|
||||
@@ -1081,6 +1082,39 @@ export async function listContainers(all = true, envId?: number | null): Promise
|
||||
})
|
||||
);
|
||||
|
||||
// Fetch health status via inspect for containers with healthchecks where Status
|
||||
// string doesn't include health info (Podman compat API omits it from the list endpoint).
|
||||
// Skip entirely if any container already has health in Status (Docker includes it natively).
|
||||
const healthStatuses = new Map<string, string>();
|
||||
const healthRegex = /\((healthy|unhealthy|health:\s*starting)\)/i;
|
||||
const anyHasHealthInStatus = containers.some(c => healthRegex.test(c.Status || ''));
|
||||
if (!anyHasHealthInStatus) {
|
||||
// Single API call to find which containers have healthchecks configured
|
||||
try {
|
||||
const healthFiltered = await dockerJsonRequest<any[]>(
|
||||
'/containers/json?filters=' + encodeURIComponent(JSON.stringify({ health: ['healthy', 'unhealthy', 'starting'] })),
|
||||
{}, envId
|
||||
);
|
||||
const healthIds = new Set((healthFiltered || []).map((c: any) => c.Id));
|
||||
// Only inspect those specific containers (typically very few)
|
||||
if (healthIds.size > 0) {
|
||||
await Promise.all(
|
||||
[...healthIds].map(async (id) => {
|
||||
try {
|
||||
const inspect = await inspectContainer(id, envId);
|
||||
const h = inspect.State?.Health?.Status;
|
||||
if (h) healthStatuses.set(id, h);
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Ignore - health filter may not be supported
|
||||
}
|
||||
}
|
||||
|
||||
return containers.map((container) => {
|
||||
// Extract network info with IP addresses
|
||||
const networks: { [networkName: string]: { ipAddress: string } } = {};
|
||||
@@ -1103,14 +1137,25 @@ export async function listContainers(all = true, envId?: number | null): Promise
|
||||
|
||||
// Extract health status from Status string
|
||||
// Docker formats: "(healthy)", "(unhealthy)", "(health: starting)"
|
||||
// Podman compat API omits health from Status string, so fall back to inspect data
|
||||
let health: string | undefined;
|
||||
const healthMatch = container.Status?.match(/\((healthy|unhealthy|health:\s*starting)\)/i);
|
||||
if (healthMatch) {
|
||||
const matched = healthMatch[1].toLowerCase();
|
||||
// Normalize "health: starting" to just "starting"
|
||||
health = matched.includes('starting') ? 'starting' : matched;
|
||||
} else {
|
||||
const inspectHealth = healthStatuses.get(container.Id);
|
||||
if (inspectHealth) {
|
||||
health = inspectHealth === 'health: starting' ? 'starting' : inspectHealth;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse exit code from Status string (e.g., "Exited (0) 5 minutes ago")
|
||||
const exitCode = container.State === 'exited'
|
||||
? parseInt(container.Status?.match(/\((\d+)\)/)?.[1] ?? '-1')
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: container.Id,
|
||||
name: container.Names[0]?.replace(/^\//, '') || 'unnamed',
|
||||
@@ -1122,6 +1167,7 @@ export async function listContainers(all = true, envId?: number | null): Promise
|
||||
networks,
|
||||
health,
|
||||
restartCount: restartCounts.get(container.Id) || 0,
|
||||
exitCode,
|
||||
mounts,
|
||||
labels: container.Labels || {},
|
||||
command: container.Command || '',
|
||||
|
||||
@@ -109,6 +109,10 @@ if (!existsSync(GIT_REPOS_DIR)) {
|
||||
mkdirSync(GIT_REPOS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
export function getGitReposDir(): string {
|
||||
return GIT_REPOS_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redact all env var values for safe logging. Only key names are preserved.
|
||||
*/
|
||||
|
||||
@@ -204,7 +204,7 @@ export async function runEnvUpdateCheckJob(
|
||||
.map(u => {
|
||||
const currentShort = u.currentDigest?.substring(0, 12) || 'unknown';
|
||||
const newShort = u.newDigest?.substring(0, 12) || 'unknown';
|
||||
return `- ${u.containerName} (${u.imageName})\n ${currentShort}... -> ${newShort}...`;
|
||||
return `- ${u.containerName} (${u.imageName}): ${currentShort} → ${newShort}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
|
||||
@@ -77,7 +77,9 @@ export interface ContainerDetail {
|
||||
networks: Array<{ name: string; ipAddress: string }>;
|
||||
volumeCount: number;
|
||||
restartCount: number;
|
||||
exitCode?: number;
|
||||
created: number;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1545,6 +1547,12 @@ export async function listComposeStacks(envId?: number | null): Promise<ComposeS
|
||||
const result: ComposeStackInfo[] = Array.from(stacks.entries()).map(([name, containerIds]) => {
|
||||
const stackContainers = containers.filter((c) => containerIds.has(c.id));
|
||||
const runningCount = stackContainers.filter((c) => c.state === 'running').length;
|
||||
// Containers that exited with code 0 are "completed" (e.g., init/migration containers)
|
||||
// and should not count against stack health
|
||||
const completedCount = stackContainers.filter((c) =>
|
||||
c.state === 'exited' && c.exitCode === 0
|
||||
).length;
|
||||
const activeTotal = stackContainers.length - completedCount;
|
||||
|
||||
const containerDetails: ContainerDetail[] = stackContainers
|
||||
.map((c) => {
|
||||
@@ -1580,7 +1588,9 @@ export async function listComposeStacks(envId?: number | null): Promise<ComposeS
|
||||
networks,
|
||||
volumeCount,
|
||||
restartCount: c.restartCount || 0,
|
||||
created: c.created
|
||||
exitCode: c.exitCode,
|
||||
created: c.created,
|
||||
labels: c.labels || {}
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.service.localeCompare(b.service));
|
||||
@@ -1590,11 +1600,13 @@ export async function listComposeStacks(envId?: number | null): Promise<ComposeS
|
||||
containers: Array.from(containerIds),
|
||||
containerDetails,
|
||||
status:
|
||||
runningCount === stackContainers.length
|
||||
? 'running'
|
||||
: runningCount === 0
|
||||
? 'stopped'
|
||||
: 'partial'
|
||||
activeTotal === 0
|
||||
? 'stopped'
|
||||
: runningCount >= activeTotal
|
||||
? 'running'
|
||||
: runningCount === 0
|
||||
? 'stopped'
|
||||
: 'partial'
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user