277 lines
7.9 KiB
TypeScript
277 lines
7.9 KiB
TypeScript
|
|
/**
|
||
|
|
* StackGalleryRegistry - Main registry class
|
||
|
|
* Integrates smartregistry with Stack.Gallery's auth, storage, and database
|
||
|
|
*/
|
||
|
|
|
||
|
|
import * as plugins from './plugins.ts';
|
||
|
|
import { initDb, closeDb, isDbConnected } from './models/db.ts';
|
||
|
|
import { StackGalleryAuthProvider } from './providers/auth.provider.ts';
|
||
|
|
import { StackGalleryStorageHooks } from './providers/storage.provider.ts';
|
||
|
|
import { ApiRouter } from './api/router.ts';
|
||
|
|
|
||
|
|
export interface IRegistryConfig {
|
||
|
|
// MongoDB configuration
|
||
|
|
mongoUrl: string;
|
||
|
|
mongoDb: string;
|
||
|
|
|
||
|
|
// S3 configuration
|
||
|
|
s3Endpoint: string;
|
||
|
|
s3AccessKey: string;
|
||
|
|
s3SecretKey: string;
|
||
|
|
s3Bucket: string;
|
||
|
|
s3Region?: string;
|
||
|
|
|
||
|
|
// Server configuration
|
||
|
|
host?: string;
|
||
|
|
port?: number;
|
||
|
|
|
||
|
|
// Registry settings
|
||
|
|
storagePath?: string;
|
||
|
|
enableUpstreamCache?: boolean;
|
||
|
|
upstreamCacheExpiry?: number; // hours
|
||
|
|
|
||
|
|
// JWT configuration
|
||
|
|
jwtSecret?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class StackGalleryRegistry {
|
||
|
|
private config: IRegistryConfig;
|
||
|
|
private smartBucket: plugins.smartbucket.SmartBucket | null = null;
|
||
|
|
private smartRegistry: plugins.smartregistry.SmartRegistry | null = null;
|
||
|
|
private authProvider: StackGalleryAuthProvider | null = null;
|
||
|
|
private storageHooks: StackGalleryStorageHooks | null = null;
|
||
|
|
private apiRouter: ApiRouter | null = null;
|
||
|
|
private isInitialized = false;
|
||
|
|
|
||
|
|
constructor(config: IRegistryConfig) {
|
||
|
|
this.config = {
|
||
|
|
host: '0.0.0.0',
|
||
|
|
port: 3000,
|
||
|
|
storagePath: 'packages',
|
||
|
|
enableUpstreamCache: true,
|
||
|
|
upstreamCacheExpiry: 24,
|
||
|
|
...config,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize the registry
|
||
|
|
*/
|
||
|
|
public async init(): Promise<void> {
|
||
|
|
if (this.isInitialized) return;
|
||
|
|
|
||
|
|
console.log('[StackGalleryRegistry] Initializing...');
|
||
|
|
|
||
|
|
// Initialize MongoDB
|
||
|
|
console.log('[StackGalleryRegistry] Connecting to MongoDB...');
|
||
|
|
await initDb(this.config.mongoUrl, this.config.mongoDb);
|
||
|
|
console.log('[StackGalleryRegistry] MongoDB connected');
|
||
|
|
|
||
|
|
// Initialize S3/SmartBucket
|
||
|
|
console.log('[StackGalleryRegistry] Initializing S3 storage...');
|
||
|
|
this.smartBucket = new plugins.smartbucket.SmartBucket({
|
||
|
|
accessKey: this.config.s3AccessKey,
|
||
|
|
accessSecret: this.config.s3SecretKey,
|
||
|
|
endpoint: this.config.s3Endpoint,
|
||
|
|
bucketName: this.config.s3Bucket,
|
||
|
|
});
|
||
|
|
console.log('[StackGalleryRegistry] S3 storage initialized');
|
||
|
|
|
||
|
|
// Initialize auth provider
|
||
|
|
this.authProvider = new StackGalleryAuthProvider();
|
||
|
|
|
||
|
|
// Initialize storage hooks
|
||
|
|
this.storageHooks = new StackGalleryStorageHooks({
|
||
|
|
bucket: this.smartBucket,
|
||
|
|
basePath: this.config.storagePath!,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Initialize smartregistry
|
||
|
|
console.log('[StackGalleryRegistry] Initializing smartregistry...');
|
||
|
|
this.smartRegistry = new plugins.smartregistry.SmartRegistry({
|
||
|
|
authProvider: this.authProvider,
|
||
|
|
storageHooks: this.storageHooks,
|
||
|
|
storage: {
|
||
|
|
type: 's3',
|
||
|
|
bucket: this.smartBucket,
|
||
|
|
basePath: this.config.storagePath,
|
||
|
|
},
|
||
|
|
upstreamCache: this.config.enableUpstreamCache
|
||
|
|
? {
|
||
|
|
enabled: true,
|
||
|
|
expiryHours: this.config.upstreamCacheExpiry,
|
||
|
|
}
|
||
|
|
: undefined,
|
||
|
|
});
|
||
|
|
console.log('[StackGalleryRegistry] smartregistry initialized');
|
||
|
|
|
||
|
|
// Initialize API router
|
||
|
|
console.log('[StackGalleryRegistry] Initializing API router...');
|
||
|
|
this.apiRouter = new ApiRouter();
|
||
|
|
console.log('[StackGalleryRegistry] API router initialized');
|
||
|
|
|
||
|
|
this.isInitialized = true;
|
||
|
|
console.log('[StackGalleryRegistry] Initialization complete');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Start the HTTP server
|
||
|
|
*/
|
||
|
|
public async start(): Promise<void> {
|
||
|
|
if (!this.isInitialized) {
|
||
|
|
await this.init();
|
||
|
|
}
|
||
|
|
|
||
|
|
const port = this.config.port!;
|
||
|
|
const host = this.config.host!;
|
||
|
|
|
||
|
|
console.log(`[StackGalleryRegistry] Starting server on ${host}:${port}...`);
|
||
|
|
|
||
|
|
Deno.serve(
|
||
|
|
{ port, hostname: host },
|
||
|
|
async (request: Request): Promise<Response> => {
|
||
|
|
return await this.handleRequest(request);
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
console.log(`[StackGalleryRegistry] Server running on http://${host}:${port}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle incoming HTTP request
|
||
|
|
*/
|
||
|
|
private async handleRequest(request: Request): Promise<Response> {
|
||
|
|
const url = new URL(request.url);
|
||
|
|
const path = url.pathname;
|
||
|
|
|
||
|
|
// Health check
|
||
|
|
if (path === '/health' || path === '/healthz') {
|
||
|
|
return this.healthCheck();
|
||
|
|
}
|
||
|
|
|
||
|
|
// API endpoints (handled by REST API layer)
|
||
|
|
if (path.startsWith('/api/')) {
|
||
|
|
return await this.handleApiRequest(request);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Registry protocol endpoints
|
||
|
|
// NPM: /-/..., /@scope/package, /package
|
||
|
|
// OCI: /v2/...
|
||
|
|
// Maven: /maven2/...
|
||
|
|
// PyPI: /simple/..., /pypi/...
|
||
|
|
// Cargo: /api/v1/crates/...
|
||
|
|
// Composer: /packages.json, /p/...
|
||
|
|
// RubyGems: /api/v1/gems/..., /gems/...
|
||
|
|
|
||
|
|
if (this.smartRegistry) {
|
||
|
|
try {
|
||
|
|
return await this.smartRegistry.handleRequest(request);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('[StackGalleryRegistry] Request error:', error);
|
||
|
|
return new Response(
|
||
|
|
JSON.stringify({ error: 'Internal server error' }),
|
||
|
|
{
|
||
|
|
status: 500,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return new Response('Not Found', { status: 404 });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle API requests
|
||
|
|
*/
|
||
|
|
private async handleApiRequest(request: Request): Promise<Response> {
|
||
|
|
if (!this.apiRouter) {
|
||
|
|
return new Response(
|
||
|
|
JSON.stringify({ error: 'API router not initialized' }),
|
||
|
|
{
|
||
|
|
status: 503,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
}
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return await this.apiRouter.handle(request);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Health check endpoint
|
||
|
|
*/
|
||
|
|
private healthCheck(): Response {
|
||
|
|
const healthy = this.isInitialized && isDbConnected();
|
||
|
|
|
||
|
|
const status = {
|
||
|
|
status: healthy ? 'healthy' : 'unhealthy',
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
services: {
|
||
|
|
mongodb: isDbConnected() ? 'connected' : 'disconnected',
|
||
|
|
s3: this.smartBucket ? 'initialized' : 'not initialized',
|
||
|
|
registry: this.smartRegistry ? 'initialized' : 'not initialized',
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
return new Response(JSON.stringify(status), {
|
||
|
|
status: healthy ? 200 : 503,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Stop the registry
|
||
|
|
*/
|
||
|
|
public async stop(): Promise<void> {
|
||
|
|
console.log('[StackGalleryRegistry] Shutting down...');
|
||
|
|
await closeDb();
|
||
|
|
this.isInitialized = false;
|
||
|
|
console.log('[StackGalleryRegistry] Shutdown complete');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the smartregistry instance
|
||
|
|
*/
|
||
|
|
public getSmartRegistry(): plugins.smartregistry.SmartRegistry | null {
|
||
|
|
return this.smartRegistry;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the smartbucket instance
|
||
|
|
*/
|
||
|
|
public getSmartBucket(): plugins.smartbucket.SmartBucket | null {
|
||
|
|
return this.smartBucket;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if registry is initialized
|
||
|
|
*/
|
||
|
|
public getIsInitialized(): boolean {
|
||
|
|
return this.isInitialized;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create registry from environment variables
|
||
|
|
*/
|
||
|
|
export function createRegistryFromEnv(): StackGalleryRegistry {
|
||
|
|
const config: IRegistryConfig = {
|
||
|
|
mongoUrl: Deno.env.get('MONGODB_URL') || 'mongodb://localhost:27017',
|
||
|
|
mongoDb: Deno.env.get('MONGODB_DB') || 'stackgallery',
|
||
|
|
s3Endpoint: Deno.env.get('S3_ENDPOINT') || 'http://localhost:9000',
|
||
|
|
s3AccessKey: Deno.env.get('S3_ACCESS_KEY') || 'minioadmin',
|
||
|
|
s3SecretKey: Deno.env.get('S3_SECRET_KEY') || 'minioadmin',
|
||
|
|
s3Bucket: Deno.env.get('S3_BUCKET') || 'registry',
|
||
|
|
s3Region: Deno.env.get('S3_REGION'),
|
||
|
|
host: Deno.env.get('HOST') || '0.0.0.0',
|
||
|
|
port: parseInt(Deno.env.get('PORT') || '3000', 10),
|
||
|
|
storagePath: Deno.env.get('STORAGE_PATH') || 'packages',
|
||
|
|
enableUpstreamCache: Deno.env.get('ENABLE_UPSTREAM_CACHE') !== 'false',
|
||
|
|
upstreamCacheExpiry: parseInt(Deno.env.get('UPSTREAM_CACHE_EXPIRY') || '24', 10),
|
||
|
|
jwtSecret: Deno.env.get('JWT_SECRET'),
|
||
|
|
};
|
||
|
|
|
||
|
|
return new StackGalleryRegistry(config);
|
||
|
|
}
|