@controlplane/cli
Version:
Control Plane Corporation CLI
976 lines • 45.4 kB
JavaScript
"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