UNPKG

@deploystack/docker-to-iac

Version:

Translate docker run and docker compose file to Infrastructure as Code

238 lines (237 loc) 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const base_parser_1 = require("./base-parser"); const getImageUrl_1 = require("../utils/getImageUrl"); const constructImageString_1 = require("../utils/constructImageString"); const parsePort_1 = require("../utils/parsePort"); const parseCommand_1 = require("../utils/parseCommand"); const getRenderServiceType_1 = require("../utils/getRenderServiceType"); const isRenderDatabaseService_1 = require("../utils/isRenderDatabaseService"); const defaultParserConfig = { files: [ { path: 'render.yaml', templateFormat: base_parser_1.TemplateFormat.yaml, isMain: true, description: 'Render Blueprint configuration' } ], subscriptionName: 'starter', region: 'oregon', diskSizeGB: 10 }; class RenderParser extends base_parser_1.BaseParser { // Legacy method implementation (calls parseFiles under the hood) parse(config, templateFormat = base_parser_1.TemplateFormat.yaml) { return super.parse(config, templateFormat); } // New multi-file implementation parseFiles(config) { const services = []; const databases = []; const keyvalueServices = []; const databaseServiceMap = new Map(); // First pass: Identify database services and register them for (const [serviceName, serviceConfig] of Object.entries(config.services)) { const imageUrl = (0, getImageUrl_1.getImageUrl)((0, constructImageString_1.constructImageString)(serviceConfig.image)); // Check if PostgreSQL - should go into databases section if ((0, isRenderDatabaseService_1.isRenderDatabaseService)(serviceConfig.image) && imageUrl.includes('postgres')) { // Create a database instead of a service const dbName = this.sanitizeName(`${serviceName}-db`); // Track the mapping between service name and database name databaseServiceMap.set(serviceName, dbName); databases.push({ name: dbName, plan: 'free', // PostgreSQL free plan - there is no starter for PostgreSQL. }); continue; } // Check if Redis - should go into services section as type: redis if ((0, isRenderDatabaseService_1.isRenderDatabaseService)(serviceConfig.image) && imageUrl.includes('redis')) { const redisName = this.sanitizeName(serviceName); // Track the mapping between service name and Redis service name databaseServiceMap.set(serviceName, redisName); keyvalueServices.push({ type: 'redis', name: redisName, plan: 'free', // Redis free plan - there is no starter for Redis. ipAllowList: [ { source: '0.0.0.0/0', description: 'everywhere' } ] }); continue; } // Non-database services will be processed in the second pass } // Second pass: Process regular services with database connections for (const [serviceName, serviceConfig] of Object.entries(config.services)) { // Skip database services that have already been processed if (databaseServiceMap.has(serviceName)) { continue; } const ports = new Set(); if (serviceConfig.ports) { serviceConfig.ports.forEach(portMapping => { if (typeof portMapping === 'object' && portMapping !== null) { ports.add(portMapping.container); } else { const parsedPort = (0, parsePort_1.parsePort)(portMapping); if (parsedPort) { ports.add(parsedPort); } } }); } // Start with base environment variables const environmentVariables = { ...serviceConfig.environment }; if (ports.size > 0) { environmentVariables['PORT'] = Array.from(ports)[0].toString(); } // Prepare the basic service definition const service = { name: serviceName, type: (0, getRenderServiceType_1.getRenderServiceType)(serviceConfig.image), runtime: 'image', image: { url: (0, getImageUrl_1.getImageUrl)((0, constructImageString_1.constructImageString)(serviceConfig.image)) }, startCommand: (0, parseCommand_1.parseCommand)(serviceConfig.command), plan: defaultParserConfig.subscriptionName, region: defaultParserConfig.region, envVars: [] }; // Process service connections - this is provider specific logic if (config.serviceConnections) { // First, add all regular environment variables except those in service connections for (const [key, value] of Object.entries(environmentVariables)) { // Skip variables that will be handled by service connections const isHandledByConnection = config.serviceConnections.some(conn => conn.fromService === serviceName && Object.keys(conn.variables).includes(key)); if (!isHandledByConnection) { service.envVars.push({ key, value: value.toString() }); } } // Then add service connection variables with proper Render syntax for (const connection of config.serviceConnections) { if (connection.fromService === serviceName) { // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const [varName, varInfo] of Object.entries(connection.variables)) { // Check if the target is a database service if (databaseServiceMap.has(connection.toService)) { const targetServiceName = databaseServiceMap.get(connection.toService); const targetImageUrl = (0, getImageUrl_1.getImageUrl)((0, constructImageString_1.constructImageString)(config.services[connection.toService].image)); // Different handling based on database type if (targetImageUrl.includes('postgres')) { // PostgreSQL uses fromDatabase service.envVars.push({ key: varName, fromDatabase: { name: targetServiceName, // This is the database name with -db suffix property: connection.property || 'connectionString' } }); } else if (targetImageUrl.includes('redis')) { // Redis uses fromService with type: redis service.envVars.push({ key: varName, fromService: { name: targetServiceName, type: 'redis', property: connection.property || 'connectionString' } }); } } else { // Regular service connection service.envVars.push({ key: varName, fromService: { name: connection.toService, type: (0, getRenderServiceType_1.getRenderServiceType)(config.services[connection.toService].image), property: connection.property || 'hostport' } }); } } } } } else { // No service connections, just add all environment variables service.envVars = Object.entries(environmentVariables).map(([key, value]) => ({ key, value: value.toString() })); } // Add disk configuration if volumes are present if (serviceConfig.volumes && serviceConfig.volumes.length > 0) { // Take the first volume - Render only supports one disk per service const volume = serviceConfig.volumes[0]; service.disk = { name: this.generateDiskName(serviceName, volume.container), mountPath: volume.container, sizeGB: defaultParserConfig.diskSizeGB }; // If there are more volumes, log a warning if (serviceConfig.volumes.length > 1) { console.warn(`Warning: Service ${serviceName} has multiple volumes. Only the first volume will be configured as a Render disk.`); } } services.push(service); } const renderConfig = { services: [...services, ...keyvalueServices] }; // Add databases section if we have any if (databases.length > 0) { renderConfig.databases = databases; } // Return object with a single file - convert to string return { 'render.yaml': { content: this.formatFileContent(renderConfig, base_parser_1.TemplateFormat.yaml), format: base_parser_1.TemplateFormat.yaml, isMain: true } }; } sanitizeName(name) { // Sanitize the name to match Render's requirements return name.toLowerCase() .replace(/[^a-z0-9-]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } generateDiskName(serviceName, mountPath) { // Create a disk name from service name and mount path const sanitizedPath = mountPath .replace(/^\//, '') .replace(/[^a-zA-Z0-9-]/g, '-'); // Combine and truncate to 63 characters (common DNS label length limit) let name = `${serviceName}-${sanitizedPath}`.toLowerCase(); if (name.length > 63) { name = name.substring(0, 63); name = name.replace(/-+$/, ''); } return name; } getInfo() { return { providerWebsite: 'https://render.com/docs', providerName: 'Render', providerNameAbbreviation: 'RND', languageOfficialDocs: 'https://docs.render.com/infrastructure-as-code', languageAbbreviation: 'RND', languageName: 'Render Blue Print', defaultParserConfig: defaultParserConfig }; } } exports.default = new RenderParser();