Skip to content
Open
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
2 changes: 2 additions & 0 deletions schema/firebase-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"definitions": {
"ActiveRuntime": {
"enum": [
"dart3",
"nodejs18",
"nodejs20",
"nodejs22",
Expand Down Expand Up @@ -913,6 +914,7 @@
},
"runtime": {
"enum": [
"dart3",
"nodejs18",
"nodejs20",
"nodejs22",
Expand Down
2 changes: 1 addition & 1 deletion src/deploy/functions/backend.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe("Backend", () => {
},
},
],
containerConcurrency: 80,
maxInstanceRequestConcurrency: 80,
},
generation: 1,
createTime: "2023-01-01T00:00:00Z",
Expand Down
5 changes: 5 additions & 0 deletions src/deploy/functions/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@
return allMemoryOptions.includes(mem as MemoryOptions);
}

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

Check warning on line 198 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 @@ -407,6 +407,11 @@

// State of the endpoint.
state?: EndpointState;

// Fields for Cloud Run platform (for no-build path)
baseImageUri?: string;
command?: string[];
args?: string[];
};

export interface RequiredAPI {
Expand Down Expand Up @@ -572,8 +577,8 @@
existingBackend.endpoints[endpoint.region] || {};
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
}
} catch (err: any) {

Check warning on line 580 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 581 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 581 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 {
Expand Down
17 changes: 12 additions & 5 deletions src/deploy/functions/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,8 @@
export type MemoryOption = 128 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768;
const allMemoryOptions: MemoryOption[] = [128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768];

// Run is an automatic migration from gcfv2 and is not used on the wire.
export type FunctionsPlatform = Exclude<backend.FunctionsPlatform, "run">;
export const AllFunctionsPlatforms: FunctionsPlatform[] = ["gcfv1", "gcfv2"];
export type FunctionsPlatform = backend.FunctionsPlatform;
export const AllFunctionsPlatforms: FunctionsPlatform[] = ["gcfv1", "gcfv2", "run"];
export type VpcEgressSetting = backend.VpcEgressSettings;
export const AllVpcEgressSettings: VpcEgressSetting[] = ["PRIVATE_RANGES_ONLY", "ALL_TRAFFIC"];
export type IngressSetting = backend.IngressSettings;
Expand All @@ -241,8 +240,8 @@
// Defaults to false. If true, the function will be ignored during the deploy process.
omit?: Field<boolean>;

// Defaults to "gcfv2". "Run" will be an additional option defined later
platform?: "gcfv1" | "gcfv2";
// Defaults to "gcfv2".
platform?: "gcfv1" | "gcfv2" | "run";

// Necessary for the GCF API to determine what code to load with the Functions Framework.
// Will become optional once "run" is supported as a platform
Expand Down Expand Up @@ -287,6 +286,11 @@
environmentVariables?: Record<string, string | Expression<string>> | null;
secretEnvironmentVariables?: SecretEnvVar[] | null;
labels?: Record<string, string | Expression<string>> | null;

// Fields for Cloud Run platform (for no-build path)
baseImageUri?: string;
command?: string[];
args?: string[];
};

type SecretParam = ReturnType<typeof defineSecret>;
Expand Down Expand Up @@ -474,7 +478,7 @@
// List param, we try resolving a String param instead.
try {
regions = params.resolveList(bdEndpoint.region, paramValues);
} catch (err: any) {

Check warning on line 481 in src/deploy/functions/build.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
if (err instanceof ExprParseError) {
regions = [params.resolveString(bdEndpoint.region, paramValues)];
} else {
Expand Down Expand Up @@ -503,6 +507,9 @@
"environmentVariables",
"labels",
"secretEnvironmentVariables",
"baseImageUri",
"command",
"args",
);
r.resolveStrings(bkEndpoint, bdEndpoint, "serviceAccount");

Expand Down
7 changes: 4 additions & 3 deletions src/deploy/functions/checkIam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
["iam.serviceAccounts.actAs"],
);
passed = iamResult.passed;
} catch (err: any) {

Check warning on line 44 in src/deploy/functions/checkIam.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
logger.debug("[functions] service account IAM check errored, deploy may fail:", err);
// we want to fail this check open and not rethrow since it's informational only
return;
Expand All @@ -61,7 +61,6 @@
/**
* Checks a functions deployment for HTTP function creation, and tests IAM
* permissions accordingly.
*
* @param context The deploy context.
* @param options The command-wide options object.
* @param payload The deploy payload.
Expand All @@ -74,11 +73,13 @@
if (!payload.functions) {
return;
}
const filters = context.filters || getEndpointFilters(options, context.config!);

Check warning on line 76 in src/deploy/functions/checkIam.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
const wantBackends = Object.values(payload.functions).map(({ wantBackend }) => wantBackend);
const httpEndpoints = [...flattenArray(wantBackends.map((b) => backend.allEndpoints(b)))]
.filter(backend.isHttpsTriggered || backend.isDataConnectGraphqlTriggered)
.filter((f) => endpointMatchesAnyFilter(f, filters));
.filter((f) => backend.isHttpsTriggered(f) || backend.isDataConnectGraphqlTriggered(f))
.filter((f) => endpointMatchesAnyFilter(f, filters))
// Services with platform: "run" are not GCFv1 or GCFv2 functions and are handled separately.
.filter((f) => f.platform !== "run");
Copy link
Contributor

Choose a reason for hiding this comment

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

q: why exclude run?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

iiuc GCF and Run have different permissions (cloudfunctions.functions.setIamPolicy vs. run.services.setIamPolicy) and the specific check where it is excluded here is for GCF permission

GCF: https://docs.cloud.google.com/functions/docs/reference/iam/permissions#functions
Run: https://docs.cloud.google.com/run/docs/reference/iam/permissions#services

For current no build mvp implementation, I think the current approach is implemented so that we try to set the policy in setInvoker (within fabricator.ts) and if it fails iiuc there should be an IAM error from Cloud Run


const existing = await backend.existingBackend(context);
const newHttpsEndpoints = httpEndpoints.filter(backend.missingEndpoint(existing));
Expand All @@ -97,7 +98,7 @@
try {
const iamResult = await iam.testIamPermissions(context.projectId, [PERMISSION]);
passed = iamResult.passed;
} catch (e: any) {

Check warning on line 101 in src/deploy/functions/checkIam.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
logger.debug(
"[functions] failed http create setIamPolicy permission check. deploy may fail:",
e,
Expand Down Expand Up @@ -130,7 +131,7 @@
}

/** Callback reducer function */
function reduceEventsToServices(services: Array<Service>, endpoint: backend.Endpoint) {

Check warning on line 134 in src/deploy/functions/checkIam.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
const service = serviceForEndpoint(endpoint);
if (service.requiredProjectBindings && !services.find((s) => s.name === service.name)) {
services.push(service);
Expand All @@ -149,7 +150,7 @@
* Finds the required project level IAM bindings for the Pub/Sub service agent.
* If the user enabled Pub/Sub on or before April 8, 2021, then we must enable the token creator role.
* @param projectNumber project number
* @param existingPolicy the project level IAM policy

Check warning on line 153 in src/deploy/functions/checkIam.ts

View workflow job for this annotation

GitHub Actions / lint (20)

@param "existingPolicy" does not match an existing function parameter
*/
export function obtainPubSubServiceAgentBindings(projectNumber: string): iam.Binding[] {
const serviceAccountTokenCreatorBinding: iam.Binding = {
Expand Down
6 changes: 3 additions & 3 deletions src/deploy/functions/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as experiments from "../../experiments";
import { findEndpoint } from "./backend";
import { deploy as extDeploy } from "../extensions";
import { getProjectNumber } from "../../getProjectNumber";
import * as path from "path";

setGracefulCleanup();

Expand Down Expand Up @@ -79,8 +80,7 @@ export async function uploadSourceV2(
),
};

// Legacy behavior: use the GCF API
if (!experiments.isEnabled("runfunctions")) {
if (!experiments.isEnabled("runfunctions") && !v2Endpoints.some((e) => e.platform === "run")) {
if (process.env.GOOGLE_CLOUD_QUOTA_PROJECT) {
logLabeledWarning(
"functions",
Expand Down Expand Up @@ -116,7 +116,7 @@ export async function uploadSourceV2(
},
},
});
const objectPath = `${source.functionsSourceV2Hash}.zip`;
const objectPath = `${source.functionsSourceV2Hash}${path.extname(source.functionsSourceV2!)}`;
await gcs.upload(
uploadOpts,
`${bucketName}/${objectPath}`,
Expand Down
7 changes: 6 additions & 1 deletion src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export async function prepare(
);
}

if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2")) {
if (backend.someEndpoint(wantBackend, (e) => e.platform === "gcfv2" || e.platform === "run")) {
const schPathSet = new Set<string>();
for (const e of backend.allEndpoints(wantBackend)) {
if (
Expand All @@ -233,11 +233,16 @@ export async function prepare(
schPathSet.add(e.dataConnectGraphqlTrigger.schemaFilePath);
}
}
const exportType = backend.someEndpoint(wantBackend, (e) => e.platform === "run")
? "tar.gz"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit* I think zip deploy may add support for .zip soonish?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see, do you think above would be best to add as a todo once they add the support or preemptively add it now?

: "zip";
const packagedSource = await prepareFunctionsUpload(
options.config.projectDir,
sourceDir,
localCfg,
[...schPathSet],
undefined,
{ exportType },
);
source.functionsSourceV2 = packagedSource?.pathToSource;
source.functionsSourceV2Hash = packagedSource?.hash;
Expand Down
10 changes: 7 additions & 3 deletions src/deploy/functions/prepareFunctionsUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,16 @@ async function packageSource(
config: projectConfig.ValidatedSingle,
additionalSources: string[],
runtimeConfig: any,
options?: { exportType: "zip" | "tar.gz" },
): Promise<PackagedSourceInfo | undefined> {
const tmpFile = tmp.fileSync({ prefix: "firebase-functions-", postfix: ".zip" }).name;
const exportType = options?.exportType || "zip";
const postfix = `.${exportType}`;
const tmpFile = tmp.fileSync({ prefix: "firebase-functions-", postfix }).name;
const fileStream = fs.createWriteStream(tmpFile, {
flags: "w",
encoding: "binary",
});
const archive = archiver("zip");
const archive = exportType === "tar.gz" ? archiver("tar", { gzip: true }) : archiver("zip");
const hashes: string[] = [];

// We must ignore firebase-debug.log or weird things happen if
Expand Down Expand Up @@ -154,8 +157,9 @@ export async function prepareFunctionsUpload(
config: projectConfig.ValidatedSingle,
additionalSources: string[],
runtimeConfig?: backend.RuntimeConfigValues,
options?: { exportType: "zip" | "tar.gz" },
): Promise<PackagedSourceInfo | undefined> {
return packageSource(projectDir, sourceDir, config, additionalSources, runtimeConfig);
return packageSource(projectDir, sourceDir, config, additionalSources, runtimeConfig, options);
}

export function convertToSortedKeyValueArray(config: any): SortedConfig {
Expand Down
107 changes: 107 additions & 0 deletions src/deploy/functions/release/fabricator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as pollerNS from "../../../operation-poller";
import * as pubsubNS from "../../../gcp/pubsub";
import * as schedulerNS from "../../../gcp/cloudscheduler";
import * as runNS from "../../../gcp/run";
import * as runV2NS from "../../../gcp/runv2";
import * as cloudtasksNS from "../../../gcp/cloudtasks";
import * as backend from "../backend";
import * as scraper from "./sourceTokenScraper";
Expand All @@ -32,6 +33,7 @@ describe("Fabricator", () => {
let pubsub: sinon.SinonStubbedInstance<typeof pubsubNS>;
let scheduler: sinon.SinonStubbedInstance<typeof schedulerNS>;
let run: sinon.SinonStubbedInstance<typeof runNS>;
let runv2: sinon.SinonStubbedInstance<typeof runV2NS>;
let tasks: sinon.SinonStubbedInstance<typeof cloudtasksNS>;
let services: sinon.SinonStubbedInstance<typeof servicesNS>;
let identityPlatform: sinon.SinonStubbedInstance<typeof identityPlatformNS>;
Expand All @@ -44,6 +46,7 @@ describe("Fabricator", () => {
pubsub = sinon.stub(pubsubNS);
scheduler = sinon.stub(schedulerNS);
run = sinon.stub(runNS);
runv2 = sinon.stub(runV2NS);
tasks = sinon.stub(cloudtasksNS);
services = sinon.stub(servicesNS);
identityPlatform = sinon.stub(identityPlatformNS);
Expand All @@ -53,6 +56,7 @@ describe("Fabricator", () => {
scheduler.jobFromEndpoint.restore();
tasks.queueFromEndpoint.restore();
tasks.queueNameForEndpoint.restore();
runv2.serviceFromEndpoint.restore();
gcf.createFunction.rejects(new Error("unexpected gcf.createFunction"));
gcf.updateFunction.rejects(new Error("unexpected gcf.updateFunction"));
gcf.deleteFunction.rejects(new Error("unexpected gcf.deleteFunction"));
Expand All @@ -74,6 +78,10 @@ describe("Fabricator", () => {
run.setInvokerUpdate.rejects(new Error("unexpected run.setInvokerUpdate"));
run.replaceService.rejects(new Error("unexpected run.replaceService"));
run.updateService.rejects(new Error("Unexpected run.updateService"));
runv2.createService.rejects(new Error("unexpected runv2.createService"));
runv2.updateService.rejects(new Error("unexpected runv2.updateService"));
runv2.deleteService.rejects(new Error("unexpected runv2.deleteService"));
runv2.getService.rejects(new Error("unexpected runv2.getService"));
poller.pollOperation.rejects(new Error("unexpected poller.pollOperation"));
pubsub.createTopic.rejects(new Error("unexpected pubsub.createTopic"));
pubsub.deleteTopic.rejects(new Error("unexpected pubsub.deleteTopic"));
Expand Down Expand Up @@ -1704,4 +1712,103 @@ describe("Fabricator", () => {
expect(ep2Result?.error?.message).to.match(/delete function/);
});
});

describe("createRunService", () => {
it("creates a Cloud Run service with correct configuration", async () => {
runv2.createService.resolves({ uri: "https://service", name: "service" } as any);
run.setInvokerUpdate.resolves();

const ep = endpoint(
{ httpsTrigger: {} },
{
platform: "run",
baseImageUri: "gcr.io/base",
command: ["cmd"],
args: ["arg"],
},
);
await fab.createRunService(ep);

expect(runv2.createService).to.have.been.calledWith(
ep.project,
ep.region,
ep.id,
sinon.match({
template: {
containers: [
sinon.match({
image: "scratch",
baseImageUri: "gcr.io/base",
command: ["cmd"],
args: ["arg"],
sourceCode: {
cloudStorageSource: {
bucket: "bucket",
object: "object",
generation: "42",
},
},
}),
],
},
}),
);
});
});

describe("updateRunService", () => {
it("updates a Cloud Run service with correct configuration", async () => {
runv2.updateService.resolves({ uri: "https://service", name: "service" } as any);
run.setInvokerUpdate.resolves();

const ep = endpoint(
{ httpsTrigger: {} },
{
platform: "run",
baseImageUri: "gcr.io/base-v2",
},
);
// Mock update to include the endpoint
const update: planner.EndpointUpdate = {
endpoint: ep,
};

await fab.updateRunService(update);

expect(runv2.updateService).to.have.been.calledWith(
sinon.match({
name: `projects/${ep.project}/locations/${ep.region}/services/${ep.id}`,
template: {
containers: [
sinon.match({
baseImageUri: "gcr.io/base-v2",
}),
],
},
}),
);
});
});

describe("deleteRunService", () => {
it("deletes the Cloud Run service", async () => {
runv2.deleteService.resolves();
const ep = endpoint({ httpsTrigger: {} }, { platform: "run" });

await fab.deleteRunService(ep);

expect(runv2.deleteService).to.have.been.calledWith(ep.project, ep.region, ep.id);
});

it("ignores 404s", async () => {
const err = new Error("Not Found");
(err as any).status = 404;
runv2.deleteService.rejects(err);
const ep = endpoint({ httpsTrigger: {} }, { platform: "run" });

await fab.deleteRunService(ep);

expect(runv2.deleteService).to.have.been.called;
});
});
});
Loading
Loading