feat: Implement platform service providers for MinIO and MongoDB
- Added base interface and abstract class for platform service providers. - Created MinIOProvider class for S3-compatible storage with deployment, provisioning, and deprovisioning functionalities. - Implemented MongoDBProvider class for MongoDB service with similar capabilities. - Introduced error handling utilities for better error management. - Developed TokensComponent for managing registry tokens in the UI, including creation, deletion, and display of tokens.
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
import * as plugins from '../plugins.ts';
|
||||
import { logger } from '../logging.ts';
|
||||
import type { Onebox } from './onebox.ts';
|
||||
import type { IApiResponse } from '../types.ts';
|
||||
import type { IApiResponse, ICreateRegistryTokenRequest, IRegistryTokenView } from '../types.ts';
|
||||
|
||||
export class OneboxHttpServer {
|
||||
private oneboxRef: Onebox;
|
||||
@@ -263,9 +263,28 @@ export class OneboxHttpServer {
|
||||
} else if (path.match(/^\/api\/registry\/tags\/[^/]+$/)) {
|
||||
const serviceName = path.split('/').pop()!;
|
||||
return await this.handleGetRegistryTagsRequest(serviceName);
|
||||
} else if (path.match(/^\/api\/registry\/token\/[^/]+$/)) {
|
||||
const serviceName = path.split('/').pop()!;
|
||||
return await this.handleGetRegistryTokenRequest(serviceName);
|
||||
} else if (path === '/api/registry/tokens' && method === 'GET') {
|
||||
return await this.handleListRegistryTokensRequest(req);
|
||||
} else if (path === '/api/registry/tokens' && method === 'POST') {
|
||||
return await this.handleCreateRegistryTokenRequest(req);
|
||||
} else if (path.match(/^\/api\/registry\/tokens\/\d+$/) && method === 'DELETE') {
|
||||
const tokenId = Number(path.split('/').pop());
|
||||
return await this.handleDeleteRegistryTokenRequest(tokenId);
|
||||
// Platform Services endpoints
|
||||
} else if (path === '/api/platform-services' && method === 'GET') {
|
||||
return await this.handleListPlatformServicesRequest();
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq)$/) && method === 'GET') {
|
||||
const type = path.split('/').pop()!;
|
||||
return await this.handleGetPlatformServiceRequest(type);
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq)\/start$/) && method === 'POST') {
|
||||
const type = path.split('/')[3];
|
||||
return await this.handleStartPlatformServiceRequest(type);
|
||||
} else if (path.match(/^\/api\/platform-services\/(mongodb|minio|redis|postgresql|rabbitmq)\/stop$/) && method === 'POST') {
|
||||
const type = path.split('/')[3];
|
||||
return await this.handleStopPlatformServiceRequest(type);
|
||||
} else if (path.match(/^\/api\/services\/[^/]+\/platform-resources$/) && method === 'GET') {
|
||||
const serviceName = path.split('/')[3];
|
||||
return await this.handleGetServicePlatformResourcesRequest(serviceName);
|
||||
} else {
|
||||
return this.jsonResponse({ success: false, error: 'Not found' }, 404);
|
||||
}
|
||||
@@ -1032,6 +1051,183 @@ export class OneboxHttpServer {
|
||||
});
|
||||
}
|
||||
|
||||
// ============ Platform Services Endpoints ============
|
||||
|
||||
private async handleListPlatformServicesRequest(): Promise<Response> {
|
||||
try {
|
||||
const platformServices = this.oneboxRef.platformServices.getAllPlatformServices();
|
||||
const providers = this.oneboxRef.platformServices.getAllProviders();
|
||||
|
||||
// Build response with provider info
|
||||
const result = providers.map((provider) => {
|
||||
const service = platformServices.find((s) => s.type === provider.type);
|
||||
return {
|
||||
type: provider.type,
|
||||
displayName: provider.displayName,
|
||||
resourceTypes: provider.resourceTypes,
|
||||
status: service?.status || 'not-deployed',
|
||||
containerId: service?.containerId,
|
||||
createdAt: service?.createdAt,
|
||||
updatedAt: service?.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
return this.jsonResponse({ success: true, data: result });
|
||||
} catch (error) {
|
||||
logger.error(`Failed to list platform services: ${error.message}`);
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: error.message || 'Failed to list platform services',
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGetPlatformServiceRequest(type: string): Promise<Response> {
|
||||
try {
|
||||
const provider = this.oneboxRef.platformServices.getProvider(type);
|
||||
if (!provider) {
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: `Unknown platform service type: ${type}`,
|
||||
}, 404);
|
||||
}
|
||||
|
||||
const service = this.oneboxRef.database.getPlatformServiceByType(type);
|
||||
|
||||
// Get resource count
|
||||
const allResources = service?.id
|
||||
? this.oneboxRef.database.getPlatformResourcesByPlatformService(service.id)
|
||||
: [];
|
||||
|
||||
return this.jsonResponse({
|
||||
success: true,
|
||||
data: {
|
||||
type: provider.type,
|
||||
displayName: provider.displayName,
|
||||
resourceTypes: provider.resourceTypes,
|
||||
status: service?.status || 'not-deployed',
|
||||
containerId: service?.containerId,
|
||||
config: provider.getDefaultConfig(),
|
||||
resourceCount: allResources.length,
|
||||
createdAt: service?.createdAt,
|
||||
updatedAt: service?.updatedAt,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get platform service ${type}: ${error.message}`);
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: error.message || 'Failed to get platform service',
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleStartPlatformServiceRequest(type: string): Promise<Response> {
|
||||
try {
|
||||
const provider = this.oneboxRef.platformServices.getProvider(type);
|
||||
if (!provider) {
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: `Unknown platform service type: ${type}`,
|
||||
}, 404);
|
||||
}
|
||||
|
||||
logger.info(`Starting platform service: ${type}`);
|
||||
const service = await this.oneboxRef.platformServices.ensureRunning(type);
|
||||
|
||||
return this.jsonResponse({
|
||||
success: true,
|
||||
message: `Platform service ${provider.displayName} started`,
|
||||
data: {
|
||||
type: service.type,
|
||||
status: service.status,
|
||||
containerId: service.containerId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to start platform service ${type}: ${error.message}`);
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: error.message || 'Failed to start platform service',
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleStopPlatformServiceRequest(type: string): Promise<Response> {
|
||||
try {
|
||||
const provider = this.oneboxRef.platformServices.getProvider(type);
|
||||
if (!provider) {
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: `Unknown platform service type: ${type}`,
|
||||
}, 404);
|
||||
}
|
||||
|
||||
logger.info(`Stopping platform service: ${type}`);
|
||||
await this.oneboxRef.platformServices.stopPlatformService(type);
|
||||
|
||||
return this.jsonResponse({
|
||||
success: true,
|
||||
message: `Platform service ${provider.displayName} stopped`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to stop platform service ${type}: ${error.message}`);
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: error.message || 'Failed to stop platform service',
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGetServicePlatformResourcesRequest(serviceName: string): Promise<Response> {
|
||||
try {
|
||||
const service = this.oneboxRef.services.getService(serviceName);
|
||||
if (!service) {
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: 'Service not found',
|
||||
}, 404);
|
||||
}
|
||||
|
||||
const resources = await this.oneboxRef.services.getServicePlatformResources(serviceName);
|
||||
|
||||
// Format resources for API response (mask sensitive credentials)
|
||||
const formattedResources = resources.map((r) => ({
|
||||
id: r.resource.id,
|
||||
resourceType: r.resource.resourceType,
|
||||
resourceName: r.resource.resourceName,
|
||||
platformService: {
|
||||
type: r.platformService.type,
|
||||
name: r.platformService.name,
|
||||
status: r.platformService.status,
|
||||
},
|
||||
// Include env var mappings (show keys, not values)
|
||||
envVars: Object.keys(r.credentials).reduce((acc, key) => {
|
||||
// Mask sensitive values
|
||||
const value = r.credentials[key];
|
||||
if (key.toLowerCase().includes('password') || key.toLowerCase().includes('secret')) {
|
||||
acc[key] = '********';
|
||||
} else {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>),
|
||||
createdAt: r.resource.createdAt,
|
||||
}));
|
||||
|
||||
return this.jsonResponse({
|
||||
success: true,
|
||||
data: formattedResources,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get platform resources for service ${serviceName}: ${error.message}`);
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: error.message || 'Failed to get platform resources',
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Registry Endpoints ============
|
||||
|
||||
private async handleGetRegistryTagsRequest(serviceName: string): Promise<Response> {
|
||||
@@ -1047,51 +1243,206 @@ export class OneboxHttpServer {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGetRegistryTokenRequest(serviceName: string): Promise<Response> {
|
||||
// ============ Registry Token Management Endpoints ============
|
||||
|
||||
private async handleListRegistryTokensRequest(req: Request): Promise<Response> {
|
||||
try {
|
||||
// Get the service to verify it exists
|
||||
const service = this.oneboxRef.database.getServiceByName(serviceName);
|
||||
if (!service) {
|
||||
const tokens = this.oneboxRef.database.getAllRegistryTokens();
|
||||
|
||||
// Convert to view format (mask token hash, add computed fields)
|
||||
const tokenViews: IRegistryTokenView[] = tokens.map(token => {
|
||||
const now = Date.now();
|
||||
const isExpired = token.expiresAt !== null && token.expiresAt < now;
|
||||
|
||||
// Generate scope display string
|
||||
let scopeDisplay: string;
|
||||
if (token.scope === 'all') {
|
||||
scopeDisplay = 'All services';
|
||||
} else if (Array.isArray(token.scope)) {
|
||||
scopeDisplay = token.scope.length === 1
|
||||
? token.scope[0]
|
||||
: `${token.scope.length} services`;
|
||||
} else {
|
||||
scopeDisplay = 'Unknown';
|
||||
}
|
||||
|
||||
return {
|
||||
id: token.id!,
|
||||
name: token.name,
|
||||
type: token.type,
|
||||
scope: token.scope,
|
||||
scopeDisplay,
|
||||
expiresAt: token.expiresAt,
|
||||
createdAt: token.createdAt,
|
||||
lastUsedAt: token.lastUsedAt,
|
||||
createdBy: token.createdBy,
|
||||
isExpired,
|
||||
};
|
||||
});
|
||||
|
||||
return this.jsonResponse({ success: true, data: tokenViews });
|
||||
} catch (error) {
|
||||
logger.error(`Failed to list registry tokens: ${error.message}`);
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: error.message || 'Failed to list registry tokens',
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCreateRegistryTokenRequest(req: Request): Promise<Response> {
|
||||
try {
|
||||
const body = await req.json() as ICreateRegistryTokenRequest;
|
||||
|
||||
// Validate request
|
||||
if (!body.name || !body.type || !body.scope || !body.expiresIn) {
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: 'Service not found',
|
||||
}, 404);
|
||||
error: 'Missing required fields: name, type, scope, expiresIn',
|
||||
}, 400);
|
||||
}
|
||||
|
||||
// If service already has a token, return it
|
||||
if (service.registryToken) {
|
||||
if (body.type !== 'global' && body.type !== 'ci') {
|
||||
return this.jsonResponse({
|
||||
success: true,
|
||||
data: {
|
||||
token: service.registryToken,
|
||||
repository: serviceName,
|
||||
baseUrl: this.oneboxRef.registry.getBaseUrl(),
|
||||
},
|
||||
});
|
||||
success: false,
|
||||
error: 'Invalid token type. Must be "global" or "ci"',
|
||||
}, 400);
|
||||
}
|
||||
|
||||
// Generate new token
|
||||
const token = await this.oneboxRef.registry.createServiceToken(serviceName);
|
||||
// Validate scope
|
||||
if (body.scope !== 'all' && !Array.isArray(body.scope)) {
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: 'Scope must be "all" or an array of service names',
|
||||
}, 400);
|
||||
}
|
||||
|
||||
// Save token to database
|
||||
this.oneboxRef.database.updateService(service.id!, {
|
||||
registryToken: token,
|
||||
registryRepository: serviceName,
|
||||
// If scope is array of services, validate they exist
|
||||
if (Array.isArray(body.scope)) {
|
||||
for (const serviceName of body.scope) {
|
||||
const service = this.oneboxRef.database.getServiceByName(serviceName);
|
||||
if (!service) {
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: `Service not found: ${serviceName}`,
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate expiration timestamp
|
||||
const now = Date.now();
|
||||
let expiresAt: number | null = null;
|
||||
if (body.expiresIn !== 'never') {
|
||||
const daysMap: Record<string, number> = {
|
||||
'30d': 30,
|
||||
'90d': 90,
|
||||
'365d': 365,
|
||||
};
|
||||
const days = daysMap[body.expiresIn];
|
||||
if (days) {
|
||||
expiresAt = now + (days * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate token (random 32 bytes as hex)
|
||||
const plainToken = crypto.randomUUID() + crypto.randomUUID();
|
||||
|
||||
// Hash the token for storage (using simple hash for now)
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(plainToken);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const tokenHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
// Get username from auth token
|
||||
const authHeader = req.headers.get('Authorization');
|
||||
let createdBy = 'system';
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
try {
|
||||
const decoded = atob(authHeader.slice(7));
|
||||
createdBy = decoded.split(':')[0];
|
||||
} catch {
|
||||
// Keep default
|
||||
}
|
||||
}
|
||||
|
||||
// Create token in database
|
||||
const token = this.oneboxRef.database.createRegistryToken({
|
||||
name: body.name,
|
||||
tokenHash,
|
||||
type: body.type,
|
||||
scope: body.scope,
|
||||
expiresAt,
|
||||
createdAt: now,
|
||||
lastUsedAt: null,
|
||||
createdBy,
|
||||
});
|
||||
|
||||
// Build view response
|
||||
let scopeDisplay: string;
|
||||
if (token.scope === 'all') {
|
||||
scopeDisplay = 'All services';
|
||||
} else if (Array.isArray(token.scope)) {
|
||||
scopeDisplay = token.scope.length === 1
|
||||
? token.scope[0]
|
||||
: `${token.scope.length} services`;
|
||||
} else {
|
||||
scopeDisplay = 'Unknown';
|
||||
}
|
||||
|
||||
const tokenView: IRegistryTokenView = {
|
||||
id: token.id!,
|
||||
name: token.name,
|
||||
type: token.type,
|
||||
scope: token.scope,
|
||||
scopeDisplay,
|
||||
expiresAt: token.expiresAt,
|
||||
createdAt: token.createdAt,
|
||||
lastUsedAt: token.lastUsedAt,
|
||||
createdBy: token.createdBy,
|
||||
isExpired: false,
|
||||
};
|
||||
|
||||
return this.jsonResponse({
|
||||
success: true,
|
||||
data: {
|
||||
token: token,
|
||||
repository: serviceName,
|
||||
baseUrl: this.oneboxRef.registry.getBaseUrl(),
|
||||
token: tokenView,
|
||||
plainToken, // Only returned once at creation
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get registry token for ${serviceName}: ${error.message}`);
|
||||
logger.error(`Failed to create registry token: ${error.message}`);
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: error.message || 'Failed to get registry token',
|
||||
error: error.message || 'Failed to create registry token',
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDeleteRegistryTokenRequest(tokenId: number): Promise<Response> {
|
||||
try {
|
||||
// Check if token exists
|
||||
const token = this.oneboxRef.database.getRegistryTokenById(tokenId);
|
||||
if (!token) {
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: 'Token not found',
|
||||
}, 404);
|
||||
}
|
||||
|
||||
// Delete the token
|
||||
this.oneboxRef.database.deleteRegistryToken(tokenId);
|
||||
|
||||
return this.jsonResponse({
|
||||
success: true,
|
||||
message: 'Token deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete registry token ${tokenId}: ${error.message}`);
|
||||
return this.jsonResponse({
|
||||
success: false,
|
||||
error: error.message || 'Failed to delete registry token',
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user