-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add experimental feature to export functions' IaC #10117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
61552be
f1df64a
dfb7f5f
78ce0b4
4d0ddb2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { Command } from "../command"; | ||
| import { FirebaseError } from "../error"; | ||
| import * as iac from "../functions/iac/export"; | ||
| import { normalizeAndValidate, configForCodebase } from "../functions/projectConfig"; | ||
| import * as clc from "colorette"; | ||
| import { logger } from "../logger"; | ||
|
|
||
| const EXPORTERS: Record<string, iac.Exporter> = { | ||
| internal: iac.getInternalIac, | ||
| }; | ||
|
|
||
| export const command = new Command("functions:export") | ||
| .description("export Cloud Functions code and configuration") | ||
| .option("--format <format>", `Format of the output. Can be ${Object.keys(EXPORTERS).join(", ")}.`) | ||
| .option( | ||
| "--codebase <codebase>", | ||
| "Optional codebase to export. If not specified, exports the default or only codebase.", | ||
| ) | ||
| .action(async (options: any) => { | ||
|
inlined marked this conversation as resolved.
|
||
| if (!options.format || !Object.keys(EXPORTERS).includes(options.format)) { | ||
|
Check warning on line 20 in src/commands/functions-export.ts
|
||
| throw new FirebaseError(`Must specify --format as ${Object.keys(EXPORTERS).join(", ")}.`); | ||
| } | ||
|
|
||
| const config = normalizeAndValidate(options.config?.src?.functions); | ||
|
Check warning on line 24 in src/commands/functions-export.ts
|
||
| let codebaseConfig; | ||
| if (options.codebase) { | ||
| codebaseConfig = configForCodebase(config, options.codebase); | ||
|
Check warning on line 27 in src/commands/functions-export.ts
|
||
| } else if (config.length === 1) { | ||
| codebaseConfig = config[0]; | ||
| } else { | ||
| codebaseConfig = configForCodebase(config, "default"); | ||
| } | ||
|
inlined marked this conversation as resolved.
inlined marked this conversation as resolved.
|
||
|
|
||
| if (!codebaseConfig.source) { | ||
| throw new FirebaseError("Codebase does not have a local source directory."); | ||
| } | ||
|
|
||
| const manifest = await EXPORTERS[options.format](options, codebaseConfig); | ||
|
|
||
| for (const [file, contents] of Object.entries(manifest)) { | ||
| logger.info(`Manifest file: ${clc.bold(file)}`); | ||
| logger.info(contents); | ||
| } | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { expect } from "chai"; | ||
| import * as sinon from "sinon"; | ||
| import * as yaml from "js-yaml"; | ||
|
|
||
| import * as exportIac from "./export"; | ||
| import * as runtimes from "../../deploy/functions/runtimes"; | ||
| import * as supported from "../../deploy/functions/runtimes/supported"; | ||
| import * as functionsConfig from "../../functionsConfig"; | ||
| import * as functionsEnv from "../../functions/env"; | ||
| import * as projectUtils from "../../projectUtils"; | ||
| import * as projectConfig from "../projectConfig"; | ||
| describe("export", () => { | ||
| let needProjectIdStub: sinon.SinonStub; | ||
|
|
||
| const mockDelegate = { | ||
| language: "nodejs", | ||
| runtime: supported.latest("nodejs"), | ||
| validate: sinon.stub(), | ||
| build: sinon.stub(), | ||
| discoverBuild: sinon.stub(), | ||
| bin: "node", | ||
| watch: sinon.stub(), | ||
| } as const; | ||
|
|
||
| beforeEach(() => { | ||
| sinon.stub(functionsConfig, "getFirebaseConfig").resolves({ projectId: "my-project" }); | ||
| sinon.stub(functionsEnv, "loadFirebaseEnvs").returns({}); | ||
| sinon.stub(runtimes, "getRuntimeDelegate").resolves(mockDelegate); | ||
| sinon.stub(supported, "guardVersionSupport"); | ||
| needProjectIdStub = sinon.stub(projectUtils, "needProjectId").returns("my-project"); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| sinon.restore(); | ||
| mockDelegate.validate.reset(); | ||
| mockDelegate.build.reset(); | ||
| mockDelegate.discoverBuild.reset(); | ||
| }); | ||
|
|
||
| describe("getInternalIac", () => { | ||
| it("should return functions.yaml with discovered build", async () => { | ||
| const mockBuild = { endpoints: { "my-func": { platform: "gcfv1" } } }; | ||
| mockDelegate.discoverBuild.resolves(mockBuild); | ||
|
|
||
| const options = { config: { path: (s: string) => s, projectDir: "dir" } }; | ||
| const codebase: projectConfig.ValidatedSingle = { | ||
| source: "src", | ||
| codebase: "default", | ||
| runtime: supported.latest("nodejs") as supported.ActiveRuntime, | ||
| }; | ||
|
|
||
| const result = await exportIac.getInternalIac(options, codebase); | ||
|
|
||
| expect(needProjectIdStub.calledOnce).to.be.true; | ||
| expect(mockDelegate.validate.calledOnce).to.be.true; | ||
| expect(mockDelegate.build.calledOnce).to.be.true; | ||
| expect(mockDelegate.discoverBuild.calledOnce).to.be.true; | ||
| expect(result).to.deep.equal({ | ||
| "functions.yaml": yaml.dump(mockBuild), | ||
| }); | ||
| }); | ||
|
|
||
| it("should throw if codebase has no source", async () => { | ||
| const options = { config: { path: (s: string) => s, projectDir: "dir" } }; | ||
| const codebase: projectConfig.ValidatedSingle = { | ||
| codebase: "default", | ||
| runtime: supported.latest("nodejs") as supported.ActiveRuntime, | ||
| } as unknown as projectConfig.ValidatedSingle; | ||
|
|
||
| await expect(exportIac.getInternalIac(options, codebase)).to.be.rejectedWith( | ||
| "Cannot export a codebase with no source", | ||
| ); | ||
| }); | ||
|
|
||
| it("should throw an error if discoverBuild fails", async () => { | ||
| mockDelegate.discoverBuild.rejects(new Error("Failed to discover build")); | ||
|
|
||
| const options = { config: { path: (s: string) => s, projectDir: "dir" } }; | ||
| const codebase: projectConfig.ValidatedSingle = { | ||
| source: "src", | ||
| codebase: "default", | ||
| runtime: supported.latest("nodejs") as supported.ActiveRuntime, | ||
| }; | ||
|
|
||
| await expect(exportIac.getInternalIac(options, codebase)).to.be.rejectedWith( | ||
| "Failed to discover build", | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
|
inlined marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import * as runtimes from "../../deploy/functions/runtimes"; | ||
| import * as supported from "../../deploy/functions/runtimes/supported"; | ||
| import * as functionsConfig from "../../functionsConfig"; | ||
| import * as projectConfig from "../projectConfig"; | ||
| import * as functionsEnv from "../../functions/env"; | ||
| import { logger } from "../../logger"; | ||
| import * as yaml from "js-yaml"; | ||
| import { needProjectId } from "../../projectUtils"; | ||
| import { FirebaseError } from "../../error"; | ||
|
|
||
| export type Exporter = ( | ||
| options: any, | ||
|
inlined marked this conversation as resolved.
|
||
| codebase: projectConfig.ValidatedSingle, | ||
| ) => Promise<Record<string, string>>; | ||
|
|
||
| /** | ||
| * Exports the functions.yaml format of the codebase. | ||
| */ | ||
|
inlined marked this conversation as resolved.
|
||
| export async function getInternalIac( | ||
| options: any, | ||
|
inlined marked this conversation as resolved.
inlined marked this conversation as resolved.
|
||
| codebase: projectConfig.ValidatedSingle, | ||
| ): Promise<Record<string, string>> { | ||
| if (!codebase.source) { | ||
| throw new FirebaseError("Cannot export a codebase with no source"); | ||
| } | ||
| const projectId = needProjectId(options); | ||
|
|
||
| const firebaseConfig = await functionsConfig.getFirebaseConfig(options); | ||
| const firebaseEnvs = functionsEnv.loadFirebaseEnvs(firebaseConfig, projectId); | ||
|
|
||
| const delegateContext: runtimes.DelegateContext = { | ||
| projectId, | ||
| sourceDir: options.config.path(codebase.source), | ||
| projectDir: options.config.projectDir, | ||
| runtime: codebase.runtime, | ||
| }; | ||
|
inlined marked this conversation as resolved.
|
||
|
|
||
| const runtimeDelegate = await runtimes.getRuntimeDelegate(delegateContext); | ||
| logger.debug(`Validating ${runtimeDelegate.language} source`); | ||
| supported.guardVersionSupport(runtimeDelegate.runtime); | ||
| await runtimeDelegate.validate(); | ||
|
|
||
| logger.debug(`Building ${runtimeDelegate.language} source`); | ||
| await runtimeDelegate.build(); | ||
|
|
||
| logger.debug(`Discovering ${runtimeDelegate.language} source`); | ||
| const build = await runtimeDelegate.discoverBuild( | ||
|
inlined marked this conversation as resolved.
|
||
| {}, // Assume empty runtimeConfig | ||
| firebaseEnvs, | ||
| ); | ||
|
|
||
| return { | ||
| "functions.yaml": yaml.dump(build), | ||
| }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.