UNPKG

firebase-tools

Version:
386 lines (385 loc) 20.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EVENTARC_SOURCE_ENV = void 0; exports.prepare = prepare; exports.inferDetailsFromExisting = inferDetailsFromExisting; exports.updateEndpointTargetedStatus = updateEndpointTargetedStatus; exports.inferBlockingDetails = inferBlockingDetails; exports.resolveCpuAndConcurrency = resolveCpuAndConcurrency; exports.loadCodebases = loadCodebases; exports.warnIfNewGenkitFunctionIsMissingSecrets = warnIfNewGenkitFunctionIsMissingSecrets; exports.ensureAllRequiredAPIsEnabled = ensureAllRequiredAPIsEnabled; const clc = require("colorette"); const proto = require("../../gcp/proto"); 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) { 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, context.config); 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; context.codebaseDeployEvents = {}; let runtimeConfig = { firebase: firebaseConfig }; const targetedCodebaseConfigs = context.config.filter((cfg) => codebases.includes(cfg.codebase)); if (checkAPIsEnabled[1] && targetedCodebaseConfigs.some(projectConfig_1.shouldUseRuntimeConfig)) { runtimeConfig = { ...runtimeConfig, ...(await (0, prepareFunctionsUpload_1.getFunctionsConfig)(projectId)) }; } context.hasRuntimeConfig = Object.keys(runtimeConfig).some((k) => k !== "firebase"); 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 localCfg = (0, projectConfig_1.requireLocal)(config, "Remote sources are not supported."); const userEnvOpt = { functionsSource: options.config.path(localCfg.source), projectId: projectId, projectAlias: options.projectAlias, }; proto.convertIfPresent(userEnvOpt, localCfg, "configDir", (cd) => options.config.path(cd)); const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt); const envs = { ...userEnvs, ...firebaseEnvs }; const { backend: wantBackend, envs: resolvedEnvs } = await build.resolveBackend({ build: wantBuild, firebaseConfig, userEnvs, nonInteractive: options.nonInteractive, isEmulator: false, }); functionsEnv.writeResolvedParams(resolvedEnvs, userEnvs, userEnvOpt); let hasEnvsFromParams = false; wantBackend.environmentVariables = envs; for (const envName of Object.keys(resolvedEnvs)) { const isList = resolvedEnvs[envName]?.legalList; const envValue = resolvedEnvs[envName]?.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 = { ...(wantBackend.environmentVariables || {}) }; let resource; if (endpoint.platform === "gcfv1") { resource = `projects/${endpoint.project}/locations/${endpoint.region}/functions/${endpoint.id}`; } else if (endpoint.platform === "gcfv2" || endpoint.platform === "run") { 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 cfg = (0, projectConfig_1.configForCodebase)(context.config, codebase); const localCfg = (0, projectConfig_1.requireLocal)(cfg, "Remote sources are not supported."); const sourceDirName = localCfg.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" || e.platform === "run")) { const schPathSet = new Set(); for (const e of backend.allEndpoints(wantBackend)) { if (backend.isDataConnectGraphqlTriggered(e) && e.dataConnectGraphqlTrigger.schemaFilePath) { schPathSet.add(e.dataConnectGraphqlTrigger.schemaFilePath); } } const exportType = backend.someEndpoint(wantBackend, (e) => e.platform === "run") ? "tar.gz" : "zip"; const packagedSource = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(options.config.projectDir, sourceDir, localCfg, [...schPathSet], undefined, { exportType }); source.functionsSourceV2 = packagedSource?.pathToSource; source.functionsSourceV2Hash = packagedSource?.hash; } if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv1")) { const configForUpload = (0, projectConfig_1.shouldUseRuntimeConfig)(localCfg) ? runtimeConfig : undefined; const packagedSource = await (0, prepareFunctionsUpload_1.prepareFunctionsUpload)(options.config.projectDir, sourceDir, localCfg, [], configForUpload); source.functionsSourceV1 = packagedSource?.pathToSource; source.functionsSourceV1Hash = 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 || []); validate.checkFiltersIntegrity(wantBackends, context.filters); (0, applyHash_1.applyBackendHashToBackends)(wantBackends, context); } function inferDetailsFromExisting(want, have, usedDotenv) { for (const wantE of backend.allEndpoints(want)) { const haveE = have.endpoints[wantE.region]?.[wantE.id]; if (!haveE) { continue; } wantE.runServiceId = haveE.runServiceId; if (!usedDotenv) { wantE.environmentVariables = { ...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); } } 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); } } } function inferBlockingDetails(want) { 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 = !!blockingEp.blockingTrigger.options?.accessToken); idToken || (idToken = !!blockingEp.blockingTrigger.options?.idToken); refreshToken || (refreshToken = !!blockingEp.blockingTrigger.options?.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; } } 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; } } } 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`); const codebaseRuntimeConfig = (0, projectConfig_1.shouldUseRuntimeConfig)(codebaseConfig) ? runtimeConfig : { firebase: firebaseConfig }; const discoveredBuild = await runtimeDelegate.discoverBuild(codebaseRuntimeConfig, { ...firebaseEnvs, GOOGLE_CLOUD_QUOTA_PROJECT: projectId, }); discoveredBuild.runtime = codebaseConfig.runtime; build.applyPrefix(discoveredBuild, codebaseConfig.prefix || ""); wantBuilds[codebase] = discoveredBuild; } return wantBuilds; } async function warnIfNewGenkitFunctionIsMissingSecrets(have, want, options) { if (options.force) { return; } const newAndMissingSecrets = backend.allEndpoints(backend.matchingBackend(want, (e) => { if (!backend.isCallableTriggered(e) || !e.callableTrigger.genkitAction) { return false; } if (e.secretEnvironmentVariables?.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"); } } } 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); } }