Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
142 changes: 90 additions & 52 deletions src/deploy/functions/backend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import * as args from "./args";
import * as backend from "./backend";
import * as gcf from "../../gcp/cloudfunctions";
import * as gcfV2 from "../../gcp/cloudfunctionsv2";
import * as run from "../../gcp/runv2";
import * as experiments from "../../experiments";
import * as utils from "../../utils";
import * as projectConfig from "../../functions/projectConfig";

Expand Down Expand Up @@ -67,6 +69,41 @@ describe("Backend", () => {
updateTime: new Date(),
};

const RUN_SERVICE: run.Service = {
name: "projects/project/locations/region/services/id",
labels: {
"goog-managed-by": "cloud-functions",
"goog-cloudfunctions-runtime": "nodejs16",
"firebase-functions-codebase": "default",
},
annotations: {
"cloudfunctions.googleapis.com/function-id": "id",
"cloudfunctions.googleapis.com/trigger-type": "HTTP_TRIGGER",
},
template: {
containers: [
{
name: "worker",
image: "image",
env: [{ name: "FUNCTION_TARGET", value: "function" }],
resources: {
limits: {
cpu: "1",
memory: "256Mi",
},
},
},
],
containerConcurrency: 80,
},
generation: 1,
createTime: "2023-01-01T00:00:00Z",
updateTime: "2023-01-01T00:00:00Z",
creator: "user",
lastModifier: "user",
etag: "etag",
};

const HAVE_CLOUD_FUNCTION: gcf.CloudFunction = {
...CLOUD_FUNCTION,
buildId: "buildId",
Expand Down Expand Up @@ -126,18 +163,24 @@ describe("Backend", () => {
describe("existing backend", () => {
let listAllFunctions: sinon.SinonStub;
let listAllFunctionsV2: sinon.SinonStub;
let listServices: sinon.SinonStub;
let logLabeledWarning: sinon.SinonSpy;
let isEnabled: sinon.SinonStub;

beforeEach(() => {
listAllFunctions = sinon.stub(gcf, "listAllFunctions").rejects("Unexpected call");
listAllFunctionsV2 = sinon.stub(gcfV2, "listAllFunctions").rejects("Unexpected v2 call");
listServices = sinon.stub(run, "listServices").rejects("Unexpected run call");
logLabeledWarning = sinon.spy(utils, "logLabeledWarning");
isEnabled = sinon.stub(experiments, "isEnabled").returns(false);
});

afterEach(() => {
listAllFunctions.restore();
listAllFunctionsV2.restore();
listServices.restore();
logLabeledWarning.restore();
isEnabled.restore();
});

function newContext(): args.Context {
Expand Down Expand Up @@ -210,58 +253,6 @@ describe("Backend", () => {
);
});

it("should read v1 functions only when user is not allowlisted for v2", async () => {
listAllFunctions.onFirstCall().resolves({
functions: [
{
...HAVE_CLOUD_FUNCTION,
httpsTrigger: {},
},
],
unreachable: [],
});
listAllFunctionsV2.throws(
new FirebaseError("HTTP Error: 404, Method not found", { status: 404 }),
);

const have = await backend.existingBackend(newContext());

expect(have).to.deep.equal(backend.of({ ...ENDPOINT, httpsTrigger: {} }));
});

it("should throw an error if v2 list api throws an error", async () => {
listAllFunctions.onFirstCall().resolves({
functions: [],
unreachable: [],
});
listAllFunctionsV2.throws(
new FirebaseError("HTTP Error: 500, Internal Error", { status: 500 }),
);

await expect(backend.existingBackend(newContext())).to.be.rejectedWith(
"HTTP Error: 500, Internal Error",
);
});

it("should read v1 functions only when user is not allowlisted for v2", async () => {
listAllFunctions.onFirstCall().resolves({
functions: [
{
...HAVE_CLOUD_FUNCTION,
httpsTrigger: {},
},
],
unreachable: [],
});
listAllFunctionsV2.throws(
new FirebaseError("HTTP Error: 404, Method not found", { status: 404 }),
);

const have = await backend.existingBackend(newContext());

expect(have).to.deep.equal(backend.of({ ...ENDPOINT, httpsTrigger: {} }));
});

it("should read v2 functions when enabled", async () => {
listAllFunctions.onFirstCall().resolves({
functions: [],
Expand Down Expand Up @@ -318,6 +309,53 @@ describe("Backend", () => {

expect(have).to.deep.equal(want);
});

it("should read v2 functions from Cloud Run when experiment is enabled", async () => {
isEnabled.withArgs("functionsrunapionly").returns(true);
listAllFunctions.onFirstCall().resolves({
functions: [],
unreachable: [],
});
listServices.onFirstCall().resolves([RUN_SERVICE]);

const have = await backend.existingBackend(newContext());

const wantEndpoint = {
...ENDPOINT,
platform: "gcfv2" as const,
concurrency: 80,
cpu: 1,
httpsTrigger: {},
availableMemoryMb: 256 as const,
environmentVariables: {
FUNCTION_TARGET: "function",
},
labels: {
"goog-managed-by": "cloud-functions",
"goog-cloudfunctions-runtime": "nodejs16",
"firebase-functions-codebase": "default",
},
secretEnvironmentVariables: [],
};
delete wantEndpoint.state;

expect(have).to.deep.equal(backend.of(wantEndpoint));
expect(listAllFunctionsV2).to.not.have.been.called;
});

it("should handle Cloud Run list errors gracefully when experiment is enabled", async () => {
isEnabled.withArgs("functionsrunapionly").returns(true);
listAllFunctions.onFirstCall().resolves({
functions: [],
unreachable: [],
});
listServices.rejects(new Error("Random error"));

const context = newContext();
await backend.existingBackend(context);

expect(context.unreachableRegions?.run).to.deep.equal(["unknown"]);
});
});

describe("checkAvailability", () => {
Expand Down
27 changes: 18 additions & 9 deletions src/deploy/functions/backend.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as gcf from "../../gcp/cloudfunctions";
import * as gcfV2 from "../../gcp/cloudfunctionsv2";
import * as run from "../../gcp/runv2";
import * as utils from "../../utils";
import { Runtime } from "./runtimes/supported";
import { FirebaseError } from "../../error";
import { Context } from "./args";
import { assertExhaustive, flattenArray } from "../../functional";
import { logger } from "../../logger";
import * as experiments from "../../experiments";

/** Retry settings for a ScheduleSpec. */
export interface ScheduleRetryConfig {
Expand Down Expand Up @@ -180,7 +183,7 @@
return allMemoryOptions.includes(mem as MemoryOptions);
}

export function isValidEgressSetting(egress: unknown): egress is VpcEgressSettings {

Check warning on line 186 in src/deploy/functions/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
return egress === "PRIVATE_RANGES_ONLY" || egress === "ALL_TRAFFIC";
}

Expand Down Expand Up @@ -550,21 +553,27 @@
}
unreachableRegions.gcfV1 = gcfV1Results.unreachable;

let gcfV2Results;
try {
gcfV2Results = await gcfV2.listAllFunctions(ctx.projectId);
if (experiments.isEnabled("functionsrunapionly")) {
try {
const runServices = await run.listServices(ctx.projectId);
for (const service of runServices) {
const endpoint = run.endpointFromService(service);
existingBackend.endpoints[endpoint.region] =
existingBackend.endpoints[endpoint.region] || {};
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
}
} catch (err: any) {

Check warning on line 565 in src/deploy/functions/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
logger.debug(err.message);

Check warning on line 566 in src/deploy/functions/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .message on an `any` value

Check warning on line 566 in src/deploy/functions/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Error`
unreachableRegions.run = ["unknown"];
}
} else {
const gcfV2Results = await gcfV2.listAllFunctions(ctx.projectId);
for (const apiFunction of gcfV2Results.functions) {
const endpoint = gcfV2.endpointFromFunction(apiFunction);
existingBackend.endpoints[endpoint.region] = existingBackend.endpoints[endpoint.region] || {};
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
}
unreachableRegions.gcfV2 = gcfV2Results.unreachable;
} catch (err: any) {
if (err.status === 404 && err.message?.toLowerCase().includes("method not found")) {
// customer has preview enabled without allowlist set
} else {
throw err;
}
}

ctx.existingBackend = existingBackend;
Expand Down
2 changes: 1 addition & 1 deletion src/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import { prepareFrameworks } from "../frameworks";
import { Context as HostingContext } from "./hosting/context";
import { addPinnedFunctionsToOnlyString, hasPinnedFunctions } from "./hosting/prepare";
import { isRunningInGithubAction } from "../init/features/hosting/github";
import { isRunningInGithubAction } from "../utils";
import { TARGET_PERMISSIONS } from "../commands/deploy";
import { requirePermissions } from "../requirePermissions";
import { Options } from "../options";
Expand All @@ -46,9 +46,9 @@

export type DeployOptions = Options & { dryRun?: boolean };

type Chain = ((context: any, options: any, payload: any) => Promise<unknown>)[];

Check warning on line 49 in src/deploy/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 49 in src/deploy/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 49 in src/deploy/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

const chain = async function (fns: Chain, context: any, options: any, payload: any): Promise<void> {

Check warning on line 51 in src/deploy/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 51 in src/deploy/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 51 in src/deploy/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
for (const latest of fns) {
await latest(context, options, payload);
}
Expand Down
6 changes: 5 additions & 1 deletion src/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as leven from "leven";
import { basename } from "path";
import { configstore } from "./configstore";
import { FirebaseError } from "./error";
import { isRunningInGithubAction } from "./init/features/hosting/github";
import { isRunningInGithubAction } from "./utils";

export interface Experiment {
shortDescription: string;
Expand Down Expand Up @@ -62,6 +62,10 @@ export const ALL_EXPERIMENTS = experiments({
"Functions created using the V2 API target Cloud Run Functions (not production ready)",
public: false,
},
functionsrunapionly: {
shortDescription: "Use Cloud Run API to list v2 functions",
public: false,
},

// Emulator experiments
emulatoruisnapshot: {
Expand Down
2 changes: 1 addition & 1 deletion src/gcp/runv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as backend from "../deploy/functions/backend";
import { CODEBASE_LABEL } from "../functions/constants";
import { EnvVar, mebibytes, PlaintextEnvVar, SecretEnvVar } from "./k8s";
import { latest, Runtime } from "../deploy/functions/runtimes/supported";
import { logger } from "..";
import { logger } from "../logger";
import { partition } from "../functional";

export const API_VERSION = "v2";
Expand Down
4 changes: 0 additions & 4 deletions src/init/features/hosting/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,3 @@ async function encryptServiceAccountJSON(serviceAccountJSON: string, key: string
// Base64 the encrypted secret
return Buffer.from(encryptedBytes).toString("base64");
}

export function isRunningInGithubAction() {
return process.env.GITHUB_ACTION_REPOSITORY === HOSTING_GITHUB_ACTION_NAME.split("@")[0];
}
7 changes: 7 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,13 @@ export function isRunningInWSL(): boolean {
return !!process.env.WSL_DISTRO_NAME;
}

/**
* Indicates whether the end-user is running the CLI from a GitHub Action.
*/
export function isRunningInGithubAction(): boolean {
return process.env.GITHUB_ACTION_REPOSITORY === "FirebaseExtended/action-hosting-deploy";
}

/**
* Generates a date that is 30 days from Date.now()
*/
Expand Down
Loading