mirror of
https://github.com/Finsys/dockhand.git
synced 2026-06-18 03:20:43 +03:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 988e65bd5b |
+16
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dockhand",
|
||||
"private": true,
|
||||
"version": "1.0.16",
|
||||
"version": "1.0.17",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bunx --bun vite dev",
|
||||
@@ -31,6 +31,21 @@
|
||||
"test:files": "bun test tests/container-files.test.ts",
|
||||
"test:license": "bun test tests/license.test.ts",
|
||||
"test:activity": "bun test tests/activity-dashboard.test.ts",
|
||||
"test:health": "bun test tests/health-system.test.ts",
|
||||
"test:containers:advanced": "bun test tests/container-advanced.test.ts",
|
||||
"test:networks:advanced": "bun test tests/network-advanced.test.ts",
|
||||
"test:volumes:advanced": "bun test tests/volume-advanced.test.ts",
|
||||
"test:prune": "bun test tests/prune-operations.test.ts",
|
||||
"test:schedules": "bun test tests/schedule-management.test.ts",
|
||||
"test:preferences": "bun test tests/settings-preferences.test.ts",
|
||||
"test:stacks:advanced": "bun test tests/stack-advanced.test.ts",
|
||||
"test:system": "bun test tests/system-info.test.ts",
|
||||
"test:auth": "bun test tests/auth-settings.test.ts",
|
||||
"test:config-sets": "bun test tests/config-sets.test.ts",
|
||||
"test:registries": "bun test tests/registries.test.ts",
|
||||
"test:activity:advanced": "bun test tests/activity-advanced.test.ts",
|
||||
"test:env-settings": "bun test tests/environment-settings.test.ts",
|
||||
"test:git-creds": "bun test tests/git-credentials.test.ts",
|
||||
"test:all": "bun test tests/",
|
||||
"test:quick": "bun test tests/api-smoke.test.ts tests/notifications.test.ts",
|
||||
"test:integration": "bun test tests/api-smoke.test.ts tests/crud-operations.test.ts tests/scheduling.test.ts tests/hawser-connection.test.ts",
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
[
|
||||
{
|
||||
"version": "1.0.17",
|
||||
"date": "2026-02-09",
|
||||
"comingSoon": false,
|
||||
"changes": [
|
||||
{ "type": "fix", "text": "Fix scanner failure on rootless Docker" },
|
||||
{ "type": "fix", "text": "Increase Hawser compose operation timeout" },
|
||||
{ "type": "fix", "text": "Fix regression in stack container updates" }
|
||||
],
|
||||
"imageTag": "fnsys/dockhand:v1.0.17"
|
||||
},
|
||||
{
|
||||
"version": "1.0.16",
|
||||
"date": "2026-02-09",
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/codemirror/autocomplete"
|
||||
},
|
||||
{
|
||||
"name": "@codemirror/commands",
|
||||
"version": "6.10.0",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/codemirror/commands"
|
||||
},
|
||||
{
|
||||
"name": "@codemirror/commands",
|
||||
"version": "6.10.1",
|
||||
@@ -547,7 +553,7 @@
|
||||
},
|
||||
{
|
||||
"name": "svelte",
|
||||
"version": "5.46.4",
|
||||
"version": "5.47.1",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/sveltejs/svelte"
|
||||
},
|
||||
|
||||
@@ -1338,6 +1338,195 @@ export async function createContainer(options: CreateContainerOptions, envId?: n
|
||||
return { id: result.Id, start: () => startContainer(result.Id, envId) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreate a container using full Config/HostConfig passthrough from inspect data.
|
||||
* Passes Config and HostConfig directly from inspect to create, only changing
|
||||
* the image. No field mapping or stripping.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Stop container
|
||||
* 2. Rename to name-old (frees the name for the new container)
|
||||
* 3. Disconnect all networks (frees static IPs)
|
||||
* 4. Create new container with original name, one network
|
||||
* 5. Connect additional networks
|
||||
* 6. Start new container
|
||||
* 7. Remove old container
|
||||
*
|
||||
* On failure: rollback (rename old back, reconnect networks, restart old)
|
||||
*/
|
||||
export async function recreateContainerFromInspect(
|
||||
inspectData: any,
|
||||
newImage: string,
|
||||
envId?: number | null,
|
||||
log?: (msg: string) => void
|
||||
): Promise<{ Id: string }> {
|
||||
const config = inspectData.Config || {};
|
||||
const hostConfig = inspectData.HostConfig || {};
|
||||
const networks: Record<string, any> = inspectData.NetworkSettings?.Networks || {};
|
||||
const name = inspectData.Name?.replace(/^\//, '') || '';
|
||||
const oldContainerId = inspectData.Id;
|
||||
const wasRunning = inspectData.State?.Running;
|
||||
|
||||
// 1. Stop the container
|
||||
if (wasRunning) {
|
||||
log?.('Stopping container...');
|
||||
await stopContainer(oldContainerId, envId);
|
||||
}
|
||||
|
||||
// 2. Rename old container to free the name
|
||||
log?.('Renaming old container...');
|
||||
await dockerFetch(
|
||||
`/containers/${oldContainerId}/rename?name=${encodeURIComponent(name + '-old')}`,
|
||||
{ method: 'POST' },
|
||||
envId
|
||||
).then(r => { if (!r.ok) throw new Error('Failed to rename old container'); });
|
||||
|
||||
// 3. Disconnect all networks from old container (frees static IPs)
|
||||
// Capture the first network for use during container creation
|
||||
let initialNetworkName: string | null = null;
|
||||
let initialNetworkConfig: any = null;
|
||||
|
||||
for (const [netName, netConfig] of Object.entries(networks)) {
|
||||
const networkId = (netConfig as any).NetworkID;
|
||||
if (networkId) {
|
||||
try {
|
||||
await disconnectContainerFromNetwork(networkId, oldContainerId, true, envId);
|
||||
} catch {
|
||||
// Best effort - network may already be disconnected
|
||||
}
|
||||
}
|
||||
|
||||
// Use first network for creation
|
||||
if (!initialNetworkName) {
|
||||
initialNetworkName = netName;
|
||||
initialNetworkConfig = netConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// Rollback helper: restore old container on failure
|
||||
const rollback = async () => {
|
||||
try {
|
||||
log?.('Rolling back: restoring old container...');
|
||||
// Rename back
|
||||
await dockerFetch(
|
||||
`/containers/${oldContainerId}/rename?name=${encodeURIComponent(name)}`,
|
||||
{ method: 'POST' },
|
||||
envId
|
||||
).catch(() => {});
|
||||
|
||||
// Reconnect networks using full EndpointSettings from inspect
|
||||
for (const [, netConfig] of Object.entries(networks)) {
|
||||
const nc = netConfig as any;
|
||||
if (nc.NetworkID) {
|
||||
await connectContainerToNetworkRaw(nc.NetworkID, oldContainerId, nc, envId).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Restart
|
||||
if (wasRunning) {
|
||||
await startContainer(oldContainerId, envId).catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
log?.('Rollback failed');
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Build create config - pass Config and HostConfig directly from inspect
|
||||
const createConfig: any = {
|
||||
...config,
|
||||
Image: newImage,
|
||||
HostConfig: hostConfig
|
||||
};
|
||||
|
||||
// Preserve anonymous volumes from Mounts not in HostConfig.Binds
|
||||
const existingBinds = new Set((hostConfig.Binds || []).map((b: string) => {
|
||||
const parts = b.split(':');
|
||||
return parts.length >= 2 ? parts[1] : parts[0];
|
||||
}));
|
||||
const mounts = inspectData.Mounts || [];
|
||||
const additionalBinds: string[] = [];
|
||||
for (const mount of mounts) {
|
||||
if (mount.Type === 'volume' && mount.Name && mount.Destination) {
|
||||
if (!existingBinds.has(mount.Destination)) {
|
||||
additionalBinds.push(`${mount.Name}:${mount.Destination}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (additionalBinds.length > 0) {
|
||||
createConfig.HostConfig = {
|
||||
...hostConfig,
|
||||
Binds: [...(hostConfig.Binds || []), ...additionalBinds]
|
||||
};
|
||||
}
|
||||
|
||||
// Docker can only connect to one network at creation. Pass the first network
|
||||
// from the old container's settings to avoid getting a random bridge IP.
|
||||
// Clear MacAddress for Docker API < 1.44 compatibility.
|
||||
if (initialNetworkName && initialNetworkConfig) {
|
||||
const endpointConfig = { ...initialNetworkConfig };
|
||||
delete endpointConfig.MacAddress;
|
||||
createConfig.NetworkingConfig = {
|
||||
EndpointsConfig: {
|
||||
[initialNetworkName]: endpointConfig
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Create new container
|
||||
log?.('Creating new container...');
|
||||
let newContainerId: string;
|
||||
try {
|
||||
const result = await dockerJsonRequest<{ Id: string }>(
|
||||
`/containers/create?name=${encodeURIComponent(name)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(createConfig)
|
||||
},
|
||||
envId
|
||||
);
|
||||
newContainerId = result.Id;
|
||||
} catch (createError: any) {
|
||||
log?.(`Create failed: ${createError.message}`);
|
||||
await rollback();
|
||||
throw createError;
|
||||
}
|
||||
|
||||
// 6. Connect additional networks using full EndpointSettings from inspect
|
||||
for (const [netName, netConfig] of Object.entries(networks)) {
|
||||
if (netName === initialNetworkName) continue; // Already connected at creation
|
||||
|
||||
const nc = netConfig as any;
|
||||
if (nc.NetworkID) {
|
||||
try {
|
||||
await connectContainerToNetworkRaw(nc.NetworkID, newContainerId, nc, envId);
|
||||
} catch (netError: any) {
|
||||
log?.(`Warning: Failed to connect to network "${netName}": ${netError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Start new container
|
||||
if (wasRunning) {
|
||||
log?.('Starting new container...');
|
||||
try {
|
||||
await startContainer(newContainerId, envId);
|
||||
} catch (startError: any) {
|
||||
log?.(`Start failed: ${startError.message}, rolling back...`);
|
||||
// Remove failed new container
|
||||
await removeContainer(newContainerId, true, envId).catch(() => {});
|
||||
await rollback();
|
||||
throw startError;
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Remove old container (best effort)
|
||||
log?.('Removing old container...');
|
||||
await removeContainer(oldContainerId, true, envId).catch(() => {});
|
||||
|
||||
log?.('Container recreated successfully');
|
||||
return { Id: newContainerId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all container options from Docker inspect data.
|
||||
* This preserves ALL container settings for recreation.
|
||||
@@ -2767,6 +2956,37 @@ export async function connectContainerToNetwork(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a container to a network using a raw EndpointSettings object from inspect data.
|
||||
* Passes the full EndpointSettings as-is, preserving all fields (Links, DriverOpts,
|
||||
* IPAMConfig.LinkLocalIPs, MacAddress, etc.) without manual field extraction.
|
||||
*/
|
||||
export async function connectContainerToNetworkRaw(
|
||||
networkId: string,
|
||||
containerId: string,
|
||||
endpointSettings: any,
|
||||
envId?: number | null
|
||||
): Promise<void> {
|
||||
const body: any = {
|
||||
Container: containerId,
|
||||
EndpointConfig: endpointSettings
|
||||
};
|
||||
|
||||
const response = await dockerFetch(
|
||||
`/networks/${networkId}/connect`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
},
|
||||
envId
|
||||
);
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
throw new Error(data.message || 'Failed to connect container to network');
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnectContainerFromNetwork(
|
||||
networkId: string,
|
||||
containerId: string,
|
||||
|
||||
@@ -593,19 +593,21 @@ async function runScannerContainerCore(
|
||||
(connectionType === 'direct' && !env?.host);
|
||||
|
||||
let hostSocketPath: string;
|
||||
let containerUser: string | undefined;
|
||||
let rootlessUid: string | undefined;
|
||||
|
||||
if (isLocalSocket) {
|
||||
// Local socket environment - detect host socket path (handles rootless Docker)
|
||||
hostSocketPath = getHostDockerSocket();
|
||||
console.log(`[Scanner] Local socket scan (${connectionType || 'default'}) - detected host Docker socket: ${hostSocketPath}`);
|
||||
|
||||
// For user-specific Docker sockets, run scanner as that user
|
||||
// e.g., /run/user/1000/docker.sock -> run as UID 1000
|
||||
// For user-specific Docker sockets (rootless Docker), detect UID for cache ownership
|
||||
// but do NOT set container user — in rootless Docker, root inside the container
|
||||
// maps to the socket-owning UID on the host via user namespace remapping
|
||||
const uid = extractUidFromSocketPath(hostSocketPath);
|
||||
if (uid) {
|
||||
containerUser = uid;
|
||||
console.log(`[Scanner] Rootless Docker detected (UID ${containerUser})`);
|
||||
rootlessUid = uid;
|
||||
console.log(`[Scanner] Rootless Docker detected (UID ${rootlessUid})`);
|
||||
console.log(`[Scanner] Scanner will run as root inside container (maps to UID ${rootlessUid} on host via user namespace)`);
|
||||
}
|
||||
} else {
|
||||
// Remote environment (direct with host/hawser-standard/hawser-edge)
|
||||
@@ -620,9 +622,9 @@ async function runScannerContainerCore(
|
||||
let cacheBind: string;
|
||||
const volumeName = scannerType === 'grype' ? GRYPE_VOLUME_NAME : TRIVY_VOLUME_NAME;
|
||||
|
||||
if (containerUser) {
|
||||
if (rootlessUid) {
|
||||
// Rootless Docker: use bind mount from data directory with correct ownership
|
||||
const hostCachePath = await ensureScannerCacheDir(scannerType, containerUser);
|
||||
const hostCachePath = await ensureScannerCacheDir(scannerType, rootlessUid);
|
||||
cacheBind = `${hostCachePath}:${basePath}`;
|
||||
console.log(`[Scanner] Rootless mode - using bind mount: ${cacheBind}`);
|
||||
} else {
|
||||
@@ -646,10 +648,6 @@ async function runScannerContainerCore(
|
||||
|
||||
console.log(`[Scanner] Running ${scannerType} with cache mounted at ${basePath}`);
|
||||
console.log(`[Scanner] Container command: ${cmd.join(' ')}`);
|
||||
if (containerUser) {
|
||||
console.log(`[Scanner] Running scanner container as UID ${containerUser} to match socket owner`);
|
||||
}
|
||||
|
||||
// Run the scanner container with a 10-minute timeout to prevent indefinite hangs
|
||||
const output = await runContainerWithStreaming({
|
||||
image: scannerImage,
|
||||
@@ -657,7 +655,6 @@ async function runScannerContainerCore(
|
||||
binds,
|
||||
env: envVars,
|
||||
name: `dockhand-${scannerType}-${Date.now()}`,
|
||||
user: containerUser,
|
||||
envId,
|
||||
timeout: 600_000, // 10 minutes
|
||||
onStderr: (data) => {
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
* Container Auto-Update Task
|
||||
*
|
||||
* Handles automatic container updates with vulnerability scanning.
|
||||
*
|
||||
* For containers that are part of a Docker Compose stack, updates use
|
||||
* `docker compose up -d` to preserve ALL configuration from the compose file
|
||||
* (network aliases, static IPs, health checks, resource limits, etc.).
|
||||
*
|
||||
* For standalone containers, updates use container recreation with comprehensive
|
||||
* settings preservation.
|
||||
* Uses direct Docker API recreation with full Config/HostConfig passthrough
|
||||
* from inspect data. No compose commands for
|
||||
* individual container updates, no manual field mapping, zero settings loss.
|
||||
*/
|
||||
|
||||
import type { ScheduleTrigger, VulnerabilityCriteria } from '../../db';
|
||||
@@ -26,7 +22,6 @@ import {
|
||||
pullImage,
|
||||
listContainers,
|
||||
inspectContainer,
|
||||
createContainer,
|
||||
stopContainer,
|
||||
startContainer,
|
||||
removeContainer,
|
||||
@@ -37,12 +32,12 @@ import {
|
||||
removeTempImage,
|
||||
tagImage,
|
||||
connectContainerToNetwork,
|
||||
extractContainerOptions
|
||||
disconnectContainerFromNetwork,
|
||||
recreateContainerFromInspect
|
||||
} from '../../docker';
|
||||
import { getScannerSettings, scanImage, type ScanResult, type VulnerabilitySeverity } from '../../scanner';
|
||||
import { sendEventNotification } from '../../notifications';
|
||||
import { parseImageNameAndTag, shouldBlockUpdate, combineScanSummaries, isSystemContainer } from './update-utils';
|
||||
import { getStackComposeFile, updateStackService, pullStackService } from '../../stacks';
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
@@ -392,19 +387,6 @@ export async function runContainerUpdate(
|
||||
log(`Container is using image: ${imageNameFromConfig}`);
|
||||
log(`Current image ID: ${currentImageId?.substring(0, 19)}`);
|
||||
|
||||
// Detect if container is part of a Docker Compose stack
|
||||
const containerLabels = inspectData.Config?.Labels || {};
|
||||
const composeProject = containerLabels['com.docker.compose.project'];
|
||||
const composeService = containerLabels['com.docker.compose.service'];
|
||||
const composeConfigFiles = containerLabels['com.docker.compose.project.config_files'];
|
||||
const isStackContainer = !!(composeProject && composeService);
|
||||
|
||||
if (isStackContainer) {
|
||||
log(`Container is part of compose stack: ${composeProject} (service: ${composeService}, configFiles: ${composeConfigFiles || 'none'})`);
|
||||
} else {
|
||||
log(`Container is standalone (not part of a compose stack)`);
|
||||
}
|
||||
|
||||
// Get scanner and schedule settings early to determine scan strategy
|
||||
const [scannerSettings, updateSetting] = await Promise.all([
|
||||
getScannerSettings(envId),
|
||||
@@ -458,150 +440,7 @@ export async function runContainerUpdate(
|
||||
const newDigest = registryCheck.registryDigest;
|
||||
|
||||
// =============================================================================
|
||||
// STACK CONTAINER: Compose-native flow
|
||||
// =============================================================================
|
||||
// 1. Check if we have the compose file
|
||||
// 2. docker compose pull <service>
|
||||
// 3. Scan if enabled, block if needed
|
||||
// 4. docker compose up -d <service>
|
||||
// =============================================================================
|
||||
|
||||
if (isStackContainer) {
|
||||
const composeResult = await getStackComposeFile(composeProject, envId, composeConfigFiles);
|
||||
log(`Compose lookup result: success=${composeResult.success}, composePath=${composeResult.composePath || 'none'}`);
|
||||
|
||||
if (composeResult.success) {
|
||||
log(`Using compose-native update for stack: ${composeProject}`);
|
||||
|
||||
try {
|
||||
// Pull via docker compose
|
||||
log(`Running: docker compose pull ${composeService}`);
|
||||
const pullResult = await pullStackService(composeProject, composeService, envId, composeConfigFiles);
|
||||
if (!pullResult.success) {
|
||||
throw new Error(pullResult.error || 'docker compose pull failed');
|
||||
}
|
||||
log(`Compose pull completed`);
|
||||
|
||||
// Get new image ID
|
||||
const newImageId = await getImageIdByTag(imageNameFromConfig, envId);
|
||||
if (!newImageId) {
|
||||
throw new Error('Failed to get new image ID after compose pull');
|
||||
}
|
||||
log(`New image ID: ${newImageId.substring(0, 19)}`);
|
||||
|
||||
// Scan if enabled
|
||||
let scanOutcome: ScanOutcome = { blocked: false };
|
||||
if (shouldScan) {
|
||||
try {
|
||||
scanOutcome = await scanAndCheckBlock({
|
||||
newImageId,
|
||||
currentImageId,
|
||||
envId,
|
||||
vulnerabilityCriteria,
|
||||
log
|
||||
});
|
||||
|
||||
if (scanOutcome.blocked) {
|
||||
// Restore old tag so container keeps using safe image
|
||||
log(`Restoring original tag to safe image...`);
|
||||
const [oldRepo, oldTag] = parseImageNameAndTag(imageNameFromConfig);
|
||||
await tagImage(currentImageId, oldRepo, oldTag, envId);
|
||||
|
||||
await updateScheduleExecution(execution.id, {
|
||||
status: 'skipped',
|
||||
completedAt: new Date().toISOString(),
|
||||
duration: Date.now() - startTime,
|
||||
details: buildBlockedDetails(
|
||||
containerName,
|
||||
vulnerabilityCriteria,
|
||||
scanOutcome.reason!,
|
||||
scanOutcome.scanResults!,
|
||||
scanOutcome.scanSummary!
|
||||
)
|
||||
});
|
||||
|
||||
await sendEventNotification('auto_update_blocked', {
|
||||
title: 'Auto-update blocked',
|
||||
message: `Container "${containerName}" update blocked: ${scanOutcome.reason}`,
|
||||
type: 'warning'
|
||||
}, envId);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (scanError: any) {
|
||||
log(`Scan failed: ${scanError.message}`);
|
||||
log(`Restoring original tag...`);
|
||||
const [oldRepo, oldTag] = parseImageNameAndTag(imageNameFromConfig);
|
||||
await tagImage(currentImageId, oldRepo, oldTag, envId);
|
||||
|
||||
await updateScheduleExecution(execution.id, {
|
||||
status: 'failed',
|
||||
completedAt: new Date().toISOString(),
|
||||
duration: Date.now() - startTime,
|
||||
errorMessage: `Vulnerability scan failed: ${scanError.message}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply update via docker compose up
|
||||
log(`Running: docker compose up -d ${composeService}`);
|
||||
const upResult = await updateStackService(composeProject, composeService, envId, composeConfigFiles);
|
||||
if (!upResult.success) {
|
||||
throw new Error(upResult.error || 'docker compose up failed');
|
||||
}
|
||||
|
||||
// Success
|
||||
await updateAutoUpdateLastUpdated(containerName, envId);
|
||||
log(`Successfully updated container: ${containerName}`);
|
||||
|
||||
await updateScheduleExecution(execution.id, {
|
||||
status: 'success',
|
||||
completedAt: new Date().toISOString(),
|
||||
duration: Date.now() - startTime,
|
||||
details: buildSuccessDetails(
|
||||
containerName,
|
||||
newDigest,
|
||||
vulnerabilityCriteria,
|
||||
scanOutcome.scanResults,
|
||||
scanOutcome.scanSummary
|
||||
)
|
||||
});
|
||||
|
||||
await sendEventNotification('auto_update_success', {
|
||||
title: 'Container auto-updated',
|
||||
message: `Container "${containerName}" was updated to a new image version`,
|
||||
type: 'success'
|
||||
}, envId);
|
||||
|
||||
return;
|
||||
|
||||
} catch (composeError: any) {
|
||||
log(`Compose update failed: ${composeError.message}`);
|
||||
await updateScheduleExecution(execution.id, {
|
||||
status: 'failed',
|
||||
completedAt: new Date().toISOString(),
|
||||
duration: Date.now() - startTime,
|
||||
errorMessage: `Stack update failed: ${composeError.message}`
|
||||
});
|
||||
|
||||
await sendEventNotification('auto_update_failed', {
|
||||
title: 'Auto-update failed',
|
||||
message: `Container "${containerName}" auto-update failed: ${composeError.message}`,
|
||||
type: 'error'
|
||||
}, envId);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No compose file found - fall through to standalone flow
|
||||
log(`No compose file found for stack "${composeProject}" - using standalone update`);
|
||||
log(`TIP: Import this stack into Dockhand for compose-native updates`);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STANDALONE CONTAINER: Temp-tag protection flow
|
||||
// PULL & SCAN: Temp-tag protection flow
|
||||
// =============================================================================
|
||||
// 1. Pull new image (overwrites tag)
|
||||
// 2. Restore original tag to OLD image (safety)
|
||||
@@ -726,16 +565,10 @@ export async function runContainerUpdate(
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RECREATE CONTAINER
|
||||
// RECREATE CONTAINER (full config passthrough from inspect data)
|
||||
// =============================================================================
|
||||
|
||||
if (isStackContainer) {
|
||||
log(`External stack - recreating container directly`);
|
||||
log(`WARNING: Some compose settings may not be preserved`);
|
||||
} else {
|
||||
log(`Recreating standalone container...`);
|
||||
}
|
||||
|
||||
log(`Recreating container with full config passthrough...`);
|
||||
const success = await recreateContainer(containerName, envId, log);
|
||||
|
||||
if (success) {
|
||||
@@ -786,8 +619,9 @@ export async function runContainerUpdate(
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Recreate a standalone container with comprehensive settings preservation.
|
||||
* Extracts and preserves 50+ container settings from the original container.
|
||||
* Recreate a container using full Config/HostConfig passthrough from inspect data.
|
||||
* Passes inspect data directly to Docker API create, only changing the image.
|
||||
* No manual field mapping — zero settings loss.
|
||||
*/
|
||||
export async function recreateContainer(
|
||||
containerName: string,
|
||||
@@ -804,104 +638,11 @@ export async function recreateContainer(
|
||||
}
|
||||
|
||||
const inspectData = await inspectContainer(container.id, envId) as any;
|
||||
const wasRunning = inspectData.State.Running;
|
||||
const hostConfig = inspectData.HostConfig;
|
||||
const config = inspectData.Config;
|
||||
const imageName = inspectData.Config?.Image;
|
||||
|
||||
log?.(`Recreating container: ${containerName} (was running: ${wasRunning})`);
|
||||
log?.(`Preserving all container settings...`);
|
||||
log?.(`Recreating container: ${containerName} (image: ${imageName})`);
|
||||
|
||||
if (wasRunning) {
|
||||
log?.('Stopping container...');
|
||||
await stopContainer(container.id, envId);
|
||||
}
|
||||
|
||||
log?.('Removing old container...');
|
||||
await removeContainer(container.id, true, envId);
|
||||
|
||||
const containerOptions = extractContainerOptions(inspectData);
|
||||
|
||||
// Handle additional networks
|
||||
const networkSettings = inspectData.NetworkSettings?.Networks || {};
|
||||
const primaryNetwork = hostConfig.NetworkMode || 'bridge';
|
||||
const shortContainerId = container.id.substring(0, 12);
|
||||
const composeProject = config.Labels?.['com.docker.compose.project'];
|
||||
const composeService = config.Labels?.['com.docker.compose.service'];
|
||||
|
||||
interface NetworkInfo {
|
||||
name: string;
|
||||
aliases: string[];
|
||||
ipv4Address: string | undefined;
|
||||
ipv6Address: string | undefined;
|
||||
gwPriority: number | undefined;
|
||||
}
|
||||
|
||||
const additionalNetworks: NetworkInfo[] = [];
|
||||
|
||||
for (const [netName, netConfig] of Object.entries(networkSettings)) {
|
||||
const netConf = netConfig as any;
|
||||
const isPrimary = netName === primaryNetwork ||
|
||||
(primaryNetwork === 'bridge' && (netName === 'bridge' || netName === 'default'));
|
||||
|
||||
if (isPrimary) {
|
||||
if (containerOptions.networkAliases?.length) {
|
||||
log?.(`Primary network aliases: ${containerOptions.networkAliases.join(', ')}`);
|
||||
}
|
||||
if (containerOptions.networkIpv4Address) {
|
||||
log?.(`Primary network static IPv4: ${containerOptions.networkIpv4Address}`);
|
||||
}
|
||||
} else {
|
||||
const secondaryAliases = ((netConf.Aliases?.length > 0 ? netConf.Aliases : netConf.DNSNames) || [])
|
||||
.filter((a: string) => a !== container.id && a !== shortContainerId);
|
||||
|
||||
if (composeProject && composeService) {
|
||||
if (!secondaryAliases.includes(composeService)) {
|
||||
secondaryAliases.push(composeService);
|
||||
}
|
||||
const projectService = `${composeProject}-${composeService}`;
|
||||
if (!secondaryAliases.includes(projectService)) {
|
||||
secondaryAliases.push(projectService);
|
||||
}
|
||||
}
|
||||
|
||||
additionalNetworks.push({
|
||||
name: netName,
|
||||
aliases: secondaryAliases,
|
||||
ipv4Address: netConf.IPAMConfig?.IPv4Address || undefined,
|
||||
ipv6Address: netConf.IPAMConfig?.IPv6Address || undefined,
|
||||
gwPriority: netConf.GwPriority !== undefined && netConf.GwPriority !== 0
|
||||
? netConf.GwPriority : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalNetworks.length > 0) {
|
||||
log?.(`Will reconnect to ${additionalNetworks.length} additional network(s)`);
|
||||
}
|
||||
|
||||
log?.('Creating new container...');
|
||||
const newContainer = await createContainer(containerOptions, envId);
|
||||
|
||||
for (const netInfo of additionalNetworks) {
|
||||
try {
|
||||
await connectContainerToNetwork(netInfo.name, newContainer.id, envId, {
|
||||
aliases: netInfo.aliases.length > 0 ? netInfo.aliases : undefined,
|
||||
ipv4Address: netInfo.ipv4Address,
|
||||
ipv6Address: netInfo.ipv6Address,
|
||||
gwPriority: netInfo.gwPriority
|
||||
});
|
||||
log?.(` Connected to: ${netInfo.name}`);
|
||||
} catch (netError: any) {
|
||||
log?.(` Warning: Failed to connect to "${netInfo.name}": ${netError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (wasRunning) {
|
||||
log?.('Starting new container...');
|
||||
await newContainer.start();
|
||||
}
|
||||
|
||||
log?.('Container recreated successfully');
|
||||
await recreateContainerFromInspect(inspectData, imageName, envId, log);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
log?.(`Failed to recreate container: ${error.message}`);
|
||||
@@ -909,42 +650,3 @@ export async function recreateContainer(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a container that is part of a Docker Compose stack.
|
||||
* Uses `docker compose up -d <service>` which preserves all compose configuration.
|
||||
*
|
||||
* @returns true if update succeeded, false if stack not found (use fallback)
|
||||
*/
|
||||
export async function updateStackContainer(
|
||||
stackName: string,
|
||||
serviceName: string,
|
||||
envId?: number,
|
||||
log?: (msg: string) => void,
|
||||
composeConfigPath?: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
log?.(`Looking up stack: ${stackName}`);
|
||||
|
||||
const composeResult = await getStackComposeFile(stackName, envId, composeConfigPath);
|
||||
|
||||
if (!composeResult.success || !composeResult.content) {
|
||||
log?.(`No compose file found for stack "${stackName}"`);
|
||||
log?.(`TIP: Import the stack in Dockhand for compose-native updates`);
|
||||
return false;
|
||||
}
|
||||
|
||||
log?.(`Running: docker compose up -d ${serviceName}`);
|
||||
const result = await updateStackService(stackName, serviceName, envId, composeConfigPath);
|
||||
|
||||
if (result.success) {
|
||||
log?.(`Service ${serviceName} updated via docker compose`);
|
||||
return true;
|
||||
} else {
|
||||
log?.(`docker compose up failed: ${result.error || 'Unknown error'}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error: any) {
|
||||
log?.(`Stack update error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,13 +26,12 @@ import {
|
||||
isDigestBasedImage,
|
||||
getImageIdByTag,
|
||||
removeTempImage,
|
||||
tagImage
|
||||
tagImage,
|
||||
} from '../../docker';
|
||||
import { sendEventNotification } from '../../notifications';
|
||||
import { getScannerSettings, scanImage, type VulnerabilitySeverity } from '../../scanner';
|
||||
import { parseImageNameAndTag, shouldBlockUpdate, combineScanSummaries, isSystemContainer } from './update-utils';
|
||||
import { recreateContainer, updateStackContainer } from './container-update';
|
||||
import { pullStackService } from '../../stacks';
|
||||
import { recreateContainer } from './container-update';
|
||||
|
||||
interface UpdateInfo {
|
||||
containerId: string;
|
||||
@@ -236,34 +235,14 @@ export async function runEnvUpdateCheckJob(
|
||||
try {
|
||||
await log(`\nUpdating: ${update.containerName}`);
|
||||
|
||||
// Get full container config
|
||||
const inspectData = await inspectContainer(update.containerId, environmentId) as any;
|
||||
const containerConfig = inspectData.Config;
|
||||
|
||||
// Detect stack membership early (needed for both pull and recreate)
|
||||
const containerLabels = containerConfig.Labels || {};
|
||||
const composeProject = containerLabels['com.docker.compose.project'];
|
||||
const composeService = containerLabels['com.docker.compose.service'];
|
||||
const composeConfigFiles = containerLabels['com.docker.compose.project.config_files'];
|
||||
const isStackContainer = !!(composeProject && composeService);
|
||||
|
||||
// SAFE-PULL FLOW
|
||||
if (shouldScan && !isDigestBasedImage(update.imageName)) {
|
||||
const tempTag = getTempImageTag(update.imageName);
|
||||
await log(` Safe-pull with temp tag: ${tempTag}`);
|
||||
|
||||
// Step 1: Pull new image
|
||||
if (isStackContainer) {
|
||||
await log(` Pulling via compose (stack: ${composeProject}, service: ${composeService})...`);
|
||||
const pullResult = await pullStackService(composeProject, composeService, environmentId, composeConfigFiles);
|
||||
if (!pullResult.success) {
|
||||
await log(` Compose pull failed, falling back to direct pull...`);
|
||||
await pullImage(update.imageName, () => {}, environmentId);
|
||||
}
|
||||
} else {
|
||||
await log(` Pulling ${update.imageName}...`);
|
||||
await pullImage(update.imageName, () => {}, environmentId);
|
||||
}
|
||||
await log(` Pulling ${update.imageName}...`);
|
||||
await pullImage(update.imageName, () => {}, environmentId);
|
||||
|
||||
// Step 2: Get new image ID
|
||||
const newImageId = await getImageIdByTag(update.imageName, environmentId);
|
||||
@@ -372,39 +351,15 @@ export async function runEnvUpdateCheckJob(
|
||||
} catch { /* ignore cleanup errors */ }
|
||||
} else {
|
||||
// Simple pull (no scanning or digest-based image)
|
||||
if (isStackContainer) {
|
||||
await log(` Pulling via compose (stack: ${composeProject}, service: ${composeService})...`);
|
||||
const pullResult = await pullStackService(composeProject, composeService, environmentId, composeConfigFiles);
|
||||
if (!pullResult.success) {
|
||||
await log(` Compose pull failed, falling back to direct pull...`);
|
||||
await pullImage(update.imageName, () => {}, environmentId);
|
||||
}
|
||||
} else {
|
||||
await log(` Pulling ${update.imageName}...`);
|
||||
await pullImage(update.imageName, () => {}, environmentId);
|
||||
}
|
||||
await log(` Pulling ${update.imageName}...`);
|
||||
await pullImage(update.imageName, () => {}, environmentId);
|
||||
}
|
||||
|
||||
// Recreate container using compose-native or full recreation
|
||||
if (isStackContainer) {
|
||||
await log(` Updating via compose (stack: ${composeProject}, service: ${composeService})`);
|
||||
const stackSuccess = await updateStackContainer(
|
||||
composeProject, composeService, environmentId,
|
||||
(msg) => { log(` ${msg}`); },
|
||||
composeConfigFiles
|
||||
);
|
||||
if (!stackSuccess) {
|
||||
await log(` Compose file not found, falling back to container recreation...`);
|
||||
const ok = await recreateContainer(update.containerName, environmentId,
|
||||
(msg) => { log(` ${msg}`); });
|
||||
if (!ok) throw new Error('Container recreation failed');
|
||||
}
|
||||
} else {
|
||||
await log(` Recreating standalone container...`);
|
||||
const ok = await recreateContainer(update.containerName, environmentId,
|
||||
(msg) => { log(` ${msg}`); });
|
||||
if (!ok) throw new Error('Container recreation failed');
|
||||
}
|
||||
// Recreate container with full config passthrough
|
||||
await log(` Recreating container...`);
|
||||
const ok = await recreateContainer(update.containerName, environmentId,
|
||||
(msg) => { log(` ${msg}`); });
|
||||
if (!ok) throw new Error('Container recreation failed');
|
||||
|
||||
await log(` Updated successfully`);
|
||||
successCount++;
|
||||
|
||||
@@ -15,8 +15,7 @@ import { auditContainer } from '$lib/server/audit';
|
||||
import { getScannerSettings, scanImage } from '$lib/server/scanner';
|
||||
import { saveVulnerabilityScan, removePendingContainerUpdate, type VulnerabilityCriteria } from '$lib/server/db';
|
||||
import { parseImageNameAndTag, shouldBlockUpdate, combineScanSummaries, isDockhandContainer } from '$lib/server/scheduler/tasks/update-utils';
|
||||
import { recreateContainer, updateStackContainer } from '$lib/server/scheduler/tasks/container-update';
|
||||
import { pullStackService } from '$lib/server/stacks';
|
||||
import { recreateContainer } from '$lib/server/scheduler/tasks/container-update';
|
||||
|
||||
export interface ScanResult {
|
||||
critical: number;
|
||||
@@ -208,13 +207,6 @@ export const POST: RequestHandler = async (event) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Detect stack membership early (needed for both pull and recreate)
|
||||
const containerLabels = config.Labels || {};
|
||||
const composeProject = containerLabels['com.docker.compose.project'];
|
||||
const composeService = containerLabels['com.docker.compose.service'];
|
||||
const composeConfigFiles = containerLabels['com.docker.compose.project.config_files'];
|
||||
const isStackContainer = !!(composeProject && composeService);
|
||||
|
||||
// Step 1: Pull latest image
|
||||
safeEnqueue({
|
||||
type: 'progress',
|
||||
@@ -227,37 +219,18 @@ export const POST: RequestHandler = async (event) => {
|
||||
});
|
||||
|
||||
try {
|
||||
if (isStackContainer) {
|
||||
const pullResult = await pullStackService(composeProject, composeService!, envIdNum, composeConfigFiles);
|
||||
if (!pullResult.success) {
|
||||
// Fallback to direct pull
|
||||
await pullImage(imageName, (data: any) => {
|
||||
if (data.status) {
|
||||
safeEnqueue({
|
||||
type: 'pull_log',
|
||||
containerId,
|
||||
containerName,
|
||||
pullStatus: data.status,
|
||||
pullId: data.id,
|
||||
pullProgress: data.progress
|
||||
});
|
||||
}
|
||||
}, envIdNum);
|
||||
await pullImage(imageName, (data: any) => {
|
||||
if (data.status) {
|
||||
safeEnqueue({
|
||||
type: 'pull_log',
|
||||
containerId,
|
||||
containerName,
|
||||
pullStatus: data.status,
|
||||
pullId: data.id,
|
||||
pullProgress: data.progress
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await pullImage(imageName, (data: any) => {
|
||||
if (data.status) {
|
||||
safeEnqueue({
|
||||
type: 'pull_log',
|
||||
containerId,
|
||||
containerName,
|
||||
pullStatus: data.status,
|
||||
pullId: data.id,
|
||||
pullProgress: data.progress
|
||||
});
|
||||
}
|
||||
}, envIdNum);
|
||||
}
|
||||
}, envIdNum);
|
||||
} catch (pullError: any) {
|
||||
safeEnqueue({
|
||||
type: 'progress',
|
||||
@@ -481,73 +454,22 @@ export const POST: RequestHandler = async (event) => {
|
||||
let updateSuccess = false;
|
||||
let newContainerId = containerId;
|
||||
|
||||
if (isStackContainer) {
|
||||
// ===================================================================
|
||||
// STACK CONTAINER: Use docker compose up -d to preserve ALL settings
|
||||
// ===================================================================
|
||||
safeEnqueue({
|
||||
type: 'progress',
|
||||
containerId,
|
||||
containerName,
|
||||
step: 'creating',
|
||||
current: i + 1,
|
||||
total: containerIds.length,
|
||||
message: `Updating stack ${composeProject} (service: ${composeService})...`
|
||||
});
|
||||
safeEnqueue({
|
||||
type: 'progress',
|
||||
containerId,
|
||||
containerName,
|
||||
step: 'creating',
|
||||
current: i + 1,
|
||||
total: containerIds.length,
|
||||
message: `Recreating ${containerName}...`
|
||||
});
|
||||
|
||||
// Try stack-based update first
|
||||
const stackSuccess = await updateStackContainer(composeProject, composeService!, envIdNum, logProgress, composeConfigFiles);
|
||||
|
||||
if (stackSuccess) {
|
||||
updateSuccess = true;
|
||||
// Find the new container ID
|
||||
const updatedContainers = await listContainers(true, envIdNum);
|
||||
const updatedContainer = updatedContainers.find(c => c.name === containerName);
|
||||
if (updatedContainer) {
|
||||
newContainerId = updatedContainer.id;
|
||||
}
|
||||
} else {
|
||||
// Fallback: Stack is external, use container recreation with full settings
|
||||
safeEnqueue({
|
||||
type: 'progress',
|
||||
containerId,
|
||||
containerName,
|
||||
step: 'creating',
|
||||
current: i + 1,
|
||||
total: containerIds.length,
|
||||
message: `Recreating ${containerName} (external stack, preserving all settings)...`
|
||||
});
|
||||
|
||||
updateSuccess = await recreateContainer(containerName, envIdNum, logProgress);
|
||||
if (updateSuccess) {
|
||||
const updatedContainers = await listContainers(true, envIdNum);
|
||||
const updatedContainer = updatedContainers.find(c => c.name === containerName);
|
||||
if (updatedContainer) {
|
||||
newContainerId = updatedContainer.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// ===================================================================
|
||||
// STANDALONE CONTAINER: Use shared recreation with ALL settings
|
||||
// ===================================================================
|
||||
safeEnqueue({
|
||||
type: 'progress',
|
||||
containerId,
|
||||
containerName,
|
||||
step: 'creating',
|
||||
current: i + 1,
|
||||
total: containerIds.length,
|
||||
message: `Recreating ${containerName} (preserving all settings)...`
|
||||
});
|
||||
|
||||
updateSuccess = await recreateContainer(containerName, envIdNum, logProgress);
|
||||
if (updateSuccess) {
|
||||
const updatedContainers = await listContainers(true, envIdNum);
|
||||
const updatedContainer = updatedContainers.find(c => c.name === containerName);
|
||||
if (updatedContainer) {
|
||||
newContainerId = updatedContainer.id;
|
||||
}
|
||||
updateSuccess = await recreateContainer(containerName, envIdNum, logProgress);
|
||||
if (updateSuccess) {
|
||||
const updatedContainers = await listContainers(true, envIdNum);
|
||||
const updatedContainer = updatedContainers.find(c => c.name === containerName);
|
||||
if (updatedContainer) {
|
||||
newContainerId = updatedContainer.id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { listContainers, pullImage, inspectContainer } from '$lib/server/docker';
|
||||
import { auditContainer } from '$lib/server/audit';
|
||||
import { recreateContainer, updateStackContainer } from '$lib/server/scheduler/tasks/container-update';
|
||||
import { recreateContainer } from '$lib/server/scheduler/tasks/container-update';
|
||||
|
||||
export interface BatchUpdateResult {
|
||||
containerId: string;
|
||||
@@ -75,47 +75,15 @@ export const POST: RequestHandler = async (event) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Detect if container is part of a Docker Compose stack
|
||||
const containerLabels = config.Labels || {};
|
||||
const composeProject = containerLabels['com.docker.compose.project'];
|
||||
const composeService = containerLabels['com.docker.compose.service'];
|
||||
const isStackContainer = !!composeProject;
|
||||
|
||||
let updateSuccess = false;
|
||||
let newContainerId = containerId;
|
||||
|
||||
if (isStackContainer) {
|
||||
// Stack container: Try docker compose up -d first
|
||||
const stackSuccess = await updateStackContainer(composeProject, composeService!, envIdNum);
|
||||
|
||||
if (stackSuccess) {
|
||||
updateSuccess = true;
|
||||
// Find the new container ID
|
||||
const updatedContainers = await listContainers(true, envIdNum);
|
||||
const updatedContainer = updatedContainers.find(c => c.name === containerName);
|
||||
if (updatedContainer) {
|
||||
newContainerId = updatedContainer.id;
|
||||
}
|
||||
} else {
|
||||
// Fallback: Stack is external, use container recreation
|
||||
updateSuccess = await recreateContainer(containerName, envIdNum);
|
||||
if (updateSuccess) {
|
||||
const updatedContainers = await listContainers(true, envIdNum);
|
||||
const updatedContainer = updatedContainers.find(c => c.name === containerName);
|
||||
if (updatedContainer) {
|
||||
newContainerId = updatedContainer.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standalone container: Use shared recreation with ALL settings
|
||||
updateSuccess = await recreateContainer(containerName, envIdNum);
|
||||
if (updateSuccess) {
|
||||
const updatedContainers = await listContainers(true, envIdNum);
|
||||
const updatedContainer = updatedContainers.find(c => c.name === containerName);
|
||||
if (updatedContainer) {
|
||||
newContainerId = updatedContainer.id;
|
||||
}
|
||||
updateSuccess = await recreateContainer(containerName, envIdNum);
|
||||
if (updateSuccess) {
|
||||
const updatedContainers = await listContainers(true, envIdNum);
|
||||
const updatedContainer = updatedContainers.find(c => c.name === containerName);
|
||||
if (updatedContainer) {
|
||||
newContainerId = updatedContainer.id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import CronEditor from '$lib/components/cron-editor.svelte';
|
||||
import VulnerabilityCriteriaSelector, { type VulnerabilityCriteria } from '$lib/components/VulnerabilityCriteriaSelector.svelte';
|
||||
import { currentEnvironment } from '$lib/stores/environment';
|
||||
import { Ship, Cable, ExternalLink, AlertTriangle, Info, Layers } from 'lucide-svelte';
|
||||
import { Ship, Cable, ExternalLink, AlertTriangle, Info } from 'lucide-svelte';
|
||||
import type { SystemContainerType } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
@@ -12,8 +12,6 @@
|
||||
cronExpression: string;
|
||||
vulnerabilityCriteria: VulnerabilityCriteria;
|
||||
systemContainer?: SystemContainerType | null;
|
||||
isComposeContainer?: boolean;
|
||||
composeStackName?: string;
|
||||
onenablechange?: (enabled: boolean) => void;
|
||||
oncronchange?: (cron: string) => void;
|
||||
oncriteriachange?: (criteria: VulnerabilityCriteria) => void;
|
||||
@@ -24,8 +22,6 @@
|
||||
cronExpression = $bindable(),
|
||||
vulnerabilityCriteria = $bindable(),
|
||||
systemContainer = null,
|
||||
isComposeContainer = false,
|
||||
composeStackName = '',
|
||||
onenablechange,
|
||||
oncronchange,
|
||||
oncriteriachange
|
||||
@@ -97,20 +93,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if isComposeContainer && enabled}
|
||||
<div class="flex items-start gap-2 rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-950">
|
||||
<Layers class="mt-0.5 h-4 w-4 text-blue-600 dark:text-blue-400 shrink-0" />
|
||||
<div class="text-xs text-blue-800 dark:text-blue-200">
|
||||
<p class="font-medium">Stack container update behavior</p>
|
||||
<p class="mt-1 text-blue-700 dark:text-blue-300">
|
||||
This container is part of the <strong>{composeStackName}</strong> stack.
|
||||
Updates will use <code class="rounded bg-blue-100 px-1 dark:bg-blue-900">docker compose up -d</code>
|
||||
to preserve all configuration from the compose file.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if enabled}
|
||||
<CronEditor
|
||||
value={cronExpression}
|
||||
|
||||
@@ -125,9 +125,6 @@
|
||||
autoUpdateEnabled: boolean;
|
||||
autoUpdateCronExpression: string;
|
||||
vulnerabilityCriteria: VulnerabilityCriteria;
|
||||
// Compose stack info
|
||||
isComposeContainer?: boolean;
|
||||
composeStackName?: string;
|
||||
// Config sets
|
||||
configSets: ConfigSet[];
|
||||
selectedConfigSetId: string;
|
||||
@@ -192,8 +189,6 @@
|
||||
autoUpdateEnabled = $bindable(),
|
||||
autoUpdateCronExpression = $bindable(),
|
||||
vulnerabilityCriteria = $bindable(),
|
||||
isComposeContainer = false,
|
||||
composeStackName = '',
|
||||
configSets,
|
||||
selectedConfigSetId = $bindable(),
|
||||
errors = $bindable(),
|
||||
@@ -1468,8 +1463,6 @@
|
||||
bind:cronExpression={autoUpdateCronExpression}
|
||||
bind:vulnerabilityCriteria={vulnerabilityCriteria}
|
||||
systemContainer={detectSystemContainer(image)}
|
||||
{isComposeContainer}
|
||||
{composeStackName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1108,8 +1108,6 @@
|
||||
bind:autoUpdateEnabled
|
||||
bind:autoUpdateCronExpression
|
||||
bind:vulnerabilityCriteria
|
||||
{isComposeContainer}
|
||||
{composeStackName}
|
||||
{configSets}
|
||||
bind:selectedConfigSetId
|
||||
bind:errors
|
||||
|
||||
Reference in New Issue
Block a user