UNPKG

@gati-framework/cli

Version:

CLI tool for Gati framework - create, develop, build and deploy cloud-native applications

489 lines 14.7 kB
/** * @module runtime/deployment/kubernetes * @description Kubernetes deployment manifest generator for Gati applications */ import * as ejs from 'ejs'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; // Template paths (ESM compatibility) const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const TEMPLATES_DIR = join(__dirname, 'templates'); const DOCKERFILE_TEMPLATE = join(TEMPLATES_DIR, 'Dockerfile.ejs'); const DEPLOYMENT_TEMPLATE = join(TEMPLATES_DIR, 'deployment.yaml.ejs'); const SERVICE_TEMPLATE = join(TEMPLATES_DIR, 'service.yaml.ejs'); const HPA_TEMPLATE = join(TEMPLATES_DIR, 'hpa.yaml.ejs'); const INGRESS_TEMPLATE = join(TEMPLATES_DIR, 'ingress.yaml.ejs'); const HELM_CHART_TEMPLATE = join(TEMPLATES_DIR, 'helm', 'Chart.yaml.ejs'); const HELM_VALUES_TEMPLATE = join(TEMPLATES_DIR, 'helm', 'values.yaml.ejs'); /** * Generate Dockerfile for Gati application * * @param config - Dockerfile configuration * @returns Generated Dockerfile content * * @example * ```typescript * const dockerfile = generateDockerfile({ * nodeVersion: '20', * appName: 'my-gati-app', * port: 3000, * startCommand: 'pnpm start' * }); * ``` */ export function generateDockerfile(config) { const template = readFileSync(DOCKERFILE_TEMPLATE, 'utf-8'); const data = { nodeVersion: config.nodeVersion, appName: config.appName, port: config.port, buildCommand: config.buildCommand, startCommand: config.startCommand, additionalDependencies: config.additionalDependencies || [], buildArgs: config.buildArgs || {}, }; return ejs.render(template, data); } /** * Generate Kubernetes Deployment manifest * * @param config - Deployment configuration * @returns Generated Deployment YAML * * @example * ```typescript * const deployment = generateDeployment({ * name: 'my-app', * namespace: 'default', * replicas: 3, * image: 'my-app:latest', * imagePullPolicy: 'IfNotPresent', * containerPort: 3000, * env: [], * resources: { * limits: { cpu: '1000m', memory: '512Mi' }, * requests: { cpu: '500m', memory: '256Mi' } * } * }); * ``` */ export function generateDeployment(config) { const template = readFileSync(DEPLOYMENT_TEMPLATE, 'utf-8'); // Add default probes if not provided const data = { ...config, livenessProbe: config.livenessProbe || { path: '/health', port: config.containerPort, initialDelaySeconds: 30, periodSeconds: 10, timeoutSeconds: 5, successThreshold: 1, failureThreshold: 3, }, readinessProbe: config.readinessProbe || { path: '/health', port: config.containerPort, initialDelaySeconds: 15, periodSeconds: 10, timeoutSeconds: 5, successThreshold: 1, failureThreshold: 3, }, }; return ejs.render(template, data); } /** * Generate Kubernetes Service manifest * * @param config - Service configuration * @returns Generated Service YAML * * @example * ```typescript * const service = generateService({ * name: 'my-app', * namespace: 'default', * type: 'LoadBalancer', * port: 80, * targetPort: 3000, * selector: { app: 'my-app' } * }); * ``` */ export function generateService(config) { const template = readFileSync(SERVICE_TEMPLATE, 'utf-8'); return ejs.render(template, config); } /** * Generate Kubernetes HPA (Horizontal Pod Autoscaler) manifest * * @param config - HPA configuration * @returns Generated HPA YAML * * @example * ```typescript * const hpa = generateHPA({ * name: 'my-app', * namespace: 'default', * targetDeployment: 'my-app', * minReplicas: 2, * maxReplicas: 10, * targetCPUUtilizationPercentage: 70, * targetMemoryUtilizationPercentage: 80 * }); * ``` */ export function generateHPA(config) { const template = readFileSync(HPA_TEMPLATE, 'utf-8'); return ejs.render(template, config); } /** * Generate Kubernetes Ingress manifest * * @param config - Ingress configuration * @returns Generated Ingress YAML * * @example * ```typescript * const ingress = generateIngress({ * name: 'my-app', * namespace: 'default', * ingressClassName: 'nginx', * rules: [{ * host: 'api.example.com', * paths: [{ * path: '/', * pathType: 'Prefix', * serviceName: 'my-app', * servicePort: 80 * }] * }], * tls: [{ * hosts: ['api.example.com'], * secretName: 'my-app-tls' * }] * }); * ``` */ export function generateIngress(config) { const template = readFileSync(INGRESS_TEMPLATE, 'utf-8'); return ejs.render(template, config); } /** * Generate Helm Chart files * * @param config - Helm chart configuration * @returns Object containing all Helm chart files * * @example * ```typescript * const helmChart = generateHelmChart({ * name: 'my-app', * version: '1.0.0', * appVersion: '1.0.0', * description: 'My Gati Application', * values: { replicaCount: 3, ... } * }); * ``` */ export function generateHelmChart(config) { const chartTemplate = readFileSync(HELM_CHART_TEMPLATE, 'utf-8'); const valuesTemplate = readFileSync(HELM_VALUES_TEMPLATE, 'utf-8'); // Read Helm template files (these are not EJS templates, just plain YAML) const helmTemplatesDir = join(TEMPLATES_DIR, 'helm', 'templates'); const deploymentTemplate = readFileSync(join(helmTemplatesDir, 'deployment.yaml'), 'utf-8'); const serviceTemplate = readFileSync(join(helmTemplatesDir, 'service.yaml'), 'utf-8'); const configMapTemplate = readFileSync(join(helmTemplatesDir, 'configmap.yaml'), 'utf-8'); const secretTemplate = readFileSync(join(helmTemplatesDir, 'secret.yaml'), 'utf-8'); const helpersTemplate = readFileSync(join(helmTemplatesDir, '_helpers.tpl'), 'utf-8'); const serviceAccountTemplate = readFileSync(join(helmTemplatesDir, 'serviceaccount.yaml'), 'utf-8'); const hpaTemplate = readFileSync(join(helmTemplatesDir, 'hpa.yaml'), 'utf-8'); return { chartYaml: ejs.render(chartTemplate, config), valuesYaml: ejs.render(valuesTemplate, config.values), deploymentTemplate, serviceTemplate, configMapTemplate, secretTemplate, helpersTemplate, serviceAccountTemplate, hpaTemplate, }; } /** * Validate Kubernetes manifest YAML syntax * * @param yaml - YAML content to validate * @returns True if valid, throws error if invalid * * @throws {Error} If YAML is invalid */ export function validateManifest(yaml) { // Basic validation checks if (!yaml || yaml.trim().length === 0) { throw new Error('Manifest is empty'); } // Check for required Kubernetes fields if (!yaml.includes('apiVersion')) { throw new Error('Missing apiVersion field'); } if (!yaml.includes('kind')) { throw new Error('Missing kind field'); } if (!yaml.includes('metadata')) { throw new Error('Missing metadata field'); } // Check for valid YAML structure (basic check) const lines = yaml.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Skip undefined lines if (line === undefined) { continue; } // Skip empty lines and comments if (!line.trim() || line.trim().startsWith('#')) { continue; } const indent = line.search(/\S/); // Check for tab characters (not allowed in YAML) if (line.includes('\t')) { throw new Error(`Invalid YAML: Tab character found on line ${i + 1}`); } // Check indent is multiple of 2 if (indent % 2 !== 0) { throw new Error(`Invalid YAML: Incorrect indentation on line ${i + 1}`); } } return true; } /** * Get default resource configuration for environment * * @param env - Deployment environment * @returns Resource configuration */ export function getDefaultResources(env) { const configs = { development: { limits: { cpu: '500m', memory: '512Mi', }, requests: { cpu: '250m', memory: '256Mi', }, }, staging: { limits: { cpu: '1000m', memory: '1Gi', }, requests: { cpu: '500m', memory: '512Mi', }, }, production: { limits: { cpu: '2000m', memory: '2Gi', }, requests: { cpu: '1000m', memory: '1Gi', }, }, }; return configs[env]; } /** * Get default environment variables for Gati app * * @param env - Deployment environment * @param port - Application port * @returns Array of environment variables */ export function getDefaultEnvironment(env, port) { return [ { name: 'NODE_ENV', value: env === 'development' ? 'development' : 'production', }, { name: 'PORT', value: port.toString(), }, { name: 'GATI_ENVIRONMENT', value: env, }, ]; } /** * Generate complete deployment manifests for a Gati application * * @param appName - Application name * @param namespace - Kubernetes namespace * @param env - Deployment environment * @param options - Additional configuration options * @returns Complete set of deployment manifests * * @example * ```typescript * const manifests = generateCompleteManifests( * 'my-app', * 'default', * 'production', * { * nodeVersion: '20', * port: 3000, * replicas: 3, * image: 'my-registry/my-app:v1.0.0' * } * ); * ``` */ export function generateCompleteManifests(appName, namespace, env, options = {}) { const port = options.port || 3000; const nodeVersion = options.nodeVersion || '20'; const replicas = options.replicas || 3; const image = options.image || `${appName}:latest`; const startCommand = options.startCommand || 'pnpm start'; const serviceType = options.serviceType || 'LoadBalancer'; // Generate Dockerfile const dockerfile = generateDockerfile({ nodeVersion, appName, port, buildCommand: options.buildCommand || 'pnpm build', startCommand, }); // Get default resources for environment const resources = getDefaultResources(env); // Combine default and additional environment variables const envVars = [ ...getDefaultEnvironment(env, port), ...(options.additionalEnv || []), ]; // Generate Deployment const deployment = generateDeployment({ name: appName, namespace, replicas, image, imagePullPolicy: env === 'development' ? 'IfNotPresent' : 'Always', containerPort: port, env: envVars, resources, }); // Generate Service const service = generateService({ name: appName, namespace, type: serviceType, port: 80, targetPort: port, selector: { app: appName }, }); // Generate Helm Chart const helm = generateHelmChart({ name: appName, version: '1.0.0', appVersion: '1.0.0', description: `Helm chart for ${appName}`, values: { replicaCount: replicas, image: { repository: image.split(':')[0] || image, tag: image.split(':')[1] || 'latest', pullPolicy: env === 'development' ? 'IfNotPresent' : 'Always', }, service: { type: serviceType, port: 80, targetPort: port, }, resources, env: envVars, autoscaling: options.enableAutoscaling ? { enabled: true, minReplicas: options.minReplicas || 2, maxReplicas: options.maxReplicas || 10, targetCPUUtilizationPercentage: options.targetCPUUtilization || 70, } : { enabled: false, minReplicas: 1, maxReplicas: 1, targetCPUUtilizationPercentage: 80, }, }, }); // Generate HPA (if autoscaling enabled and not development) let hpa; if (options.enableAutoscaling && env !== 'development') { hpa = generateHPA({ name: `${appName}-hpa`, namespace, targetDeployment: appName, minReplicas: options.minReplicas || 2, maxReplicas: options.maxReplicas || 10, targetCPUUtilizationPercentage: options.targetCPUUtilization || 70, targetMemoryUtilizationPercentage: options.targetMemoryUtilization, }); } // Generate Ingress (if enabled) let ingress; if (options.enableIngress && options.ingressHost) { const ingressConfig = { name: `${appName}-ingress`, namespace, ingressClassName: options.ingressClassName || 'nginx', rules: [ { host: options.ingressHost, paths: [ { path: '/', pathType: 'Prefix', serviceName: appName, servicePort: 80, }, ], }, ], }; // Add TLS if enabled if (options.enableTLS) { ingressConfig.tls = [ { hosts: [options.ingressHost], secretName: options.tlsSecretName || `${appName}-tls`, }, ]; } ingress = generateIngress(ingressConfig); } return { dockerfile, deployment, service, hpa, ingress, helm: { chartYaml: helm.chartYaml, valuesYaml: helm.valuesYaml, deploymentTemplate: helm.deploymentTemplate, serviceTemplate: helm.serviceTemplate, configMapTemplate: helm.configMapTemplate, secretTemplate: helm.secretTemplate, }, }; } //# sourceMappingURL=kubernetes.js.map