f40ef6b7c0
Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
208 lines
7.2 KiB
TypeScript
208 lines
7.2 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import * as paths from '../paths.js';
|
|
import type { Cloudly } from '../classes.cloudly.js';
|
|
import type { ExternalRegistryManager } from './classes.externalregistrymanager.js';
|
|
|
|
@plugins.smartdata.managed()
|
|
export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalRegistry, plugins.servezoneInterfaces.data.IExternalRegistry, ExternalRegistryManager> {
|
|
// 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<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) {
|
|
const externalRegistry = new ExternalRegistry();
|
|
externalRegistry.id = await this.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 this.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<plugins.servezoneInterfaces.data.IExternalRegistry['data']>
|
|
) {
|
|
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 this.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) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
this.data.status = 'error';
|
|
this.data.lastError = errorMessage;
|
|
await this.save();
|
|
return { success: false, message: errorMessage };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
};
|
|
}
|
|
}
|