diff --git a/spec/v1/cloud-functions.spec.ts b/spec/v1/cloud-functions.spec.ts index 05e80bd21..5c861f3d6 100644 --- a/spec/v1/cloud-functions.spec.ts +++ b/spec/v1/cloud-functions.spec.ts @@ -41,7 +41,7 @@ describe('makeCloudFunction', () => { legacyEventType: 'providers/provider/eventTypes/event', }; - it('should put a __trigger on the returned CloudFunction', () => { + it('should put a __trigger/__endpoint on the returned CloudFunction', () => { const cf = makeCloudFunction({ provider: 'mock.provider', eventType: 'mock.event', @@ -49,6 +49,7 @@ describe('makeCloudFunction', () => { triggerResource: () => 'resource', handler: () => null, }); + expect(cf.__trigger).to.deep.equal({ eventTrigger: { eventType: 'mock.provider.mock.event', @@ -56,10 +57,22 @@ describe('makeCloudFunction', () => { service: 'service', }, }); + + expect(cf.__endpoint).to.deep.equal({ + platform: 'gcfv1', + eventTrigger: { + eventType: 'mock.provider.mock.event', + eventFilters: { + resource: 'resource', + }, + retry: false, + }, + }); }); - it('should have legacy event type in __trigger if provided', () => { + it('should have legacy event type in __trigger/__endpoint if provided', () => { const cf = makeCloudFunction(cloudFunctionArgs); + expect(cf.__trigger).to.deep.equal({ eventTrigger: { eventType: 'providers/provider/eventTypes/event', @@ -67,6 +80,92 @@ describe('makeCloudFunction', () => { service: 'service', }, }); + + expect(cf.__endpoint).to.deep.equal({ + platform: 'gcfv1', + eventTrigger: { + eventType: 'providers/provider/eventTypes/event', + eventFilters: { + resource: 'resource', + }, + retry: false, + }, + }); + }); + + it('should include converted options in __endpoint', () => { + const cf = makeCloudFunction({ + provider: 'mock.provider', + eventType: 'mock.event', + service: 'service', + triggerResource: () => 'resource', + handler: () => null, + options: { + timeoutSeconds: 10, + regions: ['us-central1'], + memory: '128MB', + serviceAccount: 'foo@google.com', + }, + }); + + expect(cf.__endpoint).to.deep.equal({ + platform: 'gcfv1', + timeoutSeconds: 10, + region: ['us-central1'], + availableMemoryMb: 128, + serviceAccountEmail: 'foo@google.com', + eventTrigger: { + eventType: 'mock.provider.mock.event', + eventFilters: { + resource: 'resource', + }, + retry: false, + }, + }); + }); + + it('should set retry given failure policy in __endpoint', () => { + const cf = makeCloudFunction({ + provider: 'mock.provider', + eventType: 'mock.event', + service: 'service', + triggerResource: () => 'resource', + handler: () => null, + options: { failurePolicy: { retry: {} } }, + }); + + expect(cf.__endpoint).to.deep.equal({ + platform: 'gcfv1', + eventTrigger: { + eventType: 'mock.provider.mock.event', + eventFilters: { + resource: 'resource', + }, + retry: true, + }, + }); + }); + + it('should setup a scheduleTrigger in __endpoint given a schedule', () => { + const schedule = { + schedule: 'every 5 minutes', + retryConfig: { retryCount: 3 }, + timeZone: 'America/New_York', + }; + const cf = makeCloudFunction({ + provider: 'mock.provider', + eventType: 'mock.event', + service: 'service', + triggerResource: () => 'resource', + handler: () => null, + options: { + schedule, + }, + }); + expect(cf.__endpoint).to.deep.equal({ + platform: 'gcfv1', + scheduleTrigger: schedule, + }); }); it('should construct the right context for event', () => { diff --git a/spec/v1/providers/https.spec.ts b/spec/v1/providers/https.spec.ts index 025c99a7a..f820fe277 100644 --- a/spec/v1/providers/https.spec.ts +++ b/spec/v1/providers/https.spec.ts @@ -22,7 +22,7 @@ import { expect } from 'chai'; import * as express from 'express'; -import * as _ from 'lodash'; + import * as functions from '../../../src/index'; import * as https from '../../../src/providers/https'; import { @@ -94,11 +94,15 @@ function runHandler( describe('CloudHttpsBuilder', () => { describe('#onRequest', () => { - it('should return a Trigger with appropriate values', () => { + it('should return a trigger/endpoint with appropriate values', () => { const result = https.onRequest((req, resp) => { resp.send(200); }); expect(result.__trigger).to.deep.equal({ httpsTrigger: {} }); + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv1', + httpsTrigger: {}, + }); }); it('should allow both region and runtime options to be set', () => { @@ -115,37 +119,51 @@ describe('CloudHttpsBuilder', () => { expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); expect(fn.__trigger.httpsTrigger.invoker).to.deep.equal(['private']); + + expect(fn.__endpoint.region).to.deep.equal(['us-east1']); + expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); + expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); + expect(fn.__endpoint.httpsTrigger.invoker).to.deep.equal(['private']); }); }); }); describe('handler namespace', () => { describe('#onRequest', () => { - it('should return an empty trigger', () => { + it('should return an empty trigger and endpoint', () => { const result = functions.handler.https.onRequest((req, res) => { res.send(200); }); expect(result.__trigger).to.deep.equal({}); + expect(result.__endpoint).to.deep.equal({}); }); }); describe('#onCall', () => { - it('should return an empty trigger', () => { + it('should return an empty trigger and endpoint', () => { const result = functions.handler.https.onCall(() => null); expect(result.__trigger).to.deep.equal({}); + expect(result.__endpoint).to.deep.equal({}); }); }); }); describe('#onCall', () => { - it('should return a Trigger with appropriate values', () => { + it('should return a trigger/endpoint with appropriate values', () => { const result = https.onCall((data) => { return 'response'; }); + expect(result.__trigger).to.deep.equal({ httpsTrigger: {}, labels: { 'deployment-callable': 'true' }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv1', + callableTrigger: {}, + labels: {}, + }); }); it('should allow both region and runtime options to be set', () => { @@ -160,6 +178,10 @@ describe('#onCall', () => { expect(fn.__trigger.regions).to.deep.equal(['us-east1']); expect(fn.__trigger.availableMemoryMb).to.deep.equal(256); expect(fn.__trigger.timeout).to.deep.equal('90s'); + + expect(fn.__endpoint.region).to.deep.equal(['us-east1']); + expect(fn.__endpoint.availableMemoryMb).to.deep.equal(256); + expect(fn.__endpoint.timeoutSeconds).to.deep.equal(90); }); it('has a .run method', () => { diff --git a/spec/v2/providers/helpers.ts b/spec/v2/providers/helpers.ts index a654a4944..a499be69a 100644 --- a/spec/v2/providers/helpers.ts +++ b/spec/v2/providers/helpers.ts @@ -33,3 +33,22 @@ export const FULL_TRIGGER = { hello: 'world', }, }; + +export const FULL_ENDPOINT = { + platform: 'gcfv2', + region: ['us-west1'], + availableMemoryMb: 512, + timeoutSeconds: 60, + minInstances: 1, + maxInstances: 3, + concurrency: 20, + vpc: { + connector: 'aConnector', + egressSettings: 'ALL_TRAFFIC', + }, + serviceAccountEmail: 'root@', + ingressSettings: 'ALLOW_ALL', + labels: { + hello: 'world', + }, +}; diff --git a/spec/v2/providers/https.spec.ts b/spec/v2/providers/https.spec.ts index a28cb1c9c..e7c34dad0 100644 --- a/spec/v2/providers/https.spec.ts +++ b/spec/v2/providers/https.spec.ts @@ -7,7 +7,7 @@ import { expectedResponseHeaders, MockRequest, } from '../../fixtures/mockrequest'; -import { FULL_OPTIONS, FULL_TRIGGER } from './helpers'; +import { FULL_ENDPOINT, FULL_OPTIONS, FULL_TRIGGER } from './helpers'; /** * RunHandlerResult contains the data from an express.Response. @@ -82,10 +82,11 @@ describe('onRequest', () => { delete process.env.GCLOUD_PROJECT; }); - it('should return a minimal trigger with appropriate values', () => { + it('should return a minimal trigger/endpoint with appropriate values', () => { const result = https.onRequest((req, res) => { res.send(200); }); + expect(result.__trigger).to.deep.equal({ apiVersion: 2, platform: 'gcfv2', @@ -94,9 +95,15 @@ describe('onRequest', () => { }, labels: {}, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + httpsTrigger: {}, + labels: {}, + }); }); - it('should create a complex trigger with appropriate values', () => { + it('should create a complex trigger/endpoint with appropriate values', () => { const result = https.onRequest( { ...FULL_OPTIONS, @@ -107,6 +114,7 @@ describe('onRequest', () => { res.send(200); } ); + expect(result.__trigger).to.deep.equal({ ...FULL_TRIGGER, httpsTrigger: { @@ -115,6 +123,14 @@ describe('onRequest', () => { }, regions: ['us-west1', 'us-central1'], }); + + expect(result.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + httpsTrigger: { + invoker: ['service-account1@', 'service-account2@'], + }, + region: ['us-west1', 'us-central1'], + }); }); it('should merge options and globalOptions', () => { @@ -148,6 +164,17 @@ describe('onRequest', () => { regions: ['us-west1', 'us-central1'], labels: {}, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + httpsTrigger: { + invoker: ['private'], + }, + concurrency: 20, + minInstances: 3, + region: ['us-west1', 'us-central1'], + labels: {}, + }); }); it('should be an express handler', async () => { @@ -209,8 +236,9 @@ describe('onCall', () => { delete process.env.GCLOUD_PROJECT; }); - it('should return a minimal trigger with appropriate values', () => { + it('should return a minimal trigger/endpoint with appropriate values', () => { const result = https.onCall((request) => 42); + expect(result.__trigger).to.deep.equal({ apiVersion: 2, platform: 'gcfv2', @@ -221,10 +249,17 @@ describe('onCall', () => { 'deployment-callable': 'true', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + callableTrigger: {}, + }); }); - it('should create a complex trigger with appropriate values', () => { + it('should create a complex trigger/endpoint with appropriate values', () => { const result = https.onCall(FULL_OPTIONS, (request) => 42); + expect(result.__trigger).to.deep.equal({ ...FULL_TRIGGER, httpsTrigger: { @@ -235,6 +270,14 @@ describe('onCall', () => { 'deployment-callable': 'true', }, }); + + expect(result.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + callableTrigger: {}, + labels: { + ...FULL_ENDPOINT.labels, + }, + }); }); it('should merge options and globalOptions', () => { @@ -265,6 +308,15 @@ describe('onCall', () => { 'deployment-callable': 'true', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + callableTrigger: {}, + concurrency: 20, + minInstances: 3, + region: ['us-west1', 'us-central1'], + labels: {}, + }); }); it('has a .run method', () => { diff --git a/spec/v2/providers/pubsub.spec.ts b/spec/v2/providers/pubsub.spec.ts index 4fc338614..f48e10f72 100644 --- a/spec/v2/providers/pubsub.spec.ts +++ b/spec/v2/providers/pubsub.spec.ts @@ -3,13 +3,21 @@ import { expect } from 'chai'; import { CloudEvent } from '../../../src/v2/core'; import * as options from '../../../src/v2/options'; import * as pubsub from '../../../src/v2/providers/pubsub'; -import { FULL_OPTIONS, FULL_TRIGGER } from './helpers'; +import { FULL_ENDPOINT, FULL_OPTIONS, FULL_TRIGGER } from './helpers'; const EVENT_TRIGGER = { eventType: 'google.cloud.pubsub.topic.v1.messagePublished', resource: 'projects/aProject/topics/topic', }; +const ENDPOINT_EVENT_TRIGGER = { + eventType: 'google.cloud.pubsub.topic.v1.messagePublished', + eventFilters: { + topic: 'topic', + }, + retry: false, +}; + describe('onMessagePublished', () => { beforeEach(() => { options.setGlobalOptions({}); @@ -20,25 +28,38 @@ describe('onMessagePublished', () => { delete process.env.GCLOUD_PROJECT; }); - it('should return a minimal trigger with appropriate values', () => { + it('should return a minimal trigger/endpoint with appropriate values', () => { const result = pubsub.onMessagePublished('topic', () => 42); + expect(result.__trigger).to.deep.equal({ apiVersion: 2, platform: 'gcfv2', eventTrigger: EVENT_TRIGGER, labels: {}, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + eventTrigger: ENDPOINT_EVENT_TRIGGER, + labels: {}, + }); }); - it('should create a complex trigger with appropriate values', () => { + it('should create a complex trigger/endpoint with appropriate values', () => { const result = pubsub.onMessagePublished( { ...FULL_OPTIONS, topic: 'topic' }, () => 42 ); + expect(result.__trigger).to.deep.equal({ ...FULL_TRIGGER, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + ...FULL_ENDPOINT, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); }); it('should merge options and globalOptions', () => { @@ -66,6 +87,45 @@ describe('onMessagePublished', () => { labels: {}, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + concurrency: 20, + minInstances: 3, + region: ['us-west1'], + labels: {}, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); + }); + + it('should convert retry option if appropriate', () => { + const result = pubsub.onMessagePublished( + { + topic: 'topic', + region: 'us-west1', + minInstances: 3, + retry: true, + }, + () => 42 + ); + + expect(result.__trigger).to.deep.equal({ + apiVersion: 2, + platform: 'gcfv2', + minInstances: 3, + regions: ['us-west1'], + labels: {}, + eventTrigger: EVENT_TRIGGER, + failurePolicy: { retry: true }, + }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + minInstances: 3, + region: ['us-west1'], + labels: {}, + eventTrigger: { ...ENDPOINT_EVENT_TRIGGER, retry: true }, + }); }); it('should have a .run method', () => { diff --git a/spec/v2/providers/storage.spec.ts b/spec/v2/providers/storage.spec.ts index 601b5c024..8c2e25576 100644 --- a/spec/v2/providers/storage.spec.ts +++ b/spec/v2/providers/storage.spec.ts @@ -10,6 +10,14 @@ const EVENT_TRIGGER = { resource: 'some-bucket', }; +const ENDPOINT_EVENT_TRIGGER = { + eventType: 'event-type', + eventFilters: { + bucket: 'some-bucket', + }, + retry: false, +}; + describe('v2/storage', () => { describe('getOptsAndBucket', () => { it('should return the default bucket with empty opts', () => { @@ -68,7 +76,7 @@ describe('v2/storage', () => { configStub.restore(); }); - it('should create a minimal trigger with bucket', () => { + it('should create a minimal trigger/endpoint with bucket', () => { const result = storage.onOperation('event-type', 'some-bucket', () => 42); expect(result.__trigger).to.deep.equal({ @@ -76,9 +84,15 @@ describe('v2/storage', () => { labels: {}, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); }); - it('should create a minimal trigger with opts', () => { + it('should create a minimal trigger/endpoint with opts', () => { configStub.returns({ storageBucket: 'default-bucket' }); const result = storage.onOperation( @@ -96,6 +110,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_EVENT_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + region: ['us-west1'], + }); }); it('should create a minimal trigger with bucket with opts and bucket', () => { @@ -110,9 +136,15 @@ describe('v2/storage', () => { labels: {}, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); }); - it('should create a complex trigger with appropriate values', () => { + it('should create a complex trigger/endpoint with appropriate values', () => { const result = storage.onOperation( 'event-type', { @@ -139,6 +171,26 @@ describe('v2/storage', () => { }, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + region: ['us-west1'], + availableMemoryMb: 512, + timeoutSeconds: 60, + minInstances: 1, + maxInstances: 3, + concurrency: 20, + vpc: { + connector: 'aConnector', + egressSettings: 'ALL_TRAFFIC', + }, + serviceAccountEmail: 'root@', + ingressSettings: 'ALLOW_ALL', + labels: { + hello: 'world', + }, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); }); it('should merge options and globalOptions', () => { @@ -166,6 +218,15 @@ describe('v2/storage', () => { labels: {}, eventTrigger: EVENT_TRIGGER, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + concurrency: 20, + minInstances: 3, + region: ['us-west1'], + labels: {}, + eventTrigger: ENDPOINT_EVENT_TRIGGER, + }); }); }); @@ -174,6 +235,10 @@ describe('v2/storage', () => { ...EVENT_TRIGGER, eventType: storage.archivedEvent, }; + const ENDPOINT_ARCHIVED_TRIGGER = { + ...ENDPOINT_EVENT_TRIGGER, + eventType: storage.archivedEvent, + }; let configStub: sinon.SinonStub; beforeEach(() => { @@ -197,6 +262,17 @@ describe('v2/storage', () => { resource: 'default-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_ARCHIVED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + }); }); it('should accept bucket and handler', () => { @@ -210,6 +286,17 @@ describe('v2/storage', () => { resource: 'my-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_ARCHIVED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + }); }); it('should accept opts and handler', () => { @@ -227,6 +314,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_ARCHIVED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + region: ['us-west1'], + }); }); it('should accept opts and handler, default bucket', () => { @@ -243,6 +342,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_ARCHIVED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + region: ['us-west1'], + }); }); }); @@ -251,6 +362,10 @@ describe('v2/storage', () => { ...EVENT_TRIGGER, eventType: storage.finalizedEvent, }; + const ENDPOINT_FINALIZED_TRIGGER = { + ...ENDPOINT_EVENT_TRIGGER, + eventType: storage.finalizedEvent, + }; let configStub: sinon.SinonStub; beforeEach(() => { @@ -274,6 +389,17 @@ describe('v2/storage', () => { resource: 'default-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_FINALIZED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + }); }); it('should accept bucket and handler', () => { @@ -287,6 +413,17 @@ describe('v2/storage', () => { resource: 'my-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_FINALIZED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + }); }); it('should accept opts and handler', () => { @@ -304,6 +441,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_FINALIZED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + region: ['us-west1'], + }); }); it('should accept opts and handler, default bucket', () => { @@ -323,6 +472,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_FINALIZED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + region: ['us-west1'], + }); }); }); @@ -331,6 +492,10 @@ describe('v2/storage', () => { ...EVENT_TRIGGER, eventType: storage.deletedEvent, }; + const ENDPOINT_DELETED_TRIGGER = { + ...ENDPOINT_EVENT_TRIGGER, + eventType: storage.deletedEvent, + }; let configStub: sinon.SinonStub; beforeEach(() => { @@ -355,6 +520,17 @@ describe('v2/storage', () => { }, }); + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_DELETED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + }); + configStub.restore(); }); @@ -369,6 +545,17 @@ describe('v2/storage', () => { resource: 'my-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_DELETED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + }); }); it('should accept opts and handler', () => { @@ -386,6 +573,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_DELETED_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + region: ['us-west1'], + }); }); it('should accept opts and handler, default bucket', () => { @@ -402,6 +601,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_DELETED_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + region: ['us-west1'], + }); }); }); @@ -410,6 +621,10 @@ describe('v2/storage', () => { ...EVENT_TRIGGER, eventType: storage.metadataUpdatedEvent, }; + const ENDPOINT_METADATA_TRIGGER = { + ...ENDPOINT_EVENT_TRIGGER, + eventType: storage.metadataUpdatedEvent, + }; let configStub: sinon.SinonStub; beforeEach(() => { @@ -434,6 +649,17 @@ describe('v2/storage', () => { }, }); + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_METADATA_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + }); + configStub.restore(); }); @@ -448,6 +674,17 @@ describe('v2/storage', () => { resource: 'my-bucket', }, }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_METADATA_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + }); }); it('should accept opts and handler', () => { @@ -465,6 +702,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_METADATA_TRIGGER, + eventFilters: { + bucket: 'my-bucket', + }, + }, + region: ['us-west1'], + }); }); it('should accept opts and handler, default bucket', () => { @@ -484,6 +733,18 @@ describe('v2/storage', () => { }, regions: ['us-west1'], }); + + expect(result.__endpoint).to.deep.equal({ + platform: 'gcfv2', + labels: {}, + eventTrigger: { + ...ENDPOINT_METADATA_TRIGGER, + eventFilters: { + bucket: 'default-bucket', + }, + }, + region: ['us-west1'], + }); }); }); }); diff --git a/src/cloud-functions.ts b/src/cloud-functions.ts index 688c4eb78..5f7823c2f 100644 --- a/src/cloud-functions.ts +++ b/src/cloud-functions.ts @@ -37,6 +37,7 @@ import { durationFromSeconds, serviceAccountFromShorthand, } from './common/encoding'; +import { ManifestEndpoint } from './common/manifest'; /** @hidden */ const WILDCARD_REGEX = new RegExp('{[^/{}]*}', 'g'); @@ -283,6 +284,14 @@ export interface TriggerAnnotated { }; } +/** + * @hidden + * EndpointAnnotated is used to generate the manifest that conforms to the container contract. + */ +export interface EndpointAnnotated { + __endpoint: ManifestEndpoint; +} + /** * A Runnable has a `run` method which directly invokes the user-defined * function - useful for unit testing. @@ -301,6 +310,7 @@ export interface Runnable { * arguments. */ export type HttpsFunction = TriggerAnnotated & + EndpointAnnotated & ((req: Request, resp: Response) => void | Promise); /** @@ -312,6 +322,7 @@ export type HttpsFunction = TriggerAnnotated & */ export type CloudFunction = Runnable & TriggerAnnotated & + EndpointAnnotated & ((input: any, context?: any) => PromiseLike | any); /** @hidden */ @@ -322,9 +333,9 @@ export interface MakeCloudFunctionArgs { dataConstructor?: (raw: Event) => EventData; eventType: string; handler?: (data: EventData, context: EventContext) => PromiseLike | any; - labels?: { [key: string]: any }; + labels?: Record; legacyEventType?: string; - options?: { [key: string]: any }; + options?: DeploymentOptions; /* * TODO: should remove `provider` and require a fully qualified `eventType` * once all providers have migrated to new format. @@ -432,6 +443,37 @@ export function makeCloudFunction({ }, }); + Object.defineProperty(cloudFunction, '__endpoint', { + get: () => { + if (triggerResource() == null) { + return undefined; + } + + const endpoint: ManifestEndpoint = { + platform: 'gcfv1', + ...optionsToEndpoint(options), + }; + + if (options.schedule) { + endpoint.scheduleTrigger = options.schedule; + } else { + endpoint.eventTrigger = { + eventType: legacyEventType || provider + '.' + eventType, + eventFilters: { + resource: triggerResource(), + }, + retry: !!options.failurePolicy, + }; + } + + if (Object.keys(labels).length > 0) { + endpoint.labels = { ...endpoint.labels, ...labels }; + } + + return endpoint; + }, + }); + cloudFunction.run = handler || contextOnlyHandler; return cloudFunction; } @@ -545,3 +587,49 @@ export function optionsToTrigger(options: DeploymentOptions) { return trigger; } + +export function optionsToEndpoint( + options: DeploymentOptions +): ManifestEndpoint { + const endpoint: ManifestEndpoint = {}; + copyIfPresent( + endpoint, + options, + 'minInstances', + 'maxInstances', + 'ingressSettings', + 'labels', + 'timeoutSeconds' + ); + convertIfPresent(endpoint, options, 'region', 'regions'); + convertIfPresent( + endpoint, + options, + 'serviceAccountEmail', + 'serviceAccount', + (sa) => sa + ); + if (options.vpcConnector) { + const vpc: ManifestEndpoint['vpc'] = { connector: options.vpcConnector }; + convertIfPresent( + vpc, + options, + 'egressSettings', + 'vpcConnectorEgressSettings' + ); + endpoint.vpc = vpc; + } + convertIfPresent(endpoint, options, 'availableMemoryMb', 'memory', (mem) => { + const memoryLookup = { + '128MB': 128, + '256MB': 256, + '512MB': 512, + '1GB': 1024, + '2GB': 2048, + '4GB': 4096, + '8GB': 8192, + }; + return memoryLookup[mem]; + }); + return endpoint; +} diff --git a/src/common/manifest.ts b/src/common/manifest.ts new file mode 100644 index 000000000..02784b68c --- /dev/null +++ b/src/common/manifest.ts @@ -0,0 +1,79 @@ +// The MIT License (MIT) +// +// Copyright (c) 2021 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/** + * @internal + * An definition of a function as appears in the Manifest. + */ +export interface ManifestEndpoint { + entryPoint?: string; + region?: string[]; + platform?: string; + availableMemoryMb?: number; + maxInstances?: number; + minInstances?: number; + concurrency?: number; + serviceAccountEmail?: string; + timeoutSeconds?: number; + vpc?: { + connector: string; + egressSettings?: string; + }; + labels?: Record; + ingressSettings?: string; + environmentVariables?: Record; + + httpsTrigger?: { + invoker?: string[]; + }; + + callableTrigger?: {}; + + eventTrigger?: { + eventFilters: Record; + eventType: string; + retry: boolean; + region?: string; + serviceAccountEmail?: string; + }; + + scheduleTrigger?: { + schedule?: string; + timezone?: string; + retryConfig?: { + retryCount?: number; + maxRetryDuration?: string; + minBackoffDuration?: string; + maxBackoffDuration?: string; + maxDoublings?: number; + }; + }; +} + +/** + * @internal + * An definition of a function deployment as appears in the Manifest. + **/ +export interface ManifestBackend { + specVersion: 'v1alpha1'; + requiredAPIs: Record; + endpoints: Record; +} diff --git a/src/handler-builder.ts b/src/handler-builder.ts index ad4cd1541..d620eee57 100644 --- a/src/handler-builder.ts +++ b/src/handler-builder.ts @@ -70,6 +70,7 @@ export class HandlerBuilder { ): HttpsFunction => { const func = https._onRequestWithOptions(handler, {}); func.__trigger = {}; + func.__endpoint = {}; return func; }, onCall: ( @@ -80,6 +81,7 @@ export class HandlerBuilder { ): HttpsFunction => { const func = https._onCallWithOptions(handler, {}); func.__trigger = {}; + func.__endpoint = {}; return func; }, }; diff --git a/src/providers/https.ts b/src/providers/https.ts index 8a48f41fe..842ebd84c 100644 --- a/src/providers/https.ts +++ b/src/providers/https.ts @@ -22,7 +22,12 @@ import * as express from 'express'; -import { HttpsFunction, optionsToTrigger, Runnable } from '../cloud-functions'; +import { + HttpsFunction, + optionsToEndpoint, + optionsToTrigger, + Runnable, +} from '../cloud-functions'; import { convertIfPresent, convertInvoker } from '../common/encoding'; import { CallableContext, @@ -77,6 +82,19 @@ export function _onRequestWithOptions( convertInvoker ); // TODO parse the options + + cloudFunction.__endpoint = { + platform: 'gcfv1', + ...optionsToEndpoint(options), + httpsTrigger: {}, + }; + convertIfPresent( + cloudFunction.__endpoint.httpsTrigger, + options, + 'invoker', + 'invoker', + convertInvoker + ); return cloudFunction; } @@ -105,6 +123,13 @@ export function _onCallWithOptions( }; func.__trigger.labels['deployment-callable'] = 'true'; + func.__endpoint = { + platform: 'gcfv1', + labels: {}, + ...optionsToEndpoint(options), + callableTrigger: {}, + }; + func.run = handler; return func; diff --git a/src/v2/core.ts b/src/v2/core.ts index d471c107b..c790be7a8 100644 --- a/src/v2/core.ts +++ b/src/v2/core.ts @@ -20,6 +20,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import { ManifestEndpoint } from '../common/manifest'; + /** @internal */ export interface TriggerAnnotation { concurrency?: number; @@ -91,7 +93,8 @@ export interface CloudEvent { export interface CloudFunction { (raw: CloudEvent): any | Promise; - __trigger: unknown; + __trigger?: unknown; + __endpoint: ManifestEndpoint; run(event: CloudEvent): any | Promise; } diff --git a/src/v2/options.ts b/src/v2/options.ts index 076cb8532..6591554ad 100644 --- a/src/v2/options.ts +++ b/src/v2/options.ts @@ -21,14 +21,16 @@ // SOFTWARE. import { + convertIfPresent, + copyIfPresent, durationFromSeconds, serviceAccountFromShorthand, } from '../common/encoding'; -import { convertIfPresent, copyIfPresent } from '../common/encoding'; import * as logger from '../logger'; import { TriggerAnnotation } from './core'; import { declaredParams } from './params'; import { ParamSpec } from './params/types'; +import { ManifestEndpoint } from '../common/manifest'; /** * List of all regions supported by Cloud Functions v2 @@ -279,6 +281,52 @@ export function optionsToTriggerAnnotations( return annotation; } +/** + * Apply GlobalOptions to endpoint manifest. + * @internal + */ +export function optionsToEndpoint( + opts: GlobalOptions | EventHandlerOptions +): ManifestEndpoint { + const endpoint: ManifestEndpoint = {}; + copyIfPresent( + endpoint, + opts, + 'concurrency', + 'minInstances', + 'maxInstances', + 'ingressSettings', + 'labels', + 'timeoutSeconds' + ); + convertIfPresent(endpoint, opts, 'serviceAccountEmail', 'serviceAccount'); + if (opts.vpcConnector) { + const vpc: ManifestEndpoint['vpc'] = { connector: opts.vpcConnector }; + convertIfPresent(vpc, opts, 'egressSettings', 'vpcConnectorEgressSettings'); + endpoint.vpc = vpc; + } + convertIfPresent(endpoint, opts, 'availableMemoryMb', 'memory', (mem) => { + const memoryLookup = { + '128MB': 128, + '256MB': 256, + '512MB': 512, + '1GB': 1024, + '2GB': 2048, + '4GB': 4096, + '8GB': 8192, + }; + return memoryLookup[mem]; + }); + convertIfPresent(endpoint, opts, 'region', 'region', (region) => { + if (typeof region === 'string') { + return [region]; + } + return region; + }); + + return endpoint; +} + /** * @hidden */ diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index 9e4c466e4..b89a26ed4 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -24,6 +24,7 @@ import * as cors from 'cors'; import * as express from 'express'; import { convertIfPresent, convertInvoker } from '../../common/encoding'; +import * as options from '../options'; import { CallableRequest, FunctionsErrorCode, @@ -31,7 +32,7 @@ import { onCallHandler, Request, } from '../../common/providers/https'; -import * as options from '../options'; +import { ManifestEndpoint } from '../../common/manifest'; export { Request, CallableRequest, FunctionsErrorCode, HttpsError }; @@ -46,7 +47,10 @@ export interface HttpsOptions extends Omit { export type HttpsFunction = (( req: Request, res: express.Response -) => void | Promise) & { __trigger: unknown }; +) => void | Promise) & { + __trigger?: unknown; + __endpoint: ManifestEndpoint; +}; export interface CallableFunction extends HttpsFunction { run(data: CallableRequest): Return; } @@ -95,6 +99,7 @@ export function onRequest( }); }; } + Object.defineProperty(handler, '__trigger', { get: () => { const baseOpts = options.optionsToTriggerAnnotations( @@ -130,6 +135,30 @@ export function onRequest( return trigger; }, }); + + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); + // global options calls region a scalar and https allows it to be an array, + // but optionsToTriggerAnnotations handles both cases. + const specificOpts = options.optionsToEndpoint(opts as options.GlobalOptions); + const endpoint: Partial = { + platform: 'gcfv2', + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + httpsTrigger: {}, + }; + convertIfPresent( + endpoint.httpsTrigger, + opts, + 'invoker', + 'invoker', + convertInvoker + ); + (handler as HttpsFunction).__endpoint = endpoint; + return handler as HttpsFunction; } @@ -191,6 +220,21 @@ export function onCall>( }, }); + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); + // global options calls region a scalar and https allows it to be an array, + // but optionsToManifestEndpoint handles both cases. + const specificOpts = options.optionsToEndpoint(opts as options.GlobalOptions); + func.__endpoint = { + platform: 'gcfv2', + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + callableTrigger: {}, + }; + func.run = handler; return func; } diff --git a/src/v2/providers/pubsub.ts b/src/v2/providers/pubsub.ts index cca25b294..81c4bca68 100644 --- a/src/v2/providers/pubsub.ts +++ b/src/v2/providers/pubsub.ts @@ -1,5 +1,7 @@ -import { CloudEvent, CloudFunction } from '../core'; import * as options from '../options'; +import { CloudEvent, CloudFunction } from '../core'; +import { copyIfPresent } from '../../common/encoding'; +import { ManifestEndpoint } from '../../common/manifest'; /** * Interface representing a Google Cloud Pub/Sub message. @@ -133,7 +135,7 @@ export function onMessagePublished( func.run = handler; - // TypeScript doesn't recongize defineProperty as adding a property and complains + // TypeScript doesn't recognize defineProperty as adding a property and complains // that __trigger doesn't exist. We can either cast to any and lose all type safety // or we can just assign a meaningless value before calling defineProperty. func.__trigger = 'silence the transpiler'; @@ -164,5 +166,25 @@ export function onMessagePublished( }, }); + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); + const specificOpts = options.optionsToEndpoint(opts); + + const endpoint: ManifestEndpoint = { + platform: 'gcfv2', + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + eventTrigger: { + eventType: 'google.cloud.pubsub.topic.v1.messagePublished', + eventFilters: { topic }, + retry: false, + }, + }; + copyIfPresent(endpoint.eventTrigger, opts, 'retry', 'retry'); + func.__endpoint = endpoint; + return func; } diff --git a/src/v2/providers/storage.ts b/src/v2/providers/storage.ts index e58405820..c092d25a2 100644 --- a/src/v2/providers/storage.ts +++ b/src/v2/providers/storage.ts @@ -20,9 +20,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import * as options from '../options'; import { firebaseConfig } from '../../config'; import { CloudEvent, CloudFunction } from '../core'; -import * as options from '../options'; +import { copyIfPresent } from '../../common/encoding'; +import { ManifestEndpoint } from '../../common/manifest'; /** * An object within Google Cloud Storage. @@ -313,10 +315,11 @@ export function onOperation( func.run = handler; - // TypeScript doesn't recongize defineProperty as adding a property and complains - // that __trigger doesn't exist. We can either cast to any and lose all type safety + // TypeScript doesn't recognize defineProperty as adding a property and complains + // that __endpoint doesn't exist. We can either cast to any and lose all type safety // or we can just assign a meaningless value before calling defineProperty. func.__trigger = 'silence the transpiler'; + func.__endpoint = {} as ManifestEndpoint; Object.defineProperty(func, '__trigger', { get: () => { @@ -341,6 +344,34 @@ export function onOperation( }, }); + // SDK may attempt to read FIREBASE_CONFIG env var to fetch the default bucket name. + // To prevent runtime errors when FIREBASE_CONFIG env var is missing, we use getters. + Object.defineProperty(func, '__endpoint', { + get: () => { + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); + const specificOpts = options.optionsToEndpoint(opts); + + const endpoint: ManifestEndpoint = { + platform: 'gcfv2', + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + eventTrigger: { + eventType: eventType, + eventFilters: { + bucket, + }, + retry: false, + }, + }; + copyIfPresent(endpoint.eventTrigger, opts, 'retry', 'retry'); + return endpoint; + }, + }); + return func; }