Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions src/commands/functions-list.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,50 @@
import { Command } from "../command";
import * as args from "../deploy/functions/args";
import { needProjectId } from "../projectUtils";
import { Options } from "../options";
import { requirePermissions } from "../requirePermissions";
import * as backend from "../deploy/functions/backend";
import { logger } from "../logger";
import * as Table from "cli-table3";
import { listServices, endpointFromService } from "../gcp/runv2";

export const command = new Command("functions:list")
.description("list all deployed functions in your Firebase project")
.before(requirePermissions, ["cloudfunctions.functions.list"])
.before(requirePermissions, ["run.services.list"])
.action(async (options: Options) => {
const context = {
projectId: needProjectId(options),
} as args.Context;
const existing = await backend.existingBackend(context);
const endpointsList = backend.allEndpoints(existing).sort(backend.compareFunctions);
const projectId = needProjectId(options);

let services: any[] = [];

Check warning on line 16 in src/commands/functions-list.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
try {
logger.info(`Listing functions in project ${projectId}...`);
services = await listServices(projectId);
} catch (err: any) {

Check warning on line 20 in src/commands/functions-list.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
logger.debug(`Failed to list services:`, err);
logger.error(
`Failed to list functions. Ensure you have the Cloud Run Admin API enabled and the necessary permissions.`,
);
return [];
}

if (services.length === 0) {
logger.info(`No functions found in project ${projectId}.`);
return [];
}

const endpointsList = services
.map((service) => endpointFromService(service))

Check warning on line 34 in src/commands/functions-list.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Omit<Service, ServiceOutputFields>`
.sort(backend.compareFunctions);

const table = new Table({
head: ["Function", "Version", "Trigger", "Location", "Memory", "Runtime"],
head: ["Function", "Platform", "Trigger", "Location", "Memory", "Runtime"],
style: { head: ["yellow"] },
});

for (const endpoint of endpointsList) {
const trigger = backend.endpointTriggerType(endpoint);
const availableMemoryMb = endpoint.availableMemoryMb || "---";
const entry = [
endpoint.id,
endpoint.platform === "gcfv2" ? "v2" : "v1",
endpoint.platform,
trigger,
endpoint.region,
availableMemoryMb,
Expand Down
79 changes: 74 additions & 5 deletions src/gcp/runv2.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { expect } from "chai";
import * as sinon from "sinon";

import * as runv2 from "./runv2";
import * as backend from "../deploy/functions/backend";
import { latest } from "../deploy/functions/runtimes/supported";
import { CODEBASE_LABEL } from "../functions/constants";
import { Client } from "../apiv2";
import { FirebaseError } from "../error";

describe("runv2", () => {
const PROJECT_ID = "project-id";
Expand Down Expand Up @@ -95,13 +98,13 @@
httpsTrigger: {},
environmentVariables: { FOO: "bar" },
};
const expectedServiceInput = JSON.parse(

Check warning on line 101 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
JSON.stringify({
...BASE_RUN_SERVICE,
name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${FUNCTION_ID.toLowerCase()}`,
}),
);
expectedServiceInput.template.containers[0].env.unshift({ name: "FOO", value: "bar" });

Check warning on line 107 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 107 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .template on an `any` value

expect(runv2.serviceFromEndpoint(endpoint, IMAGE_URI)).to.deep.equal(expectedServiceInput);
});
Expand All @@ -114,13 +117,13 @@
{ key: "MY_SECRET", secret: "secret-name", projectId: PROJECT_ID, version: "1" },
],
};
const expectedServiceInput = JSON.parse(

Check warning on line 120 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
JSON.stringify({
...BASE_RUN_SERVICE,
name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${FUNCTION_ID.toLowerCase()}`,
}),
);
expectedServiceInput.template.containers[0].env.unshift({

Check warning on line 126 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 126 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .template on an `any` value
name: "MY_SECRET",
valueSource: { secretKeyRef: { secret: "secret-name", version: "1" } },
});
Expand All @@ -134,7 +137,7 @@
minInstances: 1,
maxInstances: 10,
};
const expectedServiceInput = JSON.parse(

Check warning on line 140 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
JSON.stringify({
...BASE_RUN_SERVICE,
name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${FUNCTION_ID.toLowerCase()}`,
Expand Down Expand Up @@ -201,13 +204,11 @@
const service: Omit<runv2.Service, runv2.ServiceOutputFields> = {
...BASE_RUN_SERVICE,
name: `projects/${PROJECT_ID}/locations/${LOCATION}/services/${SERVICE_ID}`,
labels: {
[runv2.RUNTIME_LABEL]: latest("nodejs"),
},
annotations: {
...BASE_RUN_SERVICE.annotations,
[runv2.FUNCTION_ID_ANNOTATION]: FUNCTION_ID, // Using FUNCTION_ID_ANNOTATION as primary source for id
[runv2.FUNCTION_TARGET_ANNOTATION]: "customEntryPoint",
[runv2.TRIGGER_TYPE_ANNOTATION]: "HTTP_TRIGGER",
},
template: {
containers: [
Expand Down Expand Up @@ -239,6 +240,7 @@
httpsTrigger: {},
labels: {
[runv2.RUNTIME_LABEL]: latest("nodejs"),
[runv2.CLIENT_NAME_LABEL]: "firebase-functions",
},
environmentVariables: {},
secretEnvironmentVariables: [],
Expand All @@ -259,6 +261,7 @@
...BASE_RUN_SERVICE.annotations,
[runv2.FUNCTION_ID_ANNOTATION]: FUNCTION_ID, // Using FUNCTION_ID_ANNOTATION as primary source for id
[runv2.FUNCTION_TARGET_ANNOTATION]: "customEntryPoint",
[runv2.TRIGGER_TYPE_ANNOTATION]: "HTTP_TRIGGER",
},
template: {
containers: [
Expand Down Expand Up @@ -379,7 +382,7 @@
const service: runv2.Service = JSON.parse(JSON.stringify(BASE_RUN_SERVICE));
service.template.containers![0].env = [
{ name: "FOO", value: "bar" },
{
{

Check failure on line 385 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Delete `·`

Check failure on line 385 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Delete `·`

Check failure on line 385 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Delete `·`
name: "MY_SECRET",
valueSource: {
secretKeyRef: {
Expand Down Expand Up @@ -442,7 +445,7 @@
entryPoint: SERVICE_ID, // No FUNCTION_TARGET_ANNOTATION
availableMemoryMb: 128,
cpu: 0.5,
httpsTrigger: {},
eventTrigger: { eventType: "unknown", retry: false },
labels: {},
environmentVariables: {},
secretEnvironmentVariables: [],
Expand All @@ -452,4 +455,70 @@
expect(runv2.endpointFromService(service)).to.deep.equal(expectedEndpoint);
});
});

describe("listServices", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like its missing test on whether filtering by label works correctly

let sandbox: sinon.SinonSandbox;
let getStub: sinon.SinonStub;

beforeEach(() => {
sandbox = sinon.createSandbox();
getStub = sandbox.stub(Client.prototype, "get");
});

afterEach(() => {
sandbox.restore();
});

it("should return a list of services", async () => {
const mockServices = [

Check failure on line 473 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Replace `⏎········{·name:·"service1"·},⏎········{·name:·"service2"·},⏎······` with `{·name:·"service1"·},·{·name:·"service2"·}`

Check failure on line 473 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Replace `⏎········{·name:·"service1"·},⏎········{·name:·"service2"·},⏎······` with `{·name:·"service1"·},·{·name:·"service2"·}`

Check failure on line 473 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Replace `⏎········{·name:·"service1"·},⏎········{·name:·"service2"·},⏎······` with `{·name:·"service1"·},·{·name:·"service2"·}`
{ name: "service1" },
{ name: "service2" },
];
getStub.resolves({ status: 200, body: { services: mockServices } });

const services = await runv2.listServices(PROJECT_ID);

expect(services).to.deep.equal(mockServices);
expect(getStub).to.have.been.calledOnceWithExactly(
`/projects/${PROJECT_ID}/locations/-/services`,
{ queryParams: {} },
);
});

it("should handle pagination", async () => {
const mockServices1 = [{ name: "service1" }];
const mockServices2 = [{ name: "service2" }];
getStub
.onFirstCall()
.resolves({ status: 200, body: { services: mockServices1, nextPageToken: "nextPage" } });
getStub

Check failure on line 494 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Replace `⏎········.onSecondCall()⏎········` with `.onSecondCall()`

Check failure on line 494 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Replace `⏎········.onSecondCall()⏎········` with `.onSecondCall()`

Check failure on line 494 in src/gcp/runv2.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Replace `⏎········.onSecondCall()⏎········` with `.onSecondCall()`
.onSecondCall()
.resolves({ status: 200, body: { services: mockServices2 } });

const services = await runv2.listServices(PROJECT_ID);

expect(services).to.deep.equal([...mockServices1, ...mockServices2]);
expect(getStub).to.have.been.calledTwice;
expect(getStub.firstCall).to.have.been.calledWithExactly(
`/projects/${PROJECT_ID}/locations/-/services`,
{ queryParams: {} },
);
expect(getStub.secondCall).to.have.been.calledWithExactly(
`/projects/${PROJECT_ID}/locations/-/services`,
{ queryParams: { pageToken: "nextPage" } },
);
});

it("should throw an error if the API call fails", async () => {
getStub.resolves({ status: 500, body: "Internal Server Error" });

try {
await runv2.listServices(PROJECT_ID);
expect.fail("Should have thrown an error");
} catch (err: any) {
expect(err).to.be.instanceOf(FirebaseError);
expect(err.message).to.contain("Failed to list services: 500 Internal Server Error");
}
});
});
});
35 changes: 32 additions & 3 deletions src/gcp/runv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,34 @@
return svc;
}

export async function listServices(projectId: string): Promise<Service[]> {
let allServices: Service[] = [];
let pageToken: string | undefined = undefined;

do {
const queryParams: Record<string, string> = {};
if (pageToken) {
queryParams["pageToken"] = pageToken;
}

const res = await client.get<{ services?: Service[]; nextPageToken?: string }>(
`/projects/${projectId}/locations/-/services`,
{ queryParams },
);

if (res.status !== 200) {
throw new FirebaseError(`Failed to list services: ${res.status} ${res.body}`);
}

if (res.body.services) {
allServices = allServices.concat(res.body.services);
}
pageToken = res.body.nextPageToken;
} while (pageToken);

return allServices;
}

// TODO: Replace with real version:
function functionNameToServiceName(id: string): string {
return id.toLowerCase().replace(/_/g, "-");
Expand Down Expand Up @@ -486,10 +514,11 @@
env.find((e) => e.name === FUNCTION_TARGET_ENV)?.value ||
service.annotations?.[FUNCTION_TARGET_ANNOTATION] ||
service.annotations?.[FUNCTION_ID_ANNOTATION] ||

Check failure on line 517 in src/gcp/runv2.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Delete `⏎··`

Check failure on line 517 in src/gcp/runv2.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Delete `⏎··`

Check failure on line 517 in src/gcp/runv2.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Delete `⏎··`
id,

// TODO: trigger types.
httpsTrigger: {},
...(service.annotations?.[TRIGGER_TYPE_ANNOTATION] === "HTTP_TRIGGER"
? { httpsTrigger: {} }
: { eventTrigger: { eventType: service.annotations?.[TRIGGER_TYPE_ANNOTATION] || "unknown", retry: false } }),

Check failure on line 521 in src/gcp/runv2.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Replace `·eventTrigger:·{·eventType:·service.annotations?.[TRIGGER_TYPE_ANNOTATION]·||·"unknown",·retry:·false·}` with `⏎··········eventTrigger:·{⏎············eventType:·service.annotations?.[TRIGGER_TYPE_ANNOTATION]·||·"unknown",⏎············retry:·false,⏎··········},⏎·······`

Check failure on line 521 in src/gcp/runv2.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Replace `·eventTrigger:·{·eventType:·service.annotations?.[TRIGGER_TYPE_ANNOTATION]·||·"unknown",·retry:·false·}` with `⏎··········eventTrigger:·{⏎············eventType:·service.annotations?.[TRIGGER_TYPE_ANNOTATION]·||·"unknown",⏎············retry:·false,⏎··········},⏎·······`

Check failure on line 521 in src/gcp/runv2.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Replace `·eventTrigger:·{·eventType:·service.annotations?.[TRIGGER_TYPE_ANNOTATION]·||·"unknown",·retry:·false·}` with `⏎··········eventTrigger:·{⏎············eventType:·service.annotations?.[TRIGGER_TYPE_ANNOTATION]·||·"unknown",⏎············retry:·false,⏎··········},⏎·······`
};
proto.renameIfPresent(endpoint, service.template, "concurrency", "containerConcurrency");
proto.renameIfPresent(endpoint, service.labels || {}, "codebase", CODEBASE_LABEL);
Expand Down
Loading