Skip to content

Commit a15cc04

Browse files
authored
updates runv2.ts with listServices function and trigger updates (#9482)
* updates runv2.ts with listServices function and trigger updates
1 parent b897caa commit a15cc04

File tree

3 files changed

+171
-7
lines changed

3 files changed

+171
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Adds listServices and also defines trigger within runv2.ts [#9482]

src/gcp/runv2.spec.ts

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { expect } from "chai";
2+
import * as sinon from "sinon";
23

34
import * as runv2 from "./runv2";
45
import * as backend from "../deploy/functions/backend";
56
import { latest } from "../deploy/functions/runtimes/supported";
67
import { CODEBASE_LABEL } from "../functions/constants";
8+
import { Client } from "../apiv2";
9+
import { FirebaseError } from "../error";
710

811
describe("runv2", () => {
912
const PROJECT_ID = "project-id";
@@ -201,13 +204,11 @@ describe("runv2", () => {
201204
const service: Omit<runv2.Service, runv2.ServiceOutputFields> = {
202205
...BASE_RUN_SERVICE,
203206
name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`,
204-
labels: {
205-
[runv2.RUNTIME_LABEL]: latest("nodejs"),
206-
},
207207
annotations: {
208208
...BASE_RUN_SERVICE.annotations,
209209
[runv2.FUNCTION_ID_ANNOTATION]: FUNCTION_ID, // Using FUNCTION_ID_ANNOTATION as primary source for id
210210
[runv2.FUNCTION_TARGET_ANNOTATION]: "customEntryPoint",
211+
[runv2.TRIGGER_TYPE_ANNOTATION]: "HTTP_TRIGGER",
211212
},
212213
template: {
213214
containers: [
@@ -239,6 +240,7 @@ describe("runv2", () => {
239240
httpsTrigger: {},
240241
labels: {
241242
[runv2.RUNTIME_LABEL]: latest("nodejs"),
243+
[runv2.CLIENT_NAME_LABEL]: "firebase-functions",
242244
},
243245
environmentVariables: {},
244246
secretEnvironmentVariables: [],
@@ -259,6 +261,7 @@ describe("runv2", () => {
259261
...BASE_RUN_SERVICE.annotations,
260262
[runv2.FUNCTION_ID_ANNOTATION]: FUNCTION_ID, // Using FUNCTION_ID_ANNOTATION as primary source for id
261263
[runv2.FUNCTION_TARGET_ANNOTATION]: "customEntryPoint",
264+
[runv2.TRIGGER_TYPE_ANNOTATION]: "HTTP_TRIGGER",
262265
},
263266
template: {
264267
containers: [
@@ -442,7 +445,7 @@ describe("runv2", () => {
442445
entryPoint: SERVICE_ID, // No FUNCTION_TARGET_ANNOTATION
443446
availableMemoryMb: 128,
444447
cpu: 0.5,
445-
httpsTrigger: {},
448+
eventTrigger: { eventType: "unknown", retry: false },
446449
labels: {},
447450
environmentVariables: {},
448451
secretEnvironmentVariables: [],
@@ -452,4 +455,114 @@ describe("runv2", () => {
452455
expect(runv2.endpointFromService(service)).to.deep.equal(expectedEndpoint);
453456
});
454457
});
458+
459+
describe("listServices", () => {
460+
let sandbox: sinon.SinonSandbox;
461+
let getStub: sinon.SinonStub;
462+
463+
beforeEach(() => {
464+
sandbox = sinon.createSandbox();
465+
getStub = sandbox.stub(Client.prototype, "get");
466+
});
467+
468+
afterEach(() => {
469+
sandbox.restore();
470+
});
471+
472+
it("should return a list of services", async () => {
473+
const mockServices = [
474+
{
475+
name: "service1",
476+
labels: { "goog-managed-by": "cloud-functions" },
477+
},
478+
{
479+
name: "service2",
480+
labels: { "goog-managed-by": "firebase-functions" },
481+
},
482+
];
483+
getStub.resolves({ status: 200, body: { services: mockServices } });
484+
485+
const services = await runv2.listServices(PROJECT_ID);
486+
487+
expect(services).to.deep.equal(mockServices);
488+
expect(getStub).to.have.been.calledOnceWithExactly(
489+
`/projects/${PROJECT_ID}/locations/-/services`,
490+
{ queryParams: {} },
491+
);
492+
});
493+
494+
it("should handle pagination", async () => {
495+
const mockServices1 = [
496+
{
497+
name: "service1",
498+
labels: { "goog-managed-by": "cloud-functions" },
499+
},
500+
];
501+
const mockServices2 = [
502+
{
503+
name: "service2",
504+
labels: { "goog-managed-by": "firebase-functions" },
505+
},
506+
];
507+
getStub
508+
.onFirstCall()
509+
.resolves({ status: 200, body: { services: mockServices1, nextPageToken: "nextPage" } });
510+
getStub.onSecondCall().resolves({ status: 200, body: { services: mockServices2 } });
511+
512+
const services = await runv2.listServices(PROJECT_ID);
513+
514+
expect(services).to.deep.equal([...mockServices1, ...mockServices2]);
515+
expect(getStub).to.have.been.calledTwice;
516+
expect(getStub.firstCall).to.have.been.calledWithExactly(
517+
`/projects/${PROJECT_ID}/locations/-/services`,
518+
{ queryParams: {} },
519+
);
520+
expect(getStub.secondCall).to.have.been.calledWithExactly(
521+
`/projects/${PROJECT_ID}/locations/-/services`,
522+
{ queryParams: { pageToken: "nextPage" } },
523+
);
524+
});
525+
526+
it("should throw an error if the API call fails", async () => {
527+
getStub.resolves({ status: 500, body: "Internal Server Error" });
528+
529+
try {
530+
await runv2.listServices(PROJECT_ID);
531+
expect.fail("Should have thrown an error");
532+
} catch (err: any) {
533+
expect(err).to.be.instanceOf(FirebaseError);
534+
expect(err.message).to.contain("Failed to list services. HTTP Error: 500");
535+
}
536+
});
537+
538+
it("should filter for gcfv2 and firebase-managed services", async () => {
539+
const mockServices = [
540+
{
541+
name: "service1",
542+
labels: { "goog-managed-by": "cloud-functions" },
543+
},
544+
{
545+
name: "service2",
546+
labels: { "goog-managed-by": "firebase-functions" },
547+
},
548+
{
549+
name: "service3",
550+
labels: { "goog-managed-by": "other" },
551+
},
552+
{
553+
name: "service4",
554+
labels: {},
555+
},
556+
];
557+
getStub.resolves({ status: 200, body: { services: mockServices } });
558+
559+
const services = await runv2.listServices(PROJECT_ID);
560+
561+
expect(services).to.deep.equal([mockServices[0], mockServices[1]]);
562+
expect(getStub).to.have.been.calledOnceWithExactly(
563+
`/projects/${PROJECT_ID}/locations/-/services`,
564+
{ queryParams: {} },
565+
);
566+
});
567+
});
455568
});

src/gcp/runv2.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,49 @@ export async function updateService(service: Omit<Service, ServiceOutputFields>)
200200
return svc;
201201
}
202202

203+
/**
204+
* Lists Cloud Run services in the given project.
205+
*
206+
* This method only returns services with the "goog-managed-by" label set to
207+
* "cloud-functions" or "firebase-functions".
208+
*/
209+
export async function listServices(projectId: string): Promise<Service[]> {
210+
const allServices: Service[] = [];
211+
let pageToken: string | undefined = undefined;
212+
213+
do {
214+
const queryParams: Record<string, string> = {};
215+
if (pageToken) {
216+
queryParams["pageToken"] = pageToken;
217+
}
218+
219+
const res = await client.get<{ services?: Service[]; nextPageToken?: string }>(
220+
`/projects/${projectId}/locations/-/services`,
221+
{ queryParams },
222+
);
223+
224+
if (res.status !== 200) {
225+
throw new FirebaseError(`Failed to list services. HTTP Error: ${res.status}`, {
226+
original: res.body as any,
227+
});
228+
}
229+
230+
if (res.body.services) {
231+
for (const service of res.body.services) {
232+
if (
233+
service.labels?.[CLIENT_NAME_LABEL] === "cloud-functions" ||
234+
service.labels?.[CLIENT_NAME_LABEL] === "firebase-functions"
235+
) {
236+
allServices.push(service);
237+
}
238+
}
239+
}
240+
pageToken = res.body.nextPageToken;
241+
} while (pageToken);
242+
243+
return allServices;
244+
}
245+
203246
// TODO: Replace with real version:
204247
function functionNameToServiceName(id: string): string {
205248
return id.toLowerCase().replace(/_/g, "-");
@@ -487,9 +530,16 @@ export function endpointFromService(service: Omit<Service, ServiceOutputFields>)
487530
service.annotations?.[FUNCTION_TARGET_ANNOTATION] ||
488531
service.annotations?.[FUNCTION_ID_ANNOTATION] ||
489532
id,
490-
491-
// TODO: trigger types.
492-
httpsTrigger: {},
533+
// TODO: Figure out how to encode all trigger types to the underlying Run service that is compatible with both V2 functions and "direct to run" functions
534+
...(service.annotations?.[TRIGGER_TYPE_ANNOTATION] === "HTTP_TRIGGER"
535+
? { httpsTrigger: {} }
536+
: {
537+
eventTrigger: {
538+
eventType: service.annotations?.[TRIGGER_TYPE_ANNOTATION] || "unknown",
539+
// TODO: Figure out how to recover the retry info from Run (vs Functions API) as we currently default to false.
540+
retry: false,
541+
},
542+
}),
493543
};
494544
proto.renameIfPresent(endpoint, service.template, "concurrency", "containerConcurrency");
495545
proto.renameIfPresent(endpoint, service.labels || {}, "codebase", CODEBASE_LABEL);

0 commit comments

Comments
 (0)