@metaplay/metaplay-auth 1.4.2 → 1.6.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/dist/index.js DELETED
@@ -1,644 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import Docker from 'dockerode';
4
- import { loginAndSaveTokens, machineLoginAndSaveTokens, extendCurrentSession, loadTokens, removeTokens } from './src/auth.js';
5
- import { StackAPI } from './src/stackapi.js';
6
- import { checkGameServerDeployment } from './src/deployment.js';
7
- import { logger, setLogLevel } from './src/logging.js';
8
- import { isValidFQDN, executeCommand } from './src/utils.js';
9
- import { exit } from 'process';
10
- import { tmpdir } from 'os';
11
- import { join } from 'path';
12
- import { randomBytes } from 'crypto';
13
- import { writeFile, unlink } from 'fs/promises';
14
- import { existsSync } from 'fs';
15
- import { KubeConfig } from '@kubernetes/client-node';
16
- import * as semver from 'semver';
17
- /**
18
- * Helper for parsing the GameserverId type from the command line arguments. Accepts either the gameserver address
19
- * (idler-test.p1.metaplay.io), a shorthand address (metaplay-idler-test) or the (organization, project, environment)
20
- * tuple from options.
21
- */
22
- function resolveGameserverId(address, options) {
23
- // If address is specified, use it, otherwise assume options has organization, project, and environment
24
- if (address) {
25
- if (isValidFQDN(address)) {
26
- return { gameserver: address };
27
- }
28
- else {
29
- const parts = address.split('-');
30
- if (parts.length !== 3) {
31
- throw new Error('Invalid gameserver address syntax: specify either <organization>-<project>-<environment> or a fully-qualified domain name (eg, idler-develop.p1.metaplay.io)');
32
- }
33
- return { organization: parts[0], project: parts[1], environment: parts[2] };
34
- }
35
- }
36
- else if (options.organization && options.project && options.environment) {
37
- return { organization: options.organization, project: options.project, environment: options.environment };
38
- }
39
- else {
40
- throw new Error('Could not determine target environment from arguments: You need to specify either a gameserver address or an organization, project, and environment. Run this command with --help flag for more information.');
41
- }
42
- }
43
- const program = new Command();
44
- program
45
- .name('metaplay-auth')
46
- .description('Authenticate with Metaplay and get AWS and Kubernetes credentials for game servers.')
47
- .version('1.4.2')
48
- .option('-d, --debug', 'enable debug output')
49
- .hook('preAction', (thisCommand) => {
50
- // Handle debug flag for all commands.
51
- const opts = thisCommand.opts();
52
- if (opts.debug) {
53
- setLogLevel(0);
54
- }
55
- else {
56
- setLogLevel(10);
57
- }
58
- });
59
- program.command('login')
60
- .description('login to your Metaplay account')
61
- .action(async () => {
62
- await loginAndSaveTokens();
63
- });
64
- program.command('machine-login')
65
- .description('login to the Metaplay cloud using a machine account (using credentials in environment variable METAPLAY_CREDENTIALS)')
66
- .option('--dev-credentials', 'machine user credentials to use, only for dev purposes, use METAPLAY_CREDENTIALS env variable for better safety!')
67
- .action(async (options) => {
68
- // Get credentials from command line or from METAPLAY_CREDENTIALS environment variable
69
- let credentials;
70
- if (options.devCredentials) {
71
- credentials = options.devCredentials;
72
- }
73
- else {
74
- credentials = process.env.METAPLAY_CREDENTIALS;
75
- if (!credentials || credentials === '') {
76
- throw new Error('Unable to find the credentials, the environment variable METAPLAY_CREDENTIALS is not defined!');
77
- }
78
- }
79
- // Parse the clientId and clientSecret from the credentials (separate by a '+' character)
80
- // \note We can't be certain that the secret does not contain pluses so split at the first occurrence
81
- const splitOffset = credentials.indexOf('+');
82
- if (splitOffset === -1) {
83
- throw new Error('Invalid format for credentials, you should copy-paste the value from the developer portal verbatim');
84
- }
85
- const clientId = credentials.substring(0, splitOffset);
86
- const clientSecret = credentials.substring(splitOffset + 1);
87
- // Login with machine user and save the tokens
88
- await machineLoginAndSaveTokens(clientId, clientSecret);
89
- });
90
- program.command('logout')
91
- .description('log out of your Metaplay account')
92
- .action(async () => {
93
- console.log('Logging out by removing locally stored tokens...');
94
- try {
95
- // TODO: Could check if the tokens existed in the first place and print something nicer if doing an unnecessary operation?
96
- await removeTokens();
97
- console.log('Done! You are now logged out.');
98
- }
99
- catch (error) {
100
- if (error instanceof Error) {
101
- console.error(`Error logging out: ${error.message}`);
102
- }
103
- exit(1);
104
- }
105
- });
106
- program.command('show-tokens')
107
- .description('show loaded tokens')
108
- .hook('preAction', async () => {
109
- await extendCurrentSession();
110
- })
111
- .action(async (options) => {
112
- try {
113
- // TODO: Could detect if not logged in and fail more gracefully?
114
- const tokens = await loadTokens();
115
- console.log(tokens);
116
- }
117
- catch (error) {
118
- if (error instanceof Error) {
119
- console.error(`Error showing tokens: ${error.message}`);
120
- }
121
- exit(1);
122
- }
123
- });
124
- program.command('get-kubeconfig')
125
- .description('get kubeconfig for target environment')
126
- .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
127
- .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
128
- .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
129
- .option('-p, --project <project>', 'project name (e.g. idler)')
130
- .option('-e, --environment <environment>', 'environment name (e.g. develop)')
131
- .option('-t, --type <credentials-type>', 'type of credentials handling in kubeconfig (static or dynamic)')
132
- .option('--output <kubeconfig-path>', 'path of the output file where to write kubeconfig (written to stdout if not specified)')
133
- .hook('preAction', async () => {
134
- await extendCurrentSession();
135
- })
136
- .action(async (gameserver, options) => {
137
- try {
138
- const tokens = await loadTokens();
139
- const stackApi = new StackAPI(tokens.access_token, options.stackApi);
140
- const gameserverId = resolveGameserverId(gameserver, options);
141
- // Default to credentialsType==dynamic for human users, and credentialsType==static for machine users
142
- const isHumanUser = !!tokens.refresh_token;
143
- const credentialsType = options.credentialsType ?? (isHumanUser ? 'dynamic' : 'static');
144
- // Generate kubeconfig
145
- let kubeconfigPayload;
146
- if (credentialsType === 'dynamic') {
147
- logger.debug('Fetching kubeconfig with execcredential');
148
- kubeconfigPayload = await stackApi.getKubeConfigExecCredential(gameserverId);
149
- }
150
- else if (credentialsType === 'static') {
151
- logger.debug('Fetching kubeconfig with embedded secret');
152
- kubeconfigPayload = await stackApi.getKubeConfig(gameserverId);
153
- }
154
- else {
155
- throw new Error('Invalid credentials type; must be either "static" or "dynamic"');
156
- }
157
- // Write kubeconfig to output (file or stdout)
158
- if (options.output) {
159
- logger.debug(`Writing kubeconfig to file ${options.output}`);
160
- await writeFile(options.output, kubeconfigPayload, { mode: 0o600 });
161
- console.log(`Wrote kubeconfig to ${options.output}`);
162
- }
163
- else {
164
- console.log(kubeconfigPayload);
165
- }
166
- }
167
- catch (error) {
168
- if (error instanceof Error) {
169
- console.error('Error getting KubeConfig:', error);
170
- }
171
- exit(1);
172
- }
173
- });
174
- /**
175
- * Get the Kubernetes credentials in the execcredential format which can be used within the `kubeconfig` file:
176
- * The kubeconfig can invoke this command to fetch the Kubernetes credentials just-in-time which allows us to
177
- * generate kubeconfig files that don't contain access tokens and are longer-lived (the authentication is the
178
- * same as that of metaplay-auth itself). Use `metaplay-auth get-kubeconfig -t dynamic ...` to create a
179
- * kubeconfig that uses this command.
180
- */
181
- // todo: maybe this should be a hidden command as it's not very useful for end users and clutters help?
182
- program.command('get-kubernetes-execcredential')
183
- .description('[internal] get kubernetes credentials in execcredential format (used from the generated kubeconfigs)')
184
- .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
185
- .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
186
- .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
187
- .option('-p, --project <project>', 'project name (e.g. idler)')
188
- .option('-e, --environment <environment>', 'environment name (e.g. develop)')
189
- .hook('preAction', async () => {
190
- await extendCurrentSession();
191
- })
192
- .action(async (gameserver, options) => {
193
- try {
194
- const tokens = await loadTokens();
195
- const stackApi = new StackAPI(tokens.access_token, options.stackApi);
196
- const gameserverId = resolveGameserverId(gameserver, options);
197
- const credentials = await stackApi.getKubeExecCredential(gameserverId);
198
- console.log(credentials);
199
- }
200
- catch (error) {
201
- if (error instanceof Error) {
202
- console.error(`Error getting Kubernetes ExecCredential: ${error.message}`);
203
- }
204
- exit(1);
205
- }
206
- });
207
- program.command('get-aws-credentials')
208
- .description('get AWS credentials for target environment')
209
- .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
210
- .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
211
- .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
212
- .option('-p, --project <project>', 'project name (e.g. idler)')
213
- .option('-e, --environment <environment>', 'environment name (e.g. develop)')
214
- .option('-f, --format <format>', 'output format (json or env)', 'json')
215
- .hook('preAction', async () => {
216
- await extendCurrentSession();
217
- })
218
- .action(async (gameserver, options) => {
219
- try {
220
- if (options.format !== 'json' && options.format !== 'env') {
221
- throw new Error('Invalid format; must be one of json or env');
222
- }
223
- const tokens = await loadTokens();
224
- const stackApi = new StackAPI(tokens.access_token, options.stackApi);
225
- const gameserverId = resolveGameserverId(gameserver, options);
226
- const credentials = await stackApi.getAwsCredentials(gameserverId);
227
- if (options.format === 'env') {
228
- console.log(`export AWS_ACCESS_KEY_ID=${credentials.AccessKeyId}`);
229
- console.log(`export AWS_SECRET_ACCESS_KEY=${credentials.SecretAccessKey}`);
230
- console.log(`export AWS_SESSION_TOKEN=${credentials.SessionToken}`);
231
- }
232
- else {
233
- console.log(JSON.stringify({
234
- ...credentials,
235
- Version: 1 // this is needed to comply with `aws` format for external credential providers
236
- }));
237
- }
238
- }
239
- catch (error) {
240
- if (error instanceof Error) {
241
- console.error(`Error getting AWS credentials: ${error.message}`);
242
- }
243
- exit(1);
244
- }
245
- });
246
- program.command('get-docker-login')
247
- .description('get docker login credentials for pushing the server image to target environment')
248
- .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
249
- .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
250
- .option('-p, --project <project>', 'project name (e.g. idler)')
251
- .option('-e, --environment <environment>', 'environment name (e.g. develop)')
252
- .option('-f, --format <format>', 'output format (json or env)', 'json')
253
- .hook('preAction', async () => {
254
- await extendCurrentSession();
255
- })
256
- .action(async (gameserver, options) => {
257
- try {
258
- if (options.format !== 'json' && options.format !== 'env') {
259
- throw new Error('Invalid format; must be one of json or env');
260
- }
261
- const tokens = await loadTokens();
262
- const stackApi = new StackAPI(tokens.access_token, options.stackApi);
263
- const gameserverId = resolveGameserverId(gameserver, options);
264
- // Get environment info (region is needed for ECR)
265
- logger.debug('Get environment info');
266
- const environment = await stackApi.getEnvironmentDetails(gameserverId);
267
- const dockerRepo = environment.deployment.ecr_repo;
268
- // Resolve docker credentials for remote registry
269
- logger.debug('Get docker credentials');
270
- const dockerCredentials = await stackApi.getDockerCredentials(gameserverId);
271
- const { username, password } = dockerCredentials;
272
- // Output the docker repo & credentials
273
- if (options.format === 'env') {
274
- console.log(`export DOCKER_REPO=${dockerRepo}`);
275
- console.log(`export DOCKER_USERNAME=${username}`);
276
- console.log(`export DOCKER_PASSWORD=${password}`);
277
- }
278
- else {
279
- console.log(JSON.stringify({
280
- dockerRepo,
281
- username,
282
- password
283
- }));
284
- }
285
- }
286
- catch (error) {
287
- if (error instanceof Error) {
288
- console.error(`Error getting docker login credentials: ${error.message}`);
289
- }
290
- exit(1);
291
- }
292
- });
293
- program.command('get-environment')
294
- .description('get details of an environment')
295
- .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
296
- .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
297
- .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
298
- .option('-p, --project <project>', 'project name (e.g. idler)')
299
- .option('-e, --environment <environment>', 'environment name (e.g. develop)')
300
- .hook('preAction', async () => {
301
- await extendCurrentSession();
302
- })
303
- .action(async (gameserver, options) => {
304
- try {
305
- const tokens = await loadTokens();
306
- const stackApi = new StackAPI(tokens.access_token, options.stackApi);
307
- const gameserverId = resolveGameserverId(gameserver, options);
308
- const environment = await stackApi.getEnvironmentDetails(gameserverId);
309
- console.log(JSON.stringify(environment));
310
- }
311
- catch (error) {
312
- if (error instanceof Error) {
313
- console.error(`Error getting environment details: ${error.message}`);
314
- }
315
- exit(1);
316
- }
317
- });
318
- program.command('push-docker-image')
319
- .description('push docker image into the target environment image registry')
320
- .argument('gameserver', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
321
- .argument('image-name', 'full name of the docker image to push (eg, the gameserver:<sha>)')
322
- .hook('preAction', async () => {
323
- await extendCurrentSession();
324
- })
325
- .action(async (gameserver, imageName, options) => {
326
- try {
327
- console.log(`Pushing docker image ${imageName} to target environment ${gameserver}...`);
328
- const tokens = await loadTokens();
329
- const stackApi = new StackAPI(tokens.access_token, options.stackApi);
330
- const gameserverId = resolveGameserverId(gameserver, options);
331
- // Get environment info (region is needed for ECR)
332
- logger.debug('Get environment info');
333
- const envInfo = await stackApi.getEnvironmentDetails(gameserverId);
334
- // Resolve docker credentials for remote registry
335
- logger.debug('Get docker credentials');
336
- const dockerCredentials = await stackApi.getDockerCredentials(gameserverId);
337
- // Resolve tag from src image
338
- if (!imageName) {
339
- throw new Error('Must specify a valid docker image name as the image-name argument');
340
- }
341
- const srcImageParts = imageName.split(':');
342
- if (srcImageParts.length !== 2 || srcImageParts[0].length === 0 || srcImageParts[1].length === 0) {
343
- throw new Error(`Invalid docker image name '${imageName}', expecting the name in format 'name:tag'`);
344
- }
345
- const imageTag = srcImageParts[1];
346
- // Resolve source image
347
- const srcImageName = imageName;
348
- const dstRepoName = envInfo.deployment.ecr_repo;
349
- const dstImageName = `${dstRepoName}:${imageTag}`;
350
- const dockerApi = new Docker();
351
- const srcDockerImage = dockerApi.getImage(srcImageName);
352
- // If names don't match, tag the src image as dst
353
- if (srcImageName !== dstImageName) {
354
- logger.debug(`Tagging image ${srcImageName} as ${dstImageName}`);
355
- await srcDockerImage.tag({ repo: dstRepoName, tag: imageTag });
356
- }
357
- // Push the image
358
- logger.debug(`Push image ${dstImageName}`);
359
- const dstDockerImage = dockerApi.getImage(dstImageName);
360
- const authConfig = { username: dockerCredentials.username, password: dockerCredentials.password, serveraddress: dockerCredentials.registryUrl };
361
- const pushStream = await dstDockerImage.push({ authconfig: authConfig, tag: options.imageTag });
362
- // Follow push progress & wait until completed
363
- logger.debug('Following image push stream...');
364
- await new Promise((resolve, reject) => {
365
- dockerApi.modem.followProgress(pushStream, (error, result) => {
366
- if (error) {
367
- logger.debug('Failed to push image:', error);
368
- reject(error);
369
- }
370
- else {
371
- // result contains an array of all the progress objects
372
- logger.debug('Succesfully finished pushing image');
373
- resolve(result);
374
- }
375
- }, (obj) => {
376
- // console.log('Progress:', obj)
377
- // { status: 'Preparing', progressDetail: {}, id: '82730adcaeb0' }
378
- // { status: 'Layer already exists', progressDetail: {}, id: '7cd701fff13a' }
379
- });
380
- });
381
- console.log(`Successfully pushed docker image to ${dstImageName}!`);
382
- }
383
- catch (error) {
384
- if (error instanceof Error) {
385
- console.error(`Failed to push docker image: ${error.message}`);
386
- }
387
- exit(1);
388
- }
389
- });
390
- program.command('deploy-server')
391
- .description('deploy a game server image to target environment')
392
- .argument('gameserver', 'address of gameserver (e.g. idler-develop.p1.metaplay.io)')
393
- .argument('image-tag', 'docker image tag to deploy (usually the SHA of the build)')
394
- .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
395
- .option('-f, --values <path-to-values-file>', 'path to Helm values file to use for this deployment')
396
- .option('--local-chart-path <path-to-chart-directory>', 'path to local Helm chart directory (to use a chart from local disk)')
397
- .option('--helm-chart-repo <url>', 'override the URL of the Helm chart repository (eg, https://charts.metaplay.dev/testing)')
398
- .option('--helm-chart-version <version>', 'override the Helm chart version (eg, 0.6.0)')
399
- .option('--deployment-name', 'Helm deployment name to use', 'gameserver')
400
- .hook('preAction', async () => {
401
- await extendCurrentSession();
402
- })
403
- .action(async (gameserver, imageTag, options) => {
404
- try {
405
- const tokens = await loadTokens();
406
- const stackApi = new StackAPI(tokens.access_token, options.stackApi);
407
- const gameserverId = resolveGameserverId(gameserver, options);
408
- console.log(`Deploying server to ${gameserver} with image tag ${imageTag}...`);
409
- // Fetch target environment details
410
- const envInfo = await stackApi.getEnvironmentDetails(gameserverId);
411
- if (!imageTag) {
412
- throw new Error('Must specify a valid docker image tag as the image-tag argument, usually the SHA of the build');
413
- }
414
- if (!options.deploymentName) {
415
- throw new Error(`Invalid Helm deployment name '${options.deploymentName}'; specify one with --deployment-name or use the default`);
416
- }
417
- // Fetch Docker credentials for target environment registry
418
- const dockerCredentials = await stackApi.getDockerCredentials(gameserverId);
419
- // Resolve information about docker image
420
- // const dockerApi = new Docker({
421
- // host: dockerCredentials.registryUrl,
422
- // port: 443,
423
- // protocol: 'https',
424
- // username: dockerCredentials.username,
425
- // headers: {
426
- // Authorization: `Bearer ${dockerCredentials.password}`,
427
- // Host: dockerCredentials.registryUrl.replace('https://', ''),
428
- // }
429
- // })
430
- const dockerApi = new Docker();
431
- const dockerRepo = envInfo.deployment.ecr_repo;
432
- const imageName = `${dockerRepo}:${imageTag}`;
433
- logger.debug(`Fetch docker image labels for ${imageName}`);
434
- let imageLabels;
435
- try {
436
- const localDockerImage = dockerApi.getImage(imageName);
437
- imageLabels = (await localDockerImage.inspect()).Config.Labels || {};
438
- }
439
- catch (err) {
440
- logger.debug(`Failed to resolve docker image metadata from local image ${imageName}: ${err}`);
441
- }
442
- // If wasn't able to resolve the images, pull image from the target environment registry & resolve labels
443
- if (imageLabels === undefined) {
444
- // Pull the image from remote registry
445
- try {
446
- console.log(`Image ${imageName} not found locally -- pulling docker image from target environment registry...`);
447
- const authConfig = { username: dockerCredentials.username, password: dockerCredentials.password, serveraddress: dockerCredentials.registryUrl };
448
- const pullStream = await dockerApi.pull(imageName, { authconfig: authConfig });
449
- // Follow pull progress & wait until completed
450
- logger.debug('Following image pull stream...');
451
- await new Promise((resolve, reject) => {
452
- dockerApi.modem.followProgress(pullStream, (error, result) => {
453
- if (error) {
454
- logger.debug('Failed to pull image:', error);
455
- reject(error);
456
- }
457
- else {
458
- // result contains an array of all the progress objects
459
- logger.debug('Succesfully finished pulling image');
460
- resolve(result);
461
- }
462
- }, (obj) => {
463
- // console.log('Progress:', obj)
464
- // { status: 'Preparing', progressDetail: {}, id: '82730adcaeb0' }
465
- // { status: 'Layer already exists', progressDetail: {}, id: '7cd701fff13a' }
466
- });
467
- });
468
- }
469
- catch (err) {
470
- throw new Error(`Failed to fetch docker image ${imageName} from target environment registry: ${err}`);
471
- }
472
- // Resolve the labels
473
- try {
474
- logger.debug('Get docker labels again (with pulled image)');
475
- const localDockerImage = dockerApi.getImage(imageName);
476
- imageLabels = (await localDockerImage.inspect()).Config.Labels || {};
477
- }
478
- catch (err) {
479
- throw new Error(`Failed to resolve docker image metadata from pulled image ${imageName}: ${err}`);
480
- }
481
- }
482
- // Try to resolve SDK version and Helm repo and chart version from the docker labels
483
- // \note These only exist for SDK R28 and newer images
484
- logger.debug('Docker image labels: ', JSON.stringify(imageLabels));
485
- const sdkVersion = imageLabels['io.metaplay.sdk_version'];
486
- // Resolve helmChartRepo, in order of precedence:
487
- // - Specified in the cli options
488
- // - Specified in the docker image label
489
- // - Fall back to 'https://charts.metaplay.dev'
490
- const helmChartRepo = options.helmChartRepo ?? imageLabels['io.metaplay.default_helm_repo'] ?? 'https://charts.metaplay.dev';
491
- // Resolve helmChartVersion, in order of precedence:
492
- // - Specified in the cli options
493
- // - Specified in the docker image label
494
- // - Unknown, error out!
495
- const helmChartVersion = options.helmChartVersion ?? imageLabels['io.metaplay.default_server_chart_version'];
496
- if (!helmChartVersion) {
497
- throw new Error('No Helm chart version defined. With pre-R28 SDK versions, you must specify the Helm chart repository explicitly with --helm-chart-version.');
498
- }
499
- // A values file is required (at least for now it makes no sense to deploy without one)
500
- if (!options.values) {
501
- throw new Error('Path to a Helm values file must be specified with --values');
502
- }
503
- // \If Helm chart >= 0.7.0, check that sdkVersion is defined in docker image labels (the labels were added in R28)
504
- const helmChartSemver = semver.parse(helmChartVersion);
505
- if (!helmChartSemver) {
506
- throw new Error(`Resolve Helm chart version '${helmChartVersion}' is not a valid SemVer!`);
507
- }
508
- if (semver.gte(helmChartSemver, new semver.SemVer('0.7.0'), true) && !sdkVersion) {
509
- throw new Error('Helm chart versions >=0.7.0 are only compatible with SDK versions 28.0.0 and above.');
510
- }
511
- // Fetch kubeconfig and write it to a temporary file
512
- // \todo allow passing a custom kubeconfig file?
513
- const kubeconfigPayload = await stackApi.getKubeConfig(gameserverId);
514
- const kubeconfigPath = join(tmpdir(), randomBytes(20).toString('hex'));
515
- logger.debug(`Write temporary kubeconfig in ${kubeconfigPath}`);
516
- await writeFile(kubeconfigPath, kubeconfigPayload, { mode: 0o600 });
517
- try {
518
- // Construct Helm invocation
519
- const chartNameOrPath = options.localChartPath ?? 'metaplay-gameserver';
520
- const helmArgs = ['upgrade', '--install', '--wait'] // \note wait for the pods to stabilize -- otherwise status check can read state before any changes to pods are applied
521
- .concat(['--kubeconfig', kubeconfigPath])
522
- .concat(['-n', envInfo.deployment.kubernetes_namespace])
523
- .concat(['--values', options.values])
524
- .concat(['--set-string', `image.tag=${imageTag}`])
525
- .concat(sdkVersion ? ['--set-string', `sdk.version=${sdkVersion}`] : [])
526
- .concat(!options.chartPath ? ['--repo', helmChartRepo, '--version', helmChartVersion] : [])
527
- .concat([options.deploymentName])
528
- .concat([chartNameOrPath]);
529
- logger.info(`Execute: helm ${helmArgs.join(' ')}`);
530
- // Execute Helm
531
- let helmResult;
532
- try {
533
- helmResult = await executeCommand('helm', helmArgs);
534
- // \todo output something from Helm result?
535
- }
536
- catch (err) {
537
- throw new Error(`Failed to execute 'helm': ${err}. You need to have Helm v3 installed to deploy a game server with metaplay-auth.`);
538
- }
539
- // Throw on Helm non-success exit code
540
- if (helmResult.code !== 0) {
541
- throw new Error(`Helm deploy failed with exit code ${helmResult.code}: ${helmResult.stderr}`);
542
- }
543
- const testingRepoSuffix = (!options.chartPath && helmChartRepo !== 'https://charts.metaplay.dev') ? ` from repo ${helmChartRepo}` : '';
544
- console.log(`Game server deployed to ${gameserver} with tag ${imageTag} using chart version ${helmChartVersion}${testingRepoSuffix}!`);
545
- }
546
- finally {
547
- // Remove temporary kubeconfig file
548
- await unlink(kubeconfigPath);
549
- }
550
- // Check the status of the game server deployment
551
- try {
552
- const kubeconfig = new KubeConfig();
553
- kubeconfig.loadFromString(kubeconfigPayload);
554
- console.log('Validating game server deployment...');
555
- const exitCode = await checkGameServerDeployment(envInfo.deployment.kubernetes_namespace, kubeconfig);
556
- exit(exitCode);
557
- }
558
- catch (error) {
559
- console.error(`Failed to resolve game server deployment status: ${error}`);
560
- exit(2);
561
- }
562
- }
563
- catch (error) {
564
- if (error instanceof Error) {
565
- console.error(`Error deploying game server into target environment: ${error.message}`);
566
- }
567
- exit(1);
568
- }
569
- });
570
- program.command('check-server-status')
571
- .description('check the status of a deployed server and print out information that is helpful in debugging failed deployments')
572
- .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
573
- .hook('preAction', async () => {
574
- await extendCurrentSession();
575
- })
576
- .action(async (gameserver, options) => {
577
- const tokens = await loadTokens();
578
- const stackApi = new StackAPI(tokens.access_token, options.stackApi);
579
- const gameserverId = resolveGameserverId(gameserver, options);
580
- try {
581
- logger.debug('Get environment info');
582
- const envInfo = await stackApi.getEnvironmentDetails(gameserverId);
583
- const kubernetesNamespace = envInfo.deployment.kubernetes_namespace;
584
- // Load kubeconfig from file and throw error if validation fails.
585
- logger.debug('Get kubeconfig');
586
- const kubeconfig = new KubeConfig();
587
- try {
588
- // Fetch kubeconfig and write it to a temporary file
589
- // \todo allow passing a custom kubeconfig file?
590
- const kubeconfigPayload = await stackApi.getKubeConfig(gameserverId);
591
- kubeconfig.loadFromString(kubeconfigPayload);
592
- }
593
- catch (error) {
594
- throw new Error(`Failed to load or validate kubeconfig. ${error}`);
595
- }
596
- // Run the checks and exit with success/failure exitCode depending on result
597
- console.log(`Validating game server deployment in namespace ${kubernetesNamespace}`);
598
- const exitCode = await checkGameServerDeployment(kubernetesNamespace, kubeconfig);
599
- exit(exitCode);
600
- }
601
- catch (error) {
602
- console.error(`Failed to check deployment status: ${error.message}`);
603
- exit(1);
604
- }
605
- });
606
- program.command('check-deployment')
607
- .description('[deprecated] check that a game server was successfully deployed, or print out useful error messages in case of failure')
608
- .argument('[namespace]', 'kubernetes namespace of the deployment')
609
- .action(async (namespace) => {
610
- console.error('DEPRECATED! Use the "metaplay-auth check-server-status [gameserver]" command instead! This command will be removed soon.');
611
- try {
612
- if (!namespace) {
613
- throw new Error('Must specify value for argument "namespace"');
614
- }
615
- // Check that the KUBECONFIG environment variable exists
616
- const kubeconfigPath = process.env.KUBECONFIG;
617
- if (!kubeconfigPath) {
618
- throw new Error('The KUBECONFIG environment variable must be specified');
619
- }
620
- // Check that the kubeconfig file exists
621
- if (!existsSync(kubeconfigPath)) {
622
- throw new Error(`The environment variable KUBECONFIG points to a file '${kubeconfigPath}' that doesn't exist`);
623
- }
624
- // Create Kubernetes API instance (with default kubeconfig)
625
- const kubeconfig = new KubeConfig();
626
- // Load kubeconfig from file and throw error if validation fails.
627
- try {
628
- kubeconfig.loadFromFile(kubeconfigPath);
629
- }
630
- catch (error) {
631
- throw new Error(`Failed to load or validate kubeconfig. ${error}`);
632
- }
633
- // Run the checks and exit with success/failure exitCode depending on result
634
- console.log(`Validating game server deployment in namespace ${namespace}`);
635
- const exitCode = await checkGameServerDeployment(namespace, kubeconfig);
636
- exit(exitCode);
637
- }
638
- catch (error) {
639
- console.error(`Failed to check deployment status: ${error.message}`);
640
- exit(1);
641
- }
642
- });
643
- void program.parseAsync();
644
- //# sourceMappingURL=index.js.map