import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import type { Cloudly } from 'ts/classes.cloudly.js'; import type { ExternalRegistryManager } from './classes.externalregistrymanager.js'; export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc { // STATIC public static async getRegistryById(registryIdArg: string) { const externalRegistry = await this.getInstance({ id: registryIdArg, }); return externalRegistry; } public static async getRegistries() { const externalRegistries = await this.getInstances({}); return externalRegistries; } public static async getDefaultRegistry(type: 'docker' | 'npm' = 'docker') { const defaultRegistry = await this.getInstance({ 'data.type': type, 'data.isDefault': true, }); return defaultRegistry; } public static async createExternalRegistry(registryDataArg: Partial) { const externalRegistry = new ExternalRegistry(); externalRegistry.id = await ExternalRegistry.getNewId(); externalRegistry.data = { type: registryDataArg.type || 'docker', name: registryDataArg.name || '', url: registryDataArg.url || '', username: registryDataArg.username, password: registryDataArg.password, description: registryDataArg.description, isDefault: registryDataArg.isDefault || false, authType: registryDataArg.authType || (registryDataArg.username || registryDataArg.password ? 'basic' : 'none'), insecure: registryDataArg.insecure || false, namespace: registryDataArg.namespace, proxy: registryDataArg.proxy, config: registryDataArg.config, status: 'unverified', createdAt: Date.now(), updatedAt: Date.now(), }; // If this is set as default, unset other defaults of the same type if (externalRegistry.data.isDefault) { const existingDefaults = await ExternalRegistry.getInstances({ 'data.type': externalRegistry.data.type, 'data.isDefault': true, }); for (const existingDefault of existingDefaults) { existingDefault.data.isDefault = false; await existingDefault.save(); } } await externalRegistry.save(); return externalRegistry; } public static async updateExternalRegistry( registryIdArg: string, registryDataArg: Partial ) { const externalRegistry = await this.getRegistryById(registryIdArg); if (!externalRegistry) { throw new Error(`Registry with id ${registryIdArg} not found`); } // If setting as default, unset other defaults of the same type if (registryDataArg.isDefault && !externalRegistry.data.isDefault) { const existingDefaults = await ExternalRegistry.getInstances({ 'data.type': externalRegistry.data.type, 'data.isDefault': true, }); for (const existingDefault of existingDefaults) { if (existingDefault.id !== registryIdArg) { existingDefault.data.isDefault = false; await existingDefault.save(); } } } // Update fields Object.assign(externalRegistry.data, registryDataArg, { updatedAt: Date.now(), }); await externalRegistry.save(); return externalRegistry; } public static async deleteExternalRegistry(registryIdArg: string) { const externalRegistry = await this.getRegistryById(registryIdArg); if (!externalRegistry) { return false; } await externalRegistry.delete(); return true; } // INSTANCE @plugins.smartdata.svDb() public id: string; @plugins.smartdata.svDb() public data: plugins.servezoneInterfaces.data.IExternalRegistry['data']; constructor() { super(); } /** * Verify the registry connection */ public async verifyConnection(): Promise<{ success: boolean; message?: string }> { try { // For Docker registries, try to access the v2 API if (this.data.type === 'docker') { const registryUrl = this.data.url.replace(/\/$/, ''); // Remove trailing slash // Build headers based on auth type const headers: any = {}; if (this.data.authType === 'basic' && this.data.username && this.data.password) { headers['Authorization'] = 'Basic ' + Buffer.from(`${this.data.username}:${this.data.password}`).toString('base64'); } else if (this.data.authType === 'token' && this.data.password) { // For token auth, password field contains the token headers['Authorization'] = `Bearer ${this.data.password}`; } // For 'none' auth type or missing credentials, no auth header is added // Try to access the Docker Registry v2 API const response = await fetch(`${registryUrl}/v2/`, { headers, // Allow insecure if configured ...(this.data.insecure ? { rejectUnauthorized: false } : {}), }).catch(err => { throw new Error(`Failed to connect: ${err.message}`); }); if (response.status === 200) { // 200 means successful (either public or authenticated) this.data.status = 'active'; this.data.lastVerified = Date.now(); this.data.lastError = undefined; await this.save(); return { success: true, message: 'Registry connection successful' }; } else if (response.status === 401 && this.data.authType === 'none') { // 401 with no auth means registry exists but needs auth throw new Error('Registry requires authentication'); } else if (response.status === 401) { throw new Error('Authentication failed - check credentials'); } else { throw new Error(`Registry returned status ${response.status}`); } } // For npm registries, implement npm-specific verification if (this.data.type === 'npm') { // TODO: Implement npm registry verification this.data.status = 'unverified'; return { success: false, message: 'NPM registry verification not yet implemented' }; } return { success: false, message: 'Unknown registry type' }; } catch (error) { this.data.status = 'error'; this.data.lastError = error.message; await this.save(); return { success: false, message: error.message }; } } /** * Get the full registry URL with namespace if applicable */ public getFullRegistryUrl(): string { let url = this.data.url.replace(/\/$/, ''); // Remove trailing slash if (this.data.namespace) { url = `${url}/${this.data.namespace}`; } return url; } /** * Get Docker auth config for this registry */ public getDockerAuthConfig() { if (this.data.type !== 'docker') { return null; } return { username: this.data.username, password: this.data.password, email: this.data.config?.dockerConfig?.email, serveraddress: this.data.config?.dockerConfig?.serverAddress || this.data.url, }; } }