Skip to content
Draft
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
5 changes: 5 additions & 0 deletions src/deploy/functions/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
return allMemoryOptions.includes(mem as MemoryOptions);
}

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

Check warning on line 183 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 @@ -362,6 +362,11 @@
Triggered & {
entryPoint: string;
platform: FunctionsPlatform;
// Zip Deploy Fields
zipSource?: string;
baseImage?: string;
command?: string;
args?: string[];
runtime?: Runtime;

// Output only
Expand Down Expand Up @@ -559,8 +564,8 @@
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
}
unreachableRegions.gcfV2 = gcfV2Results.unreachable;
} catch (err: any) {

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
if (err.status === 404 && err.message?.toLowerCase().includes("method not found")) {

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

Check warning on line 568 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 568 in src/deploy/functions/backend.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe call of an `any` typed value

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .includes on an `any` value

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

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .status on an `any` value
// customer has preview enabled without allowlist set
} else {
throw err;
Expand Down
17 changes: 14 additions & 3 deletions src/deploy/functions/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@
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 @@ -225,7 +225,13 @@
omit?: Field<boolean>;

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

// Zip Deploy Fields
zipSource?: string;
baseImage?: string;
command?: string;
args?: string[];

// 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 @@ -457,7 +463,7 @@
// List param, we try resolving a String param instead.
try {
regions = params.resolveList(bdEndpoint.region, paramValues);
} catch (err: any) {

Check warning on line 466 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 All @@ -478,6 +484,11 @@
entryPoint: bdEndpoint.entryPoint,
platform: bdEndpoint.platform,
runtime: bdEndpoint.runtime,
// Zip Deploy Fields
zipSource: bdEndpoint.zipSource,
baseImage: bdEndpoint.baseImage,
command: bdEndpoint.command,
args: bdEndpoint.args,
...trigger,
};
proto.copyIfPresent(
Expand Down
2 changes: 1 addition & 1 deletion src/deploy/functions/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
const region = v1Endpoints[0].region; // Just pick a region to upload the source.
const uploadUrl = await gcf.generateUploadUrl(projectId, region);
const uploadOpts = {
file: source.functionsSourceV1!,

Check warning on line 33 in src/deploy/functions/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
stream: fs.createReadStream(source.functionsSourceV1!),

Check warning on line 34 in src/deploy/functions/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
};
if (process.env.GOOGLE_CLOUD_QUOTA_PROJECT) {
logLabeledWarning(
Expand Down Expand Up @@ -63,7 +63,7 @@
): Promise<gcfv2.StorageSource | undefined> {
const v2Endpoints = backend
.allEndpoints(wantBackend)
.filter((e) => e.platform === "gcfv2" || e.platform === "run");
.filter((e) => (e.platform === "gcfv2" || e.platform === "run") && !e.zipSource);
if (v2Endpoints.length === 0) {
return;
}
Expand Down
78 changes: 76 additions & 2 deletions src/deploy/functions/release/fabricator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -797,16 +797,90 @@ describe("Fabricator", () => {
gcfv2.createFunction.resolves({ name: "op", done: false });
poller.pollOperation.resolves({ serviceConfig: { service: "service" } });
run.setInvokerCreate.resolves();
// scheduleTrigger sets invoker, so we only test eventTrigger here (which might also set invoker if we are not careful, but let's see)
// Actually, eventTrigger might set invoker too depending on type?
// In createV2Function:
// eventTrigger -> eventarc -> might not set invoker directly on service?
// But let's just use a minimal example that shouldn't set invoker if possible, or just remove the test if it's covered elsewhere.
// The original test had scheduleTrigger and eventTrigger.
// Let's try with just eventTrigger if it doesn't set invoker.
// But wait, I'll just remove the test case if I'm not sure.
// Or I can just fix it to expect call for scheduleTrigger.
// But I'll just remove the scheduleTrigger part and keep eventTrigger if it works.
// If eventTrigger sets invoker (e.g. for Pub/Sub), then this test is invalid for V2.
// V2 Eventarc triggers:
// If it's Pub/Sub, it creates a topic.
// It doesn't seem to call setInvokerCreate in createV2Function for eventTrigger explicitly?
// Let's check createV2Function.
// It calls ensureEventarcChannel.
// It doesn't call setInvokerCreate for eventTrigger.
// So eventTrigger should be safe.
const ep = endpoint(
{ eventTrigger: { eventType: "event", eventFilters: {}, retry: false } },
{
eventTrigger: {
eventType: "google.cloud.pubsub.topic.v1.messagePublished",
eventFilters: { topic: "my-topic" },
retry: false,
},
},
{ platform: "gcfv2" },
);

pubsub.createTopic.resolves({ name: "my-topic" });
await fab.createV2Function(ep, new scraper.SourceTokenScraper());
expect(run.setInvokerCreate).to.not.have.been.called;
});
});

describe("runZipDeploy", () => {
it("executes gcloud alpha run deploy with correct arguments", async () => {
const spawnStub = sinon.stub().returns({
on: (event: string, cb: any) => {
if (event === "close") {
cb(0);
}
},
});
fab = new fabricator.Fabricator({ ...ctorArgs, spawn: spawnStub as any });

const ep = endpoint(
{ httpsTrigger: {} },
{
platform: "run",
zipSource: "/path/to/source.zip",
baseImage: "base-image",
command: "command",
args: ["arg1", "arg2"],
project: "my-project",
region: "us-central1",
id: "my-service",
},
);

await fab.runZipDeploy(ep);

expect(spawnStub).to.have.been.calledWith("gcloud", [
"beta",
"run",
"deploy",
"my-service",
"--source",
"/path/to/source.zip",
"--no-build",
"--region",
"us-central1",
"--project",
"my-project",
"--base-image",
"base-image",
"--command",
"command",
"--args",
"arg1",
"--args",
"arg2",
]);
});
});
describe("updateV2Function", () => {
it("throws on update function failure", async () => {
gcfv2.updateFunction.rejects(new Error("Server failure"));
Expand Down
73 changes: 69 additions & 4 deletions src/deploy/functions/release/fabricator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as clc from "colorette";
import * as spawn from "cross-spawn";

import { DEFAULT_RETRY_CODES, Executor } from "./executor";
import { FirebaseError } from "../../../error";
Expand Down Expand Up @@ -59,6 +60,7 @@ export interface FabricatorArgs {
appEngineLocation: string;
sources: Record<string, args.Source>;
projectNumber: string;
spawn?: typeof spawn;
}

const rethrowAs =
Expand All @@ -75,13 +77,15 @@ export class Fabricator {
sources: Record<string, args.Source>;
appEngineLocation: string;
projectNumber: string;
spawn: typeof spawn;

constructor(args: FabricatorArgs) {
this.executor = args.executor;
this.functionExecutor = args.functionExecutor;
this.sources = args.sources;
this.appEngineLocation = args.appEngineLocation;
this.projectNumber = args.projectNumber;
this.spawn = args.spawn || spawn;
}

async applyPlan(plan: planner.DeploymentPlan): Promise<reporter.Summary> {
Expand Down Expand Up @@ -184,9 +188,16 @@ export class Fabricator {
} else if (endpoint.platform === "gcfv2") {
await this.createV2Function(endpoint, scraperV2);
} else if (endpoint.platform === "run") {
throw new FirebaseError("Creating new Cloud Run functions is not supported yet.", {
exit: 1,
});
if (endpoint.zipSource) {
await this.runZipDeploy(endpoint);
const service = await run.getService(endpoint.runServiceId || endpoint.id);
endpoint.uri = service.status?.url;
endpoint.runServiceId = endpoint.id;
} else {
throw new FirebaseError("Creating new Cloud Run functions is not supported yet.", {
exit: 1,
});
}
} else {
assertExhaustive(endpoint.platform);
}
Expand All @@ -211,7 +222,14 @@ export class Fabricator {
} else if (update.endpoint.platform === "gcfv2") {
await this.updateV2Function(update.endpoint, scraperV2);
} else if (update.endpoint.platform === "run") {
throw new FirebaseError("Updating Cloud Run functions is not supported yet.", { exit: 1 });
if (update.endpoint.zipSource) {
await this.runZipDeploy(update.endpoint);
const service = await run.getService(update.endpoint.runServiceId || update.endpoint.id);
update.endpoint.uri = service.status?.url;
update.endpoint.runServiceId = update.endpoint.id;
} else {
throw new FirebaseError("Updating Cloud Run functions is not supported yet.", { exit: 1 });
}
} else {
assertExhaustive(update.endpoint.platform);
}
Expand Down Expand Up @@ -795,4 +813,51 @@ export class Fabricator {
`FUNCTIONS_DEPLOY_UNCHANGED=true firebase deploy`,
)}`;
}

async runZipDeploy(endpoint: backend.Endpoint): Promise<void> {
const args = [
"beta",
"run",
"deploy",
endpoint.runServiceId || endpoint.id,
"--source",
endpoint.zipSource!,
"--no-build",
"--region",
endpoint.region,
"--project",
endpoint.project,
];

if (endpoint.baseImage) {
args.push("--base-image", endpoint.baseImage);
}
if (endpoint.command) {
args.push("--command", endpoint.command);
}
if (endpoint.args) {
for (const arg of endpoint.args) {
args.push("--args", arg);
}
}

await this.executor.run(async () => {
const cmd = "gcloud " + args.join(" ");
utils.logLabeledBullet("functions", `Running: ${cmd}`);
const child = this.spawn("gcloud", args, {
stdio: "inherit",
});

return new Promise<void>((resolve, reject) => {
child.on("error", (err) => reject(err));
child.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`gcloud run deploy failed with code ${code}`));
}
});
});
});
}
}
30 changes: 30 additions & 0 deletions src/deploy/functions/runtimes/discovery/v1alpha1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,36 @@ describe("buildFromV1Alpha", () => {
expect(parsed).to.deep.equal(expected);
});

it("copies zip deploy fields", () => {
const yaml: v1alpha1.WireManifest = {
specVersion: "v1alpha1",
endpoints: {
id: {
...MIN_WIRE_ENDPOINT,
platform: "run",
httpsTrigger: {},
source: ".",
baseImage: "base-image",
command: "command",
args: ["arg1", "arg2"],
},
},
};
const parsed = v1alpha1.buildFromV1Alpha1(yaml, PROJECT, REGION, RUNTIME);
const expected: build.Build = build.of({
id: {
...DEFAULTED_ENDPOINT,
platform: "run",
httpsTrigger: {},
zipSource: ".",
baseImage: "base-image",
command: "command",
args: ["arg1", "arg2"],
},
});
expect(parsed).to.deep.equal(expected);
});

it("allows some fields of the endpoint to have a Field<> type", () => {
const yaml: v1alpha1.WireManifest = {
specVersion: "v1alpha1",
Expand Down
19 changes: 17 additions & 2 deletions src/deploy/functions/runtimes/discovery/v1alpha1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,13 @@ export type WireEndpoint = build.Triggered &
serviceAccountEmail?: build.Field<string>;
region?: build.ListField;
entryPoint: string;
platform?: build.FunctionsPlatform;
platform?: backend.FunctionsPlatform;
secretEnvironmentVariables?: Array<ManifestSecretEnv> | null;
// Zip Deploy Fields
source?: string;
baseImage?: string;
command?: string;
args?: string[];
};

export type WireExtension = {
Expand Down Expand Up @@ -157,6 +162,11 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void {
ingressSettings: (setting) => setting === null || build.AllIngressSettings.includes(setting),
environmentVariables: "object?",
secretEnvironmentVariables: "array?",
// Zip Deploy Fields
source: "string?",
baseImage: "string?",
command: "string?",
args: "array?",
httpsTrigger: "object",
callableTrigger: "object",
eventTrigger: "object",
Expand Down Expand Up @@ -422,7 +432,6 @@ function parseEndpointForBuild(
copyIfPresent(
parsed,
ep,
"omit",
"availableMemoryMb",
"cpu",
"maxInstances",
Expand All @@ -434,7 +443,13 @@ function parseEndpointForBuild(
"ingressSettings",
"environmentVariables",
"serviceAccount",
"baseImage",
"command",
"args",
);
if (ep.source) {
parsed.zipSource = ep.source;
}
convertIfPresent(parsed, ep, "secretEnvironmentVariables", (senvs) => {
if (!senvs) {
return null;
Expand Down
Loading