From 6063048e5e0e028eeb0bbd7af30b50b22793b2a4 Mon Sep 17 00:00:00 2001 From: Lasim Date: Fri, 11 Apr 2025 18:15:14 +0200 Subject: [PATCH 1/4] feat: enhance database and service connection handling for Render and DigitalOcean - Added support for managed database configurations in DigitalOcean and Render. - Introduced `isManaged` property in database and service type configurations. - Enhanced environment variable resolution to support database references. - Implemented utility functions for detecting database URLs and managing service connections. - Updated container and application configuration types to accommodate new properties. --- src/config/connection-properties.ts | 61 ++++++++ src/config/digitalocean/database-types.ts | 8 +- src/config/render/service-types.ts | 17 ++- src/config/service-connections.ts | 6 +- src/index.ts | 34 +++-- src/parsers/digitalocean.ts | 92 +++++++++++- src/parsers/render.ts | 154 +++++++++++++++++++-- src/types/container-config.ts | 15 +- src/types/service-connections.ts | 3 + src/utils/detectDatabaseEnvVars.ts | 89 ++++++++++++ src/utils/detectDatabaseUrl.ts | 55 ++++++++ src/utils/isDigitalOceanManagedDatabase.ts | 16 +++ src/utils/isRenderDatabaseService.ts | 23 +++ src/utils/resolveServiceConnections.ts | 100 ++++++++++++- 14 files changed, 629 insertions(+), 44 deletions(-) create mode 100644 src/config/connection-properties.ts create mode 100644 src/utils/detectDatabaseEnvVars.ts create mode 100644 src/utils/detectDatabaseUrl.ts create mode 100644 src/utils/isDigitalOceanManagedDatabase.ts create mode 100644 src/utils/isRenderDatabaseService.ts diff --git a/src/config/connection-properties.ts b/src/config/connection-properties.ts new file mode 100644 index 0000000..353860e --- /dev/null +++ b/src/config/connection-properties.ts @@ -0,0 +1,61 @@ +export interface PropertyMapping { + render: string; + digitalOcean: string; +} + +export const servicePropertyMappings: Record = { + 'host': { + render: 'host', + digitalOcean: 'PRIVATE_DOMAIN' + }, + 'port': { + render: 'port', + digitalOcean: 'PRIVATE_PORT' + }, + 'hostport': { + render: 'hostport', + digitalOcean: 'PRIVATE_URL' + } +}; + +export const databasePropertyMappings: Record = { + 'connectionString': { + render: 'connectionString', + digitalOcean: 'DATABASE_URL' + }, + 'username': { + render: 'user', + digitalOcean: 'USERNAME' + }, + 'password': { + render: 'password', + digitalOcean: 'PASSWORD' + }, + 'databaseName': { + render: 'database', + digitalOcean: 'DATABASE' + } +}; + +/** + * Gets the correct property name for a specific provider + * + * @param property - The generic property name + * @param provider - The target provider + * @param isDatabase - Whether this is a database property + * @returns The provider-specific property name + */ +export function getPropertyForProvider( + property: string, + provider: 'render' | 'digitalOcean', + isDatabase: boolean +): string { + const mappings = isDatabase ? databasePropertyMappings : servicePropertyMappings; + + if (!mappings[property]) { + console.warn(`Unknown property: ${property}. Using as-is.`); + return property; + } + + return mappings[property][provider] || property; +} diff --git a/src/config/digitalocean/database-types.ts b/src/config/digitalocean/database-types.ts index 62c3ba9..8c76767 100644 --- a/src/config/digitalocean/database-types.ts +++ b/src/config/digitalocean/database-types.ts @@ -2,6 +2,7 @@ interface DatabaseConfig { engine: string; description: string; portNumber: number; + isManaged?: boolean; } interface DigitalOceanDatabaseConfig { @@ -24,12 +25,13 @@ export const digitalOceanDatabaseConfig: DigitalOceanDatabaseConfig = { }, 'docker.io/library/postgres': { engine: 'PG', - description: 'PostgreSQL database service - requires managed database service due to TCP protocol', - portNumber: 5432 + description: 'PostgreSQL database service - creates a managed database instance', + portNumber: 5432, + isManaged: true }, 'docker.io/library/redis': { engine: 'REDIS', - description: 'Redis database service - requires managed database service due to TCP protocol', + description: 'Redis database service - creates a managed database instance', portNumber: 6379 }, 'docker.io/library/mongodb': { diff --git a/src/config/render/service-types.ts b/src/config/render/service-types.ts index 06b2e2b..6e66213 100644 --- a/src/config/render/service-types.ts +++ b/src/config/render/service-types.ts @@ -2,6 +2,7 @@ interface RenderServiceTypeConfig { type: string; description: string; versions: string; + isManaged?: boolean; } interface RenderServiceTypesConfig { @@ -23,12 +24,18 @@ export const renderServiceTypesConfig: RenderServiceTypesConfig = { versions: '*' }, 'docker.io/library/postgres': { - type: 'pserv', - description: 'PostgreSQL database service - requires private service type due to TCP protocol', - versions: '*' + type: 'database', + description: 'PostgreSQL database - creates a managed database in databases section', + versions: '*', + isManaged: true + }, + 'docker.io/library/redis': { + type: 'redis', + description: 'Redis database - creates a keyvalue service with type: redis', + versions: '*', + isManaged: true } } }; -// Export types for use in other files -export type { RenderServiceTypeConfig, RenderServiceTypesConfig }; \ No newline at end of file +export type { RenderServiceTypeConfig, RenderServiceTypesConfig }; diff --git a/src/config/service-connections.ts b/src/config/service-connections.ts index 0a7c785..327e7d3 100644 --- a/src/config/service-connections.ts +++ b/src/config/service-connections.ts @@ -6,14 +6,14 @@ import { ProviderConnectionConfig } from '../types/service-connections'; * in a way that can be templated with environment variables */ export const providerConnectionConfigs: Record = { - // Render.com - using blueprint fromService syntax + // Render.com - using blueprint fromService or fromDatabase syntax 'RND': { useProviderNativeReferences: true, implementationType: 'blueprint-reference' }, - // DigitalOcean App Platform - simple service name with transformation + // DigitalOcean App Platform - service name or database reference 'DOP': { serviceNameTransformer: 'digitalOcean' } -}; \ No newline at end of file +}; diff --git a/src/index.ts b/src/index.ts index 90395ac..f170225 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { createSourceParser } from './sources/factory'; import { parseEnvFile } from './utils/parseEnvFile'; import { resolveServiceConnections } from './utils/resolveServiceConnections'; import { providerConnectionConfigs } from './config/service-connections'; +import { generateDatabaseServiceConnections } from './utils/detectDatabaseEnvVars'; // Store for generated environment variables const generatedEnvVars = new Map>>(); @@ -96,23 +97,32 @@ function translate(content: string, options: TranslateOptions): TranslationResul persistenceKey: options.persistenceKey }); - // Process service connections if provided + // Process service connections if provided, or auto-detect them let resolvedServiceConnections; - if (options.serviceConnections && providerConnectionConfigs[providerAbbreviation]) { + + if (providerConnectionConfigs[providerAbbreviation]) { // Get the provider-specific connection configuration const providerConnectionConfig = providerConnectionConfigs[providerAbbreviation]; - // Resolve service connections based on provider config - resolvedServiceConnections = resolveServiceConnections( - containerConfig, - options.serviceConnections, - providerConnectionConfig - ); + // If no service connections provided, try to auto-detect database connections + const serviceConnections = options.serviceConnections || { + mappings: generateDatabaseServiceConnections(containerConfig) + }; - // Add service connections to the container config - // to be accessed by parsers that use native reference mechanisms - if (resolvedServiceConnections) { - containerConfig.serviceConnections = resolvedServiceConnections; + // Only proceed if we have mappings + if (serviceConnections.mappings.length > 0) { + // Resolve service connections based on provider config + resolvedServiceConnections = resolveServiceConnections( + containerConfig, + serviceConnections, + providerConnectionConfig + ); + + // Add service connections to the container config + // to be accessed by parsers that use native reference mechanisms + if (resolvedServiceConnections) { + containerConfig.serviceConnections = resolvedServiceConnections; + } } } diff --git a/src/parsers/digitalocean.ts b/src/parsers/digitalocean.ts index be32a1d..25ccc94 100644 --- a/src/parsers/digitalocean.ts +++ b/src/parsers/digitalocean.ts @@ -5,6 +5,7 @@ import { parseCommand } from '../utils/parseCommand'; import { digitalOceanParserServiceName } from '../utils/digitalOceanParserServiceName'; import { normalizeDigitalOceanImageInfo } from '../utils/normalizeDigitalOceanImageInfo'; import { getDigitalOceanDatabaseType } from '../utils/getDigitalOceanDatabaseType'; +import { isDigitalOceanManagedDatabase } from '../utils/isDigitalOceanManagedDatabase'; const defaultParserConfig: ParserConfig = { files: [ @@ -36,9 +37,34 @@ class DigitalOceanParser extends BaseParser { parseFiles(config: ApplicationConfig): { [path: string]: FileOutput } { const services: Array = []; + const databases: Array = []; + const databaseServiceMap = new Map(); let isFirstService = true; + // First pass: identify database services that should be managed databases for (const [serviceName, serviceConfig] of Object.entries(config.services)) { + if (isDigitalOceanManagedDatabase(serviceConfig.image)) { + // Create a database entry + const dbName = digitalOceanParserServiceName(`${serviceName}-db`); + + // Track the mapping between service name and database name + databaseServiceMap.set(serviceName, dbName); + + // Get database config + const dbConfig = getDigitalOceanDatabaseType(serviceConfig.image); + + // Create database config object with proper typing + const dbEntry: any = { + name: dbName, + engine: dbConfig?.engine || 'PG' // Default to PostgreSQL if unknown + }; + + databases.push(dbEntry); + + // Skip creating a service for this database + continue; + } + const dockerImageInfo = serviceConfig.image; const databaseConfig = getDigitalOceanDatabaseType(dockerImageInfo); const normalizedImage = normalizeDigitalOceanImageInfo(dockerImageInfo); @@ -57,12 +83,65 @@ class DigitalOceanParser extends BaseParser { envs: Object.entries(serviceConfig.environment) .map(([key, value]) => ({ key, - value: value.toString() + value: value.toString(), + scope: 'RUN_TIME' })) }; - if (databaseConfig) { - // Database/TCP service configuration + // Check for database references in environment variables + for (let i = 0; i < baseService.envs.length; i++) { + const env = baseService.envs[i]; + + // Look for database URLs with format postgresql://username:password@hostname:port/database + const dbUrlMatch = env.value.match(/postgresql:\/\/.*:.*@(.*?):(.*?)\/(.*)/); + if (dbUrlMatch) { + const targetServiceName = dbUrlMatch[1]; + + // Check if the referenced hostname is a known database service + if (databaseServiceMap.has(targetServiceName)) { + // Update the env var to use the database reference + baseService.envs[i] = { + key: env.key, + value: `\${${databaseServiceMap.get(targetServiceName)}.DATABASE_URL}`, + scope: 'RUN_TIME' + }; + } + } + } + + // Process connections + if (config.serviceConnections) { + for (const connection of config.serviceConnections) { + if (connection.fromService === serviceName) { + for (const [varName] of Object.entries(connection.variables)) { + // Find the environment variable + const envIndex = baseService.envs.findIndex(env => env.key === varName); + + // Check if target service is a database + if (databaseServiceMap.has(connection.toService)) { + if (envIndex !== -1) { + // Replace the existing variable with a database reference + baseService.envs[envIndex] = { + key: varName, + value: `\${${databaseServiceMap.get(connection.toService)}.DATABASE_URL}`, + scope: 'RUN_TIME' + }; + } else { + // Add a new variable with the database reference + baseService.envs.push({ + key: varName, + value: `\${${databaseServiceMap.get(connection.toService)}.DATABASE_URL}`, + scope: 'RUN_TIME' + }); + } + } + } + } + } + } + + if (databaseConfig && !isDigitalOceanManagedDatabase(dockerImageInfo)) { + // Non-managed database/TCP service configuration services.push({ ...baseService, health_check: { @@ -96,7 +175,7 @@ class DigitalOceanParser extends BaseParser { } } - const digitalOceanConfig = { + const digitalOceanConfig: any = { spec: { name: 'deploystack', region: defaultParserConfig.region, @@ -104,6 +183,11 @@ class DigitalOceanParser extends BaseParser { } }; + // Add databases section if we have any + if (databases.length > 0) { + digitalOceanConfig.spec.databases = databases; + } + return { '.do/deploy.template.yaml': { content: this.formatFileContent(digitalOceanConfig, TemplateFormat.yaml), diff --git a/src/parsers/render.ts b/src/parsers/render.ts index ede8b6f..8b6b34d 100644 --- a/src/parsers/render.ts +++ b/src/parsers/render.ts @@ -5,6 +5,7 @@ import { constructImageString } from '../utils/constructImageString'; import { parsePort } from '../utils/parsePort'; import { parseCommand } from '../utils/parseCommand'; import { getRenderServiceType } from '../utils/getRenderServiceType'; +import { isRenderDatabaseService } from '../utils/isRenderDatabaseService'; const defaultParserConfig: ParserConfig = { files: [ @@ -29,8 +30,51 @@ class RenderParser extends BaseParser { // New multi-file implementation parseFiles(config: ApplicationConfig): { [path: string]: FileOutput } { const services: Array = []; + const databases: Array = []; + const keyvalueServices: Array = []; + const databaseServiceMap = new Map(); for (const [serviceName, serviceConfig] of Object.entries(config.services)) { + const imageUrl = getImageUrl(constructImageString(serviceConfig.image)); + + // Check if PostgreSQL - should go into databases section + if (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: defaultParserConfig.subscriptionName + }); + + continue; + } + + // Check if Redis - should go into services section as type: redis + if (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; + } + const ports = new Set(); if (serviceConfig.ports) { @@ -66,7 +110,7 @@ class RenderParser extends BaseParser { if (config.serviceConnections) { for (const connection of config.serviceConnections) { if (connection.fromService === serviceName) { - // For each referenced variable + for (const [varName] of Object.entries(connection.variables)) { // Define the type for the env parameter explicitly const index = service.envVars.findIndex((env: { key: string, value?: string }) => env.key === varName); @@ -76,15 +120,88 @@ class RenderParser extends BaseParser { service.envVars.splice(index, 1); } - // Add using Render Blueprint fromService syntax - service.envVars.push({ - key: varName, - fromService: { - name: connection.toService, - type: getRenderServiceType(config.services[connection.toService].image), - property: 'hostport' // Default to hostport for most connections + // Check if target service is a database + if (databaseServiceMap.has(connection.toService)) { + const targetService = databaseServiceMap.get(connection.toService); + const targetImageUrl = getImageUrl(constructImageString(config.services[connection.toService].image)); + + // Check if PostgreSQL (use fromDatabase) or Redis (use fromService) + if (targetImageUrl.includes('postgres')) { + // Add using Render Blueprint fromDatabase syntax for PostgreSQL + service.envVars.push({ + key: varName, + fromDatabase: { + name: targetService, // This is already the database name with -db suffix + property: 'connectionString' + } + }); + } else if (targetImageUrl.includes('redis')) { + // Add using Render Blueprint fromService syntax for Redis + service.envVars.push({ + key: varName, + fromService: { + name: targetService, + type: 'redis', + property: 'connectionString' // Redis connection property + } + }); } - }); + } else { + // Regular service connection with Render Blueprint fromService syntax + service.envVars.push({ + key: varName, + fromService: { + name: connection.toService, + type: getRenderServiceType(config.services[connection.toService].image), + property: 'hostport' // Default to hostport for most connections + } + }); + } + } + } + } + } + + // Add database connection environment variables + for (const [key, value] of Object.entries(environmentVariables)) { + if (typeof value === 'string') { + // Look for database URLs with format postgresql://username:password@hostname:port/database + const dbUrlMatch = value.match(/postgresql:\/\/.*:.*@(.*?):(.*?)\/(.*)/); + if (dbUrlMatch) { + const targetServiceName = dbUrlMatch[1]; + + // Check if the referenced hostname is a known database service + if (databaseServiceMap.has(targetServiceName)) { + const targetService = databaseServiceMap.get(targetServiceName); + const targetImageUrl = getImageUrl(constructImageString(config.services[targetServiceName].image)); + + // Find and remove the original env var + const index = service.envVars.findIndex((env: { key: string }) => env.key === key); + if (index > -1) { + service.envVars.splice(index, 1); + } + + // Check if PostgreSQL or Redis + if (targetImageUrl.includes('postgres')) { + // Add using Render Blueprint fromDatabase syntax for PostgreSQL + service.envVars.push({ + key, + fromDatabase: { + name: targetService, + property: 'connectionString' + } + }); + } else if (targetImageUrl.includes('redis')) { + // Add using Render Blueprint fromService syntax for Redis + service.envVars.push({ + key, + fromService: { + name: targetService, + type: 'redis', + property: 'connectionString' + } + }); + } } } } @@ -110,10 +227,15 @@ class RenderParser extends BaseParser { services.push(service); } - const renderConfig = { - services + const renderConfig: any = { + 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': { @@ -124,6 +246,14 @@ class RenderParser extends BaseParser { }; } + private sanitizeName(name: string): string { + // Sanitize the name to match Render's requirements + return name.toLowerCase() + .replace(/[^a-z0-9-]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); + } + private generateDiskName(serviceName: string, mountPath: string): string { // Create a disk name from service name and mount path const sanitizedPath = mountPath diff --git a/src/types/container-config.ts b/src/types/container-config.ts index fdd1432..fed0067 100644 --- a/src/types/container-config.ts +++ b/src/types/container-config.ts @@ -23,13 +23,26 @@ export interface ContainerConfig { environment: { [key: string]: string }; command?: string; restart?: string; + envVars?: Array<{ + key: string; + value?: string; + fromService?: { + name: string; + type: string; + property: string; + }; + fromDatabase?: { + name: string; + property: string; + }; + }>; } export interface ApplicationConfig { services: { [key: string]: ContainerConfig; }; - serviceConnections?: ResolvedServiceConnection[]; // Add this line + serviceConnections?: ResolvedServiceConnection[]; } export interface FileOutput { diff --git a/src/types/service-connections.ts b/src/types/service-connections.ts index e9f0982..42a04be 100644 --- a/src/types/service-connections.ts +++ b/src/types/service-connections.ts @@ -33,6 +33,9 @@ export interface ServiceConnectionMapping { // Environment variable names that contain service references environmentVariables: string[]; + + // What property to reference (default could be 'hostport' for services or 'connectionString' for databases) + property?: string; } /** diff --git a/src/utils/detectDatabaseEnvVars.ts b/src/utils/detectDatabaseEnvVars.ts new file mode 100644 index 0000000..e61599e --- /dev/null +++ b/src/utils/detectDatabaseEnvVars.ts @@ -0,0 +1,89 @@ +import { detectDatabaseUrl } from './detectDatabaseUrl'; + +/** + * Detects and returns environment variables that appear to be database connection strings + * along with their corresponding service name + */ +export function detectDatabaseEnvironmentVariables( + environment: Record, + knownServices: string[] +): Map { + // Map of service name to array of environment variable keys + const serviceToEnvVars = new Map(); + + for (const [key, value] of Object.entries(environment)) { + if (typeof value === 'string') { + // Common database URL environment variable names + const isDatabaseUrlKey = + key === 'DATABASE_URL' || + key.includes('_DATABASE_URL') || + key.includes('_CONNECTION_STRING') || + key.includes('_CONN_URL') || + key === 'POSTGRES_URL' || + key === 'REDIS_URL' || + key === 'MONGODB_URI'; + + // Try to detect database URL + const databaseUrl = detectDatabaseUrl(value); + + if (databaseUrl && knownServices.includes(databaseUrl.host)) { + // This is a database URL environment variable referencing a known service + const serviceEnvVars = serviceToEnvVars.get(databaseUrl.host) || []; + serviceEnvVars.push(key); + serviceToEnvVars.set(databaseUrl.host, serviceEnvVars); + } else if (isDatabaseUrlKey) { + // Check for exact service name matches in the URL + for (const serviceName of knownServices) { + if (value.includes(`@${serviceName}:`) || value.includes(`@${serviceName}/`)) { + const serviceEnvVars = serviceToEnvVars.get(serviceName) || []; + serviceEnvVars.push(key); + serviceToEnvVars.set(serviceName, serviceEnvVars); + break; + } + } + } + } + } + + return serviceToEnvVars; +} + +/** + * Automatically creates service connection configurations based on environment variables + */ +export function generateDatabaseServiceConnections( + config: { services: Record }> } +): Array<{ fromService: string, toService: string, environmentVariables: string[] }> { + const connections: Array<{ + fromService: string, + toService: string, + environmentVariables: string[] + }> = []; + + // Get all service names + const serviceNames = Object.keys(config.services); + + // Check each service for database connections to other services + for (const fromService of serviceNames) { + const environment = config.services[fromService].environment; + + // Detect database environment variables + const databaseConnections = detectDatabaseEnvironmentVariables( + environment, + serviceNames + ); + + // Create connection mappings + for (const [toService, envVars] of databaseConnections.entries()) { + if (envVars.length > 0) { + connections.push({ + fromService, + toService, + environmentVariables: envVars + }); + } + } + } + + return connections; +} diff --git a/src/utils/detectDatabaseUrl.ts b/src/utils/detectDatabaseUrl.ts new file mode 100644 index 0000000..f0f1fcf --- /dev/null +++ b/src/utils/detectDatabaseUrl.ts @@ -0,0 +1,55 @@ +/** + * Detects various database URL formats and extracts the host/service name + */ +export function detectDatabaseUrl(value: string): { + type: 'postgresql' | 'mysql' | 'redis' | 'mongodb' | 'unknown', + host: string, + port: string, + database: string +} | null { + // PostgreSQL format: postgresql://username:password@hostname:port/database + const pgMatch = value.match(/postgresql:\/\/.*:.*@([^:]+):(\d+)\/([^?]+)/); + if (pgMatch) { + return { + type: 'postgresql', + host: pgMatch[1], + port: pgMatch[2], + database: pgMatch[3] + }; + } + + // MySQL format: mysql://username:password@hostname:port/database + const mysqlMatch = value.match(/mysql:\/\/.*:.*@([^:]+):(\d+)\/([^?]+)/); + if (mysqlMatch) { + return { + type: 'mysql', + host: mysqlMatch[1], + port: mysqlMatch[2], + database: mysqlMatch[3] + }; + } + + // Redis format: redis://username:password@hostname:port + const redisMatch = value.match(/redis:\/\/.*:.*@([^:]+):(\d+)/); + if (redisMatch) { + return { + type: 'redis', + host: redisMatch[1], + port: redisMatch[2], + database: '' // Redis doesn't have database names in the same way + }; + } + + // MongoDB format: mongodb://username:password@hostname:port/database + const mongoMatch = value.match(/mongodb:\/\/.*:.*@([^:]+):(\d+)\/([^?]+)/); + if (mongoMatch) { + return { + type: 'mongodb', + host: mongoMatch[1], + port: mongoMatch[2], + database: mongoMatch[3] + }; + } + + return null; +} diff --git a/src/utils/isDigitalOceanManagedDatabase.ts b/src/utils/isDigitalOceanManagedDatabase.ts new file mode 100644 index 0000000..6e56674 --- /dev/null +++ b/src/utils/isDigitalOceanManagedDatabase.ts @@ -0,0 +1,16 @@ +import { DockerImageInfo } from '../parsers/base-parser'; +import { getImageUrl } from './getImageUrl'; +import { constructImageString } from './constructImageString'; +import { digitalOceanDatabaseConfig } from '../config/digitalocean/database-types'; + +export function isDigitalOceanManagedDatabase(image: DockerImageInfo): boolean { + const normalizedImage = getImageUrl(constructImageString(image)); + + for (const [configImage, dbConfig] of Object.entries(digitalOceanDatabaseConfig.databases)) { + if (normalizedImage.includes(configImage) && dbConfig.isManaged) { + return true; + } + } + + return false; +} diff --git a/src/utils/isRenderDatabaseService.ts b/src/utils/isRenderDatabaseService.ts new file mode 100644 index 0000000..cb208f6 --- /dev/null +++ b/src/utils/isRenderDatabaseService.ts @@ -0,0 +1,23 @@ +import { DockerImageInfo } from '../parsers/base-parser'; +import { getImageUrl } from './getImageUrl'; +import { constructImageString } from './constructImageString'; +import { renderServiceTypesConfig } from '../config/render/service-types'; + +export function isRenderDatabaseService(image: DockerImageInfo): boolean { + const normalizedImage = getImageUrl(constructImageString(image)); + + // Check for managed database services (PostgreSQL and Redis) + if (normalizedImage.includes('postgres') || normalizedImage.includes('redis')) { + // Find explicit configuration + for (const [configImage, serviceConfig] of Object.entries(renderServiceTypesConfig.serviceTypes)) { + if (normalizedImage.includes(configImage) && serviceConfig.isManaged) { + return true; + } + } + + // Default to true for postgres and redis even if not explicitly configured + return true; + } + + return false; +} diff --git a/src/utils/resolveServiceConnections.ts b/src/utils/resolveServiceConnections.ts index f77a77f..a5bdcdd 100644 --- a/src/utils/resolveServiceConnections.ts +++ b/src/utils/resolveServiceConnections.ts @@ -5,6 +5,12 @@ import { ProviderConnectionConfig } from '../types/service-connections'; import { getServiceNameTransformer } from './serviceNameTransformers'; +import { isRenderDatabaseService } from './isRenderDatabaseService'; +import { isDigitalOceanManagedDatabase } from './isDigitalOceanManagedDatabase'; +import { getRenderServiceType } from './getRenderServiceType'; +import { getImageUrl } from './getImageUrl'; +import { constructImageString } from './constructImageString'; +import { getPropertyForProvider } from '../config/connection-properties'; /** * Replace service references in environment variable values based on provider configuration @@ -17,7 +23,7 @@ export function resolveServiceConnections( const resolvedConnections: ResolvedServiceConnection[] = []; for (const mapping of serviceConnections.mappings) { - const { fromService, toService, environmentVariables } = mapping; + const { fromService, toService, environmentVariables, property } = mapping; // Skip if services don't exist if (!config.services[fromService] || !config.services[toService]) { @@ -35,6 +41,25 @@ export function resolveServiceConnections( variables: {} }; + // Determine if target service is a database (based on provider) + const isTargetDatabase = + (providerConfig.implementationType === 'blueprint-reference' && isRenderDatabaseService(config.services[toService].image)) || + (providerConfig.serviceNameTransformer === 'digitalOcean' && isDigitalOceanManagedDatabase(config.services[toService].image)); + + // Get the target service image URL for specific checks + const targetImageUrl = getImageUrl(constructImageString(config.services[toService].image)); + + // Get the requested property or use default + const requestedProperty = property || + (isTargetDatabase ? 'connectionString' : 'hostport'); + + // Get provider-specific property name + const providerProperty = getPropertyForProvider( + requestedProperty, + providerConfig.implementationType === 'blueprint-reference' ? 'render' : 'digitalOcean', + isTargetDatabase + ); + // Process each environment variable that references the target service for (const varName of environmentVariables) { const matchingVarNames = Object.keys(serviceEnv).filter(envKey => @@ -46,15 +71,30 @@ export function resolveServiceConnections( const originalValue = serviceEnv[matchedVarName]; let transformedValue = originalValue; - if (!providerConfig.useProviderNativeReferences) { + // For Render.com with blueprint references + if (providerConfig.implementationType === 'blueprint-reference') { + // We'll handle this by setting a flag and updating at the end of the loop + // as Render uses a different structure than simple string replacement + + // Remove existing environment variable as it will be replaced with a fromService reference + delete config.services[fromService].environment[matchedVarName]; + } + // For DigitalOcean and other providers that use string replacement + else { // Get the appropriate transformer function const transformerFn = getServiceNameTransformer(providerConfig.serviceNameTransformer); // Transform the service name const transformedServiceName = transformerFn(toService); - // Use the transformed name - transformedValue = transformedServiceName; + // Handle different types of references based on service type + if (isTargetDatabase && providerConfig.serviceNameTransformer === 'digitalOcean') { + // For databases on DigitalOcean, use the database reference syntax + transformedValue = `\${${transformedServiceName}-db.${providerProperty}}`; + } else { + // For regular services on DigitalOcean, use the service reference syntax + transformedValue = `\${${transformedServiceName}.${providerProperty}}`; + } // Update the environment variable config.services[fromService].environment[matchedVarName] = transformedValue; @@ -68,6 +108,58 @@ export function resolveServiceConnections( } } + + // For Render.com, add fromService or fromDatabase references directly to the service config + if (providerConfig.implementationType === 'blueprint-reference') { + // Ensure envVars exists + if (!config.services[fromService].envVars) { + config.services[fromService].envVars = []; + } + + // Get the actual service with envVars + const serviceConfig = config.services[fromService] as any; + + for (const varName of environmentVariables) { + // If this is a database target + if (isTargetDatabase) { + // Specifically check for PostgreSQL + if (targetImageUrl.includes('postgres')) { + + // This should be fromDatabase with the postgres-db name + serviceConfig.envVars.push({ + key: varName, + fromDatabase: { + name: `${toService}-db`, // Add -db suffix for database name + property: 'connectionString' // Force connectionString for databases + } + }); + } else if (targetImageUrl.includes('redis')) { + // Redis uses fromService with type: redis + serviceConfig.envVars.push({ + key: varName, + fromService: { + name: toService, + type: 'redis', + property: 'connectionString' + } + }); + } + } else { + console.log(`Adding regular fromService reference for ${varName}`); + // Regular service reference + serviceConfig.envVars.push({ + key: varName, + fromService: { + name: toService, + type: getRenderServiceType(config.services[toService].image), + property: providerProperty + } + }); + } + } + } + + resolvedConnections.push(resolvedConnection); } From b75b5062dfe41f344bf6a10c7e00e17ddd21428f Mon Sep 17 00:00:00 2001 From: Lasim Date: Fri, 11 Apr 2025 21:10:50 +0200 Subject: [PATCH 2/4] refactor: remove detectDatabaseUrl function and related environment variable detection logic --- src/utils/detectDatabaseEnvVars.ts | 84 +----------------------------- src/utils/detectDatabaseUrl.ts | 55 ------------------- 2 files changed, 2 insertions(+), 137 deletions(-) delete mode 100644 src/utils/detectDatabaseUrl.ts diff --git a/src/utils/detectDatabaseEnvVars.ts b/src/utils/detectDatabaseEnvVars.ts index e61599e..d613728 100644 --- a/src/utils/detectDatabaseEnvVars.ts +++ b/src/utils/detectDatabaseEnvVars.ts @@ -1,89 +1,9 @@ -import { detectDatabaseUrl } from './detectDatabaseUrl'; - -/** - * Detects and returns environment variables that appear to be database connection strings - * along with their corresponding service name - */ -export function detectDatabaseEnvironmentVariables( - environment: Record, - knownServices: string[] -): Map { - // Map of service name to array of environment variable keys - const serviceToEnvVars = new Map(); - - for (const [key, value] of Object.entries(environment)) { - if (typeof value === 'string') { - // Common database URL environment variable names - const isDatabaseUrlKey = - key === 'DATABASE_URL' || - key.includes('_DATABASE_URL') || - key.includes('_CONNECTION_STRING') || - key.includes('_CONN_URL') || - key === 'POSTGRES_URL' || - key === 'REDIS_URL' || - key === 'MONGODB_URI'; - - // Try to detect database URL - const databaseUrl = detectDatabaseUrl(value); - - if (databaseUrl && knownServices.includes(databaseUrl.host)) { - // This is a database URL environment variable referencing a known service - const serviceEnvVars = serviceToEnvVars.get(databaseUrl.host) || []; - serviceEnvVars.push(key); - serviceToEnvVars.set(databaseUrl.host, serviceEnvVars); - } else if (isDatabaseUrlKey) { - // Check for exact service name matches in the URL - for (const serviceName of knownServices) { - if (value.includes(`@${serviceName}:`) || value.includes(`@${serviceName}/`)) { - const serviceEnvVars = serviceToEnvVars.get(serviceName) || []; - serviceEnvVars.push(key); - serviceToEnvVars.set(serviceName, serviceEnvVars); - break; - } - } - } - } - } - - return serviceToEnvVars; -} - /** * Automatically creates service connection configurations based on environment variables */ export function generateDatabaseServiceConnections( config: { services: Record }> } ): Array<{ fromService: string, toService: string, environmentVariables: string[] }> { - const connections: Array<{ - fromService: string, - toService: string, - environmentVariables: string[] - }> = []; - - // Get all service names - const serviceNames = Object.keys(config.services); - - // Check each service for database connections to other services - for (const fromService of serviceNames) { - const environment = config.services[fromService].environment; - - // Detect database environment variables - const databaseConnections = detectDatabaseEnvironmentVariables( - environment, - serviceNames - ); - - // Create connection mappings - for (const [toService, envVars] of databaseConnections.entries()) { - if (envVars.length > 0) { - connections.push({ - fromService, - toService, - environmentVariables: envVars - }); - } - } - } - - return connections; + // Return an empty array - no auto-detection + return []; } diff --git a/src/utils/detectDatabaseUrl.ts b/src/utils/detectDatabaseUrl.ts deleted file mode 100644 index f0f1fcf..0000000 --- a/src/utils/detectDatabaseUrl.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Detects various database URL formats and extracts the host/service name - */ -export function detectDatabaseUrl(value: string): { - type: 'postgresql' | 'mysql' | 'redis' | 'mongodb' | 'unknown', - host: string, - port: string, - database: string -} | null { - // PostgreSQL format: postgresql://username:password@hostname:port/database - const pgMatch = value.match(/postgresql:\/\/.*:.*@([^:]+):(\d+)\/([^?]+)/); - if (pgMatch) { - return { - type: 'postgresql', - host: pgMatch[1], - port: pgMatch[2], - database: pgMatch[3] - }; - } - - // MySQL format: mysql://username:password@hostname:port/database - const mysqlMatch = value.match(/mysql:\/\/.*:.*@([^:]+):(\d+)\/([^?]+)/); - if (mysqlMatch) { - return { - type: 'mysql', - host: mysqlMatch[1], - port: mysqlMatch[2], - database: mysqlMatch[3] - }; - } - - // Redis format: redis://username:password@hostname:port - const redisMatch = value.match(/redis:\/\/.*:.*@([^:]+):(\d+)/); - if (redisMatch) { - return { - type: 'redis', - host: redisMatch[1], - port: redisMatch[2], - database: '' // Redis doesn't have database names in the same way - }; - } - - // MongoDB format: mongodb://username:password@hostname:port/database - const mongoMatch = value.match(/mongodb:\/\/.*:.*@([^:]+):(\d+)\/([^?]+)/); - if (mongoMatch) { - return { - type: 'mongodb', - host: mongoMatch[1], - port: mongoMatch[2], - database: mongoMatch[3] - }; - } - - return null; -} From 08779f6e9c1de96117a226ad2e5f3c41ff5eb878 Mon Sep 17 00:00:00 2001 From: Lasim Date: Fri, 11 Apr 2025 21:47:12 +0200 Subject: [PATCH 3/4] refactor: streamline service connection handling and remove provider-specific logic --- src/config/service-connections.ts | 19 ---- src/index.ts | 40 ++++---- src/parsers/digitalocean.ts | 55 +++++------ src/parsers/render.ts | 131 +++++++++++-------------- src/types/service-connections.ts | 3 + src/utils/resolveServiceConnections.ts | 125 ++--------------------- 6 files changed, 112 insertions(+), 261 deletions(-) delete mode 100644 src/config/service-connections.ts diff --git a/src/config/service-connections.ts b/src/config/service-connections.ts deleted file mode 100644 index 327e7d3..0000000 --- a/src/config/service-connections.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ProviderConnectionConfig } from '../types/service-connections'; - -/** - * Provider-specific connection formats - * Note: AWS CloudFormation removed as it doesn't support direct service-to-service communication - * in a way that can be templated with environment variables - */ -export const providerConnectionConfigs: Record = { - // Render.com - using blueprint fromService or fromDatabase syntax - 'RND': { - useProviderNativeReferences: true, - implementationType: 'blueprint-reference' - }, - - // DigitalOcean App Platform - service name or database reference - 'DOP': { - serviceNameTransformer: 'digitalOcean' - } -}; diff --git a/src/index.ts b/src/index.ts index f170225..750a189 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,6 @@ import digitalOceanParserInstance from './parsers/digitalocean'; import { createSourceParser } from './sources/factory'; import { parseEnvFile } from './utils/parseEnvFile'; import { resolveServiceConnections } from './utils/resolveServiceConnections'; -import { providerConnectionConfigs } from './config/service-connections'; import { generateDatabaseServiceConnections } from './utils/detectDatabaseEnvVars'; // Store for generated environment variables @@ -88,41 +87,38 @@ function translate(content: string, options: TranslateOptions): TranslationResul throw new Error(`Unsupported target language: ${options.target}`); } - // Get provider abbreviation for service connection config lookup - const providerAbbreviation = parser.getInfo().languageAbbreviation; - const containerConfig = getProcessedConfig(content, options.source, { envGeneration: options.environmentVariableGeneration, envVariables: options.environmentVariables, persistenceKey: options.persistenceKey }); - // Process service connections if provided, or auto-detect them + // Process service connections if provided let resolvedServiceConnections; - if (providerConnectionConfigs[providerAbbreviation]) { - // Get the provider-specific connection configuration - const providerConnectionConfig = providerConnectionConfigs[providerAbbreviation]; + if (options.serviceConnections && options.serviceConnections.mappings.length > 0) { + // Use the simplified service connection resolver - no provider-specific transformations + resolvedServiceConnections = resolveServiceConnections( + containerConfig, + options.serviceConnections + ); - // If no service connections provided, try to auto-detect database connections - const serviceConnections = options.serviceConnections || { - mappings: generateDatabaseServiceConnections(containerConfig) - }; + // Add service connections to the container config for parsers to use + containerConfig.serviceConnections = resolvedServiceConnections; + } else if (options.target.toLowerCase() !== 'cfn') { + // Auto-detect database connections if not AWS CloudFormation + // (CloudFormation doesn't support direct service-to-service references) + const autoDetectedMappings = generateDatabaseServiceConnections(containerConfig); - // Only proceed if we have mappings - if (serviceConnections.mappings.length > 0) { - // Resolve service connections based on provider config + if (autoDetectedMappings.length > 0) { + // Process auto-detected connections resolvedServiceConnections = resolveServiceConnections( containerConfig, - serviceConnections, - providerConnectionConfig + { mappings: autoDetectedMappings } ); - // Add service connections to the container config - // to be accessed by parsers that use native reference mechanisms - if (resolvedServiceConnections) { - containerConfig.serviceConnections = resolvedServiceConnections; - } + // Add to container config + containerConfig.serviceConnections = resolvedServiceConnections; } } diff --git a/src/parsers/digitalocean.ts b/src/parsers/digitalocean.ts index 25ccc94..1dc092b 100644 --- a/src/parsers/digitalocean.ts +++ b/src/parsers/digitalocean.ts @@ -69,6 +69,7 @@ class DigitalOceanParser extends BaseParser { const databaseConfig = getDigitalOceanDatabaseType(dockerImageInfo); const normalizedImage = normalizeDigitalOceanImageInfo(dockerImageInfo); + // Prepare base service configuration const baseService = { name: digitalOceanParserServiceName(serviceName), image: { @@ -88,49 +89,41 @@ class DigitalOceanParser extends BaseParser { })) }; - // Check for database references in environment variables - for (let i = 0; i < baseService.envs.length; i++) { - const env = baseService.envs[i]; - - // Look for database URLs with format postgresql://username:password@hostname:port/database - const dbUrlMatch = env.value.match(/postgresql:\/\/.*:.*@(.*?):(.*?)\/(.*)/); - if (dbUrlMatch) { - const targetServiceName = dbUrlMatch[1]; - - // Check if the referenced hostname is a known database service - if (databaseServiceMap.has(targetServiceName)) { - // Update the env var to use the database reference - baseService.envs[i] = { - key: env.key, - value: `\${${databaseServiceMap.get(targetServiceName)}.DATABASE_URL}`, - scope: 'RUN_TIME' - }; - } - } - } - - // Process connections + // Process service connections - this is provider specific logic if (config.serviceConnections) { for (const connection of config.serviceConnections) { if (connection.fromService === serviceName) { - for (const [varName] of Object.entries(connection.variables)) { - // Find the environment variable + for (const [varName, varInfo] of Object.entries(connection.variables)) { + // Find the environment variable index const envIndex = baseService.envs.findIndex(env => env.key === varName); - // Check if target service is a database + // Check if target service is a managed database if (databaseServiceMap.has(connection.toService)) { + const dbServiceName = databaseServiceMap.get(connection.toService)!; + const transformedValue = `\${${dbServiceName}.DATABASE_URL}`; + if (envIndex !== -1) { - // Replace the existing variable with a database reference - baseService.envs[envIndex] = { + // Update existing variable + baseService.envs[envIndex].value = transformedValue; + } else { + // Add new variable + baseService.envs.push({ key: varName, - value: `\${${databaseServiceMap.get(connection.toService)}.DATABASE_URL}`, + value: transformedValue, scope: 'RUN_TIME' - }; + }); + } + } else { + // Regular service connection - not typically used in DigitalOcean but included for completeness + const targetServiceName = digitalOceanParserServiceName(connection.toService); + const transformedValue = `\${${targetServiceName}.PRIVATE_URL}`; + + if (envIndex !== -1) { + baseService.envs[envIndex].value = transformedValue; } else { - // Add a new variable with the database reference baseService.envs.push({ key: varName, - value: `\${${databaseServiceMap.get(connection.toService)}.DATABASE_URL}`, + value: transformedValue, scope: 'RUN_TIME' }); } diff --git a/src/parsers/render.ts b/src/parsers/render.ts index 8b6b34d..68f4449 100644 --- a/src/parsers/render.ts +++ b/src/parsers/render.ts @@ -34,6 +34,7 @@ class RenderParser extends BaseParser { const keyvalueServices: Array = []; const databaseServiceMap = new Map(); + // First pass: Identify database services and register them for (const [serviceName, serviceConfig] of Object.entries(config.services)) { const imageUrl = getImageUrl(constructImageString(serviceConfig.image)); @@ -75,23 +76,39 @@ class RenderParser extends BaseParser { 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 => { - const parsedPort = parsePort(`${portMapping.host}:${portMapping.container}`); - if (parsedPort) { - ports.add(parsedPort); + if (typeof portMapping === 'object' && portMapping !== null) { + ports.add(portMapping.container); + } else { + const parsedPort = 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: any = { name: serviceName, type: getRenderServiceType(serviceConfig.image), @@ -100,111 +117,77 @@ class RenderParser extends BaseParser { startCommand: parseCommand(serviceConfig.command), plan: defaultParserConfig.subscriptionName, region: defaultParserConfig.region, - envVars: Object.entries(environmentVariables).map(([key, value]) => ({ - key, - value: value.toString() - })) + envVars: [] }; - // Process any service connections for Render Blueprint + // 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) { - - for (const [varName] of Object.entries(connection.variables)) { - // Define the type for the env parameter explicitly - const index = service.envVars.findIndex((env: { key: string, value?: string }) => env.key === varName); - - // Remove existing env var if found - if (index > -1) { - service.envVars.splice(index, 1); - } - - // Check if target service is a database + for (const [varName, varInfo] of Object.entries(connection.variables)) { + // Check if the target is a database service if (databaseServiceMap.has(connection.toService)) { - const targetService = databaseServiceMap.get(connection.toService); + const targetServiceName = databaseServiceMap.get(connection.toService); const targetImageUrl = getImageUrl(constructImageString(config.services[connection.toService].image)); - // Check if PostgreSQL (use fromDatabase) or Redis (use fromService) + // Different handling based on database type if (targetImageUrl.includes('postgres')) { - // Add using Render Blueprint fromDatabase syntax for PostgreSQL + // PostgreSQL uses fromDatabase service.envVars.push({ key: varName, fromDatabase: { - name: targetService, // This is already the database name with -db suffix - property: 'connectionString' + name: targetServiceName, // This is the database name with -db suffix + property: connection.property || 'connectionString' } }); } else if (targetImageUrl.includes('redis')) { - // Add using Render Blueprint fromService syntax for Redis + // Redis uses fromService with type: redis service.envVars.push({ key: varName, fromService: { - name: targetService, + name: targetServiceName, type: 'redis', - property: 'connectionString' // Redis connection property + property: connection.property || 'connectionString' } }); } } else { - // Regular service connection with Render Blueprint fromService syntax + // Regular service connection service.envVars.push({ key: varName, fromService: { name: connection.toService, type: getRenderServiceType(config.services[connection.toService].image), - property: 'hostport' // Default to hostport for most connections - } - }); - } - } - } - } - } - - // Add database connection environment variables - for (const [key, value] of Object.entries(environmentVariables)) { - if (typeof value === 'string') { - // Look for database URLs with format postgresql://username:password@hostname:port/database - const dbUrlMatch = value.match(/postgresql:\/\/.*:.*@(.*?):(.*?)\/(.*)/); - if (dbUrlMatch) { - const targetServiceName = dbUrlMatch[1]; - - // Check if the referenced hostname is a known database service - if (databaseServiceMap.has(targetServiceName)) { - const targetService = databaseServiceMap.get(targetServiceName); - const targetImageUrl = getImageUrl(constructImageString(config.services[targetServiceName].image)); - - // Find and remove the original env var - const index = service.envVars.findIndex((env: { key: string }) => env.key === key); - if (index > -1) { - service.envVars.splice(index, 1); - } - - // Check if PostgreSQL or Redis - if (targetImageUrl.includes('postgres')) { - // Add using Render Blueprint fromDatabase syntax for PostgreSQL - service.envVars.push({ - key, - fromDatabase: { - name: targetService, - property: 'connectionString' - } - }); - } else if (targetImageUrl.includes('redis')) { - // Add using Render Blueprint fromService syntax for Redis - service.envVars.push({ - key, - fromService: { - name: targetService, - type: 'redis', - property: 'connectionString' + 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 diff --git a/src/types/service-connections.ts b/src/types/service-connections.ts index 42a04be..4846f95 100644 --- a/src/types/service-connections.ts +++ b/src/types/service-connections.ts @@ -66,6 +66,9 @@ export interface ResolvedServiceConnection { // Target service being referenced toService: string; + // Which property to use for the connection (hostport, connectionString, etc) + property?: string; + // Environment variables that were transformed variables: { [key: string]: ResolvedConnectionVariable; diff --git a/src/utils/resolveServiceConnections.ts b/src/utils/resolveServiceConnections.ts index a5bdcdd..d470dfe 100644 --- a/src/utils/resolveServiceConnections.ts +++ b/src/utils/resolveServiceConnections.ts @@ -1,29 +1,22 @@ import { ApplicationConfig } from '../types/container-config'; import { ServiceConnectionsConfig, - ResolvedServiceConnection, - ProviderConnectionConfig + ResolvedServiceConnection } from '../types/service-connections'; -import { getServiceNameTransformer } from './serviceNameTransformers'; -import { isRenderDatabaseService } from './isRenderDatabaseService'; -import { isDigitalOceanManagedDatabase } from './isDigitalOceanManagedDatabase'; -import { getRenderServiceType } from './getRenderServiceType'; -import { getImageUrl } from './getImageUrl'; -import { constructImageString } from './constructImageString'; -import { getPropertyForProvider } from '../config/connection-properties'; /** - * Replace service references in environment variable values based on provider configuration + * Resolve service connections between components without provider-specific transformations + * This provides basic information about service connections that each parser can use + * to implement its own specific connection syntax */ export function resolveServiceConnections( config: ApplicationConfig, - serviceConnections: ServiceConnectionsConfig, - providerConfig: ProviderConnectionConfig + serviceConnections: ServiceConnectionsConfig ): ResolvedServiceConnection[] { const resolvedConnections: ResolvedServiceConnection[] = []; for (const mapping of serviceConnections.mappings) { - const { fromService, toService, environmentVariables, property } = mapping; + const { fromService, toService, environmentVariables } = mapping; // Skip if services don't exist if (!config.services[fromService] || !config.services[toService]) { @@ -38,28 +31,10 @@ export function resolveServiceConnections( const resolvedConnection: ResolvedServiceConnection = { fromService, toService, + property: mapping.property || 'hostport', // Default property is hostport variables: {} }; - // Determine if target service is a database (based on provider) - const isTargetDatabase = - (providerConfig.implementationType === 'blueprint-reference' && isRenderDatabaseService(config.services[toService].image)) || - (providerConfig.serviceNameTransformer === 'digitalOcean' && isDigitalOceanManagedDatabase(config.services[toService].image)); - - // Get the target service image URL for specific checks - const targetImageUrl = getImageUrl(constructImageString(config.services[toService].image)); - - // Get the requested property or use default - const requestedProperty = property || - (isTargetDatabase ? 'connectionString' : 'hostport'); - - // Get provider-specific property name - const providerProperty = getPropertyForProvider( - requestedProperty, - providerConfig.implementationType === 'blueprint-reference' ? 'render' : 'digitalOcean', - isTargetDatabase - ); - // Process each environment variable that references the target service for (const varName of environmentVariables) { const matchingVarNames = Object.keys(serviceEnv).filter(envKey => @@ -69,97 +44,17 @@ export function resolveServiceConnections( if (matchingVarNames.length > 0) { for (const matchedVarName of matchingVarNames) { const originalValue = serviceEnv[matchedVarName]; - let transformedValue = originalValue; - - // For Render.com with blueprint references - if (providerConfig.implementationType === 'blueprint-reference') { - // We'll handle this by setting a flag and updating at the end of the loop - // as Render uses a different structure than simple string replacement - - // Remove existing environment variable as it will be replaced with a fromService reference - delete config.services[fromService].environment[matchedVarName]; - } - // For DigitalOcean and other providers that use string replacement - else { - // Get the appropriate transformer function - const transformerFn = getServiceNameTransformer(providerConfig.serviceNameTransformer); - - // Transform the service name - const transformedServiceName = transformerFn(toService); - - // Handle different types of references based on service type - if (isTargetDatabase && providerConfig.serviceNameTransformer === 'digitalOcean') { - // For databases on DigitalOcean, use the database reference syntax - transformedValue = `\${${transformedServiceName}-db.${providerProperty}}`; - } else { - // For regular services on DigitalOcean, use the service reference syntax - transformedValue = `\${${transformedServiceName}.${providerProperty}}`; - } - - // Update the environment variable - config.services[fromService].environment[matchedVarName] = transformedValue; - } + // Store the original value but don't transform it here + // Let each parser implement its own transformation resolvedConnection.variables[matchedVarName] = { originalValue, - transformedValue + transformedValue: originalValue // Initially the same, parsers will override this }; } } } - - // For Render.com, add fromService or fromDatabase references directly to the service config - if (providerConfig.implementationType === 'blueprint-reference') { - // Ensure envVars exists - if (!config.services[fromService].envVars) { - config.services[fromService].envVars = []; - } - - // Get the actual service with envVars - const serviceConfig = config.services[fromService] as any; - - for (const varName of environmentVariables) { - // If this is a database target - if (isTargetDatabase) { - // Specifically check for PostgreSQL - if (targetImageUrl.includes('postgres')) { - - // This should be fromDatabase with the postgres-db name - serviceConfig.envVars.push({ - key: varName, - fromDatabase: { - name: `${toService}-db`, // Add -db suffix for database name - property: 'connectionString' // Force connectionString for databases - } - }); - } else if (targetImageUrl.includes('redis')) { - // Redis uses fromService with type: redis - serviceConfig.envVars.push({ - key: varName, - fromService: { - name: toService, - type: 'redis', - property: 'connectionString' - } - }); - } - } else { - console.log(`Adding regular fromService reference for ${varName}`); - // Regular service reference - serviceConfig.envVars.push({ - key: varName, - fromService: { - name: toService, - type: getRenderServiceType(config.services[toService].image), - property: providerProperty - } - }); - } - } - } - - resolvedConnections.push(resolvedConnection); } From 7a18378270870fdc4a9d1bb2c7b77ec0d8819d2c Mon Sep 17 00:00:00 2001 From: Lasim Date: Fri, 11 Apr 2025 22:04:20 +0200 Subject: [PATCH 4/4] fix: suppress eslint warnings for unused variables in service connection handling --- src/parsers/digitalocean.ts | 1 + src/parsers/render.ts | 1 + src/utils/detectDatabaseEnvVars.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/parsers/digitalocean.ts b/src/parsers/digitalocean.ts index 1dc092b..03d334f 100644 --- a/src/parsers/digitalocean.ts +++ b/src/parsers/digitalocean.ts @@ -93,6 +93,7 @@ class DigitalOceanParser extends BaseParser { if (config.serviceConnections) { 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)) { // Find the environment variable index const envIndex = baseService.envs.findIndex(env => env.key === varName); diff --git a/src/parsers/render.ts b/src/parsers/render.ts index 68f4449..45343cf 100644 --- a/src/parsers/render.ts +++ b/src/parsers/render.ts @@ -141,6 +141,7 @@ class RenderParser extends BaseParser { // 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)) { diff --git a/src/utils/detectDatabaseEnvVars.ts b/src/utils/detectDatabaseEnvVars.ts index d613728..6e1ce46 100644 --- a/src/utils/detectDatabaseEnvVars.ts +++ b/src/utils/detectDatabaseEnvVars.ts @@ -2,6 +2,7 @@ * Automatically creates service connection configurations based on environment variables */ export function generateDatabaseServiceConnections( + // eslint-disable-next-line @typescript-eslint/no-unused-vars config: { services: Record }> } ): Array<{ fromService: string, toService: string, environmentVariables: string[] }> { // Return an empty array - no auto-detection