Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- Add Warsaw (europe-central2) Cloud Function Location to Firebase Extension template.
- Add Singapore (asia-southeast1) as a valid Firebase Realtime Database location.
111 changes: 111 additions & 0 deletions src/checkFirebaseSDKVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as _ from "lodash";
import * as clc from "cli-color";
import * as path from "path";
import * as semver from "semver";
import * as spawn from "cross-spawn";

import * as utils from "./utils";
import { FirebaseError } from "./error";
import { logger } from "./logger";

interface NpmListResult {
name: string;
dependencies: {
"firebase-functions": {
version: string;
from: string;
resolved: string;
};
};
}

interface NpmShowResult {
"dist-tags": {
latest: string;
};
}

/**
* Returns the version of firebase-functions SDK specified by package.json and package-lock.json.
* @param sourceDir Source directory of functions code
* @return version string (e.g. "3.1.2"), or void if firebase-functions is not in package.json
* or if we had trouble getting the version.
*/
export function getFunctionsSDKVersion(sourceDir: string): string | void {
try {
const child = spawn.sync("npm", ["list", "firebase-functions", "--json=true"], {
cwd: sourceDir,
encoding: "utf8",
});
if (child.error) {
logger.debug("getFunctionsSDKVersion encountered error:", child.error.stack);
return;
}
const output: NpmListResult = JSON.parse(child.stdout);
return _.get(output, ["dependencies", "firebase-functions", "version"]);
} catch (e) {
logger.debug("getFunctionsSDKVersion encountered error:", e);
return;
}
}

/**
* Checks if firebase-functions SDK is not the latest version in NPM, and prints update notice if it is outdated.
* If it is unable to do the check, it does nothing.
* @param options Options object from "firebase deploy" command.
*/
export function checkFunctionsSDKVersion(options: any): void {
if (!options.config.has("functions")) {
return;
}

const sourceDirName = options.config.get("functions.source");
if (!sourceDirName) {
throw new FirebaseError(
`No functions code detected at default location ("./functions"), and no "functions.source" defined in "firebase.json"`
);
}
const sourceDir = path.join(options.config.projectDir, sourceDirName);
const currentVersion = getFunctionsSDKVersion(sourceDir);
if (!currentVersion) {
logger.debug("getFunctionsSDKVersion was unable to retrieve 'firebase-functions' version");
return;
}
try {
const child = spawn.sync("npm", ["show", "firebase-functions", "--json=true"], {
encoding: "utf8",
});
if (child.error) {
logger.debug(
"checkFunctionsSDKVersion was unable to fetch information from NPM",
child.error.stack
);
return;
}
const output: NpmShowResult = JSON.parse(child.stdout);
if (_.isEmpty(output)) {
return;
}
const latest = _.get(output, ["dist-tags", "latest"]);

if (semver.lt(currentVersion, latest)) {
utils.logWarning(
clc.bold.yellow("functions: ") +
"package.json indicates an outdated version of firebase-functions.\nPlease upgrade using " +
clc.bold("npm install --save firebase-functions@latest") +
" in your functions directory."
);
if (semver.satisfies(currentVersion, "0.x") && semver.satisfies(latest, "1.x")) {
utils.logWarning(
clc.bold.yellow("functions: ") +
"Please note that there will be breaking changes when you upgrade.\n Go to " +
clc.bold("https://firebase.google.com/docs/functions/beta-v1-diff") +
" to learn more."
);
}
}
} catch (e) {
logger.debug("checkFunctionsSDKVersion encountered error:", e);
return;
}
}
2 changes: 2 additions & 0 deletions src/commands/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { requireDatabaseInstance } = require("../requireDatabaseInstance");
const { requirePermissions } = require("../requirePermissions");
const { checkServiceAccountIam } = require("../deploy/functions/checkIam");
const checkValidTargetFilters = require("../checkValidTargetFilters");
const checkFunctionsSDKVersion = require("../checkFirebaseSDKVersion").checkFunctionsSDKVersion;
const { Command } = require("../command");
const deploy = require("../deploy");
const requireConfig = require("../requireConfig");
Expand Down Expand Up @@ -78,6 +79,7 @@ module.exports = new Command("deploy")
}
})
.before(checkValidTargetFilters)
.before(checkFunctionsSDKVersion)
.action(function (options) {
return deploy(options.filteredTargets, options);
});
3 changes: 3 additions & 0 deletions src/deploy/functions/args.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ReadStream } from "fs";

import * as backend from "./backend";
import * as gcfV2 from "../../gcp/cloudfunctionsv2";

Expand All @@ -19,6 +21,7 @@ export interface Context {

// Filled in the "prepare" phase.
functionsSource?: string;
runtimeChoice?: backend.Runtime;
runtimeConfigEnabled?: boolean;
firebaseConfig?: FirebaseConfig;

Expand Down
17 changes: 15 additions & 2 deletions src/deploy/functions/backend.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as proto from "../../gcp/proto";
import * as gcf from "../../gcp/cloudfunctions";
import * as gcfV2 from "../../gcp/cloudfunctionsv2";
import * as cloudscheduler from "../../gcp/cloudscheduler";
import * as utils from "../../utils";
import * as runtimes from "./runtimes";
import { FirebaseError } from "../../error";
import { Context } from "./args";
import { logger } from "../../logger";
import { previews } from "../../previews";

/** Retry settings for a ScheduleSpec. */
Expand Down Expand Up @@ -115,6 +116,18 @@ export function memoryOptionDisplayName(option: MemoryOptions): string {

export const SCHEDULED_FUNCTION_LABEL = Object.freeze({ deployment: "firebase-schedule" });

/** Supported runtimes for new Cloud Functions. */
export type Runtime = "nodejs10" | "nodejs12" | "nodejs14";

/** Runtimes that can be found in existing backends but not used for new functions. */
export type DeprecatedRuntime = "nodejs6" | "nodejs8";
const RUNTIMES: string[] = ["nodejs10", "nodejs12", "nodejs14"];

/** Type deduction helper for a runtime string. */
export function isValidRuntime(runtime: string): runtime is Runtime {
return RUNTIMES.includes(runtime);
}

/**
* IDs used to identify a regional resource.
* This type exists so we can have lightweight references from a Pub/Sub topic
Expand All @@ -138,7 +151,7 @@ export interface FunctionSpec extends TargetIds {
apiVersion: FunctionsApiVersion;
entryPoint: string;
trigger: HttpsTrigger | EventTrigger;
runtime: runtimes.Runtime | runtimes.DeprecatedRuntime;
runtime: Runtime | DeprecatedRuntime;

labels?: Record<string, string>;
environmentVariables?: Record<string, string>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ function isPermissionError(e: { context?: { body?: { error?: { status?: string }
* of the deployed functions.
*
* @param projectId Project ID upon which to check enablement.
* @param runtime The runtime as declared in package.json, e.g. `nodejs10`.
*/
export async function ensureCloudBuildEnabled(projectId: string): Promise<void> {
export async function checkRuntimeDependencies(projectId: string, runtime: string): Promise<void> {
try {
await ensure(projectId, CLOUD_BUILD_API, "functions");
} catch (e) {
Expand Down
53 changes: 53 additions & 0 deletions src/deploy/functions/discovery/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { FirebaseError } from "../../../error";
import { logger } from "../../../logger";
import * as backend from "../backend";
import * as args from "../args";
import * as jsTriggerParsing from "./jsexports/parseTriggers";
import { Options } from "../../../options";

type BackendDiscoveryStrategy = (
context: args.Context,
options: Options,
runtimeConfig: backend.RuntimeConfigValues,
env: backend.EnvironmentVariables
) => Promise<backend.Backend>;

type UseBackendDiscoveryStrategy = (context: args.Context) => Promise<boolean>;

type Strategy = {
name: string;
useStrategy: UseBackendDiscoveryStrategy;
discoverBackend: BackendDiscoveryStrategy;
};

const STRATEGIES: Strategy[] = [
{
name: "parseJSExports",
useStrategy: jsTriggerParsing.useStrategy,
discoverBackend: jsTriggerParsing.discoverBackend,
},
];

// TODO(inlined): Replace runtimeConfigValues with ENV variables.
// TODO(inlined): Parse the Runtime within this method instead of before it. We need this to support other languages.
export async function discoverBackendSpec(
context: args.Context,
options: Options,
runtimeConfigValues: backend.RuntimeConfigValues,
envs: backend.EnvironmentVariables
): Promise<backend.Backend> {
let strategy: Strategy | undefined = undefined;
for (const testStrategy of STRATEGIES) {
if (await testStrategy.useStrategy(context)) {
strategy = testStrategy;
break;
}
}

if (strategy) {
logger.debug("Analyizing backend with strategy", strategy.name);
} else {
throw new FirebaseError("Cannot determine how to analyze backend");
}
return strategy.discoverBackend(context, options, runtimeConfigValues, envs);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as backend from "../../backend";
import * as api from "../../../../api";
import * as proto from "../../../../gcp/proto";
import * as args from "../../args";
import * as runtimes from "../../runtimes";
import { Options } from "../../../../options";

const TRIGGER_PARSER = path.resolve(__dirname, "./triggerParser.js");

Expand Down Expand Up @@ -118,23 +118,23 @@ export function useStrategy(context: args.Context): Promise<boolean> {
}

export async function discoverBackend(
projectId: string,
sourceDir: string,
runtime: runtimes.Runtime,
context: args.Context,
options: Options,
configValues: backend.RuntimeConfigValues,
envs: backend.EnvironmentVariables
): Promise<backend.Backend> {
const triggerAnnotations = await parseTriggers(projectId, sourceDir, configValues, envs);
const sourceDir = options.config.path(options.config.get("functions.source") as string);
const triggerAnnotations = await parseTriggers(context.projectId, sourceDir, configValues, envs);
const want: backend.Backend = { ...backend.empty(), environmentVariables: envs };
for (const annotation of triggerAnnotations) {
addResourcesToBackend(projectId, runtime, annotation, want);
addResourcesToBackend(context.projectId, context.runtimeChoice!, annotation, want);
}
return want;
}

export function addResourcesToBackend(
projectId: string,
runtime: runtimes.Runtime,
runtime: backend.Runtime,
annotation: TriggerAnnotation,
want: backend.Backend
) {
Expand Down
Loading