@metaplay/metaplay-auth 1.4.2 → 1.5.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.
Binary file
package/index.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander'
3
3
  import Docker from 'dockerode'
4
- import { loginAndSaveTokens, machineLoginAndSaveTokens, extendCurrentSession, loadTokens, removeTokens } from './src/auth.js'
5
- import { StackAPI, type GameserverId } from './src/stackapi.js'
6
- import { checkGameServerDeployment } from './src/deployment.js'
4
+ import { loginAndSaveTokens, machineLoginAndSaveTokens, extendCurrentSession, loadTokens, removeTokens, TokenSet } from './src/auth.js'
5
+ import { StackAPI, defaultStackApiBaseUrl } from './src/stackapi.js'
6
+ import { checkGameServerDeployment, debugGameServer } from './src/deployment.js'
7
7
  import { logger, setLogLevel } from './src/logging.js'
8
- import { isValidFQDN, executeCommand } from './src/utils.js'
8
+ import { isValidFQDN, executeCommand, getGameserverAdminUrl } from './src/utils.js'
9
+ import { TargetEnvironment } from './src/targetenvironment.js'
9
10
  import { exit } from 'process'
10
11
  import { tmpdir } from 'os'
11
12
  import { join } from 'path'
@@ -14,28 +15,66 @@ import { writeFile, unlink } from 'fs/promises'
14
15
  import { existsSync } from 'fs'
15
16
  import { KubeConfig } from '@kubernetes/client-node'
16
17
  import * as semver from 'semver'
18
+ import { PACKAGE_VERSION } from './src/version.js'
19
+
20
+ function resolveTargetEnvironmentFromFQDN (tokens: TokenSet, environmentDomain: string): TargetEnvironment {
21
+ const adminApiDomain = getGameserverAdminUrl(environmentDomain)
22
+ // \todo using the old <environment>/.infra path as v1 API needs organization, project, environment which we don't have with FQDN environments
23
+ const environmentApiBaseUrl = `https://${adminApiDomain}/.infra`
24
+
25
+ return new TargetEnvironment(
26
+ tokens.access_token,
27
+ environmentDomain,
28
+ defaultStackApiBaseUrl,
29
+ environmentApiBaseUrl)
30
+ }
31
+
32
+ async function resolveTargetEnvironmentFromTuple (tokens: TokenSet, organization: string, project: string, environment: string, stackApiUrl?: string): Promise<TargetEnvironment> {
33
+ // \todo Later on, fetch the environment information from the portal (not StackAPI), including the stack and its stackApiBaseUrl
34
+ const stackApiBaseUrl = stackApiUrl ?? defaultStackApiBaseUrl
35
+ const stackApi = new StackAPI(tokens.access_token, stackApiBaseUrl)
36
+ const environmentDomain = await stackApi.resolveManagedEnvironmentFQDN(organization, project, environment)
37
+ const environmentApiBaseUrl = `${stackApiBaseUrl}/v1/servers/${organization}/${project}/${environment}`
38
+
39
+ return new TargetEnvironment(
40
+ tokens.access_token,
41
+ environmentDomain,
42
+ stackApiBaseUrl,
43
+ environmentApiBaseUrl,
44
+ organization,
45
+ project,
46
+ environment)
47
+ }
17
48
 
18
49
  /**
19
- * Helper for parsing the GameserverId type from the command line arguments. Accepts either the gameserver address
50
+ * Helper for parsing the TargetEnvironment type from the command line arguments. Accepts either the gameserver address
20
51
  * (idler-test.p1.metaplay.io), a shorthand address (metaplay-idler-test) or the (organization, project, environment)
21
52
  * tuple from options.
22
53
  */
23
- function resolveGameserverId (address: string | undefined, options: any): GameserverId {
54
+ async function resolveTargetEnvironment (address: string | undefined, options: { organization?: string, project?: string, environment?: string, stackApi?: string }): Promise<TargetEnvironment> {
55
+ const tokens = await loadTokens()
56
+
24
57
  // If address is specified, use it, otherwise assume options has organization, project, and environment
25
58
  if (address) {
59
+ // Address is either FQDN or 'organization-project-environment' tuple
26
60
  if (isValidFQDN(address)) {
27
- return { gameserver: address }
61
+ if (options.stackApi) {
62
+ throw new Error('--stack-api override only supported with organization-project-environment naming, not with FQDN')
63
+ }
64
+ return resolveTargetEnvironmentFromFQDN(tokens, address)
28
65
  } else {
29
66
  const parts = address.split('-')
30
67
  if (parts.length !== 3) {
31
68
  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
69
  }
33
- return { organization: parts[0], project: parts[1], environment: parts[2] }
70
+ return await resolveTargetEnvironmentFromTuple(tokens, parts[0], parts[1], parts[2], options.stackApi)
34
71
  }
35
72
  } else if (options.organization && options.project && options.environment) {
36
- return { organization: options.organization, project: options.project, environment: options.environment }
73
+ // Parse tuple from command-line options (output to stderr to avoid messing up '$(eval metaplay-auth ... --format env)' invocations
74
+ console.warn(`Warning: Specifying the target environment with -o (--organization), -p (--project), and -e (--environment) is deprecated! Use the '${options.organization}-${options.project}-${options.environment}' syntax instead.`)
75
+ return await resolveTargetEnvironmentFromTuple(tokens, options.organization, options.project, options.environment, options.stackApi)
37
76
  } else {
38
- 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.')
77
+ throw new Error('Could not determine target environment from arguments: You need to specify either an environment FQDN or an organization, project, and environment. Run this command with --help flag for more information.')
39
78
  }
40
79
  }
41
80
 
@@ -44,7 +83,7 @@ const program = new Command()
44
83
  program
45
84
  .name('metaplay-auth')
46
85
  .description('Authenticate with Metaplay and get AWS and Kubernetes credentials for game servers.')
47
- .version('1.4.2')
86
+ .version(PACKAGE_VERSION)
48
87
  .option('-d, --debug', 'enable debug output')
49
88
  .hook('preAction', (thisCommand) => {
50
89
  // Handle debug flag for all commands.
@@ -67,11 +106,12 @@ program.command('machine-login')
67
106
  .option('--dev-credentials', 'machine user credentials to use, only for dev purposes, use METAPLAY_CREDENTIALS env variable for better safety!')
68
107
  .action(async (options) => {
69
108
  // Get credentials from command line or from METAPLAY_CREDENTIALS environment variable
70
- let credentials
109
+ let credentials: string
71
110
  if (options.devCredentials) {
72
111
  credentials = options.devCredentials
73
112
  } else {
74
- credentials = process.env.METAPLAY_CREDENTIALS
113
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
114
+ credentials = process.env.METAPLAY_CREDENTIALS!
75
115
  if (!credentials || credentials === '') {
76
116
  throw new Error('Unable to find the credentials, the environment variable METAPLAY_CREDENTIALS is not defined!')
77
117
  }
@@ -116,7 +156,7 @@ program.command('show-tokens')
116
156
  try {
117
157
  // TODO: Could detect if not logged in and fail more gracefully?
118
158
  const tokens = await loadTokens()
119
- console.log(tokens)
159
+ console.log(JSON.stringify(tokens, undefined, 2))
120
160
  } catch (error) {
121
161
  if (error instanceof Error) {
122
162
  console.error(`Error showing tokens: ${error.message}`)
@@ -129,32 +169,31 @@ program.command('get-kubeconfig')
129
169
  .description('get kubeconfig for target environment')
130
170
  .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
131
171
  .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
132
- .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
133
- .option('-p, --project <project>', 'project name (e.g. idler)')
134
- .option('-e, --environment <environment>', 'environment name (e.g. develop)')
172
+ .option('-o, --organization <organization>', '[deprecated] organization name (e.g. metaplay)')
173
+ .option('-p, --project <project>', '[deprecated] project name (e.g. idler)')
174
+ .option('-e, --environment <environment>', '[deprecated] environment name (e.g. develop)')
135
175
  .option('-t, --type <credentials-type>', 'type of credentials handling in kubeconfig (static or dynamic)')
136
176
  .option('--output <kubeconfig-path>', 'path of the output file where to write kubeconfig (written to stdout if not specified)')
137
177
  .hook('preAction', async () => {
138
178
  await extendCurrentSession()
139
179
  })
140
- .action(async (gameserver, options) => {
180
+ .action(async (gameserver: string | undefined, options: { stackApi?: string, organization?: string, project?: string, environment?: string, type?: string, output?: string }) => {
141
181
  try {
142
- const tokens = await loadTokens()
143
- const stackApi = new StackAPI(tokens.access_token, options.stackApi)
144
- const gameserverId = resolveGameserverId(gameserver, options)
182
+ const targetEnv = await resolveTargetEnvironment(gameserver, options)
145
183
 
146
184
  // Default to credentialsType==dynamic for human users, and credentialsType==static for machine users
185
+ const tokens = await loadTokens()
147
186
  const isHumanUser = !!tokens.refresh_token
148
- const credentialsType = options.credentialsType ?? (isHumanUser ? 'dynamic' : 'static')
187
+ const credentialsType = options.type ?? (isHumanUser ? 'dynamic' : 'static')
149
188
 
150
189
  // Generate kubeconfig
151
190
  let kubeconfigPayload
152
191
  if (credentialsType === 'dynamic') {
153
192
  logger.debug('Fetching kubeconfig with execcredential')
154
- kubeconfigPayload = await stackApi.getKubeConfigExecCredential(gameserverId)
193
+ kubeconfigPayload = await targetEnv.getKubeConfigExecCredential()
155
194
  } else if (credentialsType === 'static') {
156
195
  logger.debug('Fetching kubeconfig with embedded secret')
157
- kubeconfigPayload = await stackApi.getKubeConfig(gameserverId)
196
+ kubeconfigPayload = await targetEnv.getKubeConfig()
158
197
  } else {
159
198
  throw new Error('Invalid credentials type; must be either "static" or "dynamic"')
160
199
  }
@@ -187,19 +226,16 @@ program.command('get-kubernetes-execcredential')
187
226
  .description('[internal] get kubernetes credentials in execcredential format (used from the generated kubeconfigs)')
188
227
  .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
189
228
  .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
190
- .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
191
- .option('-p, --project <project>', 'project name (e.g. idler)')
192
- .option('-e, --environment <environment>', 'environment name (e.g. develop)')
229
+ .option('-o, --organization <organization>', '[deprecated] organization name (e.g. metaplay)')
230
+ .option('-p, --project <project>', '[deprecated] project name (e.g. idler)')
231
+ .option('-e, --environment <environment>', '[deprecated] environment name (e.g. develop)')
193
232
  .hook('preAction', async () => {
194
233
  await extendCurrentSession()
195
234
  })
196
- .action(async (gameserver, options) => {
235
+ .action(async (gameserver: string | undefined, options: { stackApi?: string, organization?: string, project?: string, environment?: string }) => {
197
236
  try {
198
- const tokens = await loadTokens()
199
- const stackApi = new StackAPI(tokens.access_token, options.stackApi)
200
- const gameserverId = resolveGameserverId(gameserver, options)
201
-
202
- const credentials = await stackApi.getKubeExecCredential(gameserverId)
237
+ const targetEnv = await resolveTargetEnvironment(gameserver, options)
238
+ const credentials = await targetEnv.getKubeExecCredential()
203
239
  console.log(credentials)
204
240
  } catch (error) {
205
241
  if (error instanceof Error) {
@@ -213,24 +249,23 @@ program.command('get-aws-credentials')
213
249
  .description('get AWS credentials for target environment')
214
250
  .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
215
251
  .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
216
- .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
217
- .option('-p, --project <project>', 'project name (e.g. idler)')
218
- .option('-e, --environment <environment>', 'environment name (e.g. develop)')
252
+ .option('-o, --organization <organization>', '[deprecated] organization name (e.g. metaplay)')
253
+ .option('-p, --project <project>', '[deprecated] project name (e.g. idler)')
254
+ .option('-e, --environment <environment>', '[deprecated] environment name (e.g. develop)')
219
255
  .option('-f, --format <format>', 'output format (json or env)', 'json')
220
256
  .hook('preAction', async () => {
221
257
  await extendCurrentSession()
222
258
  })
223
- .action(async (gameserver, options) => {
259
+ .action(async (gameserver: string | undefined, options: { stackApi?: string, organization?: string, project?: string, environment?: string, format?: string }) => {
224
260
  try {
225
261
  if (options.format !== 'json' && options.format !== 'env') {
226
262
  throw new Error('Invalid format; must be one of json or env')
227
263
  }
228
264
 
229
- const tokens = await loadTokens()
230
- const stackApi = new StackAPI(tokens.access_token, options.stackApi)
231
- const gameserverId = resolveGameserverId(gameserver, options)
265
+ const targetEnv = await resolveTargetEnvironment(gameserver, options)
232
266
 
233
- const credentials = await stackApi.getAwsCredentials(gameserverId)
267
+ // Get the AWS credentials
268
+ const credentials = await targetEnv.getAwsCredentials()
234
269
 
235
270
  if (options.format === 'env') {
236
271
  console.log(`export AWS_ACCESS_KEY_ID=${credentials.AccessKeyId}`)
@@ -251,33 +286,34 @@ program.command('get-aws-credentials')
251
286
  })
252
287
 
253
288
  program.command('get-docker-login')
254
- .description('get docker login credentials for pushing the server image to target environment')
289
+ .description('[deprecated] get docker login credentials for pushing the server image to target environment')
255
290
  .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
256
- .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
257
- .option('-p, --project <project>', 'project name (e.g. idler)')
258
- .option('-e, --environment <environment>', 'environment name (e.g. develop)')
291
+ .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
292
+ .option('-o, --organization <organization>', '[deprecated] organization name (e.g. metaplay)')
293
+ .option('-p, --project <project>', '[deprecated] project name (e.g. idler)')
294
+ .option('-e, --environment <environment>', '[deprecated] environment name (e.g. develop)')
259
295
  .option('-f, --format <format>', 'output format (json or env)', 'json')
260
296
  .hook('preAction', async () => {
261
297
  await extendCurrentSession()
262
298
  })
263
- .action(async (gameserver, options) => {
299
+ .action(async (gameserver: string | undefined, options: { stackApi?: string, organization?: string, project?: string, environment?: string, format?: string }) => {
264
300
  try {
265
301
  if (options.format !== 'json' && options.format !== 'env') {
266
302
  throw new Error('Invalid format; must be one of json or env')
267
303
  }
268
304
 
269
- const tokens = await loadTokens()
270
- const stackApi = new StackAPI(tokens.access_token, options.stackApi)
271
- const gameserverId = resolveGameserverId(gameserver, options)
305
+ console.warn('The get-docker-login command is deprecated! Use the push-docker-image command instead.')
306
+
307
+ const targetEnv = await resolveTargetEnvironment(gameserver, options)
272
308
 
273
309
  // Get environment info (region is needed for ECR)
274
310
  logger.debug('Get environment info')
275
- const environment = await stackApi.getEnvironmentDetails(gameserverId)
311
+ const environment = await targetEnv.getEnvironmentDetails()
276
312
  const dockerRepo = environment.deployment.ecr_repo
277
313
 
278
314
  // Resolve docker credentials for remote registry
279
315
  logger.debug('Get docker credentials')
280
- const dockerCredentials = await stackApi.getDockerCredentials(gameserverId)
316
+ const dockerCredentials = await targetEnv.getDockerCredentials()
281
317
  const { username, password } = dockerCredentials
282
318
 
283
319
  // Output the docker repo & credentials
@@ -304,20 +340,18 @@ program.command('get-environment')
304
340
  .description('get details of an environment')
305
341
  .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
306
342
  .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
307
- .option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
308
- .option('-p, --project <project>', 'project name (e.g. idler)')
309
- .option('-e, --environment <environment>', 'environment name (e.g. develop)')
343
+ .option('-o, --organization <organization>', '[deprecated] organization name (e.g. metaplay)')
344
+ .option('-p, --project <project>', '[deprecated] project name (e.g. idler)')
345
+ .option('-e, --environment <environment>', '[deprecated] environment name (e.g. develop)')
310
346
  .hook('preAction', async () => {
311
347
  await extendCurrentSession()
312
348
  })
313
- .action(async (gameserver, options) => {
349
+ .action(async (gameserver: string | undefined, options: { stackApi?: string, organization?: string, project?: string, environment?: string }) => {
314
350
  try {
315
- const tokens = await loadTokens()
316
- const stackApi = new StackAPI(tokens.access_token, options.stackApi)
317
- const gameserverId = resolveGameserverId(gameserver, options)
351
+ const targetEnv = await resolveTargetEnvironment(gameserver, options)
318
352
 
319
- const environment = await stackApi.getEnvironmentDetails(gameserverId)
320
- console.log(JSON.stringify(environment))
353
+ const environment = await targetEnv.getEnvironmentDetails()
354
+ console.log(JSON.stringify(environment, undefined, 2))
321
355
  } catch (error) {
322
356
  if (error instanceof Error) {
323
357
  console.error(`Error getting environment details: ${error.message}`)
@@ -330,24 +364,23 @@ program.command('push-docker-image')
330
364
  .description('push docker image into the target environment image registry')
331
365
  .argument('gameserver', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
332
366
  .argument('image-name', 'full name of the docker image to push (eg, the gameserver:<sha>)')
367
+ .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
333
368
  .hook('preAction', async () => {
334
369
  await extendCurrentSession()
335
370
  })
336
- .action(async (gameserver, imageName, options) => {
371
+ .action(async (gameserver: string | undefined, imageName: string | undefined, options: { stackApi?: string, imageTag?: string }) => {
337
372
  try {
338
373
  console.log(`Pushing docker image ${imageName} to target environment ${gameserver}...`)
339
374
 
340
- const tokens = await loadTokens()
341
- const stackApi = new StackAPI(tokens.access_token, options.stackApi)
342
- const gameserverId = resolveGameserverId(gameserver, options)
375
+ const targetEnv = await resolveTargetEnvironment(gameserver, options)
343
376
 
344
377
  // Get environment info (region is needed for ECR)
345
378
  logger.debug('Get environment info')
346
- const envInfo = await stackApi.getEnvironmentDetails(gameserverId)
379
+ const envInfo = await targetEnv.getEnvironmentDetails()
347
380
 
348
381
  // Resolve docker credentials for remote registry
349
382
  logger.debug('Get docker credentials')
350
- const dockerCredentials = await stackApi.getDockerCredentials(gameserverId)
383
+ const dockerCredentials = await targetEnv.getDockerCredentials()
351
384
 
352
385
  // Resolve tag from src image
353
386
  if (!imageName) {
@@ -385,11 +418,11 @@ program.command('push-docker-image')
385
418
  pushStream,
386
419
  (error: Error | null, result: any[]) => {
387
420
  if (error) {
388
- logger.debug('Failed to push image:', error)
421
+ logger.debug('Failed to push docker image to target repository:', error)
389
422
  reject(error)
390
423
  } else {
391
424
  // result contains an array of all the progress objects
392
- logger.debug('Succesfully finished pushing image')
425
+ logger.debug('Succesfully finished pushing docker image')
393
426
  resolve(result)
394
427
  }
395
428
  },
@@ -411,10 +444,10 @@ program.command('push-docker-image')
411
444
 
412
445
  program.command('deploy-server')
413
446
  .description('deploy a game server image to target environment')
414
- .argument('gameserver', 'address of gameserver (e.g. idler-develop.p1.metaplay.io)')
447
+ .argument('gameserver', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
415
448
  .argument('image-tag', 'docker image tag to deploy (usually the SHA of the build)')
449
+ .requiredOption('-f, --values <path-to-values-file>', 'path to Helm values file to use for this deployment')
416
450
  .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
417
- .option('-f, --values <path-to-values-file>', 'path to Helm values file to use for this deployment')
418
451
  .option('--local-chart-path <path-to-chart-directory>', 'path to local Helm chart directory (to use a chart from local disk)')
419
452
  .option('--helm-chart-repo <url>', 'override the URL of the Helm chart repository (eg, https://charts.metaplay.dev/testing)')
420
453
  .option('--helm-chart-version <version>', 'override the Helm chart version (eg, 0.6.0)')
@@ -422,27 +455,26 @@ program.command('deploy-server')
422
455
  .hook('preAction', async () => {
423
456
  await extendCurrentSession()
424
457
  })
425
- .action(async (gameserver, imageTag, options) => {
458
+ .action(async (gameserver: string | undefined, imageTag: string | undefined, options: { values: string, stackApi?: string, localChartPath?: string, helmChartRepo?: string, helmChartVersion?: string, deploymentName?: string }) => {
426
459
  try {
427
- const tokens = await loadTokens()
428
- const stackApi = new StackAPI(tokens.access_token, options.stackApi)
429
- const gameserverId = resolveGameserverId(gameserver, options)
460
+ const targetEnv = await resolveTargetEnvironment(gameserver, options)
430
461
 
431
462
  console.log(`Deploying server to ${gameserver} with image tag ${imageTag}...`)
432
463
 
433
464
  // Fetch target environment details
434
- const envInfo = await stackApi.getEnvironmentDetails(gameserverId)
465
+ const envInfo = await targetEnv.getEnvironmentDetails()
435
466
 
436
467
  if (!imageTag) {
437
468
  throw new Error('Must specify a valid docker image tag as the image-tag argument, usually the SHA of the build')
438
469
  }
470
+ // \todo validate that imageTag is just the version part (i.e, contains no ':')
439
471
 
440
472
  if (!options.deploymentName) {
441
473
  throw new Error(`Invalid Helm deployment name '${options.deploymentName}'; specify one with --deployment-name or use the default`)
442
474
  }
443
475
 
444
476
  // Fetch Docker credentials for target environment registry
445
- const dockerCredentials = await stackApi.getDockerCredentials(gameserverId)
477
+ const dockerCredentials = await targetEnv.getDockerCredentials()
446
478
 
447
479
  // Resolve information about docker image
448
480
  // const dockerApi = new Docker({
@@ -473,7 +505,7 @@ program.command('deploy-server')
473
505
  try {
474
506
  console.log(`Image ${imageName} not found locally -- pulling docker image from target environment registry...`)
475
507
  const authConfig = { username: dockerCredentials.username, password: dockerCredentials.password, serveraddress: dockerCredentials.registryUrl }
476
- const pullStream = await dockerApi.pull(imageName, { authconfig: authConfig })
508
+ const pullStream = (await dockerApi.pull(imageName, { authconfig: authConfig }))
477
509
 
478
510
  // Follow pull progress & wait until completed
479
511
  logger.debug('Following image pull stream...')
@@ -527,7 +559,7 @@ program.command('deploy-server')
527
559
  // - Unknown, error out!
528
560
  const helmChartVersion = options.helmChartVersion ?? imageLabels['io.metaplay.default_server_chart_version']
529
561
  if (!helmChartVersion) {
530
- 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.')
562
+ throw new Error('No Helm chart version defined. With pre-R28 SDK versions, you must specify the Helm chart version explicitly with --helm-chart-version=<version>.')
531
563
  }
532
564
 
533
565
  // A values file is required (at least for now it makes no sense to deploy without one)
@@ -546,7 +578,7 @@ program.command('deploy-server')
546
578
 
547
579
  // Fetch kubeconfig and write it to a temporary file
548
580
  // \todo allow passing a custom kubeconfig file?
549
- const kubeconfigPayload = await stackApi.getKubeConfig(gameserverId)
581
+ const kubeconfigPayload = await targetEnv.getKubeConfig()
550
582
  const kubeconfigPath = join(tmpdir(), randomBytes(20).toString('hex'))
551
583
  logger.debug(`Write temporary kubeconfig in ${kubeconfigPath}`)
552
584
  await writeFile(kubeconfigPath, kubeconfigPayload, { mode: 0o600 })
@@ -561,7 +593,7 @@ program.command('deploy-server')
561
593
  .concat(['--values', options.values])
562
594
  .concat(['--set-string', `image.tag=${imageTag}`])
563
595
  .concat(sdkVersion ? ['--set-string', `sdk.version=${sdkVersion}`] : [])
564
- .concat(!options.chartPath ? ['--repo', helmChartRepo, '--version', helmChartVersion] : [])
596
+ .concat(!options.localChartPath ? ['--repo', helmChartRepo, '--version', helmChartVersion] : [])
565
597
  .concat([options.deploymentName])
566
598
  .concat([chartNameOrPath])
567
599
  logger.info(`Execute: helm ${helmArgs.join(' ')}`)
@@ -569,18 +601,18 @@ program.command('deploy-server')
569
601
  // Execute Helm
570
602
  let helmResult
571
603
  try {
572
- helmResult = await executeCommand('helm', helmArgs)
604
+ helmResult = await executeCommand('helm', helmArgs, false)
573
605
  // \todo output something from Helm result?
574
606
  } catch (err) {
575
607
  throw new Error(`Failed to execute 'helm': ${err}. You need to have Helm v3 installed to deploy a game server with metaplay-auth.`)
576
608
  }
577
609
 
578
610
  // Throw on Helm non-success exit code
579
- if (helmResult.code !== 0) {
580
- throw new Error(`Helm deploy failed with exit code ${helmResult.code}: ${helmResult.stderr}`)
611
+ if (helmResult.exitCode !== 0) {
612
+ throw new Error(`Helm deploy failed with exit code ${helmResult.exitCode}: ${helmResult.stderr}`)
581
613
  }
582
614
 
583
- const testingRepoSuffix = (!options.chartPath && helmChartRepo !== 'https://charts.metaplay.dev') ? ` from repo ${helmChartRepo}` : ''
615
+ const testingRepoSuffix = (!options.localChartPath && helmChartRepo !== 'https://charts.metaplay.dev') ? ` from repo ${helmChartRepo}` : ''
584
616
  console.log(`Game server deployed to ${gameserver} with tag ${imageTag} using chart version ${helmChartVersion}${testingRepoSuffix}!`)
585
617
  } finally {
586
618
  // Remove temporary kubeconfig file
@@ -593,7 +625,7 @@ program.command('deploy-server')
593
625
  kubeconfig.loadFromString(kubeconfigPayload)
594
626
 
595
627
  console.log('Validating game server deployment...')
596
- const exitCode = await checkGameServerDeployment(envInfo.deployment.kubernetes_namespace, kubeconfig)
628
+ const exitCode = await checkGameServerDeployment(envInfo.deployment.kubernetes_namespace, kubeconfig, imageTag)
597
629
  exit(exitCode)
598
630
  } catch (error) {
599
631
  console.error(`Failed to resolve game server deployment status: ${error}`)
@@ -610,26 +642,25 @@ program.command('deploy-server')
610
642
  program.command('check-server-status')
611
643
  .description('check the status of a deployed server and print out information that is helpful in debugging failed deployments')
612
644
  .argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
645
+ .option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
613
646
  .hook('preAction', async () => {
614
647
  await extendCurrentSession()
615
648
  })
616
- .action(async (gameserver: string | undefined, options) => {
617
- const tokens = await loadTokens()
618
- const stackApi = new StackAPI(tokens.access_token, options.stackApi)
619
- const gameserverId = resolveGameserverId(gameserver, options)
649
+ .action(async (gameserver: string | undefined, options: { stackApi?: string }) => {
650
+ const targetEnv = await resolveTargetEnvironment(gameserver, options)
620
651
 
621
652
  try {
622
653
  logger.debug('Get environment info')
623
- const envInfo = await stackApi.getEnvironmentDetails(gameserverId)
654
+ const envInfo = await targetEnv.getEnvironmentDetails()
624
655
  const kubernetesNamespace = envInfo.deployment.kubernetes_namespace
625
656
 
626
657
  // Load kubeconfig from file and throw error if validation fails.
627
658
  logger.debug('Get kubeconfig')
628
659
  const kubeconfig = new KubeConfig()
629
660
  try {
630
- // Fetch kubeconfig and write it to a temporary file
661
+ // Initialize kubeconfig with the payload fetched from the cloud
631
662
  // \todo allow passing a custom kubeconfig file?
632
- const kubeconfigPayload = await stackApi.getKubeConfig(gameserverId)
663
+ const kubeconfigPayload = await targetEnv.getKubeConfig()
633
664
  kubeconfig.loadFromString(kubeconfigPayload)
634
665
  } catch (error) {
635
666
  throw new Error(`Failed to load or validate kubeconfig. ${error}`)
@@ -637,7 +668,8 @@ program.command('check-server-status')
637
668
 
638
669
  // Run the checks and exit with success/failure exitCode depending on result
639
670
  console.log(`Validating game server deployment in namespace ${kubernetesNamespace}`)
640
- const exitCode = await checkGameServerDeployment(kubernetesNamespace, kubeconfig)
671
+ // \todo Get requiredImageTag from the Helm chart
672
+ const exitCode = await checkGameServerDeployment(kubernetesNamespace, kubeconfig, /* requiredImageTag: */ null)
641
673
  exit(exitCode)
642
674
  } catch (error: any) {
643
675
  console.error(`Failed to check deployment status: ${error.message}`)
@@ -678,7 +710,8 @@ program.command('check-deployment')
678
710
 
679
711
  // Run the checks and exit with success/failure exitCode depending on result
680
712
  console.log(`Validating game server deployment in namespace ${namespace}`)
681
- const exitCode = await checkGameServerDeployment(namespace, kubeconfig)
713
+ // \todo Get requiredImageTag from the Helm chart
714
+ const exitCode = await checkGameServerDeployment(namespace, kubeconfig, /* requiredImageTag: */ null)
682
715
  exit(exitCode)
683
716
  } catch (error: any) {
684
717
  console.error(`Failed to check deployment status: ${error.message}`)
@@ -686,4 +719,19 @@ program.command('check-deployment')
686
719
  }
687
720
  })
688
721
 
722
+ program.command('debug-server')
723
+ .description('run an ephemeral debug container against a game server pod running in the cloud')
724
+ .argument('gameserver', 'address of gameserver (e.g., metaplay-idler-develop or idler-develop.p1.metaplay.io)')
725
+ .argument('[pod-name]', 'name of the pod to debug (must be specified if deployment has multiple pods)')
726
+ .hook('preAction', async () => {
727
+ await extendCurrentSession()
728
+ })
729
+ .action(async (gameserver: string, podName: string | undefined, options: { stackApi?: string }) => {
730
+ // Resolve target environment
731
+ const targetEnv = await resolveTargetEnvironment(gameserver, options)
732
+
733
+ // Exec 'kubectl debug ...'
734
+ await debugGameServer(targetEnv, podName)
735
+ })
736
+
689
737
  void program.parseAsync()
package/package.json CHANGED
@@ -1,45 +1,43 @@
1
1
  {
2
2
  "name": "@metaplay/metaplay-auth",
3
3
  "description": "Utility CLI for authenticating with the Metaplay Auth and making authenticated calls to infrastructure endpoints.",
4
- "version": "1.4.2",
4
+ "version": "1.5.0",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "homepage": "https://metaplay.io",
8
8
  "bin": {
9
- "metaplay-auth": "dist/index.js"
9
+ "metaplay-auth": "dist/index.cjs"
10
10
  },
11
11
  "scripts": {
12
12
  "dev": "tsx index.ts",
13
- "prepublish": "tsc"
13
+ "bake-version": "node -p \"'export const PACKAGE_VERSION = ' + JSON.stringify(require('./package.json').version)\" > src/version.ts"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@metaplay/eslint-config": "workspace:*",
20
- "@metaplay/typescript-config": "workspace:*",
21
- "@types/dockerode": "^3.3.28",
20
+ "@types/dockerode": "^3.3.29",
22
21
  "@types/express": "^4.17.21",
23
22
  "@types/js-yaml": "^4.0.9",
24
23
  "@types/jsonwebtoken": "^9.0.5",
25
24
  "@types/jwk-to-pem": "^2.0.3",
26
- "@types/node": "^20.12.5",
25
+ "@types/node": "^20.14.8",
26
+ "@types/semver": "^7.5.8",
27
+ "esbuild": "^0.23.0",
27
28
  "tsx": "^4.7.1",
28
- "typescript": "^5.1.6",
29
- "vitest": "^1.4.0"
30
- },
31
- "dependencies": {
32
- "@aws-sdk/client-ecr": "^3.549.0",
33
- "@kubernetes/client-node": "^1.0.0-rc4",
29
+ "typescript": "5.4.x",
30
+ "vitest": "^1.4.0",
31
+ "@aws-sdk/client-ecr": "^3.620.0",
32
+ "@kubernetes/client-node": "^1.0.0-rc6",
34
33
  "@ory/client": "^1.9.0",
35
- "@types/semver": "^7.5.8",
36
34
  "commander": "^12.0.0",
37
35
  "dockerode": "^4.0.2",
38
36
  "h3": "^1.11.1",
39
37
  "js-yaml": "^4.1.0",
40
38
  "jsonwebtoken": "^9.0.2",
41
39
  "jwk-to-pem": "^2.0.5",
42
- "open": "^10.1.0",
40
+ "open": "^8.4.2",
43
41
  "semver": "^7.6.0",
44
42
  "tslog": "^4.9.2"
45
43
  }
package/src/auth.ts CHANGED
@@ -12,6 +12,12 @@ import { setSecret, getSecret, removeSecret } from './secret_store.js'
12
12
 
13
13
  import { logger } from './logging.js'
14
14
 
15
+ export interface TokenSet {
16
+ id_token?: string
17
+ access_token: string
18
+ refresh_token?: string
19
+ }
20
+
15
21
  // oauth2 client details (maybe move these to be discovered from some online location to make changes easier to manage?)
16
22
  const clientId = 'c16ea663-ced3-46c6-8f85-38c9681fe1f0'
17
23
  const baseURL = 'https://auth.metaplay.dev'
@@ -275,7 +281,7 @@ async function extendCurrentSessionWithRefreshToken (refreshToken: string): Prom
275
281
 
276
282
  // Check if the response is OK
277
283
  if (!response.ok) {
278
- const responseJSON = await response.json()
284
+ const responseJSON = await response.json() as { error: string, error_description: string }
279
285
 
280
286
  logger.error('Failed to refresh tokens.')
281
287
  logger.error(`Error Type: ${responseJSON.error}`)
@@ -288,7 +294,7 @@ async function extendCurrentSessionWithRefreshToken (refreshToken: string): Prom
288
294
  throw new Error('Failed extending current session, exiting. Please log in again.')
289
295
  }
290
296
 
291
- return await response.json()
297
+ return await response.json() as { id_token: string, access_token: string, refresh_token: string }
292
298
  }
293
299
 
294
300
  /**
@@ -310,7 +316,7 @@ async function getTokensWithAuthorizationCode (state: string, redirectUri: strin
310
316
  body: `grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${clientId}&code_verifier=${verifier}&state=${encodeURIComponent(state)}`
311
317
  })
312
318
 
313
- return await response.json()
319
+ return await response.json() as { id_token: string, access_token: string, refresh_token: string }
314
320
  } catch (error) {
315
321
  if (error instanceof Error) {
316
322
  logger.error(`Error exchanging code for tokens: ${error.message}`)
@@ -322,9 +328,9 @@ async function getTokensWithAuthorizationCode (state: string, redirectUri: strin
322
328
  /**
323
329
  * Load tokens from the local secret store.
324
330
  */
325
- export async function loadTokens (): Promise<{ id_token?: string, access_token: string, refresh_token?: string }> {
331
+ export async function loadTokens (): Promise<TokenSet> {
326
332
  try {
327
- const tokens = await getSecret('tokens') as { id_token?: string, access_token: string, refresh_token?: string }
333
+ const tokens = await getSecret('tokens') as TokenSet
328
334
 
329
335
  if (!tokens) {
330
336
  throw new Error('Unable to load tokens. You need to login first.')