@catladder/cli
Version:
Panter cli tool for cloud CI/CD and DevOps
237 lines (224 loc) • 6.88 kB
text/typescript
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);
});
};