forked from dienianindya/gsi_ess_mobile
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
212 lines
8.5 KiB
212 lines
8.5 KiB
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.updateEndpointSecret = exports.pruneAndDestroySecrets = exports.pruneSecrets = exports.inUse = exports.getSecretVersions = exports.of = exports.ensureSecret = exports.ensureValidKey = exports.labels = exports.isFirebaseManaged = void 0;
|
|
const utils = require("../utils");
|
|
const poller = require("../operation-poller");
|
|
const gcf = require("../gcp/cloudfunctions");
|
|
const secretManager_1 = require("../gcp/secretManager");
|
|
const error_1 = require("../error");
|
|
const utils_1 = require("../utils");
|
|
const prompt_1 = require("../prompt");
|
|
const env_1 = require("./env");
|
|
const logger_1 = require("../logger");
|
|
const api_1 = require("../api");
|
|
const functional_1 = require("../functional");
|
|
const FIREBASE_MANGED = "firebase-managed";
|
|
function isFirebaseManaged(secret) {
|
|
return Object.keys(secret.labels || []).includes(FIREBASE_MANGED);
|
|
}
|
|
exports.isFirebaseManaged = isFirebaseManaged;
|
|
function labels() {
|
|
return { [FIREBASE_MANGED]: "true" };
|
|
}
|
|
exports.labels = labels;
|
|
function toUpperSnakeCase(key) {
|
|
return key
|
|
.replace(/[.-]/g, "_")
|
|
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
.toUpperCase();
|
|
}
|
|
async function ensureValidKey(key, options) {
|
|
const transformedKey = toUpperSnakeCase(key);
|
|
if (transformedKey !== key) {
|
|
if (options.force) {
|
|
throw new error_1.FirebaseError("Secret key must be in UPPER_SNAKE_CASE.");
|
|
}
|
|
(0, utils_1.logWarning)(`By convention, secret key must be in UPPER_SNAKE_CASE.`);
|
|
const confirm = await (0, prompt_1.promptOnce)({
|
|
name: "updateKey",
|
|
type: "confirm",
|
|
default: true,
|
|
message: `Would you like to use ${transformedKey} as key instead?`,
|
|
}, options);
|
|
if (!confirm) {
|
|
throw new error_1.FirebaseError("Secret key must be in UPPER_SNAKE_CASE.");
|
|
}
|
|
}
|
|
try {
|
|
(0, env_1.validateKey)(transformedKey);
|
|
}
|
|
catch (err) {
|
|
throw new error_1.FirebaseError(`Invalid secret key ${transformedKey}`, { children: [err] });
|
|
}
|
|
return transformedKey;
|
|
}
|
|
exports.ensureValidKey = ensureValidKey;
|
|
async function ensureSecret(projectId, name, options) {
|
|
try {
|
|
const secret = await (0, secretManager_1.getSecret)(projectId, name);
|
|
if (!isFirebaseManaged(secret)) {
|
|
if (!options.force) {
|
|
(0, utils_1.logWarning)("Your secret is not managed by Firebase. " +
|
|
"Firebase managed secrets are automatically pruned to reduce your monthly cost for using Secret Manager. ");
|
|
const confirm = await (0, prompt_1.promptOnce)({
|
|
name: "updateLabels",
|
|
type: "confirm",
|
|
default: true,
|
|
message: `Would you like to have your secret ${secret.name} managed by Firebase?`,
|
|
}, options);
|
|
if (confirm) {
|
|
return (0, secretManager_1.patchSecret)(projectId, secret.name, Object.assign(Object.assign({}, secret.labels), labels()));
|
|
}
|
|
}
|
|
}
|
|
return secret;
|
|
}
|
|
catch (err) {
|
|
if (err.status !== 404) {
|
|
throw err;
|
|
}
|
|
}
|
|
return await (0, secretManager_1.createSecret)(projectId, name, labels());
|
|
}
|
|
exports.ensureSecret = ensureSecret;
|
|
function of(endpoints) {
|
|
return endpoints.reduce((envs, endpoint) => [...envs, ...(endpoint.secretEnvironmentVariables || [])], []);
|
|
}
|
|
exports.of = of;
|
|
function getSecretVersions(endpoint) {
|
|
return (endpoint.secretEnvironmentVariables || []).reduce((memo, { secret, version }) => {
|
|
memo[secret] = version || "";
|
|
return memo;
|
|
}, {});
|
|
}
|
|
exports.getSecretVersions = getSecretVersions;
|
|
function inUse(projectInfo, secret, endpoint) {
|
|
const { projectId, projectNumber } = projectInfo;
|
|
for (const sev of of([endpoint])) {
|
|
if ((sev.projectId === projectId || sev.projectId === projectNumber) &&
|
|
sev.secret === secret.name) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
exports.inUse = inUse;
|
|
async function pruneSecrets(projectInfo, endpoints) {
|
|
const { projectId, projectNumber } = projectInfo;
|
|
const pruneKey = (name, version) => `${name}@${version}`;
|
|
const prunedSecrets = new Set();
|
|
const haveSecrets = await (0, secretManager_1.listSecrets)(projectId, `labels.${FIREBASE_MANGED}=true`);
|
|
for (const secret of haveSecrets) {
|
|
const versions = await (0, secretManager_1.listSecretVersions)(projectId, secret.name, `NOT state: DESTROYED`);
|
|
for (const version of versions) {
|
|
prunedSecrets.add(pruneKey(secret.name, version.versionId));
|
|
}
|
|
}
|
|
const secrets = [];
|
|
for (const secret of of(endpoints)) {
|
|
if (!secret.version) {
|
|
throw new error_1.FirebaseError(`Secret ${secret.secret} version is unexpectedly empty.`);
|
|
}
|
|
if (secret.projectId === projectId || secret.projectId === projectNumber) {
|
|
if (secret.version) {
|
|
secrets.push(Object.assign(Object.assign({}, secret), { version: secret.version }));
|
|
}
|
|
}
|
|
}
|
|
for (const sev of secrets) {
|
|
let name = sev.secret;
|
|
if (name.includes("/")) {
|
|
const secret = (0, secretManager_1.parseSecretResourceName)(name);
|
|
name = secret.name;
|
|
}
|
|
let version = sev.version;
|
|
if (version === "latest") {
|
|
const resolved = await (0, secretManager_1.getSecretVersion)(projectId, name, version);
|
|
version = resolved.versionId;
|
|
}
|
|
prunedSecrets.delete(pruneKey(name, version));
|
|
}
|
|
return Array.from(prunedSecrets)
|
|
.map((key) => key.split("@"))
|
|
.map(([secret, version]) => ({ projectId, version, secret, key: secret }));
|
|
}
|
|
exports.pruneSecrets = pruneSecrets;
|
|
async function pruneAndDestroySecrets(projectInfo, endpoints) {
|
|
const { projectId, projectNumber } = projectInfo;
|
|
logger_1.logger.debug("Pruning secrets to find unused secret versions...");
|
|
const unusedSecrets = await module.exports.pruneSecrets({ projectId, projectNumber }, endpoints);
|
|
if (unusedSecrets.length === 0) {
|
|
return { destroyed: [], erred: [] };
|
|
}
|
|
const destroyed = [];
|
|
const erred = [];
|
|
const msg = unusedSecrets.map((s) => `${s.secret}@${s.version}`);
|
|
logger_1.logger.debug(`Found unused secret versions: ${msg}. Destroying them...`);
|
|
const destroyResults = await utils.allSettled(unusedSecrets.map(async (sev) => {
|
|
await (0, secretManager_1.destroySecretVersion)(sev.projectId, sev.secret, sev.version);
|
|
return sev;
|
|
}));
|
|
for (const result of destroyResults) {
|
|
if (result.status === "fulfilled") {
|
|
destroyed.push(result.value);
|
|
}
|
|
else {
|
|
erred.push(result.reason);
|
|
}
|
|
}
|
|
return { destroyed, erred };
|
|
}
|
|
exports.pruneAndDestroySecrets = pruneAndDestroySecrets;
|
|
async function updateEndpointSecret(projectInfo, secretVersion, endpoint) {
|
|
const { projectId, projectNumber } = projectInfo;
|
|
if (!inUse(projectInfo, secretVersion.secret, endpoint)) {
|
|
return endpoint;
|
|
}
|
|
const updatedSevs = [];
|
|
for (const sev of of([endpoint])) {
|
|
const updatedSev = Object.assign({}, sev);
|
|
if ((updatedSev.projectId === projectId || updatedSev.projectId === projectNumber) &&
|
|
updatedSev.secret === secretVersion.secret.name) {
|
|
updatedSev.version = secretVersion.versionId;
|
|
}
|
|
updatedSevs.push(updatedSev);
|
|
}
|
|
if (endpoint.platform === "gcfv1") {
|
|
const fn = gcf.functionFromEndpoint(endpoint, "");
|
|
const op = await gcf.updateFunction({
|
|
name: fn.name,
|
|
runtime: fn.runtime,
|
|
entryPoint: fn.entryPoint,
|
|
secretEnvironmentVariables: updatedSevs,
|
|
});
|
|
const gcfV1PollerOptions = {
|
|
apiOrigin: api_1.functionsOrigin,
|
|
apiVersion: gcf.API_VERSION,
|
|
masterTimeout: 25 * 60 * 1000,
|
|
maxBackoff: 10000,
|
|
pollerName: `update-${endpoint.region}-${endpoint.id}`,
|
|
operationResourceName: op.name,
|
|
};
|
|
const cfn = await poller.pollOperation(gcfV1PollerOptions);
|
|
return gcf.endpointFromFunction(cfn);
|
|
}
|
|
else if (endpoint.platform === "gcfv2") {
|
|
throw new error_1.FirebaseError(`Unsupported platform ${endpoint.platform}`);
|
|
}
|
|
else {
|
|
(0, functional_1.assertExhaustive)(endpoint.platform);
|
|
}
|
|
}
|
|
exports.updateEndpointSecret = updateEndpointSecret;
|