firebase-tools
Version:
Command-Line Interface for Firebase
215 lines (214 loc) • 9.66 kB
JavaScript
;
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)}`);
}
}