@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.
- package/CHANGELOG.md +23 -0
- package/dist/index.cjs +292 -0
- package/dist/sshcrypto-OMBCGRSN.node +0 -0
- package/index.ts +142 -94
- package/package.json +12 -14
- package/src/auth.ts +11 -5
- package/src/deployment.ts +124 -14
- package/src/stackapi.ts +18 -363
- package/src/targetenvironment.ts +307 -0
- package/src/utils.ts +49 -9
- package/src/version.ts +1 -0
- package/dist/index.js +0 -644
- package/dist/index.js.map +0 -1
- package/dist/src/auth.js +0 -373
- package/dist/src/auth.js.map +0 -1
- package/dist/src/deployment.js +0 -250
- package/dist/src/deployment.js.map +0 -1
- package/dist/src/logging.js +0 -18
- package/dist/src/logging.js.map +0 -1
- package/dist/src/secret_store.js +0 -79
- package/dist/src/secret_store.js.map +0 -1
- package/dist/src/stackapi.js +0 -302
- package/dist/src/stackapi.js.map +0 -1
- package/dist/src/utils.js +0 -62
- package/dist/src/utils.js.map +0 -1
- package/dist/tests/utils.spec.js +0 -18
- package/dist/tests/utils.spec.js.map +0 -1
- package/tests/utils.spec.ts +0 -20
- package/vitest.config.ts +0 -7
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
193
|
+
kubeconfigPayload = await targetEnv.getKubeConfigExecCredential()
|
|
155
194
|
} else if (credentialsType === 'static') {
|
|
156
195
|
logger.debug('Fetching kubeconfig with embedded secret')
|
|
157
|
-
kubeconfigPayload = await
|
|
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
|
|
199
|
-
const
|
|
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
|
|
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
|
-
|
|
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('-
|
|
257
|
-
.option('-
|
|
258
|
-
.option('-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
580
|
-
throw new Error(`Helm deploy failed with exit code ${helmResult.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
//
|
|
661
|
+
// Initialize kubeconfig with the payload fetched from the cloud
|
|
631
662
|
// \todo allow passing a custom kubeconfig file?
|
|
632
|
-
const kubeconfigPayload = await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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.
|
|
9
|
+
"metaplay-auth": "dist/index.cjs"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "tsx index.ts",
|
|
13
|
-
"
|
|
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
|
-
"@
|
|
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.
|
|
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": "
|
|
29
|
-
"vitest": "^1.4.0"
|
|
30
|
-
|
|
31
|
-
|
|
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": "^
|
|
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<
|
|
331
|
+
export async function loadTokens (): Promise<TokenSet> {
|
|
326
332
|
try {
|
|
327
|
-
const tokens = await getSecret('tokens') as
|
|
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.')
|