UNPKG

@chimoney.io/iaas-k8s-deployment

Version:

Kubernetes Infrastructure as a Service deployment package for streamlined multi-cloud environments

507 lines 20.5 kB
import { z } from "zod"; import { Buffer } from "buffer"; // In Node.js, Buffer is global. Add this if in a non-Node env needing explicit import. // ============================================================================= // Zod Schema Definitions // ============================================================================= export const LogLevelSchema = z.enum([ "debug", "info", "warn", "error", "silent", ]); export const DeploymentActionSchema = z.enum([ "up", "preview", "destroy", "outputs", "refresh", "rollback", ]); export const DeploymentStatusSchema = z.enum([ "initializing", "configuring", "deploying", "completed", "failed", "rolling-back", ]); // Enhanced Cloud Configuration Schemas export const AwsCloudConfigSchema = z.object({ region: z.string().min(1, "AWS region is required"), profile: z.string().optional(), accessKeyId: z.string().optional(), secretAccessKey: z.string().optional(), }); export const GcpCloudConfigSchema = z.object({ project: z.string().min(1, "GCP project is required"), region: z.string().min(1, "GCP region is required"), zone: z.string().optional(), credentials: z.union([z.string(), z.object({}).passthrough()]).optional(), // Path, JSON string, or service account object }); export const CloudConfigSchema = z.union([ AwsCloudConfigSchema, GcpCloudConfigSchema, ]); // Kubernetes namespace validation (DNS-1123 compliant) export const NamespaceSchema = z .string() .min(1, "Namespace name is required") .max(63, "Namespace name must be 63 characters or less") .regex(/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/, "Namespace name must be DNS-1123 compliant: lowercase letters, numbers, and hyphens only, starting and ending with alphanumeric"); export const DeploymentTypeSchema = z.enum(["shared", "dedicated"]); export const DeploymentConfigSchema = z.object({ stackName: z .string() .min(1, "Stack name is required") .regex(/^[a-zA-Z0-9-_]+$/, "Stack name can only contain alphanumeric characters, hyphens, and underscores"), secretsJson: z .string() .min(1, "Secrets JSON is required") .refine((val) => { try { const parsed = JSON.parse(val); return typeof parsed === "object" && parsed !== null; } catch { return false; } }, "Secrets JSON must be valid JSON object"), companyName: z .string() .min(1, "Company name is required") .regex(/^[a-zA-Z0-9-_]+$/, "Company name can only contain alphanumeric characters, hyphens, and underscores"), cloudProvider: z.enum(["aws", "gcp"]).optional(), helmChartPath: z.string().optional(), namespace: NamespaceSchema.optional(), deploymentType: DeploymentTypeSchema.optional(), }); export const DeploymentOptionsSchema = z.object({ action: DeploymentActionSchema, stackName: z .string() .min(1, "Stack name is required") .regex(/^[a-zA-Z0-9-_]+$/, "Stack name can only contain alphanumeric characters, hyphens, and underscores"), secretsJson: z .string() .min(1, "Secrets JSON is required") .refine((val) => { try { const parsed = JSON.parse(val); return typeof parsed === "object" && parsed !== null; } catch { return false; } }, "Secrets JSON must be valid JSON object"), companyName: z .string() .min(1, "Company name is required") .regex(/^[a-zA-Z0-9-_]+$/, "Company name can only contain alphanumeric characters, hyphens, and underscores"), workDir: z.string().optional(), helmChartPath: z.string().optional(), logLevel: LogLevelSchema.optional(), validateConfig: z.boolean().optional(), enableRollback: z.boolean().optional(), timeout: z.number().min(1).max(7200).optional(), // 1 second to 2 hours // Enhanced cloud configuration cloudProvider: z.enum(["aws", "gcp"]).optional(), cloudConfig: CloudConfigSchema.optional(), autoSetupConfig: z.boolean().optional(), // Namespace-based deployment support namespace: NamespaceSchema.optional(), deploymentType: DeploymentTypeSchema, planTier: z.enum(["basic", "standard", "premium", "enterprise"]).optional(), kubecostEnabled: z.boolean().optional(), billingAccountId: z.string().optional(), // Dynamic Helm values configuration defaultDomain: z.string().optional(), authDomain: z.string().optional(), openPaymentsDomain: z.string().optional(), connectorDomain: z.string().optional(), enableRafikiAuth: z.boolean().optional(), enableRafikiBackend: z.boolean().optional(), enableNginx: z.boolean().optional(), enableRedis: z.boolean().optional(), // Image configuration rafikiAuthImage: z .object({ repository: z.string().optional(), tag: z.string().optional(), pullPolicy: z.string().optional(), }) .optional(), rafikiBackendImage: z .object({ repository: z.string().optional(), tag: z.string().optional(), pullPolicy: z.string().optional(), }) .optional(), nginxImage: z .object({ repository: z.string().optional(), tag: z.string().optional(), pullPolicy: z.string().optional(), }) .optional(), redisImage: z .object({ repository: z.string().optional(), tag: z.string().optional(), pullPolicy: z.string().optional(), }) .optional(), // Ingress configuration ingressClassName: z.string().optional(), // HPA and network policy configuration dedicatedDeploymentHpaEnabledByDefault: z.boolean().optional(), sharedDeploymentNetworkPolicyEnabled: z.boolean().optional(), ingressControllerNamespace: z.string().optional(), ingressControllerPodSelectorLabels: z.record(z.string()).optional(), allowedExternalEgressRules: z .array(z.object({ cidr: z.string(), ports: z .array(z.object({ port: z.number(), protocol: z.enum(["TCP", "UDP"]), })) .optional(), })) .optional(), }); // ============================================================================= // Enhanced Validation Functions // ============================================================================= export function validateDeploymentOptionsWithZod(options) { const result = DeploymentOptionsSchema.safeParse(options); if (result.success) { return []; } return result.error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message, value: issue.path.reduce((obj, key) => obj?.[key], options), })); } export function validateDeploymentConfigWithZod(config) { const result = DeploymentConfigSchema.safeParse(config); if (result.success) { return []; } return result.error.issues.map((issue) => ({ field: issue.path.join("."), message: issue.message, value: issue.path.reduce((obj, key) => obj?.[key], config), })); } // ============================================================================= // Additional Validation Helpers // ============================================================================= // This function validates the expected structure for Kubernetes secrets processing. export function validateSecretsStructure(secretsJson) { const errors = []; try { const secrets = JSON.parse(secretsJson); if (!secrets.kubernetesSecrets) { errors.push({ field: "kubernetesSecrets", message: "Top-level 'kubernetesSecrets' key is missing in secrets JSON.", value: secrets, }); return errors; // Stop further validation if the main key is missing } for (const serviceName of Object.keys(secrets.kubernetesSecrets)) { const serviceSecretConfig = secrets.kubernetesSecrets[serviceName]; if (typeof serviceSecretConfig !== "object" || serviceSecretConfig === null) { errors.push({ field: `kubernetesSecrets.${serviceName}`, message: `Configuration for '${serviceName}' must be an object.`, value: serviceSecretConfig, }); continue; } if (!serviceSecretConfig.stringData) { errors.push({ field: `kubernetesSecrets.${serviceName}.stringData`, message: `'stringData' key is missing for service '${serviceName}' in secrets JSON.`, value: serviceSecretConfig, }); } else if (typeof serviceSecretConfig.stringData !== "object" || serviceSecretConfig.stringData === null) { errors.push({ field: `kubernetesSecrets.${serviceName}.stringData`, message: `'stringData' field for service '${serviceName}' must be an object.`, value: serviceSecretConfig.stringData, }); } } } catch (e) { // This case is also handled by the main try-catch in validateAndEncodeSecrets, // but specific parsing errors for structure can be caught here too. errors.push({ field: "secretsJson", message: `Invalid JSON format when validating secrets structure: ${e instanceof Error ? e.message : String(e)}`, value: secretsJson.substring(0, 100) + "...", }); } return errors; } // ============================================================================= // Secret Processing Utilities (stringData support) // ============================================================================= /** * @deprecated Base64 encoding is no longer needed for stringData secrets. * This function is kept for legacy compatibility only. * Encodes a single string value to Base64. */ export function encodeSecretValue(value) { return Buffer.from(value, "utf8").toString("base64"); } /** * @deprecated Base64 decoding is no longer needed for stringData secrets. * This function is kept for legacy compatibility only. * Decodes a single Base64 encoded string value. */ export function decodeSecretValue(encodedValue) { return Buffer.from(encodedValue, "base64").toString("utf8"); } /** * @deprecated Base64 detection is no longer needed for stringData secrets. * This function is kept for legacy compatibility only. * Checks if a string is already Base64 encoded. */ export function isBase64Encoded(str) { if (str === "" || str.trim() === "") { return false; } try { // The most reliable way: decode and re-encode. If it's the same, it was valid base64. return Buffer.from(str, "base64").toString("base64") === str; } catch (e) { // If decoding throws an error, it's not valid Base64. return false; } } /** * Enhanced version that provides detailed validation and processing for Kubernetes secrets. * This function recursively processes the secrets object and validates * string values found under any 'stringData' key within the 'kubernetesSecrets' structure. * Since we're using stringData, no base64 encoding is needed. */ function validateSecretsForKubernetesWithReport(secretsInput) { // Expecting secretsInput to be the full parsed JSON, e.g., { kubernetesSecrets: { ... } } const processedSecrets = JSON.parse(JSON.stringify(secretsInput)); // Deep clone const report = {}; function processObject(obj, currentPathParts) { for (const [key, value] of Object.entries(obj)) { const newPathParts = [...currentPathParts, key]; const fullPathKey = newPathParts.join("."); if (typeof value === "string") { // Check if the immediate parent key is 'stringData'. const isInTargetStringDataObject = currentPathParts.length > 0 && currentPathParts[currentPathParts.length - 1] === "stringData"; if (isInTargetStringDataObject) { if (value.startsWith("PLEASE_REPLACE_WITH_") || value.startsWith("<REPLACE_WITH_") || value.includes("REPLACE_WITH")) { report[fullPathKey] = "placeholder"; console.warn(`Found placeholder value for ${fullPathKey}: ${value}`); continue; // Skip processing for placeholders } // For stringData, we don't need to encode - just mark as processed report[fullPathKey] = "processed"; } else { report[fullPathKey] = "skipped"; } } else if (typeof value === "object" && value !== null) { processObject(value, newPathParts); } else { const isInTargetStringDataObject = currentPathParts.length > 0 && currentPathParts[currentPathParts.length - 1] === "stringData"; if (!isInTargetStringDataObject) { report[fullPathKey] = "skipped"; } else { if (value !== null && (typeof value === "number" || typeof value === "boolean")) { // Convert numbers and booleans to strings for Kubernetes secret stringData values obj[key] = String(value); report[fullPathKey] = "processed"; } else { report[fullPathKey] = "skipped"; // Other types (null, undefined under stringData) } } } } } // Start processing from the root of the cloned secrets object if (processedSecrets.kubernetesSecrets) { processObject(processedSecrets.kubernetesSecrets, ["kubernetesSecrets"]); } else { // If kubernetesSecrets key is missing, the report will be empty, // and no processing will happen. Structure validation should catch this. console.warn("validateSecretsForKubernetesWithReport: 'kubernetesSecrets' key not found in input. No secrets will be processed."); } return { processedSecrets, report }; } export function validateAndProcessSecrets(secretsJson) { let errors = []; let processingReport = {}; try { const secrets = JSON.parse(secretsJson); const structureErrors = validateSecretsStructure(secretsJson); // Pass the raw JSON string errors.push(...structureErrors); if (errors.length > 0) { return { isValid: false, processedSecretsJson: secretsJson, errors, // processingReport will be empty or undefined here }; } const { processedSecrets, report } = validateSecretsForKubernetesWithReport(secrets); // Pass the parsed 'secrets' object processingReport = report; console.log("Secret processing report:", report); for (const [key, status] of Object.entries(report)) { if (status === "placeholder") { // Attempt to get the original placeholder value for the error message let placeholderValue = "PLACEHOLDER_VALUE_NOT_RETRIEVED"; const pathParts = key.split("."); try { placeholderValue = pathParts.reduce((acc, part) => acc && acc[part], secrets); } catch (e) { /* ignore if path is invalid, shouldn't happen */ } errors.push({ field: key, message: `Placeholder value found for '${key}'. Please replace '${placeholderValue}'.`, value: placeholderValue, }); } } if (errors.length > 0) { return { isValid: false, // If placeholders are considered errors making it invalid processedSecretsJson: JSON.stringify(processedSecrets), // Still return the (partially) processed secrets errors, processingReport, }; } return { isValid: true, processedSecretsJson: JSON.stringify(processedSecrets), errors: [], // errors is empty if we reach here processingReport: report, }; } catch (e) { // This catch is primarily for JSON.parse(secretsJson) failure errors.push({ field: "secretsJson", message: `Invalid JSON format for secrets: ${e instanceof Error ? e.message : String(e)}`, value: secretsJson.substring(0, 100) + "...", }); return { isValid: false, processedSecretsJson: secretsJson, errors, processingReport, }; } } // Legacy function name for backward compatibility - now just calls the new function export function validateAndEncodeSecrets(secretsJson) { const result = validateAndProcessSecrets(secretsJson); // Map the new report statuses to old ones for backward compatibility const encodingReport = {}; if (result.processingReport) { for (const [key, status] of Object.entries(result.processingReport)) { switch (status) { case "processed": encodingReport[key] = "encoded"; // For backward compatibility break; case "placeholder": encodingReport[key] = "placeholder"; break; case "skipped": encodingReport[key] = "skipped"; break; } } } return { isValid: result.isValid, encodedSecretsJson: result.processedSecretsJson, errors: result.errors, encodingReport, }; } // ============================================================================= // Type Guards // ============================================================================= export function isValidDeploymentAction(action) { return DeploymentActionSchema.safeParse(action).success; } export function isValidLogLevel(level) { return LogLevelSchema.safeParse(level).success; } export function isValidDeploymentStatus(status) { return DeploymentStatusSchema.safeParse(status).success; } export function isValidNamespace(namespace) { return NamespaceSchema.safeParse(namespace).success; } export function isValidDeploymentType(deploymentType) { return DeploymentTypeSchema.safeParse(deploymentType).success; } // ============================================================================= // Namespace-specific Validation Functions // ============================================================================= export function validateNamespaceConfiguration(namespace, deploymentType) { const errors = []; // Validate namespace name if (!isValidNamespace(namespace)) { errors.push({ field: "namespace", message: "Invalid namespace name. Must be DNS-1123 compliant: lowercase letters, numbers, and hyphens only, starting and ending with alphanumeric characters, max 63 chars", value: namespace, }); } // For shared deployments, namespace is required if (deploymentType === "shared" && !namespace) { errors.push({ field: "namespace", message: "Namespace is required for shared cluster deployments", value: namespace, }); } // Reserved namespace names const reservedNamespaces = [ "default", "kube-system", "kube-public", "kube-node-lease", "kubernetes-dashboard", ]; if (reservedNamespaces.includes(namespace)) { errors.push({ field: "namespace", message: `Namespace '${namespace}' is reserved and cannot be used for deployments`, value: namespace, }); } return errors; } //# sourceMappingURL=validation.js.map