@deploystack/docker-to-iac
Version:
Translate docker run and docker compose file to Infrastructure as Code
621 lines (610 loc) • 22.4 kB
JavaScript
"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 helmDatabaseConnections_1 = require("../utils/helmDatabaseConnections");
const defaultParserConfig = {
files: [
{
path: 'Chart.yaml',
templateFormat: base_parser_1.TemplateFormat.yaml,
isMain: true,
description: 'Helm Chart definition file'
},
{
path: 'values.yaml',
templateFormat: base_parser_1.TemplateFormat.yaml,
isMain: false,
description: 'Helm Chart values configuration'
},
{
path: 'templates/deployment.yaml',
templateFormat: base_parser_1.TemplateFormat.yaml,
isMain: false,
description: 'Kubernetes Deployment template'
},
{
path: 'templates/service.yaml',
templateFormat: base_parser_1.TemplateFormat.yaml,
isMain: false,
description: 'Kubernetes Service template'
},
{
path: 'templates/configmap.yaml',
templateFormat: base_parser_1.TemplateFormat.yaml,
isMain: false,
description: 'Kubernetes ConfigMap template for environment variables'
},
{
path: 'templates/secret.yaml',
templateFormat: base_parser_1.TemplateFormat.yaml,
isMain: false,
description: 'Kubernetes Secret template for sensitive data'
}
],
cpu: '100m',
memory: '128Mi'
};
class HelmParser extends base_parser_1.BaseParser {
parse(config, templateFormat = base_parser_1.TemplateFormat.yaml) {
return super.parse(config, templateFormat);
}
parseFiles(config) {
const files = {};
// Auto-detect database connections if not already provided
if (!config.serviceConnections) {
const detectedConnections = (0, helmDatabaseConnections_1.detectHelmDatabaseConnections)(config);
if (detectedConnections.length > 0) {
// Transform the detected connections into ResolvedServiceConnection objects
config.serviceConnections = detectedConnections.map(conn => ({
fromService: conn.fromService,
toService: conn.toService,
property: 'hostport', // Default property
variables: conn.environmentVariables.reduce((obj, varName) => {
obj[varName] = {
originalValue: config.services[conn.fromService].environment[varName],
transformedValue: config.services[conn.fromService].environment[varName]
};
return obj;
}, {})
}));
}
}
// Process Chart.yaml - the main chart definition with dependencies
const chartYaml = this.generateChartYaml(config);
files['Chart.yaml'] = {
content: this.formatFileContent(chartYaml, base_parser_1.TemplateFormat.yaml),
format: base_parser_1.TemplateFormat.yaml,
isMain: true
};
// Process values.yaml
const valuesYaml = this.generateValuesYaml(config);
files['values.yaml'] = {
content: this.formatFileContent(valuesYaml, base_parser_1.TemplateFormat.yaml),
format: base_parser_1.TemplateFormat.yaml,
isMain: false
};
// Process template files
files['templates/deployment.yaml'] = {
content: this.formatFileContent(this.generateDeploymentTemplate(), base_parser_1.TemplateFormat.yaml),
format: base_parser_1.TemplateFormat.yaml,
isMain: false
};
files['templates/service.yaml'] = {
content: this.formatFileContent(this.generateServiceTemplate(), base_parser_1.TemplateFormat.yaml),
format: base_parser_1.TemplateFormat.yaml,
isMain: false
};
files['templates/configmap.yaml'] = {
content: this.formatFileContent(this.generateConfigMapTemplate(), base_parser_1.TemplateFormat.yaml),
format: base_parser_1.TemplateFormat.yaml,
isMain: false
};
files['templates/secret.yaml'] = {
content: this.formatFileContent(this.generateSecretTemplate(), base_parser_1.TemplateFormat.yaml),
format: base_parser_1.TemplateFormat.yaml,
isMain: false
};
// Add NOTES.txt file
files['templates/NOTES.txt'] = {
content: this.generateNotesTemplate(),
format: base_parser_1.TemplateFormat.text,
isMain: false
};
// Add _helpers.tpl file
files['templates/_helpers.tpl'] = {
content: this.generateHelpersTemplate(),
format: base_parser_1.TemplateFormat.text,
isMain: false
};
return files;
}
generateChartYaml(config) {
const chart = {
apiVersion: 'v2',
name: 'deploystack-app',
description: 'A Helm chart for DeployStack application generated from Docker configuration',
type: 'application',
version: '0.1.0',
appVersion: '1.0.0',
maintainers: [
{
name: 'DeployStack',
email: 'hello@deploystack.io'
}
],
dependencies: []
};
// Add dependencies for database services if config is provided
if (config) {
for (const [serviceName, serviceConfig] of Object.entries(config.services)) {
if ((0, helmDatabaseConnections_1.isHelmDatabaseImage)(serviceConfig.image)) {
// Get database config from our util
const dbConfig = (0, helmDatabaseConnections_1.processHelmDatabaseConfig)(serviceName, serviceConfig);
if (dbConfig) {
chart.dependencies.push({
name: serviceName,
repository: dbConfig.repository || 'https://charts.bitnami.com/bitnami',
version: dbConfig.version || '^1.0.0',
condition: `dependencies.${serviceName}.enabled`
});
}
}
}
}
// Remove empty dependencies array if no dependencies
if (chart.dependencies.length === 0) {
chart.dependencies = [];
}
return chart;
}
generateValuesYaml(config) {
const values = {
global: {
nameOverride: '',
fullnameOverride: '',
imagePullSecrets: [],
podAnnotations: {},
podSecurityContext: {},
securityContext: {}
},
services: {},
dependencies: {}
};
// Track database services that should be managed with dependency charts
const managedDatabases = new Map();
// First pass: identify database services that should use Helm charts
for (const [serviceName, serviceConfig] of Object.entries(config.services)) {
if ((0, helmDatabaseConnections_1.isHelmDatabaseImage)(serviceConfig.image)) {
// Process database config using our utility
const dbValues = (0, helmDatabaseConnections_1.processHelmDatabaseConfig)(serviceName, serviceConfig);
if (dbValues) {
// Register this as a managed database
managedDatabases.set(serviceName, dbValues);
// Add the dependency to values
values.dependencies[serviceName] = {
enabled: true,
...dbValues
};
// Skip this service in the regular services loop
continue;
}
}
}
// Process each service in the docker compose
for (const [serviceName, serviceConfig] of Object.entries(config.services)) {
// Skip managed databases
if (managedDatabases.has(serviceName)) {
continue;
}
const ports = new Set();
if (serviceConfig.ports) {
serviceConfig.ports.forEach(port => {
if (typeof port === 'object' && port !== null) {
ports.add(port.container);
}
else {
const parsedPort = (0, parsePort_1.parsePort)(port);
if (parsedPort) {
ports.add(parsedPort);
}
}
});
}
// Generate values for this service
const serviceValues = {
enabled: true,
replicaCount: 1,
image: {
// Pass false to includeTag parameter to get repository without the tag
repository: (0, getImageUrl_1.getImageUrl)((0, constructImageString_1.constructImageString)(serviceConfig.image), false),
pullPolicy: 'IfNotPresent',
tag: serviceConfig.image.tag || 'latest'
},
service: {
type: 'ClusterIP',
ports: Array.from(ports).map(port => ({
port: port,
targetPort: port,
protocol: 'TCP'
}))
},
environment: this.categorizeEnvironmentVariables(serviceConfig.environment),
resources: {
limits: {
cpu: '500m',
memory: '512Mi'
},
requests: {
cpu: defaultParserConfig.cpu,
memory: defaultParserConfig.memory
}
},
autoscaling: {
enabled: false,
minReplicas: 1,
maxReplicas: 10,
targetCPUUtilizationPercentage: 80
},
volumes: serviceConfig.volumes.map(volume => ({
name: this.sanitizeKubernetesName(`${serviceName}-${volume.container.replace(/\//g, '-')}`),
mountPath: volume.container,
hostPath: volume.host
}))
};
// Only add the command if it's not empty
const cmdString = (0, parseCommand_1.parseCommand)(serviceConfig.command);
if (cmdString) {
serviceValues.command = cmdString.split(' ');
}
// Process service connections if provided
if (config.serviceConnections) {
serviceValues.dependencies = [];
for (const connection of config.serviceConnections) {
if (connection.fromService === serviceName) {
// Check if the target is a managed database
const targetService = connection.toService;
const isTargetManagedDb = managedDatabases.has(targetService);
serviceValues.dependencies.push({
service: targetService,
variables: connection.variables,
isDatabaseDependency: isTargetManagedDb
});
}
}
}
values.services[serviceName] = serviceValues;
}
return values;
}
/**
* Categorizes environment variables into regular and sensitive
* This allows Helm to separate them into ConfigMaps and Secrets
*/
categorizeEnvironmentVariables(environment) {
const sensitive = {};
const regular = {};
const sensitiveKeywords = ['password', 'secret', 'key', 'token', 'auth'];
for (const [key, value] of Object.entries(environment)) {
const lowerKey = key.toLowerCase();
const isSensitive = sensitiveKeywords.some(keyword => lowerKey.includes(keyword));
if (isSensitive) {
sensitive[key] = value;
}
else {
regular[key] = value;
}
}
return { regular, sensitive };
}
/**
* Sanitizes a string to be used as a Kubernetes resource name
*/
sanitizeKubernetesName(name) {
return name
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.substring(0, 63);
}
/**
* Generates the Kubernetes Deployment template for Helm
*/
generateDeploymentTemplate() {
return `
{{- range $key, $service := .Values.services }}
{{- if $service.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "deploystack.fullname" $ }}-{{ $key }}
labels:
{{- include "deploystack.labels" $ | nindent 4 }}
app.kubernetes.io/component: {{ $key }}
spec:
{{- if not $service.autoscaling.enabled }}
replicas: {{ $service.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "deploystack.selectorLabels" $ | nindent 6 }}
app.kubernetes.io/component: {{ $key }}
template:
metadata:
{{- with $.Values.global.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "deploystack.selectorLabels" $ | nindent 8 }}
app.kubernetes.io/component: {{ $key }}
spec:
{{- with $.Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml $.Values.global.podSecurityContext | nindent 8 }}
containers:
- name: {{ $key }}
securityContext:
{{- toYaml $.Values.global.securityContext | nindent 12 }}
image: "{{ $service.image.repository }}:{{ $service.image.tag | default "latest" }}"
imagePullPolicy: {{ $service.image.pullPolicy }}
{{- if $service.command }}
command:
{{- toYaml $service.command | nindent 12 }}
{{- end }}
ports:
{{- range $service.service.ports }}
- name: http-{{ .port }}
containerPort: {{ .targetPort }}
protocol: {{ .protocol }}
{{- end }}
{{- if or $service.environment.regular $service.environment.sensitive }}
env:
{{- range $envKey, $envValue := $service.environment.regular }}
- name: {{ $envKey }}
valueFrom:
configMapKeyRef:
name: {{ include "deploystack.fullname" $ }}-{{ $key }}-config
key: {{ $envKey }}
{{- end }}
{{- range $envKey, $envValue := $service.environment.sensitive }}
- name: {{ $envKey }}
valueFrom:
secretKeyRef:
name: {{ include "deploystack.fullname" $ }}-{{ $key }}-secret
key: {{ $envKey }}
{{- end }}
{{- if $service.dependencies }}
{{- range $service.dependencies }}
{{- range $envKey, $envValue := .variables }}
- name: {{ $envKey }}
valueFrom:
configMapKeyRef:
name: {{ include "deploystack.fullname" $ }}-{{ $key }}-dependencies
key: {{ $envKey }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- with $service.volumes }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml $service.resources | nindent 12 }}
{{- with $service.volumes }}
volumes:
{{- range . }}
- name: {{ .name }}
hostPath:
path: {{ .hostPath }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
`;
}
/**
* Generates the Kubernetes Service template for Helm
*/
generateServiceTemplate() {
return `
{{- range $key, $service := .Values.services }}
{{- if $service.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "deploystack.fullname" $ }}-{{ $key }}
labels:
{{- include "deploystack.labels" $ | nindent 4 }}
app.kubernetes.io/component: {{ $key }}
spec:
type: {{ $service.service.type }}
ports:
{{- range $service.service.ports }}
- port: {{ .port }}
targetPort: {{ .targetPort }}
protocol: {{ .protocol }}
name: http-{{ .port }}
{{- end }}
selector:
{{- include "deploystack.selectorLabels" $ | nindent 4 }}
app.kubernetes.io/component: {{ $key }}
{{- end }}
{{- end }}
`;
}
/**
* Generates the Kubernetes ConfigMap template for Helm
*/
generateConfigMapTemplate() {
return `
{{- range $key, $service := .Values.services }}
{{- if $service.enabled }}
{{- if $service.environment.regular }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "deploystack.fullname" $ }}-{{ $key }}-config
labels:
{{- include "deploystack.labels" $ | nindent 4 }}
app.kubernetes.io/component: {{ $key }}
data:
{{- toYaml $service.environment.regular | nindent 2 }}
{{- end }}
{{- if $service.dependencies }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "deploystack.fullname" $ }}-{{ $key }}-dependencies
labels:
{{- include "deploystack.labels" $ | nindent 4 }}
app.kubernetes.io/component: {{ $key }}
data:
{{- range $service.dependencies }}
{{- $serviceName := .service }}
{{- range $envKey, $envValue := .variables }}
{{ $envKey }}: {{ include "deploystack.serviceReference" (dict "service" (index $.Values.services $serviceName) "serviceKey" $serviceName) }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
`;
}
/**
* Generates the Kubernetes Secret template for Helm
*/
generateSecretTemplate() {
return `
{{- range $key, $service := .Values.services }}
{{- if $service.enabled }}
{{- if $service.environment.sensitive }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "deploystack.fullname" $ }}-{{ $key }}-secret
labels:
{{- include "deploystack.labels" $ | nindent 4 }}
app.kubernetes.io/component: {{ $key }}
type: Opaque
data:
{{- range $envKey, $envValue := $service.environment.sensitive }}
{{ $envKey }}: {{ $envValue | b64enc | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
`;
}
/**
* Generates the NOTES.txt template for Helm
*/
generateNotesTemplate() {
return `
Thank you for installing {{ .Chart.Name }}.
Your application has been deployed successfully!
{{- range $key, $service := .Values.services }}
{{- if and $service.enabled $service.service.ports }}
Service {{ $key }}:
{{- if contains "NodePort" $service.service.type }}
Access on: http://NODE_IP:NODE_PORT
Get the NodePort by running: kubectl get svc {{ include "deploystack.fullname" $ }}-{{ $key }}
{{- else if contains "LoadBalancer" $service.service.type }}
Access on: http://EXTERNAL_IP:PORT
Get the LoadBalancer IP by running: kubectl get svc {{ include "deploystack.fullname" $ }}-{{ $key }}
{{- else if contains "ClusterIP" $service.service.type }}
Access within the cluster only
Forward ports by running: kubectl port-forward svc/{{ include "deploystack.fullname" $ }}-{{ $key }} LOCAL_PORT:SERVICE_PORT
{{- end }}
{{- end }}
{{- end }}
For more information on managing your application, see the Helm documentation:
https://helm.sh/docs/
`;
}
/**
* Generates the _helpers.tpl template for Helm
*/
generateHelpersTemplate() {
return `
{{/*
Expand the name of the chart.
*/}}
{{- define "deploystack.name" -}}
{{- default .Chart.Name .Values.global.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "deploystack.fullname" -}}
{{- if .Values.global.fullnameOverride }}
{{- .Values.global.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.global.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "deploystack.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "deploystack.labels" -}}
helm.sh/chart: {{ include "deploystack.chart" . }}
{{ include "deploystack.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "deploystack.selectorLabels" -}}
app.kubernetes.io/name: {{ include "deploystack.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Service reference helper
*/}}
{{- define "deploystack.serviceReference" -}}
{{- if .service }}
{{- if .service.service.ports }}
{{- with index .service.service.ports 0 }}
{{ $.serviceKey }}.{{ $.serviceName }}.svc.cluster.local:{{ .port }}
{{- end }}
{{- else }}
{{ $.serviceKey }}.{{ $.serviceName }}.svc.cluster.local
{{- end }}
{{- else }}
unknown-service-reference
{{- end }}
{{- end }}
`;
}
getInfo() {
return {
providerWebsite: 'https://helm.sh/',
providerName: 'Kubernetes',
providerNameAbbreviation: 'K8S',
languageOfficialDocs: 'https://helm.sh/docs/',
languageAbbreviation: 'HELM',
languageName: 'Helm Chart',
defaultParserConfig: defaultParserConfig
};
}
}
exports.default = new HelmParser();