@metaplay/metaplay-auth 1.4.1 → 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 +29 -0
- package/dist/index.cjs +292 -0
- package/dist/sshcrypto-OMBCGRSN.node +0 -0
- package/index.ts +208 -156
- package/package.json +12 -14
- package/src/auth.ts +11 -5
- package/src/deployment.ts +124 -14
- package/src/stackapi.ts +19 -314
- package/src/targetenvironment.ts +307 -0
- package/src/utils.ts +49 -9
- package/src/version.ts +1 -0
- package/dist/index.js +0 -638
- 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 -264
- 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
package/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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
|
-
import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr'
|
|
11
11
|
import { tmpdir } from 'os'
|
|
12
12
|
import { join } from 'path'
|
|
13
13
|
import { randomBytes } from 'crypto'
|
|
@@ -15,28 +15,66 @@ import { writeFile, unlink } from 'fs/promises'
|
|
|
15
15
|
import { existsSync } from 'fs'
|
|
16
16
|
import { KubeConfig } from '@kubernetes/client-node'
|
|
17
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
|
+
}
|
|
18
48
|
|
|
19
49
|
/**
|
|
20
|
-
* Helper for parsing the
|
|
50
|
+
* Helper for parsing the TargetEnvironment type from the command line arguments. Accepts either the gameserver address
|
|
21
51
|
* (idler-test.p1.metaplay.io), a shorthand address (metaplay-idler-test) or the (organization, project, environment)
|
|
22
52
|
* tuple from options.
|
|
23
53
|
*/
|
|
24
|
-
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
|
+
|
|
25
57
|
// If address is specified, use it, otherwise assume options has organization, project, and environment
|
|
26
58
|
if (address) {
|
|
59
|
+
// Address is either FQDN or 'organization-project-environment' tuple
|
|
27
60
|
if (isValidFQDN(address)) {
|
|
28
|
-
|
|
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)
|
|
29
65
|
} else {
|
|
30
66
|
const parts = address.split('-')
|
|
31
67
|
if (parts.length !== 3) {
|
|
32
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)')
|
|
33
69
|
}
|
|
34
|
-
return
|
|
70
|
+
return await resolveTargetEnvironmentFromTuple(tokens, parts[0], parts[1], parts[2], options.stackApi)
|
|
35
71
|
}
|
|
36
72
|
} else if (options.organization && options.project && options.environment) {
|
|
37
|
-
|
|
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)
|
|
38
76
|
} else {
|
|
39
|
-
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.')
|
|
40
78
|
}
|
|
41
79
|
}
|
|
42
80
|
|
|
@@ -45,7 +83,7 @@ const program = new Command()
|
|
|
45
83
|
program
|
|
46
84
|
.name('metaplay-auth')
|
|
47
85
|
.description('Authenticate with Metaplay and get AWS and Kubernetes credentials for game servers.')
|
|
48
|
-
.version(
|
|
86
|
+
.version(PACKAGE_VERSION)
|
|
49
87
|
.option('-d, --debug', 'enable debug output')
|
|
50
88
|
.hook('preAction', (thisCommand) => {
|
|
51
89
|
// Handle debug flag for all commands.
|
|
@@ -68,11 +106,12 @@ program.command('machine-login')
|
|
|
68
106
|
.option('--dev-credentials', 'machine user credentials to use, only for dev purposes, use METAPLAY_CREDENTIALS env variable for better safety!')
|
|
69
107
|
.action(async (options) => {
|
|
70
108
|
// Get credentials from command line or from METAPLAY_CREDENTIALS environment variable
|
|
71
|
-
let credentials
|
|
109
|
+
let credentials: string
|
|
72
110
|
if (options.devCredentials) {
|
|
73
111
|
credentials = options.devCredentials
|
|
74
112
|
} else {
|
|
75
|
-
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
114
|
+
credentials = process.env.METAPLAY_CREDENTIALS!
|
|
76
115
|
if (!credentials || credentials === '') {
|
|
77
116
|
throw new Error('Unable to find the credentials, the environment variable METAPLAY_CREDENTIALS is not defined!')
|
|
78
117
|
}
|
|
@@ -117,7 +156,7 @@ program.command('show-tokens')
|
|
|
117
156
|
try {
|
|
118
157
|
// TODO: Could detect if not logged in and fail more gracefully?
|
|
119
158
|
const tokens = await loadTokens()
|
|
120
|
-
console.log(tokens)
|
|
159
|
+
console.log(JSON.stringify(tokens, undefined, 2))
|
|
121
160
|
} catch (error) {
|
|
122
161
|
if (error instanceof Error) {
|
|
123
162
|
console.error(`Error showing tokens: ${error.message}`)
|
|
@@ -130,32 +169,31 @@ program.command('get-kubeconfig')
|
|
|
130
169
|
.description('get kubeconfig for target environment')
|
|
131
170
|
.argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
132
171
|
.option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
|
|
133
|
-
.option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
|
|
134
|
-
.option('-p, --project <project>', 'project name (e.g. idler)')
|
|
135
|
-
.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)')
|
|
136
175
|
.option('-t, --type <credentials-type>', 'type of credentials handling in kubeconfig (static or dynamic)')
|
|
137
176
|
.option('--output <kubeconfig-path>', 'path of the output file where to write kubeconfig (written to stdout if not specified)')
|
|
138
177
|
.hook('preAction', async () => {
|
|
139
178
|
await extendCurrentSession()
|
|
140
179
|
})
|
|
141
|
-
.action(async (gameserver, options) => {
|
|
180
|
+
.action(async (gameserver: string | undefined, options: { stackApi?: string, organization?: string, project?: string, environment?: string, type?: string, output?: string }) => {
|
|
142
181
|
try {
|
|
143
|
-
const
|
|
144
|
-
const stackApi = new StackAPI(tokens.access_token, options.stackApi)
|
|
145
|
-
const gameserverId = resolveGameserverId(gameserver, options)
|
|
182
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
146
183
|
|
|
147
184
|
// Default to credentialsType==dynamic for human users, and credentialsType==static for machine users
|
|
185
|
+
const tokens = await loadTokens()
|
|
148
186
|
const isHumanUser = !!tokens.refresh_token
|
|
149
|
-
const credentialsType = options.
|
|
187
|
+
const credentialsType = options.type ?? (isHumanUser ? 'dynamic' : 'static')
|
|
150
188
|
|
|
151
189
|
// Generate kubeconfig
|
|
152
190
|
let kubeconfigPayload
|
|
153
191
|
if (credentialsType === 'dynamic') {
|
|
154
192
|
logger.debug('Fetching kubeconfig with execcredential')
|
|
155
|
-
kubeconfigPayload = await
|
|
193
|
+
kubeconfigPayload = await targetEnv.getKubeConfigExecCredential()
|
|
156
194
|
} else if (credentialsType === 'static') {
|
|
157
195
|
logger.debug('Fetching kubeconfig with embedded secret')
|
|
158
|
-
kubeconfigPayload = await
|
|
196
|
+
kubeconfigPayload = await targetEnv.getKubeConfig()
|
|
159
197
|
} else {
|
|
160
198
|
throw new Error('Invalid credentials type; must be either "static" or "dynamic"')
|
|
161
199
|
}
|
|
@@ -188,19 +226,16 @@ program.command('get-kubernetes-execcredential')
|
|
|
188
226
|
.description('[internal] get kubernetes credentials in execcredential format (used from the generated kubeconfigs)')
|
|
189
227
|
.argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
190
228
|
.option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
|
|
191
|
-
.option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
|
|
192
|
-
.option('-p, --project <project>', 'project name (e.g. idler)')
|
|
193
|
-
.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)')
|
|
194
232
|
.hook('preAction', async () => {
|
|
195
233
|
await extendCurrentSession()
|
|
196
234
|
})
|
|
197
|
-
.action(async (gameserver, options) => {
|
|
235
|
+
.action(async (gameserver: string | undefined, options: { stackApi?: string, organization?: string, project?: string, environment?: string }) => {
|
|
198
236
|
try {
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const gameserverId = resolveGameserverId(gameserver, options)
|
|
202
|
-
|
|
203
|
-
const credentials = await stackApi.getKubeExecCredential(gameserverId)
|
|
237
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
238
|
+
const credentials = await targetEnv.getKubeExecCredential()
|
|
204
239
|
console.log(credentials)
|
|
205
240
|
} catch (error) {
|
|
206
241
|
if (error instanceof Error) {
|
|
@@ -214,24 +249,23 @@ program.command('get-aws-credentials')
|
|
|
214
249
|
.description('get AWS credentials for target environment')
|
|
215
250
|
.argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
216
251
|
.option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
|
|
217
|
-
.option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
|
|
218
|
-
.option('-p, --project <project>', 'project name (e.g. idler)')
|
|
219
|
-
.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)')
|
|
220
255
|
.option('-f, --format <format>', 'output format (json or env)', 'json')
|
|
221
256
|
.hook('preAction', async () => {
|
|
222
257
|
await extendCurrentSession()
|
|
223
258
|
})
|
|
224
|
-
.action(async (gameserver, options) => {
|
|
259
|
+
.action(async (gameserver: string | undefined, options: { stackApi?: string, organization?: string, project?: string, environment?: string, format?: string }) => {
|
|
225
260
|
try {
|
|
226
261
|
if (options.format !== 'json' && options.format !== 'env') {
|
|
227
262
|
throw new Error('Invalid format; must be one of json or env')
|
|
228
263
|
}
|
|
229
264
|
|
|
230
|
-
const
|
|
231
|
-
const stackApi = new StackAPI(tokens.access_token, options.stackApi)
|
|
232
|
-
const gameserverId = resolveGameserverId(gameserver, options)
|
|
265
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
233
266
|
|
|
234
|
-
|
|
267
|
+
// Get the AWS credentials
|
|
268
|
+
const credentials = await targetEnv.getAwsCredentials()
|
|
235
269
|
|
|
236
270
|
if (options.format === 'env') {
|
|
237
271
|
console.log(`export AWS_ACCESS_KEY_ID=${credentials.AccessKeyId}`)
|
|
@@ -252,59 +286,35 @@ program.command('get-aws-credentials')
|
|
|
252
286
|
})
|
|
253
287
|
|
|
254
288
|
program.command('get-docker-login')
|
|
255
|
-
.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')
|
|
256
290
|
.argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
257
|
-
.option('-
|
|
258
|
-
.option('-
|
|
259
|
-
.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)')
|
|
260
295
|
.option('-f, --format <format>', 'output format (json or env)', 'json')
|
|
261
296
|
.hook('preAction', async () => {
|
|
262
297
|
await extendCurrentSession()
|
|
263
298
|
})
|
|
264
|
-
.action(async (gameserver, options) => {
|
|
299
|
+
.action(async (gameserver: string | undefined, options: { stackApi?: string, organization?: string, project?: string, environment?: string, format?: string }) => {
|
|
265
300
|
try {
|
|
266
301
|
if (options.format !== 'json' && options.format !== 'env') {
|
|
267
302
|
throw new Error('Invalid format; must be one of json or env')
|
|
268
303
|
}
|
|
269
304
|
|
|
270
|
-
|
|
271
|
-
const stackApi = new StackAPI(tokens.access_token, options.stackApi)
|
|
272
|
-
const gameserverId = resolveGameserverId(gameserver, options)
|
|
305
|
+
console.warn('The get-docker-login command is deprecated! Use the push-docker-image command instead.')
|
|
273
306
|
|
|
274
|
-
|
|
275
|
-
logger.debug('Get AWS credentials from Metaplay')
|
|
276
|
-
const credentials = await stackApi.getAwsCredentials(gameserverId)
|
|
307
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
277
308
|
|
|
278
309
|
// Get environment info (region is needed for ECR)
|
|
279
310
|
logger.debug('Get environment info')
|
|
280
|
-
const environment = await
|
|
281
|
-
const awsRegion = environment.deployment.aws_region
|
|
311
|
+
const environment = await targetEnv.getEnvironmentDetails()
|
|
282
312
|
const dockerRepo = environment.deployment.ecr_repo
|
|
283
313
|
|
|
284
|
-
//
|
|
285
|
-
logger.debug('
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
accessKeyId: credentials.AccessKeyId,
|
|
289
|
-
secretAccessKey: credentials.SecretAccessKey,
|
|
290
|
-
sessionToken: credentials.SessionToken
|
|
291
|
-
},
|
|
292
|
-
region: awsRegion
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
// Fetch the ECR docker authentication token
|
|
296
|
-
logger.debug('Fetch ECR login credentials from AWS')
|
|
297
|
-
const command = new GetAuthorizationTokenCommand({})
|
|
298
|
-
const response = await client.send(command)
|
|
299
|
-
if (!response.authorizationData || response.authorizationData.length === 0 || !response.authorizationData[0].authorizationToken) {
|
|
300
|
-
throw new Error('Received an empty authorization token response for ECR repository')
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Parse username and password from the response (separated by a ':')
|
|
304
|
-
logger.debug('Parse ECR response')
|
|
305
|
-
const authorization64 = response.authorizationData[0].authorizationToken
|
|
306
|
-
const authorization = Buffer.from(authorization64, 'base64').toString()
|
|
307
|
-
const [username, password] = authorization.split(':')
|
|
314
|
+
// Resolve docker credentials for remote registry
|
|
315
|
+
logger.debug('Get docker credentials')
|
|
316
|
+
const dockerCredentials = await targetEnv.getDockerCredentials()
|
|
317
|
+
const { username, password } = dockerCredentials
|
|
308
318
|
|
|
309
319
|
// Output the docker repo & credentials
|
|
310
320
|
if (options.format === 'env') {
|
|
@@ -330,20 +340,18 @@ program.command('get-environment')
|
|
|
330
340
|
.description('get details of an environment')
|
|
331
341
|
.argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
332
342
|
.option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
|
|
333
|
-
.option('-o, --organization <organization>', 'organization name (e.g. metaplay)')
|
|
334
|
-
.option('-p, --project <project>', 'project name (e.g. idler)')
|
|
335
|
-
.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)')
|
|
336
346
|
.hook('preAction', async () => {
|
|
337
347
|
await extendCurrentSession()
|
|
338
348
|
})
|
|
339
|
-
.action(async (gameserver, options) => {
|
|
349
|
+
.action(async (gameserver: string | undefined, options: { stackApi?: string, organization?: string, project?: string, environment?: string }) => {
|
|
340
350
|
try {
|
|
341
|
-
const
|
|
342
|
-
const stackApi = new StackAPI(tokens.access_token, options.stackApi)
|
|
343
|
-
const gameserverId = resolveGameserverId(gameserver, options)
|
|
351
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
344
352
|
|
|
345
|
-
const environment = await
|
|
346
|
-
console.log(JSON.stringify(environment))
|
|
353
|
+
const environment = await targetEnv.getEnvironmentDetails()
|
|
354
|
+
console.log(JSON.stringify(environment, undefined, 2))
|
|
347
355
|
} catch (error) {
|
|
348
356
|
if (error instanceof Error) {
|
|
349
357
|
console.error(`Error getting environment details: ${error.message}`)
|
|
@@ -356,51 +364,23 @@ program.command('push-docker-image')
|
|
|
356
364
|
.description('push docker image into the target environment image registry')
|
|
357
365
|
.argument('gameserver', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
358
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/)')
|
|
359
368
|
.hook('preAction', async () => {
|
|
360
369
|
await extendCurrentSession()
|
|
361
370
|
})
|
|
362
|
-
.action(async (gameserver, imageName, options) => {
|
|
371
|
+
.action(async (gameserver: string | undefined, imageName: string | undefined, options: { stackApi?: string, imageTag?: string }) => {
|
|
363
372
|
try {
|
|
364
373
|
console.log(`Pushing docker image ${imageName} to target environment ${gameserver}...`)
|
|
365
374
|
|
|
366
|
-
const
|
|
367
|
-
const stackApi = new StackAPI(tokens.access_token, options.stackApi)
|
|
368
|
-
const gameserverId = resolveGameserverId(gameserver, options)
|
|
369
|
-
|
|
370
|
-
// Fetch AWS credentials from Metaplay cloud
|
|
371
|
-
logger.debug('Get AWS credentials from Metaplay')
|
|
372
|
-
const credentials = await stackApi.getAwsCredentials(gameserverId)
|
|
375
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
373
376
|
|
|
374
377
|
// Get environment info (region is needed for ECR)
|
|
375
378
|
logger.debug('Get environment info')
|
|
376
|
-
const envInfo = await
|
|
377
|
-
const awsRegion = envInfo.deployment.aws_region
|
|
378
|
-
|
|
379
|
-
// Create ECR client with credentials
|
|
380
|
-
logger.debug('Create ECR client')
|
|
381
|
-
const client = new ECRClient({
|
|
382
|
-
credentials: {
|
|
383
|
-
accessKeyId: credentials.AccessKeyId,
|
|
384
|
-
secretAccessKey: credentials.SecretAccessKey,
|
|
385
|
-
sessionToken: credentials.SessionToken
|
|
386
|
-
},
|
|
387
|
-
region: awsRegion
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
// Fetch the ECR docker authentication token
|
|
391
|
-
logger.debug('Fetch ECR login credentials from AWS')
|
|
392
|
-
const command = new GetAuthorizationTokenCommand({})
|
|
393
|
-
const response = await client.send(command)
|
|
394
|
-
if (!response.authorizationData || response.authorizationData.length === 0 || !response.authorizationData[0].authorizationToken) {
|
|
395
|
-
throw new Error('Received an empty authorization token response for ECR repository')
|
|
396
|
-
}
|
|
379
|
+
const envInfo = await targetEnv.getEnvironmentDetails()
|
|
397
380
|
|
|
398
|
-
//
|
|
399
|
-
logger.debug('
|
|
400
|
-
const
|
|
401
|
-
const authorization = Buffer.from(authorization64, 'base64').toString()
|
|
402
|
-
const [username, password] = authorization.split(':')
|
|
403
|
-
const authConfig = { username, password, serveraddress: '' } // \todo serveraddress value? https://stackoverflow.com/questions/54252777/how-to-push-image-with-dockerode-image-not-pushed-but-no-error
|
|
381
|
+
// Resolve docker credentials for remote registry
|
|
382
|
+
logger.debug('Get docker credentials')
|
|
383
|
+
const dockerCredentials = await targetEnv.getDockerCredentials()
|
|
404
384
|
|
|
405
385
|
// Resolve tag from src image
|
|
406
386
|
if (!imageName) {
|
|
@@ -428,20 +408,21 @@ program.command('push-docker-image')
|
|
|
428
408
|
// Push the image
|
|
429
409
|
logger.debug(`Push image ${dstImageName}`)
|
|
430
410
|
const dstDockerImage = dockerApi.getImage(dstImageName)
|
|
411
|
+
const authConfig = { username: dockerCredentials.username, password: dockerCredentials.password, serveraddress: dockerCredentials.registryUrl }
|
|
431
412
|
const pushStream = await dstDockerImage.push({ authconfig: authConfig, tag: options.imageTag })
|
|
432
413
|
|
|
433
414
|
// Follow push progress & wait until completed
|
|
434
|
-
logger.debug('Following push stream...')
|
|
415
|
+
logger.debug('Following image push stream...')
|
|
435
416
|
await new Promise((resolve, reject) => {
|
|
436
417
|
dockerApi.modem.followProgress(
|
|
437
418
|
pushStream,
|
|
438
419
|
(error: Error | null, result: any[]) => {
|
|
439
420
|
if (error) {
|
|
440
|
-
logger.debug('Failed to push image:', error)
|
|
421
|
+
logger.debug('Failed to push docker image to target repository:', error)
|
|
441
422
|
reject(error)
|
|
442
423
|
} else {
|
|
443
424
|
// result contains an array of all the progress objects
|
|
444
|
-
logger.debug('Succesfully finished pushing image')
|
|
425
|
+
logger.debug('Succesfully finished pushing docker image')
|
|
445
426
|
resolve(result)
|
|
446
427
|
}
|
|
447
428
|
},
|
|
@@ -463,10 +444,10 @@ program.command('push-docker-image')
|
|
|
463
444
|
|
|
464
445
|
program.command('deploy-server')
|
|
465
446
|
.description('deploy a game server image to target environment')
|
|
466
|
-
.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)')
|
|
467
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')
|
|
468
450
|
.option('-s, --stack-api <stack-api-base-path>', 'explicit stack api (e.g. https://infra.p1.metaplay.io/stackapi/)')
|
|
469
|
-
.option('-f, --values <path-to-values-file>', 'path to Helm values file to use for this deployment')
|
|
470
451
|
.option('--local-chart-path <path-to-chart-directory>', 'path to local Helm chart directory (to use a chart from local disk)')
|
|
471
452
|
.option('--helm-chart-repo <url>', 'override the URL of the Helm chart repository (eg, https://charts.metaplay.dev/testing)')
|
|
472
453
|
.option('--helm-chart-version <version>', 'override the Helm chart version (eg, 0.6.0)')
|
|
@@ -474,36 +455,91 @@ program.command('deploy-server')
|
|
|
474
455
|
.hook('preAction', async () => {
|
|
475
456
|
await extendCurrentSession()
|
|
476
457
|
})
|
|
477
|
-
.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 }) => {
|
|
478
459
|
try {
|
|
479
|
-
const
|
|
480
|
-
const stackApi = new StackAPI(tokens.access_token, options.stackApi)
|
|
481
|
-
const gameserverId = resolveGameserverId(gameserver, options)
|
|
460
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
482
461
|
|
|
483
462
|
console.log(`Deploying server to ${gameserver} with image tag ${imageTag}...`)
|
|
484
463
|
|
|
485
464
|
// Fetch target environment details
|
|
486
|
-
const envInfo = await
|
|
465
|
+
const envInfo = await targetEnv.getEnvironmentDetails()
|
|
487
466
|
|
|
488
467
|
if (!imageTag) {
|
|
489
468
|
throw new Error('Must specify a valid docker image tag as the image-tag argument, usually the SHA of the build')
|
|
490
469
|
}
|
|
470
|
+
// \todo validate that imageTag is just the version part (i.e, contains no ':')
|
|
491
471
|
|
|
492
472
|
if (!options.deploymentName) {
|
|
493
|
-
throw new Error(`Invalid Helm deployment name '${options.deploymentName}'`)
|
|
473
|
+
throw new Error(`Invalid Helm deployment name '${options.deploymentName}'; specify one with --deployment-name or use the default`)
|
|
494
474
|
}
|
|
495
475
|
|
|
476
|
+
// Fetch Docker credentials for target environment registry
|
|
477
|
+
const dockerCredentials = await targetEnv.getDockerCredentials()
|
|
478
|
+
|
|
496
479
|
// Resolve information about docker image
|
|
480
|
+
// const dockerApi = new Docker({
|
|
481
|
+
// host: dockerCredentials.registryUrl,
|
|
482
|
+
// port: 443,
|
|
483
|
+
// protocol: 'https',
|
|
484
|
+
// username: dockerCredentials.username,
|
|
485
|
+
// headers: {
|
|
486
|
+
// Authorization: `Bearer ${dockerCredentials.password}`,
|
|
487
|
+
// Host: dockerCredentials.registryUrl.replace('https://', ''),
|
|
488
|
+
// }
|
|
489
|
+
// })
|
|
490
|
+
const dockerApi = new Docker()
|
|
497
491
|
const dockerRepo = envInfo.deployment.ecr_repo
|
|
498
492
|
const imageName = `${dockerRepo}:${imageTag}`
|
|
499
|
-
logger.debug(`Fetch docker image
|
|
493
|
+
logger.debug(`Fetch docker image labels for ${imageName}`)
|
|
500
494
|
let imageLabels
|
|
501
495
|
try {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
imageLabels = (await dockerImage.inspect()).Config.Labels || {}
|
|
496
|
+
const localDockerImage = dockerApi.getImage(imageName)
|
|
497
|
+
imageLabels = (await localDockerImage.inspect()).Config.Labels || {}
|
|
505
498
|
} catch (err) {
|
|
506
|
-
|
|
499
|
+
logger.debug(`Failed to resolve docker image metadata from local image ${imageName}: ${err}`)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// If wasn't able to resolve the images, pull image from the target environment registry & resolve labels
|
|
503
|
+
if (imageLabels === undefined) {
|
|
504
|
+
// Pull the image from remote registry
|
|
505
|
+
try {
|
|
506
|
+
console.log(`Image ${imageName} not found locally -- pulling docker image from target environment registry...`)
|
|
507
|
+
const authConfig = { username: dockerCredentials.username, password: dockerCredentials.password, serveraddress: dockerCredentials.registryUrl }
|
|
508
|
+
const pullStream = (await dockerApi.pull(imageName, { authconfig: authConfig }))
|
|
509
|
+
|
|
510
|
+
// Follow pull progress & wait until completed
|
|
511
|
+
logger.debug('Following image pull stream...')
|
|
512
|
+
await new Promise((resolve, reject) => {
|
|
513
|
+
dockerApi.modem.followProgress(
|
|
514
|
+
pullStream,
|
|
515
|
+
(error: Error | null, result: any[]) => {
|
|
516
|
+
if (error) {
|
|
517
|
+
logger.debug('Failed to pull image:', error)
|
|
518
|
+
reject(error)
|
|
519
|
+
} else {
|
|
520
|
+
// result contains an array of all the progress objects
|
|
521
|
+
logger.debug('Succesfully finished pulling image')
|
|
522
|
+
resolve(result)
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
(obj: any) => {
|
|
526
|
+
// console.log('Progress:', obj)
|
|
527
|
+
// { status: 'Preparing', progressDetail: {}, id: '82730adcaeb0' }
|
|
528
|
+
// { status: 'Layer already exists', progressDetail: {}, id: '7cd701fff13a' }
|
|
529
|
+
})
|
|
530
|
+
})
|
|
531
|
+
} catch (err) {
|
|
532
|
+
throw new Error(`Failed to fetch docker image ${imageName} from target environment registry: ${err}`)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Resolve the labels
|
|
536
|
+
try {
|
|
537
|
+
logger.debug('Get docker labels again (with pulled image)')
|
|
538
|
+
const localDockerImage = dockerApi.getImage(imageName)
|
|
539
|
+
imageLabels = (await localDockerImage.inspect()).Config.Labels || {}
|
|
540
|
+
} catch (err) {
|
|
541
|
+
throw new Error(`Failed to resolve docker image metadata from pulled image ${imageName}: ${err}`)
|
|
542
|
+
}
|
|
507
543
|
}
|
|
508
544
|
|
|
509
545
|
// Try to resolve SDK version and Helm repo and chart version from the docker labels
|
|
@@ -523,7 +559,7 @@ program.command('deploy-server')
|
|
|
523
559
|
// - Unknown, error out!
|
|
524
560
|
const helmChartVersion = options.helmChartVersion ?? imageLabels['io.metaplay.default_server_chart_version']
|
|
525
561
|
if (!helmChartVersion) {
|
|
526
|
-
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>.')
|
|
527
563
|
}
|
|
528
564
|
|
|
529
565
|
// A values file is required (at least for now it makes no sense to deploy without one)
|
|
@@ -542,7 +578,7 @@ program.command('deploy-server')
|
|
|
542
578
|
|
|
543
579
|
// Fetch kubeconfig and write it to a temporary file
|
|
544
580
|
// \todo allow passing a custom kubeconfig file?
|
|
545
|
-
const kubeconfigPayload = await
|
|
581
|
+
const kubeconfigPayload = await targetEnv.getKubeConfig()
|
|
546
582
|
const kubeconfigPath = join(tmpdir(), randomBytes(20).toString('hex'))
|
|
547
583
|
logger.debug(`Write temporary kubeconfig in ${kubeconfigPath}`)
|
|
548
584
|
await writeFile(kubeconfigPath, kubeconfigPayload, { mode: 0o600 })
|
|
@@ -557,7 +593,7 @@ program.command('deploy-server')
|
|
|
557
593
|
.concat(['--values', options.values])
|
|
558
594
|
.concat(['--set-string', `image.tag=${imageTag}`])
|
|
559
595
|
.concat(sdkVersion ? ['--set-string', `sdk.version=${sdkVersion}`] : [])
|
|
560
|
-
.concat(!options.
|
|
596
|
+
.concat(!options.localChartPath ? ['--repo', helmChartRepo, '--version', helmChartVersion] : [])
|
|
561
597
|
.concat([options.deploymentName])
|
|
562
598
|
.concat([chartNameOrPath])
|
|
563
599
|
logger.info(`Execute: helm ${helmArgs.join(' ')}`)
|
|
@@ -565,18 +601,18 @@ program.command('deploy-server')
|
|
|
565
601
|
// Execute Helm
|
|
566
602
|
let helmResult
|
|
567
603
|
try {
|
|
568
|
-
helmResult = await executeCommand('helm', helmArgs)
|
|
604
|
+
helmResult = await executeCommand('helm', helmArgs, false)
|
|
569
605
|
// \todo output something from Helm result?
|
|
570
606
|
} catch (err) {
|
|
571
607
|
throw new Error(`Failed to execute 'helm': ${err}. You need to have Helm v3 installed to deploy a game server with metaplay-auth.`)
|
|
572
608
|
}
|
|
573
609
|
|
|
574
610
|
// Throw on Helm non-success exit code
|
|
575
|
-
if (helmResult.
|
|
576
|
-
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}`)
|
|
577
613
|
}
|
|
578
614
|
|
|
579
|
-
const testingRepoSuffix = (!options.
|
|
615
|
+
const testingRepoSuffix = (!options.localChartPath && helmChartRepo !== 'https://charts.metaplay.dev') ? ` from repo ${helmChartRepo}` : ''
|
|
580
616
|
console.log(`Game server deployed to ${gameserver} with tag ${imageTag} using chart version ${helmChartVersion}${testingRepoSuffix}!`)
|
|
581
617
|
} finally {
|
|
582
618
|
// Remove temporary kubeconfig file
|
|
@@ -589,7 +625,7 @@ program.command('deploy-server')
|
|
|
589
625
|
kubeconfig.loadFromString(kubeconfigPayload)
|
|
590
626
|
|
|
591
627
|
console.log('Validating game server deployment...')
|
|
592
|
-
const exitCode = await checkGameServerDeployment(envInfo.deployment.kubernetes_namespace, kubeconfig)
|
|
628
|
+
const exitCode = await checkGameServerDeployment(envInfo.deployment.kubernetes_namespace, kubeconfig, imageTag)
|
|
593
629
|
exit(exitCode)
|
|
594
630
|
} catch (error) {
|
|
595
631
|
console.error(`Failed to resolve game server deployment status: ${error}`)
|
|
@@ -606,26 +642,25 @@ program.command('deploy-server')
|
|
|
606
642
|
program.command('check-server-status')
|
|
607
643
|
.description('check the status of a deployed server and print out information that is helpful in debugging failed deployments')
|
|
608
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/)')
|
|
609
646
|
.hook('preAction', async () => {
|
|
610
647
|
await extendCurrentSession()
|
|
611
648
|
})
|
|
612
|
-
.action(async (gameserver: string | undefined, options) => {
|
|
613
|
-
const
|
|
614
|
-
const stackApi = new StackAPI(tokens.access_token, options.stackApi)
|
|
615
|
-
const gameserverId = resolveGameserverId(gameserver, options)
|
|
649
|
+
.action(async (gameserver: string | undefined, options: { stackApi?: string }) => {
|
|
650
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
616
651
|
|
|
617
652
|
try {
|
|
618
653
|
logger.debug('Get environment info')
|
|
619
|
-
const envInfo = await
|
|
654
|
+
const envInfo = await targetEnv.getEnvironmentDetails()
|
|
620
655
|
const kubernetesNamespace = envInfo.deployment.kubernetes_namespace
|
|
621
656
|
|
|
622
657
|
// Load kubeconfig from file and throw error if validation fails.
|
|
623
658
|
logger.debug('Get kubeconfig')
|
|
624
659
|
const kubeconfig = new KubeConfig()
|
|
625
660
|
try {
|
|
626
|
-
//
|
|
661
|
+
// Initialize kubeconfig with the payload fetched from the cloud
|
|
627
662
|
// \todo allow passing a custom kubeconfig file?
|
|
628
|
-
const kubeconfigPayload = await
|
|
663
|
+
const kubeconfigPayload = await targetEnv.getKubeConfig()
|
|
629
664
|
kubeconfig.loadFromString(kubeconfigPayload)
|
|
630
665
|
} catch (error) {
|
|
631
666
|
throw new Error(`Failed to load or validate kubeconfig. ${error}`)
|
|
@@ -633,7 +668,8 @@ program.command('check-server-status')
|
|
|
633
668
|
|
|
634
669
|
// Run the checks and exit with success/failure exitCode depending on result
|
|
635
670
|
console.log(`Validating game server deployment in namespace ${kubernetesNamespace}`)
|
|
636
|
-
|
|
671
|
+
// \todo Get requiredImageTag from the Helm chart
|
|
672
|
+
const exitCode = await checkGameServerDeployment(kubernetesNamespace, kubeconfig, /* requiredImageTag: */ null)
|
|
637
673
|
exit(exitCode)
|
|
638
674
|
} catch (error: any) {
|
|
639
675
|
console.error(`Failed to check deployment status: ${error.message}`)
|
|
@@ -674,7 +710,8 @@ program.command('check-deployment')
|
|
|
674
710
|
|
|
675
711
|
// Run the checks and exit with success/failure exitCode depending on result
|
|
676
712
|
console.log(`Validating game server deployment in namespace ${namespace}`)
|
|
677
|
-
|
|
713
|
+
// \todo Get requiredImageTag from the Helm chart
|
|
714
|
+
const exitCode = await checkGameServerDeployment(namespace, kubeconfig, /* requiredImageTag: */ null)
|
|
678
715
|
exit(exitCode)
|
|
679
716
|
} catch (error: any) {
|
|
680
717
|
console.error(`Failed to check deployment status: ${error.message}`)
|
|
@@ -682,4 +719,19 @@ program.command('check-deployment')
|
|
|
682
719
|
}
|
|
683
720
|
})
|
|
684
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
|
+
|
|
685
737
|
void program.parseAsync()
|