Skip to content

Commit 4b7b9d0

Browse files
authored
Add ability to set command flags to Gcloud emulators (#926)
1 parent 5810da8 commit 4b7b9d0

File tree

6 files changed

+152
-32
lines changed

6 files changed

+152
-32
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { GenericContainer, Wait } from "testcontainers";
2+
import { EmulatorFlagsManager } from "./emulator-flags-manager";
3+
4+
export class AbstractGcloudEmulator extends GenericContainer {
5+
private readonly flagsManager: EmulatorFlagsManager;
6+
7+
constructor(
8+
image: string,
9+
port: number,
10+
private readonly cmd: string
11+
) {
12+
super(image);
13+
this.flagsManager = new EmulatorFlagsManager();
14+
this.withExposedPorts(port)
15+
.withFlag("host-port", `0.0.0.0:${port}`)
16+
.withWaitStrategy(Wait.forLogMessage(/.*running.*/))
17+
.withStartupTimeout(120_000);
18+
}
19+
20+
/**
21+
* Adds flag as argument to emulator start command.
22+
* Adding same flag name twice replaces existing flag value.
23+
* @param name flag name. Must be set to non-empty string. May optionally contain -- prefix.
24+
* @param value flag value. May be empty string.
25+
* @returns this instance for chaining.
26+
*/
27+
public withFlag(name: string, value: string) {
28+
this.flagsManager.withFlag(name, value);
29+
return this;
30+
}
31+
32+
public override async beforeContainerCreated(): Promise<void> {
33+
this.withCommand(["/bin/sh", "-c", `${this.cmd} ${this.flagsManager.expandFlags()}`]);
34+
}
35+
}

packages/modules/gcloud/src/datastore-emulator-container.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
1+
import { AbstractStartedContainer, StartedTestContainer } from "testcontainers";
2+
import { AbstractGcloudEmulator } from "./abstract-gcloud-emulator";
23

34
const EMULATOR_PORT = 8080;
4-
const CMD = `gcloud beta emulators firestore start --host-port 0.0.0.0:${EMULATOR_PORT} --database-mode=datastore-mode`;
55
const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk";
66

7-
export class DatastoreEmulatorContainer extends GenericContainer {
7+
export class DatastoreEmulatorContainer extends AbstractGcloudEmulator {
88
constructor(image = DEFAULT_IMAGE) {
9-
super(image);
10-
this.withExposedPorts(EMULATOR_PORT)
11-
.withCommand(["/bin/sh", "-c", CMD])
12-
.withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1))
13-
.withStartupTimeout(120_000);
9+
super(image, EMULATOR_PORT, "gcloud beta emulators firestore start");
10+
this.withFlag("database-mode", `datastore-mode`);
1411
}
1512

1613
public override async start(): Promise<StartedDatastoreEmulatorContainer> {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { EmulatorFlagsManager } from "./emulator-flags-manager";
2+
3+
describe("EmulatorFlagsManager", () => {
4+
it("should add flag without --", async () => {
5+
const flagsManager = new EmulatorFlagsManager().withFlag("database-mode", "firestore-native");
6+
7+
const flags = flagsManager.expandFlags();
8+
9+
expect(flags.trim()).toEqual("--database-mode=firestore-native");
10+
});
11+
12+
it("should add flag with --", async () => {
13+
const flagsManager = new EmulatorFlagsManager().withFlag("--database-mode", "firestore-native");
14+
15+
const flags = flagsManager.expandFlags();
16+
17+
expect(flags.trim()).toEqual("--database-mode=firestore-native");
18+
});
19+
20+
it("should add many flags", async () => {
21+
const flagsManager = new EmulatorFlagsManager()
22+
.withFlag("database-mode", "firestore-native")
23+
.withFlag("--host-port", "0.0.0.0:8080");
24+
25+
const flags = flagsManager.expandFlags();
26+
27+
expect(flags.trim()).toEqual("--database-mode=firestore-native --host-port=0.0.0.0:8080");
28+
});
29+
30+
it("should overwrite same flag if added more than once", async () => {
31+
const flagsManager = new EmulatorFlagsManager()
32+
.withFlag("database-mode", "firestore-native")
33+
.withFlag("--database-mode", "datastore-mode");
34+
35+
const flags = flagsManager.expandFlags();
36+
37+
expect(flags.trim()).toEqual("--database-mode=datastore-mode");
38+
});
39+
40+
it("should add flag with no value", async () => {
41+
const flagsManager = new EmulatorFlagsManager().withFlag("database-mode", "").withFlag("--host-port", "");
42+
43+
const flags = flagsManager.expandFlags();
44+
45+
expect(flags.trim()).toEqual("--database-mode --host-port");
46+
});
47+
48+
it("should throw if flag name not set", async () => {
49+
expect(() => new EmulatorFlagsManager().withFlag("", "firestore-native")).toThrowError();
50+
});
51+
52+
it("should clear all flags added", async () => {
53+
const flagsManager = new EmulatorFlagsManager()
54+
.withFlag("database-mode", "firestore-native")
55+
.withFlag("host-port", "0.0.0.0:8080");
56+
57+
flagsManager.clearFlags();
58+
const flags = flagsManager.expandFlags();
59+
60+
expect(flags.trim()).toEqual("");
61+
});
62+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export class EmulatorFlagsManager {
2+
private flags: { [name: string]: string } = {};
3+
4+
/**
5+
* Adds flag as argument to emulator start command.
6+
* Adding same flag name twice replaces existing flag value.
7+
* @param name flag name. Must be set to non-empty string. May optionally contain -- prefix.
8+
* @param value flag value. May be empty string.
9+
* @returns this instance for chaining.
10+
*/
11+
public withFlag(name: string, value: string): this {
12+
if (!name) throw new Error("Flag name must be set.");
13+
if (name.startsWith("--")) this.flags[name] = value;
14+
else this.flags[`--${name}`] = value;
15+
return this;
16+
}
17+
18+
private flagToString(name: string, value: string): string {
19+
return `${name}${value ? "=" + value : ""}`;
20+
}
21+
22+
/**
23+
*
24+
* @returns string with all flag names and values, concatenated in same order they were added.
25+
*/
26+
public expandFlags(): string {
27+
return `${Object.keys(this.flags).reduce((p, c) => p + " " + this.flagToString(c, this.flags[c]), "")}`;
28+
}
29+
30+
/**
31+
* Clears all added flags.
32+
*/
33+
public clearFlags() {
34+
this.flags = {};
35+
}
36+
}

packages/modules/gcloud/src/firestore-emulator-container.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
1+
import { AbstractStartedContainer, StartedTestContainer } from "testcontainers";
2+
import { AbstractGcloudEmulator } from "./abstract-gcloud-emulator";
23

34
const EMULATOR_PORT = 8080;
4-
const CMD = `gcloud beta emulators firestore start --host-port 0.0.0.0:${EMULATOR_PORT}`;
55
const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/cloud-sdk";
66

7-
export class FirestoreEmulatorContainer extends GenericContainer {
7+
export class FirestoreEmulatorContainer extends AbstractGcloudEmulator {
88
constructor(image = DEFAULT_IMAGE) {
9-
super(image);
10-
this.withExposedPorts(EMULATOR_PORT)
11-
.withCommand(["/bin/sh", "-c", CMD])
12-
.withWaitStrategy(Wait.forLogMessage(RegExp(".*running.*"), 1))
13-
.withStartupTimeout(120_000);
9+
super(image, EMULATOR_PORT, "gcloud beta emulators firestore start");
1410
}
1511

1612
public override async start(): Promise<StartedFirestoreEmulatorContainer> {

packages/modules/gcloud/src/pubsub-emulator-container.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,25 @@
1-
import type { StartedTestContainer } from "testcontainers";
2-
import { AbstractStartedContainer, GenericContainer, Wait } from "testcontainers";
1+
import { AbstractStartedContainer, StartedTestContainer, Wait } from "testcontainers";
2+
import { AbstractGcloudEmulator } from "./abstract-gcloud-emulator";
33

44
const EMULATOR_PORT = 8085;
5-
const CMD = "gcloud beta emulators pubsub start --host-port 0.0.0.0:8085";
65
const DEFAULT_IMAGE = "gcr.io/google.com/cloudsdktool/google-cloud-cli";
76

8-
export class PubSubEmulatorContainer extends GenericContainer {
9-
private _projectId?: string;
7+
export class PubSubEmulatorContainer extends AbstractGcloudEmulator {
8+
private projectId?: string;
109

1110
constructor(image = DEFAULT_IMAGE) {
12-
super(image);
13-
14-
this.withExposedPorts(EMULATOR_PORT)
15-
.withWaitStrategy(Wait.forLogMessage(/Server started/g, 1))
16-
.withStartupTimeout(120_000);
11+
super(image, EMULATOR_PORT, "gcloud beta emulators pubsub start");
12+
this.withWaitStrategy(Wait.forLogMessage(/Server started/g));
1713
}
1814

19-
public withProjectId(projectId: string): PubSubEmulatorContainer {
20-
this._projectId = projectId;
15+
public withProjectId(projectId: string): this {
16+
this.projectId = projectId;
2117
return this;
2218
}
2319

2420
public override async start(): Promise<StartedPubSubEmulatorContainer> {
25-
// Determine the valid command-line prompt when starting the Pub/Sub emulator
26-
const selectedProjectId = this._projectId ?? "test-project";
27-
const commandLine = `${CMD} --project=${selectedProjectId}`;
28-
this.withCommand(["/bin/sh", "-c", commandLine]);
21+
const selectedProjectId = this.projectId ?? "test-project";
22+
this.withFlag("project", selectedProjectId);
2923

3024
return new StartedPubSubEmulatorContainer(await super.start(), selectedProjectId);
3125
}

0 commit comments

Comments
 (0)