diff --git a/packages/modules/gcloud/src/abstract-gcloud-emulator.ts b/packages/modules/gcloud/src/abstract-gcloud-emulator.ts new file mode 100644 index 000000000..0846e76bf --- /dev/null +++ b/packages/modules/gcloud/src/abstract-gcloud-emulator.ts @@ -0,0 +1,35 @@ +import { GenericContainer, Wait } from "testcontainers"; +import { EmulatorFlagsManager } from "./emulator-flags-manager"; + +export class AbstractGcloudEmulator extends GenericContainer { + private readonly flagsManager: EmulatorFlagsManager; + + constructor( + image: string, + port: number, + private readonly cmd: string + ) { + super(image); + this.flagsManager = new EmulatorFlagsManager(); + this.withExposedPorts(port) + .withFlag("host-port", `0.0.0.0:${port}`) + .withWaitStrategy(Wait.forLogMessage(/.*running.*/)) + .withStartupTimeout(120_000); + } + + /** + * Adds flag as argument to emulator start command. + * Adding same flag name twice replaces existing flag value. + * @param name flag name. Must be set to non-empty string. May optionally contain -- prefix. + * @param value flag value. May be empty string. + * @returns this instance for chaining. + */ + public withFlag(name: string, value: string) { + this.flagsManager.withFlag(name, value); + return this; + } + + public override async beforeContainerCreated(): Promise { + this.withCommand(["/bin/sh", "-c", `${this.cmd} ${this.flagsManager.expandFlags()}`]); + } +} diff --git a/packages/modules/gcloud/src/datastore-emulator-container.ts b/packages/modules/gcloud/src/datastore-emulator-container.ts index 8846c0ab2..a8f5f4b1d 100644 --- a/packages/modules/gcloud/src/datastore-emulator-container.ts +++ b/packages/modules/gcloud/src/datastore-emulator-container.ts @@ -1,16 +1,13 @@ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, StartedTestContainer } from "testcontainers"; +import { AbstractGcloudEmulator } from "./abstract-gcloud-emulator"; const EMULATOR_PORT = 8080; -const CMD = `gcloud beta emulators firestore start --host-port 0.0.0.0:${EMULATOR_PORT} --database-mode=datastore-mode`; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk"; -export class DatastoreEmulatorContainer extends GenericContainer { +export class DatastoreEmulatorContainer extends AbstractGcloudEmulator { constructor(image = DEFAULT_IMAGE) { - super(image); - this.withExposedPorts(EMULATOR_PORT) - .withCommand(["/bin/sh", "-c", CMD]) - .withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1)) - .withStartupTimeout(120_000); + super(image, EMULATOR_PORT, "gcloud beta emulators firestore start"); + this.withFlag("database-mode", `datastore-mode`); } public override async start(): Promise { diff --git a/packages/modules/gcloud/src/emulator-flags-manager.test.ts b/packages/modules/gcloud/src/emulator-flags-manager.test.ts new file mode 100644 index 000000000..15df56b5a --- /dev/null +++ b/packages/modules/gcloud/src/emulator-flags-manager.test.ts @@ -0,0 +1,62 @@ +import { EmulatorFlagsManager } from "./emulator-flags-manager"; + +describe("EmulatorFlagsManager", () => { + it("should add flag without --", async () => { + const flagsManager = new EmulatorFlagsManager().withFlag("database-mode", "firestore-native"); + + const flags = flagsManager.expandFlags(); + + expect(flags.trim()).toEqual("--database-mode=firestore-native"); + }); + + it("should add flag with --", async () => { + const flagsManager = new EmulatorFlagsManager().withFlag("--database-mode", "firestore-native"); + + const flags = flagsManager.expandFlags(); + + expect(flags.trim()).toEqual("--database-mode=firestore-native"); + }); + + it("should add many flags", async () => { + const flagsManager = new EmulatorFlagsManager() + .withFlag("database-mode", "firestore-native") + .withFlag("--host-port", "0.0.0.0:8080"); + + const flags = flagsManager.expandFlags(); + + expect(flags.trim()).toEqual("--database-mode=firestore-native --host-port=0.0.0.0:8080"); + }); + + it("should overwrite same flag if added more than once", async () => { + const flagsManager = new EmulatorFlagsManager() + .withFlag("database-mode", "firestore-native") + .withFlag("--database-mode", "datastore-mode"); + + const flags = flagsManager.expandFlags(); + + expect(flags.trim()).toEqual("--database-mode=datastore-mode"); + }); + + it("should add flag with no value", async () => { + const flagsManager = new EmulatorFlagsManager().withFlag("database-mode", "").withFlag("--host-port", ""); + + const flags = flagsManager.expandFlags(); + + expect(flags.trim()).toEqual("--database-mode --host-port"); + }); + + it("should throw if flag name not set", async () => { + expect(() => new EmulatorFlagsManager().withFlag("", "firestore-native")).toThrowError(); + }); + + it("should clear all flags added", async () => { + const flagsManager = new EmulatorFlagsManager() + .withFlag("database-mode", "firestore-native") + .withFlag("host-port", "0.0.0.0:8080"); + + flagsManager.clearFlags(); + const flags = flagsManager.expandFlags(); + + expect(flags.trim()).toEqual(""); + }); +}); diff --git a/packages/modules/gcloud/src/emulator-flags-manager.ts b/packages/modules/gcloud/src/emulator-flags-manager.ts new file mode 100644 index 000000000..a8209abef --- /dev/null +++ b/packages/modules/gcloud/src/emulator-flags-manager.ts @@ -0,0 +1,36 @@ +export class EmulatorFlagsManager { + private flags: { [name: string]: string } = {}; + + /** + * Adds flag as argument to emulator start command. + * Adding same flag name twice replaces existing flag value. + * @param name flag name. Must be set to non-empty string. May optionally contain -- prefix. + * @param value flag value. May be empty string. + * @returns this instance for chaining. + */ + public withFlag(name: string, value: string): this { + if (!name) throw new Error("Flag name must be set."); + if (name.startsWith("--")) this.flags[name] = value; + else this.flags[`--${name}`] = value; + return this; + } + + private flagToString(name: string, value: string): string { + return `${name}${value ? "=" + value : ""}`; + } + + /** + * + * @returns string with all flag names and values, concatenated in same order they were added. + */ + public expandFlags(): string { + return `${Object.keys(this.flags).reduce((p, c) => p + " " + this.flagToString(c, this.flags[c]), "")}`; + } + + /** + * Clears all added flags. + */ + public clearFlags() { + this.flags = {}; + } +} diff --git a/packages/modules/gcloud/src/firestore-emulator-container.ts b/packages/modules/gcloud/src/firestore-emulator-container.ts index 261dbc234..98a91e89a 100644 --- a/packages/modules/gcloud/src/firestore-emulator-container.ts +++ b/packages/modules/gcloud/src/firestore-emulator-container.ts @@ -1,16 +1,12 @@ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, StartedTestContainer } from "testcontainers"; +import { AbstractGcloudEmulator } from "./abstract-gcloud-emulator"; const EMULATOR_PORT = 8080; -const CMD = `gcloud beta emulators firestore start --host-port 0.0.0.0:${EMULATOR_PORT}`; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk"; -export class FirestoreEmulatorContainer extends GenericContainer { +export class FirestoreEmulatorContainer extends AbstractGcloudEmulator { constructor(image = DEFAULT_IMAGE) { - super(image); - this.withExposedPorts(EMULATOR_PORT) - .withCommand(["/bin/sh", "-c", CMD]) - .withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1)) - .withStartupTimeout(120_000); + super(image, EMULATOR_PORT, "gcloud beta emulators firestore start"); } public override async start(): Promise { diff --git a/packages/modules/gcloud/src/pubsub-emulator-container.ts b/packages/modules/gcloud/src/pubsub-emulator-container.ts index 9535dd84e..66196b24a 100644 --- a/packages/modules/gcloud/src/pubsub-emulator-container.ts +++ b/packages/modules/gcloud/src/pubsub-emulator-container.ts @@ -1,31 +1,25 @@ -import type { StartedTestContainer } from "testcontainers"; -import { AbstractStartedContainer, GenericContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, StartedTestContainer, Wait } from "testcontainers"; +import { AbstractGcloudEmulator } from "./abstract-gcloud-emulator"; const EMULATOR_PORT = 8085; -const CMD = "gcloud beta emulators pubsub start --host-port 0.0.0.0:8085"; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/google-cloud-cli"; -export class PubSubEmulatorContainer extends GenericContainer { - private _projectId?: string; +export class PubSubEmulatorContainer extends AbstractGcloudEmulator { + private projectId?: string; constructor(image = DEFAULT_IMAGE) { - super(image); - - this.withExposedPorts(EMULATOR_PORT) - .withWaitStrategy(Wait.forLogMessage(/Server started/g, 1)) - .withStartupTimeout(120_000); + super(image, EMULATOR_PORT, "gcloud beta emulators pubsub start"); + this.withWaitStrategy(Wait.forLogMessage(/Server started/g)); } - public withProjectId(projectId: string): PubSubEmulatorContainer { - this._projectId = projectId; + public withProjectId(projectId: string): this { + this.projectId = projectId; return this; } public override async start(): Promise { - // Determine the valid command-line prompt when starting the Pub/Sub emulator - const selectedProjectId = this._projectId ?? "test-project"; - const commandLine = `${CMD} --project=${selectedProjectId}`; - this.withCommand(["/bin/sh", "-c", commandLine]); + const selectedProjectId = this.projectId ?? "test-project"; + this.withFlag("project", selectedProjectId); return new StartedPubSubEmulatorContainer(await super.start(), selectedProjectId); }