UNPKG

firebase-tools

Version:
215 lines (214 loc) • 9.66 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.deploy = deploy; exports.revisionTemplate = revisionTemplate; exports.localBuildTemplate = localBuildTemplate; const fs = require("fs"); const os = require("os"); const path = require("path"); const constants_1 = require("../../apphosting/constants"); const localbuilds_1 = require("../../apphosting/localbuilds"); const error_1 = require("../../error"); const artifactregistry = require("../../gcp/artifactregistry"); const runv2 = require("../../gcp/runv2"); const gcs = require("../../gcp/storage"); const getProjectNumber_1 = require("../../getProjectNumber"); const logger_1 = require("../../logger"); const utils_1 = require("../../utils"); const prepare_1 = require("../apphosting/prepare"); const util_1 = require("../apphosting/util"); const buildEnv_1 = require("./buildEnv"); const util_2 = require("./util"); const DEPLOY_MESSAGE_ANNOTATION = "firebase.google.com/deploy-message"; const ROLLOUT_TIMEOUT_MS = 10 * 60 * 1000; const NO_IMAGE = "scratch"; async function deploy(context, options, payload) { for (const svc of payload.run?.services || []) { if (svc.config.localBuild) { await deployLocalBuild(context, options, svc); } else { await deployRemoteBuild(context, options, svc); } } } async function deployRemoteBuild(context, options, svc) { const { projectId } = context; const source = await uploadSource(projectId, options, svc.config); const image = await buildImage(projectId, svc, source); const template = revisionTemplate(svc, image, options.message); svc.deployed = await rollOut(projectId, svc, template); } async function deployLocalBuild(context, options, svc) { const { projectId } = context; const scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), `run-local-build-${svc.config.serviceId}-`)); try { const build = await buildLocally(context, options, svc, scratchDir); const builtApp = await uploadBuiltApp(projectId, options, svc.config, scratchDir, build); const message = options.message; const template = localBuildTemplate(svc, build, builtApp, message); svc.deployed = await rollOut(projectId, svc, template); } finally { removeTempPath(scratchDir); } } async function buildLocally(context, options, svc, scratchDir) { const { config } = svc; const { serviceId, region } = config; const cfg = toAppHostingConfig(config); (0, localbuilds_1.validateLocalBuildNodeVersion)({ runtime: { value: svc.baseImage?.split("/").pop() } }, path.join(options.config.projectDir, cfg.rootDir)); (0, utils_1.logLabeledBullet)("run", `Starting local build for service ${serviceId} in ${region}`); const secrets = (0, buildEnv_1.secretNames)(svc.buildEnv || {}); if (secrets.length) { (0, utils_1.logLabeledBullet)("run", `Reading build secrets ${secrets.join(", ")} with your credentials. Their values may be ` + `included in the build output.`); } await (0, prepare_1.prepareLocalBuildScratchDirectory)(options.config.projectDir, scratchDir, cfg); const { outputFiles, buildConfig } = await (0, localbuilds_1.localBuild)(context.projectId, scratchDir, (0, buildEnv_1.toLocalBuildEnv)(svc.buildEnv || {}), { rootDir: config.rootDir, allowLocalBuildSecrets: true }); const env = (buildConfig.env || []) .filter((e) => e.variable && e.value !== undefined && (!e.availability || e.availability.includes("RUNTIME"))) .map((e) => ({ name: e.variable, value: e.value })); return { outputFiles, runCommand: buildConfig.runCommand, env }; } async function uploadSource(projectId, options, config) { const { serviceId, region } = config; const bucketName = await sourceBucket(projectId, options, region); const cfg = toAppHostingConfig(config); const archive = await (0, util_1.createSourceDeployArchive)(cfg, path.join(options.config.projectDir, cfg.rootDir)); (0, utils_1.logLabeledBullet)("run", `Uploading source for service ${serviceId} in ${region}...`); const { bucket, object } = await gcs.uploadObject({ file: archive, stream: fs.createReadStream(archive) }, bucketName, gcs.ContentType.ZIP); return { bucket, object }; } async function uploadBuiltApp(projectId, options, config, scratchDir, build) { const { serviceId, region } = config; const bucketName = await sourceBucket(projectId, options, region); const archive = await (0, util_1.createLocalBuildTarArchive)(toAppHostingConfig(config), scratchDir, build.outputFiles); (0, utils_1.logLabeledBullet)("run", `Uploading built app for service ${serviceId} in ${region}...`); const { bucket, object } = await gcs.uploadObject({ file: archive, stream: fs.createReadStream(archive) }, bucketName, gcs.ContentType.TAR, constants_1.CLOUD_RUN_SIZE_LIMIT_BYTES); return { bucket, object }; } async function sourceBucket(projectId, options, region) { const baseName = `firebase-run-src-${await (0, getProjectNumber_1.getProjectNumber)(options)}-${region.toLowerCase()}`; return gcs.upsertBucket({ product: "run", createMessage: `Creating Cloud Storage bucket in ${region} to store Cloud Run source code uploads at ${baseName}...`, projectId, req: { baseName, purposeLabel: `run-source-${region.toLowerCase()}`, location: region, lifecycle: { rule: [{ action: { type: "Delete" }, condition: { age: 30 } }] }, }, }); } async function buildImage(projectId, svc, source) { const { serviceId, region } = svc.config; await artifactregistry.ensureDockerRepository(projectId, region, "cloud-run-source-deploy"); const imageUri = `${region}-docker.pkg.dev/${projectId}/cloud-run-source-deploy/${serviceId}:${Date.now()}`; (0, utils_1.logLabeledBullet)("run", `Building service ${serviceId} in ${region}...`); const buildpackBuild = { environmentVariables: { ...(0, buildEnv_1.nonSecretValues)(svc.buildEnv || {}), X_GOOGLE_TARGET_PLATFORM: "fah", FIREBASE_OUTPUT_BUNDLE_DIR: "/workspace/.apphosting", }, }; if (svc.baseImage) { buildpackBuild.baseImage = svc.baseImage; buildpackBuild.enableAutomaticUpdates = true; } await runv2.submitBuild(projectId, region, { storageSource: source, imageUri, buildpackBuild }); return imageUri; } function revisionTemplate(svc, image, message) { const { serviceId, region } = svc.config; const template = svc.existing ? (0, utils_1.cloneDeep)(svc.existing.template) : { containers: [{ name: serviceId, image }] }; delete template.revision; const container = (0, util_2.mainContainer)(template); if (!container) { throw new error_1.FirebaseError(`Service ${serviceId} in ${region} has no containers.`); } container.image = image; if (container.sourceCode) { delete container.sourceCode; delete container.command; } if (svc.baseImage) { container.baseImageUri = svc.baseImage; } else { delete container.baseImageUri; } if (message) { template.annotations = { ...template.annotations, [DEPLOY_MESSAGE_ANNOTATION]: message }; } else if (template.annotations) { delete template.annotations[DEPLOY_MESSAGE_ANNOTATION]; } return template; } function localBuildTemplate(svc, build, builtApp, message) { const template = revisionTemplate(svc, NO_IMAGE, message); const container = (0, util_2.mainContainer)(template); container.sourceCode = { cloudStorageSource: builtApp }; const command = build.runCommand?.trim(); if (command) { container.command = command.split(/\s+/); } else { delete container.command; } if (build.env.length) { const buildNames = new Set(build.env.map((e) => e.name)); const otherEnv = (container.env || []).filter((e) => !buildNames.has(e.name)); container.env = [...otherEnv, ...build.env]; } return template; } function rollOut(projectId, svc, template) { if (svc.existing) { return deployRevision(svc.existing, template); } return createNewService(projectId, svc.config, template); } function createNewService(projectId, config, template) { const { serviceId, region } = config; return runv2.createService(projectId, region, serviceId, { name: `projects/${projectId}/locations/${region}/services/${serviceId}`, template, client: "cli-firebase", invokerIamDisabled: true, ingress: "INGRESS_TRAFFIC_ALL", }, { pollTimeoutMs: ROLLOUT_TIMEOUT_MS }); } function deployRevision(service, template) { const tags = (service.traffic || []) .filter((t) => t.tag) .map((t) => ({ type: t.type, revision: t.revision, tag: t.tag })); return runv2.updateService({ name: service.name, template, traffic: [{ type: "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST", percent: 100 }, ...tags], }, { updateMask: ["template", "traffic"], pollTimeoutMs: ROLLOUT_TIMEOUT_MS }); } function toAppHostingConfig(config) { return { backendId: config.serviceId, rootDir: config.rootDir || "", ignore: config.ignore, localBuild: config.localBuild, }; } function removeTempPath(tempPath) { try { fs.rmSync(tempPath, { recursive: true, force: true }); } catch (err) { logger_1.logger.debug(`Failed to clean up ${tempPath}: ${(0, error_1.getErrMsg)(err)}`); } }