UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

976 lines 45.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.mirrorNotesFile = exports.renderChartNotes = exports.extractTgzToDir = exports.hydrateAllChartDependencies = exports.shellEscape = exports.decodeReleaseFromSecret = exports.encodeRelease = exports.getReleaseSecretNameLegacy = exports.getReleaseSecretNameWithoutRevision = exports.getReleaseSecretName = exports.extractHelmConfig = exports.extractHelmDefaultValues = exports.extractHelmCustomValues = exports.extractHelmChartMetadata = exports.getHelmTemplateResult = exports.runHelmTemplate = exports.prepareChartIfNecessary = exports.processHelmArgs = void 0; const tmp = require("tmp"); const fs = require("fs"); const path = require("path"); const zlib = require("zlib"); const tar = require("tar-stream"); const jsyaml = require("js-yaml"); const stream_1 = require("stream"); const util_1 = require("util"); const logger_1 = require("../util/logger"); const child_process_1 = require("child_process"); const names_1 = require("../util/names"); const fs_1 = require("fs"); const io_1 = require("../util/io"); const pako_1 = require("pako"); const api_1 = require("../rest/api"); const objects_1 = require("../util/objects"); const constants_1 = require("./constants"); // Exported // /** * Processes CLI args for a `helm template [NAME] [CHART]` style, mirroring Helm v3 semantics. * * @param {Arguments<HelmSingleRelease & HelmSingleChart & HelmTemplateOptions>} args - Parsed arguments object that may include a positional NAME (`release`), a positional CHART (`chart`), and flags like `generateName`. * @returns {void} Mutates `args.release` and `args.chart` in place to reflect Helm's argument resolution rules. * @throws {Error} If `--generate-name` is used together with an explicit NAME. * @throws {Error} If no CHART is provided. * @throws {Error} If the resolved NAME exceeds the maximum allowed length. * @throws {Error} If the resolved NAME violates DNS-1123 label rules. */ function processHelmArgs(args) { var _a, _b; // Normalize the candidate release (defensive against undefined and extra whitespace) let release = ((_a = args.release) !== null && _a !== void 0 ? _a : '').trim(); // Normalize the candidate chart (defensive against undefined and extra whitespace) let chart = ((_b = args.chart) !== null && _b !== void 0 ? _b : '').trim(); // Capture whether automatic name generation was requested const generateName = Boolean(args.generateName); // Shift a single positional into CHART to match Helm behavior if (release && !chart) { // Treat the provided token as a chart reference (path/repo/chart/URL/OCI) chart = release; // Clear the explicit release since it was reinterpreted as chart release = ''; } // Require an explicit chart reference (do not default to ".") if (!chart) { throw new Error('ERROR: Must either provide a chart name or specify --help'); } // Enforce mutual exclusivity between explicit NAME and --generate-name if (generateName && release) { throw new Error('ERROR: Cannot set --generate-name and also specify a release name.'); } // Validate a user-supplied release name if present if (release) { // Ensure the explicit name is safe for Kubernetes validateReleaseNameOrThrow(release); } else if (generateName) { // Assign the generated name to the working variable release = generateHelmStyleReleaseName(chart); } // Persist the resolved release name back into the args object args.release = release; // Persist the resolved chart reference back into the args object args.chart = chart; // Throw an error if the user didn't specify a release name nor did they request a generated one if (args.release.length === 0) { throw new Error(constants_1.HELM_MISSING_RELEASE_NAME_ERROR); } } exports.processHelmArgs = processHelmArgs; /** * Fetches a Helm chart into a temporary directory using `helm pull --untar` if necessary, * updates `args.chart` to the extracted chart path, and returns a cleanup function. * * @param {Session} session - Session interface used to report user-facing errors via `abort({ message })`. * @param {Arguments<HelmSingleChart & HelmTemplateOptions & DebugOptions>} args - Parsed arguments object that includes a positional CHART (`chart`). * @returns {HelmChartCleanupFn} A zero-arg function that deletes the created temporary directory when invoked or null is returned if operation is not applicable. * @throws {Error} If `args.chart` is missing, `helm` is not available, the pull fails, or the extracted chart directory cannot be resolved. */ function prepareChartIfNecessary(session, args) { var _a, _b, _c; // Ensure the caller has provided a chart reference in this.args.chart const chartRef = ((_a = args.chart) !== null && _a !== void 0 ? _a : '').trim(); // Enforce that a chart reference is present before attempting the pull if (!chartRef) { session.abort({ message: 'ERROR: chart reference is required' }); } // Detect whether the chart reference uses the OCI scheme const isOci = chartRef.startsWith('oci://'); // Let's do nothing if the user didn't specify any of OCI chart or a repo if (!isOci && !args.repo) { return null; } // Create a secure temporary directory for extracting the pulled chart const tmpDirResult = tmp.dirSync({ unsafeCleanup: true }); // Capture the absolute path to the created temporary directory const tmpDir = tmpDirResult.name; // Build a robust Helm pull command beginning with the base executable let cmd = 'helm pull'; // Append the chart reference (can be repo/chart, URL, or an OCI ref) cmd += ` ${shellEscape(chartRef)}`; // Append the repo flag only for non-OCI references if (!isOci && args.repo && args.repo.trim().length > 0) { cmd += ` --repo ${shellEscape(args.repo)}`; } // Append the version flag when provided to enforce reproducibility if (args.version && args.version.trim().length > 0) { cmd += ` --version ${shellEscape(args.version)}`; } // Append flags to untar directly into our temporary directory cmd += ` --untar --untardir ${shellEscape(tmpDir)}`; try { // Print the command we are using to the debug log logger_1.logger.debug(cmd); // Execute the Helm pull command with strict buffering and UTF-8 encoding const stdout = (0, child_process_1.execSync)(cmd, { stdio: 'pipe', encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, }); // If debug or verbose flags are enabled, log the Helm pull output if (stdout.trim() && (args.debug || args.verbose)) { logger_1.logger.debug(`Helm pull stdout:\n${stdout}`); } // Enumerate entries inside the temporary directory to locate the extracted chart folder const entries = fs.readdirSync(tmpDir, { withFileTypes: true }); // Filter to directories only, which should contain the chart root const dirs = entries.filter((e) => e.isDirectory()); // Derive a best-effort expected folder name from the chart reference tail segment const expectedNameHint = deriveExpectedChartDirName(chartRef); // Resolve the chart directory by exact match or by falling back to a single directory const resolvedDirName = (_c = (_b = dirs.find((d) => d.name === expectedNameHint)) === null || _b === void 0 ? void 0 : _b.name) !== null && _c !== void 0 ? _c : (dirs.length === 1 ? dirs[0].name : undefined); // Throw a descriptive error if the extracted chart directory cannot be determined if (!resolvedDirName) { throw new Error(`ERROR: unable to resolve extracted chart directory in ${tmpDir}`); } // Compute the absolute path to the extracted chart root directory const chartPath = path.join(tmpDir, resolvedDirName); // Update the caller’s chart path to point at the extracted chart directory args.chart = chartPath; } catch (e) { // Make sure the tmp directory is cleaned up if an error occurred try { tmpDirResult.removeCallback(); } catch (e) { // Swallow the error to keep cleanup idempotent and non-fatal } // Attempt to extract standard error output for a clearer abort message const stderrText = typeof (e === null || e === void 0 ? void 0 : e.stderr) === 'string' && e.stderr.length ? e.stderr : ''; // Append stderr content in debug/verbose modes to avoid noisy default output if (stderrText) { session.abort({ message: stderrText.trim() }); } // Rethrow the original error if we couldn't extract standard error throw e; } // Return a cleanup function that removes the temporary directory and its contents return tmpDirResult.removeCallback; } exports.prepareChartIfNecessary = prepareChartIfNecessary; /** * Execute `helm template` command. * * @param {Arguments<HelmSingleRelease & HelmSingleChart & HelmTemplateOptions & DebugOptions>} args - Parsed Helm arguments including `release` and `chart`. * @param {string} org - Organization identifier to inject as values. * @param {string | undefined} gvc - Optional GVC identifier to inject as values. * @param {boolean | undefined} includeDebug - Whether to include `--debug` in the output. * @returns {string} The output of the helm template command execution. */ function runHelmTemplate(args, org, gvc, includeDebug) { var _a, _b, _c, _d, _e, _f; // Build the common option list with awareness of the chart scheme const flags = formatHelmCliOptions(args, org, gvc, includeDebug); // Compose the final command with escaped release and chart const cmd = `helm template ${args.release} ${args.chart} ${flags.join(' ')}`.trim(); // Print the command on debug / verbose if (includeDebug) { logger_1.logger.debug(cmd); } // Execute the command and handle its errors try { // Make sure output is NOT inherited by the parent process return (0, child_process_1.execSync)(cmd, { stdio: 'pipe', encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, }); } catch (e) { const stderr = (_c = (_b = (_a = e === null || e === void 0 ? void 0 : e.stderr) === null || _a === void 0 ? void 0 : _a.toString) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : ''; const stdout = (_f = (_e = (_d = e === null || e === void 0 ? void 0 : e.stdout) === null || _d === void 0 ? void 0 : _d.toString) === null || _e === void 0 ? void 0 : _e.call(_d)) !== null && _f !== void 0 ? _f : ''; // Helm sometimes mixes channels const combined = `${stderr}${stdout}`.trim(); // Check for the specific "missing dependencies" failure if (combined.includes(constants_1.HELM_DEPENDENCY_MISSING_ERROR)) { throw new Error('ERROR: Helm chart dependencies are missing. You may need to re-run the command with the `--dependency-update` option to fetch missing dependencies.'); } // Otherwise, surface only the tool's message (not the Node stack) throw new Error(combined || e.message || 'ERROR: helm template command failed for unknown reasons.'); } } exports.runHelmTemplate = runHelmTemplate; function getHelmTemplateResult(session, args, org, gvc) { // Generate a Helm template using the Helm template CLI command const template = runHelmTemplate(args, org, gvc, (args.debug || args.verbose)); // Print verbose if (args.debug || args.verbose) { session.out(template); } // Load template output into cpln resources const templateResources = jsyaml.safeLoadAll(template).filter((item) => item !== null && typeof item === 'object'); // Sort by kind priority (0, objects_1.sortIHasKindByPriority)(templateResources, false); // Template resources cannot be empty if (templateResources.length === 0) { session.abort({ message: 'ERROR: There were no resources found in the generated template. Make sure you have templates in your chart.', }); } // Create a fast lookup set for the allow-listed CPLN kinds const allowedKinds = new Set(api_1.Kinds); // Identify resources that are likely not CPLN (Kubernetes-native or unknown kinds) const offenders = templateResources.filter((res) => { // Read the kind as a string if present const kind = typeof res.kind === 'string' ? res.kind : undefined; // Determine if apiVersion exists which strongly indicates a Kubernetes manifest const hasApiVersion = Object.prototype.hasOwnProperty.call(res, 'apiVersion'); // Determine if metadata exists which also indicates a Kubernetes manifest const hasMetadata = Object.prototype.hasOwnProperty.call(res, 'metadata'); // Determine if kind is missing or not in the CPLN allow-list const kindUnknown = !kind || !allowedKinds.has(kind); // Flag the resource if any of the non-CPLN indicators are present return hasApiVersion || hasMetadata || kindUnknown; }); // Abort with details if any non-CPLN resources were detected if (offenders.length > 0) { // Abort with an explicit message and helpful offender examples session.abort({ message: 'ERROR: Some resources in the rendered template are not CPLN resources. ' + 'Remove Kubernetes manifests (objects with apiVersion/metadata) and ensure every resource kind is supported by CPLN.', }); } // Return the template resources return { manifest: template, resources: templateResources }; } exports.getHelmTemplateResult = getHelmTemplateResult; function extractHelmChartMetadata(chartDirPath) { const filePath = path.join(chartDirPath, 'Chart.yaml'); return (0, io_1.loadObject)((0, fs_1.readFileSync)(filePath, 'utf-8')); } exports.extractHelmChartMetadata = extractHelmChartMetadata; function extractHelmCustomValues(pathsToValues) { let values = []; // Read and add custom values if specified if (pathsToValues) { if (typeof pathsToValues === 'string') { pathsToValues = [pathsToValues]; } // Iterate over each custom values path and add it for (const path of pathsToValues) { const customValues = (0, fs_1.readFileSync)(path, 'utf-8'); if (customValues) { values.push(customValues.trim()); } } } return values; } exports.extractHelmCustomValues = extractHelmCustomValues; function extractHelmDefaultValues(chartDirPath) { return jsyaml.load((0, child_process_1.execSync)(`helm show values ${chartDirPath}`).toString()); } exports.extractHelmDefaultValues = extractHelmDefaultValues; function extractHelmConfig(args, valuesFiles) { let config = {}; // Apply "values" for (const valuesFile of valuesFiles) { Object.assign(config, jsyaml.safeLoad(valuesFile)); } // Apply "set" applySetProperty(config, args.set); // Apply "set-string" applySetProperty(config, args.setString); // Apply "set-file" applySetFileProperty(config, args.setFile); return config; } exports.extractHelmConfig = extractHelmConfig; function getReleaseSecretName(release, revision) { // Construct the name of the secret const name = `${constants_1.CPLN_RELEASE_NAME_PREFIX}-${release}`; // Make the name safe const safeName = (0, names_1.safeCplnName)(59, name); // Append the version number to the safe name and return it return `${safeName}-v${revision}`; } exports.getReleaseSecretName = getReleaseSecretName; function getReleaseSecretNameWithoutRevision(release) { // Get the secret name of the release using 0 as the version number const name = getReleaseSecretName(release, 0); // The length of the version part is 3 ('-v0'.length === 3) const versionPartLength = 3; // Trim out the verion part and return the secret name return name.substring(0, name.length - versionPartLength); } exports.getReleaseSecretNameWithoutRevision = getReleaseSecretNameWithoutRevision; function getReleaseSecretNameLegacy(release) { return `${constants_1.CPLN_RELEASE_NAME_PREFIX_LEGACY}-${release}`; } exports.getReleaseSecretNameLegacy = getReleaseSecretNameLegacy; async function encodeRelease(release) { // Gzip the release object const gzipped = (0, pako_1.gzip)(JSON.stringify(release)); // Base64-encode and return return Buffer.from(gzipped).toString('base64'); } exports.encodeRelease = encodeRelease; function decodeReleaseFromSecret(secret) { // If the secret is not valid, throw an error if (!secret.data || !secret.data.payload) { throw new Error('ERROR: Invalid release revision secret data.'); } // Decode the base64-encoded payload const decoded = Buffer.from(secret.data.payload, 'base64'); // Gunzip the decoded data const gunzipped = (0, pako_1.ungzip)(new Uint8Array(decoded), { to: 'string' }); // Parse the JSON and return the release object return JSON.parse(gunzipped); } exports.decodeReleaseFromSecret = decodeReleaseFromSecret; /** * Escapes a shell argument minimally for safe inclusion in a command string. * * @param {string} s - The raw string to escape for shell usage. * @returns {string} A safely quoted string suitable for shell execution. */ function shellEscape(s) { // Ensure we operate on a defined, trimmed string const v = String(s !== null && s !== void 0 ? s : '').trim(); // Wrap the string in single quotes and escape existing single quotes const escaped = `'${v.replace(/'/g, `'\\''`)}'`; // Return the escaped token return escaped; } exports.shellEscape = shellEscape; /** * Recursively ensures all dependencies are fetched and extracts any packaged subcharts (*.tgz|*.tar.gz) * at or below `chartRoot`, so the entire chart tree exists on disk for notes processing. * * @param {string} chartRoot - Absolute path to the chart root (temporary working copy). * @returns {Promise<void>} Resolves when all nested deps are fetched and archives expanded. * @throws {Error} If extraction or dependency fetching fails at any level. */ async function hydrateAllChartDependencies(chartRoot) { // Initialize a breadth-first queue seeded with the top-level chart root const queue = [chartRoot]; // Process the queue until empty while (queue.length > 0) { // Dequeue the next chart directory to process const currentChartDir = queue.shift(); // Ensure dependencies for this chart are present in its local `charts/` runHelmDependency(currentChartDir); // Compute the absolute path to this chart's `charts/` directory const chartsDir = path.join(currentChartDir, 'charts'); // Continue if no `charts/` directory exists (nothing to extract/enqueue here) if (!fs.existsSync(chartsDir) || !fs.statSync(chartsDir).isDirectory()) { continue; } // Read entries under `charts/` to find archives and/or unpacked subcharts const entries = fs.readdirSync(chartsDir, { withFileTypes: true }); // Iterate entries to expand any packaged archives found at this level for (const entry of entries) { // Skip non-files if (!entry.isFile()) { continue; } // Compute absolute path to the candidate archive const absArchive = path.join(chartsDir, entry.name); // Detect supported archive extensions const isTgz = /\.tgz$/i.test(entry.name); const isTarGz = /\.tar\.gz$/i.test(entry.name); // Skip non-archives if (!isTgz && !isTarGz) { continue; } // Extract the archive into the current `charts/` directory await extractTgzToDir(absArchive, chartsDir); // Attempt to remove the archive after successful extraction try { fs.unlinkSync(absArchive); } catch (_a) { // Ignore deletion errors to keep idempotency } } // Refresh entries after extraction to discover unpacked subchart directories const refreshed = fs.readdirSync(chartsDir, { withFileTypes: true }); // Enqueue any subchart directories for recursive processing for (const ent of refreshed) { // Only directories can be subchart roots if (!ent.isDirectory()) { continue; } // Compute absolute path to the discovered subchart const subChartRoot = path.join(chartsDir, ent.name); // Enqueue the subchart to hydrate its dependencies and extract its archives queue.push(subChartRoot); } } } exports.hydrateAllChartDependencies = hydrateAllChartDependencies; /** * Extracts a single `.tgz`/`.tar.gz` archive into a destination directory using tar-stream. * * @param {string} archivePath - Absolute path to the tar.gz file to extract. * @param {string} destDir - Absolute path to the destination directory to write files into. * @returns {Promise<void>} Resolves when extraction completes successfully. * @throws {Error} If the tarball cannot be read or an entry fails to write. */ async function extractTgzToDir(archivePath, destDir) { // Create a tar extractor to read entries from the tar archive const extract = tar.extract(); // Register a handler for each tar entry encountered during extraction extract.on('entry', (header, stream, next) => { // Derive a sanitized output path to prevent path traversal const outPath = safeJoin(destDir, header.name); // Ensure the entry’s parent directory exists fs.mkdirSync(path.dirname(outPath), { recursive: true }); // Handle directory entries by ensuring existence and advancing if (header.type === 'directory') { fs.mkdirSync(outPath, { recursive: true }); stream.resume(); next(); return; } // Handle regular file entries by piping the stream to a write stream if (header.type === 'file') { const out = fs.createWriteStream(outPath, { mode: header.mode }); // Pipe the tar entry into the destination file stream.pipe(out); // Advance to next entry only after the write stream finishes out.on('finish', next); // Propagate write errors as extraction failures out.on('error', (err) => extract.destroy(err)); // Return early since next() is handled in the 'finish' event return; } // Skip other entry types (symlink, hardlink, etc.) safely stream.resume(); next(); }); // Create a gunzip stream to decompress the gzip layer const gunzip = zlib.createGunzip(); // Create a readable stream for the archive const input = fs.createReadStream(archivePath); // Pipe: input (file) -> gunzip -> extract (tar) input.pipe(gunzip).pipe(extract); // Await completion or error of the extractor stream await (0, util_1.promisify)(stream_1.finished)(extract); } exports.extractTgzToDir = extractTgzToDir; /** * Discovers/writes wrapper templates (root-only or recursive), runs `helm template` *without* `--show-only`, * extracts wrapper documents (matched by `# Source: .../cpln_notes_wrapper.yaml`), and returns a single * "NOTES:" block with the parent chart first, then subcharts in original order. * * @param {Arguments<HelmSingleRelease & HelmSingleChart & HelmTemplateOptions & DebugOptions>} args - Parsed Helm args (must include `chart` path). * @param {string} org - Organization identifier to inject as values. * @param {string | undefined} gvc - Optional GVC identifier to inject as values. * @param {boolean | undefined} includeDebug - Whether to include `--debug` in the output. * @param {boolean} renderSubchartNotes - When true, also write wrappers in all nested subcharts. * @returns {string} A single "NOTES:" block with parent notes first, then subchart notes, or empty string if none found. */ function renderChartNotes(args, org, gvc, includeDebug, renderSubchartNotes) { // Capture the chart root from args const chartRoot = String(args.chart); // Prepare wrapper files at the root (and optionally all subcharts) prepareNotesWrappers(chartRoot, renderSubchartNotes); // Run Helm template without `--show-only` to render everything including our wrappers const fullYaml = runHelmTemplate(args, org, gvc, Boolean(includeDebug)); // Return early when the renderer output is empty if (fullYaml.length === 0) { return ''; } // Normalize Windows newlines for consistent parsing const text = fullYaml.replace(/\r\n/g, '\n'); // Split the stream into individual YAML documents on bare '---' separators const chunks = text.split(/\n---\s*\n/g); // Prepare a collection of extracted sections with ordering metadata const items = []; // Iterate each chunk as a potential wrapper document for (let i = 0; i < chunks.length; i++) { // Capture the current chunk string const chunk = chunks[i]; // Match the Helm Source header that ends with our wrapper filename and capture the path const match = chunk.match(/(^|\n)#\s*Source:\s+([^\n]*cpln_notes_wrapper\.yaml)\s*$/m); // Skip non-wrapper documents when no match is found if (!match) { continue; } // Extract the source path captured from the header const sourcePath = match[2]; // Compute charts depth by counting '/charts/' segments const chartsDepth = (sourcePath.match(/(?:^|\/)charts\//g) || []).length; // Initialize a doc holder let doc; // Attempt to parse the YAML document defensively try { doc = jsyaml.safeLoad(chunk); } catch (_a) { // Continue to next chunk on parse error continue; } // Skip documents that do not match our expected shape if (!doc || typeof doc !== 'object' || typeof doc.data !== 'string') { continue; } // Capture the notes body string const body = doc.data; // Skip empty bodies after trimming if (body.trim().length === 0) { continue; } // Push the extracted section with ordering metadata items.push({ body: body.trimEnd(), sourcePath: sourcePath, chartsDepth: chartsDepth, order: i, }); } // Return empty when no wrapper documents were found if (items.length === 0) { return ''; } // Sort by charts depth (root first), then preserve original Helm render order items.sort((a, b) => { // Place shallower depth first (0 = root) if (a.chartsDepth !== b.chartsDepth) { return a.chartsDepth - b.chartsDepth; } // Preserve original order among same-depth items return a.order - b.order; }); // Map the ordered items to their bodies const orderedBodies = items.map((it) => it.body); // Join sections with a single newline so they appear one after another under a single header const combined = orderedBodies.join('\n'); // Prefix a single "NOTES:" header for the entire block to match Helm UX return `NOTES:\n${combined}`; } exports.renderChartNotes = renderChartNotes; // Local // /** * Formats common Helm CLI options (values, sets, repo, version, post-renderer, etc.) into an array of flags. * * @param {Arguments<HelmSingleChart & HelmTemplateOptions & DebugOptions>} args - Parsed Helm arguments that includes helm related options. * @param {string} org - The organization string to inject as `--set` overrides. * @param {string | undefined} gvc - The optional GVC string to inject as `--set` overrides. * @param {boolean | undefined} includeDebug - Whether to append `--debug` explicitly. * @returns {string[]} A list of CLI flags suitable to append to `helm template` or `helm install`. */ function formatHelmCliOptions(args, org, gvc, includeDebug) { // Initialize the options array to collect CLI flags const options = []; // Prepare a list of custom key/value injections for org and global aliases const customOptions = [ // Placeholder comment for formating purposes `--set cpln.org=${org}`, `--set globals.cpln.org=${org}`, `--set global.cpln.org=${org}`, ]; // Push GVC variants only when provided if (gvc) { customOptions.push(`--set cpln.gvc=${gvc}`); customOptions.push(`--set globals.cpln.gvc=${gvc}`); customOptions.push(`--set global.cpln.gvc=${gvc}`); } // Normalize and append --set entries if present if (args.set && args.set.length > 0) { if (typeof args.set === 'string') { args.set = [args.set]; } options.push(args.set.map((set) => `--set ${safeSet(set)}`).join(' ')); } // Normalize and append --set-string entries if present if (args.setString) { if (typeof args.setString === 'string') { args.setString = [args.setString]; } options.push(args.setString.map((setString) => `--set-string ${safeSet(setString)}`).join(' ')); } // Normalize and append --set-file entries if present if (args.setFile) { if (typeof args.setFile === 'string') { args.setFile = [args.setFile]; } options.push(args.setFile.map((setFile) => `--set-file ${safeSet(setFile)}`).join(' ')); } // Append dependency update flag when requested if (args.dependencyUpdate) { options.push(`--dependency-update`); } // Append description if present (harmless on template, useful on install) if (args.description) { options.push(`--description ${shellEscape(args.description)}`); } // Append post-renderer binary path if present if (args.postRenderer) { options.push(`--post-renderer ${shellEscape(args.postRenderer)}`); } // Append post-renderer args if present if (args.postRendererArgs && args.postRendererArgs.length > 0) { if (typeof args.postRendererArgs === 'string') { args.postRendererArgs = [args.postRendererArgs]; } options.push(args.postRendererArgs.map((postRendererArgs) => `--post-renderer-args ${safeSet(postRendererArgs)}`).join(' ')); } // Append repo arg if present if (args.repo) { options.push(`--repo ${args.repo}`); } // Normalize and append --values entries if present if (args.values && args.values.length > 0) { if (typeof args.values === 'string') { args.values = [args.values]; } options.push(args.values.map((values) => `--values ${values}`).join(' ')); } // Append verify if requested if (args.verify) { options.push(`--verify`); } // Append version if provided if (args.version) { options.push(`--version ${shellEscape(args.version)}`); } // Append username if provided if (args.username) { options.push(`--username ${args.username}`); } // Append password if provided if (args.password) { options.push(`--password ${args.password}`); } // Append ca-file if provided if (args.caFile) { options.push(`--ca-file ${args.caFile}`); } // Append cert-file if provided if (args.certFile) { options.push(`--cert-file ${args.certFile}`); } // Append key-file if provided if (args.keyFile) { options.push(`--key-file ${args.keyFile}`); } // Append insecure-skip-tls-verify if provided if (args.insecureSkipTlsVerify) { options.push(`--insecure-skip-tls-verify`); } // Append debug only when explicitly requested if (includeDebug) { options.push(`--debug`); } // Append the custom org/gvc injections at the end options.push(customOptions.join(' ')); // Return the assembled list of flags return options; } function safeSet(input) { const index = input.indexOf('='); if (index === -1) { return input; } const key = input.substring(0, index); const value = input.substring(index + 1); return `"${key}"="${value}"`; } function applySetProperty(config, input) { if (!input) { return; } // Make sure we have an array const entries = Array.isArray(input) ? input : [input]; // Iterate over each entry and update config for (const entry of entries) { const index = entry.indexOf('='); // Skip entry if it doesn't have an equal sign, it will be caught later by helm template command if (index === -1) { continue; } const key = entry.substring(0, index); const value = entry.substring(index + 1); // Update helm config config[key] = value; } } function applySetFileProperty(config, input) { if (!input) { return; } // Make sure we have an array const entries = Array.isArray(input) ? input : [input]; // Iterate over each entry and update config for (const entry of entries) { const index = entry.indexOf('='); // Skip entry if it doesn't have an equal sign, it will be caught later by helm template command if (index === -1) { continue; } const key = entry.substring(0, index); const value = entry.substring(index + 1); // Skip if the path in value doesn't exists, it will be caught later by helm template command if (!(0, fs_1.existsSync)(value)) { continue; } // Update helm config config[key] = (0, fs_1.readFileSync)(value, 'utf-8'); } } /** * Validates a Helm release name against length and DNS-1123 label constraints. * * @param {string} name - The candidate release name to validate. * @returns {void} Returns normally if the name is valid. * @throws {Error} If `name` exceeds the maximum allowed length. * @throws {Error} If `name` fails DNS-1123 label validation. */ function validateReleaseNameOrThrow(name) { // Check length against the Helm-conservative limit if (name.length > constants_1.CPLN_HELM_RELEASE_MAX_LENGTH) { throw new Error(`ERROR: Release name "${name}" exceeds max length of ${constants_1.CPLN_HELM_RELEASE_MAX_LENGTH} characters.`); } // Check DNS-1123 label compliance to ensure secret name safety if (!constants_1.CPLN_HELM_DNS1123_LABEL.test(name)) { throw new Error(`ERROR: Invalid release name "${name}". Must match DNS-1123 label (lowercase alphanumeric or "-", start/end with alphanumeric).`); } } /** * Generates a Helm-style release name for `--generate-name`, following the common pattern: * `<chart-name>-<unix-epoch-seconds>` (e.g., "demo-chart-1757437152"). * * @param {string} chartRef - The chart reference (path, oci://..., repo/chart, or URL) used to derive the chart name. * @returns {string} A DNS-1123 compliant release name. */ function generateHelmStyleReleaseName(chartRef) { // Extract a best-effort chart name from the reference (e.g., "repo/chart" -> "chart", "/path/demo-chart" -> "demo-chart") const rawChartName = deriveExpectedChartDirName(chartRef); // Sanitize the chart name to DNS-1123 label rules (lowercase alnum + "-", start/end alnum) const safeChartName = sanitizeDns1123(rawChartName); // Compute the current Unix epoch time in seconds (10 digits; matches observed Helm output style) const epochSeconds = Math.floor(Date.now() / 1000); // Compose the candidate name using "<chart>-<epoch>" let candidate = `${safeChartName}-${epochSeconds}`; // Return the finalized release name token return candidate; } /** * Derives the expected chart directory name from a chart reference. * * @param {string} ref - The chart reference (e.g., "bitnami/nginx", "oci://reg/org/chart", or a URL). * @returns {string} The last meaningful path segment that likely matches the extracted directory name. */ function deriveExpectedChartDirName(ref) { // Normalize the reference by trimming whitespace const r = ref.trim(); // Remove the OCI scheme if present for consistent parsing const withoutScheme = r.replace(/^oci:\/\//, ''); // Split on both forward and backward slashes to cover all path styles const parts = withoutScheme.split(/[\\/]/).filter(Boolean); // Return the final segment as the expected chart directory name return parts.length > 0 ? parts[parts.length - 1] : r; } /** * Sanitizes an arbitrary string into a DNS-1123 label (lowercase alnum + "-", start/end alnum). * * @param {string} input - The input string to sanitize. * @returns {string} A DNS-1123-compliant token (non-empty; 'chart' is used as the value if everything was stripped). */ function sanitizeDns1123(input) { // Lowercase and trim whitespace to normalize the token let out = String(input !== null && input !== void 0 ? input : '') .toLowerCase() .trim(); // Replace any character that is not [a-z0-9-] with a hyphen out = out.replace(/[^a-z0-9-]+/g, '-'); // Remove leading and trailing hyphens to satisfy start/end alphanumeric rule out = out.replace(/^-+/, '').replace(/-+$/, ''); // Provide a random name as a fallback token if the result is empty if (!out) { out = 'chart'; } // Return the sanitized token return out; } /** * Safely joins a base directory with a target path and prevents path traversal. * * @param {string} baseDir - The destination base directory. * @param {string} targetPath - The path from the archive header. * @returns {string} A resolved path guaranteed to be within `baseDir`. * @throws {Error} If the resolved path escapes `baseDir`. */ function safeJoin(baseDir, targetPath) { // Resolve the candidate path within the base directory const resolved = path.resolve(baseDir, targetPath); // Normalize the base directory for a consistent startsWith check const base = path.resolve(baseDir) + path.sep; // Throw if the resolved path would escape the base directory if (!resolved.startsWith(base)) { throw new Error(`Unsafe path detected in archive: ${targetPath}`); } // Return the safe, absolute path return resolved; } /** * Ensures a chart's dependencies are hydrated into its local `charts/`. * * @param {string} chartDir - Absolute path to a chart root. * @throws {Error} If both build and update fail. */ function runHelmDependency(chartDir) { var _a, _b, _c, _d, _e, _f; // Build the common exec options (pipe output; limit to 10MB; decode as utf8) const opts = { stdio: 'pipe', encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, }; // Build the `helm dependency build` command string const cmdBuild = `helm dependency build ${shellEscape(chartDir)}`; // Attempt the build path first (uses Chart.lock when present) try { // Execute the build command synchronously (0, child_process_1.execSync)(cmdBuild, opts); // Return early on success return; } catch (_) { // Swallow and try update next } // Build the `helm dependency update` command string const cmdUpdate = `helm dependency update --skip-refresh ${shellEscape(chartDir)}`; // Attempt the update path as a fallback try { // Execute the update command synchronously (0, child_process_1.execSync)(cmdUpdate, opts); } catch (e) { // Extract stderr if present const stderr = (_c = (_b = (_a = e === null || e === void 0 ? void 0 : e.stderr) === null || _a === void 0 ? void 0 : _a.toString) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : ''; // Extract stdout if present const stdout = (_f = (_e = (_d = e === null || e === void 0 ? void 0 : e.stdout) === null || _d === void 0 ? void 0 : _d.toString) === null || _e === void 0 ? void 0 : _e.call(_d)) !== null && _f !== void 0 ? _f : ''; // Helm sometimes mixes channels; combine for maximum signal const combined = `${stderr}${stdout}`.trim(); // Throw a concise error (avoid Node stack noise) throw new Error(combined || e.message || 'ERROR: helm dependency command failed for unknown reasons.'); } } /** * Prepares wrapper templates for charts that have NOTES, writing into the root * chart only or recursively into all nested subcharts when requested. * * @param {string} chartRoot - Absolute path to the top-level chart root. * @param {boolean} includeSubcharts - When true, also prepare wrappers in subcharts. * @returns {void} Returns after wrappers are written where applicable. */ function prepareNotesWrappers(chartRoot, includeSubcharts) { // Initialize the list of chart directories to process const charts = includeSubcharts ? findAllCharts(chartRoot) : [chartRoot]; // Iterate all selected charts for (const dir of charts) { // Mirror NOTES.txt into files/__cpln_notes.tpl const mirrored = mirrorNotesFile(dir); // Skip charts that do not have a NOTES.txt if (!mirrored) { continue; } // Write wrapper at templates/cpln_notes_wrapper.yaml const wrapperAbs = writeNotesWrapper(dir); // Skip if the wrapper failed to materialize for any reason if (!fs.existsSync(wrapperAbs) || !fs.statSync(wrapperAbs).isFile()) { continue; } } } /** * Finds all chart roots (root + nested subcharts at any depth) starting at `chartRoot`. * * @param {string} chartRoot - Absolute path to the top-level chart root. * @returns {string[]} Absolute paths to every discovered chart root. */ function findAllCharts(chartRoot) { // Initialize a breadth-first queue with the top-level chart const queue = [chartRoot]; // Initialize the collection of discovered charts const charts = []; // Walk the tree breadth-first while (queue.length > 0) { // Dequeue the next chart directory const dir = queue.shift(); // Record this chart directory charts.push(dir); // Compute `charts/` under this chart const chartsDir = path.join(dir, 'charts'); // Skip if no `charts/` directory if (!fs.existsSync(chartsDir) || !fs.statSync(chartsDir).isDirectory()) { continue; } // Read child entries const entries = fs.readdirSync(chartsDir, { withFileTypes: true }); // Enqueue any subchart directories for (const ent of entries) { if (!ent.isDirectory()) { continue; } const sub = path.join(chartsDir, ent.name); queue.push(sub); } } // Return the list of all chart roots return charts; } /** * Writes a minimal wrapper template at `templates/cpln_notes_wrapper.yaml` that renders the mirrored file into `data: |-`. * * @param {string} chartDir - Absolute path to a chart root. * @returns {string} Absolute path to the wrapper file. */ function writeNotesWrapper(chartDir) { // Compute wrapper absolute path const wrapperAbs = path.join(chartDir, 'templates', 'cpln_notes_wrapper.yaml'); // Compose wrapper contents const contents = ['data: |-', ' {{- tpl (.Files.Get "files/__cpln_notes.tpl") . | nindent 2 }}', ''].join('\n'); // Ensure parent directory exists fs.mkdirSync(path.dirname(wrapperAbs), { recursive: true }); // Write the wrapper file fs.writeFileSync(wrapperAbs, contents, 'utf8'); // Return absolute path to wrapper return wrapperAbs; } /** * Mirrors `templates/NOTES.txt` to `files/__cpln_notes.tpl` within `chartDir` so `.Files.Get` can read it. * * @param {string} chartDir - Absolute path to a chart root. * @returns {string | undefined} Absolute path to the mirrored file or undefined if NOTES.txt doesn't exist. */ function mirrorNotesFile(chartDir) { // Compute absolute path to source NOTES.txt const src = path.join(chartDir, 'templates', 'NOTES.txt'); // Return early if NOTES.txt is absent if (!fs.existsSync(src) || !fs.statSync(src).isFile()) { return undefined; } // Compute destination `files/` directory const filesDir = path.join(chartDir, 'files'); // Ensure `files/` directory exists fs.mkdirSync(filesDir, { recursive: true }); // Compute destination path under files/ const dest = path.join(filesDir, '__cpln_notes.tpl'); // Read NOTES.txt contents const txt = fs.readFileSync(src, 'utf8'); // Write mirrored template file fs.writeFileSync(dest, txt, 'utf8'); // Return destination path return dest; } exports.mirrorNotesFile = mirrorNotesFile; //# sourceMappingURL=functions.js.map