/** * Onebox Registry Manager * * Manages the local Docker registry using: * - @push.rocks/smarts3 (S3-compatible server with filesystem storage) * - @push.rocks/smartregistry (OCI-compliant Docker registry) */ import * as plugins from '../plugins.ts'; import { logger } from '../logging.ts'; export class RegistryManager { private s3Server: any = null; private registry: any = null; private jwtSecret: string; private baseUrl: string; private isInitialized = false; constructor(private options: { dataDir?: string; port?: number; baseUrl?: string; } = {}) { this.jwtSecret = this.getJwtSecret(); this.baseUrl = options.baseUrl || 'localhost:5000'; } /** * Initialize the registry (start smarts3 and smartregistry) */ async init(): Promise { if (this.isInitialized) { logger.warn('Registry already initialized'); return; } try { const dataDir = this.options.dataDir || './.nogit/registry-data'; const port = this.options.port || 4000; logger.info(`Starting smarts3 server on port ${port}...`); // 1. Start smarts3 server (S3-compatible storage with filesystem backend) this.s3Server = await plugins.smarts3.Smarts3.createAndStart({ server: { port: port, host: '0.0.0.0', }, storage: { bucketsDir: dataDir, cleanSlate: false, // Preserve data across restarts }, }); logger.success(`smarts3 server started on port ${port}`); // 2. Configure smartregistry to use smarts3 logger.info('Initializing smartregistry...'); this.registry = new plugins.smartregistry.SmartRegistry({ storage: { endpoint: 'localhost', port: port, accessKey: 'onebox', // smarts3 doesn't validate credentials accessSecret: 'onebox', useSsl: false, region: 'us-east-1', bucketName: 'onebox-registry', }, auth: { jwtSecret: this.jwtSecret, ociTokens: { enabled: true, issuer: 'onebox-registry', service: 'onebox-registry', }, }, oci: { enabled: true, basePath: '/v2', }, }); await this.registry.init(); this.isInitialized = true; logger.success('Onebox Registry initialized successfully'); } catch (error) { logger.error(`Failed to initialize registry: ${error.message}`); throw error; } } /** * Handle incoming HTTP requests to the registry */ async handleRequest(req: Request): Promise { if (!this.isInitialized) { return new Response('Registry not initialized', { status: 503 }); } try { return await this.registry.handleRequest(req); } catch (error) { logger.error(`Registry request error: ${error.message}`); return new Response('Internal registry error', { status: 500 }); } } /** * Get all tags for a repository */ async getImageTags(repository: string): Promise { if (!this.isInitialized) { throw new Error('Registry not initialized'); } try { const tags = await this.registry.getTags(repository); return tags || []; } catch (error) { logger.warn(`Failed to get tags for ${repository}: ${error.message}`); return []; } } /** * Get the manifest digest for a specific image tag */ async getImageDigest(repository: string, tag: string): Promise { if (!this.isInitialized) { throw new Error('Registry not initialized'); } try { // Check if getManifest method exists (API may have changed) if (typeof this.registry.getManifest !== 'function') { // Method not available in current API version return null; } const manifest = await this.registry.getManifest(repository, tag); if (manifest && manifest.digest) { return manifest.digest; } return null; } catch (error) { // Only log if it's not a "not a function" error if (!error.message.includes('not a function')) { logger.warn(`Failed to get digest for ${repository}:${tag}: ${error.message}`); } return null; } } /** * Delete an image by tag */ async deleteImage(repository: string, tag: string): Promise { if (!this.isInitialized) { throw new Error('Registry not initialized'); } try { await this.registry.deleteManifest(repository, tag); logger.info(`Deleted image ${repository}:${tag}`); } catch (error) { logger.error(`Failed to delete image ${repository}:${tag}: ${error.message}`); throw error; } } /** * Get or generate the JWT secret for token signing */ private getJwtSecret(): string { // In production, this should be stored securely // For now, use a consistent secret stored in environment or generate one const secret = Deno.env.get('REGISTRY_JWT_SECRET'); if (secret) { return secret; } // Generate a random secret (this will be different on each restart) // In production, you'd want to persist this const randomSecret = crypto.randomUUID() + crypto.randomUUID(); logger.warn('Using generated JWT secret (will be different on restart)'); logger.warn('Set REGISTRY_JWT_SECRET environment variable for persistence'); return randomSecret; } /** * Get the registry base URL */ getBaseUrl(): string { return this.baseUrl; } /** * Get the full image name for a service */ getImageName(serviceName: string, tag: string = 'latest'): string { return `${this.baseUrl}/${serviceName}:${tag}`; } /** * Stop the registry and smarts3 server */ async stop(): Promise { if (this.s3Server) { try { await this.s3Server.stop(); logger.info('smarts3 server stopped'); } catch (error) { logger.error(`Error stopping smarts3: ${error.message}`); } } this.isInitialized = false; logger.info('Registry stopped'); } }