UNPKG

@catladder/cli

Version:

Panter cli tool for cloud CI/CD and DevOps

237 lines (224 loc) 6.88 kB
import { getSecretVarName } from "@catladder/pipeline"; import { stripIndents } from "common-tags"; import { difference } from "lodash"; import type { CommandInstance } from "vorpal"; import type Vorpal from "vorpal"; import { getAllComponentsWithAllEnvsHierarchical, getEnvironment, getEnvVarsResolved, getJobOnlyEnvVarsResolved, getProjectComponents, parseChoice, } from "../../../../config/getProjectConfig"; import { editAsFile } from "../../../../utils/editAsFile"; import { upsertAllVariables } from "../../../../utils/gitlab"; import { delay } from "../../../../utils/promise"; import { allEnvsAndAllComponents } from "./utils/autocompletions"; type Vars = { [env: string]: { [componentName: string]: Record<string, string>; }; }; /* for convenience, parse json objects. that makes it easier to edit secrets that are object */ const resolveJson = (v: Vars) => Object.fromEntries( Object.entries(v).map(([componentName, envs]) => { return [ componentName, Object.fromEntries( Object.entries(envs).map(([env, secrets]) => [ env, Object.fromEntries( Object.entries(secrets).map(([key, value]) => { try { return [key, JSON.parse(value)]; } catch (e) { return [key, value]; } }), ), ]), ), ]; }), ); const getSecretEnvVarKeysToConfigure = async ( env: string, componentName: string, ) => { const { secretEnvVarKeys, jobOnlyVars } = await getEnvironment( env, componentName, ); return [ ...jobOnlyVars.build.secretEnvVarKeys, ...jobOnlyVars.deploy.secretEnvVarKeys, ...secretEnvVarKeys, ] .filter((k) => !k.hidden) .map((k) => k.key); }; const getEnvVarsToEdit = async ( instance: CommandInstance, env: string, componentName: string, ) => { const secretEnvVarKeys = await getSecretEnvVarKeysToConfigure( env, componentName, ); const normalEnvVars = await getEnvVarsResolved(instance, env, componentName); const jobOnlyEnvVars = await getJobOnlyEnvVarsResolved( instance, env, componentName, ); const allEnvVars = { ...normalEnvVars, ...jobOnlyEnvVars, }; return Object.fromEntries( secretEnvVarKeys.map((key) => { const value = allEnvVars[key]; // due to some quirky way to resolve these variables, unset variables have the $CL_ prefix, so we remove thouse here const variableIsNotSet = value === "$" + getSecretVarName(env, componentName, key); return [key, variableIsNotSet ? "🚨 FILL ME" : value]; }), ); }; const doItFor = async ( instance: CommandInstance, envAndComponents: { [componentName: string]: string[]; }, ) => { let valuesToEdit: Vars = Object.fromEntries( await Promise.all( Object.entries(envAndComponents).map(async ([componentName, envs]) => [ componentName, Object.fromEntries( await Promise.all( envs.map(async (env) => [ env, await getEnvVarsToEdit(instance, env, componentName), ]), ), ), ]), ), ); let hasErrors = true; while (hasErrors) { valuesToEdit = await editAsFile( resolveJson(valuesToEdit), stripIndents` Please fill in all secrets for: ${Object.entries(envAndComponents) .map( ([componentName, envs]) => `- ${componentName}: ${envs.join(", ")}`, ) .join("\n")} `, ); // check for errors hasErrors = false; for (const [componentName, envs] of Object.entries(envAndComponents)) { for (const env of envs) { const usedKeys = valuesToEdit[componentName][env] ? Object.keys(valuesToEdit[componentName][env]) : []; // check whether newValues have the exact number of keys const secretEnvVarKeys = await getSecretEnvVarKeysToConfigure( env, componentName, ); const extranous = difference(usedKeys, secretEnvVarKeys); const missing = difference(secretEnvVarKeys, usedKeys); if (extranous.length > 0 || missing.length > 0) { instance.log(""); instance.log( `😿 Oh no! There is something wrong with "${componentName}"`, ); instance.log(""); if (extranous.length > 0) { instance.log("these secrets are not declared in the config"); extranous.forEach((key) => instance.log(key)); instance.log(""); } if (missing.length > 0) { instance.log("these secrets have not been provided:"); missing.forEach((key) => instance.log(key)); instance.log(""); } await delay(1000); const { shouldContinue } = await instance.prompt({ default: true, message: "Try again? 🤔", name: "shouldContinue", type: "confirm", }); if (!shouldContinue) { throw new Error("abort"); } hasErrors = true; } } } } instance.log(""); instance.log("upserting all variables, please wait..."); instance.log(""); for (const [componentName, envs] of Object.entries(envAndComponents)) { for (const env of envs) { instance.log("upserting " + env + ":" + componentName + "...\n"); await upsertAllVariables( instance, valuesToEdit[componentName][env], env, componentName, ); instance.log(""); instance.log("✅ " + env + ":" + componentName); instance.log("--------------------------------\n"); } } instance.log("done! 😻"); instance.log(""); }; export const projectConfigSecrets = async ( vorpal: CommandInstance, envComponent?: string, ) => { if (!envComponent) { const allEnvAndcomponents = await getAllComponentsWithAllEnvsHierarchical(); await doItFor(vorpal, allEnvAndcomponents); } else { const { env, componentName } = parseChoice(envComponent); // componentName can be null. in this case, iterate over all components if (!componentName) { const components = await getProjectComponents(); await doItFor( vorpal, Object.fromEntries(components.map((c) => [c, [env]])), ); } if (componentName) { await doItFor(vorpal, { [componentName]: [env], }); } } }; export default async (vorpal: Vorpal) => { vorpal .command( "project-config-secrets [envComponent]", "setup/update secrets stored in pass", ) .autocomplete(await allEnvsAndAllComponents()) .action(async function ({ envComponent }) { return await projectConfigSecrets(this, envComponent); }); };