Skip to content
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

Assign emulator ports and resolve hostnames upfront. #5083

Merged
merged 6 commits into from Oct 7, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 10 additions & 13 deletions scripts/emulator-tests/functionsEmulator.spec.ts
Expand Up @@ -10,13 +10,12 @@ import * as logform from "logform";

import { EmulatedTriggerDefinition } from "../../src/emulator/functionsEmulatorShared";
import { FunctionsEmulator } from "../../src/emulator/functionsEmulator";
import { Emulators } from "../../src/emulator/types";
import { EmulatorInfo, Emulators } from "../../src/emulator/types";
import { FakeEmulator } from "../../src/test/emulators/fakeEmulator";
import { TIMEOUT_LONG, TIMEOUT_MED, MODULE_ROOT } from "./fixtures";
import { logger } from "../../src/logger";
import * as registry from "../../src/emulator/registry";
import * as secretManager from "../../src/gcp/secretManager";
import { findAvailablePort } from "../../src/emulator/portUtils";

if ((process.env.DEBUG || "").toLowerCase().includes("spec")) {
const dropLogLevels = (info: logform.TransformableInfo) => info.message;
Expand Down Expand Up @@ -614,22 +613,20 @@ describe("FunctionsEmulator-Hub", function () {
});

describe("environment variables", () => {
const host = "127.0.0.1";
const startFakeEmulator = async (emulator: Emulators): Promise<number> => {
const port = await findAvailablePort(host, 4000);
const fake = new FakeEmulator(emulator, host, port);
const startFakeEmulator = async (emulator: Emulators): Promise<EmulatorInfo> => {
const fake = await FakeEmulator.create(emulator);
await registry.EmulatorRegistry.start(fake);
return port;
return fake.getInfo();
};

afterEach(() => {
return registry.EmulatorRegistry.stopAll();
});

it("should set env vars when the emulator is running", async () => {
const databasePort = await startFakeEmulator(Emulators.DATABASE);
const firestorePort = await startFakeEmulator(Emulators.FIRESTORE);
const authPort = await startFakeEmulator(Emulators.AUTH);
const database = await startFakeEmulator(Emulators.DATABASE);
const firestore = await startFakeEmulator(Emulators.FIRESTORE);
const auth = await startFakeEmulator(Emulators.AUTH);

await useFunction(emu, "functionId", () => {
return {
Expand All @@ -649,9 +646,9 @@ describe("FunctionsEmulator-Hub", function () {
.get("/fake-project-id/us-central1/functionId")
.expect(200)
.then((res) => {
expect(res.body.databaseHost).to.eql(`${host}:${databasePort}`);
expect(res.body.firestoreHost).to.eql(`${host}:${firestorePort}`);
expect(res.body.authHost).to.eql(`${host}:${authPort}`);
expect(res.body.databaseHost).to.eql(`${database.host}:${database.port}`);
expect(res.body.firestoreHost).to.eql(`${firestore.host}:${firestore.port}`);
expect(res.body.authHost).to.eql(`${auth.host}:${auth.port}`);
});
}).timeout(TIMEOUT_MED);

Expand Down
121 changes: 121 additions & 0 deletions src/emulator/ExpressBasedEmulator.ts
@@ -0,0 +1,121 @@
import * as cors from "cors";
import * as express from "express";
import * as bodyParser from "body-parser";

import * as utils from "../utils";
import { Emulators, EmulatorInstance, EmulatorInfo, ListenSpec } from "./types";
import { createServer } from "node:http";
import { IPV4_UNSPECIFIED, IPV6_UNSPECIFIED } from "./dns";
import { ListenOptions } from "node:net";

export interface ExpressBasedEmulatorOptions {
listen: ListenSpec[];
noCors?: boolean;
noBodyParser?: boolean;
}

export abstract class ExpressBasedEmulator implements EmulatorInstance {
static PATH_EXPORT = "/_admin/export";
static PATH_DISABLE_FUNCTIONS = "/functions/disableBackgroundTriggers";
static PATH_ENABLE_FUNCTIONS = "/functions/enableBackgroundTriggers";
static PATH_EMULATORS = "/emulators";

private destroyers = new Set<() => Promise<void>>();

constructor(private options: ExpressBasedEmulatorOptions) {}

protected createExpressApp(): Promise<express.Express> {
const app = express();
if (!this.options.noCors) {
// Enable CORS for all APIs, all origins (reflected), and all headers (reflected).
// This is enabled by default since most emulators are cookieless.
app.use(cors({ origin: true }));

// Return access-control-allow-private-network heder if requested
// Enables accessing locahost when site is exposed via tunnel see https://github.com/firebase/firebase-tools/issues/4227
// Aligns with https://wicg.github.io/private-network-access/#headers
// Replace with cors option if adopted, see https://github.com/expressjs/cors/issues/236
app.use((req, res, next) => {
if (req.headers["access-control-request-private-network"]) {
res.setHeader("access-control-allow-private-network", "true");
}
next();
});
}
if (!this.options.noBodyParser) {
app.use(bodyParser.json()); // used in most emulators
}
app.set("json spaces", 2);

return Promise.resolve(app);
}

async start(): Promise<void> {
const app = await this.createExpressApp();

const promises = [];
const specs = this.options.listen;

const listenOptions: ListenOptions[] = [];

const dualStackPorts = new Set();
for (const spec of specs) {
if (spec.address === IPV6_UNSPECIFIED.address) {
if (specs.some((s) => s.port === spec.port && s.address === IPV4_UNSPECIFIED.address)) {
// We can use the default dual-stack behavior in Node.js to listen on
// the same port on both IPv4 and IPv6 unspecified addresses on most OSes.
// https://nodejs.org/api/net.html#serverlistenport-host-backlog-callback
listenOptions.push({
port: spec.port,
ipv6Only: false,
});
dualStackPorts.add(spec.port);
}
}
}
// Then add options for non-dual-stack addresses and ports.
for (const spec of specs) {
if (!dualStackPorts.has(spec.port)) {
listenOptions.push({
host: spec.address,
port: spec.port,
ipv6Only: spec.family === "IPv6",
});
}
}

for (const opt of listenOptions) {
promises.push(
new Promise((resolve, reject) => {
const server = createServer(app).listen(opt);
server.once("listening", resolve);
server.once("error", reject);
this.destroyers.add(utils.createDestroyer(server));
})
);
}
}

async connect(): Promise<void> {
// no-op
}

async stop(): Promise<void> {
const promises = [];
for (const destroyer of this.destroyers) {
promises.push(destroyer().then(() => this.destroyers.delete(destroyer)));
}
await Promise.all(promises);
}

getInfo(): EmulatorInfo {
return {
name: this.getName(),
listen: this.options.listen,
host: this.options.listen[0].address,
port: this.options.listen[0].port,
};
}

abstract getName(): Emulators;
}