Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/config/connection-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export interface PropertyMapping {
render: string;
digitalOcean: string;
}

export const servicePropertyMappings: Record<string, PropertyMapping> = {
'host': {
render: 'host',
digitalOcean: 'PRIVATE_DOMAIN'
},
'port': {
render: 'port',
digitalOcean: 'PRIVATE_PORT'
},
'hostport': {
render: 'hostport',
digitalOcean: 'PRIVATE_URL'
}
};

export const databasePropertyMappings: Record<string, PropertyMapping> = {
'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;
}
8 changes: 5 additions & 3 deletions src/config/digitalocean/database-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ interface DatabaseConfig {
engine: string;
description: string;
portNumber: number;
isManaged?: boolean;
}

interface DigitalOceanDatabaseConfig {
Expand All @@ -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': {
Expand Down
17 changes: 12 additions & 5 deletions src/config/render/service-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ interface RenderServiceTypeConfig {
type: string;
description: string;
versions: string;
isManaged?: boolean;
}

interface RenderServiceTypesConfig {
Expand All @@ -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 };
export type { RenderServiceTypeConfig, RenderServiceTypesConfig };
19 changes: 0 additions & 19 deletions src/config/service-connections.ts

This file was deleted.

34 changes: 20 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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
const generatedEnvVars = new Map<string, Record<string, Record<string, string>>>();
Expand Down Expand Up @@ -87,9 +87,6 @@ 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,
Expand All @@ -98,20 +95,29 @@ function translate(content: string, options: TranslateOptions): TranslationResul

// Process service connections if provided
let resolvedServiceConnections;
if (options.serviceConnections && providerConnectionConfigs[providerAbbreviation]) {
// Get the provider-specific connection configuration
const providerConnectionConfig = providerConnectionConfigs[providerAbbreviation];

// Resolve service connections based on provider config

if (options.serviceConnections && options.serviceConnections.mappings.length > 0) {
// Use the simplified service connection resolver - no provider-specific transformations
resolvedServiceConnections = resolveServiceConnections(
containerConfig,
options.serviceConnections,
providerConnectionConfig
options.serviceConnections
);

// Add service connections to the container config
// to be accessed by parsers that use native reference mechanisms
if (resolvedServiceConnections) {
// 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);

if (autoDetectedMappings.length > 0) {
// Process auto-detected connections
resolvedServiceConnections = resolveServiceConnections(
containerConfig,
{ mappings: autoDetectedMappings }
);

// Add to container config
containerConfig.serviceConnections = resolvedServiceConnections;
}
}
Expand Down
86 changes: 82 additions & 4 deletions src/parsers/digitalocean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -36,13 +37,39 @@ class DigitalOceanParser extends BaseParser {

parseFiles(config: ApplicationConfig): { [path: string]: FileOutput } {
const services: Array<any> = [];
const databases: Array<any> = [];
const databaseServiceMap = new Map<string, string>();
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);

// Prepare base service configuration
const baseService = {
name: digitalOceanParserServiceName(serviceName),
image: {
Expand All @@ -57,12 +84,58 @@ 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
// Process service connections - this is provider specific logic
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);

// 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) {
// Update existing variable
baseService.envs[envIndex].value = transformedValue;
} else {
// Add new variable
baseService.envs.push({
key: varName,
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 {
baseService.envs.push({
key: varName,
value: transformedValue,
scope: 'RUN_TIME'
});
}
}
}
}
}
}

if (databaseConfig && !isDigitalOceanManagedDatabase(dockerImageInfo)) {
// Non-managed database/TCP service configuration
services.push({
...baseService,
health_check: {
Expand Down Expand Up @@ -96,14 +169,19 @@ class DigitalOceanParser extends BaseParser {
}
}

const digitalOceanConfig = {
const digitalOceanConfig: any = {
spec: {
name: 'deploystack',
region: defaultParserConfig.region,
services
}
};

// 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),
Expand Down
Loading