diff --git a/src/deploy/extensions/prepare.spec.ts b/src/deploy/extensions/prepare.spec.ts new file mode 100644 index 00000000000..f8f6e8dc2a3 --- /dev/null +++ b/src/deploy/extensions/prepare.spec.ts @@ -0,0 +1,94 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { prepareDynamicExtensions } from "./prepare"; +import * as planner from "./planner"; +import * as projectUtils from "../../projectUtils"; +import * as extensionsHelper from "../../extensions/extensionsHelper"; +import * as requirePermissions from "../../requirePermissions"; +import { Context, Payload } from "./args"; +import * as v2FunctionHelper from "./v2FunctionHelper"; +import * as tos from "../../extensions/tos"; + +describe("Extensions prepare", () => { + describe("prepareDynamicExtensions", () => { + let haveDynamicStub: sinon.SinonStub; + let ensureExtensionsApiEnabledStub: sinon.SinonStub; + let requirePermissionsStub: sinon.SinonStub; + let needProjectIdStub: sinon.SinonStub; + let needProjectNumberStub: sinon.SinonStub; + + beforeEach(() => { + haveDynamicStub = sinon.stub(planner, "haveDynamic").resolves([]); + ensureExtensionsApiEnabledStub = sinon + .stub(extensionsHelper, "ensureExtensionsApiEnabled") + .resolves(); + requirePermissionsStub = sinon.stub(requirePermissions, "requirePermissions").resolves(); + needProjectIdStub = sinon.stub(projectUtils, "needProjectId").returns("test-project"); + needProjectNumberStub = sinon.stub(projectUtils, "needProjectNumber").resolves("123456"); + }); + + afterEach(() => { + haveDynamicStub.restore(); + ensureExtensionsApiEnabledStub.restore(); + requirePermissionsStub.restore(); + needProjectIdStub.restore(); + needProjectNumberStub.restore(); + }); + + it("should swallow errors and exit cleanly if the extensions API is down", async () => { + haveDynamicStub.rejects(new Error("Extensions API is having an outage")); + + const context: Context = {}; + const payload: Payload = {}; + const options: any = { + config: { + src: { functions: { source: "functions" } }, + }, + }; + const builds = {}; + + // This should not throw. + await expect(prepareDynamicExtensions(context, options, payload, builds)).to.not.be.rejected; + }); + + it("should proceed normally if extensions API is healthy", async () => { + haveDynamicStub.resolves([ + { + instanceId: "test-extension", + ref: { publisherId: "test", extensionId: "test", version: "0.1.0" }, + params: {}, + systemParams: {}, + labels: { codebase: "default" }, + }, + ]); + + const context: Context = {}; + const payload: Payload = {}; + const options: any = { + config: { + get: () => [], + src: { functions: { source: "functions" } }, + }, + rc: { getEtags: () => [] }, + dryRun: true, + }; + const builds = {}; + + const wantDynamicStub: sinon.SinonStub = sinon.stub(planner, "wantDynamic").resolves([]); + const v2apistub: sinon.SinonStub = sinon + .stub(v2FunctionHelper, "ensureNecessaryV2ApisAndRoles") + .resolves(); + const tosStub: sinon.SinonStub = sinon + .stub(tos, "getAppDeveloperTOSStatus") + .resolves({ lastAcceptedVersion: "1.0.0" } as any); + + // Expect successful completion + await expect(prepareDynamicExtensions(context, options, payload, builds)).to.not.be.rejected; + + wantDynamicStub.restore(); + v2apistub.restore(); + tosStub.restore(); + }); + }); +}); diff --git a/src/deploy/extensions/prepare.ts b/src/deploy/extensions/prepare.ts index cf3de21efa2..72a42cf1976 100644 --- a/src/deploy/extensions/prepare.ts +++ b/src/deploy/extensions/prepare.ts @@ -22,6 +22,7 @@ import { Build } from "../functions/build"; import { getEndpointFilters } from "../functions/functionsDeployHelper"; import { normalizeAndValidate } from "../../functions/projectConfig"; import { DeployOptions } from ".."; +import { logLabeledError } from "../../utils"; const matchesInstanceId = (dep: planner.InstanceSpec) => (test: planner.InstanceSpec) => { return dep.instanceId === test.instanceId; @@ -173,13 +174,23 @@ export async function prepareDynamicExtensions( const projectId = needProjectId(options); const projectNumber = await needProjectNumber(options); - await ensureExtensionsApiEnabled(options); - await requirePermissions(options, ["firebaseextensions.instances.list"]); + let haveExtensions: planner.DeploymentInstanceSpec[] = []; + try { + await ensureExtensionsApiEnabled(options); + await requirePermissions(options, ["firebaseextensions.instances.list"]); - let haveExtensions = await planner.haveDynamic(projectId); - haveExtensions = haveExtensions.filter((e) => - extensionMatchesAnyFilter(e.labels?.codebase, e.instanceId, filters), - ); + haveExtensions = await planner.haveDynamic(projectId); + haveExtensions = haveExtensions.filter((e) => + extensionMatchesAnyFilter(e.labels?.codebase, e.instanceId, filters), + ); + } catch (err) { + logLabeledError( + "extensions", + "Failed to fetch the list of extensions. Assuming for now that there are no existing extensions. " + + "If you are trying to install an extension through Firebase Functions this may fail later.", + ); + return; + } if (Object.keys(extensions).length === 0 && haveExtensions.length === 0) { // Nothing defined, and nothing to delete