From 92acd30262341416b72d876d10518283954bdfa9 Mon Sep 17 00:00:00 2001 From: nfebe Date: Thu, 16 Apr 2026 23:03:35 +0100 Subject: [PATCH] fix(types): Add service_credentials to ServiceMetadata Adds the type definition for per-service registry credential mapping, matching the new agent API field. Signed-off-by: nfebe --- src/components/NewDeploymentModal.vue | 89 ++++++--- src/services/api.ts | 3 + src/types/index.ts | 2 + src/utils/compose.ts | 37 ++++ src/views/DeploymentDetailView.test.ts | 4 + src/views/DeploymentDetailView.vue | 244 +++++++++++++++++-------- src/views/SettingsView.vue | 72 ++++++-- 7 files changed, 337 insertions(+), 114 deletions(-) create mode 100644 src/utils/compose.ts diff --git a/src/components/NewDeploymentModal.vue b/src/components/NewDeploymentModal.vue index f3cba3c..aefed94 100644 --- a/src/components/NewDeploymentModal.vue +++ b/src/components/NewDeploymentModal.vue @@ -439,6 +439,35 @@ + + +
+
+ +
+
+ {{ svc }} + +
+
+ + Override the default credential for individual services pulling from other registries. + +
+
+
@@ -1356,6 +1385,7 @@ import { oneDark } from "@codemirror/theme-one-dark"; import BaseModal from "@/components/base/BaseModal.vue"; import { deploymentsApi, templatesApi, settingsApi, containersApi, composeApi, credentialsApi } from "@/services/api"; import type { RegistryCredential } from "@/types"; +import { extractComposeServiceNames } from "@/utils/compose"; import { useNotificationsStore } from "@/stores/notifications"; interface TemplateMount { @@ -1422,6 +1452,16 @@ const showRegistryPassword = ref(false); const existingCredentials = ref([]); const loadingCredentials = ref(false); +const composeServiceNames = computed(() => extractComposeServiceNames(form.composeContent)); + +const setServiceCredential = (service: string, credentialId: string) => { + if (!credentialId) { + delete form.registry.serviceCredentials[service]; + } else { + form.registry.serviceCredentials[service] = credentialId; + } +}; + const advancedOptions = reactive({ multiDomain: false, multiDatabase: false, @@ -1604,6 +1644,7 @@ const form = reactive({ password: "", saveCredential: false, credentialName: "", + serviceCredentials: {} as Record, }, }); @@ -2238,6 +2279,7 @@ watch( password: "", saveCredential: false, credentialName: "", + serviceCredentials: {}, }; showRegistryPassword.value = false; existingDbContainers.value = []; @@ -2384,6 +2426,10 @@ const handleCreate = async () => { credential_name: form.registry.credentialName || `${form.name}-registry`, }; } + const serviceCreds = Object.fromEntries(Object.entries(form.registry.serviceCredentials).filter(([, id]) => id)); + if (Object.keys(serviceCreds).length > 0) { + payload.service_credentials = serviceCreds; + } } const mapDbToPayload = (db: DatabaseFormConfig) => ({ @@ -2430,9 +2476,8 @@ const handleCreate = async () => { payload.databases = databases; } + const domainsArray = []; if (finalDomain) { - const domainsArray = []; - domainsArray.push({ id: "primary", domain: finalDomain, @@ -2458,28 +2503,28 @@ const handleCreate = async () => { } } } - - payload.metadata = { - name: form.name, - type: "web", - networking: { - expose: true, - domain: finalDomain, - container_port: form.networking.ports[0]?.containerPort || 80, - protocol: form.networking.protocol || "http", - }, - ssl: { - enabled: form.ssl.enabled, - auto_cert: form.ssl.autoCert, - }, - healthcheck: { - path: "/health", - interval: "30s", - }, - domains: domainsArray.length > 1 ? domainsArray : undefined, - }; } + payload.metadata = { + name: form.name, + type: "web", + networking: { + expose: Boolean(finalDomain), + domain: finalDomain || "", + container_port: form.networking.ports[0]?.containerPort || 80, + protocol: form.networking.protocol || "http", + }, + ssl: { + enabled: form.ssl.enabled, + auto_cert: form.ssl.autoCert, + }, + healthcheck: { + path: "/health", + interval: "30s", + }, + domains: domainsArray.length > 1 ? domainsArray : undefined, + }; + await deploymentsApi.create(payload); emit("created"); } catch (e: any) { diff --git a/src/services/api.ts b/src/services/api.ts index 3d6ee2d..1cd177d 100755 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -63,6 +63,7 @@ export interface ServiceMetadata { interval: string; }; credential_id?: string; + service_credentials?: Record; } export interface EnvVar { @@ -631,6 +632,7 @@ export const credentialsApi = { create: (data: { name: string; registry_type_slug: string; + registry_url?: string; username: string; password: string; email?: string; @@ -640,6 +642,7 @@ export const credentialsApi = { id: string, data: { name?: string; + registry_url?: string; username?: string; password?: string; email?: string; diff --git a/src/types/index.ts b/src/types/index.ts index baa403b..7125862 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -28,6 +28,7 @@ export interface ServiceMetadata { quick_actions?: QuickAction[]; security?: DeploymentSecurityConfig; credential_id?: string; + service_credentials?: Record; domains?: DomainConfig[]; databases?: DatabaseConfig[]; } @@ -167,6 +168,7 @@ export interface RegistryCredential { id: string; name: string; registry_type_slug: string; + registry_url?: string; username: string; email?: string; is_default: boolean; diff --git a/src/utils/compose.ts b/src/utils/compose.ts new file mode 100644 index 0000000..033f4de --- /dev/null +++ b/src/utils/compose.ts @@ -0,0 +1,37 @@ +export function extractComposeServiceNames(content: string): string[] { + if (!content) return []; + const lines = content.split(/\r?\n/); + let inServices = false; + let childIndent = -1; + const names: string[] = []; + + for (const raw of lines) { + if (/^\s*#/.test(raw) || raw.trim() === "") continue; + + const indent = raw.match(/^\s*/)?.[0].length ?? 0; + + if (!inServices) { + if (/^services\s*:\s*$/.test(raw)) { + inServices = true; + childIndent = -1; + } + continue; + } + + if (indent === 0) { + inServices = false; + continue; + } + + if (childIndent === -1) { + childIndent = indent; + } + + if (indent !== childIndent) continue; + + const match = raw.match(/^\s*([A-Za-z0-9_.-]+)\s*:\s*$/); + if (match) names.push(match[1]); + } + + return names; +} diff --git a/src/views/DeploymentDetailView.test.ts b/src/views/DeploymentDetailView.test.ts index 2fdb277..41eb02b 100644 --- a/src/views/DeploymentDetailView.test.ts +++ b/src/views/DeploymentDetailView.test.ts @@ -91,6 +91,10 @@ vi.mock("@/services/api", () => ({ write: vi.fn().mockResolvedValue({ data: { message: "Written" } }), getContent: vi.fn().mockResolvedValue({ data: { content: "" } }), }, + credentialsApi: { + list: vi.fn().mockResolvedValue({ data: { credentials: [] } }), + get: vi.fn().mockResolvedValue({ data: { credential: null } }), + }, })); vi.mock("@/composables/useNotifications", () => ({ diff --git a/src/views/DeploymentDetailView.vue b/src/views/DeploymentDetailView.vue index 7a968df..6be2429 100755 --- a/src/views/DeploymentDetailView.vue +++ b/src/views/DeploymentDetailView.vue @@ -120,7 +120,7 @@
Registry - +
+
+ Per-service + + + + {{ svc }} → {{ credentialNameFor(credId) }} + + +