1.0.9
This commit is contained in:
1 parent
62636426bf
commit
c185d00dc3
60 files changed
+2830
-918
No files matched your search
@@ -0,0 +1,99 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { execInContainer } from '$lib/server/docker';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
// Shell paths to check
|
||||
const SHELLS_TO_CHECK = [
|
||||
{ path: '/bin/bash', label: 'Bash' },
|
||||
{ path: '/bin/sh', label: 'Shell (sh)' },
|
||||
{ path: '/bin/zsh', label: 'Zsh' },
|
||||
{ path: '/bin/ash', label: 'Ash (Alpine)' }
|
||||
];
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
const envIdNum = envId ? parseInt(envId) : undefined;
|
||||
|
||||
// Permission check - need exec permission to detect shells
|
||||
if (auth.authEnabled && !await auth.can('containers', 'exec', envIdNum)) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Environment access check (enterprise only)
|
||||
if (envIdNum && auth.isEnterprise && !await auth.canAccessEnvironment(envIdNum)) {
|
||||
return json({ error: 'Access denied to this environment' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const containerId = params.id;
|
||||
const availableShells: string[] = [];
|
||||
|
||||
// Check each shell by testing if the file exists and is executable
|
||||
// Use a single command to check all shells at once for efficiency
|
||||
const checkCommand = SHELLS_TO_CHECK.map(s =>
|
||||
`test -x ${s.path} && echo "${s.path}"`
|
||||
).join('; ');
|
||||
|
||||
try {
|
||||
const output = await execInContainer(
|
||||
containerId,
|
||||
['sh', '-c', checkCommand],
|
||||
envIdNum
|
||||
);
|
||||
|
||||
// Parse output - each line is an available shell path
|
||||
const lines = output.trim().split('\n').filter(Boolean);
|
||||
availableShells.push(...lines);
|
||||
} catch {
|
||||
// If even sh fails, try checking with test commands individually
|
||||
// This handles edge cases where sh might not be available
|
||||
for (const shell of SHELLS_TO_CHECK) {
|
||||
try {
|
||||
await execInContainer(
|
||||
containerId,
|
||||
['test', '-x', shell.path],
|
||||
envIdNum
|
||||
);
|
||||
availableShells.push(shell.path);
|
||||
} catch {
|
||||
// Shell not available, continue to next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine default shell - prefer bash, then sh, then first available
|
||||
let defaultShell: string | null = null;
|
||||
if (availableShells.includes('/bin/bash')) {
|
||||
defaultShell = '/bin/bash';
|
||||
} else if (availableShells.includes('/bin/sh')) {
|
||||
defaultShell = '/bin/sh';
|
||||
} else if (availableShells.length > 0) {
|
||||
defaultShell = availableShells[0];
|
||||
}
|
||||
|
||||
return json({
|
||||
shells: availableShells,
|
||||
defaultShell,
|
||||
allShells: SHELLS_TO_CHECK.map(s => ({
|
||||
path: s.path,
|
||||
label: s.label,
|
||||
available: availableShells.includes(s.path)
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error detecting shells:', error);
|
||||
return json({
|
||||
error: 'Failed to detect shells',
|
||||
shells: [],
|
||||
defaultShell: null,
|
||||
allShells: SHELLS_TO_CHECK.map(s => ({
|
||||
path: s.path,
|
||||
label: s.label,
|
||||
available: false
|
||||
}))
|
||||
}, { status: 200 }); // Return 200 with empty results rather than 500
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import type { RequestHandler } from './$types';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { listContainers, inspectContainer, checkImageUpdateAvailable } from '$lib/server/docker';
|
||||
import { clearPendingContainerUpdates, addPendingContainerUpdate } from '$lib/server/db';
|
||||
import { isSystemContainer } from '$lib/server/scheduler/tasks/update-utils';
|
||||
|
||||
export interface UpdateCheckResult {
|
||||
containerId: string;
|
||||
@@ -36,7 +37,10 @@ export const POST: RequestHandler = async ({ url, cookies }) => {
|
||||
await clearPendingContainerUpdates(envIdNum);
|
||||
}
|
||||
|
||||
const containers = await listContainers(true, envIdNum);
|
||||
const allContainers = await listContainers(true, envIdNum);
|
||||
|
||||
// Filter out system containers (Dockhand, Hawser) - they cannot be updated from within Dockhand
|
||||
const containers = allContainers.filter(c => !isSystemContainer(c.image));
|
||||
|
||||
// Check container for updates
|
||||
const checkContainer = async (container: typeof containers[0]): Promise<UpdateCheckResult> => {
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
getContainerEventStats,
|
||||
getEnvSetting,
|
||||
hasEnvironments,
|
||||
getEnvUpdateCheckSettings
|
||||
getEnvUpdateCheckSettings,
|
||||
getPendingContainerUpdates
|
||||
} from '$lib/server/db';
|
||||
import {
|
||||
listContainers,
|
||||
@@ -61,6 +62,7 @@ export interface EnvironmentStats {
|
||||
paused: number;
|
||||
restarting: number;
|
||||
unhealthy: number;
|
||||
pendingUpdates: number;
|
||||
};
|
||||
images: {
|
||||
total: number;
|
||||
@@ -165,7 +167,7 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
labels: parseLabels(env.labels),
|
||||
connectionType: (env.connectionType as 'socket' | 'direct' | 'hawser-standard' | 'hawser-edge') || 'socket',
|
||||
online: false,
|
||||
containers: { total: 0, running: 0, stopped: 0, paused: 0, restarting: 0, unhealthy: 0 },
|
||||
containers: { total: 0, running: 0, stopped: 0, paused: 0, restarting: 0, unhealthy: 0, pendingUpdates: 0 },
|
||||
images: { total: 0, totalSize: 0 },
|
||||
volumes: { total: 0, totalSize: 0 },
|
||||
containersSize: 0,
|
||||
@@ -252,10 +254,11 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
envStats.stacks.partial = stacks.filter((s: any) => s.status === 'partial').length;
|
||||
envStats.stacks.stopped = stacks.filter((s: any) => s.status === 'stopped').length;
|
||||
|
||||
// Get latest metrics and event stats in parallel
|
||||
const [latestMetrics, eventStats] = await Promise.all([
|
||||
// Get latest metrics, event stats, and pending updates in parallel
|
||||
const [latestMetrics, eventStats, pendingUpdates] = await Promise.all([
|
||||
getLatestHostMetrics(env.id),
|
||||
getContainerEventStats(env.id)
|
||||
getContainerEventStats(env.id),
|
||||
getPendingContainerUpdates(env.id)
|
||||
]);
|
||||
|
||||
if (latestMetrics) {
|
||||
@@ -272,6 +275,8 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
today: eventStats.today
|
||||
};
|
||||
|
||||
envStats.containers.pendingUpdates = pendingUpdates.length;
|
||||
|
||||
} catch (error) {
|
||||
// Convert technical error messages to user-friendly ones
|
||||
const errorStr = String(error);
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
getContainerEventStats,
|
||||
getContainerEvents,
|
||||
getEnvSetting,
|
||||
getEnvUpdateCheckSettings
|
||||
getEnvUpdateCheckSettings,
|
||||
getPendingContainerUpdates
|
||||
} from '$lib/server/db';
|
||||
import {
|
||||
listContainers,
|
||||
@@ -136,7 +137,7 @@ async function getEnvironmentStatsProgressive(
|
||||
labels: parseLabels(env.labels),
|
||||
connectionType: (env.connectionType as 'socket' | 'direct' | 'hawser-standard' | 'hawser-edge') || 'socket',
|
||||
online: false,
|
||||
containers: { total: 0, running: 0, stopped: 0, paused: 0, restarting: 0, unhealthy: 0 },
|
||||
containers: { total: 0, running: 0, stopped: 0, paused: 0, restarting: 0, unhealthy: 0, pendingUpdates: 0 },
|
||||
images: { total: 0, totalSize: 0 },
|
||||
volumes: { total: 0, totalSize: 0 },
|
||||
containersSize: 0,
|
||||
@@ -174,11 +175,12 @@ async function getEnvironmentStatsProgressive(
|
||||
// Get all database stats in parallel for better performance
|
||||
// NOTE: We do NOT block on getDockerInfo() here - slow environments would block all others
|
||||
// Instead, we determine online status from whether listContainers succeeds
|
||||
const [latestMetrics, eventStats, recentEventsResult, metricsHistory] = await Promise.all([
|
||||
const [latestMetrics, eventStats, recentEventsResult, metricsHistory, pendingUpdates] = await Promise.all([
|
||||
getLatestHostMetrics(env.id),
|
||||
getContainerEventStats(env.id),
|
||||
getContainerEvents({ environmentId: env.id, limit: 10 }),
|
||||
getHostMetrics(30, env.id)
|
||||
getHostMetrics(30, env.id),
|
||||
getPendingContainerUpdates(env.id)
|
||||
]);
|
||||
|
||||
if (latestMetrics) {
|
||||
@@ -195,6 +197,8 @@ async function getEnvironmentStatsProgressive(
|
||||
today: eventStats.today
|
||||
};
|
||||
|
||||
envStats.containers.pendingUpdates = pendingUpdates.length;
|
||||
|
||||
if (recentEventsResult.events.length > 0) {
|
||||
envStats.recentEvents = recentEventsResult.events.map(e => ({
|
||||
container_name: e.containerName || 'unknown',
|
||||
@@ -221,6 +225,7 @@ async function getEnvironmentStatsProgressive(
|
||||
scannerEnabled: envStats.scannerEnabled,
|
||||
updateCheckEnabled: envStats.updateCheckEnabled,
|
||||
updateCheckAutoUpdate: envStats.updateCheckAutoUpdate,
|
||||
containers: { ...envStats.containers },
|
||||
loading: { ...envStats.loading }
|
||||
});
|
||||
|
||||
@@ -251,6 +256,7 @@ async function getEnvironmentStatsProgressive(
|
||||
envStats.containers.paused = containers.filter((c: any) => c.state === 'paused').length;
|
||||
envStats.containers.restarting = containers.filter((c: any) => c.state === 'restarting').length;
|
||||
envStats.containers.unhealthy = containers.filter((c: any) => c.health === 'unhealthy').length;
|
||||
// Note: pendingUpdates is already set from DB query, preserve it
|
||||
envStats.loading!.containers = false;
|
||||
|
||||
onPartialUpdate({
|
||||
|
||||
@@ -109,7 +109,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
stackName: trimmedStackName,
|
||||
environmentId: data.environmentId || null,
|
||||
repositoryId: repositoryId,
|
||||
composePath: data.composePath || 'docker-compose.yml',
|
||||
composePath: data.composePath || 'compose.yaml',
|
||||
envFilePath: data.envFilePath || null,
|
||||
autoUpdate: data.autoUpdate || false,
|
||||
autoUpdateSchedule: data.autoUpdateSchedule || 'daily',
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getRegistry } from '$lib/server/db';
|
||||
import { getRegistryAuth } from '$lib/server/docker';
|
||||
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
try {
|
||||
const registryId = url.searchParams.get('registry');
|
||||
const lastParam = url.searchParams.get('last'); // For pagination
|
||||
|
||||
if (!registryId) {
|
||||
return json({ error: 'Registry ID is required' }, { status: 400 });
|
||||
@@ -20,22 +24,20 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
return json({ error: 'Docker Hub does not support catalog listing. Please use search instead.' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Build the catalog URL
|
||||
let catalogUrl = registry.url;
|
||||
if (!catalogUrl.endsWith('/')) {
|
||||
catalogUrl += '/';
|
||||
const { baseUrl, authHeader } = await getRegistryAuth(registry, 'registry:catalog:*');
|
||||
|
||||
// Build catalog URL with pagination
|
||||
let catalogUrl = `${baseUrl}/v2/_catalog?n=${PAGE_SIZE}`;
|
||||
if (lastParam) {
|
||||
catalogUrl += `&last=${encodeURIComponent(lastParam)}`;
|
||||
}
|
||||
catalogUrl += 'v2/_catalog';
|
||||
|
||||
// Prepare headers
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
// Add auth if credentials are present
|
||||
if (registry.username && registry.password) {
|
||||
const credentials = Buffer.from(`${registry.username}:${registry.password}`).toString('base64');
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
if (authHeader) {
|
||||
headers['Authorization'] = authHeader;
|
||||
}
|
||||
|
||||
const response = await fetch(catalogUrl, {
|
||||
@@ -56,7 +58,18 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
const data = await response.json();
|
||||
|
||||
// The V2 API returns { repositories: [...] }
|
||||
const repositories = data.repositories || [];
|
||||
const repositories: string[] = data.repositories || [];
|
||||
|
||||
// Parse Link header for pagination
|
||||
// Format: </v2/_catalog?last=xxx&n=100>; rel="next"
|
||||
let nextLast: string | null = null;
|
||||
const linkHeader = response.headers.get('Link');
|
||||
if (linkHeader) {
|
||||
const nextMatch = linkHeader.match(/<[^>]*[?&]last=([^&>]+)[^>]*>;\s*rel="next"/);
|
||||
if (nextMatch) {
|
||||
nextLast = decodeURIComponent(nextMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// For each repository, we could fetch tags, but that's expensive
|
||||
// Just return the repository names for now
|
||||
@@ -68,7 +81,14 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
is_automated: false
|
||||
}));
|
||||
|
||||
return json(results);
|
||||
return json({
|
||||
repositories: results,
|
||||
pagination: {
|
||||
pageSize: PAGE_SIZE,
|
||||
hasMore: !!nextLast,
|
||||
nextLast: nextLast
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching registry catalog:', error);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getRegistry } from '$lib/server/db';
|
||||
import { getRegistryAuth } from '$lib/server/docker';
|
||||
|
||||
function isDockerHub(url: string): boolean {
|
||||
const lower = url.toLowerCase();
|
||||
@@ -37,22 +38,18 @@ export const DELETE: RequestHandler = async ({ url }) => {
|
||||
return json({ error: 'Docker Hub does not support image deletion via API. Please use the Docker Hub web interface.' }, { status: 400 });
|
||||
}
|
||||
|
||||
let baseUrl = registry.url;
|
||||
if (!baseUrl.endsWith('/')) {
|
||||
baseUrl += '/';
|
||||
}
|
||||
const { baseUrl, authHeader } = await getRegistryAuth(registry, `repository:${imageName}:pull,push,delete`);
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'application/vnd.docker.distribution.manifest.v2+json'
|
||||
};
|
||||
|
||||
if (registry.username && registry.password) {
|
||||
const credentials = Buffer.from(`${registry.username}:${registry.password}`).toString('base64');
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
if (authHeader) {
|
||||
headers['Authorization'] = authHeader;
|
||||
}
|
||||
|
||||
// Step 1: Get the manifest digest
|
||||
const manifestUrl = `${baseUrl}v2/${imageName}/manifests/${tag}`;
|
||||
const manifestUrl = `${baseUrl}/v2/${imageName}/manifests/${tag}`;
|
||||
const headResponse = await fetch(manifestUrl, {
|
||||
method: 'HEAD',
|
||||
headers
|
||||
@@ -74,7 +71,7 @@ export const DELETE: RequestHandler = async ({ url }) => {
|
||||
}
|
||||
|
||||
// Step 2: Delete the manifest by digest
|
||||
const deleteUrl = `${baseUrl}v2/${imageName}/manifests/${digest}`;
|
||||
const deleteUrl = `${baseUrl}/v2/${imageName}/manifests/${digest}`;
|
||||
const deleteResponse = await fetch(deleteUrl, {
|
||||
method: 'DELETE',
|
||||
headers
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getRegistry } from '$lib/server/db';
|
||||
import { getRegistryAuth } from '$lib/server/docker';
|
||||
|
||||
interface SearchResult {
|
||||
name: string;
|
||||
@@ -44,46 +45,30 @@ async function searchDockerHub(term: string, limit: number): Promise<SearchResul
|
||||
}
|
||||
|
||||
async function searchPrivateRegistry(registry: any, term: string, limit: number): Promise<SearchResult[]> {
|
||||
// Private registries use the V2 catalog API
|
||||
let baseUrl = registry.url;
|
||||
if (!baseUrl.endsWith('/')) {
|
||||
baseUrl += '/';
|
||||
}
|
||||
const results: string[] = [];
|
||||
|
||||
const catalogUrl = `${baseUrl}v2/_catalog?n=1000`;
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
if (registry.username && registry.password) {
|
||||
const credentials = Buffer.from(`${registry.username}:${registry.password}`).toString('base64');
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
const response = await fetch(catalogUrl, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authentication failed');
|
||||
// Strategy 1: If term looks like an image name (contains /), try direct lookup first
|
||||
// This is much faster than iterating through catalog for large registries like ghcr.io
|
||||
if (term.includes('/')) {
|
||||
const directResult = await tryDirectImageLookup(registry, term);
|
||||
if (directResult) {
|
||||
results.push(term);
|
||||
}
|
||||
throw new Error(`Registry returned error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const repositories = data.repositories || [];
|
||||
|
||||
// Filter repositories by search term (case-insensitive)
|
||||
const termLower = term.toLowerCase();
|
||||
const filtered = repositories
|
||||
.filter((name: string) => name.toLowerCase().includes(termLower))
|
||||
.slice(0, limit);
|
||||
// Strategy 2: Fall back to catalog search for partial matches or if direct lookup failed
|
||||
if (results.length < limit) {
|
||||
const catalogResults = await searchCatalog(registry, term, limit - results.length);
|
||||
// Add catalog results, avoiding duplicates
|
||||
for (const name of catalogResults) {
|
||||
if (!results.includes(name)) {
|
||||
results.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return results in the same format as Docker Hub
|
||||
return filtered.map((name: string) => ({
|
||||
return results.map((name: string) => ({
|
||||
name,
|
||||
description: '',
|
||||
star_count: 0,
|
||||
@@ -92,6 +77,103 @@ 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 {
|
||||
const { baseUrl, authHeader } = await getRegistryAuth(registry, `repository:${imageName}:pull`);
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
if (authHeader) {
|
||||
headers['Authorization'] = authHeader;
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/v2/${imageName}/tags/list`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
// 200 = image exists, 404 = doesn't exist
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Search through catalog (slow for large registries, limited to first few pages)
|
||||
async function searchCatalog(registry: any, term: string, limit: number): Promise<string[]> {
|
||||
const { baseUrl, authHeader } = await getRegistryAuth(registry, 'registry:catalog:*');
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
if (authHeader) {
|
||||
headers['Authorization'] = authHeader;
|
||||
}
|
||||
|
||||
const termLower = term.toLowerCase();
|
||||
const results: string[] = [];
|
||||
const PAGE_SIZE = 200;
|
||||
const MAX_PAGES = 3; // Limit pages to avoid long waits on huge registries
|
||||
|
||||
let lastRepo: string | null = null;
|
||||
let pagesSearched = 0;
|
||||
|
||||
while (results.length < limit && pagesSearched < MAX_PAGES) {
|
||||
let catalogUrl = `${baseUrl}/v2/_catalog?n=${PAGE_SIZE}`;
|
||||
if (lastRepo) {
|
||||
catalogUrl += `&last=${encodeURIComponent(lastRepo)}`;
|
||||
}
|
||||
|
||||
const response = await fetch(catalogUrl, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authentication failed');
|
||||
}
|
||||
throw new Error(`Registry returned error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const repositories: string[] = data.repositories || [];
|
||||
|
||||
if (repositories.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Filter and add matching repos
|
||||
for (const name of repositories) {
|
||||
if (name.toLowerCase().includes(termLower)) {
|
||||
results.push(name);
|
||||
if (results.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get last repo for next page
|
||||
lastRepo = repositories[repositories.length - 1];
|
||||
|
||||
// Check if there are more pages
|
||||
const linkHeader = response.headers.get('Link');
|
||||
if (!linkHeader || !linkHeader.includes('rel="next"')) {
|
||||
if (repositories.length < PAGE_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pagesSearched++;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const term = url.searchParams.get('term');
|
||||
const limit = parseInt(url.searchParams.get('limit') || '25', 10);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getRegistry } from '$lib/server/db';
|
||||
import { getRegistryAuth } from '$lib/server/docker';
|
||||
|
||||
interface TagInfo {
|
||||
name: string;
|
||||
@@ -72,21 +73,15 @@ async function fetchDockerHubTags(imageName: string, page: number = 1, pageSize:
|
||||
}
|
||||
|
||||
async function fetchRegistryTags(registry: any, imageName: string): Promise<TagInfo[]> {
|
||||
// Standard V2 registry API
|
||||
let baseUrl = registry.url;
|
||||
if (!baseUrl.endsWith('/')) {
|
||||
baseUrl += '/';
|
||||
}
|
||||
|
||||
const tagsUrl = `${baseUrl}v2/${imageName}/tags/list`;
|
||||
const { baseUrl, authHeader } = await getRegistryAuth(registry, `repository:${imageName}:pull`);
|
||||
const tagsUrl = `${baseUrl}/v2/${imageName}/tags/list`;
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
if (registry.username && registry.password) {
|
||||
const credentials = Buffer.from(`${registry.username}:${registry.password}`).toString('base64');
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
if (authHeader) {
|
||||
headers['Authorization'] = authHeader;
|
||||
}
|
||||
|
||||
const response = await fetch(tagsUrl, {
|
||||
|
||||
@@ -37,13 +37,17 @@ export const POST: RequestHandler = async ({ params, request, url, cookies }) =>
|
||||
currentComposePath = source.composePath;
|
||||
currentDir = dirname(source.composePath);
|
||||
} else {
|
||||
// Stack uses default directory structure
|
||||
// Stack uses default directory structure - check all valid compose filenames
|
||||
const stackDir = await findStackDir(name, envIdNum);
|
||||
if (stackDir) {
|
||||
const defaultComposePath = join(stackDir, 'docker-compose.yml');
|
||||
if (existsSync(defaultComposePath)) {
|
||||
currentComposePath = defaultComposePath;
|
||||
currentDir = stackDir;
|
||||
const composeNames = ['compose.yaml', 'compose.yml', 'docker-compose.yml', 'docker-compose.yaml'];
|
||||
for (const fileName of composeNames) {
|
||||
const composePath = join(stackDir, fileName);
|
||||
if (existsSync(composePath)) {
|
||||
currentComposePath = composePath;
|
||||
currentDir = stackDir;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
|
||||
return json({
|
||||
stackDir,
|
||||
composePath: `${stackDir}/docker-compose.yml`,
|
||||
composePath: `${stackDir}/compose.yaml`,
|
||||
envPath: `${stackDir}/.env`,
|
||||
source: location ? 'custom' : 'default'
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user