-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Firebase Functions can handle an Extensions outage #9986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8cb3e4d
81c5cb7
9a9dd4d
97c2479
4405ce4
1ca6b5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
Check warning on line 84 in src/deploy/extensions/prepare.spec.ts
|
||
|
|
||
| // Expect successful completion | ||
| await expect(prepareDynamicExtensions(context, options, payload, builds)).to.not.be.rejected; | ||
|
|
||
| wantDynamicStub.restore(); | ||
| v2apistub.restore(); | ||
| tosStub.restore(); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
|
Comment on lines
186
to
193
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This To make the error handling more robust, we should only suppress server-side errors (HTTP status >= 500) that are indicative of an actual outage. Other client-side errors should be re-thrown so the user is properly notified and can address the underlying issue. } catch (err: unknown) {
if (err instanceof FirebaseError && err.status >= 500) {
logLabeledError(
"extensions",
"Firebase Extensions is having an outage. Skipping extensions from functions codebase.",
);
return;
}
throw err;
} |
||
|
|
||
| if (Object.keys(extensions).length === 0 && haveExtensions.length === 0) { | ||
| // Nothing defined, and nothing to delete | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.