@rocketmakers/shell-gcloud 0.1.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.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ ## Overview
2
+
3
+ Contains functions for working with [gcloud](https://cloud.google.com/sdk/gcloud) and [gsutil](https://cloud.google.com/storage/docs/gsutil).
@@ -0,0 +1,2 @@
1
+ export declare function toBASE64(value: string): string;
2
+ export declare function fromBASE64(encoded: string): string;
package/lib/base64.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toBASE64 = toBASE64;
4
+ exports.fromBASE64 = fromBASE64;
5
+ function toBASE64(value) {
6
+ return Buffer.from(value).toString('base64');
7
+ }
8
+ function fromBASE64(encoded) {
9
+ const buff = Buffer.from(encoded, 'base64');
10
+ return buff.toString('ascii');
11
+ }
package/lib/env.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function cloneEnvironment(): NodeJS.ProcessEnv;
2
+ export declare function isCI(): boolean;
package/lib/env.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cloneEnvironment = cloneEnvironment;
4
+ exports.isCI = isCI;
5
+ function cloneEnvironment() {
6
+ return Object.create(process.env);
7
+ }
8
+ function isCI() {
9
+ return process.env.CI === 'true';
10
+ }
@@ -0,0 +1,95 @@
1
+ import { IShellExecutor } from '@rocketmakers/shell/executor';
2
+ import { IShellOptions } from '@rocketmakers/shell/options';
3
+ /**
4
+ * Starts an application default login if no application default credentials are already detected.
5
+ * See https://cloud.google.com/sdk/gcloud/reference/auth/application-default
6
+ *
7
+ * This will need user interaction so it is not supported for use in CI.
8
+ * To authenticate in CI, use setupAuthFromEnvironment().
9
+ */
10
+ export declare function applicationDefaultLogin(shell: IShellExecutor, options?: IShellOptions): Promise<void>;
11
+ /**
12
+ * Gets the path to the gcloud executable
13
+ * @returns The path to the gcloud executable
14
+ */
15
+ export declare function getBinFolder(shell: IShellExecutor, options?: IShellOptions): Promise<string>;
16
+ /**
17
+ * Data required to write service account credentials locally and prepare for GCP authentication
18
+ */
19
+ export interface ISetupGoogleAuth {
20
+ /** Current system environment variables */
21
+ env: NodeJS.ProcessEnv;
22
+ /** Optional base64 encoded GCP service account */
23
+ credentialsContentBase64?: string;
24
+ }
25
+ /**
26
+ * Activates service account found at the given credentials path
27
+ *
28
+ * @param {string} credentialsPath Path to the GCP service account credentials
29
+ */
30
+ export declare function activateServiceAccount(shell: IShellExecutor, credentialsPath: string, options?: IShellOptions): Promise<void>;
31
+ /**
32
+ * Deactivates the currently active service account
33
+ */
34
+ export declare function deactivateServiceAccount(shell: IShellExecutor, options?: IShellOptions): Promise<void>;
35
+ /**
36
+ * Activates service account found at the given credentials path
37
+ *
38
+ * @param {() => Promise<TActionResult>} action Action to be executed once a google service account is active
39
+ * @param {string} credentialsPath Path to the GCP service account credentials
40
+ * @returns {Promise<TActionResult>}
41
+ */
42
+ export declare function runWithActivatedServiceAccount<TActionResult>(shell: IShellExecutor, action: () => Promise<TActionResult>, credentialsPath: string, options?: IShellOptions): Promise<TActionResult>;
43
+ /**
44
+ * Data required to upload a directory to a given GCS bucket
45
+ */
46
+ export interface IUploadDirOptions {
47
+ /** Path to the local directory to be uploaded */
48
+ pathToDirectory: string;
49
+ /** Full path within the bucket to the parent directory */
50
+ bucketPath: string;
51
+ /** When set to true, files will be copied to the new location in parallel */
52
+ multiThread?: boolean;
53
+ /** When set to true, files will overwrite those in an existing directory of the same name */
54
+ overwrite?: boolean;
55
+ }
56
+ /**
57
+ * Uploads the given directory to the specified bucket path
58
+ *
59
+ * @param {IUploadDirOptions} options Includes options such as upload files in parallel or overwrite an existing dir with the same name
60
+ * @returns {Promise<void>}
61
+ */
62
+ export declare function uploadDirectoryToBucket(shell: IShellExecutor, options: IUploadDirOptions, shellOptions?: IShellOptions): Promise<void>;
63
+ /**
64
+ * Data required to rename a directory within a GCS bucket
65
+ */
66
+ export interface IRenameBucketDirOptions {
67
+ /** Current name of the directory within the bucket */
68
+ currentName: string;
69
+ /** Desired new name of the directory within the bucket */
70
+ newName: string;
71
+ /** Full path within the bucket to the parent directory */
72
+ bucketPath: string;
73
+ /** When set to true, files will be copied to the new location in parallel */
74
+ multiThread?: boolean;
75
+ /** When set to true, files will overwrite those in an existing directory of the same name */
76
+ overwrite?: boolean;
77
+ }
78
+ /**
79
+ * Rename an existing directory within a gcs bucket
80
+ *
81
+ * @param {IRenameBucketDirOptions} options Includes options such as upload files in parallel or overwrite an existing dir with the same name
82
+ * @returns {Promise<void>}
83
+ */
84
+ export declare function renameBucketDir(shell: IShellExecutor, options: IRenameBucketDirOptions, shellOptions?: IShellOptions): Promise<void>;
85
+ /**
86
+ * Writes a google service account key to the file specified by GOOGLE_APPLICATION_CREDENTIALS.
87
+ *
88
+ * The GOOGLE_APPLICATION_CREDENTIALS_CONTENT is expected to contain the service account key encoded as base 64.
89
+ *
90
+ * @param env The collection of environment variables where the relevant variables are expected
91
+ * @returns {Promise<{ credentialsPath: string }>}
92
+ */
93
+ export declare function setupAuthFromEnvironment({ env, credentialsContentBase64: credentialsContent, }: ISetupGoogleAuth): Promise<{
94
+ credentialsPath: string;
95
+ }>;
package/lib/gcloud.js ADDED
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applicationDefaultLogin = applicationDefaultLogin;
4
+ exports.getBinFolder = getBinFolder;
5
+ exports.activateServiceAccount = activateServiceAccount;
6
+ exports.deactivateServiceAccount = deactivateServiceAccount;
7
+ exports.runWithActivatedServiceAccount = runWithActivatedServiceAccount;
8
+ exports.uploadDirectoryToBucket = uploadDirectoryToBucket;
9
+ exports.renameBucketDir = renameBucketDir;
10
+ exports.setupAuthFromEnvironment = setupAuthFromEnvironment;
11
+ const tslib_1 = require("tslib");
12
+ const logger_1 = require("@rocketmakers/shell/logger");
13
+ const Prerequisites = tslib_1.__importStar(require("@rocketmakers/shell/prerequisites"));
14
+ const shell_fs_1 = require("@rocketmakers/shell-fs");
15
+ const fs_1 = require("fs");
16
+ const promises_1 = require("fs/promises");
17
+ const os_1 = require("os");
18
+ const path_1 = require("path");
19
+ const base64_1 = require("./base64");
20
+ const gcloud = 'gcloud';
21
+ const gsutil = 'gsutil';
22
+ const logger = (0, logger_1.createLogger)('gcloud-module');
23
+ Prerequisites.register({
24
+ command: gcloud,
25
+ description: 'Google Cloud Platform command line tools',
26
+ docsUrl: 'https://cloud.google.com/sdk/gcloud/',
27
+ }, {
28
+ command: gsutil,
29
+ description: 'Google Cloud Storage command line tools',
30
+ docsUrl: 'https://cloud.google.com/storage/docs/gsutil',
31
+ });
32
+ /**
33
+ * Starts an application default login if no application default credentials are already detected.
34
+ * See https://cloud.google.com/sdk/gcloud/reference/auth/application-default
35
+ *
36
+ * This will need user interaction so it is not supported for use in CI.
37
+ * To authenticate in CI, use setupAuthFromEnvironment().
38
+ */
39
+ async function applicationDefaultLogin(shell, options) {
40
+ const credentialsJson = (0, path_1.join)((0, os_1.homedir)(), '.config', 'gcloud', 'application_default_credentials.json');
41
+ if ((0, fs_1.existsSync)(credentialsJson)) {
42
+ logger.warn('Detected gcloud application default credentials so skipping login. If this is not right then run "gcloud auth application-default revoke".');
43
+ return;
44
+ }
45
+ logger.info('Performing gcloud application default login - your browser will open...');
46
+ await shell.exec([gcloud, 'auth', 'application-default', 'login'], options);
47
+ }
48
+ /**
49
+ * Gets the path to the gcloud executable
50
+ * @returns The path to the gcloud executable
51
+ */
52
+ async function getBinFolder(shell, options) {
53
+ return Prerequisites.which(shell, gcloud, options);
54
+ }
55
+ /**
56
+ * Activates service account found at the given credentials path
57
+ *
58
+ * @param {string} credentialsPath Path to the GCP service account credentials
59
+ */
60
+ async function activateServiceAccount(shell, credentialsPath, options) {
61
+ await shell.exec([gcloud, 'auth', 'activate-service-account', '--key-file', credentialsPath], options);
62
+ }
63
+ /**
64
+ * Deactivates the currently active service account
65
+ */
66
+ async function deactivateServiceAccount(shell, options) {
67
+ await shell.exec([gcloud, 'auth', 'revoke'], options);
68
+ }
69
+ /**
70
+ * Activates service account found at the given credentials path
71
+ *
72
+ * @param {() => Promise<TActionResult>} action Action to be executed once a google service account is active
73
+ * @param {string} credentialsPath Path to the GCP service account credentials
74
+ * @returns {Promise<TActionResult>}
75
+ */
76
+ async function runWithActivatedServiceAccount(shell, action, credentialsPath, options) {
77
+ await activateServiceAccount(shell, credentialsPath, options);
78
+ const res = await action();
79
+ await deactivateServiceAccount(shell, options);
80
+ return res;
81
+ }
82
+ /**
83
+ * Uploads the given directory to the specified bucket path
84
+ *
85
+ * @param {IUploadDirOptions} options Includes options such as upload files in parallel or overwrite an existing dir with the same name
86
+ * @returns {Promise<void>}
87
+ */
88
+ async function uploadDirectoryToBucket(shell, options, shellOptions) {
89
+ // -m (allow multi-threading and files to be uploaded in parallel - hangs on mac os)
90
+ // -n (no overwrite of existing files in bucket)
91
+ // -r (copy all contents including sub dirs)
92
+ const gsutilArgs = [
93
+ `${options.multiThread ? '-m' : ''}`,
94
+ 'cp',
95
+ `${options.overwrite ? '' : '-n'}`,
96
+ '-r',
97
+ `${options.pathToDirectory}`,
98
+ options.bucketPath,
99
+ ];
100
+ await shell.exec(['gsutil', ...gsutilArgs.filter(arg => arg !== '')], shellOptions);
101
+ }
102
+ /**
103
+ * Rename an existing directory within a gcs bucket
104
+ *
105
+ * @param {IRenameBucketDirOptions} options Includes options such as upload files in parallel or overwrite an existing dir with the same name
106
+ * @returns {Promise<void>}
107
+ */
108
+ async function renameBucketDir(shell, options, shellOptions) {
109
+ // -m (allow multi-threading and files to be uploaded in parallel - hangs on mac os)
110
+ // -n (no overwrite of existing files in bucket)
111
+ const gsutilArgs = [
112
+ `${options.multiThread ? '-m' : ''}`,
113
+ 'mv',
114
+ `${options.overwrite ? '' : '-n'}`,
115
+ `${options.bucketPath}/${options.currentName}`,
116
+ `${options.bucketPath}/${options.newName}`,
117
+ ];
118
+ await shell.exec(['gsutil', ...gsutilArgs.filter(arg => arg !== '')], shellOptions);
119
+ }
120
+ /**
121
+ * Writes a google service account key to the file specified by GOOGLE_APPLICATION_CREDENTIALS.
122
+ *
123
+ * The GOOGLE_APPLICATION_CREDENTIALS_CONTENT is expected to contain the service account key encoded as base 64.
124
+ *
125
+ * @param env The collection of environment variables where the relevant variables are expected
126
+ * @returns {Promise<{ credentialsPath: string }>}
127
+ */
128
+ async function setupAuthFromEnvironment({ env, credentialsContentBase64: credentialsContent, }) {
129
+ const credentialsPath = env.GOOGLE_APPLICATION_CREDENTIALS;
130
+ if (!credentialsPath) {
131
+ throw new Error('You need set GOOGLE_APPLICATION_CREDENTIALS to a path in an existing directory so the google service account key can be written to that file');
132
+ }
133
+ const credentialsContentBase64 = env.GOOGLE_APPLICATION_CREDENTIALS_CONTENT || credentialsContent;
134
+ if (!credentialsContentBase64) {
135
+ throw new Error(`You need to provide the GCP service account key as base64 - either directly or via GOOGLE_APPLICATION_CREDENTIALS_CONTENT - so it can be written to '${credentialsPath}'`);
136
+ }
137
+ const credentialsDirPath = (0, shell_fs_1.getDirectoryPathFromFilePath)(credentialsPath);
138
+ if (credentialsDirPath === undefined || !(0, shell_fs_1.isDirectory)(credentialsDirPath)) {
139
+ throw new Error(`Not writing GOOGLE_APPLICATION_CREDENTIALS_CONTENT to disk because '${credentialsDirPath}' is not an existing directory.`);
140
+ }
141
+ const credentialsDecoded = (0, base64_1.fromBASE64)(credentialsContentBase64);
142
+ await (0, promises_1.writeFile)(credentialsPath, credentialsDecoded);
143
+ logger.info(`Written GOOGLE_APPLICATION_CREDENTIALS_CONTENT to '${credentialsPath}'`);
144
+ return { credentialsPath };
145
+ }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ *
4
+ * The following script allows you to log into gcloud, which can be particularly useful during CI jobs. GCP service account key can be passed as a script argument or environment variable as below.
5
+ * If not running in CI then the script will automatically trigger `gcloud auth application-default login` unless you are already logged in.
6
+ *
7
+ * **Passing credentials for authentication**
8
+ * - When running in CI `GOOGLE_APPLICATION_CREDENTIALS` is a required environment variable, set as the path to a local file where the base64 decoded key will be written to support authentication.
9
+ * - The base64 encoded service account can either be passed as a script arg using `--base64Key` (see below) or set using the environment variable `GOOGLE_APPLICATION_CREDENTIALS_CONTENT`. When both are provided the env variable will take precedence.
10
+ *
11
+ * Below is an example call:
12
+ *
13
+ * ```bash
14
+ * npx rocketmakers-gcloud-auth -l=trace
15
+ *
16
+ * # Run the following to see expected args and valid values
17
+ * npx rocketmakers-gcloud-auth --help
18
+ *
19
+ * # --log (-l) - single
20
+ * # > The log level
21
+ * # > Optional Default: "info"
22
+ * # > Must be: trace | debug | info | warn | error | fatal
23
+ *
24
+ * # --base64Key (-k) - single
25
+ * # > The GCP service account key encoded as base64
26
+ * # > Optional Default: ""
27
+ * ```
28
+ *
29
+ */
30
+ export declare function runGcloudAuth(): Promise<void>;
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.runGcloudAuth = runGcloudAuth;
5
+ const tslib_1 = require("tslib");
6
+ const Args = tslib_1.__importStar(require("@rocketmakers/shell/args"));
7
+ const logger_1 = require("@rocketmakers/shell/logger");
8
+ const Prerequisites = tslib_1.__importStar(require("@rocketmakers/shell/prerequisites"));
9
+ const shell_executor_1 = require("@rocketmakers/shell-executor");
10
+ const env_1 = require("../env");
11
+ const GoogleCloud = tslib_1.__importStar(require("../gcloud"));
12
+ const logger = (0, logger_1.createLogger)('gcloud-auth');
13
+ const shell = new shell_executor_1.ShellExecutor();
14
+ /**
15
+ *
16
+ * The following script allows you to log into gcloud, which can be particularly useful during CI jobs. GCP service account key can be passed as a script argument or environment variable as below.
17
+ * If not running in CI then the script will automatically trigger `gcloud auth application-default login` unless you are already logged in.
18
+ *
19
+ * **Passing credentials for authentication**
20
+ * - When running in CI `GOOGLE_APPLICATION_CREDENTIALS` is a required environment variable, set as the path to a local file where the base64 decoded key will be written to support authentication.
21
+ * - The base64 encoded service account can either be passed as a script arg using `--base64Key` (see below) or set using the environment variable `GOOGLE_APPLICATION_CREDENTIALS_CONTENT`. When both are provided the env variable will take precedence.
22
+ *
23
+ * Below is an example call:
24
+ *
25
+ * ```bash
26
+ * npx rocketmakers-gcloud-auth -l=trace
27
+ *
28
+ * # Run the following to see expected args and valid values
29
+ * npx rocketmakers-gcloud-auth --help
30
+ *
31
+ * # --log (-l) - single
32
+ * # > The log level
33
+ * # > Optional Default: "info"
34
+ * # > Must be: trace | debug | info | warn | error | fatal
35
+ *
36
+ * # --base64Key (-k) - single
37
+ * # > The GCP service account key encoded as base64
38
+ * # > Optional Default: ""
39
+ * ```
40
+ *
41
+ */
42
+ async function runGcloudAuth() {
43
+ const args = await Args.match({
44
+ log: Args.single({
45
+ description: 'The log level',
46
+ shortName: 'l',
47
+ defaultValue: process.env.LOG_LEVEL || 'info',
48
+ validValues: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'],
49
+ }),
50
+ base64Key: Args.single({
51
+ description: 'The GCP service account key encoded as base64',
52
+ shortName: 'k',
53
+ }),
54
+ });
55
+ if (args === null || args === void 0 ? void 0 : args.log) {
56
+ (0, logger_1.setDefaultLoggerLevel)(args.log);
57
+ }
58
+ if (!args) {
59
+ if (process.argv.includes('--help')) {
60
+ return;
61
+ }
62
+ throw new Error('There was a problem parsing the arguments');
63
+ }
64
+ await Prerequisites.check(shell);
65
+ await ((0, env_1.isCI)()
66
+ ? GoogleCloud.setupAuthFromEnvironment({
67
+ env: (0, env_1.cloneEnvironment)(),
68
+ credentialsContentBase64: args.base64Key,
69
+ })
70
+ : GoogleCloud.applicationDefaultLogin(shell));
71
+ }
72
+ runGcloudAuth()
73
+ .then(() => logger.info('🚀 Done 🚀'))
74
+ .catch(err => {
75
+ logger.fatal(err);
76
+ process.exit(-1);
77
+ });
@@ -0,0 +1,2 @@
1
+ export declare function toBASE64(value: string): string;
2
+ export declare function fromBASE64(encoded: string): string;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toBASE64 = toBASE64;
4
+ exports.fromBASE64 = fromBASE64;
5
+ function toBASE64(value) {
6
+ return Buffer.from(value).toString('base64');
7
+ }
8
+ function fromBASE64(encoded) {
9
+ const buff = Buffer.from(encoded, 'base64');
10
+ return buff.toString('ascii');
11
+ }
@@ -0,0 +1,2 @@
1
+ export declare function cloneEnvironment(): NodeJS.ProcessEnv;
2
+ export declare function isCI(): boolean;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cloneEnvironment = cloneEnvironment;
4
+ exports.isCI = isCI;
5
+ function cloneEnvironment() {
6
+ return Object.create(process.env);
7
+ }
8
+ function isCI() {
9
+ return process.env.CI === 'true';
10
+ }
@@ -0,0 +1,103 @@
1
+ import { IShellExecutor } from '@rocketmakers/shell/executor';
2
+ import { IShellOptions } from '@rocketmakers/shell/options';
3
+ /**
4
+ * Indicates the location of a GKE cluster either as a region or a zone.
5
+ * This is used to interact with the 'gcloud container clusters' CLI.
6
+ */
7
+ export interface IClusterLocation {
8
+ type: 'region' | 'zone';
9
+ value: string;
10
+ }
11
+ /**
12
+ * Starts an application default login if no application default credentials are already detected.
13
+ * See https://cloud.google.com/sdk/gcloud/reference/auth/application-default
14
+ *
15
+ * This will need user interaction so it is not supported for use in CI.
16
+ * To authenticate in CI, use setupAuthFromEnvironment().
17
+ */
18
+ export declare function applicationDefaultLogin(shell: IShellExecutor, options?: IShellOptions): Promise<void>;
19
+ /**
20
+ * Gets the path to the gcloud executable
21
+ * @returns The path to the gcloud executable
22
+ */
23
+ export declare function getBinFolder(shell: IShellExecutor, options?: IShellOptions): Promise<string>;
24
+ /**
25
+ * Data required to write service account credentials locally and prepare for GCP authentication
26
+ */
27
+ export interface ISetupGoogleAuth {
28
+ /** Current system environment variables */
29
+ env: NodeJS.ProcessEnv;
30
+ /** Optional base64 encoded GCP service account */
31
+ credentialsContentBase64?: string;
32
+ }
33
+ /**
34
+ * Activates service account found at the given credentials path
35
+ *
36
+ * @param {string} credentialsPath Path to the GCP service account credentials
37
+ */
38
+ export declare function activateServiceAccount(shell: IShellExecutor, credentialsPath: string, options?: IShellOptions): Promise<void>;
39
+ /**
40
+ * Deactivates the currently active service account
41
+ */
42
+ export declare function deactivateServiceAccount(shell: IShellExecutor, options?: IShellOptions): Promise<void>;
43
+ /**
44
+ * Activates service account found at the given credentials path
45
+ *
46
+ * @param {() => Promise<TActionResult>} action Action to be executed once a google service account is active
47
+ * @param {string} credentialsPath Path to the GCP service account credentials
48
+ * @returns {Promise<TActionResult>}
49
+ */
50
+ export declare function runWithActivatedServiceAccount<TActionResult>(shell: IShellExecutor, action: () => Promise<TActionResult>, credentialsPath: string, options?: IShellOptions): Promise<TActionResult>;
51
+ /**
52
+ * Data required to upload a directory to a given GCS bucket
53
+ */
54
+ export interface IUploadDirOptions {
55
+ /** Path to the local directory to be uploaded */
56
+ pathToDirectory: string;
57
+ /** Full path within the bucket to the parent directory */
58
+ bucketPath: string;
59
+ /** When set to true, files will be copied to the new location in parallel */
60
+ multiThread?: boolean;
61
+ /** When set to true, files will overwrite those in an existing directory of the same name */
62
+ overwrite?: boolean;
63
+ }
64
+ /**
65
+ * Uploads the given directory to the specified bucket path
66
+ *
67
+ * @param {IUploadDirOptions} options Includes options such as upload files in parallel or overwrite an existing dir with the same name
68
+ * @returns {Promise<void>}
69
+ */
70
+ export declare function uploadDirectoryToBucket(shell: IShellExecutor, options: IUploadDirOptions, shellOptions?: IShellOptions): Promise<void>;
71
+ /**
72
+ * Data required to rename a directory within a GCS bucket
73
+ */
74
+ export interface IRenameBucketDirOptions {
75
+ /** Current name of the directory within the bucket */
76
+ currentName: string;
77
+ /** Desired new name of the directory within the bucket */
78
+ newName: string;
79
+ /** Full path within the bucket to the parent directory */
80
+ bucketPath: string;
81
+ /** When set to true, files will be copied to the new location in parallel */
82
+ multiThread?: boolean;
83
+ /** When set to true, files will overwrite those in an existing directory of the same name */
84
+ overwrite?: boolean;
85
+ }
86
+ /**
87
+ * Rename an existing director within a gcs bucket
88
+ *
89
+ * @param {IRenameBucketDirOptions} options Includes options such as upload files in parallel or overwrite an existing dir with the same name
90
+ * @returns {Promise<void>}
91
+ */
92
+ export declare function renameBucketDir(shell: IShellExecutor, options: IRenameBucketDirOptions, shellOptions?: IShellOptions): Promise<void>;
93
+ /**
94
+ * Writes a google service account key to the file specified by GOOGLE_APPLICATION_CREDENTIALS.
95
+ *
96
+ * The GOOGLE_APPLICATION_CREDENTIALS_CONTENT is expected to contain the service account key encoded as base 64.
97
+ *
98
+ * @param env The collection of environment variables where the relevant variables are expected
99
+ * @returns {Promise<{ credentialsPath: string }>}
100
+ */
101
+ export declare function setupAuthFromEnvironment({ env, credentialsContentBase64: credentialsContent, }: ISetupGoogleAuth): Promise<{
102
+ credentialsPath: string;
103
+ }>;
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applicationDefaultLogin = applicationDefaultLogin;
4
+ exports.getBinFolder = getBinFolder;
5
+ exports.activateServiceAccount = activateServiceAccount;
6
+ exports.deactivateServiceAccount = deactivateServiceAccount;
7
+ exports.runWithActivatedServiceAccount = runWithActivatedServiceAccount;
8
+ exports.uploadDirectoryToBucket = uploadDirectoryToBucket;
9
+ exports.renameBucketDir = renameBucketDir;
10
+ exports.setupAuthFromEnvironment = setupAuthFromEnvironment;
11
+ const tslib_1 = require("tslib");
12
+ const logger_1 = require("@rocketmakers/shell/logger");
13
+ const Prerequisites = tslib_1.__importStar(require("@rocketmakers/shell/prerequisites"));
14
+ const shell_fs_1 = require("@rocketmakers/shell-fs");
15
+ const fs_1 = require("fs");
16
+ const promises_1 = require("fs/promises");
17
+ const os_1 = require("os");
18
+ const path_1 = require("path");
19
+ const base64_1 = require("./base64");
20
+ const gcloud = 'gcloud';
21
+ const gsutil = 'gsutil';
22
+ const logger = (0, logger_1.createLogger)('gcloud-module');
23
+ Prerequisites.register({
24
+ command: gcloud,
25
+ description: 'Google Cloud Platform command line tools',
26
+ docsUrl: 'https://cloud.google.com/sdk/gcloud/',
27
+ }, {
28
+ command: gsutil,
29
+ description: 'Google Cloud Storage command line tools',
30
+ docsUrl: 'https://cloud.google.com/storage/docs/gsutil',
31
+ });
32
+ /**
33
+ * Starts an application default login if no application default credentials are already detected.
34
+ * See https://cloud.google.com/sdk/gcloud/reference/auth/application-default
35
+ *
36
+ * This will need user interaction so it is not supported for use in CI.
37
+ * To authenticate in CI, use setupAuthFromEnvironment().
38
+ */
39
+ async function applicationDefaultLogin(shell, options) {
40
+ const credentialsJson = (0, path_1.join)((0, os_1.homedir)(), '.config', 'gcloud', 'application_default_credentials.json');
41
+ if ((0, fs_1.existsSync)(credentialsJson)) {
42
+ logger.warn('Detected gcloud application default credentials so skipping login. If this is not right then run "gcloud auth application-default revoke".');
43
+ return;
44
+ }
45
+ logger.info('Performing gcloud application default login - your browser will open...');
46
+ await shell.exec([gcloud, 'auth', 'application-default', 'login'], options);
47
+ }
48
+ /**
49
+ * Gets the path to the gcloud executable
50
+ * @returns The path to the gcloud executable
51
+ */
52
+ async function getBinFolder(shell, options) {
53
+ return Prerequisites.which(shell, gcloud, options);
54
+ }
55
+ /**
56
+ * Activates service account found at the given credentials path
57
+ *
58
+ * @param {string} credentialsPath Path to the GCP service account credentials
59
+ */
60
+ async function activateServiceAccount(shell, credentialsPath, options) {
61
+ await shell.exec([gcloud, 'auth', 'activate-service-account', '--key-file', credentialsPath], options);
62
+ }
63
+ /**
64
+ * Deactivates the currently active service account
65
+ */
66
+ async function deactivateServiceAccount(shell, options) {
67
+ await shell.exec([gcloud, 'auth', 'revoke'], options);
68
+ }
69
+ /**
70
+ * Activates service account found at the given credentials path
71
+ *
72
+ * @param {() => Promise<TActionResult>} action Action to be executed once a google service account is active
73
+ * @param {string} credentialsPath Path to the GCP service account credentials
74
+ * @returns {Promise<TActionResult>}
75
+ */
76
+ async function runWithActivatedServiceAccount(shell, action, credentialsPath, options) {
77
+ await activateServiceAccount(shell, credentialsPath, options);
78
+ const res = await action();
79
+ await deactivateServiceAccount(shell, options);
80
+ return res;
81
+ }
82
+ /**
83
+ * Uploads the given directory to the specified bucket path
84
+ *
85
+ * @param {IUploadDirOptions} options Includes options such as upload files in parallel or overwrite an existing dir with the same name
86
+ * @returns {Promise<void>}
87
+ */
88
+ async function uploadDirectoryToBucket(shell, options, shellOptions) {
89
+ // -m (allow multi-threading and files to be uploaded in parallel - hangs on mac os)
90
+ // -n (no overwrite of existing files in bucket)
91
+ // -r (copy all contents including sub dirs)
92
+ const gsutilArgs = [
93
+ `${options.multiThread ? '-m' : ''}`,
94
+ 'cp',
95
+ `${options.overwrite ? '' : '-n'}`,
96
+ '-r',
97
+ `${options.pathToDirectory}`,
98
+ options.bucketPath,
99
+ ];
100
+ await shell.exec(['gsutil', ...gsutilArgs.filter(arg => arg !== '')], shellOptions);
101
+ }
102
+ /**
103
+ * Rename an existing director within a gcs bucket
104
+ *
105
+ * @param {IRenameBucketDirOptions} options Includes options such as upload files in parallel or overwrite an existing dir with the same name
106
+ * @returns {Promise<void>}
107
+ */
108
+ async function renameBucketDir(shell, options, shellOptions) {
109
+ // -m (allow multi-threading and files to be uploaded in parallel - hangs on mac os)
110
+ // -n (no overwrite of existing files in bucket)
111
+ const gsutilArgs = [
112
+ `${options.multiThread ? '-m' : ''}`,
113
+ 'mv',
114
+ `${options.overwrite ? '' : '-n'}`,
115
+ `${options.bucketPath}/${options.currentName}`,
116
+ `${options.bucketPath}/${options.newName}`,
117
+ ];
118
+ await shell.exec(['gsutil', ...gsutilArgs.filter(arg => arg !== '')], shellOptions);
119
+ }
120
+ /**
121
+ * Writes a google service account key to the file specified by GOOGLE_APPLICATION_CREDENTIALS.
122
+ *
123
+ * The GOOGLE_APPLICATION_CREDENTIALS_CONTENT is expected to contain the service account key encoded as base 64.
124
+ *
125
+ * @param env The collection of environment variables where the relevant variables are expected
126
+ * @returns {Promise<{ credentialsPath: string }>}
127
+ */
128
+ async function setupAuthFromEnvironment({ env, credentialsContentBase64: credentialsContent, }) {
129
+ const credentialsPath = env.GOOGLE_APPLICATION_CREDENTIALS;
130
+ if (!credentialsPath) {
131
+ throw new Error('You need set GOOGLE_APPLICATION_CREDENTIALS to a path in an existing directory so the google service account key can be written to that file');
132
+ }
133
+ const credentialsContentBase64 = env.GOOGLE_APPLICATION_CREDENTIALS_CONTENT || credentialsContent;
134
+ if (!credentialsContentBase64) {
135
+ throw new Error(`You need to provide the GCP service account key as base64 - either directly or via GOOGLE_APPLICATION_CREDENTIALS_CONTENT - so it can be written to '${credentialsPath}'`);
136
+ }
137
+ const credentialsDirPath = (0, shell_fs_1.getDirectoryPathFromFilePath)(credentialsPath);
138
+ if (credentialsDirPath === undefined || !(0, shell_fs_1.isDirectory)(credentialsDirPath)) {
139
+ throw new Error(`Not writing GOOGLE_APPLICATION_CREDENTIALS_CONTENT to disk because '${credentialsDirPath}' is not an existing directory.`);
140
+ }
141
+ const credentialsDecoded = (0, base64_1.fromBASE64)(credentialsContentBase64);
142
+ await (0, promises_1.writeFile)(credentialsPath, credentialsDecoded);
143
+ logger.info(`Written GOOGLE_APPLICATION_CREDENTIALS_CONTENT to '${credentialsPath}'`);
144
+ return { credentialsPath };
145
+ }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ *
4
+ * The following script allows you to log into gcloud, which can be particularly useful during CI jobs. GCP service account key can be passed as a script argument or environment variable as below.
5
+ * If not running in CI then the script will automatically trigger `gcloud auth application-default login` unless you are already logged in.
6
+ *
7
+ * **Passing credentials for authentication**
8
+ * - When running in CI `GOOGLE_APPLICATION_CREDENTIALS` is a required environment variable, set as the path to a local file where the base64 decoded key will be written to support authentication.
9
+ * - The base64 encoded service account can either be passed as a script arg using `--base64Key` (see below) or set using the environment variable `GOOGLE_APPLICATION_CREDENTIALS_CONTENT`. When both are provided the env variable will take precedence.
10
+ *
11
+ * Below is an example call:
12
+ *
13
+ * ```bash
14
+ * npx rocketmakers-gcloud-auth -l=trace
15
+ *
16
+ * # Run the following to see expected args and valid values
17
+ * npx rocketmakers-gcloud-auth --help
18
+ *
19
+ * # --log (-l) - single
20
+ * # > The log level
21
+ * # > Optional Default: "info"
22
+ * # > Must be: trace | debug | info | warn | error | fatal
23
+ *
24
+ * # --base64Key (-k) - single
25
+ * # > The GCP service account key encoded as base64
26
+ * # > Optional Default: ""
27
+ * ```
28
+ *
29
+ */
30
+ export declare function runGcloudAuth(): Promise<void>;
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.runGcloudAuth = runGcloudAuth;
5
+ const tslib_1 = require("tslib");
6
+ const Args = tslib_1.__importStar(require("@rocketmakers/shell/args"));
7
+ const logger_1 = require("@rocketmakers/shell/logger");
8
+ const Prerequisites = tslib_1.__importStar(require("@rocketmakers/shell/prerequisites"));
9
+ const shell_executor_1 = require("@rocketmakers/shell-executor");
10
+ const env_1 = require("../env");
11
+ const GoogleCloud = tslib_1.__importStar(require("../gcloud"));
12
+ const logger = (0, logger_1.createLogger)('gcloud-auth');
13
+ const shell = new shell_executor_1.ShellExecutor();
14
+ /**
15
+ *
16
+ * The following script allows you to log into gcloud, which can be particularly useful during CI jobs. GCP service account key can be passed as a script argument or environment variable as below.
17
+ * If not running in CI then the script will automatically trigger `gcloud auth application-default login` unless you are already logged in.
18
+ *
19
+ * **Passing credentials for authentication**
20
+ * - When running in CI `GOOGLE_APPLICATION_CREDENTIALS` is a required environment variable, set as the path to a local file where the base64 decoded key will be written to support authentication.
21
+ * - The base64 encoded service account can either be passed as a script arg using `--base64Key` (see below) or set using the environment variable `GOOGLE_APPLICATION_CREDENTIALS_CONTENT`. When both are provided the env variable will take precedence.
22
+ *
23
+ * Below is an example call:
24
+ *
25
+ * ```bash
26
+ * npx rocketmakers-gcloud-auth -l=trace
27
+ *
28
+ * # Run the following to see expected args and valid values
29
+ * npx rocketmakers-gcloud-auth --help
30
+ *
31
+ * # --log (-l) - single
32
+ * # > The log level
33
+ * # > Optional Default: "info"
34
+ * # > Must be: trace | debug | info | warn | error | fatal
35
+ *
36
+ * # --base64Key (-k) - single
37
+ * # > The GCP service account key encoded as base64
38
+ * # > Optional Default: ""
39
+ * ```
40
+ *
41
+ */
42
+ async function runGcloudAuth() {
43
+ const args = await Args.match({
44
+ log: Args.single({
45
+ description: 'The log level',
46
+ shortName: 'l',
47
+ defaultValue: process.env.LOG_LEVEL || 'info',
48
+ validValues: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'],
49
+ }),
50
+ base64Key: Args.single({
51
+ description: 'The GCP service account key encoded as base64',
52
+ shortName: 'k',
53
+ }),
54
+ });
55
+ if (args === null || args === void 0 ? void 0 : args.log) {
56
+ (0, logger_1.setDefaultLoggerLevel)(args.log);
57
+ }
58
+ if (!args) {
59
+ if (process.argv.includes('--help')) {
60
+ return;
61
+ }
62
+ throw new Error('There was a problem parsing the arguments');
63
+ }
64
+ await Prerequisites.check(shell);
65
+ await ((0, env_1.isCI)()
66
+ ? GoogleCloud.setupAuthFromEnvironment({
67
+ env: (0, env_1.cloneEnvironment)(),
68
+ credentialsContentBase64: args.base64Key,
69
+ })
70
+ : GoogleCloud.applicationDefaultLogin(shell));
71
+ }
72
+ runGcloudAuth()
73
+ .then(() => logger.info('🚀 Done 🚀'))
74
+ .catch(err => {
75
+ logger.fatal(err);
76
+ process.exit(-1);
77
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const suite_1 = require("@rocketmakers/shell-tests/suite");
4
+ const promises_1 = require("fs/promises");
5
+ const base64_1 = require("../source/base64");
6
+ const gcloud_1 = require("../source/gcloud");
7
+ const packagePaths_1 = require("./packagePaths");
8
+ (0, suite_1.sandboxedTestSuite)({ suiteName: 'gcloud', sandboxRootPath: (0, packagePaths_1.sandboxPath)() }, () => {
9
+ describe('setupAuthFromEnvironment', () => {
10
+ const credentialsPath = (0, packagePaths_1.sandboxPath)('credentials.json');
11
+ const credentialsContent = 'fake credentials';
12
+ const credentialsContentBase64 = (0, base64_1.toBASE64)(credentialsContent);
13
+ it('when GOOGLE_APPLICATION_CREDENTIALS and GOOGLE_APPLICATION_CREDENTIALS_CONTENT are set, the credentials are written to the file', async () => {
14
+ await (0, gcloud_1.setupAuthFromEnvironment)({
15
+ env: {
16
+ GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
17
+ GOOGLE_APPLICATION_CREDENTIALS_CONTENT: credentialsContentBase64,
18
+ },
19
+ });
20
+ const content = await (0, promises_1.readFile)(credentialsPath, { encoding: 'utf8' });
21
+ expect(content).toEqual(credentialsContent);
22
+ });
23
+ it('GOOGLE_APPLICATION_CREDENTIALS_CONTENT overrides the credentialsContentBase64 option', async () => {
24
+ const specificCredentials = 'some specific content';
25
+ const specificCredentialsBase64 = (0, base64_1.toBASE64)(specificCredentials);
26
+ await (0, gcloud_1.setupAuthFromEnvironment)({
27
+ credentialsContentBase64: specificCredentialsBase64,
28
+ env: {
29
+ GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
30
+ GOOGLE_APPLICATION_CREDENTIALS_CONTENT: credentialsContentBase64,
31
+ },
32
+ });
33
+ const content = await (0, promises_1.readFile)(credentialsPath, { encoding: 'utf8' });
34
+ expect(content).toEqual(credentialsContent);
35
+ });
36
+ it('when credentialsContentBase64 is provided without GOOGLE_APPLICATION_CREDENTIALS_CONTENT, the credentials are written to the file', async () => {
37
+ const specificCredentials = 'some specific content';
38
+ const specificCredentialsBase64 = (0, base64_1.toBASE64)(specificCredentials);
39
+ await (0, gcloud_1.setupAuthFromEnvironment)({
40
+ credentialsContentBase64: specificCredentialsBase64,
41
+ env: {
42
+ GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
43
+ },
44
+ });
45
+ const content = await (0, promises_1.readFile)(credentialsPath, { encoding: 'utf8' });
46
+ expect(content).toEqual(specificCredentials);
47
+ });
48
+ it('when GOOGLE_APPLICATION_CREDENTIALS is not set, throws an error', async () => {
49
+ expect.assertions(1);
50
+ try {
51
+ await (0, gcloud_1.setupAuthFromEnvironment)({
52
+ env: {
53
+ GOOGLE_APPLICATION_CREDENTIALS_CONTENT: credentialsContentBase64,
54
+ },
55
+ });
56
+ }
57
+ catch (err) {
58
+ expect(err.message).toContain('GOOGLE_APPLICATION_CREDENTIALS');
59
+ }
60
+ });
61
+ const notDirectoryPaths = ['/not/a/directory', ''];
62
+ notDirectoryPaths.forEach(testPath => it(`when GOOGLE_APPLICATION_CREDENTIALS is '${testPath}', throws an error`, async () => {
63
+ expect.assertions(1);
64
+ try {
65
+ await (0, gcloud_1.setupAuthFromEnvironment)({
66
+ env: {
67
+ GOOGLE_APPLICATION_CREDENTIALS: testPath,
68
+ GOOGLE_APPLICATION_CREDENTIALS_CONTENT: credentialsContentBase64,
69
+ },
70
+ });
71
+ }
72
+ catch (err) {
73
+ expect(err.message).toContain('GOOGLE_APPLICATION_CREDENTIALS');
74
+ }
75
+ }));
76
+ it('when GOOGLE_APPLICATION_CREDENTIALS_CONTENT is not set, throws an error', async () => {
77
+ expect.assertions(1);
78
+ try {
79
+ await (0, gcloud_1.setupAuthFromEnvironment)({
80
+ env: {
81
+ GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
82
+ },
83
+ });
84
+ }
85
+ catch (err) {
86
+ expect(err.message).toContain('GOOGLE_APPLICATION_CREDENTIALS_CONTENT');
87
+ }
88
+ });
89
+ });
90
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Creates an absolute path based on a relative path from the root of this package
3
+ * @param parts
4
+ */
5
+ export declare function packagePath(...parts: string[]): string;
6
+ /**
7
+ * Creates an absolute path relative to the sandbox testing path
8
+ * @param parts The parts of the path relative to the testing sandbox path
9
+ */
10
+ export declare function sandboxPath(...parts: string[]): string;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.packagePath = packagePath;
4
+ exports.sandboxPath = sandboxPath;
5
+ const path_1 = require("path");
6
+ /**
7
+ * Creates an absolute path based on a relative path from the root of this package
8
+ * @param parts
9
+ */
10
+ function packagePath(...parts) {
11
+ return (0, path_1.resolve)(__dirname, '..', ...parts);
12
+ }
13
+ /**
14
+ * Creates an absolute path relative to the sandbox testing path
15
+ * @param parts The parts of the path relative to the testing sandbox path
16
+ */
17
+ function sandboxPath(...parts) {
18
+ return (0, path_1.join)(packagePath('_tests-sandbox_'), ...parts);
19
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@rocketmakers/shell-gcloud",
3
+ "version": "0.1.0",
4
+ "bin": {
5
+ "rocketmakers-gcloud-auth": "./lib/run/gcloud-auth.js"
6
+ },
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/gcloud.d.ts",
10
+ "default": "./lib/gcloud.js"
11
+ }
12
+ },
13
+ "dependencies": {
14
+ "@rocketmakers/log": "0.1.0",
15
+ "bunyan": "1.8.12",
16
+ "tslib": "2.8.1",
17
+ "@rocketmakers/shell-executor": "0.1.0",
18
+ "@rocketmakers/shell": "0.1.0",
19
+ "@rocketmakers/shell-fs": "0.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/jest": "29.5.14",
23
+ "@types/node": "22.13.9",
24
+ "eslint": "8.57.1",
25
+ "jest": "29.7.0",
26
+ "jest-junit": "16.0.0",
27
+ "ts-jest": "29.3.0",
28
+ "typescript": "5.8.2",
29
+ "@rocketmakers/shell-tests": "0.1.0",
30
+ "eslint-config-shell": "0.1.0",
31
+ "tsconfig-shell": "0.1.0"
32
+ },
33
+ "directories": {
34
+ "lib": "lib"
35
+ },
36
+ "engines": {
37
+ "node": " ^20 || ^22"
38
+ },
39
+ "files": [
40
+ "lib/**/*.js",
41
+ "lib/**/*.d.ts",
42
+ "!lib/index.js",
43
+ "!lib/index.d.ts"
44
+ ],
45
+ "keywords": [],
46
+ "license": "MIT",
47
+ "repository": "Rocketmakers/rocketmakers-packages",
48
+ "scripts": {
49
+ "build": "tsc -p .",
50
+ "lint": "eslint . --ext .ts",
51
+ "lint-fix": "pnpm lint --fix",
52
+ "test-integration": "jest --runInBand"
53
+ }
54
+ }