Skip to content

Commit

Permalink
Refactor Functions runtime delegation process to no longer depend on …
Browse files Browse the repository at this point in the history
…cli options and context (#3994)

Code for parsing CF3 triggers from function source relies on CLI options/context object to retrieve active project id, functions source directory, project root, etc.

The refactoring here pulls out all references to CLI options and context object from the runtime delegate caller. This is done in preparation for having another caller for the runtime delegate (the Functions Emulator) in the near future.
  • Loading branch information
taeold committed Jan 12, 2022
1 parent 2158872 commit f35dedd
Show file tree
Hide file tree
Showing 7 changed files with 55 additions and 74 deletions.
32 changes: 19 additions & 13 deletions src/deploy/functions/prepare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import * as utils from "../../utils";
import { logger } from "../../logger";
import { ensureTriggerRegions } from "./triggerRegionHelper";
import { ensureServiceAgentRoles } from "./checkIam";
import e from "express";
import { DelegateContext } from "./runtimes";
import { FirebaseError } from "../../error";

function hasUserConfig(config: Record<string, unknown>): boolean {
// "firebase" key is always going to exist in runtime config.
Expand Down Expand Up @@ -53,18 +54,28 @@ export async function prepare(
options: Options,
payload: args.Payload
): Promise<void> {
if (!options.config.src.functions) {
return;
const projectId = needProjectId(options);

const sourceDirName = options.config.get("functions.source") as string;
if (!sourceDirName) {
throw new FirebaseError(
`No functions code detected at default location (./functions), and no functions.source defined in firebase.json`
);
}
const sourceDir = options.config.path(sourceDirName);

const runtimeDelegate = await runtimes.getRuntimeDelegate(context, options);
const delegateContext: DelegateContext = {
projectId,
sourceDir,
projectDir: options.config.projectDir,
runtime: (options.config.get("functions.runtime") as string) || "",
};
const runtimeDelegate = await runtimes.getRuntimeDelegate(delegateContext);
logger.debug(`Validating ${runtimeDelegate.name} source`);
await runtimeDelegate.validate();
logger.debug(`Building ${runtimeDelegate.name} source`);
await runtimeDelegate.build();

const projectId = needProjectId(options);

// Check that all necessary APIs are enabled.
const checkAPIsEnabled = await Promise.all([
ensureApiEnabled.ensure(projectId, "cloudfunctions.googleapis.com", "functions"),
Expand All @@ -85,14 +96,9 @@ export async function prepare(
context.firebaseConfig = firebaseConfig;
const runtimeConfig = await getFunctionsConfig(context);

utils.assertDefined(
options.config.src.functions.source,
"Error: 'functions.source' is not defined"
);
const source = options.config.src.functions.source;
const firebaseEnvs = functionsEnv.loadFirebaseEnvs(firebaseConfig, projectId);
const userEnvOpt = {
functionsSource: options.config.path(source),
functionsSource: sourceDir,
projectId: projectId,
projectAlias: options.projectAlias,
};
Expand Down Expand Up @@ -133,7 +139,7 @@ export async function prepare(
logBullet(
clc.cyan.bold("functions:") +
" preparing " +
clc.bold(options.config.src.functions.source) +
clc.bold(sourceDirName) +
" directory for uploading..."
);
}
Expand Down
15 changes: 4 additions & 11 deletions src/deploy/functions/runtimes/golang/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ import * as portfinder from "portfinder";
import * as spawn from "cross-spawn";

import { FirebaseError } from "../../../../error";
import { Options } from "../../../../options";
import { logger } from "../../../../logger";
import * as args from "../../args";
import * as backend from "../../backend";
import * as discovery from "../discovery";
import { needProjectId } from "../../../../projectUtils";
import * as gomod from "./gomod";
import * as runtimes from "..";

Expand All @@ -27,13 +24,9 @@ export const FUNCTIONS_CODEGEN = FUNCTIONS_SDK + "/support/codegen";
export const FUNCTIONS_RUNTIME = FUNCTIONS_SDK + "/support/runtime";

export async function tryCreateDelegate(
context: args.Context,
options: Options
context: runtimes.DelegateContext
): Promise<Delegate | undefined> {
const relativeSourceDir = options.config.get("functions.source") as string;
const sourceDir = options.config.path(relativeSourceDir);
const goModPath = path.join(sourceDir, "go.mod");
const projectId = needProjectId(options);
const goModPath = path.join(context.sourceDir, "go.mod");

let module: gomod.Module;
try {
Expand All @@ -44,7 +37,7 @@ export async function tryCreateDelegate(
return;
}

let runtime = options.config.get("functions.runtime");
let runtime = context.runtime;
if (!runtime) {
if (!module.version) {
throw new FirebaseError("Could not detect Golang version from go.mod");
Expand All @@ -61,7 +54,7 @@ export async function tryCreateDelegate(
runtime = VERSION_TO_RUNTIME[module.version];
}

return new Delegate(projectId, sourceDir, runtime, module);
return new Delegate(context.projectId, context.sourceDir, runtime, module);
}

export class Delegate {
Expand Down
36 changes: 16 additions & 20 deletions src/deploy/functions/runtimes/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Options } from "../../../options";
import * as backend from "../backend";
import * as args from "../args";
import * as golang from "./golang";
import * as node from "./node";
import * as validate from "../validate";
Expand Down Expand Up @@ -100,36 +98,34 @@ export interface RuntimeDelegate {
): Promise<backend.Backend>;
}

type Factory = (context: args.Context, options: Options) => Promise<RuntimeDelegate | undefined>;
export interface DelegateContext {
projectId: string;
// Absolute path of the Firebase project directory.
projectDir: string;
// Absolute path of the source directory.
sourceDir: string;
runtime: string;
}

type Factory = (context: DelegateContext) => Promise<RuntimeDelegate | undefined>;
const factories: Factory[] = [node.tryCreateDelegate, golang.tryCreateDelegate];

export async function getRuntimeDelegate(
context: args.Context,
options: Options
): Promise<RuntimeDelegate> {
const sourceDirName = options.config.get("functions.source") as string;
if (!sourceDirName) {
throw new FirebaseError(
`No functions code detected at default location (./functions), and no functions.source defined in firebase.json`
);
}
validate.functionsDirectoryExists(options, sourceDirName);
export async function getRuntimeDelegate(context: DelegateContext): Promise<RuntimeDelegate> {
const { projectDir, sourceDir, runtime } = context;
validate.functionsDirectoryExists(sourceDir, projectDir);

// There isn't currently an easy way to map from runtime name to a delegate, but we can at least guarantee
// that any explicit runtime from firebase.json is valid
const runtime = options.config.get("functions.runtime");
if (runtime && !isValidRuntime(runtime)) {
throw new FirebaseError("Cannot deploy function with runtime " + runtime);
throw new FirebaseError(`Cannot deploy function with runtime ${runtime}`);
}

for (const factory of factories) {
const delegate = await factory(context, options);
const delegate = await factory(context);
if (delegate) {
return delegate;
}
}

throw new FirebaseError(
`Could not detect language for functions at ${options.config.get("functions.source")}`
);
throw new FirebaseError(`Could not detect language for functions at ${sourceDir}`);
}
19 changes: 6 additions & 13 deletions src/deploy/functions/runtimes/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,28 @@ import * as fs from "fs";
import * as path from "path";

import { FirebaseError } from "../../../../error";
import { Options } from "../../../../options";
import { getRuntimeChoice } from "./parseRuntimeAndValidateSDK";
import * as args from "../../args";
import { logger } from "../../../../logger";
import * as backend from "../../backend";
import { needProjectId } from "../../../../projectUtils";
import * as runtimes from "..";
import * as validate from "./validate";
import { logger } from "../../../../logger";
import * as versioning from "./versioning";
import * as parseTriggers from "./parseTriggers";

export async function tryCreateDelegate(
context: args.Context,
options: Options
context: runtimes.DelegateContext
): Promise<Delegate | undefined> {
const projectRelativeSourceDir = options.config.get("functions.source") as string;
const sourceDir = options.config.path(projectRelativeSourceDir);
const packageJsonPath = path.join(sourceDir, "package.json");
const packageJsonPath = path.join(context.sourceDir, "package.json");

if (!(await promisify(fs.exists)(packageJsonPath))) {
logger.debug("Customer code is not Node");
return undefined;
}

// Check what runtime to use, first in firebase.json, then in 'engines' field.
let runtime = (options.config.get("functions.runtime") as runtimes.Runtime) || "";
// TODO: This method loads the Functions SDK version which is then manually loaded elsewhere.
// We should find a way to refactor this code so we're not repeatedly invoking node.
runtime = getRuntimeChoice(sourceDir, runtime);
const runtime = getRuntimeChoice(context.sourceDir, context.runtime);

if (!runtime.startsWith("nodejs")) {
logger.debug(
Expand All @@ -40,7 +33,7 @@ export async function tryCreateDelegate(
throw new FirebaseError(`Unexpected runtime ${runtime}`);
}

return new Delegate(needProjectId(options), options.config.projectDir, sourceDir, runtime);
return new Delegate(context.projectId, context.projectDir, context.sourceDir, runtime);
}

// TODO(inlined): Consider moving contents in parseRuntimeAndValidateSDK and validate around.
Expand All @@ -60,7 +53,7 @@ export class Delegate {
// Using a caching interface because we (may/will) eventually depend on the SDK version
// to decide whether to use the JS export method of discovery or the HTTP container contract
// method of discovery.
_sdkVersion: string = "";
_sdkVersion = "";
get sdkVersion() {
if (!this._sdkVersion) {
this._sdkVersion = versioning.getFunctionsSDKVersion(this.sourceDir) || "";
Expand Down
6 changes: 2 additions & 4 deletions src/deploy/functions/runtimes/node/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ const cjson = require("cjson");
function assertFunctionsSourcePresent(data: any, sourceDir: string, projectDir: string): void {
const indexJsFile = path.join(sourceDir, data.main || "index.js");
if (!fsutils.fileExistsSync(indexJsFile)) {
const msg = `${path.relative(
projectDir,
indexJsFile
)} does not exist, can't deploy Cloud Functions`;
const relativeMainPath = path.relative(projectDir, indexJsFile);
const msg = `${relativeMainPath} does not exist, can't deploy Cloud Functions`;
throw new FirebaseError(msg);
}
}
Expand Down
17 changes: 6 additions & 11 deletions src/deploy/functions/validate.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import * as path from "path";
import * as clc from "cli-color";

import { FirebaseError } from "../../error";
import { getFunctionLabel } from "./functionsDeployHelper";
import * as backend from "./backend";
import * as fsutils from "../../fsutils";
import * as projectPath from "../../projectPath";

/**
* Check that functions directory exists.
* @param options options object. In prod is an Options; in tests can just be {cwd: string}
* @param sourceDirName Relative path to source directory.
* @param sourceDir Absolute path to source directory.
* @param projectDir Absolute path to project directory.
* @throws { FirebaseError } Functions directory must exist.
*/
export function functionsDirectoryExists(
options: { cwd: string; configPath?: string },
sourceDirName: string
): void {
// Note(inlined): What's the difference between this and options.config.path(sourceDirName)?
if (!fsutils.dirExistsSync(projectPath.resolveProjectPath(options, sourceDirName))) {
export function functionsDirectoryExists(sourceDir: string, projectDir: string): void {
if (!fsutils.dirExistsSync(sourceDir)) {
const sourceDirName = path.relative(projectDir, sourceDir);
const msg =
`could not deploy functions because the ${clc.bold('"' + sourceDirName + '"')} ` +
`directory was not found. Please create it or specify a different source directory in firebase.json`;
Expand Down
4 changes: 2 additions & 2 deletions src/test/deploy/functions/validate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe("validate", () => {
dirExistsStub.returns(true);

expect(() => {
validate.functionsDirectoryExists({ cwd: "cwd" }, "sourceDirName");
validate.functionsDirectoryExists("/cwd/sourceDirName", "/cwd");
}).to.not.throw();
});

Expand All @@ -35,7 +35,7 @@ describe("validate", () => {
dirExistsStub.returns(false);

expect(() => {
validate.functionsDirectoryExists({ cwd: "cwd" }, "sourceDirName");
validate.functionsDirectoryExists("/cwd/sourceDirName", "/cwd");
}).to.throw(FirebaseError);
});
});
Expand Down

0 comments on commit f35dedd

Please sign in to comment.