diff --git a/package.json b/package.json index 413a81c45..668947f78 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,29 @@ "./logger": "./lib/logger/index.js", "./logger/compat": "./lib/logger/compat.js", "./lib/logger": "./lib/logger/index.js", - "./lib/logger/compat": "./lib/logger/compat.js" + "./lib/logger/compat": "./lib/logger/compat.js", + "./v2": "./lib/v2/index.js", + "./v2/options": "./lib/v2/options.js", + "./v2/https": "./lib/v2/providers/https.js" + }, + "typesVersions": { + "*": { + "logger/*": [ + "lib/logger/*" + ], + "v1": [ + "lib/v1" + ], + "v2": [ + "lib/v2" + ], + "v2/options": [ + "lib/v2/options" + ], + "v2/https": [ + "lib/v2/providers/https" + ] + } }, "publishConfig": { "registry": "https://wombat-dressing-room.appspot.com" diff --git a/src/index.ts b/src/index.ts index 5b98253d9..fcac62f27 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,23 @@ +// 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. + export * from './v1'; diff --git a/src/v2/base.ts b/src/v2/base.ts new file mode 100644 index 000000000..c2d9a6fe4 --- /dev/null +++ b/src/v2/base.ts @@ -0,0 +1,42 @@ +// 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 */ +export interface TriggerAnnotation { + availableMemoryMb?: number; + eventTrigger?: { + eventType: string; + resource: string; + service: string; + }; + failurePolicy?: { retry: boolean }; + httpsTrigger?: {}; + labels?: { [key: string]: string }; + regions?: string[]; + timeout?: string; + vpcConnector?: string; + vpcConnectorEgressSettings?: string; + serviceAccountEmail?: string; + ingressSettings?: string; + + // TODO: schedule +} diff --git a/src/v2/index.ts b/src/v2/index.ts new file mode 100644 index 000000000..a41333a3d --- /dev/null +++ b/src/v2/index.ts @@ -0,0 +1,28 @@ +// 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. + +import * as https from './providers/https'; +import * as logger from '../logger'; + +export { https, logger }; + +export { setGlobalOptions, GlobalOptions } from './options'; diff --git a/src/v2/options.ts b/src/v2/options.ts new file mode 100644 index 000000000..d2382a8e7 --- /dev/null +++ b/src/v2/options.ts @@ -0,0 +1,270 @@ +// 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. + +import { + durationFromSeconds, + serviceAccountFromShorthand, +} from '../common/encoding'; +import * as logger from '../logger'; +import { TriggerAnnotation } from './base'; +import { copyIfPresent, convertIfPresent } from '../common/encoding'; + +/** + * List of all regions supported by Cloud Functions v2 + */ +export const SUPPORTED_REGIONS = ['us-west1'] as const; + +/** + * A region known to be supported by CloudFunctions v2 + */ +export type SupportedRegion = typeof SUPPORTED_REGIONS[number]; + +/** + * Cloud Functions v2 min timeout value. + */ +export const MIN_TIMEOUT_SECONDS = 1; + +/** + * Cloud Functions v2 max timeout value for event handlers. + */ +export const MAX_EVENT_TIMEOUT_SECONDS = 540; + +/** + * Cloud Functions v2 max timeout for HTTPS functions. + */ +export const MAX_HTTPS_TIMEOUT_SECONDS = 36_000; + +/** + * Maximum number of requests to serve on a single instance. + */ +export const MAX_CONCURRENCY = 1_000; + +/** + * List of available memory options supported by Cloud Functions. + */ +export const SUPPORTED_MEMORY_OPTIONS = [ + '256MB', + '512MB', + '1GB', + '2GB', + '4GB', + '8GB', +] as const; + +const MemoryOptionToMB: Record = { + '256MB': 256, + '512MB': 512, + '1GB': 1024, + '2GB': 2048, + '4GB': 4096, + '8GB': 8192, +}; + +/** + * A supported memory option. + */ +export type MemoryOption = typeof SUPPORTED_MEMORY_OPTIONS[number]; + +/** + * List of available options for VpcConnectorEgressSettings. + */ +export const SUPPORTED_VPC_EGRESS_SETTINGS = [ + 'PRIVATE_RANGES_ONLY', + 'ALL_TRAFFIC', +] as const; + +/** + * A valid VPC Egress setting. + */ +export type VpcEgressSetting = typeof SUPPORTED_VPC_EGRESS_SETTINGS[number]; + +/** + * List of available options for IngressSettings. + */ +export const SUPPORTED_INGRESS_SETTINGS = [ + 'ALLOW_ALL', + 'ALLOW_INTERNAL_ONLY', + 'ALLOW_INTERNAL_AND_GCLB', +] as const; + +export type IngressSetting = typeof SUPPORTED_INGRESS_SETTINGS[number]; + +/** + * GlobalOptions are options that can be set across an entire project. + * These options are common to HTTPS and Event handling functions. + */ +export interface GlobalOptions { + /** + * Region where functions should be deployed. + * HTTP functions can override and specify more than one region. + */ + region?: SupportedRegion | string; + + /** + * Amount of memory to allocate to a function. + * A value of null restores the defaults of 256MB. + */ + memory?: MemoryOption | null; + + /** + * Timeout for the function in sections, possible values are 0 to 540. + * HTTPS functions can specify a higher timeout. + * A value of null restores the default of 60s + */ + timeoutSeconds?: number | null; + + /** + * Min number of actual instances to be running at a given time. + * Instances will be billed for memory allocation and 10% of CPU allocation + * while idle. + * A value of null restores the default min instances. + */ + minInstances?: number | null; + + /** + * Max number of instances to be running in parallel. + * A value of null restores the default max instances. + */ + maxInstances?: number | null; + + /** + * Number of requests a function can serve at once. + * Can only be applied to functions running on Cloud Functions v2. + * A value of null restores the default concurrency. + */ + concurrency?: number | null; + /** + * Connect cloud function to specified VPC connector. + * A value of null removes the VPC connector + */ + vpcConnector?: string | null; + + /** + * Egress settings for VPC connector. + * A value of null turns off VPC connector egress settings + */ + vpcConnectorEgressSettings?: VpcEgressSetting | null; + + /** + * Specific service account for the function to run as. + * A value of null restores the default service account. + */ + serviceAccount?: string | null; + + /** + * Ingress settings which control where this function can be called from. + * A value of null turns off ingress settings. + */ + ingressSettings?: IngressSetting | null; + + /** + * User labels to set on the function. + */ + labels?: Record; +} + +let globalOptions: GlobalOptions | undefined; + +/** + * Sets default options for all functions written using the v2 SDK. + * @param options Options to set as default + */ +export function setGlobalOptions(options: GlobalOptions) { + if (globalOptions) { + logger.warn('Calling setGlobalOptions twice leads to undefined behavior'); + } + globalOptions = options; +} + +/** + * Get the currently set default options. + * Used only for trigger generation. + * @internal + */ +export function getGlobalOptions(): GlobalOptions { + return globalOptions || {}; +} + +/** + * Options that can be set on an individual event-handling Cloud Function. + */ +export interface EventHandlerOptions extends GlobalOptions { + retry?: boolean; +} + +/** + * Apply GlobalOptions to trigger definitions. + * @internal + */ +export function optionsToTriggerAnnotations( + opts: GlobalOptions | EventHandlerOptions +): TriggerAnnotation { + const annotation: TriggerAnnotation = {}; + copyIfPresent( + annotation, + opts, + 'ingressSettings', + 'labels', + 'vpcConnector', + 'vpcConnectorEgressSettings' + ); + convertIfPresent( + annotation, + opts, + 'availableMemoryMb', + 'memory', + (mem: MemoryOption) => { + return MemoryOptionToMB[mem]; + } + ); + convertIfPresent(annotation, opts, 'regions', 'region', (region) => { + if (typeof 'region' === 'string') { + return [region]; + } + return region; + }); + convertIfPresent( + annotation, + opts, + 'serviceAccountEmail', + 'serviceAccount', + serviceAccountFromShorthand + ); + convertIfPresent( + annotation, + opts, + 'timeout', + 'timeoutSeconds', + durationFromSeconds + ); + convertIfPresent( + annotation, + (opts as any) as EventHandlerOptions, + 'failurePolicy', + 'retry', + (retry: boolean) => { + return retry ? { retry: true } : null; + } + ); + + return annotation; +} diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts new file mode 100644 index 000000000..7faa9c6c1 --- /dev/null +++ b/src/v2/providers/https.ts @@ -0,0 +1,158 @@ +// 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. + +import * as cors from 'cors'; +import * as express from 'express'; + +import * as common from '../../common/providers/https'; +import * as options from '../options'; + +type Request = common.Request; + +export type CallableContext = common.CallableContext; +export type FunctionsErrorCode = common.FunctionsErrorCode; +export type HttpsError = common.HttpsError; + +export interface HttpsOptions extends Omit { + region?: + | options.SupportedRegion + | string + | Array; + cors?: string | boolean; +} + +export type HttpsHandler = ( + request: Request, + response: express.Response +) => void | Promise; +export type CallableHandler = ( + data: any, + context: CallableContext +) => any | Promise; + +export type HttpsFunction = HttpsHandler & { __trigger: unknown }; + +export function onRequest( + opts: HttpsOptions, + handler: HttpsHandler +): HttpsFunction; +export function onRequest(handler: HttpsHandler): HttpsFunction; +export function onRequest( + optsOrHandler: HttpsOptions | HttpsHandler, + handler?: HttpsHandler +): HttpsFunction { + let opts: HttpsOptions; + if (arguments.length === 1) { + opts = {}; + handler = optsOrHandler as HttpsHandler; + } else { + opts = optsOrHandler as HttpsOptions; + } + + if ('cors' in opts) { + const userProvidedHandler = handler; + handler = (req: Request, res: express.Response) => { + cors({ origin: opts.cors })(req, res, () => { + userProvidedHandler(req, res); + }); + }; + } + Object.defineProperty(handler, '__trigger', { + get: () => { + const baseOpts = options.optionsToTriggerAnnotations( + options.getGlobalOptions() + ); + // global options calls region a scalar and https allows it to be an array, + // but optionsToTriggerAnnotations handles both cases. + const specificOpts = options.optionsToTriggerAnnotations( + opts as options.GlobalOptions + ); + return { + // TODO(inlined): Remove "apiVersion" once the latest version of the CLI + // has migrated to "platform". + apiVersion: 2, + platform: 'gcfv2', + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + httpsTrigger: { + allowInsecure: false, + }, + }; + }, + }); + return handler as HttpsFunction; +} + +export function onCall( + opts: HttpsOptions, + handler: CallableHandler +): HttpsFunction; +export function onCall(handler: CallableHandler): HttpsFunction; +export function onCall( + optsOrHandler: HttpsOptions | CallableHandler, + handler?: CallableHandler +): HttpsFunction { + let opts: HttpsOptions; + if (arguments.length == 1) { + opts = {}; + handler = optsOrHandler as CallableHandler; + } else { + opts = optsOrHandler as HttpsOptions; + } + + const origin = 'cors' in opts ? opts.cors : true; + const func = common.onCallHandler({ origin, methods: 'POST' }, handler); + + Object.defineProperty(func, '__trigger', { + get: () => { + const baseOpts = options.optionsToTriggerAnnotations( + options.getGlobalOptions() + ); + // global options calls region a scalar and https allows it to be an array, + // but optionsToTriggerAnnotations handles both cases. + const specificOpts = options.optionsToTriggerAnnotations( + opts as options.GlobalOptions + ); + return { + // TODO(inlined): Remove "apiVersion" once the latest version of the CLI + // has migrated to "platform". + apiVersion: 2, + platform: 'gcfv2', + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + 'deployment-callable': 'true', + }, + httpsTrigger: { + allowInsecure: false, + }, + }; + }, + }); + return func as HttpsFunction; +} diff --git a/tsconfig.release.json b/tsconfig.release.json index b8f2632fd..a53421f8f 100644 --- a/tsconfig.release.json +++ b/tsconfig.release.json @@ -10,5 +10,10 @@ "target": "es2018", "typeRoots": ["./node_modules/@types"] }, - "files": ["./src/index.ts", "./src/logger/index.ts", "./src/logger/compat.ts"] + "files": [ + "./src/index.ts", + "./src/logger/index.ts", + "./src/logger/compat.ts", + "./src/v2/index.ts" + ] }