@causa/workspace-google 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/assets/firebase-auth.json +12 -0
  2. package/dist/assets/firebase-storage.json +15 -0
  3. package/dist/cli/google-app-check.d.ts +5 -0
  4. package/dist/cli/google-app-check.js +9 -0
  5. package/dist/cli/google-identity-platform.d.ts +5 -0
  6. package/dist/cli/google-identity-platform.js +9 -0
  7. package/dist/cli/index.d.ts +2 -0
  8. package/dist/cli/index.js +2 -0
  9. package/dist/configurations/google.d.ts +15 -8
  10. package/dist/configurations/index.d.ts +1 -0
  11. package/dist/configurations/index.js +1 -1
  12. package/dist/configurations/utils.d.ts +10 -0
  13. package/dist/configurations/utils.js +17 -0
  14. package/dist/emulators/firebase-storage.d.ts +20 -0
  15. package/dist/emulators/firebase-storage.js +27 -0
  16. package/dist/emulators/firestore.d.ts +20 -0
  17. package/dist/emulators/firestore.js +27 -0
  18. package/dist/emulators/identity-platform.d.ts +16 -0
  19. package/dist/emulators/identity-platform.js +23 -0
  20. package/dist/emulators/index.d.ts +5 -0
  21. package/dist/emulators/index.js +5 -0
  22. package/dist/emulators/pubsub.d.ts +25 -0
  23. package/dist/emulators/pubsub.js +35 -0
  24. package/dist/emulators/spanner.d.ts +24 -0
  25. package/dist/emulators/spanner.js +31 -0
  26. package/dist/functions/emulator-start-firebase-storage.d.ts +17 -0
  27. package/dist/functions/emulator-start-firebase-storage.js +64 -0
  28. package/dist/functions/emulator-start-firestore.d.ts +17 -0
  29. package/dist/functions/emulator-start-firestore.js +58 -0
  30. package/dist/functions/emulator-start-identity-platform.d.ts +16 -0
  31. package/dist/functions/emulator-start-identity-platform.js +49 -0
  32. package/dist/functions/emulator-start-pubsub.d.ts +33 -0
  33. package/dist/functions/emulator-start-pubsub.js +86 -0
  34. package/dist/functions/emulator-start-spanner.d.ts +40 -0
  35. package/dist/functions/emulator-start-spanner.js +112 -0
  36. package/dist/functions/emulator-stop-firebase-storage.d.ts +9 -0
  37. package/dist/functions/emulator-stop-firebase-storage.js +17 -0
  38. package/dist/functions/emulator-stop-firestore.d.ts +9 -0
  39. package/dist/functions/emulator-stop-firestore.js +17 -0
  40. package/dist/functions/emulator-stop-identity-platform.d.ts +9 -0
  41. package/dist/functions/emulator-stop-identity-platform.js +17 -0
  42. package/dist/functions/emulator-stop-pubsub.d.ts +9 -0
  43. package/dist/functions/emulator-stop-pubsub.js +17 -0
  44. package/dist/functions/emulator-stop-spanner.d.ts +9 -0
  45. package/dist/functions/emulator-stop-spanner.js +17 -0
  46. package/dist/functions/google-app-check-generate-token.d.ts +13 -0
  47. package/dist/functions/google-app-check-generate-token.js +66 -0
  48. package/dist/functions/google-firebase-storage-merge-rules.js +1 -2
  49. package/dist/functions/google-firestore-merge-rules.js +1 -2
  50. package/dist/functions/google-identity-platform-generate-custom-token.d.ts +20 -0
  51. package/dist/functions/google-identity-platform-generate-custom-token.js +49 -0
  52. package/dist/functions/google-identity-platform-generate-token.d.ts +18 -0
  53. package/dist/functions/google-identity-platform-generate-token.js +73 -0
  54. package/dist/functions/google-services-enable.js +1 -2
  55. package/dist/functions/google-spanner-list-databases.d.ts +33 -0
  56. package/dist/functions/google-spanner-list-databases.js +66 -0
  57. package/dist/functions/index.js +18 -1
  58. package/dist/functions/project-get-artefact-destination-cloud-functions.d.ts +11 -0
  59. package/dist/functions/project-get-artefact-destination-cloud-functions.js +18 -0
  60. package/dist/functions/project-get-artefact-destination-cloud-run.d.ts +10 -0
  61. package/dist/functions/project-get-artefact-destination-cloud-run.js +17 -0
  62. package/dist/functions/project-push-artefact-cloud-functions.d.ts +11 -0
  63. package/dist/functions/project-push-artefact-cloud-functions.js +31 -0
  64. package/dist/services/firebase-app.d.ts +151 -0
  65. package/dist/services/firebase-app.errors.d.ts +26 -0
  66. package/dist/services/firebase-app.errors.js +35 -0
  67. package/dist/services/firebase-app.js +286 -0
  68. package/dist/services/firebase-emulator.d.ts +35 -0
  69. package/dist/services/firebase-emulator.js +65 -0
  70. package/dist/services/gcloud-emulator.d.ts +55 -0
  71. package/dist/services/gcloud-emulator.js +66 -0
  72. package/dist/services/google-apis.d.ts +34 -0
  73. package/dist/services/google-apis.js +48 -0
  74. package/dist/services/google-apis.types.d.ts +226 -0
  75. package/dist/services/google-apis.types.js +4 -0
  76. package/dist/services/index.d.ts +7 -0
  77. package/dist/services/index.js +7 -0
  78. package/dist/services/storage.d.ts +33 -0
  79. package/dist/services/storage.errors.d.ts +8 -0
  80. package/dist/services/storage.errors.js +11 -0
  81. package/dist/services/storage.js +36 -0
  82. package/package.json +22 -8
@@ -0,0 +1,286 @@
1
+ import { ApiKeysClient } from '@google-cloud/apikeys';
2
+ import { IAMCredentialsClient } from '@google-cloud/iam-credentials';
3
+ import { initializeApp as initializeAdminApp, } from 'firebase-admin/app';
4
+ import { initializeApp } from 'firebase/app';
5
+ import * as uuid from 'uuid';
6
+ import { FirebaseAdminServiceAccountNotFoundError, FirebaseApiKeyNotFoundError, NoFirebaseAppFoundError, } from './firebase-app.errors.js';
7
+ import { GoogleApisService } from './google-apis.js';
8
+ /**
9
+ * The types (platforms) of Firebase apps.
10
+ * Apps are split by type and cannot be listed globally.
11
+ */
12
+ const FIREBASE_APP_TYPES = ['androidApps', 'iosApps', 'webApps'];
13
+ /**
14
+ * A string expected to be found in the display name of API keys automatically created by Firebase.
15
+ */
16
+ const FIREBASE_AUTOMATIC_KEY_INFO = 'auto created by Firebase';
17
+ /**
18
+ * A service exposing generic Firebase functionalities based on the workspace configuration.
19
+ */
20
+ export class FirebaseAppService {
21
+ /**
22
+ * The GCP project ID read from the {@link WorkspaceContext} configuration.
23
+ */
24
+ projectId;
25
+ /**
26
+ * The Firebase auth domain, read from the configuration or inferred from the GCP project ID.
27
+ */
28
+ authDomain;
29
+ /**
30
+ * The logger to use.
31
+ */
32
+ logger;
33
+ /**
34
+ * The Firebase API key set in the configuration.
35
+ * This could be `undefined`, in which case {@link FirebaseAppService.findApiKey} will be called.
36
+ */
37
+ confApiKey;
38
+ /**
39
+ * The Firebase admin service account for the GCP project, read from the configuration.
40
+ * This could be `undefined`, in which case {@link FirebaseAppService.findAdminServiceAccount} will be called.
41
+ */
42
+ confAdminServiceAccount;
43
+ /**
44
+ * The Firebase app ID, read from the configuration.
45
+ * This could be `undefined`, in which case {@link FirebaseAppService.getAnyAppId} will be called.
46
+ */
47
+ confAppId;
48
+ /**
49
+ * The {@link GoogleApisService} used to make calls to IAM when listing service accounts.
50
+ */
51
+ googleApisService;
52
+ constructor(context) {
53
+ this.logger = context.logger;
54
+ this.googleApisService = context.service(GoogleApisService);
55
+ const googleConf = context.asConfiguration();
56
+ this.projectId = googleConf.getOrThrow('google.project');
57
+ this.confApiKey = googleConf.get('google.firebase.apiKey');
58
+ this.confAdminServiceAccount = googleConf.get('google.firebase.adminServiceAccount');
59
+ this.confAppId = googleConf.get('google.firebase.appId');
60
+ this.authDomain =
61
+ googleConf.get('google.firebase.authDomain') ??
62
+ `${this.projectId}.firebaseapp.com`;
63
+ }
64
+ /**
65
+ * The promise resolving to the Firebase API key, in the case it wasn't set in the configuration.
66
+ */
67
+ apiKeyPromise;
68
+ /**
69
+ * Finds a Firebase API key for the configured GCP project by listing them using the API keys API.
70
+ * The method used to find the correct API key is a hack, relying on the display name of the key.
71
+ * This is why it is preferable for the API key to be set manually in the configuration.
72
+ *
73
+ * @returns A Firebase API key for the configured GCP project.
74
+ */
75
+ async findApiKey() {
76
+ this.logger.debug(`🛂 'google.firebase.apiKey' is not set. Attempting to fetch the key from Google.`);
77
+ const keysClient = new ApiKeysClient();
78
+ const parent = `projects/${this.projectId}/locations/global`;
79
+ let name;
80
+ for await (const key of keysClient.listKeysAsync({ parent })) {
81
+ if (key.displayName?.includes(FIREBASE_AUTOMATIC_KEY_INFO) && key.name) {
82
+ name = key.name;
83
+ break;
84
+ }
85
+ }
86
+ if (!name) {
87
+ throw new FirebaseApiKeyNotFoundError(this.projectId);
88
+ }
89
+ const [{ keyString }] = await keysClient.getKeyString({ name });
90
+ if (!keyString) {
91
+ throw new Error('Unexpected empty key string returned by the API keys API.');
92
+ }
93
+ this.logger.debug(`🛂 Found Firebase API key '${name}': '${keyString}'.`);
94
+ return keyString;
95
+ }
96
+ /**
97
+ * Returns either the Firebase API key set in the configuration, or one found using the Google APIs.
98
+ *
99
+ * @returns The Firebase API key.
100
+ */
101
+ async getApiKey() {
102
+ if (this.confApiKey) {
103
+ return this.confApiKey;
104
+ }
105
+ if (!this.apiKeyPromise) {
106
+ this.apiKeyPromise = this.findApiKey();
107
+ }
108
+ return await this.apiKeyPromise;
109
+ }
110
+ /**
111
+ * The singleton {@link FirebaseApp} created by {@link FirebaseAppService.getApp}.
112
+ */
113
+ app;
114
+ /**
115
+ * Initializes and returns a {@link FirebaseApp} for the configured GCP project.
116
+ *
117
+ * @returns The {@link FirebaseApp}.
118
+ */
119
+ async getApp() {
120
+ if (this.app) {
121
+ return this.app;
122
+ }
123
+ const apiKey = await this.getApiKey();
124
+ // Due to the async call to get the API key, a parallel call to `getApp` could have created the app already.
125
+ if (this.app) {
126
+ return this.app;
127
+ }
128
+ const name = uuid.v4();
129
+ this.app = initializeApp({ projectId: this.projectId, apiKey, authDomain: this.authDomain }, name);
130
+ return this.app;
131
+ }
132
+ /**
133
+ * The promise resolving to the email of the Firebase-owned service account, in case it wasn't set in the
134
+ * configuration.
135
+ */
136
+ adminServiceAccountPromise;
137
+ /**
138
+ * Looks for the automatically-created Firebase admin service account by listing service accounts in the configured
139
+ * GCP project.
140
+ * This is a hack which uses the inferred format of the service account name used by Firebase. It is preferred to
141
+ * manually set the `google.firebase.adminServiceAccount` configuration.
142
+ *
143
+ * @returns The email of the Firebase admin service account.
144
+ */
145
+ async findAdminServiceAccount() {
146
+ this.logger.debug(`🛂 'google.firebase.adminServiceAccount' is not set. Listing service accounts in the project to find it.`);
147
+ const iamClient = await this.googleApisService.getClient('iam', 'v1', {});
148
+ const request = {
149
+ name: `projects/${this.projectId}`,
150
+ pageSize: 100,
151
+ pageToken: undefined,
152
+ };
153
+ do {
154
+ const { data } = await iamClient.projects.serviceAccounts.list(request);
155
+ const adminServiceAccount = data.accounts
156
+ ?.filter((a) => a.email != null)
157
+ .find(({ email }) => email.match(`^firebase-adminsdk-[\\w]+@${this.projectId}\\.iam\\.gserviceaccount\\.com$`));
158
+ if (adminServiceAccount) {
159
+ this.logger.debug(`🛂 Found Firebase admin service account as '${adminServiceAccount.email}'.`);
160
+ return adminServiceAccount.email;
161
+ }
162
+ if (data.nextPageToken) {
163
+ request.pageToken = data.nextPageToken;
164
+ }
165
+ } while (request.pageToken);
166
+ throw new FirebaseAdminServiceAccountNotFoundError(this.projectId);
167
+ }
168
+ /**
169
+ * Returns the email for the Firebase admin service account, either from the configuration or by listing service
170
+ * accounts using Google APIs.
171
+ *
172
+ * @returns The email for the Firebase admin service account.
173
+ */
174
+ async getAdminServiceAccount() {
175
+ if (this.confAdminServiceAccount) {
176
+ return this.confAdminServiceAccount;
177
+ }
178
+ if (!this.adminServiceAccountPromise) {
179
+ this.adminServiceAccountPromise = this.findAdminServiceAccount();
180
+ }
181
+ return await this.adminServiceAccountPromise;
182
+ }
183
+ /**
184
+ * The singleton {@link FirebaseAdminApp}, created by {@link FirebaseAppService.getAdminAppForAdminServiceAccount}.
185
+ */
186
+ adminAppForAdminServiceAccount;
187
+ /**
188
+ * Returns a Firebase admin app configured to authenticate as the Firebase admin service account.
189
+ * Using a service account rather than end user credentials ensures access to all functionalities (e.g. token
190
+ * signing), which are otherwise unavailable to end users.
191
+ *
192
+ * @returns The Firebase admin app.
193
+ */
194
+ async getAdminAppForAdminServiceAccount() {
195
+ if (this.adminAppForAdminServiceAccount) {
196
+ return this.adminAppForAdminServiceAccount;
197
+ }
198
+ const serviceAccountId = await this.getAdminServiceAccount();
199
+ const credential = await this.makeAdminCredential(serviceAccountId);
200
+ // Same reasoning as in `getApp()`.
201
+ if (this.adminAppForAdminServiceAccount) {
202
+ return this.adminAppForAdminServiceAccount;
203
+ }
204
+ const name = uuid.v4();
205
+ this.adminAppForAdminServiceAccount = initializeAdminApp({
206
+ projectId: this.projectId,
207
+ serviceAccountId,
208
+ credential,
209
+ }, name);
210
+ return this.adminAppForAdminServiceAccount;
211
+ }
212
+ /**
213
+ * Creates a {@link Credential} object that can be used when initializing a Firebase admin app.
214
+ * The returned object generates access tokens for the given service account. The application default credentials (or
215
+ * any other default authentication method) should provide authorization to sign tokens in the GCP project for this to
216
+ * work.
217
+ *
218
+ * @param serviceAccountId The ID / email of the service account to impersonate.
219
+ * @returns The {@link Credential} to use with the Firebase admin app.
220
+ */
221
+ async makeAdminCredential(serviceAccountId) {
222
+ const iamCredentialsClient = new IAMCredentialsClient();
223
+ return {
224
+ getAccessToken: async () => {
225
+ const [{ accessToken, expireTime }] = await iamCredentialsClient.generateAccessToken({
226
+ name: `projects/-/serviceAccounts/${serviceAccountId}`,
227
+ scope: ['https://www.googleapis.com/auth/cloud-platform'],
228
+ });
229
+ const expireTimestamp = parseInt(expireTime?.seconds ?? '0') * 1000;
230
+ const expires_in = Math.floor((expireTimestamp - Date.now()) / 1000);
231
+ return { access_token: accessToken ?? '', expires_in };
232
+ },
233
+ };
234
+ }
235
+ /**
236
+ * Looks for a Firebase app ID using the API and returns the first one found.
237
+ * This could be the ID of an Android, iOS, or web app.
238
+ * If no app can be found, an error is returned.
239
+ *
240
+ * @param options Options when listing the existing apps.
241
+ * @returns The first found Firebase app ID.
242
+ */
243
+ async getAnyAppId(options = {}) {
244
+ const appTypes = options.appTypes ?? FIREBASE_APP_TYPES;
245
+ const firebaseClient = await this.googleApisService.getClient('firebase', 'v1beta1', {});
246
+ const appIds = await Promise.all(appTypes.map((appType) => this.getFirstApp(firebaseClient, appType)));
247
+ const appId = appIds.find((id) => id) ?? null;
248
+ if (!appId) {
249
+ throw new NoFirebaseAppFoundError(this.projectId);
250
+ }
251
+ return appId;
252
+ }
253
+ /**
254
+ * Fetches the first Firebase app of a given type and returns its ID.
255
+ *
256
+ * @param client The Firebase API client to use.
257
+ * @param appType The type of Firebase app to list.
258
+ * @returns The ID of the first app, or `null` if none could be found.
259
+ */
260
+ async getFirstApp(client, appType) {
261
+ const { data: { apps }, } = await client.projects[appType].list({
262
+ parent: `projects/${this.projectId}`,
263
+ pageSize: 1,
264
+ });
265
+ return apps ? apps[0]?.appId ?? null : null;
266
+ }
267
+ /**
268
+ * The promise resolving to one of the Firebase App IDs available in the GCP project.
269
+ * Only set if {@link FirebaseAppService.confAppId} is not set.
270
+ */
271
+ appIdPromise;
272
+ /**
273
+ * Returns the Firebase App ID, either from the configuration or by listing apps using Google APIs.
274
+ *
275
+ * @returns The Firebase App ID.
276
+ */
277
+ async getAppId() {
278
+ if (this.confAppId) {
279
+ return this.confAppId;
280
+ }
281
+ if (!this.appIdPromise) {
282
+ this.appIdPromise = this.getAnyAppId();
283
+ }
284
+ return await this.appIdPromise;
285
+ }
286
+ }
@@ -0,0 +1,35 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { DockerContainerPublish, DockerEmulatorService } from '@causa/workspace-core';
3
+ /**
4
+ * Options when starting a Firebase emulator.
5
+ */
6
+ type FirebaseEmulatorStartOptions = Omit<NonNullable<Parameters<DockerEmulatorService['start']>[3]>, 'commandAndArgs'> & NonNullable<Parameters<DockerEmulatorService['waitForAvailability']>[2]>;
7
+ /**
8
+ * A service providing a way to start emulators exposed by the `firebase` CLI.
9
+ */
10
+ export declare class FirebaseEmulatorService {
11
+ /**
12
+ * The underlying {@link DockerEmulatorService} used to start the emulator.
13
+ */
14
+ private readonly dockerEmulatorService;
15
+ /**
16
+ * The local ("demo") GCP project used by the emulator.
17
+ */
18
+ readonly localGcpProject: string;
19
+ /**
20
+ * The version of the Firebase tools to use.
21
+ */
22
+ readonly firebaseToolsVersion: string;
23
+ constructor(context: WorkspaceContext);
24
+ /**
25
+ * Starts a Docker container running an emulator using the `firebase` CLI, and waits for it to be available.
26
+ * The version of the Firebase CLI can be specified using the `google.firebaseTools.version` configuration.
27
+ *
28
+ * @param containerName The name of the container to create.
29
+ * @param firebaseConfFile The path to a local file containing the Firebase configuration.
30
+ * @param publish A list of at least one port to expose from the container.
31
+ * @param options Options when starting the Docker container and waiting for it to be available.
32
+ */
33
+ start(containerName: string, firebaseConfFile: string, publish: [DockerContainerPublish, ...DockerContainerPublish[]], options?: FirebaseEmulatorStartOptions): Promise<void>;
34
+ }
35
+ export {};
@@ -0,0 +1,65 @@
1
+ import { DockerEmulatorService, } from '@causa/workspace-core';
2
+ import { getLocalGcpProject, } from '../configurations/index.js';
3
+ /**
4
+ * The Docker image providing the `firebase` CLI.
5
+ */
6
+ const FIREBASE_TOOLS_IMAGE = 'andreysenov/firebase-tools';
7
+ /**
8
+ * The location of the Firebase configuration file within the container.
9
+ */
10
+ const FIREBASE_CONTAINER_CONF_FILE = '/home/node/firebase.json';
11
+ /**
12
+ * A service providing a way to start emulators exposed by the `firebase` CLI.
13
+ */
14
+ export class FirebaseEmulatorService {
15
+ /**
16
+ * The underlying {@link DockerEmulatorService} used to start the emulator.
17
+ */
18
+ dockerEmulatorService;
19
+ /**
20
+ * The local ("demo") GCP project used by the emulator.
21
+ */
22
+ localGcpProject;
23
+ /**
24
+ * The version of the Firebase tools to use.
25
+ */
26
+ firebaseToolsVersion;
27
+ constructor(context) {
28
+ this.dockerEmulatorService = context.service(DockerEmulatorService);
29
+ this.localGcpProject = getLocalGcpProject(context);
30
+ this.firebaseToolsVersion =
31
+ context
32
+ .asConfiguration()
33
+ .get('google.firebase.tools.version') ?? 'latest';
34
+ }
35
+ /**
36
+ * Starts a Docker container running an emulator using the `firebase` CLI, and waits for it to be available.
37
+ * The version of the Firebase CLI can be specified using the `google.firebaseTools.version` configuration.
38
+ *
39
+ * @param containerName The name of the container to create.
40
+ * @param firebaseConfFile The path to a local file containing the Firebase configuration.
41
+ * @param publish A list of at least one port to expose from the container.
42
+ * @param options Options when starting the Docker container and waiting for it to be available.
43
+ */
44
+ async start(containerName, firebaseConfFile, publish, options = {}) {
45
+ await this.dockerEmulatorService.start(`${FIREBASE_TOOLS_IMAGE}:${this.firebaseToolsVersion}-node-18-alpine`, containerName, publish, {
46
+ ...options,
47
+ mounts: [
48
+ ...(options.mounts ?? []),
49
+ {
50
+ type: 'bind',
51
+ source: firebaseConfFile,
52
+ destination: FIREBASE_CONTAINER_CONF_FILE,
53
+ readonly: true,
54
+ },
55
+ ],
56
+ commandAndArgs: [
57
+ 'firebase',
58
+ 'emulators:start',
59
+ '-P',
60
+ this.localGcpProject,
61
+ ],
62
+ });
63
+ await this.dockerEmulatorService.waitForAvailability(containerName, `http://127.0.0.1:${publish[0].local}/`, options);
64
+ }
65
+ }
@@ -0,0 +1,55 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { DockerContainerMount, DockerContainerPublish, DockerEmulatorService } from '@causa/workspace-core';
3
+ /**
4
+ * Options when specifying an availability endpoint when starting an emulator.
5
+ */
6
+ type AvailabilityEndpointOptions = {
7
+ /**
8
+ * The endpoint that should be queried to determine when the emulator is available.
9
+ */
10
+ endpoint: string;
11
+ } & NonNullable<Parameters<DockerEmulatorService['waitForAvailability']>[2]>;
12
+ /**
13
+ * A service providing a way to start emulators exposed by the `gcloud` CLI.
14
+ */
15
+ export declare class GcloudEmulatorService {
16
+ /**
17
+ * The underlying {@link DockerEmulatorService} used to start the emulator.
18
+ */
19
+ private readonly dockerEmulatorService;
20
+ /**
21
+ * The local ("demo") GCP project used by the emulator.
22
+ */
23
+ readonly localGcpProject: string;
24
+ /**
25
+ * The version of the `gcloud` CLI (and Docker image) to use.
26
+ */
27
+ readonly gcloudVersion: string;
28
+ constructor(context: WorkspaceContext);
29
+ /**
30
+ * Starts an emulator using the Dockerized `gcloud` CLI.
31
+ * The version of the `gcloud` Docker image can be specified using the `google.gcloud.version` configuration.
32
+ *
33
+ * @param emulatorName The name of the emulator, as exposed by the `gcloud` CLI.
34
+ * @param containerName The name of the Docker container to (re)create.
35
+ * @param publish A list of at least one port to expose from the container.
36
+ * @param options Additional options.
37
+ */
38
+ start(emulatorName: string, containerName: string, publish: [DockerContainerPublish, ...DockerContainerPublish[]], options?: {
39
+ /**
40
+ * A list of Docker volumes to mount.
41
+ */
42
+ mounts?: DockerContainerMount[];
43
+ /**
44
+ * Arguments that should be added at the end of the `gcloud` command running the emulator.
45
+ */
46
+ additionalArguments?: string[];
47
+ /**
48
+ * The endpoint that should be queried and for which a 200 response should be received.
49
+ * If specified, the function will only resolve when the emulator has successfully started.
50
+ * If not specified, the emulator might still be initializing or might have failed when the function resolves.
51
+ */
52
+ availabilityEndpoint?: string | AvailabilityEndpointOptions;
53
+ }): Promise<void>;
54
+ }
55
+ export {};
@@ -0,0 +1,66 @@
1
+ import { DockerEmulatorService, } from '@causa/workspace-core';
2
+ import { getLocalGcpProject, } from '../configurations/index.js';
3
+ /**
4
+ * The URI for the Dockerized `gcloud` command, provided by Google.
5
+ */
6
+ const GCLOUD_DOCKER_IMAGE = `gcr.io/google.com/cloudsdktool/google-cloud-cli`;
7
+ /**
8
+ * A service providing a way to start emulators exposed by the `gcloud` CLI.
9
+ */
10
+ export class GcloudEmulatorService {
11
+ /**
12
+ * The underlying {@link DockerEmulatorService} used to start the emulator.
13
+ */
14
+ dockerEmulatorService;
15
+ /**
16
+ * The local ("demo") GCP project used by the emulator.
17
+ */
18
+ localGcpProject;
19
+ /**
20
+ * The version of the `gcloud` CLI (and Docker image) to use.
21
+ */
22
+ gcloudVersion;
23
+ constructor(context) {
24
+ this.dockerEmulatorService = context.service(DockerEmulatorService);
25
+ this.localGcpProject = getLocalGcpProject(context);
26
+ this.gcloudVersion =
27
+ context
28
+ .asConfiguration()
29
+ .get('google.gcloud.version') ?? 'latest';
30
+ }
31
+ /**
32
+ * Starts an emulator using the Dockerized `gcloud` CLI.
33
+ * The version of the `gcloud` Docker image can be specified using the `google.gcloud.version` configuration.
34
+ *
35
+ * @param emulatorName The name of the emulator, as exposed by the `gcloud` CLI.
36
+ * @param containerName The name of the Docker container to (re)create.
37
+ * @param publish A list of at least one port to expose from the container.
38
+ * @param options Additional options.
39
+ */
40
+ async start(emulatorName, containerName, publish, options = {}) {
41
+ // The `emulators` tag does not have a `latest` version. It is simply `emulators`, which is the default here.
42
+ const imageVersion = [this.gcloudVersion, 'emulators']
43
+ .filter((s) => s !== 'latest')
44
+ .join('-');
45
+ const dockerImage = `${GCLOUD_DOCKER_IMAGE}:${imageVersion}`;
46
+ await this.dockerEmulatorService.start(dockerImage, containerName, publish, {
47
+ commandAndArgs: [
48
+ 'gcloud',
49
+ 'beta',
50
+ 'emulators',
51
+ emulatorName,
52
+ 'start',
53
+ `--host-port=0.0.0.0:${publish[0].container}`,
54
+ `--project=${this.localGcpProject}`,
55
+ ...(options.additionalArguments ?? []),
56
+ ],
57
+ mounts: options.mounts,
58
+ });
59
+ if (options.availabilityEndpoint) {
60
+ const { endpoint, ...waitOptions } = typeof options.availabilityEndpoint === 'string'
61
+ ? { endpoint: options.availabilityEndpoint }
62
+ : options.availabilityEndpoint;
63
+ await this.dockerEmulatorService.waitForAvailability(containerName, endpoint, waitOptions);
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,34 @@
1
+ import { WorkspaceContext } from '@causa/workspace';
2
+ import { GoogleApis } from 'googleapis';
3
+ import { ApiClient, OptionsOfApiClient } from './google-apis.types.js';
4
+ /**
5
+ * A service that exposes the lowest level of Google API clients, from `googleapis`.
6
+ * Those might be needed in last resort, when no higher level client exists for an API.
7
+ */
8
+ export declare class GoogleApisService {
9
+ /**
10
+ * The GCP project ID read from the {@link WorkspaceContext} configuration.
11
+ */
12
+ readonly projectId: string;
13
+ constructor(context: WorkspaceContext);
14
+ /**
15
+ * The promise returning the `JSONClient` configured with the {@link GoogleApisService.projectId}.
16
+ */
17
+ private authClientPromise;
18
+ /**
19
+ * Possibly initializes and returns the auth client to use with Google API clients.
20
+ *
21
+ * @returns The auth client.
22
+ */
23
+ getAuthClient(): Promise<any>;
24
+ /**
25
+ * Creates a new client for one of Google's APIs.
26
+ * Authentication is automatically configured.
27
+ *
28
+ * @param api The name of the Google API.
29
+ * @param version The version of the API.
30
+ * @param arg Options passed to the client.
31
+ * @returns The created client.
32
+ */
33
+ getClient<const T extends keyof GoogleApis, const V extends string>(api: T, version: V, arg: Omit<OptionsOfApiClient<T, V>, 'auth' | 'version'>): Promise<ApiClient<T, V>>;
34
+ }
@@ -0,0 +1,48 @@
1
+ import { google } from 'googleapis';
2
+ /**
3
+ * A service that exposes the lowest level of Google API clients, from `googleapis`.
4
+ * Those might be needed in last resort, when no higher level client exists for an API.
5
+ */
6
+ export class GoogleApisService {
7
+ /**
8
+ * The GCP project ID read from the {@link WorkspaceContext} configuration.
9
+ */
10
+ projectId;
11
+ constructor(context) {
12
+ const googleConf = context.asConfiguration();
13
+ this.projectId = googleConf.getOrThrow('google.project');
14
+ }
15
+ /**
16
+ * The promise returning the `JSONClient` configured with the {@link GoogleApisService.projectId}.
17
+ */
18
+ authClientPromise;
19
+ /**
20
+ * Possibly initializes and returns the auth client to use with Google API clients.
21
+ *
22
+ * @returns The auth client.
23
+ */
24
+ async getAuthClient() {
25
+ if (!this.authClientPromise) {
26
+ const auth = new google.auth.GoogleAuth({
27
+ projectId: this.projectId,
28
+ scopes: ['https://www.googleapis.com/auth/cloud-platform'],
29
+ });
30
+ this.authClientPromise = auth.getClient();
31
+ }
32
+ return await this.authClientPromise;
33
+ }
34
+ /**
35
+ * Creates a new client for one of Google's APIs.
36
+ * Authentication is automatically configured.
37
+ *
38
+ * @param api The name of the Google API.
39
+ * @param version The version of the API.
40
+ * @param arg Options passed to the client.
41
+ * @returns The created client.
42
+ */
43
+ async getClient(api, version, arg) {
44
+ const auth = await this.getAuthClient();
45
+ const clientFn = google[api];
46
+ return clientFn({ ...arg, version, auth });
47
+ }
48
+ }