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
20 changes: 20 additions & 0 deletions src/deploy/functions/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@
httpsTrigger: HttpsTrigger;
}

/** API agnostic version of a Firebase Data Connect HTTPS trigger. */
export interface DataConnectGraphqlTrigger {
invoker?: string[] | null;
}

/** Something that has a Data Connect HTTPS trigger */
export interface DataConnectGraphqlTriggered {
dataConnectGraphqlTrigger: DataConnectGraphqlTrigger;
}

/** API agnostic version of a Firebase callable function. */
export type CallableTrigger = {
genkitAction?: string;
Expand Down Expand Up @@ -151,6 +161,8 @@
return "scheduled";
} else if (isHttpsTriggered(endpoint)) {
return "https";
} else if (isDataConnectGraphqlTriggered(endpoint)) {
return "dataConnectGraphql";
} else if (isCallableTriggered(endpoint)) {
return "callable";
} else if (isEventTriggered(endpoint)) {
Expand Down Expand Up @@ -182,7 +194,7 @@
return allMemoryOptions.includes(mem as MemoryOptions);
}

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

Check warning on line 197 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 @@ -305,6 +317,7 @@

export type Triggered =
| HttpsTriggered
| DataConnectGraphqlTriggered
| CallableTriggered
| EventTriggered
| ScheduleTriggered
Expand All @@ -316,6 +329,13 @@
return {}.hasOwnProperty.call(triggered, "httpsTrigger");
}

/** Whether something has a DataConnectGraphqlTrigger */
export function isDataConnectGraphqlTriggered(
triggered: Triggered,
): triggered is DataConnectGraphqlTriggered {
return {}.hasOwnProperty.call(triggered, "dataConnectGraphqlTrigger");
}

/** Whether something has a CallableTrigger */
export function isCallableTriggered(triggered: Triggered): triggered is CallableTriggered {
return {}.hasOwnProperty.call(triggered, "callableTrigger");
Expand Down Expand Up @@ -551,8 +571,8 @@
existingBackend.endpoints[endpoint.region] || {};
existingBackend.endpoints[endpoint.region][endpoint.id] = endpoint;
}
} catch (err: any) {

Check warning on line 574 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 575 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 575 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
42 changes: 41 additions & 1 deletion src/deploy/functions/build.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe("toBackend", () => {
expect(Object.keys(backend.endpoints).length).to.equal(0);
});

it("populates multiple specified invokers correctly", () => {
it("populates multiple specified https invokers correctly", () => {
const desiredBuild: build.Build = build.of({
func: {
platform: "gcfv1",
Expand Down Expand Up @@ -113,6 +113,46 @@ describe("toBackend", () => {
}
});

it("populates multiple specified data connect https invokers correctly", () => {
const desiredBuild: build.Build = build.of({
func: {
platform: "gcfv2",
region: ["us-central1"],
project: "project",
runtime: "nodejs16",
entryPoint: "func",
maxInstances: 42,
minInstances: 1,
serviceAccount: "service-account-1@",
vpc: {
connector: "projects/project/locations/region/connectors/connector",
egressSettings: "PRIVATE_RANGES_ONLY",
},
ingressSettings: "ALLOW_ALL",
labels: {
test: "testing",
},
dataConnectGraphqlTrigger: {
invoker: ["service-account-1@", "service-account-2@"],
},
},
});
const backend = build.toBackend(desiredBuild, {});
expect(Object.keys(backend.endpoints).length).to.equal(1);
const endpointDef = Object.values(backend.endpoints)[0];
expect(endpointDef).to.not.equal(undefined);
if (endpointDef) {
expect(endpointDef.func.id).to.equal("func");
expect(endpointDef.func.project).to.equal("project");
expect(endpointDef.func.region).to.equal("us-central1");
expect(
"dataConnectGraphqlTrigger" in endpointDef.func
? endpointDef.func.dataConnectGraphqlTrigger.invoker
: [],
).to.have.members(["service-account-1@", "service-account-2@"]);
}
});

it("populates multiple param values", () => {
const desiredBuild: build.Build = build.of({
func: {
Expand Down
25 changes: 25 additions & 0 deletions src/deploy/functions/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
invoker?: Array<ServiceAccount | Expression<string>> | null;
}

export interface DataConnectGraphqlTrigger {
// Which service account should be able to trigger this function. No value means that only the
// Firebase Data Connect P4SA can trigger this function. For more, see go/cf3-http-access-control
invoker?: Array<ServiceAccount | Expression<string>> | null;
}

// Trigger definitions for RPCs servers using the HTTP protocol defined at
// https://firebase.google.com/docs/functions/callable-reference
interface CallableTrigger {
Expand Down Expand Up @@ -150,13 +156,15 @@
}

export type HttpsTriggered = { httpsTrigger: HttpsTrigger };
export type DataConnectGraphqlTriggered = { dataConnectGraphqlTrigger: DataConnectGraphqlTrigger };
export type CallableTriggered = { callableTrigger: CallableTrigger };
export type BlockingTriggered = { blockingTrigger: BlockingTrigger };
export type EventTriggered = { eventTrigger: EventTrigger };
export type ScheduleTriggered = { scheduleTrigger: ScheduleTrigger };
export type TaskQueueTriggered = { taskQueueTrigger: TaskQueueTrigger };
export type Triggered =
| HttpsTriggered
| DataConnectGraphqlTriggered
| CallableTriggered
| BlockingTriggered
| EventTriggered
Expand All @@ -168,6 +176,13 @@
return {}.hasOwnProperty.call(triggered, "httpsTrigger");
}

/** Whether something has a DataConnectGraphqlTrigger */
export function isDataConnectGraphqlTriggered(
triggered: Triggered,
): triggered is DataConnectGraphqlTriggered {
return {}.hasOwnProperty.call(triggered, "dataConnectGraphqlTrigger");
}

/** Whether something has a CallableTrigger */
export function isCallableTriggered(triggered: Triggered): triggered is CallableTriggered {
return {}.hasOwnProperty.call(triggered, "callableTrigger");
Expand Down Expand Up @@ -456,7 +471,7 @@
// List param, we try resolving a String param instead.
try {
regions = params.resolveList(bdEndpoint.region, paramValues);
} catch (err: any) {

Check warning on line 474 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 @@ -559,6 +574,16 @@
httpsTrigger.invoker = endpoint.httpsTrigger.invoker.map(r.resolveString);
}
return { httpsTrigger };
} else if (isDataConnectGraphqlTriggered(endpoint)) {
const dataConnectGraphqlTrigger: backend.DataConnectGraphqlTrigger = {};
if (endpoint.dataConnectGraphqlTrigger.invoker === null) {
dataConnectGraphqlTrigger.invoker = null;
} else if (typeof endpoint.dataConnectGraphqlTrigger.invoker !== "undefined") {
dataConnectGraphqlTrigger.invoker = endpoint.dataConnectGraphqlTrigger.invoker.map(
r.resolveString,
);
}
return { dataConnectGraphqlTrigger };
} else if (isCallableTriggered(endpoint)) {
const trigger: CallableTriggered = { callableTrigger: {} };
proto.copyIfPresent(trigger.callableTrigger, endpoint.callableTrigger, "genkitAction");
Expand Down
2 changes: 1 addition & 1 deletion 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 @@ -60,7 +60,7 @@

/**
* Checks a functions deployment for HTTP function creation, and tests IAM
* permissions accordingly.

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

View workflow job for this annotation

GitHub Actions / lint (20)

Expected only 0 line after block description
*
* @param context The deploy context.
* @param options The command-wide options object.
Expand All @@ -74,10 +74,10 @@
if (!payload.functions) {
return;
}
const filters = context.filters || getEndpointFilters(options, context.config!);

Check warning on line 77 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)
.filter(backend.isHttpsTriggered || backend.isDataConnectGraphqlTriggered)
.filter((f) => endpointMatchesAnyFilter(f, filters));

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

Check warning on line 100 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 +130,7 @@
}

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

Check warning on line 133 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 Down
1 change: 1 addition & 0 deletions src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export async function prepare(
// This drives GA4 metric `has_runtime_config` in the functions deploy reporter.
context.hasRuntimeConfig = Object.keys(runtimeConfig).some((k) => k !== "firebase");

// TODO: Modify to also load dataconnect schema if `onGraphRequest` is used with `schemaFilePath`.
const wantBuilds = await loadCodebases(
context.config,
options,
Expand Down
59 changes: 59 additions & 0 deletions src/deploy/functions/release/fabricator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,48 @@ describe("Fabricator", () => {
});
});

describe("dataConnectGraphqlTrigger", () => {
it("doesn't set invoker by default", async () => {
gcfv2.createFunction.resolves({ name: "op", done: false });
poller.pollOperation.resolves({ serviceConfig: { service: "service" } });
run.setInvokerCreate.resolves();
const ep = endpoint({ dataConnectGraphqlTrigger: {} }, { platform: "gcfv2" });

await fab.createV2Function(ep, new scraper.SourceTokenScraper());
expect(run.setInvokerCreate).to.not.have.been.called;
});

it("sets explicit invoker", async () => {
gcfv2.createFunction.resolves({ name: "op", done: false });
poller.pollOperation.resolves({ serviceConfig: { service: "service" } });
run.setInvokerCreate.resolves();
const ep = endpoint(
{
dataConnectGraphqlTrigger: {
invoker: ["custom@"],
},
},
{ platform: "gcfv2" },
);

await fab.createV2Function(ep, new scraper.SourceTokenScraper());
expect(run.setInvokerCreate).to.have.been.calledWith(ep.project, "service", ["custom@"]);
});

it("doesn't set private invoker on create", async () => {
gcfv2.createFunction.resolves({ name: "op", done: false });
poller.pollOperation.resolves({ serviceConfig: { service: "service" } });
run.setInvokerCreate.resolves();
const ep = endpoint(
{ dataConnectGraphqlTrigger: { invoker: ["private"] } },
{ platform: "gcfv2" },
);

await fab.createV2Function(ep, new scraper.SourceTokenScraper());
expect(run.setInvokerCreate).to.not.have.been.called;
});
});

describe("callableTrigger", () => {
it("always sets invoker to public", async () => {
gcfv2.createFunction.resolves({ name: "op", done: false });
Expand Down Expand Up @@ -854,6 +896,23 @@ describe("Fabricator", () => {
expect(run.setInvokerUpdate).to.have.been.calledWith(ep.project, "service", ["custom@"]);
});

it("sets explicit invoker on dataConnectGraphqlTrigger", async () => {
gcfv2.updateFunction.resolves({ name: "op", done: false });
poller.pollOperation.resolves({ serviceConfig: { service: "service" } });
run.setInvokerUpdate.resolves();
const ep = endpoint(
{
dataConnectGraphqlTrigger: {
invoker: ["custom@"],
},
},
{ platform: "gcfv2" },
);

await fab.updateV2Function(ep, new scraper.SourceTokenScraper());
expect(run.setInvokerUpdate).to.have.been.calledWith(ep.project, "service", ["custom@"]);
});

it("sets explicit invoker on taskQueueTrigger", async () => {
gcfv2.updateFunction.resolves({ name: "op", done: false });
poller.pollOperation.resolves({ serviceConfig: { service: "service" } });
Expand Down
12 changes: 12 additions & 0 deletions src/deploy/functions/release/fabricator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,13 @@ export class Fabricator {
.run(() => run.setInvokerCreate(endpoint.project, serviceName, invoker))
.catch(rethrowAs(endpoint, "set invoker"));
}
} else if (backend.isDataConnectGraphqlTriggered(endpoint)) {
const invoker = endpoint.dataConnectGraphqlTrigger.invoker;
if (invoker && !invoker.includes("private")) {
await this.executor
.run(() => run.setInvokerCreate(endpoint.project, serviceName, invoker))
.catch(rethrowAs(endpoint, "set invoker"));
}
} else if (backend.isCallableTriggered(endpoint)) {
// Callable functions should always be public
await this.executor
Expand Down Expand Up @@ -547,6 +554,11 @@ export class Fabricator {
let invoker: string[] | undefined;
if (backend.isHttpsTriggered(endpoint)) {
invoker = endpoint.httpsTrigger.invoker === null ? ["public"] : endpoint.httpsTrigger.invoker;
} else if (backend.isDataConnectGraphqlTriggered(endpoint)) {
invoker =
endpoint.dataConnectGraphqlTrigger.invoker === null
? undefined
: endpoint.dataConnectGraphqlTrigger.invoker;
} else if (backend.isTaskQueueTriggered(endpoint)) {
invoker = endpoint.taskQueueTrigger.invoker === null ? [] : endpoint.taskQueueTrigger.invoker;
} else if (
Expand Down
4 changes: 3 additions & 1 deletion src/deploy/functions/release/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ export async function release(
* has updated the URI of endpoints after deploy.
*/
export function printTriggerUrls(results: backend.Backend): void {
const httpsFunctions = backend.allEndpoints(results).filter(backend.isHttpsTriggered);
const httpsFunctions = backend
.allEndpoints(results)
.filter(backend.isHttpsTriggered || backend.isDataConnectGraphqlTriggered);
if (httpsFunctions.length === 0) {
return;
}
Expand Down
2 changes: 2 additions & 0 deletions src/deploy/functions/release/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ export function checkForIllegalUpdate(want: backend.Endpoint, have: backend.Endp
const triggerType = (e: backend.Endpoint): string => {
if (backend.isHttpsTriggered(e)) {
return "an HTTPS";
} else if (backend.isDataConnectGraphqlTriggered(e)) {
return "a Data Connect HTTPS";
} else if (backend.isCallableTriggered(e)) {
return "a callable";
} else if (backend.isEventTriggered(e)) {
Expand Down
4 changes: 4 additions & 0 deletions src/deploy/functions/release/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ export function triggerTag(endpoint: backend.Endpoint): string {
return `${prefix}.https`;
}

if (backend.isDataConnectGraphqlTriggered(endpoint)) {
return `${prefix}.dataConnectGraphql`;
}

if (backend.isBlockingTriggered(endpoint)) {
return `${prefix}.blocking`;
}
Expand Down
27 changes: 27 additions & 0 deletions src/deploy/functions/runtimes/discovery/v1alpha1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,33 @@ describe("buildFromV1Alpha", () => {
});
});

describe("dataConnectGraphqlTriggers", () => {
it("invalid value for Data Connect https trigger key invoker", () => {
assertParserError({
endpoints: {
func: {
...MIN_ENDPOINT,
dataConnectGraphqlTrigger: { invoker: 42 },
},
},
});
});

it("cannot be used with 1st gen", () => {
assertParserError({
endpoints: {
func: {
...MIN_ENDPOINT,
platform: "gcfv1",
dataConnectGraphqlTrigger: {
invoker: "custom@",
},
},
},
});
});
});

describe("genkitTriggers", () => {
it("fails with invalid fields", () => {
assertParserError({
Expand Down
12 changes: 12 additions & 0 deletions src/deploy/functions/runtimes/discovery/v1alpha1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type WireEventTrigger = build.EventTrigger & {

export type WireEndpoint = build.Triggered &
Partial<build.HttpsTriggered> &
Partial<build.DataConnectGraphqlTriggered> &
Partial<build.CallableTriggered> &
Partial<{ eventTrigger: WireEventTrigger }> &
Partial<build.TaskQueueTriggered> &
Expand Down Expand Up @@ -158,6 +159,7 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void {
environmentVariables: "object?",
secretEnvironmentVariables: "array?",
httpsTrigger: "object",
dataConnectGraphqlTrigger: "object",
callableTrigger: "object",
eventTrigger: "object",
scheduleTrigger: "object",
Expand All @@ -176,6 +178,9 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void {
if (ep.httpsTrigger) {
triggerCount++;
}
if (ep.dataConnectGraphqlTrigger) {
triggerCount++;
}
if (ep.callableTrigger) {
triggerCount++;
}
Expand Down Expand Up @@ -213,6 +218,10 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void {
assertKeyTypes(prefix + ".httpsTrigger", ep.httpsTrigger, {
invoker: "array?",
});
} else if (build.isDataConnectGraphqlTriggered(ep)) {
assertKeyTypes(prefix + ".dataConnectGraphqlTrigger", ep.dataConnectGraphqlTrigger, {
invoker: "array?",
});
} else if (build.isCallableTriggered(ep)) {
assertKeyTypes(prefix + ".callableTrigger", ep.callableTrigger, {
genkitAction: "string?",
Expand Down Expand Up @@ -311,6 +320,9 @@ function parseEndpointForBuild(
} else if (build.isHttpsTriggered(ep)) {
triggered = { httpsTrigger: {} };
copyIfPresent(triggered.httpsTrigger, ep.httpsTrigger, "invoker");
} else if (build.isDataConnectGraphqlTriggered(ep)) {
triggered = { dataConnectGraphqlTrigger: {} };
copyIfPresent(triggered.dataConnectGraphqlTrigger, ep.dataConnectGraphqlTrigger, "invoker");
} else if (build.isCallableTriggered(ep)) {
triggered = { callableTrigger: {} };
copyIfPresent(triggered.callableTrigger, ep.callableTrigger, "genkitAction");
Expand Down
Loading
Loading