feat: integrate toast notifications in settings and layout components
- Added ToastService for managing toast notifications. - Replaced alert in settings component with toast notifications for success and error messages. - Included ToastComponent in layout for displaying notifications. - Created loading spinner component for better user experience. - Implemented domain detail component with detailed views for certificates, requirements, and services. - Added functionality to manage and display SSL certificates and their statuses. - Introduced a registry manager class for handling Docker registry operations.
This commit is contained in:
237
ts/classes/registry.ts
Normal file
237
ts/classes/registry.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 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<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,
|
||||
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<Response> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a push/pull token for a service
|
||||
*/
|
||||
async createServiceToken(serviceName: string): Promise<string> {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('Registry not initialized');
|
||||
}
|
||||
|
||||
const repository = serviceName;
|
||||
const scopes = [
|
||||
`oci:repository:${repository}:push`,
|
||||
`oci:repository:${repository}:pull`,
|
||||
];
|
||||
|
||||
// Create OCI JWT token (expires in 1 year = 365 * 24 * 60 * 60 seconds)
|
||||
const token = await this.registry.authManager.createOciToken(
|
||||
'onebox',
|
||||
scopes,
|
||||
31536000 // 365 days in seconds
|
||||
);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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<string | null> {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('Registry not initialized');
|
||||
}
|
||||
|
||||
try {
|
||||
const manifest = await this.registry.getManifest(repository, tag);
|
||||
if (manifest && manifest.digest) {
|
||||
return manifest.digest;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
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<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) {
|
||||
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<void> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user