Skip to content

Commit a74bf9e

Browse files
authored
Adds cloud run api call in backend.ts to fetch functions (#9492)
* added cloud run call to fetch functions with experiment
1 parent 1ed4791 commit a74bf9e

File tree

7 files changed

+122
-68
lines changed

7 files changed

+122
-68
lines changed

src/deploy/functions/backend.spec.ts

Lines changed: 90 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import * as args from "./args";
66
import * as backend from "./backend";
77
import * as gcf from "../../gcp/cloudfunctions";
88
import * as gcfV2 from "../../gcp/cloudfunctionsv2";
9+
import * as run from "../../gcp/runv2";
10+
import * as experiments from "../../experiments";
911
import * as utils from "../../utils";
1012
import * as projectConfig from "../../functions/projectConfig";
1113

@@ -67,6 +69,41 @@ describe("Backend", () => {
6769
updateTime: new Date(),
6870
};
6971

72+
const RUN_SERVICE: run.Service = {
73+
name: "projects/project/locations/region/services/id",
74+
labels: {
75+
"goog-managed-by": "cloud-functions",
76+
"goog-cloudfunctions-runtime": "nodejs16",
77+
"firebase-functions-codebase": "default",
78+
},
79+
annotations: {
80+
"cloudfunctions.googleapis.com/function-id": "id",
81+
"cloudfunctions.googleapis.com/trigger-type": "HTTP_TRIGGER",
82+
},
83+
template: {
84+
containers: [
85+
{
86+
name: "worker",
87+
image: "image",
88+
env: [{ name: "FUNCTION_TARGET", value: "function" }],
89+
resources: {
90+
limits: {
91+
cpu: "1",
92+
memory: "256Mi",
93+
},
94+
},
95+
},
96+
],
97+
containerConcurrency: 80,
98+
},
99+
generation: 1,
100+
createTime: "2023-01-01T00:00:00Z",
101+
updateTime: "2023-01-01T00:00:00Z",
102+
creator: "user",
103+
lastModifier: "user",
104+
etag: "etag",
105+
};
106+
70107
const HAVE_CLOUD_FUNCTION: gcf.CloudFunction = {
71108
...CLOUD_FUNCTION,
72109
buildId: "buildId",
@@ -126,18 +163,24 @@ describe("Backend", () => {
126163
describe("existing backend", () => {
127164
let listAllFunctions: sinon.SinonStub;
128165
let listAllFunctionsV2: sinon.SinonStub;
166+
let listServices: sinon.SinonStub;
129167
let logLabeledWarning: sinon.SinonSpy;
168+
let isEnabled: sinon.SinonStub;
130169

131170
beforeEach(() => {
132171
listAllFunctions = sinon.stub(gcf, "listAllFunctions").rejects("Unexpected call");
133172
listAllFunctionsV2 = sinon.stub(gcfV2, "listAllFunctions").rejects("Unexpected v2 call");
173+
listServices = sinon.stub(run, "listServices").rejects("Unexpected run call");
134174
logLabeledWarning = sinon.spy(utils, "logLabeledWarning");
175+
isEnabled = sinon.stub(experiments, "isEnabled").returns(false);
135176
});
136177

137178
afterEach(() => {
138179
listAllFunctions.restore();
139180
listAllFunctionsV2.restore();
181+
listServices.restore();
140182
logLabeledWarning.restore();
183+
isEnabled.restore();
141184
});
142185

143186
function newContext(): args.Context {
@@ -210,58 +253,6 @@ describe("Backend", () => {
210253
);
211254
});
212255

213-
it("should read v1 functions only when user is not allowlisted for v2", async () => {
214-
listAllFunctions.onFirstCall().resolves({
215-
functions: [
216-
{
217-
...HAVE_CLOUD_FUNCTION,
218-
httpsTrigger: {},
219-
},
220-
],
221-
unreachable: [],
222-
});
223-
listAllFunctionsV2.throws(
224-
new FirebaseError("HTTP Error: 404, Method not found", { status: 404 }),
225-
);
226-
227-
const have = await backend.existingBackend(newContext());
228-
229-
expect(have).to.deep.equal(backend.of({ ...ENDPOINT, httpsTrigger: {} }));
230-
});
231-
232-
it("should throw an error if v2 list api throws an error", async () => {
233-
listAllFunctions.onFirstCall().resolves({
234-
functions: [],
235-
unreachable: [],
236-
});
237-
listAllFunctionsV2.throws(
238-
new FirebaseError("HTTP Error: 500, Internal Error", { status: 500 }),
239-
);
240-
241-
await expect(backend.existingBackend(newContext())).to.be.rejectedWith(
242-
"HTTP Error: 500, Internal Error",
243-
);
244-
});
245-
246-
it("should read v1 functions only when user is not allowlisted for v2", async () => {
247-
listAllFunctions.onFirstCall().resolves({
248-
functions: [
249-
{
250-
...HAVE_CLOUD_FUNCTION,
251-
httpsTrigger: {},
252-
},
253-
],
254-
unreachable: [],
255-
});
256-
listAllFunctionsV2.throws(
257-
new FirebaseError("HTTP Error: 404, Method not found", { status: 404 }),
258-
);
259-
260-
const have = await backend.existingBackend(newContext());
261-
262-
expect(have).to.deep.equal(backend.of({ ...ENDPOINT, httpsTrigger: {} }));
263-
});
264-
265256
it("should read v2 functions when enabled", async () => {
266257
listAllFunctions.onFirstCall().resolves({
267258
functions: [],
@@ -318,6 +309,53 @@ describe("Backend", () => {
318309

319310
expect(have).to.deep.equal(want);
320311
});
312+
313+
it("should read v2 functions from Cloud Run when experiment is enabled", async () => {
314+
isEnabled.withArgs("functionsrunapionly").returns(true);
315+
listAllFunctions.onFirstCall().resolves({
316+
functions: [],
317+
unreachable: [],
318+
});
319+
listServices.onFirstCall().resolves([RUN_SERVICE]);
320+
321+
const have = await backend.existingBackend(newContext());
322+
323+
const wantEndpoint = {
324+
...ENDPOINT,
325+
platform: "gcfv2" as const,
326+
concurrency: 80,
327+
cpu: 1,
328+
httpsTrigger: {},
329+
availableMemoryMb: 256 as const,
330+
environmentVariables: {
331+
FUNCTION_TARGET: "function",
332+
},
333+
labels: {
334+
"goog-managed-by": "cloud-functions",
335+
"goog-cloudfunctions-runtime": "nodejs16",
336+
"firebase-functions-codebase": "default",
337+
},
338+
secretEnvironmentVariables: [],
339+
};
340+
delete wantEndpoint.state;
341+
342+
expect(have).to.deep.equal(backend.of(wantEndpoint));
343+
expect(listAllFunctionsV2).to.not.have.been.called;
344+
});
345+
346+
it("should handle Cloud Run list errors gracefully when experiment is enabled", async () => {
347+
isEnabled.withArgs("functionsrunapionly").returns(true);
348+
listAllFunctions.onFirstCall().resolves({
349+
functions: [],
350+
unreachable: [],
351+
});
352+
listServices.rejects(new Error("Random error"));
353+
354+
const context = newContext();
355+
await backend.existingBackend(context);
356+
357+
expect(context.unreachableRegions?.run).to.deep.equal(["unknown"]);
358+
});
321359
});
322360

323361
describe("checkAvailability", () => {

src/deploy/functions/backend.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import * as gcf from "../../gcp/cloudfunctions";
22
import * as gcfV2 from "../../gcp/cloudfunctionsv2";
3+
import * as run from "../../gcp/runv2";
34
import * as utils from "../../utils";
45
import { Runtime } from "./runtimes/supported";
56
import { FirebaseError } from "../../error";
67
import { Context } from "./args";
78
import { assertExhaustive, flattenArray } from "../../functional";
9+
import { logger } from "../../logger";
10+
import * as experiments from "../../experiments";
811

912
/** Retry settings for a ScheduleSpec. */
1013
export interface ScheduleRetryConfig {
@@ -550,21 +553,27 @@ async function loadExistingBackend(ctx: Context): Promise<Backend> {
550553
}
551554
unreachableRegions.gcfV1 = gcfV1Results.unreachable;
552555

553-
let gcfV2Results;
554-
try {
555-
gcfV2Results = await gcfV2.listAllFunctions(ctx.projectId);
556+
if (experiments.isEnabled("functionsrunapionly")) {
557+
try {
558+
const runServices = await run.listServices(ctx.projectId);
559+
for (const service of runServices) {
560+
const endpoint = run.endpointFromService(service);
561+
existingBackend.endpoints[endpoint.region] =
562+
existingBackend.endpoints[endpoint.region] || {};
563+
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
564+
}
565+
} catch (err: any) {
566+
logger.debug(err.message);
567+
unreachableRegions.run = ["unknown"];
568+
}
569+
} else {
570+
const gcfV2Results = await gcfV2.listAllFunctions(ctx.projectId);
556571
for (const apiFunction of gcfV2Results.functions) {
557572
const endpoint = gcfV2.endpointFromFunction(apiFunction);
558573
existingBackend.endpoints[endpoint.region] = existingBackend.endpoints[endpoint.region] || {};
559574
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
560575
}
561576
unreachableRegions.gcfV2 = gcfV2Results.unreachable;
562-
} catch (err: any) {
563-
if (err.status === 404 && err.message?.toLowerCase().includes("method not found")) {
564-
// customer has preview enabled without allowlist set
565-
} else {
566-
throw err;
567-
}
568577
}
569578

570579
ctx.existingBackend = existingBackend;

src/deploy/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import * as AppHostingTarget from "./apphosting";
2121
import { prepareFrameworks } from "../frameworks";
2222
import { Context as HostingContext } from "./hosting/context";
2323
import { addPinnedFunctionsToOnlyString, hasPinnedFunctions } from "./hosting/prepare";
24-
import { isRunningInGithubAction } from "../init/features/hosting/github";
24+
import { isRunningInGithubAction } from "../utils";
2525
import { TARGET_PERMISSIONS } from "../commands/deploy";
2626
import { requirePermissions } from "../requirePermissions";
2727
import { Options } from "../options";

src/experiments.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as leven from "leven";
33
import { basename } from "path";
44
import { configstore } from "./configstore";
55
import { FirebaseError } from "./error";
6-
import { isRunningInGithubAction } from "./init/features/hosting/github";
6+
import { isRunningInGithubAction } from "./utils";
77

88
export interface Experiment {
99
shortDescription: string;
@@ -62,6 +62,10 @@ export const ALL_EXPERIMENTS = experiments({
6262
"Functions created using the V2 API target Cloud Run Functions (not production ready)",
6363
public: false,
6464
},
65+
functionsrunapionly: {
66+
shortDescription: "Use Cloud Run API to list v2 functions",
67+
public: false,
68+
},
6569

6670
// Emulator experiments
6771
emulatoruisnapshot: {

src/gcp/runv2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as backend from "../deploy/functions/backend";
1111
import { CODEBASE_LABEL } from "../functions/constants";
1212
import { EnvVar, mebibytes, PlaintextEnvVar, SecretEnvVar } from "./k8s";
1313
import { latest, Runtime } from "../deploy/functions/runtimes/supported";
14-
import { logger } from "..";
14+
import { logger } from "../logger";
1515
import { partition } from "../functional";
1616

1717
export const API_VERSION = "v2";

src/init/features/hosting/github.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,3 @@ async function encryptServiceAccountJSON(serviceAccountJSON: string, key: string
688688
// Base64 the encrypted secret
689689
return Buffer.from(encryptedBytes).toString("base64");
690690
}
691-
692-
export function isRunningInGithubAction() {
693-
return process.env.GITHUB_ACTION_REPOSITORY === HOSTING_GITHUB_ACTION_NAME.split("@")[0];
694-
}

src/utils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,13 @@ export function isRunningInWSL(): boolean {
582582
return !!process.env.WSL_DISTRO_NAME;
583583
}
584584

585+
/**
586+
* Indicates whether the end-user is running the CLI from a GitHub Action.
587+
*/
588+
export function isRunningInGithubAction(): boolean {
589+
return process.env.GITHUB_ACTION_REPOSITORY === "FirebaseExtended/action-hosting-deploy";
590+
}
591+
585592
/**
586593
* Generates a date that is 30 days from Date.now()
587594
*/

0 commit comments

Comments
 (0)