@deploystack/docker-to-iac
Version:
Translate docker run and docker compose file to Infrastructure as Code
238 lines (237 loc) • 11.8 kB
JavaScript
;
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();