2025-11-24 01:31:15 +00:00
|
|
|
/**
|
|
|
|
|
* 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';
|
2025-11-25 08:25:54 +00:00
|
|
|
import { getErrorMessage } from '../utils/error.ts';
|
2025-11-24 01:31:15 +00:00
|
|
|
|
|
|
|
|
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<void> {
|
|
|
|
|
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,
|
2025-11-25 08:34:10 +00:00
|
|
|
address: '0.0.0.0',
|
2025-11-24 01:31:15 +00:00
|
|
|
},
|
|
|
|
|
storage: {
|
2025-11-25 08:34:10 +00:00
|
|
|
directory: dataDir,
|
2025-11-24 01:31:15 +00:00
|
|
|
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,
|
2025-11-25 08:34:10 +00:00
|
|
|
tokenStore: 'memory',
|
|
|
|
|
npmTokens: {
|
|
|
|
|
enabled: false,
|
|
|
|
|
},
|
2025-11-24 01:31:15 +00:00
|
|
|
ociTokens: {
|
|
|
|
|
enabled: true,
|
2025-11-25 08:34:10 +00:00
|
|
|
realm: 'onebox-registry',
|
2025-11-24 01:31:15 +00:00
|
|
|
service: 'onebox-registry',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
oci: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
basePath: '/v2',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await this.registry.init();
|
|
|
|
|
|
|
|
|
|
this.isInitialized = true;
|
|
|
|
|
logger.success('Onebox Registry initialized successfully');
|
|
|
|
|
} catch (error) {
|
2025-11-25 08:25:54 +00:00
|
|
|
logger.error(`Failed to initialize registry: ${getErrorMessage(error)}`);
|
2025-11-24 01:31:15 +00:00
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle incoming HTTP requests to the registry
|
|
|
|
|
*/
|
|
|
|
|
async handleRequest(req: Request): Promise<Response> {
|
|
|
|
|
if (!this.isInitialized) {
|
|
|
|
|
return new Response('Registry not initialized', { status: 503 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return await this.registry.handleRequest(req);
|
|
|
|
|
} catch (error) {
|
2025-11-25 08:25:54 +00:00
|
|
|
logger.error(`Registry request error: ${getErrorMessage(error)}`);
|
2025-11-24 01:31:15 +00:00
|
|
|
return new Response('Internal registry error', { status: 500 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all tags for a repository
|
|
|
|
|
*/
|
|
|
|
|
async getImageTags(repository: string): Promise<string[]> {
|
|
|
|
|
if (!this.isInitialized) {
|
|
|
|
|
throw new Error('Registry not initialized');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const tags = await this.registry.getTags(repository);
|
|
|
|
|
return tags || [];
|
|
|
|
|
} catch (error) {
|
2025-11-25 08:25:54 +00:00
|
|
|
logger.warn(`Failed to get tags for ${repository}: ${getErrorMessage(error)}`);
|
2025-11-24 01:31:15 +00:00
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the manifest digest for a specific image tag
|
|
|
|
|
*/
|
|
|
|
|
async getImageDigest(repository: string, tag: string): Promise<string | null> {
|
|
|
|
|
if (!this.isInitialized) {
|
|
|
|
|
throw new Error('Registry not initialized');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-24 19:52:35 +00:00
|
|
|
// Check if getManifest method exists (API may have changed)
|
|
|
|
|
if (typeof this.registry.getManifest !== 'function') {
|
|
|
|
|
// Method not available in current API version
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-24 01:31:15 +00:00
|
|
|
const manifest = await this.registry.getManifest(repository, tag);
|
|
|
|
|
if (manifest && manifest.digest) {
|
|
|
|
|
return manifest.digest;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
} catch (error) {
|
2025-11-24 19:52:35 +00:00
|
|
|
// Only log if it's not a "not a function" error
|
2025-11-25 08:25:54 +00:00
|
|
|
const errMsg = getErrorMessage(error);
|
|
|
|
|
if (!errMsg.includes('not a function')) {
|
|
|
|
|
logger.warn(`Failed to get digest for ${repository}:${tag}: ${errMsg}`);
|
2025-11-24 19:52:35 +00:00
|
|
|
}
|
2025-11-24 01:31:15 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete an image by tag
|
|
|
|
|
*/
|
|
|
|
|
async deleteImage(repository: string, tag: string): Promise<void> {
|
|
|
|
|
if (!this.isInitialized) {
|
|
|
|
|
throw new Error('Registry not initialized');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.registry.deleteManifest(repository, tag);
|
|
|
|
|
logger.info(`Deleted image ${repository}:${tag}`);
|
|
|
|
|
} catch (error) {
|
2025-11-25 08:25:54 +00:00
|
|
|
logger.error(`Failed to delete image ${repository}:${tag}: ${getErrorMessage(error)}`);
|
2025-11-24 01:31:15 +00:00
|
|
|
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<void> {
|
|
|
|
|
if (this.s3Server) {
|
|
|
|
|
try {
|
|
|
|
|
await this.s3Server.stop();
|
|
|
|
|
logger.info('smarts3 server stopped');
|
|
|
|
|
} catch (error) {
|
2025-11-25 08:25:54 +00:00
|
|
|
logger.error(`Error stopping smarts3: ${getErrorMessage(error)}`);
|
2025-11-24 01:31:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.isInitialized = false;
|
|
|
|
|
logger.info('Registry stopped');
|
|
|
|
|
}
|
|
|
|
|
}
|