@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/package.json
CHANGED
|
@@ -1,45 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaplay/metaplay-auth",
|
|
3
3
|
"description": "Utility CLI for authenticating with the Metaplay Auth and making authenticated calls to infrastructure endpoints.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
7
|
"homepage": "https://metaplay.io",
|
|
8
8
|
"bin": {
|
|
9
|
-
"metaplay-auth": "dist/index.
|
|
9
|
+
"metaplay-auth": "dist/index.cjs"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "tsx index.ts",
|
|
13
|
-
"
|
|
13
|
+
"bake-version": "node -p \"'export const PACKAGE_VERSION = ' + JSON.stringify(require('./package.json').version)\" > src/version.ts"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@metaplay/eslint-config": "workspace:*",
|
|
20
|
-
"@
|
|
21
|
-
"@types/dockerode": "^3.3.28",
|
|
20
|
+
"@types/dockerode": "^3.3.29",
|
|
22
21
|
"@types/express": "^4.17.21",
|
|
23
22
|
"@types/js-yaml": "^4.0.9",
|
|
24
23
|
"@types/jsonwebtoken": "^9.0.5",
|
|
25
24
|
"@types/jwk-to-pem": "^2.0.3",
|
|
26
|
-
"@types/node": "^20.
|
|
25
|
+
"@types/node": "^20.14.8",
|
|
26
|
+
"@types/semver": "^7.5.8",
|
|
27
|
+
"esbuild": "^0.23.0",
|
|
27
28
|
"tsx": "^4.7.1",
|
|
28
|
-
"typescript": "
|
|
29
|
-
"vitest": "^1.4.0"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"@aws-sdk/client-ecr": "^3.549.0",
|
|
33
|
-
"@kubernetes/client-node": "^1.0.0-rc4",
|
|
29
|
+
"typescript": "5.4.x",
|
|
30
|
+
"vitest": "^1.4.0",
|
|
31
|
+
"@aws-sdk/client-ecr": "^3.620.0",
|
|
32
|
+
"@kubernetes/client-node": "^1.0.0-rc6",
|
|
34
33
|
"@ory/client": "^1.9.0",
|
|
35
|
-
"@types/semver": "^7.5.8",
|
|
36
34
|
"commander": "^12.0.0",
|
|
37
35
|
"dockerode": "^4.0.2",
|
|
38
36
|
"h3": "^1.11.1",
|
|
39
37
|
"js-yaml": "^4.1.0",
|
|
40
38
|
"jsonwebtoken": "^9.0.2",
|
|
41
39
|
"jwk-to-pem": "^2.0.5",
|
|
42
|
-
"open": "^
|
|
40
|
+
"open": "^8.4.2",
|
|
43
41
|
"semver": "^7.6.0",
|
|
44
42
|
"tslog": "^4.9.2"
|
|
45
43
|
}
|
package/src/auth.ts
CHANGED
|
@@ -12,6 +12,12 @@ import { setSecret, getSecret, removeSecret } from './secret_store.js'
|
|
|
12
12
|
|
|
13
13
|
import { logger } from './logging.js'
|
|
14
14
|
|
|
15
|
+
export interface TokenSet {
|
|
16
|
+
id_token?: string
|
|
17
|
+
access_token: string
|
|
18
|
+
refresh_token?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
// oauth2 client details (maybe move these to be discovered from some online location to make changes easier to manage?)
|
|
16
22
|
const clientId = 'c16ea663-ced3-46c6-8f85-38c9681fe1f0'
|
|
17
23
|
const baseURL = 'https://auth.metaplay.dev'
|
|
@@ -275,7 +281,7 @@ async function extendCurrentSessionWithRefreshToken (refreshToken: string): Prom
|
|
|
275
281
|
|
|
276
282
|
// Check if the response is OK
|
|
277
283
|
if (!response.ok) {
|
|
278
|
-
const responseJSON = await response.json()
|
|
284
|
+
const responseJSON = await response.json() as { error: string, error_description: string }
|
|
279
285
|
|
|
280
286
|
logger.error('Failed to refresh tokens.')
|
|
281
287
|
logger.error(`Error Type: ${responseJSON.error}`)
|
|
@@ -288,7 +294,7 @@ async function extendCurrentSessionWithRefreshToken (refreshToken: string): Prom
|
|
|
288
294
|
throw new Error('Failed extending current session, exiting. Please log in again.')
|
|
289
295
|
}
|
|
290
296
|
|
|
291
|
-
return await response.json()
|
|
297
|
+
return await response.json() as { id_token: string, access_token: string, refresh_token: string }
|
|
292
298
|
}
|
|
293
299
|
|
|
294
300
|
/**
|
|
@@ -310,7 +316,7 @@ async function getTokensWithAuthorizationCode (state: string, redirectUri: strin
|
|
|
310
316
|
body: `grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(redirectUri)}&client_id=${clientId}&code_verifier=${verifier}&state=${encodeURIComponent(state)}`
|
|
311
317
|
})
|
|
312
318
|
|
|
313
|
-
return await response.json()
|
|
319
|
+
return await response.json() as { id_token: string, access_token: string, refresh_token: string }
|
|
314
320
|
} catch (error) {
|
|
315
321
|
if (error instanceof Error) {
|
|
316
322
|
logger.error(`Error exchanging code for tokens: ${error.message}`)
|
|
@@ -322,9 +328,9 @@ async function getTokensWithAuthorizationCode (state: string, redirectUri: strin
|
|
|
322
328
|
/**
|
|
323
329
|
* Load tokens from the local secret store.
|
|
324
330
|
*/
|
|
325
|
-
export async function loadTokens (): Promise<
|
|
331
|
+
export async function loadTokens (): Promise<TokenSet> {
|
|
326
332
|
try {
|
|
327
|
-
const tokens = await getSecret('tokens') as
|
|
333
|
+
const tokens = await getSecret('tokens') as TokenSet
|
|
328
334
|
|
|
329
335
|
if (!tokens) {
|
|
330
336
|
throw new Error('Unable to load tokens. You need to login first.')
|
package/src/deployment.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { KubeConfig, CoreV1Api, V1Pod } from '@kubernetes/client-node'
|
|
1
|
+
import { KubeConfig, CoreV1Api, type V1Pod, type CoreV1ApiListNamespacedPodRequest, type CoreV1ApiReadNamespacedPodLogRequest } from '@kubernetes/client-node'
|
|
2
2
|
import { logger } from './logging.js'
|
|
3
|
-
import
|
|
3
|
+
import { TargetEnvironment } from 'targetenvironment.js'
|
|
4
|
+
import { exit } from 'process'
|
|
5
|
+
import { unlink, writeFile } from 'fs/promises'
|
|
6
|
+
import { executeCommand, type ExecuteCommandResult } from './utils.js'
|
|
7
|
+
import os from 'os'
|
|
8
|
+
import path from 'path'
|
|
4
9
|
|
|
5
10
|
enum GameServerPodPhase {
|
|
6
11
|
Ready = 'Ready', // Successfully started and ready to accept traffic
|
|
@@ -16,7 +21,7 @@ interface GameServerPodStatus {
|
|
|
16
21
|
details?: any
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string) {
|
|
24
|
+
async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string): Promise<V1Pod[]> {
|
|
20
25
|
// Define label selector for gameserver
|
|
21
26
|
logger.debug(`Fetching game server pods from Kubernetes: namespace=${namespace}`)
|
|
22
27
|
const param: CoreV1ApiListNamespacedPodRequest = {
|
|
@@ -32,7 +37,7 @@ async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string) {
|
|
|
32
37
|
return response.items
|
|
33
38
|
} catch (error) {
|
|
34
39
|
// \todo Better error handling ..
|
|
35
|
-
console.
|
|
40
|
+
console.error('Failed to fetch pods from Kubernetes:', error)
|
|
36
41
|
throw new Error('Failed to fetch pods from Kubernetes')
|
|
37
42
|
}
|
|
38
43
|
}
|
|
@@ -77,9 +82,9 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
|
|
|
77
82
|
// Try to detecth why the previous launch failed
|
|
78
83
|
if (containerState.waiting) {
|
|
79
84
|
const reason = containerState.waiting.reason
|
|
80
|
-
if (knownContainerFailureReasons.includes(reason
|
|
85
|
+
if (reason && knownContainerFailureReasons.includes(reason)) {
|
|
81
86
|
return { phase: GameServerPodPhase.Failed, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
|
|
82
|
-
} else if (knownContainerPendingReasons.includes(reason
|
|
87
|
+
} else if (reason && knownContainerPendingReasons.includes(reason)) {
|
|
83
88
|
return { phase: GameServerPodPhase.Pending, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
|
|
84
89
|
} else {
|
|
85
90
|
return { phase: GameServerPodPhase.Unknown, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
|
|
@@ -141,7 +146,20 @@ function resolvePodStatusConditions (pod: V1Pod): GameServerPodStatus {
|
|
|
141
146
|
return { phase: GameServerPodPhase.Ready, message: 'Pod is ready to serve traffic' }
|
|
142
147
|
}
|
|
143
148
|
|
|
144
|
-
function
|
|
149
|
+
function resolvePodGameServerImageTag (pod: V1Pod): string | null {
|
|
150
|
+
// Find the shard-server spec from the pod and extract image tag from spec
|
|
151
|
+
const containerSpec = pod.spec?.containers?.find(containerSpec => containerSpec.name === 'shard-server')
|
|
152
|
+
if (!containerSpec) {
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
if (!containerSpec.image) {
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
const [_, imageTag] = containerSpec.image.split(':')
|
|
159
|
+
return imageTag
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolvePodStatus (pod: V1Pod, requiredImageTag: string | null): GameServerPodStatus {
|
|
145
163
|
// logger.debug('resolvePodStatus(): pod =', JSON.stringify(pod, undefined, 2))
|
|
146
164
|
|
|
147
165
|
if (!pod) {
|
|
@@ -152,6 +170,14 @@ function resolvePodStatus (pod: V1Pod): GameServerPodStatus {
|
|
|
152
170
|
return { phase: GameServerPodPhase.Unknown, message: 'Unable to access pod.status from Kubernetes' }
|
|
153
171
|
}
|
|
154
172
|
|
|
173
|
+
// Check the pod has the expected image.
|
|
174
|
+
if (requiredImageTag !== null) {
|
|
175
|
+
const podImageTag = resolvePodGameServerImageTag(pod)
|
|
176
|
+
if (podImageTag !== requiredImageTag) {
|
|
177
|
+
return { phase: GameServerPodPhase.Unknown, message: `Image tag is not (yet?) updated. Pod image is ${podImageTag ?? 'unknown'}, expecting ${requiredImageTag}.` }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
155
181
|
// Handle status.phase
|
|
156
182
|
const podPhase = pod.status?.phase
|
|
157
183
|
switch (podPhase) {
|
|
@@ -163,8 +189,14 @@ function resolvePodStatus (pod: V1Pod): GameServerPodStatus {
|
|
|
163
189
|
// Pod has been scheduled and start -- note that the containers may have failed!
|
|
164
190
|
return resolvePodStatusConditions(pod)
|
|
165
191
|
|
|
166
|
-
case 'Succeeded':
|
|
167
|
-
|
|
192
|
+
case 'Succeeded':
|
|
193
|
+
// Should not happen, the game server pods should never stop
|
|
194
|
+
return { phase: GameServerPodPhase.Unknown, message: 'Pod has unexpectedly terminated (with a clean exit status)' }
|
|
195
|
+
|
|
196
|
+
case 'Failed':
|
|
197
|
+
// Should not happen, the game server pods should never stop
|
|
198
|
+
return { phase: GameServerPodPhase.Unknown, message: 'Pod has unexpectedly terminated (with a failure exit status)' }
|
|
199
|
+
|
|
168
200
|
case 'Unknown':
|
|
169
201
|
default:
|
|
170
202
|
return { phase: GameServerPodPhase.Unknown, message: `Invalid pod.status.phase: ${podPhase}` }
|
|
@@ -200,11 +232,11 @@ async function fetchPodLogs (k8sApi: CoreV1Api, pod: V1Pod) {
|
|
|
200
232
|
}
|
|
201
233
|
}
|
|
202
234
|
|
|
203
|
-
async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod) {
|
|
235
|
+
async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod, requiredImageTag: string | null) {
|
|
204
236
|
// console.log('Pod:', JSON.stringify(pod, undefined, 2))
|
|
205
237
|
|
|
206
238
|
// Classify game server status
|
|
207
|
-
const podStatus = resolvePodStatus(pod)
|
|
239
|
+
const podStatus = resolvePodStatus(pod, requiredImageTag)
|
|
208
240
|
const podName = pod.metadata?.name ?? '<unable to resolve name>'
|
|
209
241
|
|
|
210
242
|
// If game server launch failed, get the error logs
|
|
@@ -218,7 +250,7 @@ async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod) {
|
|
|
218
250
|
}
|
|
219
251
|
|
|
220
252
|
async function delay (ms: number): Promise<void> {
|
|
221
|
-
|
|
253
|
+
await new Promise<void>(resolve => setTimeout(resolve, ms))
|
|
222
254
|
}
|
|
223
255
|
|
|
224
256
|
function anyPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
|
|
@@ -229,7 +261,7 @@ function allPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPo
|
|
|
229
261
|
return podStatuses.every(status => status.phase === phase)
|
|
230
262
|
}
|
|
231
263
|
|
|
232
|
-
export async function checkGameServerDeployment (namespace: string, kubeconfig: KubeConfig): Promise<number> {
|
|
264
|
+
export async function checkGameServerDeployment (namespace: string, kubeconfig: KubeConfig, requiredImageTag: string | null): Promise<number> {
|
|
233
265
|
const k8sApi = kubeconfig.makeApiClient(CoreV1Api)
|
|
234
266
|
|
|
235
267
|
// Figure out when to stop
|
|
@@ -242,7 +274,7 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
|
|
|
242
274
|
logger.debug(`Found ${pods?.length} pod(s) deployed in Kubernetes`)
|
|
243
275
|
if (pods.length > 0) {
|
|
244
276
|
// Resolve status for all pods
|
|
245
|
-
const podStatuses = await Promise.all(pods.map(async pod => await checkGameServerPod(k8sApi, pod)))
|
|
277
|
+
const podStatuses = await Promise.all(pods.map(async pod => await checkGameServerPod(k8sApi, pod, requiredImageTag)))
|
|
246
278
|
logger.debug(`Pod phases: ${podStatuses.map(status => status.phase)}`)
|
|
247
279
|
|
|
248
280
|
// Handle state of the deployment
|
|
@@ -285,3 +317,81 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
|
|
|
285
317
|
await delay(2000)
|
|
286
318
|
}
|
|
287
319
|
}
|
|
320
|
+
|
|
321
|
+
export async function debugGameServer (targetEnv: TargetEnvironment, targetPodName?: string) {
|
|
322
|
+
// Initialize kubeconfig for target environment
|
|
323
|
+
logger.debug('Get kubeconfig')
|
|
324
|
+
let kubeconfigPayload
|
|
325
|
+
const kubeconfig = new KubeConfig()
|
|
326
|
+
try {
|
|
327
|
+
kubeconfigPayload = await targetEnv.getKubeConfig()
|
|
328
|
+
kubeconfig.loadFromString(kubeconfigPayload)
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error(`Failed to fetch kubeconfig for environment: ${error}`)
|
|
331
|
+
exit(1)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Fetch environment details
|
|
335
|
+
logger.debug('Get environment details')
|
|
336
|
+
const envInfo = await targetEnv.getEnvironmentDetails()
|
|
337
|
+
const kubernetesNamespace = envInfo.deployment.kubernetes_namespace
|
|
338
|
+
|
|
339
|
+
// Resolve which pods are running in the target environment
|
|
340
|
+
logger.debug('Get running game server pods')
|
|
341
|
+
const k8sApi = kubeconfig.makeApiClient(CoreV1Api)
|
|
342
|
+
const gameServerPods = await fetchGameServerPods(k8sApi, kubernetesNamespace)
|
|
343
|
+
|
|
344
|
+
// If no pod name is specified, automaticalyl target a singleton pod or fail if there are multiple
|
|
345
|
+
if (!targetPodName) {
|
|
346
|
+
if (gameServerPods.length === 1) {
|
|
347
|
+
const metadata = gameServerPods[0].metadata
|
|
348
|
+
if (!metadata?.name) {
|
|
349
|
+
throw new Error('Unable to resolve name for the Kubernetes pod!')
|
|
350
|
+
}
|
|
351
|
+
targetPodName = metadata.name
|
|
352
|
+
} else {
|
|
353
|
+
const podNames = gameServerPods.map(pod => pod.metadata?.name).join(', ')
|
|
354
|
+
console.error(`Multiple game server pods running: ${podNames}\nPlease specify which you want to debug.`)
|
|
355
|
+
exit(1)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Execute 'kubectl debug ..'
|
|
360
|
+
const tmpKubeconfigPath = path.join(os.tmpdir(), `temp-kubeconfig-${+Date.now()}`)
|
|
361
|
+
try {
|
|
362
|
+
// Write kubeconfig to a file
|
|
363
|
+
logger.debug(`Write temporary kubeconfig to ${tmpKubeconfigPath}`)
|
|
364
|
+
await writeFile(tmpKubeconfigPath, kubeconfigPayload)
|
|
365
|
+
|
|
366
|
+
// Run kubctl
|
|
367
|
+
// const args = [`--kubeconfig=${tmpKubeconfigPath}`, `--namespace=${kubernetesNamespace}`, 'get', 'pods']
|
|
368
|
+
const args = [
|
|
369
|
+
`--kubeconfig=${tmpKubeconfigPath}`,
|
|
370
|
+
`--namespace=${kubernetesNamespace}`,
|
|
371
|
+
'debug',
|
|
372
|
+
targetPodName,
|
|
373
|
+
'-it',
|
|
374
|
+
'--profile=general',
|
|
375
|
+
'--image=metaplay/diagnostics:latest',
|
|
376
|
+
'--target=shard-server'
|
|
377
|
+
]
|
|
378
|
+
console.log(`Execute: kubectl ${args.join(' ')}`)
|
|
379
|
+
const execResult = await executeCommand('kubectl', args, true)
|
|
380
|
+
|
|
381
|
+
// Remove temporary kubeconfig
|
|
382
|
+
logger.debug('Delete temporary kubeconfig')
|
|
383
|
+
await unlink(tmpKubeconfigPath)
|
|
384
|
+
|
|
385
|
+
// Forward exit code
|
|
386
|
+
if (execResult?.exitCode !== 0) {
|
|
387
|
+
console.error(`kubectl failed failed with exit code ${execResult.exitCode}`)
|
|
388
|
+
exit(execResult.exitCode)
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error(`Failed to execute 'kubectl': ${err}. You need to have kubectl installed to debug a game server with metaplay-auth.`)
|
|
392
|
+
|
|
393
|
+
// Remove temporary kubeconfig
|
|
394
|
+
logger.debug('Delete temporary kubeconfig')
|
|
395
|
+
await unlink(tmpKubeconfigPath)
|
|
396
|
+
}
|
|
397
|
+
}
|