From 1dc91a0223d3200a33c0045efd1f8899517bf86f Mon Sep 17 00:00:00 2001 From: digital88 Date: Wed, 12 Mar 2025 23:52:45 +0300 Subject: [PATCH 1/5] feat(gloud) add ability to set cmd flags --- .../src/datastore-emulator-container.test.ts | 8 ++++ .../src/datastore-emulator-container.ts | 11 ++++-- .../src/firestore-emulator-container.test.ts | 11 +++++- .../src/firestore-emulator-container.ts | 10 +++-- .../src/generic-emulator-container.test.ts | 26 +++++++++++++ .../gcloud/src/generic-emulator-container.ts | 39 +++++++++++++++++++ .../src/pubsub-emulator-container.test.ts | 8 ++++ .../gcloud/src/pubsub-emulator-container.ts | 15 ++++--- 8 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 packages/modules/gcloud/src/generic-emulator-container.test.ts create mode 100644 packages/modules/gcloud/src/generic-emulator-container.ts diff --git a/packages/modules/gcloud/src/datastore-emulator-container.test.ts b/packages/modules/gcloud/src/datastore-emulator-container.test.ts index 36384c6e5..dea39a76a 100644 --- a/packages/modules/gcloud/src/datastore-emulator-container.test.ts +++ b/packages/modules/gcloud/src/datastore-emulator-container.test.ts @@ -25,6 +25,14 @@ describe("DatastoreEmulatorContainer", { timeout: 240_000 }, () => { // } + it("should have default host-port flag and database-mode flag", async (ctx) => { + const datastoreEmulatorContainer = new DatastoreEmulatorContainer(); + + const flags = datastoreEmulatorContainer.expandFlags(); + + expect(flags.trim()).toEqual("--host-port=0.0.0.0:8080 --database-mode=datastore-mode"); + }); + async function checkDatastore(datastoreEmulatorContainer: StartedDatastoreEmulatorContainer) { expect(datastoreEmulatorContainer).toBeDefined(); const testProjectId = "test-project"; diff --git a/packages/modules/gcloud/src/datastore-emulator-container.ts b/packages/modules/gcloud/src/datastore-emulator-container.ts index 8846c0ab2..847e69b37 100644 --- a/packages/modules/gcloud/src/datastore-emulator-container.ts +++ b/packages/modules/gcloud/src/datastore-emulator-container.ts @@ -1,14 +1,17 @@ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, StartedTestContainer, Wait } from "testcontainers"; +import { GenericGCloudEmulatorContainer } from "./generic-emulator-container"; const EMULATOR_PORT = 8080; -const CMD = `gcloud beta emulators firestore start --host-port 0.0.0.0:${EMULATOR_PORT} --database-mode=datastore-mode`; +const CMD = `gcloud beta emulators firestore start`; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk"; -export class DatastoreEmulatorContainer extends GenericContainer { +export class DatastoreEmulatorContainer extends GenericGCloudEmulatorContainer { constructor(image = DEFAULT_IMAGE) { super(image); this.withExposedPorts(EMULATOR_PORT) - .withCommand(["/bin/sh", "-c", CMD]) + .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) + .withFlag("database-mode", `datastore-mode`) + .withCommand(["/bin/sh", "-c", `${CMD} ${this.expandFlags()}`]) .withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1)) .withStartupTimeout(120_000); } diff --git a/packages/modules/gcloud/src/firestore-emulator-container.test.ts b/packages/modules/gcloud/src/firestore-emulator-container.test.ts index 2b1829ec8..7c98b0e69 100644 --- a/packages/modules/gcloud/src/firestore-emulator-container.test.ts +++ b/packages/modules/gcloud/src/firestore-emulator-container.test.ts @@ -2,7 +2,8 @@ import * as admin from "firebase-admin"; import { FirestoreEmulatorContainer, StartedFirestoreEmulatorContainer } from "./firestore-emulator-container"; describe("FirestoreEmulatorContainer", { timeout: 240_000 }, () => { - afterEach(async () => { + afterEach(async (ctx) => { + if (ctx.task.name === "should have default host-port flag") return; await admin.app().delete(); }); @@ -29,6 +30,14 @@ describe("FirestoreEmulatorContainer", { timeout: 240_000 }, () => { // } + it("should have default host-port flag", async (ctx) => { + const firestoreEmulatorContainer = new FirestoreEmulatorContainer(); + + const flags = firestoreEmulatorContainer.expandFlags(); + + expect(flags.trim()).toEqual("--host-port=0.0.0.0:8080"); + }); + async function checkFirestore(firestoreEmulatorContainer: StartedFirestoreEmulatorContainer) { expect(firestoreEmulatorContainer).toBeDefined(); const testProjectId = "test-project"; diff --git a/packages/modules/gcloud/src/firestore-emulator-container.ts b/packages/modules/gcloud/src/firestore-emulator-container.ts index 261dbc234..777ebbebc 100644 --- a/packages/modules/gcloud/src/firestore-emulator-container.ts +++ b/packages/modules/gcloud/src/firestore-emulator-container.ts @@ -1,14 +1,16 @@ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, StartedTestContainer, Wait } from "testcontainers"; +import { GenericGCloudEmulatorContainer } from "./generic-emulator-container"; const EMULATOR_PORT = 8080; -const CMD = `gcloud beta emulators firestore start --host-port 0.0.0.0:${EMULATOR_PORT}`; +const CMD = `gcloud beta emulators firestore start`; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk"; -export class FirestoreEmulatorContainer extends GenericContainer { +export class FirestoreEmulatorContainer extends GenericGCloudEmulatorContainer { constructor(image = DEFAULT_IMAGE) { super(image); this.withExposedPorts(EMULATOR_PORT) - .withCommand(["/bin/sh", "-c", CMD]) + .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) + .withCommand(["/bin/sh", "-c", `${CMD} ${this.expandFlags()}`]) .withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1)) .withStartupTimeout(120_000); } diff --git a/packages/modules/gcloud/src/generic-emulator-container.test.ts b/packages/modules/gcloud/src/generic-emulator-container.test.ts new file mode 100644 index 000000000..b8936bc4e --- /dev/null +++ b/packages/modules/gcloud/src/generic-emulator-container.test.ts @@ -0,0 +1,26 @@ + +import { GenericGCloudEmulatorContainer } from "./generic-emulator-container"; + +const IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk" + +describe("GenericGCloudEmulatorContainer", { timeout: 240_000 }, () => { + it("should have all flags added", async () => { + const genericGCloudEmulatorContainer = new GenericGCloudEmulatorContainer(IMAGE) + .withFlag("database-mode", "firestore-native"); + + const flags = genericGCloudEmulatorContainer.expandFlags(); + + expect(flags.trim()).toEqual("--database-mode=firestore-native"); + }); + + it("should overwrite flag if added more than once", async () => { + const genericGCloudEmulatorContainer = new GenericGCloudEmulatorContainer(IMAGE) + .withFlag("database-mode", "firestore-native") + .withFlag("database-mode", "datastore-mode") + .withFlag("database-mode", "datastore-mode"); + + const flags = genericGCloudEmulatorContainer.expandFlags(); + + expect(flags.trim()).toEqual("--database-mode=datastore-mode"); + }); +}); diff --git a/packages/modules/gcloud/src/generic-emulator-container.ts b/packages/modules/gcloud/src/generic-emulator-container.ts new file mode 100644 index 000000000..665342cc9 --- /dev/null +++ b/packages/modules/gcloud/src/generic-emulator-container.ts @@ -0,0 +1,39 @@ +import { GenericContainer } from "testcontainers"; + +export class GenericGCloudEmulatorContainer extends GenericContainer { + private flags: { name: string, value: string }[] = [] + + constructor(image: string) { + super(image); + } + + /** + * Adds flag name and value as argument to emulator start command. + * Adding same flag name twice replaces existing flag value. + * Provided flag name may optionally contain -- prefix. + */ + public withFlag(name: string, value: string): this { + if (!name) throw new Error("Flag name must be set.") + // replace flag if it already exists + const idx = this.flags.findIndex(f => f.name == this.trimFlagName(name)) + if (idx >= 0) + this.flags = [...this.flags.slice(0, idx), ...this.flags.slice(idx + 1)] + this.flags.push({ name, value }); + return this + } + + private trimFlagName(name: string): string { + return name?.startsWith('--') ? name.slice(2) : name + } + + private flagToString(f: { name: string, value: string }): string { + return `--${this.trimFlagName(f.name)}=${f.value}` + } + + /** + * @return all flags provided to run this emulator instance, concatenated to string. + */ + public expandFlags(): string { + return `${this.flags.reduce((p, c) => p + ' ' + this.flagToString(c), '')}` + } +} diff --git a/packages/modules/gcloud/src/pubsub-emulator-container.test.ts b/packages/modules/gcloud/src/pubsub-emulator-container.test.ts index a56c3f5f6..c407c0ad9 100644 --- a/packages/modules/gcloud/src/pubsub-emulator-container.test.ts +++ b/packages/modules/gcloud/src/pubsub-emulator-container.test.ts @@ -10,6 +10,14 @@ describe("PubSubEmulatorContainer", { timeout: 240_000 }, () => { await pubsubEmulatorContainer.stop(); }); + it("should have default host-port flag", async () => { + const pubsubEmulatorContainer = new PubSubEmulatorContainer(); + + const flags = pubsubEmulatorContainer.expandFlags(); + + expect(flags.trim()).toEqual("--host-port=0.0.0.0:8085"); + }); + async function checkPubSub(pubsubEmulatorContainer: StartedPubSubEmulatorContainer) { expect(pubsubEmulatorContainer).toBeDefined(); diff --git a/packages/modules/gcloud/src/pubsub-emulator-container.ts b/packages/modules/gcloud/src/pubsub-emulator-container.ts index 4f99165a8..9e78acef5 100644 --- a/packages/modules/gcloud/src/pubsub-emulator-container.ts +++ b/packages/modules/gcloud/src/pubsub-emulator-container.ts @@ -1,22 +1,24 @@ import type { StartedTestContainer } from "testcontainers"; -import { AbstractStartedContainer, GenericContainer, Wait } from "testcontainers"; +import { AbstractStartedContainer, Wait } from "testcontainers"; +import { GenericGCloudEmulatorContainer } from "./generic-emulator-container"; const EMULATOR_PORT = 8085; -const CMD = "gcloud beta emulators pubsub start --host-port 0.0.0.0:8085"; +const CMD = "gcloud beta emulators pubsub start"; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/google-cloud-cli"; -export class PubSubEmulatorContainer extends GenericContainer { +export class PubSubEmulatorContainer extends GenericGCloudEmulatorContainer { private _projectId?: string; constructor(image = DEFAULT_IMAGE) { super(image); this.withExposedPorts(EMULATOR_PORT) + .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) .withWaitStrategy(Wait.forLogMessage(/Server started/g, 1)) .withStartupTimeout(120_000); } - public withProjectId(projectId: string): PubSubEmulatorContainer { + public withProjectId(projectId: string): this { this._projectId = projectId; return this; } @@ -24,8 +26,9 @@ export class PubSubEmulatorContainer extends GenericContainer { 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]); + this + .withFlag("project", selectedProjectId) + .withCommand(["/bin/sh", "-c", `${CMD} ${this.expandFlags()}`]); return new StartedPubSubEmulatorContainer(await super.start(), selectedProjectId); } From b9340fa3c9c194f7ec117920eb8b9c8663fa5270 Mon Sep 17 00:00:00 2001 From: digital88 Date: Thu, 13 Mar 2025 22:10:59 +0300 Subject: [PATCH 2/5] refactored approach to flags --- .../src/datastore-emulator-container.test.ts | 26 +++++++- .../src/datastore-emulator-container.ts | 34 ++++++++-- .../gcloud/src/emulator-flags-manager.test.ts | 62 +++++++++++++++++++ .../gcloud/src/emulator-flags-manager.ts | 42 +++++++++++++ .../src/firestore-emulator-container.test.ts | 28 ++++++++- .../src/firestore-emulator-container.ts | 34 ++++++++-- .../src/generic-emulator-container.test.ts | 26 -------- .../gcloud/src/generic-emulator-container.ts | 39 ------------ .../src/pubsub-emulator-container.test.ts | 21 ++++++- .../gcloud/src/pubsub-emulator-container.ts | 39 +++++++++--- 10 files changed, 264 insertions(+), 87 deletions(-) create mode 100644 packages/modules/gcloud/src/emulator-flags-manager.test.ts create mode 100644 packages/modules/gcloud/src/emulator-flags-manager.ts delete mode 100644 packages/modules/gcloud/src/generic-emulator-container.test.ts delete mode 100644 packages/modules/gcloud/src/generic-emulator-container.ts diff --git a/packages/modules/gcloud/src/datastore-emulator-container.test.ts b/packages/modules/gcloud/src/datastore-emulator-container.test.ts index dea39a76a..edea6da33 100644 --- a/packages/modules/gcloud/src/datastore-emulator-container.test.ts +++ b/packages/modules/gcloud/src/datastore-emulator-container.test.ts @@ -1,4 +1,5 @@ import { Datastore } from "@google-cloud/datastore"; +import { Wait } from "testcontainers"; import { DatastoreEmulatorContainer, StartedDatastoreEmulatorContainer } from "./datastore-emulator-container"; describe("DatastoreEmulatorContainer", { timeout: 240_000 }, () => { @@ -25,14 +26,35 @@ describe("DatastoreEmulatorContainer", { timeout: 240_000 }, () => { // } - it("should have default host-port flag and database-mode flag", async (ctx) => { + it("should have default host-port flag and database-mode flag", async () => { const datastoreEmulatorContainer = new DatastoreEmulatorContainer(); - const flags = datastoreEmulatorContainer.expandFlags(); + const flags = datastoreEmulatorContainer["flagsManager"].expandFlags(); expect(flags.trim()).toEqual("--host-port=0.0.0.0:8080 --database-mode=datastore-mode"); }); + it("should be able to add flags after creating container", async () => { + const datastoreEmulatorContainer = new DatastoreEmulatorContainer(); + // clear all default flags + datastoreEmulatorContainer["flagsManager"].clearFlags(); + + // add some new flags + const flags = datastoreEmulatorContainer + .withFlag("host-port", "0.0.0.0:8080") + .withFlag("database-mode", "datastore-mode") + ["flagsManager"].expandFlags(); + + // check new added flags exists + expect(flags.trim()).toEqual("--host-port=0.0.0.0:8080 --database-mode=datastore-mode"); + + // check that container start command uses latest flags string + const startedContainer = await datastoreEmulatorContainer + .withWaitStrategy(Wait.forLogMessage(/.* start --host=0.0.0.0 --port=8080 --database-mode=datastore-mode/, 1)) + .start(); + await startedContainer.stop(); + }); + async function checkDatastore(datastoreEmulatorContainer: StartedDatastoreEmulatorContainer) { expect(datastoreEmulatorContainer).toBeDefined(); const testProjectId = "test-project"; diff --git a/packages/modules/gcloud/src/datastore-emulator-container.ts b/packages/modules/gcloud/src/datastore-emulator-container.ts index 847e69b37..25b801e36 100644 --- a/packages/modules/gcloud/src/datastore-emulator-container.ts +++ b/packages/modules/gcloud/src/datastore-emulator-container.ts @@ -1,21 +1,47 @@ -import { AbstractStartedContainer, StartedTestContainer, Wait } from "testcontainers"; -import { GenericGCloudEmulatorContainer } from "./generic-emulator-container"; +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { EmulatorFlagsManager } from "./emulator-flags-manager"; const EMULATOR_PORT = 8080; const CMD = `gcloud beta emulators firestore start`; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk"; -export class DatastoreEmulatorContainer extends GenericGCloudEmulatorContainer { +export class DatastoreEmulatorContainer extends GenericContainer { + private readonly _flagsManager: EmulatorFlagsManager; constructor(image = DEFAULT_IMAGE) { super(image); + this._flagsManager = new EmulatorFlagsManager(); this.withExposedPorts(EMULATOR_PORT) .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) .withFlag("database-mode", `datastore-mode`) - .withCommand(["/bin/sh", "-c", `${CMD} ${this.expandFlags()}`]) + // explicitly call withCommand() fn here + // it will be called implicitly inside prev withFlag() call + .withCommand(["/bin/sh", "-c", this.getCmd()]) .withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1)) .withStartupTimeout(120_000); } + private getCmd(): string { + return `${CMD} ${this.flagsManager.expandFlags()}`; + } + + private get flagsManager() { + return this._flagsManager; + } + + /** + * 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); + // we need to 'refresh' command as we add new flag + this.withCommand(["/bin/sh", "-c", this.getCmd()]); + return this; + } + public override async start(): Promise { return new StartedDatastoreEmulatorContainer(await super.start()); } 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..1c7019826 --- /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..8fb004dbd --- /dev/null +++ b/packages/modules/gcloud/src/emulator-flags-manager.ts @@ -0,0 +1,42 @@ +export class EmulatorFlagsManager { + private flags: { name: string; value: 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."); + // replace flag if it already exists + const idx = this.flags.findIndex((f) => f.name == this.trimFlagName(name)); + if (idx >= 0) this.flags = [...this.flags.slice(0, idx), ...this.flags.slice(idx + 1)]; + this.flags.push({ name, value }); + return this; + } + + private trimFlagName(name: string): string { + return name?.startsWith("--") ? name.slice(2) : name; + } + + private flagToString(f: { name: string; value: string }): string { + return `--${this.trimFlagName(f.name)}=${f.value}`; + } + + /** + * + * @returns string with all flag names and values, concatenated in same order they were added. + */ + public expandFlags(): string { + return `${this.flags.reduce((p, c) => p + " " + this.flagToString(c), "")}`; + } + + /** + * Clears all added flags. + */ + public clearFlags() { + this.flags = []; + } +} diff --git a/packages/modules/gcloud/src/firestore-emulator-container.test.ts b/packages/modules/gcloud/src/firestore-emulator-container.test.ts index 7c98b0e69..e8e6ab275 100644 --- a/packages/modules/gcloud/src/firestore-emulator-container.test.ts +++ b/packages/modules/gcloud/src/firestore-emulator-container.test.ts @@ -1,9 +1,10 @@ import * as admin from "firebase-admin"; +import { Wait } from "testcontainers"; import { FirestoreEmulatorContainer, StartedFirestoreEmulatorContainer } from "./firestore-emulator-container"; describe("FirestoreEmulatorContainer", { timeout: 240_000 }, () => { afterEach(async (ctx) => { - if (ctx.task.name === "should have default host-port flag") return; + if (!["should work using default version", "should work using version 468.0.0"].includes(ctx.task.name)) return; await admin.app().delete(); }); @@ -30,14 +31,35 @@ describe("FirestoreEmulatorContainer", { timeout: 240_000 }, () => { // } - it("should have default host-port flag", async (ctx) => { + it("should have default host-port flag", async () => { const firestoreEmulatorContainer = new FirestoreEmulatorContainer(); - const flags = firestoreEmulatorContainer.expandFlags(); + const flags = firestoreEmulatorContainer["flagsManager"].expandFlags(); expect(flags.trim()).toEqual("--host-port=0.0.0.0:8080"); }); + it("should be able to add flags after creating container", async () => { + const firestoreEmulatorContainer = new FirestoreEmulatorContainer(); + // clear all default flags + firestoreEmulatorContainer["flagsManager"].clearFlags(); + + // add some new flags + const flags = firestoreEmulatorContainer + .withFlag("host-port", "0.0.0.0:8080") + .withFlag("database-mode", "datastore-mode") + ["flagsManager"].expandFlags(); + + // check new added flags exists + expect(flags.trim()).toEqual("--host-port=0.0.0.0:8080 --database-mode=datastore-mode"); + + // check that container start command uses latest flags string + const startedContainer = await firestoreEmulatorContainer + .withWaitStrategy(Wait.forLogMessage(/.* start --host=0.0.0.0 --port=8080 --database-mode=datastore-mode/, 1)) + .start(); + await startedContainer.stop(); + }); + async function checkFirestore(firestoreEmulatorContainer: StartedFirestoreEmulatorContainer) { expect(firestoreEmulatorContainer).toBeDefined(); const testProjectId = "test-project"; diff --git a/packages/modules/gcloud/src/firestore-emulator-container.ts b/packages/modules/gcloud/src/firestore-emulator-container.ts index 777ebbebc..2eab7aec6 100644 --- a/packages/modules/gcloud/src/firestore-emulator-container.ts +++ b/packages/modules/gcloud/src/firestore-emulator-container.ts @@ -1,20 +1,46 @@ -import { AbstractStartedContainer, StartedTestContainer, Wait } from "testcontainers"; -import { GenericGCloudEmulatorContainer } from "./generic-emulator-container"; +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { EmulatorFlagsManager } from "./emulator-flags-manager"; const EMULATOR_PORT = 8080; const CMD = `gcloud beta emulators firestore start`; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk"; -export class FirestoreEmulatorContainer extends GenericGCloudEmulatorContainer { +export class FirestoreEmulatorContainer extends GenericContainer { + private readonly _flagsManager: EmulatorFlagsManager; constructor(image = DEFAULT_IMAGE) { super(image); + this._flagsManager = new EmulatorFlagsManager(); this.withExposedPorts(EMULATOR_PORT) .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) - .withCommand(["/bin/sh", "-c", `${CMD} ${this.expandFlags()}`]) + // explicitly call withCommand() fn here + // it will be called implicitly inside prev withFlag() call + .withCommand(["/bin/sh", "-c", this.getCmd()]) .withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1)) .withStartupTimeout(120_000); } + private getCmd(): string { + return `${CMD} ${this.flagsManager.expandFlags()}`; + } + + private get flagsManager() { + return this._flagsManager; + } + + /** + * 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); + // we need to 'refresh' command as we add new flag + this.withCommand(["/bin/sh", "-c", this.getCmd()]); + return this; + } + public override async start(): Promise { return new StartedFirestoreEmulatorContainer(await super.start()); } diff --git a/packages/modules/gcloud/src/generic-emulator-container.test.ts b/packages/modules/gcloud/src/generic-emulator-container.test.ts deleted file mode 100644 index b8936bc4e..000000000 --- a/packages/modules/gcloud/src/generic-emulator-container.test.ts +++ /dev/null @@ -1,26 +0,0 @@ - -import { GenericGCloudEmulatorContainer } from "./generic-emulator-container"; - -const IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk" - -describe("GenericGCloudEmulatorContainer", { timeout: 240_000 }, () => { - it("should have all flags added", async () => { - const genericGCloudEmulatorContainer = new GenericGCloudEmulatorContainer(IMAGE) - .withFlag("database-mode", "firestore-native"); - - const flags = genericGCloudEmulatorContainer.expandFlags(); - - expect(flags.trim()).toEqual("--database-mode=firestore-native"); - }); - - it("should overwrite flag if added more than once", async () => { - const genericGCloudEmulatorContainer = new GenericGCloudEmulatorContainer(IMAGE) - .withFlag("database-mode", "firestore-native") - .withFlag("database-mode", "datastore-mode") - .withFlag("database-mode", "datastore-mode"); - - const flags = genericGCloudEmulatorContainer.expandFlags(); - - expect(flags.trim()).toEqual("--database-mode=datastore-mode"); - }); -}); diff --git a/packages/modules/gcloud/src/generic-emulator-container.ts b/packages/modules/gcloud/src/generic-emulator-container.ts deleted file mode 100644 index 665342cc9..000000000 --- a/packages/modules/gcloud/src/generic-emulator-container.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { GenericContainer } from "testcontainers"; - -export class GenericGCloudEmulatorContainer extends GenericContainer { - private flags: { name: string, value: string }[] = [] - - constructor(image: string) { - super(image); - } - - /** - * Adds flag name and value as argument to emulator start command. - * Adding same flag name twice replaces existing flag value. - * Provided flag name may optionally contain -- prefix. - */ - public withFlag(name: string, value: string): this { - if (!name) throw new Error("Flag name must be set.") - // replace flag if it already exists - const idx = this.flags.findIndex(f => f.name == this.trimFlagName(name)) - if (idx >= 0) - this.flags = [...this.flags.slice(0, idx), ...this.flags.slice(idx + 1)] - this.flags.push({ name, value }); - return this - } - - private trimFlagName(name: string): string { - return name?.startsWith('--') ? name.slice(2) : name - } - - private flagToString(f: { name: string, value: string }): string { - return `--${this.trimFlagName(f.name)}=${f.value}` - } - - /** - * @return all flags provided to run this emulator instance, concatenated to string. - */ - public expandFlags(): string { - return `${this.flags.reduce((p, c) => p + ' ' + this.flagToString(c), '')}` - } -} diff --git a/packages/modules/gcloud/src/pubsub-emulator-container.test.ts b/packages/modules/gcloud/src/pubsub-emulator-container.test.ts index c407c0ad9..9a2a3e463 100644 --- a/packages/modules/gcloud/src/pubsub-emulator-container.test.ts +++ b/packages/modules/gcloud/src/pubsub-emulator-container.test.ts @@ -1,4 +1,5 @@ import { PubSub } from "@google-cloud/pubsub"; +import { Wait } from "testcontainers"; import { PubSubEmulatorContainer, StartedPubSubEmulatorContainer } from "./pubsub-emulator-container"; describe("PubSubEmulatorContainer", { timeout: 240_000 }, () => { @@ -13,11 +14,29 @@ describe("PubSubEmulatorContainer", { timeout: 240_000 }, () => { it("should have default host-port flag", async () => { const pubsubEmulatorContainer = new PubSubEmulatorContainer(); - const flags = pubsubEmulatorContainer.expandFlags(); + const flags = pubsubEmulatorContainer["flagsManager"].expandFlags(); expect(flags.trim()).toEqual("--host-port=0.0.0.0:8085"); }); + it("should be able to add flags after creating container", async () => { + const pubsubEmulatorContainer = new PubSubEmulatorContainer(); + // clear all default flags + pubsubEmulatorContainer["flagsManager"].clearFlags(); + + // add some new flags + const flags = pubsubEmulatorContainer.withFlag("host-port", "0.0.0.0:8081")["flagsManager"].expandFlags(); + + // check new added flags exists + expect(flags.trim()).toEqual("--host-port=0.0.0.0:8081"); + + // check that container start command uses latest flags string + const startedContainer = await pubsubEmulatorContainer + .withWaitStrategy(Wait.forLogMessage(/.* listening on 8081/, 1)) + .start(); + await startedContainer.stop(); + }); + async function checkPubSub(pubsubEmulatorContainer: StartedPubSubEmulatorContainer) { expect(pubsubEmulatorContainer).toBeDefined(); diff --git a/packages/modules/gcloud/src/pubsub-emulator-container.ts b/packages/modules/gcloud/src/pubsub-emulator-container.ts index 9e78acef5..b82ff8fb5 100644 --- a/packages/modules/gcloud/src/pubsub-emulator-container.ts +++ b/packages/modules/gcloud/src/pubsub-emulator-container.ts @@ -1,23 +1,45 @@ -import type { StartedTestContainer } from "testcontainers"; -import { AbstractStartedContainer, Wait } from "testcontainers"; -import { GenericGCloudEmulatorContainer } from "./generic-emulator-container"; +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; +import { EmulatorFlagsManager } from "./emulator-flags-manager"; const EMULATOR_PORT = 8085; const CMD = "gcloud beta emulators pubsub start"; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/google-cloud-cli"; -export class PubSubEmulatorContainer extends GenericGCloudEmulatorContainer { +export class PubSubEmulatorContainer extends GenericContainer { + private readonly _flagsManager: EmulatorFlagsManager; private _projectId?: string; constructor(image = DEFAULT_IMAGE) { super(image); - + this._flagsManager = new EmulatorFlagsManager(); this.withExposedPorts(EMULATOR_PORT) .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) .withWaitStrategy(Wait.forLogMessage(/Server started/g, 1)) .withStartupTimeout(120_000); } + private getCmd(): string { + return `${CMD} ${this.flagsManager.expandFlags()}`; + } + + private get flagsManager() { + return this._flagsManager; + } + + /** + * 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); + // we need to 'refresh' command as we add new flag + this.withCommand(["/bin/sh", "-c", this.getCmd()]); + return this; + } + public withProjectId(projectId: string): this { this._projectId = projectId; return this; @@ -26,9 +48,10 @@ export class PubSubEmulatorContainer extends GenericGCloudEmulatorContainer { public override async start(): Promise { // Determine the valid command-line prompt when starting the Pub/Sub emulator const selectedProjectId = this._projectId ?? "test-project"; - this - .withFlag("project", selectedProjectId) - .withCommand(["/bin/sh", "-c", `${CMD} ${this.expandFlags()}`]); + this.withFlag("project", selectedProjectId) + // explicitly call withCommand() fn here + // it will be called implicitly inside prev withFlag() call + .withCommand(["/bin/sh", "-c", this.getCmd()]); return new StartedPubSubEmulatorContainer(await super.start(), selectedProjectId); } From 94cefb38260b4ab5968db3b61b44d26bbb3800ca Mon Sep 17 00:00:00 2001 From: digital88 Date: Tue, 18 Mar 2025 00:56:38 +0300 Subject: [PATCH 3/5] fixed after review --- .../src/datastore-emulator-container.test.ts | 30 ----------------- .../src/datastore-emulator-container.ts | 9 ++--- .../gcloud/src/emulator-flags-manager.test.ts | 2 +- .../gcloud/src/emulator-flags-manager.ts | 20 ++++------- .../src/firestore-emulator-container.test.ts | 33 +------------------ .../src/firestore-emulator-container.ts | 9 ++--- .../src/pubsub-emulator-container.test.ts | 27 --------------- .../gcloud/src/pubsub-emulator-container.ts | 5 +-- 8 files changed, 16 insertions(+), 119 deletions(-) diff --git a/packages/modules/gcloud/src/datastore-emulator-container.test.ts b/packages/modules/gcloud/src/datastore-emulator-container.test.ts index edea6da33..36384c6e5 100644 --- a/packages/modules/gcloud/src/datastore-emulator-container.test.ts +++ b/packages/modules/gcloud/src/datastore-emulator-container.test.ts @@ -1,5 +1,4 @@ import { Datastore } from "@google-cloud/datastore"; -import { Wait } from "testcontainers"; import { DatastoreEmulatorContainer, StartedDatastoreEmulatorContainer } from "./datastore-emulator-container"; describe("DatastoreEmulatorContainer", { timeout: 240_000 }, () => { @@ -26,35 +25,6 @@ describe("DatastoreEmulatorContainer", { timeout: 240_000 }, () => { // } - it("should have default host-port flag and database-mode flag", async () => { - const datastoreEmulatorContainer = new DatastoreEmulatorContainer(); - - const flags = datastoreEmulatorContainer["flagsManager"].expandFlags(); - - expect(flags.trim()).toEqual("--host-port=0.0.0.0:8080 --database-mode=datastore-mode"); - }); - - it("should be able to add flags after creating container", async () => { - const datastoreEmulatorContainer = new DatastoreEmulatorContainer(); - // clear all default flags - datastoreEmulatorContainer["flagsManager"].clearFlags(); - - // add some new flags - const flags = datastoreEmulatorContainer - .withFlag("host-port", "0.0.0.0:8080") - .withFlag("database-mode", "datastore-mode") - ["flagsManager"].expandFlags(); - - // check new added flags exists - expect(flags.trim()).toEqual("--host-port=0.0.0.0:8080 --database-mode=datastore-mode"); - - // check that container start command uses latest flags string - const startedContainer = await datastoreEmulatorContainer - .withWaitStrategy(Wait.forLogMessage(/.* start --host=0.0.0.0 --port=8080 --database-mode=datastore-mode/, 1)) - .start(); - await startedContainer.stop(); - }); - async function checkDatastore(datastoreEmulatorContainer: StartedDatastoreEmulatorContainer) { expect(datastoreEmulatorContainer).toBeDefined(); const testProjectId = "test-project"; diff --git a/packages/modules/gcloud/src/datastore-emulator-container.ts b/packages/modules/gcloud/src/datastore-emulator-container.ts index 25b801e36..151091908 100644 --- a/packages/modules/gcloud/src/datastore-emulator-container.ts +++ b/packages/modules/gcloud/src/datastore-emulator-container.ts @@ -13,10 +13,7 @@ export class DatastoreEmulatorContainer extends GenericContainer { this.withExposedPorts(EMULATOR_PORT) .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) .withFlag("database-mode", `datastore-mode`) - // explicitly call withCommand() fn here - // it will be called implicitly inside prev withFlag() call - .withCommand(["/bin/sh", "-c", this.getCmd()]) - .withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1)) + .withWaitStrategy(Wait.forLogMessage(/.*running.*/, 1)) .withStartupTimeout(120_000); } @@ -37,12 +34,12 @@ export class DatastoreEmulatorContainer extends GenericContainer { */ public withFlag(name: string, value: string) { this.flagsManager.withFlag(name, value); - // we need to 'refresh' command as we add new flag - this.withCommand(["/bin/sh", "-c", this.getCmd()]); return this; } public override async start(): Promise { + // expand all flags and get final command + this.withCommand(["/bin/sh", "-c", this.getCmd()]); return new StartedDatastoreEmulatorContainer(await super.start()); } } diff --git a/packages/modules/gcloud/src/emulator-flags-manager.test.ts b/packages/modules/gcloud/src/emulator-flags-manager.test.ts index 1c7019826..15df56b5a 100644 --- a/packages/modules/gcloud/src/emulator-flags-manager.test.ts +++ b/packages/modules/gcloud/src/emulator-flags-manager.test.ts @@ -42,7 +42,7 @@ describe("EmulatorFlagsManager", () => { const flags = flagsManager.expandFlags(); - expect(flags.trim()).toEqual("--database-mode= --host-port="); + expect(flags.trim()).toEqual("--database-mode --host-port"); }); it("should throw if flag name not set", async () => { diff --git a/packages/modules/gcloud/src/emulator-flags-manager.ts b/packages/modules/gcloud/src/emulator-flags-manager.ts index 8fb004dbd..a8209abef 100644 --- a/packages/modules/gcloud/src/emulator-flags-manager.ts +++ b/packages/modules/gcloud/src/emulator-flags-manager.ts @@ -1,5 +1,5 @@ export class EmulatorFlagsManager { - private flags: { name: string; value: string }[] = []; + private flags: { [name: string]: string } = {}; /** * Adds flag as argument to emulator start command. @@ -10,19 +10,13 @@ export class EmulatorFlagsManager { */ public withFlag(name: string, value: string): this { if (!name) throw new Error("Flag name must be set."); - // replace flag if it already exists - const idx = this.flags.findIndex((f) => f.name == this.trimFlagName(name)); - if (idx >= 0) this.flags = [...this.flags.slice(0, idx), ...this.flags.slice(idx + 1)]; - this.flags.push({ name, value }); + if (name.startsWith("--")) this.flags[name] = value; + else this.flags[`--${name}`] = value; return this; } - private trimFlagName(name: string): string { - return name?.startsWith("--") ? name.slice(2) : name; - } - - private flagToString(f: { name: string; value: string }): string { - return `--${this.trimFlagName(f.name)}=${f.value}`; + private flagToString(name: string, value: string): string { + return `${name}${value ? "=" + value : ""}`; } /** @@ -30,13 +24,13 @@ export class EmulatorFlagsManager { * @returns string with all flag names and values, concatenated in same order they were added. */ public expandFlags(): string { - return `${this.flags.reduce((p, c) => p + " " + this.flagToString(c), "")}`; + return `${Object.keys(this.flags).reduce((p, c) => p + " " + this.flagToString(c, this.flags[c]), "")}`; } /** * Clears all added flags. */ public clearFlags() { - this.flags = []; + this.flags = {}; } } diff --git a/packages/modules/gcloud/src/firestore-emulator-container.test.ts b/packages/modules/gcloud/src/firestore-emulator-container.test.ts index e8e6ab275..2b1829ec8 100644 --- a/packages/modules/gcloud/src/firestore-emulator-container.test.ts +++ b/packages/modules/gcloud/src/firestore-emulator-container.test.ts @@ -1,10 +1,8 @@ import * as admin from "firebase-admin"; -import { Wait } from "testcontainers"; import { FirestoreEmulatorContainer, StartedFirestoreEmulatorContainer } from "./firestore-emulator-container"; describe("FirestoreEmulatorContainer", { timeout: 240_000 }, () => { - afterEach(async (ctx) => { - if (!["should work using default version", "should work using version 468.0.0"].includes(ctx.task.name)) return; + afterEach(async () => { await admin.app().delete(); }); @@ -31,35 +29,6 @@ describe("FirestoreEmulatorContainer", { timeout: 240_000 }, () => { // } - it("should have default host-port flag", async () => { - const firestoreEmulatorContainer = new FirestoreEmulatorContainer(); - - const flags = firestoreEmulatorContainer["flagsManager"].expandFlags(); - - expect(flags.trim()).toEqual("--host-port=0.0.0.0:8080"); - }); - - it("should be able to add flags after creating container", async () => { - const firestoreEmulatorContainer = new FirestoreEmulatorContainer(); - // clear all default flags - firestoreEmulatorContainer["flagsManager"].clearFlags(); - - // add some new flags - const flags = firestoreEmulatorContainer - .withFlag("host-port", "0.0.0.0:8080") - .withFlag("database-mode", "datastore-mode") - ["flagsManager"].expandFlags(); - - // check new added flags exists - expect(flags.trim()).toEqual("--host-port=0.0.0.0:8080 --database-mode=datastore-mode"); - - // check that container start command uses latest flags string - const startedContainer = await firestoreEmulatorContainer - .withWaitStrategy(Wait.forLogMessage(/.* start --host=0.0.0.0 --port=8080 --database-mode=datastore-mode/, 1)) - .start(); - await startedContainer.stop(); - }); - async function checkFirestore(firestoreEmulatorContainer: StartedFirestoreEmulatorContainer) { expect(firestoreEmulatorContainer).toBeDefined(); const testProjectId = "test-project"; diff --git a/packages/modules/gcloud/src/firestore-emulator-container.ts b/packages/modules/gcloud/src/firestore-emulator-container.ts index 2eab7aec6..3ed8e985d 100644 --- a/packages/modules/gcloud/src/firestore-emulator-container.ts +++ b/packages/modules/gcloud/src/firestore-emulator-container.ts @@ -12,10 +12,7 @@ export class FirestoreEmulatorContainer extends GenericContainer { this._flagsManager = new EmulatorFlagsManager(); this.withExposedPorts(EMULATOR_PORT) .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) - // explicitly call withCommand() fn here - // it will be called implicitly inside prev withFlag() call - .withCommand(["/bin/sh", "-c", this.getCmd()]) - .withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1)) + .withWaitStrategy(Wait.forLogMessage(/.*running.*/, 1)) .withStartupTimeout(120_000); } @@ -36,12 +33,12 @@ export class FirestoreEmulatorContainer extends GenericContainer { */ public withFlag(name: string, value: string) { this.flagsManager.withFlag(name, value); - // we need to 'refresh' command as we add new flag - this.withCommand(["/bin/sh", "-c", this.getCmd()]); return this; } public override async start(): Promise { + // expand all flags and get final command + this.withCommand(["/bin/sh", "-c", this.getCmd()]); return new StartedFirestoreEmulatorContainer(await super.start()); } } diff --git a/packages/modules/gcloud/src/pubsub-emulator-container.test.ts b/packages/modules/gcloud/src/pubsub-emulator-container.test.ts index 9a2a3e463..a56c3f5f6 100644 --- a/packages/modules/gcloud/src/pubsub-emulator-container.test.ts +++ b/packages/modules/gcloud/src/pubsub-emulator-container.test.ts @@ -1,5 +1,4 @@ import { PubSub } from "@google-cloud/pubsub"; -import { Wait } from "testcontainers"; import { PubSubEmulatorContainer, StartedPubSubEmulatorContainer } from "./pubsub-emulator-container"; describe("PubSubEmulatorContainer", { timeout: 240_000 }, () => { @@ -11,32 +10,6 @@ describe("PubSubEmulatorContainer", { timeout: 240_000 }, () => { await pubsubEmulatorContainer.stop(); }); - it("should have default host-port flag", async () => { - const pubsubEmulatorContainer = new PubSubEmulatorContainer(); - - const flags = pubsubEmulatorContainer["flagsManager"].expandFlags(); - - expect(flags.trim()).toEqual("--host-port=0.0.0.0:8085"); - }); - - it("should be able to add flags after creating container", async () => { - const pubsubEmulatorContainer = new PubSubEmulatorContainer(); - // clear all default flags - pubsubEmulatorContainer["flagsManager"].clearFlags(); - - // add some new flags - const flags = pubsubEmulatorContainer.withFlag("host-port", "0.0.0.0:8081")["flagsManager"].expandFlags(); - - // check new added flags exists - expect(flags.trim()).toEqual("--host-port=0.0.0.0:8081"); - - // check that container start command uses latest flags string - const startedContainer = await pubsubEmulatorContainer - .withWaitStrategy(Wait.forLogMessage(/.* listening on 8081/, 1)) - .start(); - await startedContainer.stop(); - }); - async function checkPubSub(pubsubEmulatorContainer: StartedPubSubEmulatorContainer) { expect(pubsubEmulatorContainer).toBeDefined(); diff --git a/packages/modules/gcloud/src/pubsub-emulator-container.ts b/packages/modules/gcloud/src/pubsub-emulator-container.ts index b82ff8fb5..86e155822 100644 --- a/packages/modules/gcloud/src/pubsub-emulator-container.ts +++ b/packages/modules/gcloud/src/pubsub-emulator-container.ts @@ -35,8 +35,6 @@ export class PubSubEmulatorContainer extends GenericContainer { */ public withFlag(name: string, value: string) { this.flagsManager.withFlag(name, value); - // we need to 'refresh' command as we add new flag - this.withCommand(["/bin/sh", "-c", this.getCmd()]); return this; } @@ -49,8 +47,7 @@ export class PubSubEmulatorContainer extends GenericContainer { // Determine the valid command-line prompt when starting the Pub/Sub emulator const selectedProjectId = this._projectId ?? "test-project"; this.withFlag("project", selectedProjectId) - // explicitly call withCommand() fn here - // it will be called implicitly inside prev withFlag() call + // expand all flags and get final command .withCommand(["/bin/sh", "-c", this.getCmd()]); return new StartedPubSubEmulatorContainer(await super.start(), selectedProjectId); From d71d67b8b06069d043257c12abb3cf2239392c60 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 18 Mar 2025 09:33:00 +0000 Subject: [PATCH 4/5] Refactor flag management to base class --- .../gcloud/src/abstract-gcloud-emulator.ts | 35 ++++++++++++++ .../src/datastore-emulator-container.ts | 39 ++-------------- .../src/firestore-emulator-container.ts | 37 ++------------- .../gcloud/src/pubsub-emulator-container.ts | 46 ++++--------------- 4 files changed, 54 insertions(+), 103 deletions(-) create mode 100644 packages/modules/gcloud/src/abstract-gcloud-emulator.ts 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..80e8e6a4b --- /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 151091908..a8f5f4b1d 100644 --- a/packages/modules/gcloud/src/datastore-emulator-container.ts +++ b/packages/modules/gcloud/src/datastore-emulator-container.ts @@ -1,45 +1,16 @@ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; -import { EmulatorFlagsManager } from "./emulator-flags-manager"; +import { AbstractStartedContainer, StartedTestContainer } from "testcontainers"; +import { AbstractGcloudEmulator } from "./abstract-gcloud-emulator"; const EMULATOR_PORT = 8080; -const CMD = `gcloud beta emulators firestore start`; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk"; -export class DatastoreEmulatorContainer extends GenericContainer { - private readonly _flagsManager: EmulatorFlagsManager; +export class DatastoreEmulatorContainer extends AbstractGcloudEmulator { constructor(image = DEFAULT_IMAGE) { - super(image); - this._flagsManager = new EmulatorFlagsManager(); - this.withExposedPorts(EMULATOR_PORT) - .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) - .withFlag("database-mode", `datastore-mode`) - .withWaitStrategy(Wait.forLogMessage(/.*running.*/, 1)) - .withStartupTimeout(120_000); - } - - private getCmd(): string { - return `${CMD} ${this.flagsManager.expandFlags()}`; - } - - private get flagsManager() { - return this._flagsManager; - } - - /** - * 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; + super(image, EMULATOR_PORT, "gcloud beta emulators firestore start"); + this.withFlag("database-mode", `datastore-mode`); } public override async start(): Promise { - // expand all flags and get final command - this.withCommand(["/bin/sh", "-c", this.getCmd()]); return new StartedDatastoreEmulatorContainer(await super.start()); } } diff --git a/packages/modules/gcloud/src/firestore-emulator-container.ts b/packages/modules/gcloud/src/firestore-emulator-container.ts index 3ed8e985d..98a91e89a 100644 --- a/packages/modules/gcloud/src/firestore-emulator-container.ts +++ b/packages/modules/gcloud/src/firestore-emulator-container.ts @@ -1,44 +1,15 @@ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; -import { EmulatorFlagsManager } from "./emulator-flags-manager"; +import { AbstractStartedContainer, StartedTestContainer } from "testcontainers"; +import { AbstractGcloudEmulator } from "./abstract-gcloud-emulator"; const EMULATOR_PORT = 8080; -const CMD = `gcloud beta emulators firestore start`; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk"; -export class FirestoreEmulatorContainer extends GenericContainer { - private readonly _flagsManager: EmulatorFlagsManager; +export class FirestoreEmulatorContainer extends AbstractGcloudEmulator { constructor(image = DEFAULT_IMAGE) { - super(image); - this._flagsManager = new EmulatorFlagsManager(); - this.withExposedPorts(EMULATOR_PORT) - .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) - .withWaitStrategy(Wait.forLogMessage(/.*running.*/, 1)) - .withStartupTimeout(120_000); - } - - private getCmd(): string { - return `${CMD} ${this.flagsManager.expandFlags()}`; - } - - private get flagsManager() { - return this._flagsManager; - } - - /** - * 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; + super(image, EMULATOR_PORT, "gcloud beta emulators firestore start"); } public override async start(): Promise { - // expand all flags and get final command - this.withCommand(["/bin/sh", "-c", this.getCmd()]); return new StartedFirestoreEmulatorContainer(await super.start()); } } diff --git a/packages/modules/gcloud/src/pubsub-emulator-container.ts b/packages/modules/gcloud/src/pubsub-emulator-container.ts index 86e155822..3bb2068bb 100644 --- a/packages/modules/gcloud/src/pubsub-emulator-container.ts +++ b/packages/modules/gcloud/src/pubsub-emulator-container.ts @@ -1,41 +1,15 @@ -import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; -import { EmulatorFlagsManager } from "./emulator-flags-manager"; +import { AbstractStartedContainer, StartedTestContainer, Wait } from "testcontainers"; +import { AbstractGcloudEmulator } from "./abstract-gcloud-emulator"; const EMULATOR_PORT = 8085; -const CMD = "gcloud beta emulators pubsub start"; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/google-cloud-cli"; -export class PubSubEmulatorContainer extends GenericContainer { - private readonly _flagsManager: EmulatorFlagsManager; +export class PubSubEmulatorContainer extends AbstractGcloudEmulator { private _projectId?: string; constructor(image = DEFAULT_IMAGE) { - super(image); - this._flagsManager = new EmulatorFlagsManager(); - this.withExposedPorts(EMULATOR_PORT) - .withFlag("host-port", `0.0.0.0:${EMULATOR_PORT}`) - .withWaitStrategy(Wait.forLogMessage(/Server started/g, 1)) - .withStartupTimeout(120_000); - } - - private getCmd(): string { - return `${CMD} ${this.flagsManager.expandFlags()}`; - } - - private get flagsManager() { - return this._flagsManager; - } - - /** - * 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; + super(image, EMULATOR_PORT, "gcloud beta emulators pubsub start"); + this.withWaitStrategy(Wait.forLogMessage(/Server started/g)); } public withProjectId(projectId: string): this { @@ -44,18 +18,18 @@ export class PubSubEmulatorContainer extends GenericContainer { } public override async start(): Promise { - // Determine the valid command-line prompt when starting the Pub/Sub emulator const selectedProjectId = this._projectId ?? "test-project"; - this.withFlag("project", selectedProjectId) - // expand all flags and get final command - .withCommand(["/bin/sh", "-c", this.getCmd()]); + this.withFlag("project", selectedProjectId); return new StartedPubSubEmulatorContainer(await super.start(), selectedProjectId); } } export class StartedPubSubEmulatorContainer extends AbstractStartedContainer { - constructor(startedTestContainer: StartedTestContainer, private readonly projectId: string) { + constructor( + startedTestContainer: StartedTestContainer, + private readonly projectId: string, + ) { super(startedTestContainer); } From 7b7010467175f211515d974745ab3bd81a8984c1 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 18 Mar 2025 09:42:47 +0000 Subject: [PATCH 5/5] Naming conventions --- packages/modules/gcloud/src/pubsub-emulator-container.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/modules/gcloud/src/pubsub-emulator-container.ts b/packages/modules/gcloud/src/pubsub-emulator-container.ts index 440cfea69..66196b24a 100644 --- a/packages/modules/gcloud/src/pubsub-emulator-container.ts +++ b/packages/modules/gcloud/src/pubsub-emulator-container.ts @@ -5,7 +5,7 @@ const EMULATOR_PORT = 8085; const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/google-cloud-cli"; export class PubSubEmulatorContainer extends AbstractGcloudEmulator { - private _projectId?: string; + private projectId?: string; constructor(image = DEFAULT_IMAGE) { super(image, EMULATOR_PORT, "gcloud beta emulators pubsub start"); @@ -13,12 +13,12 @@ export class PubSubEmulatorContainer extends AbstractGcloudEmulator { } public withProjectId(projectId: string): this { - this._projectId = projectId; + this.projectId = projectId; return this; } public override async start(): Promise { - const selectedProjectId = this._projectId ?? "test-project"; + const selectedProjectId = this.projectId ?? "test-project"; this.withFlag("project", selectedProjectId); return new StartedPubSubEmulatorContainer(await super.start(), selectedProjectId);