This commit is contained in:
jarek committed 2026-01-20 15:39:08 +01:00
1 parent fe48d63164
commit a3cc26d958
35 files changed
+2045 -273

No files matched your search

@@ -4,9 +4,6 @@ import { authorize } from '$lib/server/authorize';
import {
listContainers,
inspectContainer,
stopContainer,
removeContainer,
createContainer,
pullImage,
getTempImageTag,
isDigestBasedImage,
@@ -18,6 +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';
export interface ScanResult {
critical: number;
@@ -401,81 +399,115 @@ export const POST: RequestHandler = async (event) => {
} catch { /* ignore cleanup errors */ }
}
// Step 3: Stop container if running
if (wasRunning) {
// 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;
// Progress logging function for shared functions
const logProgress = (message: string) => {
safeEnqueue({
type: 'progress',
containerId,
containerName,
step: 'stopping',
step: 'creating',
current: i + 1,
total: containerIds.length,
message: `Stopping ${containerName}...`
message
});
await stopContainer(containerId, envIdNum);
}
};
// Step 4: Remove old container
safeEnqueue({
type: 'progress',
containerId,
containerName,
step: 'removing',
current: i + 1,
total: containerIds.length,
message: `Removing old container ${containerName}...`
});
await removeContainer(containerId, true, envIdNum);
let updateSuccess = false;
let newContainerId = containerId;
// Prepare port bindings
const ports: { [key: string]: { HostPort: string } } = {};
if (hostConfig.PortBindings) {
for (const [containerPort, bindings] of Object.entries(hostConfig.PortBindings)) {
if (bindings && (bindings as any[]).length > 0) {
ports[containerPort] = { HostPort: (bindings as any[])[0].HostPort || '' };
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})...`
});
// Try stack-based update first
const stackSuccess = await updateStackContainer(composeProject, composeService!, envIdNum, logProgress);
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;
}
}
}
// Step 5: Create new container
safeEnqueue({
type: 'progress',
containerId,
containerName,
step: 'creating',
current: i + 1,
total: containerIds.length,
message: `Creating new container ${containerName}...`
});
const newContainer = await createContainer({
name: containerName,
image: imageName,
ports,
volumeBinds: hostConfig.Binds || [],
env: config.Env || [],
labels: config.Labels || {},
cmd: config.Cmd || undefined,
restartPolicy: hostConfig.RestartPolicy?.Name || 'no',
networkMode: hostConfig.NetworkMode || undefined
}, envIdNum);
// Step 6: Start if was running
if (wasRunning) {
if (!updateSuccess) {
safeEnqueue({
type: 'progress',
containerId,
containerName,
step: 'starting',
step: 'failed',
current: i + 1,
total: containerIds.length,
message: `Starting ${containerName}...`
success: false,
error: 'Container recreation failed'
});
await newContainer.start();
failCount++;
continue;
}
// Audit log
await auditContainer(event, 'update', newContainer.id, containerName, envIdNum, { batchUpdate: true });
await auditContainer(event, 'update', newContainerId, containerName, envIdNum, { batchUpdate: true });
// Done with this container - use original containerId for UI consistency
safeEnqueue({
@@ -1,15 +1,9 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { authorize } from '$lib/server/authorize';
import {
listContainers,
inspectContainer,
stopContainer,
removeContainer,
createContainer,
pullImage
} from '$lib/server/docker';
import { listContainers, pullImage, inspectContainer } from '$lib/server/docker';
import { auditContainer } from '$lib/server/audit';
import { recreateContainer, updateStackContainer } from '$lib/server/scheduler/tasks/container-update';
export interface BatchUpdateResult {
containerId: string;
@@ -20,6 +14,8 @@ export interface BatchUpdateResult {
/**
* Batch update containers by recreating them with latest images.
* Preserves ALL container settings including health checks, resource limits,
* capabilities, DNS, security options, ulimits, and network connections.
* Expects JSON body: { containerIds: string[] }
*/
export const POST: RequestHandler = async (event) => {
@@ -62,9 +58,7 @@ export const POST: RequestHandler = async (event) => {
// Get full container config
const inspectData = await inspectContainer(containerId, envIdNum) as any;
const wasRunning = inspectData.State.Running;
const config = inspectData.Config;
const hostConfig = inspectData.HostConfig;
const imageName = config.Image;
const containerName = container.name;
@@ -81,47 +75,65 @@ export const POST: RequestHandler = async (event) => {
continue;
}
// Stop container if running
if (wasRunning) {
await stopContainer(containerId, envIdNum);
}
// 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;
// Remove old container
await removeContainer(containerId, true, envIdNum);
let updateSuccess = false;
let newContainerId = containerId;
// Prepare port bindings
const ports: { [key: string]: { HostPort: string } } = {};
if (hostConfig.PortBindings) {
for (const [containerPort, bindings] of Object.entries(hostConfig.PortBindings)) {
if (bindings && (bindings as any[]).length > 0) {
ports[containerPort] = { HostPort: (bindings as any[])[0].HostPort || '' };
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;
}
}
}
// Create new container
const newContainer = await createContainer({
name: containerName,
image: imageName,
ports,
volumeBinds: hostConfig.Binds || [],
env: config.Env || [],
labels: config.Labels || {},
cmd: config.Cmd || undefined,
restartPolicy: hostConfig.RestartPolicy?.Name || 'no',
networkMode: hostConfig.NetworkMode || undefined
}, envIdNum);
// Start if was running
if (wasRunning) {
await newContainer.start();
if (!updateSuccess) {
results.push({
containerId,
containerName,
success: false,
error: 'Container recreation failed'
});
continue;
}
// Audit log
await auditContainer(event, 'update', newContainer.id, containerName, envIdNum, { batchUpdate: true });
await auditContainer(event, 'update', newContainerId, containerName, envIdNum, { batchUpdate: true });
results.push({
containerId: newContainer.id,
containerId: newContainerId,
containerName,
success: true
});
+5 -4
View File
@@ -1,6 +1,6 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { inspectImage, tagImage, pushImage } from '$lib/server/docker';
import { inspectImage, tagImage, pushImage, parseRegistryUrl } from '$lib/server/docker';
import { getRegistry, getEnvironment } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { auditImage } from '$lib/server/audit';
@@ -69,8 +69,8 @@ export const POST: RequestHandler = async (event) => {
}
// Build the target tag
const registryUrl = new URL(registry.url);
const registryHost = registryUrl.host;
// Parse registry URL to get host and org path separately
const { host: registryHost, fullRegistry } = parseRegistryUrl(registry.url);
// Check if this is Docker Hub
const isDockerHub = registryHost.includes('docker.io') ||
@@ -81,7 +81,8 @@ export const POST: RequestHandler = async (event) => {
// Use custom tag if provided, otherwise use the base image name
const targetImageName = newTag || baseImageName;
// Docker Hub doesn't need host prefix - just username/image:tag
const targetTag = isDockerHub ? targetImageName : `${registryHost}/${targetImageName}`;
// For other registries, use full registry path including org (e.g., registry.example.com/org/image:tag)
const targetTag = isDockerHub ? targetImageName : `${fullRegistry}/${targetImageName}`;
// Parse repo and tag properly (handle registry:port/image:tag format)
// Find the last colon that's after the last slash (that's the tag separator)
+8 -2
View File
@@ -24,7 +24,7 @@ export const GET: RequestHandler = async ({ url }) => {
return json({ error: 'Docker Hub does not support catalog listing. Please use search instead.' }, { status: 400 });
}
const { baseUrl, authHeader } = await getRegistryAuth(registry, 'registry:catalog:*');
const { baseUrl, orgPath, authHeader } = await getRegistryAuth(registry, 'registry:catalog:*');
// Build catalog URL with pagination
let catalogUrl = `${baseUrl}/v2/_catalog?n=${PAGE_SIZE}`;
@@ -58,7 +58,13 @@ export const GET: RequestHandler = async ({ url }) => {
const data = await response.json();
// The V2 API returns { repositories: [...] }
const repositories: string[] = data.repositories || [];
let repositories: string[] = data.repositories || [];
// If the registry URL has an organization path, filter to only show repos under that path
if (orgPath) {
const orgPrefix = orgPath.replace(/^\//, ''); // Remove leading slash
repositories = repositories.filter(repo => repo.startsWith(orgPrefix + '/') || repo === orgPrefix);
}
// Parse Link header for pagination
// Format: </v2/_catalog?last=xxx&n=100>; rel="next"
+1
View File
@@ -39,6 +39,7 @@ export const DELETE: RequestHandler = async ({ url }) => {
}
const { baseUrl, authHeader } = await getRegistryAuth(registry, `repository:${imageName}:pull,push,delete`);
// Note: orgPath is not used here because imageName already contains the full repo path
const headers: HeadersInit = {
'Accept': 'application/vnd.docker.distribution.manifest.v2+json'
@@ -80,6 +80,7 @@ async function searchPrivateRegistry(registry: any, term: string, limit: number)
// Try to directly check if an image exists by querying its tags endpoint
async function tryDirectImageLookup(registry: any, imageName: string): Promise<boolean> {
try {
// Note: orgPath is not used here because imageName already contains the full repo path
const { baseUrl, authHeader } = await getRegistryAuth(registry, `repository:${imageName}:pull`);
const headers: HeadersInit = {
@@ -104,6 +105,7 @@ async function tryDirectImageLookup(registry: any, imageName: string): Promise<b
// Search through catalog (slow for large registries, limited to first few pages)
async function searchCatalog(registry: any, term: string, limit: number): Promise<string[]> {
// Note: orgPath could be used here to filter results, but search is already term-based
const { baseUrl, authHeader } = await getRegistryAuth(registry, 'registry:catalog:*');
const headers: HeadersInit = {
+1
View File
@@ -73,6 +73,7 @@ async function fetchDockerHubTags(imageName: string, page: number = 1, pageSize:
}
async function fetchRegistryTags(registry: any, imageName: string): Promise<TagInfo[]> {
// Note: orgPath is not used here because imageName already contains the full repo path
const { baseUrl, authHeader } = await getRegistryAuth(registry, `repository:${imageName}:pull`);
const tagsUrl = `${baseUrl}/v2/${imageName}/tags/list`;