Skip to content

Commit 2e9e0cf

Browse files
committed
Basic create support
This change adds support for `firebase --open-sesame golang`. After running this command, `firebase init` will support Go 1.13 as a langauge for Cloud Functions. Limitations: 1. .gitignore is empty 2. Customers cannot mix Node and Go code (WAI) 3. There is little validation being done of customer code 4. The actual deployed function params are hard coded; SDK incoming
1 parent 0052e72 commit 2e9e0cf

File tree

9 files changed

+329
-59
lines changed

9 files changed

+329
-59
lines changed

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class Config {
6868
if (
6969
this.projectDir &&
7070
!this.get("functions.source") &&
71-
fsutils.fileExistsSync(this.path("functions/package.json"))
71+
fsutils.dirExistsSync(this.path("functions"))
7272
) {
7373
this.set("functions.source", "functions");
7474
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { promisify } from "util";
2+
import * as path from "path";
3+
import * as fs from "fs";
4+
import * as spawn from "cross-spawn";
5+
6+
import { FirebaseError } from "../../../../error";
7+
import { Options } from "../../../../options";
8+
import { logger } from "../../../../logger";
9+
import * as args from "../../args";
10+
import * as backend from "../../backend";
11+
import * as getProjectId from "../../../../getProjectId";
12+
import * as runtimes from "..";
13+
14+
const VERSION_TO_RUNTIME: Record<string, runtimes.Runtime> = {
15+
"1.13": "go113",
16+
};
17+
18+
export async function tryCreateDelegate(
19+
context: args.Context,
20+
options: Options
21+
): Promise<Delegate | undefined> {
22+
const sourceDirName = options.config.get("functions.source") as string;
23+
const sourceDir = options.config.path(sourceDirName);
24+
const goModPath = path.join(sourceDir, "go.mod");
25+
const projectId = getProjectId(options);
26+
27+
let module: Module;
28+
try {
29+
const modBuffer = await promisify(fs.readFile)(goModPath);
30+
module = parseModule(modBuffer.toString("utf8"));
31+
} catch (err) {
32+
logger.debug("Customer code is not Golang code (or they aren't using modules)");
33+
return;
34+
}
35+
36+
let runtime = options.config.get("functions.runtime");
37+
if (!runtime) {
38+
if (!module.version) {
39+
throw new FirebaseError("Could not detect Golang version from go.mod");
40+
}
41+
if (!VERSION_TO_RUNTIME[module.version]) {
42+
throw new FirebaseError(
43+
`go.mod specifies Golang version ${
44+
module.version
45+
} which is unsupported by Google Cloud Functions. Valid values are ${Object.keys(
46+
VERSION_TO_RUNTIME
47+
).join(", ")}`
48+
);
49+
}
50+
runtime = VERSION_TO_RUNTIME[module.version];
51+
}
52+
53+
return new Delegate(projectId, sourceDirName, sourceDir, runtime, module);
54+
}
55+
56+
// A module can be much more complicated than this, but this is all we need so far.
57+
// for a full reference, see https://golang.org/doc/modules/gomod-ref
58+
interface Module {
59+
module: string;
60+
version: string;
61+
dependencies: Record<string, string>;
62+
}
63+
64+
export function parseModule(mod: string): Module {
65+
const module: Module = {
66+
module: "",
67+
version: "",
68+
dependencies: {},
69+
};
70+
const lines = mod.split("\n");
71+
let inRequire = false;
72+
for (const line of lines) {
73+
if (inRequire) {
74+
const endRequireMatch = /\)/.exec(line);
75+
if (endRequireMatch) {
76+
inRequire = false;
77+
continue;
78+
}
79+
80+
const requireMatch = /([^ ]+) (.*)/.exec(line);
81+
if (requireMatch) {
82+
module.dependencies[requireMatch[1]] = requireMatch[2];
83+
continue;
84+
}
85+
86+
if (line.trim()) {
87+
logger.debug("Don't know how to handle line", line, "inside a mod.go require block");
88+
}
89+
continue;
90+
}
91+
const modMatch = /^module (.*)$/.exec(line);
92+
if (modMatch) {
93+
module.module = modMatch[1];
94+
continue;
95+
}
96+
const versionMatch = /^go (\d+\.\d+)$/.exec(line);
97+
if (versionMatch) {
98+
module.version = versionMatch[1];
99+
continue;
100+
}
101+
102+
const requireMatch = /^require ([^ ]+) (.*)$/.exec(line);
103+
if (requireMatch) {
104+
module.dependencies[requireMatch[1]] = requireMatch[2];
105+
continue;
106+
}
107+
108+
const requireBlockMatch = /^require +\(/.exec(line);
109+
if (requireBlockMatch) {
110+
inRequire = true;
111+
continue;
112+
}
113+
114+
if (line.trim()) {
115+
logger.debug("Don't know how to handle line", line, "in mod.go");
116+
}
117+
}
118+
119+
return module;
120+
}
121+
122+
export class Delegate {
123+
public readonly name = "golang";
124+
125+
constructor(
126+
private readonly projectId: string,
127+
private readonly sourceDirName: string,
128+
private readonly sourceDir: string,
129+
public readonly runtime: runtimes.Runtime,
130+
private readonly module: Module
131+
) {}
132+
validate(): Promise<void> {
133+
// throw new FirebaseError("Cannot yet analyze Go source code");
134+
return Promise.resolve();
135+
}
136+
137+
build(): Promise<void> {
138+
const res = spawn.sync("go", ["build"], {
139+
cwd: this.sourceDir,
140+
stdio: "inherit",
141+
});
142+
if (res.error) {
143+
logger.debug("Got error running go build", res);
144+
throw new FirebaseError("Failed to build functions source", { children: [res.error] });
145+
}
146+
147+
return Promise.resolve();
148+
}
149+
150+
watch(): Promise<() => Promise<void>> {
151+
return Promise.resolve(() => Promise.resolve());
152+
}
153+
154+
discoverSpec(
155+
configValues: backend.RuntimeConfigValues,
156+
envs: backend.EnvironmentVariables
157+
): Promise<backend.Backend> {
158+
const stubbed: backend.Backend = {
159+
requiredAPIs: {},
160+
topics: [],
161+
schedules: [],
162+
cloudFunctions: [
163+
{
164+
apiVersion: 1,
165+
id: "HelloWorld",
166+
region: "us-central1",
167+
project: this.projectId,
168+
entryPoint: "HelloWorld",
169+
runtime: this.runtime,
170+
trigger: {
171+
allowInsecure: false,
172+
},
173+
},
174+
],
175+
environmentVariables: envs,
176+
};
177+
return Promise.resolve(stubbed);
178+
}
179+
}

src/deploy/functions/runtimes/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { Options } from "../../../options";
22
import * as backend from "../backend";
33
import * as args from "../args";
4+
import * as golang from "./golang";
45
import * as node from "./node";
56
import * as validate from "../validate";
67
import { FirebaseError } from "../../../error";
78

89
/** Supported runtimes for new Cloud Functions. */
9-
const RUNTIMES: string[] = ["nodejs10", "nodejs12", "nodejs14"];
10+
const RUNTIMES: string[] = ["nodejs10", "nodejs12", "nodejs14", "go113"];
1011
export type Runtime = typeof RUNTIMES[number];
1112

1213
/** Runtimes that can be found in existing backends but not used for new functions. */
@@ -29,6 +30,7 @@ const MESSAGE_FRIENDLY_RUNTIMES: Record<Runtime | DeprecatedRuntime, string> = {
2930
nodejs10: "Node.js 10",
3031
nodejs12: "Node.js 12",
3132
nodejs14: "Node.js 14",
33+
go113: "Go 1.13",
3234
};
3335

3436
/**
@@ -94,7 +96,7 @@ export interface RuntimeDelegate {
9496
}
9597

9698
type Factory = (context: args.Context, options: Options) => Promise<RuntimeDelegate | undefined>;
97-
const factories: Factory[] = [node.tryCreateDelegate];
99+
const factories: Factory[] = [node.tryCreateDelegate, golang.tryCreateDelegate];
98100

99101
export async function getRuntimeDelegate(
100102
context: args.Context,
@@ -108,6 +110,13 @@ export async function getRuntimeDelegate(
108110
}
109111
validate.functionsDirectoryExists(options, sourceDirName);
110112

113+
// There isn't currently an easy way to map from runtime name to a delegate, but we can at least guarantee
114+
// that any explicit runtime from firebase.json is valid
115+
const runtime = options.config.get("functions.runtime");
116+
if (runtime && !isValidRuntime(runtime)) {
117+
throw new FirebaseError("Cannot deploy function with runtime " + runtime);
118+
}
119+
111120
for (const factory of factories) {
112121
const delegate = await factory(context, options);
113122
if (delegate) {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
import * as spawn from "cross-spawn";
4+
5+
import { FirebaseError } from "../../../error";
6+
import { Config } from "../../../config";
7+
import { promptOnce } from "../../../prompt";
8+
import * as utils from "../../../utils";
9+
import { logger } from "../../../logger";
10+
import { options } from "../../../commands/auth-export";
11+
12+
const clc = require("cli-color");
13+
14+
const ADMIN_SDK = "firebase.google.com/go/v4";
15+
const RUNTIME_VERSION = "1.13";
16+
17+
const TEMPLATE_ROOT = path.resolve(__dirname, "../../../../templates/init/functions/golang");
18+
const MAIN_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "functions.go"), "utf8");
19+
const GITIGNORE_TEMPLATE = fs.readFileSync(path.join(TEMPLATE_ROOT, "_gitignore"), "utf8");
20+
21+
async function init(setup: unknown, config: Config) {
22+
await writeModFile(config);
23+
await config.askWriteProjectFile("functions/functions.go", MAIN_TEMPLATE);
24+
await config.askWriteProjectFile("functions/.gitignore", GITIGNORE_TEMPLATE);
25+
}
26+
27+
// writeModFile is meant to look like askWriteProjectFile but it generates the contents
28+
// dynamically using the go tool
29+
async function writeModFile(config: Config) {
30+
const modPath = config.path("functions/go.mod");
31+
if (fs.existsSync(modPath)) {
32+
const shoudlWriteModFile = await promptOnce({
33+
type: "confirm",
34+
message: "File " + clc.underline("functions/go.mod") + " already exists. Overwrite?",
35+
default: false,
36+
});
37+
if (!shoudlWriteModFile) {
38+
return;
39+
}
40+
41+
// Go will refuse to overwrite an existing mod file.
42+
fs.unlinkSync(modPath);
43+
}
44+
45+
// Nit(inlined) can we look at functions code and see if there's a domain mapping?
46+
const modName = await promptOnce({
47+
type: "input",
48+
message: "What would you like to name your module?",
49+
default: "acme.com/functions",
50+
});
51+
// Manually create a go mod file because (A) it's easier this way and (B) it seems to be the only
52+
// way to set the min Go version to anything but what the user has installed.
53+
config.writeProjectFile(
54+
"functions/go.mod",
55+
"module " + modName + "\n\ngo " + RUNTIME_VERSION + "\n"
56+
);
57+
58+
// Should this come later as "would you like to install dependencies" to mirror Node?
59+
// It's less clearly distinct from node where you can edit the package.json file w/o installing.
60+
// Here we're actually locking in a version in go.mod _and_ installing it in one step.
61+
const result = spawn.sync("go", ["get", ADMIN_SDK], {
62+
cwd: config.path("functions"),
63+
stdio: "inherit",
64+
});
65+
if (result.error) {
66+
logger.debug("Full output from go get command:", JSON.stringify(result, null, 2));
67+
throw new FirebaseError("Error installing dependencies", { children: [result.error] });
68+
}
69+
utils.logSuccess("Wrote " + clc.bold("functions/go.mod"));
70+
}
71+
72+
module.exports = init;

src/init/features/functions/index.js

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)