UNPKG

iop

Version:
322 lines 16.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.loadConfig = loadConfig; exports.loadSecrets = loadSecrets; const promises_1 = __importDefault(require("node:fs/promises")); const path_1 = __importDefault(require("path")); const js_yaml_1 = __importDefault(require("js-yaml")); const types_1 = require("./types"); const IOP_DIR = ".iop"; const CONFIG_FILE = "iop.yml"; const SECRETS_FILE = "secrets"; async function loadConfig() { try { const configFile = await promises_1.default.readFile(CONFIG_FILE, "utf-8"); const rawConfig = js_yaml_1.default.load(configFile); // Validate and parse using Zod schema const validationResult = types_1.IopConfigSchema.safeParse(rawConfig); if (!validationResult.success) { console.error(`\nInvalid configuration in ${CONFIG_FILE}:`); // Collect all validation issues including nested union errors const allIssues = []; // Helper function to get current value at path function getCurrentValue(obj, pathArray) { let current = obj; for (const key of pathArray) { if (current && typeof current === 'object' && key in current) { current = current[key]; } else { return undefined; } } return current; } function collectIssues(issues) { issues.forEach((issue) => { if (issue.code === 'invalid_union' && issue.unionErrors) { // For union errors, find the most specific error (longest path) to avoid generic messages let bestError = null; let maxPathLength = 0; issue.unionErrors.forEach((unionError) => { if (unionError.issues) { unionError.issues.forEach((subIssue) => { const pathLength = subIssue.path?.length || 0; if (pathLength > maxPathLength) { maxPathLength = pathLength; bestError = subIssue; } }); } }); // Only add the most specific error, ignore generic union failures if (bestError && maxPathLength > 0) { allIssues.push({ path: bestError.path.join('.'), code: bestError.code, message: bestError.message, expected: bestError.expected, received: bestError.received, currentValue: getCurrentValue(rawConfig, bestError.path) }); } } else { allIssues.push({ path: issue.path.join('.'), code: issue.code, message: issue.message, expected: issue.expected, received: issue.received, currentValue: getCurrentValue(rawConfig, issue.path) }); } }); } collectIssues(validationResult.error.errors); // Display world-class error messages allIssues.forEach((issue) => { const { path, code, message, expected, received, currentValue } = issue; // Helper function to format current value for display const formatValue = (value) => { if (value === undefined || value === null) return 'undefined'; if (typeof value === 'string') return `"${value}"`; if (typeof value === 'object') return JSON.stringify(value); return String(value); }; // Helper function to show unified error format const showError = (description, yamlFix) => { console.error(`\n Configuration Error in ${path}:`); console.error(` ${description}`); console.error(` Fix in iop.yml:`); yamlFix.forEach(line => console.error(` ${line}`)); }; // Helper function to get entity info from path const getEntityInfo = (path) => { const parts = path.split('.'); return { section: parts[0], // apps, services, ssh, etc. entity: parts[1], // web, db, etc. field: parts.slice(-1)[0] // app_port, server, etc. }; }; const { section, entity, field } = getEntityInfo(path); if (path === "name" && code === "invalid_type") { showError(`Missing project name`, [ `name: my-project` ]); } else if (path.includes("app_port") && code === "invalid_type" && expected === "number") { showError(`Found ${formatValue(currentValue)}, expected a number`, [ `${section}:`, ` ${entity}:`, ` proxy:`, ` app_port: 3000 # Common ports: 3000, 8000, 8080` ]); } else if (path.includes("server") && code === "invalid_type") { showError(`Missing server for app "${entity}"`, [ `${section}:`, ` ${entity}:`, ` server: 192.168.1.100 # Your server IP or domain` ]); } else if (path.includes("build.context") && code === "invalid_type") { showError(`Missing build context for app "${entity}"`, [ `${section}:`, ` ${entity}:`, ` build:`, ` context: . # Usually current directory` ]); // Pattern-based error handling with unified format } else if (path.includes("proxy.hosts")) { showError(`Found ${formatValue(currentValue)}, expected array of domains`, [ `${section}:`, ` ${entity}:`, ` proxy:`, ` hosts:`, ` - myapp.com`, ` - www.myapp.com` ]); } else if (path.includes("environment.plain")) { showError(`Found ${formatValue(currentValue)}, expected array of KEY=VALUE strings`, [ `environment:`, ` plain:`, ` - NODE_ENV=production`, ` - PORT=3000` ]); } else if (path.includes("environment.secret")) { showError(`Found ${formatValue(currentValue)}, expected array of variable names`, [ `environment:`, ` secret:`, ` - DATABASE_URL # References .iop/secrets`, ` - API_KEY` ]); } else if (path.includes("ports")) { showError(`Found ${formatValue(currentValue)}, expected array of port mappings`, [ `ports:`, ` - "3000:3000" # "host:container" format`, ` - "80:8080"` ]); } else if (path.includes("volumes")) { showError(`Found ${formatValue(currentValue)}, expected array of volume mappings`, [ `volumes:`, ` - "./data:/app/data" # "host:container" format`, ` - "myvolume:/var/lib/data"` ]); } else if (path.includes("ssh.username")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `ssh:`, ` username: iop # Your SSH username` ]); } else if (path.includes("ssh.port")) { showError(`Found ${formatValue(currentValue)}, expected number`, [ `ssh:`, ` port: 22 # Port number without quotes` ]); } else if (path.includes("ssh.key_file")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `ssh:`, ` key_file: ~/.ssh/id_rsa # Path to SSH private key` ]); } else if (path.includes("health_check.path")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `${section}:`, ` ${entity}:`, ` health_check:`, ` path: /api/health # Your health endpoint path` ]); } else if (path.includes("ssl")) { showError(`Found ${formatValue(currentValue)}, expected true or false`, [ `${section}:`, ` ${entity}:`, ` proxy:`, ` ssl: true # Enable HTTPS` ]); } else if (path.includes("replicas")) { showError(`Found ${formatValue(currentValue)}, expected positive number`, [ `${section}:`, ` ${entity}:`, ` replicas: 1 # Number of instances` ]); } else if (path.includes("build.dockerfile")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `${section}:`, ` ${entity}:`, ` build:`, ` dockerfile: Dockerfile # Path to Dockerfile` ]); } else if (path.includes("build.args")) { showError(`Found ${formatValue(currentValue)}, expected array of strings`, [ `${section}:`, ` ${entity}:`, ` build:`, ` args:`, ` - NODE_ENV # Environment variable names`, ` - API_URL` ]); } else if (path.includes("build.target")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `${section}:`, ` ${entity}:`, ` build:`, ` target: production # Multi-stage build target` ]); } else if (path.includes("build.platform")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `${section}:`, ` ${entity}:`, ` build:`, ` platform: linux/amd64 # Target platform` ]); } else if (path.includes("docker.registry")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `docker:`, ` registry: registry.example.com # Docker registry URL` ]); } else if (path.includes("docker.username")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `docker:`, ` username: myuser # Registry username` ]); } else if (path.includes("proxy.image")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `proxy:`, ` image: iop-proxy:latest # Custom proxy image` ]); } else if (path.includes("registry.url")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `${section}:`, ` ${entity}:`, ` registry:`, ` url: registry.example.com # Registry URL` ]); } else if (path.includes("registry.username")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `${section}:`, ` ${entity}:`, ` registry:`, ` username: myuser # Registry username` ]); } else if (path.includes("registry.password_secret")) { showError(`Found ${formatValue(currentValue)}, expected string`, [ `${section}:`, ` ${entity}:`, ` registry:`, ` password_secret: REGISTRY_PASSWORD # References .iop/secrets` ]); } else if (code === "unrecognized_keys") { showError(`Unknown field: ${path}`, [ `# Remove this field - it's not supported` ]); } else if (code === "too_small" || code === "too_big") { showError(`Invalid value ${formatValue(currentValue)}: ${message}`, [ `# Check the valid range for this field` ]); } else { // Enhanced fallback const description = expected && received ? `Found ${formatValue(currentValue)}, expected ${expected}` : `${message}`; showError(description, [ `# Run 'iop init' for a valid configuration template` ]); } }); console.error(`\nNeed help? Run: iop init --help`); throw new Error(`Invalid configuration in ${CONFIG_FILE}.`); } return validationResult.data; } catch (error) { if (error instanceof Error && error.message.startsWith("Invalid configuration")) { throw error; // Re-throw Zod validation error } console.error(`Error loading or parsing ${CONFIG_FILE}:`, error); throw error; // Re-throw other errors (e.g., file not found) } } async function loadSecrets() { const secretsPath = path_1.default.join(IOP_DIR, SECRETS_FILE); try { const secretsFile = await promises_1.default.readFile(secretsPath, "utf-8"); const parsedSecrets = {}; secretsFile.split("\n").forEach((line) => { line = line.trim(); if (line && !line.startsWith("#")) { const [key, ...valueParts] = line.split("="); if (key && valueParts.length > 0) { parsedSecrets[key.trim()] = valueParts .join("=") .trim() .replace(/^["']|["']$/g, ""); } else { console.warn(`Skipping malformed line in secrets file: ${line}`); } } }); // Validate and parse secrets using Zod schema const validationResult = types_1.IopSecretsSchema.safeParse(parsedSecrets); if (!validationResult.success) { console.error(`Invalid secrets in ${secretsPath}:`); validationResult.error.errors.forEach((err) => { console.error(` Key: ${err.path.join(".")}, Message: ${err.message}`); }); throw new Error(`Invalid secrets in ${secretsPath}.`); } return validationResult.data; } catch (error) { const nodeError = error; if (nodeError.code === "ENOENT") { console.warn(`${secretsPath} not found. Proceeding with empty secrets.`); return types_1.IopSecretsSchema.parse({}); // Return validated empty secrets } if (error instanceof Error && error.message.startsWith("Invalid secrets")) { throw error; // Re-throw Zod validation error for secrets } console.error(`Error loading or parsing ${secretsPath}:`, error); throw error; } } //# sourceMappingURL=index.js.map