@chimoney.io/iaas-k8s-deployment
Version:
Kubernetes Infrastructure as a Service deployment package for streamlined multi-cloud environments
507 lines • 20.5 kB
JavaScript
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