UNPKG

firebase-tools

Version:
361 lines (360 loc) 19.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ensureAllRequiredAPIsEnabled = exports.warnIfNewGenkitFunctionIsMissingSecrets = exports.loadCodebases = exports.resolveCpuAndConcurrency = exports.inferBlockingDetails = exports.updateEndpointTargetedStatus = exports.inferDetailsFromExisting = exports.prepare = exports.EVENTARC_SOURCE_ENV = void 0; const clc = require("colorette"); const backend = require("./backend"); const build = require("./build"); const ensureApiEnabled = require("../../ensureApiEnabled"); const functionsConfig = require("../../functionsConfig"); const functionsEnv = require("../../functions/env"); const runtimes = require("./runtimes"); const supported = require("./runtimes/supported"); const validate = require("./validate"); const ensure = require("./ensure"); const api_1 = require("../../api"); const functionsDeployHelper_1 = require("./functionsDeployHelper"); const utils_1 = require("../../utils"); const prepareFunctionsUpload_1 = require("./prepareFunctionsUpload"); const prompts_1 = require("./prompts"); const projectUtils_1 = require("../../projectUtils"); const logger_1 = require("../../logger"); const triggerRegionHelper_1 = require("./triggerRegionHelper"); const checkIam_1 = require("./checkIam"); const error_1 = require("../../error"); const projectConfig_1 = require("../../functions/projectConfig"); const v1_1 = require("../../functions/events/v1"); const serviceusage_1 = require("../../gcp/serviceusage"); const applyHash_1 = require("./cache/applyHash"); const backend_1 = require("./backend"); const functional_1 = require("../../functional"); const prepare_1 = require("../extensions/prepare"); const prompt = require("../../prompt"); exports.EVENTARC_SOURCE_ENV = "EVENTARC_CLOUD_EVENT_SOURCE"; async function prepare(context, options, payload) { var _a, _b; const projectId = (0, projectUtils_1.needProjectId)(options); const projectNumber = await (0, projectUtils_1.needProjectNumber)(options); context.config = (0, projectConfig_1.normalizeAndValidate)(options.config.src.functions); context.filters = (0, functionsDeployHelper_1.getEndpointFilters)(options); const codebases = (0, functionsDeployHelper_1.targetCodebases)(context.config, context.filters); if (codebases.length === 0) { throw new error_1.FirebaseError("No function matches given --only filters. Aborting deployment."); } for (const codebase of codebases) { (0, utils_1.logLabeledBullet)("functions", `preparing codebase ${clc.bold(codebase)} for deployment`); } const checkAPIsEnabled = await Promise.all([ ensureApiEnabled.ensure(projectId, (0, api_1.functionsOrigin)(), "functions"), ensureApiEnabled.check(projectId, (0, api_1.runtimeconfigOrigin)(), "runtimeconfig", true), ensure.cloudBuildEnabled(projectId), ensureApiEnabled.ensure(projectId, (0, api_1.artifactRegistryDomain)(), "artifactregistry"), ]); const firebaseConfig = await functionsConfig.getFirebaseConfig(options); context.firebaseConfig = firebaseConfig; let runtimeConfig = { firebase: firebaseConfig }; if (checkAPIsEnabled[1]) { runtimeConfig = Object.assign(Object.assign({}, runtimeConfig), (await (0, prepareFunctionsUpload_1.getFunctionsConfig)(projectId))); } context.codebaseDeployEvents = {}; const wantBuilds = await loadCodebases(context.config, options, firebaseConfig, runtimeConfig, context.filters); if (Object.values(wantBuilds).some((b) => b.extensions)) { const extContext = {}; const extPayload = {}; await (0, prepare_1.prepareDynamicExtensions)(extContext, options, extPayload, wantBuilds); context.extensions = extContext; payload.extensions = extPayload; } const codebaseUsesEnvs = []; const wantBackends = {}; for (const [codebase, wantBuild] of Object.entries(wantBuilds)) { const config = (0, projectConfig_1.configForCodebase)(context.config, codebase); const firebaseEnvs = functionsEnv.loadFirebaseEnvs(firebaseConfig, projectId); const userEnvOpt = { functionsSource: options.config.path(config.source), projectId: projectId, projectAlias: options.projectAlias, }; const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt); const envs = Object.assign(Object.assign({}, userEnvs), firebaseEnvs); const { backend: wantBackend, envs: resolvedEnvs } = await build.resolveBackend({ build: wantBuild, firebaseConfig, userEnvOpt, userEnvs, nonInteractive: options.nonInteractive, isEmulator: false, }); let hasEnvsFromParams = false; wantBackend.environmentVariables = envs; for (const envName of Object.keys(resolvedEnvs)) { const isList = (_a = resolvedEnvs[envName]) === null || _a === void 0 ? void 0 : _a.legalList; const envValue = (_b = resolvedEnvs[envName]) === null || _b === void 0 ? void 0 : _b.toSDK(); if (envValue && !resolvedEnvs[envName].internal && (!Object.prototype.hasOwnProperty.call(wantBackend.environmentVariables, envName) || isList)) { wantBackend.environmentVariables[envName] = envValue; hasEnvsFromParams = true; } } for (const endpoint of backend.allEndpoints(wantBackend)) { endpoint.environmentVariables = Object.assign({}, wantBackend.environmentVariables) || {}; let resource; if (endpoint.platform === "gcfv1") { resource = `projects/${endpoint.project}/locations/${endpoint.region}/functions/${endpoint.id}`; } else if (endpoint.platform === "gcfv2") { resource = `projects/${endpoint.project}/locations/${endpoint.region}/services/${endpoint.id}`; } else { (0, functional_1.assertExhaustive)(endpoint.platform); } endpoint.environmentVariables[exports.EVENTARC_SOURCE_ENV] = resource; endpoint.codebase = codebase; } wantBackends[codebase] = wantBackend; if (functionsEnv.hasUserEnvs(userEnvOpt) || hasEnvsFromParams) { codebaseUsesEnvs.push(codebase); } context.codebaseDeployEvents[codebase] = { fn_deploy_num_successes: 0, fn_deploy_num_failures: 0, fn_deploy_num_canceled: 0, fn_deploy_num_skipped: 0, }; if (wantBuild.params.length > 0) { if (wantBuild.params.every((p) => p.type !== "secret")) { context.codebaseDeployEvents[codebase].params = "env_only"; } else { context.codebaseDeployEvents[codebase].params = "with_secrets"; } } else { context.codebaseDeployEvents[codebase].params = "none"; } context.codebaseDeployEvents[codebase].runtime = wantBuild.runtime; } validate.endpointsAreUnique(wantBackends); context.sources = {}; for (const [codebase, wantBackend] of Object.entries(wantBackends)) { const config = (0, projectConfig_1.configForCodebase)(context.config, codebase); const sourceDirName = config.source; const sourceDir = options.config.path(sourceDirName); const source = {}; if (backend.someEndpoint(wantBackend, () => true)) { (0, utils_1.logLabeledBullet)("functions", `preparing ${clc.bold(sourceDirName)} directory for uploading...`); } if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) { const packagedSource = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, config); source.functionsSourceV2 = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.pathToSource; source.functionsSourceV2Hash = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.hash; } if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv1")) { const packagedSource = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(sourceDir, config, runtimeConfig); source.functionsSourceV1 = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.pathToSource; source.functionsSourceV1Hash = packagedSource === null || packagedSource === void 0 ? void 0 : packagedSource.hash; } context.sources[codebase] = source; } payload.functions = {}; const haveBackends = (0, functionsDeployHelper_1.groupEndpointsByCodebase)(wantBackends, backend.allEndpoints(await backend.existingBackend(context))); for (const [codebase, wantBackend] of Object.entries(wantBackends)) { const haveBackend = haveBackends[codebase] || backend.empty(); payload.functions[codebase] = { wantBackend, haveBackend }; } for (const [codebase, { wantBackend, haveBackend }] of Object.entries(payload.functions)) { inferDetailsFromExisting(wantBackend, haveBackend, codebaseUsesEnvs.includes(codebase)); await (0, triggerRegionHelper_1.ensureTriggerRegions)(wantBackend); resolveCpuAndConcurrency(wantBackend); validate.endpointsAreValid(wantBackend); inferBlockingDetails(wantBackend); } const wantBackend = backend.merge(...Object.values(wantBackends)); const haveBackend = backend.merge(...Object.values(haveBackends)); await ensureAllRequiredAPIsEnabled(projectNumber, wantBackend); await warnIfNewGenkitFunctionIsMissingSecrets(wantBackend, haveBackend, options); const matchingBackend = backend.matchingBackend(wantBackend, (endpoint) => { return (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(endpoint, context.filters); }); await (0, prompts_1.promptForFailurePolicies)(options, matchingBackend, haveBackend); await (0, prompts_1.promptForMinInstances)(options, matchingBackend, haveBackend); await backend.checkAvailability(context, matchingBackend); await validate.secretsAreValid(projectId, matchingBackend); await (0, checkIam_1.ensureServiceAgentRoles)(projectId, projectNumber, matchingBackend, haveBackend, options.dryRun); await (0, checkIam_1.ensureGenkitMonitoringRoles)(projectId, projectNumber, matchingBackend, haveBackend, options.dryRun); await ensure.secretAccess(projectId, matchingBackend, haveBackend, options.dryRun); updateEndpointTargetedStatus(wantBackends, context.filters || []); (0, applyHash_1.applyBackendHashToBackends)(wantBackends, context); } exports.prepare = prepare; function inferDetailsFromExisting(want, have, usedDotenv) { var _a; for (const wantE of backend.allEndpoints(want)) { const haveE = (_a = have.endpoints[wantE.region]) === null || _a === void 0 ? void 0 : _a[wantE.id]; if (!haveE) { continue; } wantE.runServiceId = haveE.runServiceId; if (!usedDotenv) { wantE.environmentVariables = Object.assign(Object.assign({}, haveE.environmentVariables), wantE.environmentVariables); } if (typeof wantE.availableMemoryMb === "undefined" && haveE.availableMemoryMb) { wantE.availableMemoryMb = haveE.availableMemoryMb; } if (typeof wantE.cpu === "undefined" && haveE.cpu) { wantE.cpu = haveE.cpu; } wantE.securityLevel = haveE.securityLevel ? haveE.securityLevel : "SECURE_ALWAYS"; maybeCopyTriggerRegion(wantE, haveE); } } exports.inferDetailsFromExisting = inferDetailsFromExisting; function maybeCopyTriggerRegion(wantE, haveE) { if (!backend.isEventTriggered(wantE) || !backend.isEventTriggered(haveE)) { return; } if (wantE.eventTrigger.region || !haveE.eventTrigger.region) { return; } if (JSON.stringify(haveE.eventTrigger.eventFilters) !== JSON.stringify(wantE.eventTrigger.eventFilters)) { return; } wantE.eventTrigger.region = haveE.eventTrigger.region; } function updateEndpointTargetedStatus(wantBackends, endpointFilters) { for (const wantBackend of Object.values(wantBackends)) { for (const endpoint of (0, backend_1.allEndpoints)(wantBackend)) { endpoint.targetedByOnly = (0, functionsDeployHelper_1.endpointMatchesAnyFilter)(endpoint, endpointFilters); } } } exports.updateEndpointTargetedStatus = updateEndpointTargetedStatus; function inferBlockingDetails(want) { var _a, _b, _c; const authBlockingEndpoints = backend .allEndpoints(want) .filter((ep) => backend.isBlockingTriggered(ep) && v1_1.AUTH_BLOCKING_EVENTS.includes(ep.blockingTrigger.eventType)); if (authBlockingEndpoints.length === 0) { return; } let accessToken = false; let idToken = false; let refreshToken = false; for (const blockingEp of authBlockingEndpoints) { accessToken || (accessToken = !!((_a = blockingEp.blockingTrigger.options) === null || _a === void 0 ? void 0 : _a.accessToken)); idToken || (idToken = !!((_b = blockingEp.blockingTrigger.options) === null || _b === void 0 ? void 0 : _b.idToken)); refreshToken || (refreshToken = !!((_c = blockingEp.blockingTrigger.options) === null || _c === void 0 ? void 0 : _c.refreshToken)); } for (const blockingEp of authBlockingEndpoints) { if (!blockingEp.blockingTrigger.options) { blockingEp.blockingTrigger.options = {}; } blockingEp.blockingTrigger.options.accessToken = accessToken; blockingEp.blockingTrigger.options.idToken = idToken; blockingEp.blockingTrigger.options.refreshToken = refreshToken; } } exports.inferBlockingDetails = inferBlockingDetails; function resolveCpuAndConcurrency(want) { for (const e of backend.allEndpoints(want)) { if (e.platform === "gcfv1") { continue; } if (e.cpu === "gcf_gen1") { e.cpu = backend.memoryToGen1Cpu(e.availableMemoryMb || backend.DEFAULT_MEMORY); } else if (!e.cpu) { e.cpu = backend.memoryToGen2Cpu(e.availableMemoryMb || backend.DEFAULT_MEMORY); } if (!e.concurrency) { e.concurrency = e.cpu >= 1 ? backend.DEFAULT_CONCURRENCY : 1; } } } exports.resolveCpuAndConcurrency = resolveCpuAndConcurrency; async function loadCodebases(config, options, firebaseConfig, runtimeConfig, filters) { const codebases = (0, functionsDeployHelper_1.targetCodebases)(config, filters); const projectId = (0, projectUtils_1.needProjectId)(options); const wantBuilds = {}; for (const codebase of codebases) { const codebaseConfig = (0, projectConfig_1.configForCodebase)(config, codebase); const sourceDirName = codebaseConfig.source; if (!sourceDirName) { throw new error_1.FirebaseError(`No functions code detected at default location (./functions), and no functions source defined in firebase.json`); } const sourceDir = options.config.path(sourceDirName); const delegateContext = { projectId, sourceDir, projectDir: options.config.projectDir, runtime: codebaseConfig.runtime, }; const firebaseJsonRuntime = codebaseConfig.runtime; if (firebaseJsonRuntime && !supported.isRuntime(firebaseJsonRuntime)) { throw new error_1.FirebaseError(`Functions codebase ${codebase} has invalid runtime ` + `${firebaseJsonRuntime} specified in firebase.json. Valid values are: \n` + Object.keys(supported.RUNTIMES) .map((s) => `- ${s}`) .join("\n")); } const runtimeDelegate = await runtimes.getRuntimeDelegate(delegateContext); logger_1.logger.debug(`Validating ${runtimeDelegate.language} source`); supported.guardVersionSupport(runtimeDelegate.runtime); await runtimeDelegate.validate(); logger_1.logger.debug(`Building ${runtimeDelegate.language} source`); await runtimeDelegate.build(); const firebaseEnvs = functionsEnv.loadFirebaseEnvs(firebaseConfig, projectId); (0, utils_1.logLabeledBullet)("functions", `Loading and analyzing source code for codebase ${codebase} to determine what to deploy`); wantBuilds[codebase] = await runtimeDelegate.discoverBuild(runtimeConfig, Object.assign(Object.assign({}, firebaseEnvs), { GOOGLE_CLOUD_QUOTA_PROJECT: projectId })); wantBuilds[codebase].runtime = codebaseConfig.runtime; } return wantBuilds; } exports.loadCodebases = loadCodebases; async function warnIfNewGenkitFunctionIsMissingSecrets(have, want, options) { if (options.force) { return; } const newAndMissingSecrets = backend.allEndpoints(backend.matchingBackend(want, (e) => { var _a; if (!backend.isCallableTriggered(e) || !e.callableTrigger.genkitAction) { return false; } if ((_a = e.secretEnvironmentVariables) === null || _a === void 0 ? void 0 : _a.length) { return false; } return !backend.hasEndpoint(have)(e); })); if (newAndMissingSecrets.length) { const message = `The function(s) ${newAndMissingSecrets.map((e) => e.id).join(", ")} use Genkit but do not have access to a secret. ` + "This may cause the function to fail if it depends on an API key. To learn more about granting a function access to " + "secrets, see https://firebase.google.com/docs/functions/config-env?gen=2nd#secret_parameters. Continue?"; if (!(await prompt.confirm({ message, nonInteractive: options.nonInteractive }))) { throw new error_1.FirebaseError("Aborted"); } } } exports.warnIfNewGenkitFunctionIsMissingSecrets = warnIfNewGenkitFunctionIsMissingSecrets; async function ensureAllRequiredAPIsEnabled(projectNumber, wantBackend) { await Promise.all(Object.values(wantBackend.requiredAPIs).map(({ api }) => { return ensureApiEnabled.ensure(projectNumber, api, "functions", false); })); if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) { const V2_APIS = [(0, api_1.cloudRunApiOrigin)(), (0, api_1.eventarcOrigin)(), (0, api_1.pubsubOrigin)(), (0, api_1.storageOrigin)()]; const enablements = V2_APIS.map((api) => { return ensureApiEnabled.ensure(projectNumber, api, "functions"); }); await Promise.all(enablements); const services = ["pubsub.googleapis.com", "eventarc.googleapis.com"]; const generateServiceAccounts = services.map((service) => { return (0, serviceusage_1.generateServiceIdentity)(projectNumber, service, "functions"); }); await Promise.all(generateServiceAccounts); } if (backend.someEndpoint(wantBackend, (e) => !!(e.secretEnvironmentVariables && e.secretEnvironmentVariables.length > 0))) { await ensureApiEnabled.ensure(projectNumber, (0, api_1.secretManagerOrigin)(), "functions", false); } } exports.ensureAllRequiredAPIsEnabled = ensureAllRequiredAPIsEnabled;