Skip to content

Commit 44e959a

Browse files
authored
Merge pull request #205 from deploystackio/feature/database
Feature/database
2 parents 6f7725e + 7a18378 commit 44e959a

13 files changed

Lines changed: 401 additions & 97 deletions
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
export interface PropertyMapping {
2+
render: string;
3+
digitalOcean: string;
4+
}
5+
6+
export const servicePropertyMappings: Record<string, PropertyMapping> = {
7+
'host': {
8+
render: 'host',
9+
digitalOcean: 'PRIVATE_DOMAIN'
10+
},
11+
'port': {
12+
render: 'port',
13+
digitalOcean: 'PRIVATE_PORT'
14+
},
15+
'hostport': {
16+
render: 'hostport',
17+
digitalOcean: 'PRIVATE_URL'
18+
}
19+
};
20+
21+
export const databasePropertyMappings: Record<string, PropertyMapping> = {
22+
'connectionString': {
23+
render: 'connectionString',
24+
digitalOcean: 'DATABASE_URL'
25+
},
26+
'username': {
27+
render: 'user',
28+
digitalOcean: 'USERNAME'
29+
},
30+
'password': {
31+
render: 'password',
32+
digitalOcean: 'PASSWORD'
33+
},
34+
'databaseName': {
35+
render: 'database',
36+
digitalOcean: 'DATABASE'
37+
}
38+
};
39+
40+
/**
41+
* Gets the correct property name for a specific provider
42+
*
43+
* @param property - The generic property name
44+
* @param provider - The target provider
45+
* @param isDatabase - Whether this is a database property
46+
* @returns The provider-specific property name
47+
*/
48+
export function getPropertyForProvider(
49+
property: string,
50+
provider: 'render' | 'digitalOcean',
51+
isDatabase: boolean
52+
): string {
53+
const mappings = isDatabase ? databasePropertyMappings : servicePropertyMappings;
54+
55+
if (!mappings[property]) {
56+
console.warn(`Unknown property: ${property}. Using as-is.`);
57+
return property;
58+
}
59+
60+
return mappings[property][provider] || property;
61+
}

src/config/digitalocean/database-types.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ interface DatabaseConfig {
22
engine: string;
33
description: string;
44
portNumber: number;
5+
isManaged?: boolean;
56
}
67

78
interface DigitalOceanDatabaseConfig {
@@ -24,12 +25,13 @@ export const digitalOceanDatabaseConfig: DigitalOceanDatabaseConfig = {
2425
},
2526
'docker.io/library/postgres': {
2627
engine: 'PG',
27-
description: 'PostgreSQL database service - requires managed database service due to TCP protocol',
28-
portNumber: 5432
28+
description: 'PostgreSQL database service - creates a managed database instance',
29+
portNumber: 5432,
30+
isManaged: true
2931
},
3032
'docker.io/library/redis': {
3133
engine: 'REDIS',
32-
description: 'Redis database service - requires managed database service due to TCP protocol',
34+
description: 'Redis database service - creates a managed database instance',
3335
portNumber: 6379
3436
},
3537
'docker.io/library/mongodb': {

src/config/render/service-types.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ interface RenderServiceTypeConfig {
22
type: string;
33
description: string;
44
versions: string;
5+
isManaged?: boolean;
56
}
67

78
interface RenderServiceTypesConfig {
@@ -23,12 +24,18 @@ export const renderServiceTypesConfig: RenderServiceTypesConfig = {
2324
versions: '*'
2425
},
2526
'docker.io/library/postgres': {
26-
type: 'pserv',
27-
description: 'PostgreSQL database service - requires private service type due to TCP protocol',
28-
versions: '*'
27+
type: 'database',
28+
description: 'PostgreSQL database - creates a managed database in databases section',
29+
versions: '*',
30+
isManaged: true
31+
},
32+
'docker.io/library/redis': {
33+
type: 'redis',
34+
description: 'Redis database - creates a keyvalue service with type: redis',
35+
versions: '*',
36+
isManaged: true
2937
}
3038
}
3139
};
3240

33-
// Export types for use in other files
34-
export type { RenderServiceTypeConfig, RenderServiceTypesConfig };
41+
export type { RenderServiceTypeConfig, RenderServiceTypesConfig };

src/config/service-connections.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/index.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import digitalOceanParserInstance from './parsers/digitalocean';
88
import { createSourceParser } from './sources/factory';
99
import { parseEnvFile } from './utils/parseEnvFile';
1010
import { resolveServiceConnections } from './utils/resolveServiceConnections';
11-
import { providerConnectionConfigs } from './config/service-connections';
11+
import { generateDatabaseServiceConnections } from './utils/detectDatabaseEnvVars';
1212

1313
// Store for generated environment variables
1414
const generatedEnvVars = new Map<string, Record<string, Record<string, string>>>();
@@ -87,9 +87,6 @@ function translate(content: string, options: TranslateOptions): TranslationResul
8787
throw new Error(`Unsupported target language: ${options.target}`);
8888
}
8989

90-
// Get provider abbreviation for service connection config lookup
91-
const providerAbbreviation = parser.getInfo().languageAbbreviation;
92-
9390
const containerConfig = getProcessedConfig(content, options.source, {
9491
envGeneration: options.environmentVariableGeneration,
9592
envVariables: options.environmentVariables,
@@ -98,20 +95,29 @@ function translate(content: string, options: TranslateOptions): TranslationResul
9895

9996
// Process service connections if provided
10097
let resolvedServiceConnections;
101-
if (options.serviceConnections && providerConnectionConfigs[providerAbbreviation]) {
102-
// Get the provider-specific connection configuration
103-
const providerConnectionConfig = providerConnectionConfigs[providerAbbreviation];
104-
105-
// Resolve service connections based on provider config
98+
99+
if (options.serviceConnections && options.serviceConnections.mappings.length > 0) {
100+
// Use the simplified service connection resolver - no provider-specific transformations
106101
resolvedServiceConnections = resolveServiceConnections(
107102
containerConfig,
108-
options.serviceConnections,
109-
providerConnectionConfig
103+
options.serviceConnections
110104
);
111105

112-
// Add service connections to the container config
113-
// to be accessed by parsers that use native reference mechanisms
114-
if (resolvedServiceConnections) {
106+
// Add service connections to the container config for parsers to use
107+
containerConfig.serviceConnections = resolvedServiceConnections;
108+
} else if (options.target.toLowerCase() !== 'cfn') {
109+
// Auto-detect database connections if not AWS CloudFormation
110+
// (CloudFormation doesn't support direct service-to-service references)
111+
const autoDetectedMappings = generateDatabaseServiceConnections(containerConfig);
112+
113+
if (autoDetectedMappings.length > 0) {
114+
// Process auto-detected connections
115+
resolvedServiceConnections = resolveServiceConnections(
116+
containerConfig,
117+
{ mappings: autoDetectedMappings }
118+
);
119+
120+
// Add to container config
115121
containerConfig.serviceConnections = resolvedServiceConnections;
116122
}
117123
}

src/parsers/digitalocean.ts

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { parseCommand } from '../utils/parseCommand';
55
import { digitalOceanParserServiceName } from '../utils/digitalOceanParserServiceName';
66
import { normalizeDigitalOceanImageInfo } from '../utils/normalizeDigitalOceanImageInfo';
77
import { getDigitalOceanDatabaseType } from '../utils/getDigitalOceanDatabaseType';
8+
import { isDigitalOceanManagedDatabase } from '../utils/isDigitalOceanManagedDatabase';
89

910
const defaultParserConfig: ParserConfig = {
1011
files: [
@@ -36,13 +37,39 @@ class DigitalOceanParser extends BaseParser {
3637

3738
parseFiles(config: ApplicationConfig): { [path: string]: FileOutput } {
3839
const services: Array<any> = [];
40+
const databases: Array<any> = [];
41+
const databaseServiceMap = new Map<string, string>();
3942
let isFirstService = true;
4043

44+
// First pass: identify database services that should be managed databases
4145
for (const [serviceName, serviceConfig] of Object.entries(config.services)) {
46+
if (isDigitalOceanManagedDatabase(serviceConfig.image)) {
47+
// Create a database entry
48+
const dbName = digitalOceanParserServiceName(`${serviceName}-db`);
49+
50+
// Track the mapping between service name and database name
51+
databaseServiceMap.set(serviceName, dbName);
52+
53+
// Get database config
54+
const dbConfig = getDigitalOceanDatabaseType(serviceConfig.image);
55+
56+
// Create database config object with proper typing
57+
const dbEntry: any = {
58+
name: dbName,
59+
engine: dbConfig?.engine || 'PG' // Default to PostgreSQL if unknown
60+
};
61+
62+
databases.push(dbEntry);
63+
64+
// Skip creating a service for this database
65+
continue;
66+
}
67+
4268
const dockerImageInfo = serviceConfig.image;
4369
const databaseConfig = getDigitalOceanDatabaseType(dockerImageInfo);
4470
const normalizedImage = normalizeDigitalOceanImageInfo(dockerImageInfo);
4571

72+
// Prepare base service configuration
4673
const baseService = {
4774
name: digitalOceanParserServiceName(serviceName),
4875
image: {
@@ -57,12 +84,58 @@ class DigitalOceanParser extends BaseParser {
5784
envs: Object.entries(serviceConfig.environment)
5885
.map(([key, value]) => ({
5986
key,
60-
value: value.toString()
87+
value: value.toString(),
88+
scope: 'RUN_TIME'
6189
}))
6290
};
6391

64-
if (databaseConfig) {
65-
// Database/TCP service configuration
92+
// Process service connections - this is provider specific logic
93+
if (config.serviceConnections) {
94+
for (const connection of config.serviceConnections) {
95+
if (connection.fromService === serviceName) {
96+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
97+
for (const [varName, varInfo] of Object.entries(connection.variables)) {
98+
// Find the environment variable index
99+
const envIndex = baseService.envs.findIndex(env => env.key === varName);
100+
101+
// Check if target service is a managed database
102+
if (databaseServiceMap.has(connection.toService)) {
103+
const dbServiceName = databaseServiceMap.get(connection.toService)!;
104+
const transformedValue = `\${${dbServiceName}.DATABASE_URL}`;
105+
106+
if (envIndex !== -1) {
107+
// Update existing variable
108+
baseService.envs[envIndex].value = transformedValue;
109+
} else {
110+
// Add new variable
111+
baseService.envs.push({
112+
key: varName,
113+
value: transformedValue,
114+
scope: 'RUN_TIME'
115+
});
116+
}
117+
} else {
118+
// Regular service connection - not typically used in DigitalOcean but included for completeness
119+
const targetServiceName = digitalOceanParserServiceName(connection.toService);
120+
const transformedValue = `\${${targetServiceName}.PRIVATE_URL}`;
121+
122+
if (envIndex !== -1) {
123+
baseService.envs[envIndex].value = transformedValue;
124+
} else {
125+
baseService.envs.push({
126+
key: varName,
127+
value: transformedValue,
128+
scope: 'RUN_TIME'
129+
});
130+
}
131+
}
132+
}
133+
}
134+
}
135+
}
136+
137+
if (databaseConfig && !isDigitalOceanManagedDatabase(dockerImageInfo)) {
138+
// Non-managed database/TCP service configuration
66139
services.push({
67140
...baseService,
68141
health_check: {
@@ -96,14 +169,19 @@ class DigitalOceanParser extends BaseParser {
96169
}
97170
}
98171

99-
const digitalOceanConfig = {
172+
const digitalOceanConfig: any = {
100173
spec: {
101174
name: 'deploystack',
102175
region: defaultParserConfig.region,
103176
services
104177
}
105178
};
106179

180+
// Add databases section if we have any
181+
if (databases.length > 0) {
182+
digitalOceanConfig.spec.databases = databases;
183+
}
184+
107185
return {
108186
'.do/deploy.template.yaml': {
109187
content: this.formatFileContent(digitalOceanConfig, TemplateFormat.yaml),

0 commit comments

Comments
 (0)