@metaplay/metaplay-auth 1.4.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +2 -0
- package/CHANGELOG.md +69 -28
- package/dist/index.cjs +292 -0
- package/dist/sshcrypto-OMBCGRSN.node +0 -0
- package/eslint.config.js +3 -0
- package/index.ts +763 -437
- package/package.json +22 -21
- package/prettier.config.js +3 -0
- package/src/auth.ts +121 -80
- package/src/buildCommand.ts +267 -0
- package/src/config.ts +12 -0
- package/src/deployment.ts +285 -52
- package/src/logging.ts +4 -4
- package/src/secret_store.ts +10 -7
- package/src/stackapi.ts +5 -381
- package/src/targetenvironment.ts +311 -0
- package/src/utils.ts +162 -31
- 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
package/index.ts
CHANGED
|
@@ -1,41 +1,196 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander'
|
|
3
3
|
import Docker from 'dockerode'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
4
|
+
import { portalBaseUrl, setPortalBaseUrl } from './src/config.js'
|
|
5
|
+
import {
|
|
6
|
+
loginAndSaveTokens,
|
|
7
|
+
machineLoginAndSaveTokens,
|
|
8
|
+
extendCurrentSession,
|
|
9
|
+
loadTokens,
|
|
10
|
+
removeTokens,
|
|
11
|
+
TokenSet,
|
|
12
|
+
} from './src/auth.js'
|
|
13
|
+
import { checkGameServerDeployment, debugGameServer } from './src/deployment.js'
|
|
14
|
+
import {
|
|
15
|
+
pathJoin,
|
|
16
|
+
isValidFQDN,
|
|
17
|
+
executeCommand,
|
|
18
|
+
removeTrailingSlash,
|
|
19
|
+
fetchHelmChartVersions,
|
|
20
|
+
resolveBestMatchingVersion,
|
|
21
|
+
} from './src/utils.js'
|
|
7
22
|
import { logger, setLogLevel } from './src/logging.js'
|
|
8
|
-
import {
|
|
23
|
+
import { TargetEnvironment } from './src/targetenvironment.js'
|
|
9
24
|
import { exit } from 'process'
|
|
10
25
|
import { tmpdir } from 'os'
|
|
11
|
-
import { join } from 'path'
|
|
12
26
|
import { randomBytes } from 'crypto'
|
|
13
27
|
import { writeFile, unlink } from 'fs/promises'
|
|
14
28
|
import { existsSync } from 'fs'
|
|
15
29
|
import { KubeConfig } from '@kubernetes/client-node'
|
|
16
30
|
import * as semver from 'semver'
|
|
31
|
+
import { PACKAGE_VERSION } from './src/version.js'
|
|
32
|
+
import { registerBuildCommand } from './src/buildCommand.js'
|
|
17
33
|
|
|
18
34
|
/**
|
|
19
|
-
*
|
|
35
|
+
* Base URL of StackAPI infra to use -- defaults to p1.metaplay.io. Override with the global --stack-api flag.
|
|
36
|
+
* Note: The dynamic `kubeconfig`s generated by `metaplay-auth` override this with '--stack-api <url>' flag.
|
|
37
|
+
*/
|
|
38
|
+
let defaultStackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a TargetEnvironment from a fully-qualified domain name (eg, 'idler-develop.p1.metaplay.io').
|
|
42
|
+
* @param tokens Tokens to use for authenticating with the portal
|
|
43
|
+
* @param environmentDomain The environment domain name (eg, 'idler-develop.p1.metaplay.io')
|
|
44
|
+
* @returns The TargetEnvironment instance needed to operate with the environment.
|
|
45
|
+
*/
|
|
46
|
+
function resolveTargetEnvironmentFromFQDN(tokens: TokenSet, environmentDomain: string): TargetEnvironment {
|
|
47
|
+
// Extract the humanId from the domain, eg: 'idler-develop.p1.metaplay.io' -> 'idler-develop'
|
|
48
|
+
const humanId = environmentDomain.split('.')[0]
|
|
49
|
+
return new TargetEnvironment(tokens.access_token, humanId, defaultStackApiBaseUrl) // \todo We could probably infer the stack API URL from the domain?
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Information about an environment received from the portal.
|
|
54
|
+
*/
|
|
55
|
+
// \todo Should share this type with the portal endpoint code that returns
|
|
56
|
+
interface PortalEnvironmentInfo {
|
|
57
|
+
/** UUID of the environment */
|
|
58
|
+
id: string
|
|
59
|
+
|
|
60
|
+
/** UUID of the project */
|
|
61
|
+
project_id: string
|
|
62
|
+
|
|
63
|
+
/** User-provided name for the environment (can change) */
|
|
64
|
+
name: string
|
|
65
|
+
|
|
66
|
+
/** TODO: What is this URL? */
|
|
67
|
+
url: string
|
|
68
|
+
|
|
69
|
+
/** Slug for the environment (simplified version of name) */
|
|
70
|
+
slug: string
|
|
71
|
+
|
|
72
|
+
/** Creation time of the environment (eg, '2024-02-02T08:20:18.457748+00:00') */
|
|
73
|
+
created_at: string
|
|
74
|
+
|
|
75
|
+
/** Type of the environment (eg, 'development' or 'production') */
|
|
76
|
+
type: string
|
|
77
|
+
|
|
78
|
+
/** Immutable human-readable identifier for the environment (eg, 'delicious-pumpkin' .. can also be legacy name like 'idler-develop') */
|
|
79
|
+
// \todo Make field mandatory when portal returns valid values
|
|
80
|
+
human_id?: string
|
|
81
|
+
|
|
82
|
+
// \todo Add stackapi url that the portal should return?
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetch information about a specific managed environment from the Metaplay portal using slugs.
|
|
87
|
+
* @param tokens Tokens to use to for authenticating with the portal.
|
|
88
|
+
* @param organization Organization slug.
|
|
89
|
+
* @param project Project slug.
|
|
90
|
+
* @param environment Environment slug.
|
|
91
|
+
* @returns The portal's information about the environment.
|
|
92
|
+
*/
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
94
|
+
async function fetchManagedEnvironmentInfo(
|
|
95
|
+
tokens: TokenSet,
|
|
96
|
+
organization: string,
|
|
97
|
+
project: string,
|
|
98
|
+
environment: string
|
|
99
|
+
): Promise<PortalEnvironmentInfo> {
|
|
100
|
+
const url = `${portalBaseUrl}/api/v1/environments/with-slugs?organization_slug=${organization}&project_slug=${project}&environment_slug=${environment}`
|
|
101
|
+
logger.debug(`Getting environment information from portal: ${url}...`)
|
|
102
|
+
const response = await fetch(url, {
|
|
103
|
+
method: 'GET',
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${tokens.access_token}`,
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Throw on server errors (eg, forbidden)
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const errorData = await response.json()
|
|
113
|
+
throw new Error(`Failed to fetch environment details with error ${response.status}: ${JSON.stringify(errorData)}`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// \todo Validate response?
|
|
117
|
+
return (await response.json()) as PortalEnvironmentInfo
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve the target environment based on (org, proj, env) tuple. We fetch the information
|
|
122
|
+
* from the portal and then construct the `TargetEnvironment` class with the required information
|
|
123
|
+
* for downstream operations.
|
|
124
|
+
* @param tokens Tokens to use for authenticating with the portal
|
|
125
|
+
* @param organization Organization slug.
|
|
126
|
+
* @param project Project slug.
|
|
127
|
+
* @param environment Environment slug.
|
|
128
|
+
* @returns The TargetEnvironment instance needed to operate with the environment.
|
|
129
|
+
*/
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
131
|
+
async function resolveTargetEnvironmentFromSlugs(
|
|
132
|
+
tokens: TokenSet,
|
|
133
|
+
organization: string,
|
|
134
|
+
project: string,
|
|
135
|
+
environment: string
|
|
136
|
+
): Promise<TargetEnvironment> {
|
|
137
|
+
// Fetch the deployment information from the portal
|
|
138
|
+
const portalEnvInfo = await fetchManagedEnvironmentInfo(tokens, organization, project, environment)
|
|
139
|
+
// \todo Use legacy '<project>-<environment>' -- should be filled in by portal!
|
|
140
|
+
const humanId = portalEnvInfo.human_id ?? `${project}-${environment}`
|
|
141
|
+
|
|
142
|
+
return new TargetEnvironment(tokens.access_token, humanId, defaultStackApiBaseUrl)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function resolveTargetEnvironmentHumanId(tokens: TokenSet, humanId: string): Promise<TargetEnvironment> {
|
|
146
|
+
// \todo Validate that the target environment exists?
|
|
147
|
+
|
|
148
|
+
return new TargetEnvironment(tokens.access_token, humanId, defaultStackApiBaseUrl)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Helper for parsing the TargetEnvironment type from the command line arguments. Accepts either the gameserver address
|
|
20
153
|
* (idler-test.p1.metaplay.io), a shorthand address (metaplay-idler-test) or the (organization, project, environment)
|
|
21
154
|
* tuple from options.
|
|
22
155
|
*/
|
|
23
|
-
function
|
|
156
|
+
async function resolveTargetEnvironment(
|
|
157
|
+
address: string | undefined,
|
|
158
|
+
options: { organization?: string; project?: string; environment?: string }
|
|
159
|
+
): Promise<TargetEnvironment> {
|
|
160
|
+
const tokens = await loadTokens()
|
|
161
|
+
|
|
24
162
|
// If address is specified, use it, otherwise assume options has organization, project, and environment
|
|
25
163
|
if (address) {
|
|
164
|
+
// Address is one of:
|
|
165
|
+
// - FQDN of the target environment, eg, 'idler-develop.p1.metaplay.io'
|
|
166
|
+
// - Tuple of '<organization>-<project>-<environment>' slugs (eg, 'metaplay-idler-develop')
|
|
167
|
+
// - Stable humanId, eg, 'delicious-elephant'
|
|
26
168
|
if (isValidFQDN(address)) {
|
|
27
|
-
return
|
|
169
|
+
return resolveTargetEnvironmentFromFQDN(tokens, address)
|
|
28
170
|
} else {
|
|
29
171
|
const parts = address.split('-')
|
|
30
|
-
if (parts.length
|
|
31
|
-
|
|
172
|
+
if (parts.length === 2) {
|
|
173
|
+
// Two parts is humanId, eg, 'delicious-elephant'
|
|
174
|
+
return await resolveTargetEnvironmentHumanId(tokens, address)
|
|
175
|
+
} else if (parts.length === 3) {
|
|
176
|
+
// Three parts is tuple of slush '<organization>-<project>-<environment>'
|
|
177
|
+
return await resolveTargetEnvironmentFromSlugs(tokens, parts[0], parts[1], parts[2])
|
|
178
|
+
} else {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Invalid environment address syntax '${address}'. Specify either "<organization>-<project>-<environment>", a humanId (eg, "delicious-elephant"), or a fully-qualified domain name (eg, idler-develop.p1.metaplay.io)`
|
|
181
|
+
)
|
|
32
182
|
}
|
|
33
|
-
return { organization: parts[0], project: parts[1], environment: parts[2] }
|
|
34
183
|
}
|
|
35
184
|
} else if (options.organization && options.project && options.environment) {
|
|
36
|
-
|
|
185
|
+
// Parse tuple from command-line options (output to stderr to avoid messing up '$(eval metaplay-auth ... --format env)' invocations
|
|
186
|
+
console.warn(
|
|
187
|
+
`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.`
|
|
188
|
+
)
|
|
189
|
+
return await resolveTargetEnvironmentFromSlugs(tokens, options.organization, options.project, options.environment)
|
|
37
190
|
} else {
|
|
38
|
-
throw new Error(
|
|
191
|
+
throw new Error(
|
|
192
|
+
'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.'
|
|
193
|
+
)
|
|
39
194
|
}
|
|
40
195
|
}
|
|
41
196
|
|
|
@@ -44,8 +199,13 @@ const program = new Command()
|
|
|
44
199
|
program
|
|
45
200
|
.name('metaplay-auth')
|
|
46
201
|
.description('Authenticate with Metaplay and get AWS and Kubernetes credentials for game servers.')
|
|
47
|
-
.version(
|
|
202
|
+
.version(PACKAGE_VERSION)
|
|
48
203
|
.option('-d, --debug', 'enable debug output')
|
|
204
|
+
.option('--portal-base-url <portal-base-url>', 'override the default portal base URL (e.g. http://localhost:3000)')
|
|
205
|
+
.option(
|
|
206
|
+
'--stack-api <stack-api-base-url>',
|
|
207
|
+
'override the default stack API base URL (e.g. https://infra.p1.metaplay.io/stackapi/)'
|
|
208
|
+
)
|
|
49
209
|
.hook('preAction', (thisCommand) => {
|
|
50
210
|
// Handle debug flag for all commands.
|
|
51
211
|
const opts = thisCommand.opts()
|
|
@@ -54,24 +214,42 @@ program
|
|
|
54
214
|
} else {
|
|
55
215
|
setLogLevel(10)
|
|
56
216
|
}
|
|
217
|
+
|
|
218
|
+
// Store the portal base URL for accessing globally
|
|
219
|
+
if (opts.portalBaseUrl) {
|
|
220
|
+
setPortalBaseUrl(opts.portalBaseUrl as string)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Store the stack API base URL for accessing globally
|
|
224
|
+
if (opts.stackApi) {
|
|
225
|
+
defaultStackApiBaseUrl = opts.stackApi as string
|
|
226
|
+
}
|
|
57
227
|
})
|
|
58
228
|
|
|
59
|
-
program
|
|
229
|
+
program
|
|
230
|
+
.command('login')
|
|
60
231
|
.description('login to your Metaplay account')
|
|
61
232
|
.action(async () => {
|
|
62
233
|
await loginAndSaveTokens()
|
|
63
234
|
})
|
|
64
235
|
|
|
65
|
-
program
|
|
66
|
-
.
|
|
67
|
-
.
|
|
236
|
+
program
|
|
237
|
+
.command('machine-login')
|
|
238
|
+
.description(
|
|
239
|
+
'login to the Metaplay cloud using a machine account (using credentials in environment variable METAPLAY_CREDENTIALS)'
|
|
240
|
+
)
|
|
241
|
+
.option(
|
|
242
|
+
'--dev-credentials',
|
|
243
|
+
'machine user credentials to use, only for dev purposes, use METAPLAY_CREDENTIALS env variable for better safety!'
|
|
244
|
+
)
|
|
68
245
|
.action(async (options) => {
|
|
69
246
|
// Get credentials from command line or from METAPLAY_CREDENTIALS environment variable
|
|
70
|
-
let credentials
|
|
247
|
+
let credentials: string
|
|
71
248
|
if (options.devCredentials) {
|
|
72
249
|
credentials = options.devCredentials
|
|
73
250
|
} else {
|
|
74
|
-
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
252
|
+
credentials = process.env.METAPLAY_CREDENTIALS!
|
|
75
253
|
if (!credentials || credentials === '') {
|
|
76
254
|
throw new Error('Unable to find the credentials, the environment variable METAPLAY_CREDENTIALS is not defined!')
|
|
77
255
|
}
|
|
@@ -81,7 +259,9 @@ program.command('machine-login')
|
|
|
81
259
|
// \note We can't be certain that the secret does not contain pluses so split at the first occurrence
|
|
82
260
|
const splitOffset = credentials.indexOf('+')
|
|
83
261
|
if (splitOffset === -1) {
|
|
84
|
-
throw new Error(
|
|
262
|
+
throw new Error(
|
|
263
|
+
'Invalid format for credentials, you should copy-paste the value from the developer portal verbatim'
|
|
264
|
+
)
|
|
85
265
|
}
|
|
86
266
|
const clientId = credentials.substring(0, splitOffset)
|
|
87
267
|
const clientSecret = credentials.substring(splitOffset + 1)
|
|
@@ -90,7 +270,8 @@ program.command('machine-login')
|
|
|
90
270
|
await machineLoginAndSaveTokens(clientId, clientSecret)
|
|
91
271
|
})
|
|
92
272
|
|
|
93
|
-
program
|
|
273
|
+
program
|
|
274
|
+
.command('logout')
|
|
94
275
|
.description('log out of your Metaplay account')
|
|
95
276
|
.action(async () => {
|
|
96
277
|
console.log('Logging out by removing locally stored tokens...')
|
|
@@ -107,16 +288,17 @@ program.command('logout')
|
|
|
107
288
|
}
|
|
108
289
|
})
|
|
109
290
|
|
|
110
|
-
program
|
|
291
|
+
program
|
|
292
|
+
.command('show-tokens')
|
|
111
293
|
.description('show loaded tokens')
|
|
112
294
|
.hook('preAction', async () => {
|
|
113
295
|
await extendCurrentSession()
|
|
114
296
|
})
|
|
115
|
-
.action(async (
|
|
297
|
+
.action(async () => {
|
|
116
298
|
try {
|
|
117
299
|
// TODO: Could detect if not logged in and fail more gracefully?
|
|
118
300
|
const tokens = await loadTokens()
|
|
119
|
-
console.log(tokens)
|
|
301
|
+
console.log(JSON.stringify(tokens, undefined, 2))
|
|
120
302
|
} catch (error) {
|
|
121
303
|
if (error instanceof Error) {
|
|
122
304
|
console.error(`Error showing tokens: ${error.message}`)
|
|
@@ -125,55 +307,62 @@ program.command('show-tokens')
|
|
|
125
307
|
}
|
|
126
308
|
})
|
|
127
309
|
|
|
128
|
-
program
|
|
310
|
+
program
|
|
311
|
+
.command('get-kubeconfig')
|
|
129
312
|
.description('get kubeconfig for target environment')
|
|
130
313
|
.argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
131
|
-
.option('-
|
|
132
|
-
.option('-
|
|
133
|
-
.option('-
|
|
134
|
-
.option('-e, --environment <environment>', 'environment name (e.g. develop)')
|
|
314
|
+
.option('-o, --organization <organization>', '[deprecated] organization name (e.g. metaplay)')
|
|
315
|
+
.option('-p, --project <project>', '[deprecated] project name (e.g. idler)')
|
|
316
|
+
.option('-e, --environment <environment>', '[deprecated] environment name (e.g. develop)')
|
|
135
317
|
.option('-t, --type <credentials-type>', 'type of credentials handling in kubeconfig (static or dynamic)')
|
|
136
|
-
.option(
|
|
318
|
+
.option(
|
|
319
|
+
'--output <kubeconfig-path>',
|
|
320
|
+
'path of the output file where to write kubeconfig (written to stdout if not specified)'
|
|
321
|
+
)
|
|
137
322
|
.hook('preAction', async () => {
|
|
138
323
|
await extendCurrentSession()
|
|
139
324
|
})
|
|
140
|
-
.action(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
kubeconfigPayload
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
325
|
+
.action(
|
|
326
|
+
async (
|
|
327
|
+
gameserver: string | undefined,
|
|
328
|
+
options: { organization?: string; project?: string; environment?: string; type?: string; output?: string }
|
|
329
|
+
) => {
|
|
330
|
+
try {
|
|
331
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
332
|
+
|
|
333
|
+
// Default to credentialsType==dynamic for human users, and credentialsType==static for machine users
|
|
334
|
+
const tokens = await loadTokens()
|
|
335
|
+
const isHumanUser = !!tokens.refresh_token
|
|
336
|
+
const credentialsType = options.type ?? (isHumanUser ? 'dynamic' : 'static')
|
|
337
|
+
|
|
338
|
+
// Generate kubeconfig
|
|
339
|
+
let kubeconfigPayload
|
|
340
|
+
if (credentialsType === 'dynamic') {
|
|
341
|
+
logger.debug('Fetching kubeconfig with execcredential')
|
|
342
|
+
kubeconfigPayload = await targetEnv.getKubeConfigWithExecCredential()
|
|
343
|
+
} else if (credentialsType === 'static') {
|
|
344
|
+
logger.debug('Fetching kubeconfig with embedded secret')
|
|
345
|
+
kubeconfigPayload = await targetEnv.getKubeConfigWithEmbeddedCredentials()
|
|
346
|
+
} else {
|
|
347
|
+
throw new Error('Invalid credentials type; must be either "static" or "dynamic"')
|
|
348
|
+
}
|
|
161
349
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
350
|
+
// Write kubeconfig to output (file or stdout)
|
|
351
|
+
if (options.output) {
|
|
352
|
+
logger.debug(`Writing kubeconfig to file ${options.output}`)
|
|
353
|
+
await writeFile(options.output, kubeconfigPayload, { mode: 0o600 })
|
|
354
|
+
console.log(`Wrote kubeconfig to ${options.output}`)
|
|
355
|
+
} else {
|
|
356
|
+
console.log(kubeconfigPayload)
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (error instanceof Error) {
|
|
360
|
+
console.error('Error getting KubeConfig:', error)
|
|
361
|
+
}
|
|
362
|
+
exit(1)
|
|
173
363
|
}
|
|
174
|
-
exit(1)
|
|
175
364
|
}
|
|
176
|
-
|
|
365
|
+
)
|
|
177
366
|
|
|
178
367
|
/**
|
|
179
368
|
* Get the Kubernetes credentials in the execcredential format which can be used within the `kubeconfig` file:
|
|
@@ -183,473 +372,583 @@ program.command('get-kubeconfig')
|
|
|
183
372
|
* kubeconfig that uses this command.
|
|
184
373
|
*/
|
|
185
374
|
// todo: maybe this should be a hidden command as it's not very useful for end users and clutters help?
|
|
186
|
-
program
|
|
375
|
+
program
|
|
376
|
+
.command('get-kubernetes-execcredential')
|
|
187
377
|
.description('[internal] get kubernetes credentials in execcredential format (used from the generated kubeconfigs)')
|
|
188
378
|
.argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
189
|
-
.option('-
|
|
190
|
-
.option('-
|
|
191
|
-
.option('-
|
|
192
|
-
.option('-e, --environment <environment>', 'environment name (e.g. develop)')
|
|
379
|
+
.option('-o, --organization <organization>', '[deprecated] organization name (e.g. metaplay)')
|
|
380
|
+
.option('-p, --project <project>', '[deprecated] project name (e.g. idler)')
|
|
381
|
+
.option('-e, --environment <environment>', '[deprecated] environment name (e.g. develop)')
|
|
193
382
|
.hook('preAction', async () => {
|
|
194
383
|
await extendCurrentSession()
|
|
195
384
|
})
|
|
196
|
-
.action(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
385
|
+
.action(
|
|
386
|
+
async (
|
|
387
|
+
gameserver: string | undefined,
|
|
388
|
+
options: { organization?: string; project?: string; environment?: string }
|
|
389
|
+
) => {
|
|
390
|
+
try {
|
|
391
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
392
|
+
const credentials = await targetEnv.getKubeExecCredential()
|
|
393
|
+
console.log(credentials)
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error instanceof Error) {
|
|
396
|
+
console.error(`Error getting Kubernetes ExecCredential: ${error.message}`)
|
|
397
|
+
}
|
|
398
|
+
exit(1)
|
|
207
399
|
}
|
|
208
|
-
exit(1)
|
|
209
400
|
}
|
|
210
|
-
|
|
401
|
+
)
|
|
211
402
|
|
|
212
|
-
program
|
|
403
|
+
program
|
|
404
|
+
.command('get-aws-credentials')
|
|
213
405
|
.description('get AWS credentials for target environment')
|
|
214
406
|
.argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
215
|
-
.option('-
|
|
216
|
-
.option('-
|
|
217
|
-
.option('-
|
|
218
|
-
.option('-e, --environment <environment>', 'environment name (e.g. develop)')
|
|
407
|
+
.option('-o, --organization <organization>', '[deprecated] organization name (e.g. metaplay)')
|
|
408
|
+
.option('-p, --project <project>', '[deprecated] project name (e.g. idler)')
|
|
409
|
+
.option('-e, --environment <environment>', '[deprecated] environment name (e.g. develop)')
|
|
219
410
|
.option('-f, --format <format>', 'output format (json or env)', 'json')
|
|
220
411
|
.hook('preAction', async () => {
|
|
221
412
|
await extendCurrentSession()
|
|
222
413
|
})
|
|
223
|
-
.action(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const credentials = await stackApi.getAwsCredentials(gameserverId)
|
|
414
|
+
.action(
|
|
415
|
+
async (
|
|
416
|
+
gameserver: string | undefined,
|
|
417
|
+
options: { organization?: string; project?: string; environment?: string; format?: string }
|
|
418
|
+
) => {
|
|
419
|
+
try {
|
|
420
|
+
if (options.format !== 'json' && options.format !== 'env') {
|
|
421
|
+
throw new Error('Invalid format; must be one of json or env')
|
|
422
|
+
}
|
|
234
423
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
424
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
425
|
+
|
|
426
|
+
// Get the AWS credentials
|
|
427
|
+
const credentials = await targetEnv.getAwsCredentials()
|
|
428
|
+
|
|
429
|
+
if (options.format === 'env') {
|
|
430
|
+
console.log(`export AWS_ACCESS_KEY_ID=${credentials.AccessKeyId}`)
|
|
431
|
+
console.log(`export AWS_SECRET_ACCESS_KEY=${credentials.SecretAccessKey}`)
|
|
432
|
+
console.log(`export AWS_SESSION_TOKEN=${credentials.SessionToken}`)
|
|
433
|
+
} else {
|
|
434
|
+
console.log(
|
|
435
|
+
JSON.stringify({
|
|
436
|
+
...credentials,
|
|
437
|
+
Version: 1, // this is needed to comply with `aws` format for external credential providers
|
|
438
|
+
})
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (error instanceof Error) {
|
|
443
|
+
console.error(`Error getting AWS credentials: ${error.message}`)
|
|
444
|
+
}
|
|
445
|
+
exit(1)
|
|
248
446
|
}
|
|
249
|
-
exit(1)
|
|
250
447
|
}
|
|
251
|
-
|
|
448
|
+
)
|
|
252
449
|
|
|
253
|
-
program
|
|
254
|
-
.
|
|
450
|
+
program
|
|
451
|
+
.command('get-docker-login')
|
|
452
|
+
.description('[deprecated] get docker login credentials for pushing the server image to target environment')
|
|
255
453
|
.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)')
|
|
454
|
+
.option('-o, --organization <organization>', '[deprecated] organization name (e.g. metaplay)')
|
|
455
|
+
.option('-p, --project <project>', '[deprecated] project name (e.g. idler)')
|
|
456
|
+
.option('-e, --environment <environment>', '[deprecated] environment name (e.g. develop)')
|
|
259
457
|
.option('-f, --format <format>', 'output format (json or env)', 'json')
|
|
260
458
|
.hook('preAction', async () => {
|
|
261
459
|
await extendCurrentSession()
|
|
262
460
|
})
|
|
263
|
-
.action(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
461
|
+
.action(
|
|
462
|
+
async (
|
|
463
|
+
gameserver: string | undefined,
|
|
464
|
+
options: { organization?: string; project?: string; environment?: string; format?: string }
|
|
465
|
+
) => {
|
|
466
|
+
try {
|
|
467
|
+
if (options.format !== 'json' && options.format !== 'env') {
|
|
468
|
+
throw new Error('Invalid format; must be one of json or env')
|
|
469
|
+
}
|
|
268
470
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
471
|
+
console.warn('The get-docker-login command is deprecated! Use the push-docker-image command instead.')
|
|
472
|
+
|
|
473
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
474
|
+
|
|
475
|
+
// Get environment info (region is needed for ECR)
|
|
476
|
+
logger.debug('Get environment info')
|
|
477
|
+
const environment = await targetEnv.getEnvironmentDetails()
|
|
478
|
+
const dockerRepo = environment.deployment.ecr_repo
|
|
479
|
+
|
|
480
|
+
// Resolve docker credentials for remote registry
|
|
481
|
+
logger.debug('Get docker credentials')
|
|
482
|
+
const dockerCredentials = await targetEnv.getDockerCredentials()
|
|
483
|
+
const { username, password } = dockerCredentials
|
|
484
|
+
|
|
485
|
+
// Output the docker repo & credentials
|
|
486
|
+
if (options.format === 'env') {
|
|
487
|
+
console.log(`export DOCKER_REPO=${dockerRepo}`)
|
|
488
|
+
console.log(`export DOCKER_USERNAME=${username}`)
|
|
489
|
+
console.log(`export DOCKER_PASSWORD=${password}`)
|
|
490
|
+
} else {
|
|
491
|
+
console.log(
|
|
492
|
+
JSON.stringify({
|
|
493
|
+
dockerRepo,
|
|
494
|
+
username,
|
|
495
|
+
password,
|
|
496
|
+
})
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
if (error instanceof Error) {
|
|
501
|
+
console.error(`Error getting docker login credentials: ${error.message}`)
|
|
502
|
+
}
|
|
503
|
+
exit(1)
|
|
298
504
|
}
|
|
299
|
-
exit(1)
|
|
300
505
|
}
|
|
301
|
-
|
|
506
|
+
)
|
|
302
507
|
|
|
303
|
-
program
|
|
508
|
+
program
|
|
509
|
+
.command('get-environment')
|
|
304
510
|
.description('get details of an environment')
|
|
305
511
|
.argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
306
|
-
.option('-
|
|
307
|
-
.option('-
|
|
308
|
-
.option('-
|
|
309
|
-
.option('-e, --environment <environment>', 'environment name (e.g. develop)')
|
|
512
|
+
.option('-o, --organization <organization>', '[deprecated] organization name (e.g. metaplay)')
|
|
513
|
+
.option('-p, --project <project>', '[deprecated] project name (e.g. idler)')
|
|
514
|
+
.option('-e, --environment <environment>', '[deprecated] environment name (e.g. develop)')
|
|
310
515
|
.hook('preAction', async () => {
|
|
311
516
|
await extendCurrentSession()
|
|
312
517
|
})
|
|
313
|
-
.action(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
518
|
+
.action(
|
|
519
|
+
async (
|
|
520
|
+
gameserver: string | undefined,
|
|
521
|
+
options: { organization?: string; project?: string; environment?: string }
|
|
522
|
+
) => {
|
|
523
|
+
try {
|
|
524
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
318
525
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
526
|
+
const environment = await targetEnv.getEnvironmentDetails()
|
|
527
|
+
console.log(JSON.stringify(environment, undefined, 2))
|
|
528
|
+
} catch (error) {
|
|
529
|
+
if (error instanceof Error) {
|
|
530
|
+
console.error(`Error getting environment details: ${error.message}`)
|
|
531
|
+
}
|
|
532
|
+
exit(1)
|
|
324
533
|
}
|
|
325
|
-
exit(1)
|
|
326
534
|
}
|
|
327
|
-
|
|
535
|
+
)
|
|
328
536
|
|
|
329
|
-
program
|
|
537
|
+
program
|
|
538
|
+
.command('push-docker-image')
|
|
330
539
|
.description('push docker image into the target environment image registry')
|
|
331
540
|
.argument('gameserver', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
332
541
|
.argument('image-name', 'full name of the docker image to push (eg, the gameserver:<sha>)')
|
|
333
542
|
.hook('preAction', async () => {
|
|
334
543
|
await extendCurrentSession()
|
|
335
544
|
})
|
|
336
|
-
.action(
|
|
337
|
-
|
|
338
|
-
|
|
545
|
+
.action(
|
|
546
|
+
async (
|
|
547
|
+
gameserver: string | undefined,
|
|
548
|
+
imageName: string | undefined,
|
|
549
|
+
options: { organization?: string; project?: string; environment?: string; imageTag?: string }
|
|
550
|
+
) => {
|
|
551
|
+
try {
|
|
552
|
+
console.log(`Pushing docker image ${imageName} to target environment ${gameserver}...`)
|
|
339
553
|
|
|
340
|
-
|
|
341
|
-
const stackApi = new StackAPI(tokens.access_token, options.stackApi)
|
|
342
|
-
const gameserverId = resolveGameserverId(gameserver, options)
|
|
554
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
343
555
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
556
|
+
// Get environment info (region is needed for ECR)
|
|
557
|
+
logger.debug('Get environment info')
|
|
558
|
+
const envInfo = await targetEnv.getEnvironmentDetails()
|
|
347
559
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
560
|
+
// Resolve docker credentials for remote registry
|
|
561
|
+
logger.debug('Get docker credentials')
|
|
562
|
+
const dockerCredentials = await targetEnv.getDockerCredentials()
|
|
351
563
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
564
|
+
// Resolve tag from src image
|
|
565
|
+
if (!imageName) {
|
|
566
|
+
throw new Error('Must specify a valid docker image name as the image-name argument')
|
|
567
|
+
}
|
|
568
|
+
const srcImageParts = imageName.split(':')
|
|
569
|
+
if (srcImageParts.length !== 2 || srcImageParts[0].length === 0 || srcImageParts[1].length === 0) {
|
|
570
|
+
throw new Error(`Invalid docker image name '${imageName}', expecting the name in format 'name:tag'`)
|
|
571
|
+
}
|
|
572
|
+
const imageTag = srcImageParts[1]
|
|
573
|
+
|
|
574
|
+
// Resolve source image
|
|
575
|
+
const srcImageName = imageName
|
|
576
|
+
const dstRepoName = envInfo.deployment.ecr_repo
|
|
577
|
+
const dstImageName = `${dstRepoName}:${imageTag}`
|
|
578
|
+
const dockerApi = new Docker()
|
|
579
|
+
const srcDockerImage = dockerApi.getImage(srcImageName)
|
|
580
|
+
|
|
581
|
+
// If names don't match, tag the src image as dst
|
|
582
|
+
if (srcImageName !== dstImageName) {
|
|
583
|
+
logger.debug(`Tagging image ${srcImageName} as ${dstImageName}`)
|
|
584
|
+
await srcDockerImage.tag({ repo: dstRepoName, tag: imageTag })
|
|
585
|
+
}
|
|
374
586
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
587
|
+
// Push the image
|
|
588
|
+
logger.debug(`Push image ${dstImageName}`)
|
|
589
|
+
const dstDockerImage = dockerApi.getImage(dstImageName)
|
|
590
|
+
const authConfig = {
|
|
591
|
+
username: dockerCredentials.username,
|
|
592
|
+
password: dockerCredentials.password,
|
|
593
|
+
serveraddress: dockerCredentials.registryUrl,
|
|
594
|
+
}
|
|
595
|
+
const pushStream = await dstDockerImage.push({ authconfig: authConfig, tag: options.imageTag })
|
|
596
|
+
|
|
597
|
+
// Follow push progress & wait until completed
|
|
598
|
+
logger.debug('Following image push stream...')
|
|
599
|
+
await new Promise((resolve, reject) => {
|
|
600
|
+
dockerApi.modem.followProgress(
|
|
601
|
+
pushStream,
|
|
602
|
+
(error: Error | null, result: any[]) => {
|
|
603
|
+
if (error) {
|
|
604
|
+
logger.debug('Failed to push docker image to target repository:', error)
|
|
605
|
+
reject(error)
|
|
606
|
+
} else {
|
|
607
|
+
// result contains an array of all the progress objects
|
|
608
|
+
logger.debug('Succesfully finished pushing docker image')
|
|
609
|
+
resolve(result)
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
() => {
|
|
613
|
+
// console.log('Progress:', obj)
|
|
614
|
+
// { status: 'Preparing', progressDetail: {}, id: '82730adcaeb0' }
|
|
615
|
+
// { status: 'Layer already exists', progressDetail: {}, id: '7cd701fff13a' }
|
|
394
616
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
} catch (error) {
|
|
405
|
-
if (error instanceof Error) {
|
|
406
|
-
console.error(`Failed to push docker image: ${error.message}`)
|
|
617
|
+
)
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
console.log(`Successfully pushed docker image to ${dstImageName}!`)
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (error instanceof Error) {
|
|
623
|
+
console.error(`Failed to push docker image: ${error.message}`)
|
|
624
|
+
}
|
|
625
|
+
exit(1)
|
|
407
626
|
}
|
|
408
|
-
exit(1)
|
|
409
627
|
}
|
|
410
|
-
|
|
628
|
+
)
|
|
411
629
|
|
|
412
|
-
program
|
|
630
|
+
program
|
|
631
|
+
.command('deploy-server')
|
|
413
632
|
.description('deploy a game server image to target environment')
|
|
414
|
-
.argument('gameserver', 'address of gameserver (e.g. idler-develop.p1.metaplay.io)')
|
|
633
|
+
.argument('gameserver', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
415
634
|
.argument('image-tag', 'docker image tag to deploy (usually the SHA of the build)')
|
|
416
|
-
.
|
|
417
|
-
.option(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
635
|
+
.requiredOption('-f, --values <path-to-values-file>', 'path to Helm values file to use for this deployment')
|
|
636
|
+
.option(
|
|
637
|
+
'--local-chart-path <path-to-chart-directory>',
|
|
638
|
+
'path to local Helm chart directory (to use a chart from local disk)'
|
|
639
|
+
)
|
|
640
|
+
.option(
|
|
641
|
+
'--helm-chart-repo <url>',
|
|
642
|
+
'override the URL of the Helm chart repository (eg, https://charts.metaplay.dev/testing)'
|
|
643
|
+
)
|
|
644
|
+
.option('--helm-chart-version <version>', 'the Helm chart version to use (eg, 0.6.0)')
|
|
421
645
|
.option('--deployment-name', 'Helm deployment name to use', 'gameserver')
|
|
422
646
|
.hook('preAction', async () => {
|
|
423
647
|
await extendCurrentSession()
|
|
424
648
|
})
|
|
425
|
-
.action(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
649
|
+
.action(
|
|
650
|
+
async (
|
|
651
|
+
gameserver: string | undefined,
|
|
652
|
+
imageTag: string | undefined,
|
|
653
|
+
options: {
|
|
654
|
+
organization?: string
|
|
655
|
+
project?: string
|
|
656
|
+
environment?: string
|
|
657
|
+
values: string
|
|
658
|
+
localChartPath?: string
|
|
659
|
+
helmChartRepo?: string
|
|
660
|
+
helmChartVersion?: string
|
|
661
|
+
deploymentName?: string
|
|
662
|
+
}
|
|
663
|
+
) => {
|
|
664
|
+
try {
|
|
665
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
432
666
|
|
|
433
|
-
|
|
434
|
-
const envInfo = await stackApi.getEnvironmentDetails(gameserverId)
|
|
667
|
+
console.log(`Deploying server to ${gameserver} with image tag ${imageTag}...`)
|
|
435
668
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
669
|
+
// Fetch target environment details
|
|
670
|
+
const envInfo = await targetEnv.getEnvironmentDetails()
|
|
439
671
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
// Resolve information about docker image
|
|
448
|
-
// const dockerApi = new Docker({
|
|
449
|
-
// host: dockerCredentials.registryUrl,
|
|
450
|
-
// port: 443,
|
|
451
|
-
// protocol: 'https',
|
|
452
|
-
// username: dockerCredentials.username,
|
|
453
|
-
// headers: {
|
|
454
|
-
// Authorization: `Bearer ${dockerCredentials.password}`,
|
|
455
|
-
// Host: dockerCredentials.registryUrl.replace('https://', ''),
|
|
456
|
-
// }
|
|
457
|
-
// })
|
|
458
|
-
const dockerApi = new Docker()
|
|
459
|
-
const dockerRepo = envInfo.deployment.ecr_repo
|
|
460
|
-
const imageName = `${dockerRepo}:${imageTag}`
|
|
461
|
-
logger.debug(`Fetch docker image labels for ${imageName}`)
|
|
462
|
-
let imageLabels
|
|
463
|
-
try {
|
|
464
|
-
const localDockerImage = dockerApi.getImage(imageName)
|
|
465
|
-
imageLabels = (await localDockerImage.inspect()).Config.Labels || {}
|
|
466
|
-
} catch (err) {
|
|
467
|
-
logger.debug(`Failed to resolve docker image metadata from local image ${imageName}: ${err}`)
|
|
468
|
-
}
|
|
672
|
+
if (!imageTag) {
|
|
673
|
+
throw new Error(
|
|
674
|
+
'Must specify a valid docker image tag as the image-tag argument, usually the SHA of the build'
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
// \todo validate that imageTag is just the version part (i.e, contains no ':')
|
|
469
678
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
console.log(`Image ${imageName} not found locally -- pulling docker image from target environment registry...`)
|
|
475
|
-
const authConfig = { username: dockerCredentials.username, password: dockerCredentials.password, serveraddress: dockerCredentials.registryUrl }
|
|
476
|
-
const pullStream = await dockerApi.pull(imageName, { authconfig: authConfig })
|
|
477
|
-
|
|
478
|
-
// Follow pull progress & wait until completed
|
|
479
|
-
logger.debug('Following image pull stream...')
|
|
480
|
-
await new Promise((resolve, reject) => {
|
|
481
|
-
dockerApi.modem.followProgress(
|
|
482
|
-
pullStream,
|
|
483
|
-
(error: Error | null, result: any[]) => {
|
|
484
|
-
if (error) {
|
|
485
|
-
logger.debug('Failed to pull image:', error)
|
|
486
|
-
reject(error)
|
|
487
|
-
} else {
|
|
488
|
-
// result contains an array of all the progress objects
|
|
489
|
-
logger.debug('Succesfully finished pulling image')
|
|
490
|
-
resolve(result)
|
|
491
|
-
}
|
|
492
|
-
},
|
|
493
|
-
(obj: any) => {
|
|
494
|
-
// console.log('Progress:', obj)
|
|
495
|
-
// { status: 'Preparing', progressDetail: {}, id: '82730adcaeb0' }
|
|
496
|
-
// { status: 'Layer already exists', progressDetail: {}, id: '7cd701fff13a' }
|
|
497
|
-
})
|
|
498
|
-
})
|
|
499
|
-
} catch (err) {
|
|
500
|
-
throw new Error(`Failed to fetch docker image ${imageName} from target environment registry: ${err}`)
|
|
679
|
+
if (!options.deploymentName) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`Invalid Helm deployment name '${options.deploymentName}'; specify one with --deployment-name or use the default`
|
|
682
|
+
)
|
|
501
683
|
}
|
|
502
684
|
|
|
503
|
-
//
|
|
685
|
+
// Fetch Docker credentials for target environment registry
|
|
686
|
+
const dockerCredentials = await targetEnv.getDockerCredentials()
|
|
687
|
+
|
|
688
|
+
// Resolve information about docker image
|
|
689
|
+
// const dockerApi = new Docker({
|
|
690
|
+
// host: dockerCredentials.registryUrl,
|
|
691
|
+
// port: 443,
|
|
692
|
+
// protocol: 'https',
|
|
693
|
+
// username: dockerCredentials.username,
|
|
694
|
+
// headers: {
|
|
695
|
+
// Authorization: `Bearer ${dockerCredentials.password}`,
|
|
696
|
+
// Host: dockerCredentials.registryUrl.replace('https://', ''),
|
|
697
|
+
// }
|
|
698
|
+
// })
|
|
699
|
+
const dockerApi = new Docker()
|
|
700
|
+
const dockerRepo = envInfo.deployment.ecr_repo
|
|
701
|
+
const imageName = `${dockerRepo}:${imageTag}`
|
|
702
|
+
logger.debug(`Fetch docker image labels for ${imageName}`)
|
|
703
|
+
let imageLabels
|
|
504
704
|
try {
|
|
505
|
-
logger.debug('Get docker labels again (with pulled image)')
|
|
506
705
|
const localDockerImage = dockerApi.getImage(imageName)
|
|
507
706
|
imageLabels = (await localDockerImage.inspect()).Config.Labels || {}
|
|
508
|
-
} catch (
|
|
509
|
-
|
|
707
|
+
} catch (error) {
|
|
708
|
+
const errMessage = error instanceof Error ? error.message : String(error)
|
|
709
|
+
logger.debug(`Failed to resolve docker image metadata from local image ${imageName}: ${errMessage}`)
|
|
510
710
|
}
|
|
511
|
-
}
|
|
512
711
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
712
|
+
// If wasn't able to resolve the images, pull image from the target environment registry & resolve labels
|
|
713
|
+
if (imageLabels === undefined) {
|
|
714
|
+
// Pull the image from remote registry
|
|
715
|
+
try {
|
|
716
|
+
console.log(
|
|
717
|
+
`Image ${imageName} not found locally -- pulling docker image from target environment registry...`
|
|
718
|
+
)
|
|
719
|
+
const authConfig = {
|
|
720
|
+
username: dockerCredentials.username,
|
|
721
|
+
password: dockerCredentials.password,
|
|
722
|
+
serveraddress: dockerCredentials.registryUrl,
|
|
723
|
+
}
|
|
724
|
+
const pullStream = await dockerApi.pull(imageName, { authconfig: authConfig })
|
|
725
|
+
|
|
726
|
+
// Follow pull progress & wait until completed
|
|
727
|
+
logger.debug('Following image pull stream...')
|
|
728
|
+
await new Promise((resolve, reject) => {
|
|
729
|
+
dockerApi.modem.followProgress(
|
|
730
|
+
pullStream,
|
|
731
|
+
(error: Error | null, result: any[]) => {
|
|
732
|
+
if (error) {
|
|
733
|
+
logger.debug('Failed to pull image:', error)
|
|
734
|
+
reject(error)
|
|
735
|
+
} else {
|
|
736
|
+
// result contains an array of all the progress objects
|
|
737
|
+
logger.debug('Succesfully finished pulling image')
|
|
738
|
+
resolve(result)
|
|
739
|
+
}
|
|
740
|
+
},
|
|
741
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
742
|
+
(obj: any) => {
|
|
743
|
+
// console.log('Progress:', obj)
|
|
744
|
+
// { status: 'Preparing', progressDetail: {}, id: '82730adcaeb0' }
|
|
745
|
+
// { status: 'Layer already exists', progressDetail: {}, id: '7cd701fff13a' }
|
|
746
|
+
}
|
|
747
|
+
)
|
|
748
|
+
})
|
|
749
|
+
} catch (error) {
|
|
750
|
+
const errMessage = error instanceof Error ? error.message : String(error)
|
|
751
|
+
throw new Error(`Failed to fetch docker image ${imageName} from target environment registry: ${errMessage}`)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Resolve the labels
|
|
755
|
+
try {
|
|
756
|
+
logger.debug('Get docker labels again (with pulled image)')
|
|
757
|
+
const localDockerImage = dockerApi.getImage(imageName)
|
|
758
|
+
imageLabels = (await localDockerImage.inspect()).Config.Labels || {}
|
|
759
|
+
} catch (error) {
|
|
760
|
+
const errMessage = error instanceof Error ? error.message : String(error)
|
|
761
|
+
throw new Error(`Failed to resolve docker image metadata from pulled image ${imageName}: ${errMessage}`)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
532
764
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
765
|
+
// Try to resolve SDK version and Helm repo and chart version from the docker labels
|
|
766
|
+
// \note These only exist for SDK R28 and newer images
|
|
767
|
+
logger.debug('Docker image labels: ', JSON.stringify(imageLabels))
|
|
768
|
+
const sdkVersion = imageLabels['io.metaplay.sdk_version']
|
|
769
|
+
|
|
770
|
+
// Resolve helmChartRepo, in order of precedence:
|
|
771
|
+
// - Specified in the cli options
|
|
772
|
+
// - Specified in the docker image label
|
|
773
|
+
// - Fall back to 'https://charts.metaplay.dev'
|
|
774
|
+
const helmChartRepo = removeTrailingSlash(
|
|
775
|
+
options.helmChartRepo ?? imageLabels['io.metaplay.default_helm_repo'] ?? 'https://charts.metaplay.dev'
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
// Resolve helmChartVersion, in order of precedence:
|
|
779
|
+
// - Specified in the cli options
|
|
780
|
+
// - Specified in the docker image label
|
|
781
|
+
// - Unknown, error out!
|
|
782
|
+
const helmChartVersionSpec = options.helmChartVersion ?? imageLabels['io.metaplay.default_server_chart_version']
|
|
783
|
+
if (!options.helmChartVersion) {
|
|
784
|
+
console.warn('You should specify the Helm chart version with --helm-chart-version=<version>!')
|
|
785
|
+
}
|
|
786
|
+
if (!helmChartVersionSpec) {
|
|
787
|
+
throw new Error(
|
|
788
|
+
'No Helm chart version defined. With pre-R28 SDK versions, you must specify the Helm chart version explicitly with --helm-chart-version=<version>.'
|
|
789
|
+
)
|
|
790
|
+
}
|
|
537
791
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
792
|
+
// Parse the Helm chart version spec into a semver.Range, or null if 'latest' is specified
|
|
793
|
+
let helmChartRange: semver.Range | null = null
|
|
794
|
+
if (helmChartVersionSpec !== 'latest-prerelease') {
|
|
795
|
+
try {
|
|
796
|
+
helmChartRange = new semver.Range(helmChartVersionSpec)
|
|
797
|
+
} catch (error) {
|
|
798
|
+
throw new Error(`Helm chart version '${helmChartVersionSpec}' is not a valid SemVer range!`)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// A values file is required (at least for now it makes no sense to deploy without one)
|
|
803
|
+
if (!options.values) {
|
|
804
|
+
throw new Error('Path to a Helm values file must be specified with --values')
|
|
805
|
+
}
|
|
546
806
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const kubeconfigPath = join(tmpdir(), randomBytes(20).toString('hex'))
|
|
551
|
-
logger.debug(`Write temporary kubeconfig in ${kubeconfigPath}`)
|
|
552
|
-
await writeFile(kubeconfigPath, kubeconfigPayload, { mode: 0o600 })
|
|
807
|
+
// Resolve available metaplay-gameserver Helm chart versions
|
|
808
|
+
const availableHelmChartVersions = await fetchHelmChartVersions(helmChartRepo, 'metaplay-gameserver')
|
|
809
|
+
logger.debug(`Available Helm chart versions: ${availableHelmChartVersions.join(', ')}`)
|
|
553
810
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
811
|
+
// Resolve the best matching Helm chart version
|
|
812
|
+
const resolvedHelmChartVersion = resolveBestMatchingVersion(availableHelmChartVersions, helmChartRange)
|
|
813
|
+
if (!resolvedHelmChartVersion) {
|
|
814
|
+
throw new Error(
|
|
815
|
+
`No Helm chart version found that satisfies '${helmChartVersionSpec}' in repository '${helmChartRepo}'`
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
logger.debug('Resolved Helm chart version: ', resolvedHelmChartVersion)
|
|
819
|
+
|
|
820
|
+
// Fetch kubeconfig and write it to a temporary file
|
|
821
|
+
// \todo allow passing a custom kubeconfig file?
|
|
822
|
+
const kubeconfigPayload = await targetEnv.getKubeConfigWithEmbeddedCredentials()
|
|
823
|
+
const kubeconfigPath = pathJoin(tmpdir(), randomBytes(20).toString('hex'))
|
|
824
|
+
logger.debug(`Write temporary kubeconfig in ${kubeconfigPath}`)
|
|
825
|
+
await writeFile(kubeconfigPath, kubeconfigPayload, { mode: 0o600 })
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
// Construct Helm invocation
|
|
829
|
+
const chartNameOrPath = options.localChartPath ?? 'metaplay-gameserver'
|
|
830
|
+
const helmArgs = ['upgrade', '--install', '--wait'] // \note wait for the pods to stabilize -- otherwise status check can read state before any changes to pods are applied
|
|
559
831
|
.concat(['--kubeconfig', kubeconfigPath])
|
|
560
832
|
.concat(['-n', envInfo.deployment.kubernetes_namespace])
|
|
561
833
|
.concat(['--values', options.values])
|
|
562
834
|
.concat(['--set-string', `image.tag=${imageTag}`])
|
|
563
835
|
.concat(sdkVersion ? ['--set-string', `sdk.version=${sdkVersion}`] : [])
|
|
564
|
-
.concat(!options.
|
|
836
|
+
.concat(!options.localChartPath ? ['--repo', helmChartRepo, '--version', resolvedHelmChartVersion] : [])
|
|
565
837
|
.concat([options.deploymentName])
|
|
566
838
|
.concat([chartNameOrPath])
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
839
|
+
logger.info(`Execute: helm ${helmArgs.join(' ')}`)
|
|
840
|
+
|
|
841
|
+
// Execute Helm
|
|
842
|
+
let helmResult
|
|
843
|
+
try {
|
|
844
|
+
helmResult = await executeCommand('helm', helmArgs)
|
|
845
|
+
// \todo output something from Helm result?
|
|
846
|
+
} catch (error) {
|
|
847
|
+
const errMessage = error instanceof Error ? error.message : String(error)
|
|
848
|
+
throw new Error(
|
|
849
|
+
`Failed to execute 'helm': ${errMessage}. You need to have Helm v3 installed to deploy a game server with metaplay-auth.`
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Throw on Helm non-success exit code
|
|
854
|
+
if (helmResult.exitCode !== 0) {
|
|
855
|
+
throw new Error(`Helm deploy failed with exit code ${helmResult.exitCode}: ${String(helmResult.stderr)}`)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const testingRepoSuffix =
|
|
859
|
+
!options.localChartPath && helmChartRepo !== 'https://charts.metaplay.dev'
|
|
860
|
+
? ` from repo ${helmChartRepo}`
|
|
861
|
+
: ''
|
|
862
|
+
console.log(
|
|
863
|
+
`Game server deployed to ${gameserver} with tag ${imageTag} using chart version ${resolvedHelmChartVersion}${testingRepoSuffix}!`
|
|
864
|
+
)
|
|
865
|
+
} finally {
|
|
866
|
+
// Remove temporary kubeconfig file
|
|
867
|
+
await unlink(kubeconfigPath)
|
|
576
868
|
}
|
|
577
869
|
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
870
|
+
// Check the status of the game server deployment
|
|
871
|
+
try {
|
|
872
|
+
const kubeconfig = new KubeConfig()
|
|
873
|
+
kubeconfig.loadFromString(kubeconfigPayload)
|
|
874
|
+
|
|
875
|
+
console.log('Validating game server deployment...')
|
|
876
|
+
const exitCode = await checkGameServerDeployment(
|
|
877
|
+
envInfo.deployment.kubernetes_namespace,
|
|
878
|
+
kubeconfig,
|
|
879
|
+
imageTag
|
|
880
|
+
)
|
|
881
|
+
exit(exitCode)
|
|
882
|
+
} catch (error) {
|
|
883
|
+
const errMessage = error instanceof Error ? error.message : String(error)
|
|
884
|
+
console.error(`Failed to resolve game server deployment status: ${errMessage}`)
|
|
885
|
+
exit(2)
|
|
581
886
|
}
|
|
582
|
-
|
|
583
|
-
const testingRepoSuffix = (!options.chartPath && helmChartRepo !== 'https://charts.metaplay.dev') ? ` from repo ${helmChartRepo}` : ''
|
|
584
|
-
console.log(`Game server deployed to ${gameserver} with tag ${imageTag} using chart version ${helmChartVersion}${testingRepoSuffix}!`)
|
|
585
|
-
} finally {
|
|
586
|
-
// Remove temporary kubeconfig file
|
|
587
|
-
await unlink(kubeconfigPath)
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Check the status of the game server deployment
|
|
591
|
-
try {
|
|
592
|
-
const kubeconfig = new KubeConfig()
|
|
593
|
-
kubeconfig.loadFromString(kubeconfigPayload)
|
|
594
|
-
|
|
595
|
-
console.log('Validating game server deployment...')
|
|
596
|
-
const exitCode = await checkGameServerDeployment(envInfo.deployment.kubernetes_namespace, kubeconfig)
|
|
597
|
-
exit(exitCode)
|
|
598
887
|
} catch (error) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (error instanceof Error) {
|
|
604
|
-
console.error(`Error deploying game server into target environment: ${error.message}`)
|
|
888
|
+
if (error instanceof Error) {
|
|
889
|
+
console.error(`Error deploying game server into target environment: ${error.message}`)
|
|
890
|
+
}
|
|
891
|
+
exit(1)
|
|
605
892
|
}
|
|
606
|
-
exit(1)
|
|
607
893
|
}
|
|
608
|
-
|
|
894
|
+
)
|
|
609
895
|
|
|
610
|
-
program
|
|
611
|
-
.
|
|
896
|
+
program
|
|
897
|
+
.command('check-server-status')
|
|
898
|
+
.description(
|
|
899
|
+
'check the status of a deployed server and print out information that is helpful in debugging failed deployments'
|
|
900
|
+
)
|
|
612
901
|
.argument('[gameserver]', 'address of gameserver (e.g. metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
613
902
|
.hook('preAction', async () => {
|
|
614
903
|
await extendCurrentSession()
|
|
615
904
|
})
|
|
616
|
-
.action(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
logger.debug('Get environment info')
|
|
623
|
-
const envInfo = await stackApi.getEnvironmentDetails(gameserverId)
|
|
624
|
-
const kubernetesNamespace = envInfo.deployment.kubernetes_namespace
|
|
905
|
+
.action(
|
|
906
|
+
async (
|
|
907
|
+
gameserver: string | undefined,
|
|
908
|
+
options: { organization?: string; project?: string; environment?: string }
|
|
909
|
+
) => {
|
|
910
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
625
911
|
|
|
626
|
-
// Load kubeconfig from file and throw error if validation fails.
|
|
627
|
-
logger.debug('Get kubeconfig')
|
|
628
|
-
const kubeconfig = new KubeConfig()
|
|
629
912
|
try {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const
|
|
633
|
-
kubeconfig.loadFromString(kubeconfigPayload)
|
|
634
|
-
} catch (error) {
|
|
635
|
-
throw new Error(`Failed to load or validate kubeconfig. ${error}`)
|
|
636
|
-
}
|
|
913
|
+
logger.debug('Get environment info')
|
|
914
|
+
const envInfo = await targetEnv.getEnvironmentDetails()
|
|
915
|
+
const kubernetesNamespace = envInfo.deployment.kubernetes_namespace
|
|
637
916
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
917
|
+
// Load kubeconfig from file and throw error if validation fails.
|
|
918
|
+
logger.debug('Get kubeconfig')
|
|
919
|
+
const kubeconfig = new KubeConfig()
|
|
920
|
+
try {
|
|
921
|
+
// Initialize kubeconfig with the payload fetched from the cloud
|
|
922
|
+
// \todo allow passing a custom kubeconfig file?
|
|
923
|
+
const kubeconfigPayload = await targetEnv.getKubeConfigWithEmbeddedCredentials()
|
|
924
|
+
kubeconfig.loadFromString(kubeconfigPayload)
|
|
925
|
+
} catch (error) {
|
|
926
|
+
const errMessage = error instanceof Error ? error.message : String(error)
|
|
927
|
+
throw new Error(`Failed to load or validate kubeconfig. ${errMessage}`)
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Run the checks and exit with success/failure exitCode depending on result
|
|
931
|
+
console.log(`Validating game server deployment in namespace ${kubernetesNamespace}`)
|
|
932
|
+
// \todo Get requiredImageTag from the Helm chart
|
|
933
|
+
const exitCode = await checkGameServerDeployment(kubernetesNamespace, kubeconfig, /* requiredImageTag: */ null)
|
|
934
|
+
exit(exitCode)
|
|
935
|
+
} catch (error: any) {
|
|
936
|
+
console.error(`Failed to check deployment status: ${error.message}`)
|
|
937
|
+
exit(1)
|
|
938
|
+
}
|
|
645
939
|
}
|
|
646
|
-
|
|
940
|
+
)
|
|
647
941
|
|
|
648
|
-
program
|
|
649
|
-
.
|
|
942
|
+
program
|
|
943
|
+
.command('check-deployment')
|
|
944
|
+
.description(
|
|
945
|
+
'[deprecated] check that a game server was successfully deployed, or print out useful error messages in case of failure'
|
|
946
|
+
)
|
|
650
947
|
.argument('[namespace]', 'kubernetes namespace of the deployment')
|
|
651
948
|
.action(async (namespace: string) => {
|
|
652
|
-
console.error(
|
|
949
|
+
console.error(
|
|
950
|
+
'DEPRECATED! Use the "metaplay-auth check-server-status [gameserver]" command instead! This command will be removed soon.'
|
|
951
|
+
)
|
|
653
952
|
|
|
654
953
|
try {
|
|
655
954
|
if (!namespace) {
|
|
@@ -673,12 +972,14 @@ program.command('check-deployment')
|
|
|
673
972
|
try {
|
|
674
973
|
kubeconfig.loadFromFile(kubeconfigPath)
|
|
675
974
|
} catch (error) {
|
|
676
|
-
|
|
975
|
+
const errMessage = error instanceof Error ? error.message : String(error)
|
|
976
|
+
throw new Error(`Failed to load or validate kubeconfig: ${errMessage}`)
|
|
677
977
|
}
|
|
678
978
|
|
|
679
979
|
// Run the checks and exit with success/failure exitCode depending on result
|
|
680
980
|
console.log(`Validating game server deployment in namespace ${namespace}`)
|
|
681
|
-
|
|
981
|
+
// \todo Get requiredImageTag from the Helm chart
|
|
982
|
+
const exitCode = await checkGameServerDeployment(namespace, kubeconfig, /* requiredImageTag: */ null)
|
|
682
983
|
exit(exitCode)
|
|
683
984
|
} catch (error: any) {
|
|
684
985
|
console.error(`Failed to check deployment status: ${error.message}`)
|
|
@@ -686,4 +987,29 @@ program.command('check-deployment')
|
|
|
686
987
|
}
|
|
687
988
|
})
|
|
688
989
|
|
|
990
|
+
program
|
|
991
|
+
.command('debug-server')
|
|
992
|
+
.description('run an ephemeral debug container against a game server pod running in the cloud')
|
|
993
|
+
.argument('gameserver', 'address of gameserver (e.g., metaplay-idler-develop or idler-develop.p1.metaplay.io)')
|
|
994
|
+
.argument('[pod-name]', 'name of the pod to debug (must be specified if deployment has multiple pods)')
|
|
995
|
+
.hook('preAction', async () => {
|
|
996
|
+
await extendCurrentSession()
|
|
997
|
+
})
|
|
998
|
+
.action(
|
|
999
|
+
async (
|
|
1000
|
+
gameserver: string,
|
|
1001
|
+
podName: string | undefined,
|
|
1002
|
+
options: { organization?: string; project?: string; environment?: string }
|
|
1003
|
+
) => {
|
|
1004
|
+
// Resolve target environment
|
|
1005
|
+
const targetEnv = await resolveTargetEnvironment(gameserver, options)
|
|
1006
|
+
|
|
1007
|
+
// Exec 'kubectl debug ...'
|
|
1008
|
+
await debugGameServer(targetEnv, podName)
|
|
1009
|
+
}
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
// Register docker build command
|
|
1013
|
+
registerBuildCommand(program)
|
|
1014
|
+
|
|
689
1015
|
void program.parseAsync()
|