@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/src/deployment.ts CHANGED
@@ -1,6 +1,17 @@
1
- import { KubeConfig, CoreV1Api, V1Pod } from '@kubernetes/client-node'
1
+ import {
2
+ KubeConfig,
3
+ CoreV1Api,
4
+ type V1Pod,
5
+ type CoreV1ApiListNamespacedPodRequest,
6
+ type CoreV1ApiReadNamespacedPodLogRequest,
7
+ } from '@kubernetes/client-node'
2
8
  import { logger } from './logging.js'
3
- import type { CoreV1ApiListNamespacedPodRequest, CoreV1ApiReadNamespacedPodLogRequest } from '@kubernetes/client-node'
9
+ import { TargetEnvironment } from 'targetenvironment.js'
10
+ import { exit } from 'process'
11
+ import { unlink, writeFile } from 'fs/promises'
12
+ import { executeCommand } from './utils.js'
13
+ import os from 'os'
14
+ import path from 'path'
4
15
 
5
16
  enum GameServerPodPhase {
6
17
  Ready = 'Ready', // Successfully started and ready to accept traffic
@@ -16,12 +27,12 @@ interface GameServerPodStatus {
16
27
  details?: any
17
28
  }
18
29
 
19
- async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string) {
30
+ async function fetchGameServerPods(k8sApi: CoreV1Api, namespace: string): Promise<V1Pod[]> {
20
31
  // Define label selector for gameserver
21
32
  logger.debug(`Fetching game server pods from Kubernetes: namespace=${namespace}`)
22
33
  const param: CoreV1ApiListNamespacedPodRequest = {
23
34
  namespace,
24
- labelSelector: 'app=metaplay-server'
35
+ labelSelector: 'app=metaplay-server',
25
36
  }
26
37
 
27
38
  try {
@@ -32,25 +43,30 @@ async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string) {
32
43
  return response.items
33
44
  } catch (error) {
34
45
  // \todo Better error handling ..
35
- console.log('Failed to fetch pods from Kubernetes:', error)
46
+ console.error('Failed to fetch pods from Kubernetes:', error)
36
47
  throw new Error('Failed to fetch pods from Kubernetes')
37
48
  }
38
49
  }
39
50
 
40
- function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
51
+ function resolvePodContainersConditions(pod: V1Pod): GameServerPodStatus {
41
52
  const containerStatuses = pod.status?.containerStatuses
42
53
  if (!containerStatuses || containerStatuses.length === 0) {
43
- return { phase: GameServerPodPhase.Unknown, message: 'Unable to determine pod container statuses: pod.status.containerStatuses is empty' }
54
+ return {
55
+ phase: GameServerPodPhase.Unknown,
56
+ message: 'Unable to determine pod container statuses: pod.status.containerStatuses is empty',
57
+ }
44
58
  }
45
59
 
46
60
  // Find the shard-server container from the pod (ignore others)
47
- const containerStatus = containerStatuses.find(status => status.name === 'shard-server')
61
+ const containerStatus = containerStatuses.find((status) => status.name === 'shard-server')
48
62
  if (!containerStatus) {
49
63
  return { phase: GameServerPodPhase.Unknown, message: 'Unable to find container shard-server from the pod' }
50
64
  }
51
65
 
52
66
  // Handle missing container state
53
- logger.debug(`Container status for pod ${pod.metadata?.name ?? '<unnamed>'}: ${JSON.stringify(containerStatus, undefined, 2)}`)
67
+ logger.debug(
68
+ `Container status for pod ${pod.metadata?.name ?? '<unnamed>'}: ${JSON.stringify(containerStatus, undefined, 2)}`
69
+ )
54
70
  const containerState = containerStatus.state
55
71
  if (!containerState) {
56
72
  return { phase: GameServerPodPhase.Unknown, message: 'Unable to get container state' }
@@ -60,14 +76,31 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
60
76
  const containerName = containerStatus.name
61
77
  if (containerState.running) {
62
78
  if (containerStatus.ready) {
63
- return { phase: GameServerPodPhase.Ready, message: `Container ${containerName} is in ready phase`, details: containerState.running }
79
+ return {
80
+ phase: GameServerPodPhase.Ready,
81
+ message: `Container ${containerName} is in ready phase`,
82
+ details: containerState.running,
83
+ }
64
84
  } else {
65
- return { phase: GameServerPodPhase.Running, message: `Container ${containerName} is in running phase`, details: containerState.running }
85
+ return {
86
+ phase: GameServerPodPhase.Running,
87
+ message: `Container ${containerName} is in running phase`,
88
+ details: containerState.running,
89
+ }
66
90
  }
67
91
  }
68
92
 
69
93
  // \note these may not be complete (or completely accurate)
70
- const knownContainerFailureReasons = ['CrashLoopBackOff', 'Error', 'ImagePullBackOff', 'CreateContainerConfigError', 'OOMKilled', 'ContainerCannotRun', 'BackOff', 'InvalidImageName']
94
+ const knownContainerFailureReasons = [
95
+ 'CrashLoopBackOff',
96
+ 'Error',
97
+ 'ImagePullBackOff',
98
+ 'CreateContainerConfigError',
99
+ 'OOMKilled',
100
+ 'ContainerCannotRun',
101
+ 'BackOff',
102
+ 'InvalidImageName',
103
+ ]
71
104
  const knownContainerPendingReasons = ['Init', 'Pending', 'PodInitializing']
72
105
 
73
106
  // Check if there's a previous terminated state (usually indicates a crash during server initialization)
@@ -77,22 +110,46 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
77
110
  // Try to detecth why the previous launch failed
78
111
  if (containerState.waiting) {
79
112
  const reason = containerState.waiting.reason
80
- if (knownContainerFailureReasons.includes(reason as string)) {
81
- return { phase: GameServerPodPhase.Failed, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
82
- } else if (knownContainerPendingReasons.includes(reason as string)) {
83
- return { phase: GameServerPodPhase.Pending, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
113
+ if (reason && knownContainerFailureReasons.includes(reason)) {
114
+ return {
115
+ phase: GameServerPodPhase.Failed,
116
+ message: `Container ${containerName} is in waiting state, reason=${reason}`,
117
+ details: containerState.waiting,
118
+ }
119
+ } else if (reason && knownContainerPendingReasons.includes(reason)) {
120
+ return {
121
+ phase: GameServerPodPhase.Pending,
122
+ message: `Container ${containerName} is in waiting state, reason=${reason}`,
123
+ details: containerState.waiting,
124
+ }
84
125
  } else {
85
- return { phase: GameServerPodPhase.Unknown, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
126
+ return {
127
+ phase: GameServerPodPhase.Unknown,
128
+ message: `Container ${containerName} is in waiting state, reason=${reason}`,
129
+ details: containerState.waiting,
130
+ }
86
131
  }
87
132
  } else if (containerState.running) {
88
133
  // This happens when the container is still initializing
89
- return { phase: GameServerPodPhase.Pending, message: `Container ${containerName} is in running state`, details: containerState.running }
134
+ return {
135
+ phase: GameServerPodPhase.Pending,
136
+ message: `Container ${containerName} is in running state`,
137
+ details: containerState.running,
138
+ }
90
139
  } else if (containerState.terminated) {
91
- return { phase: GameServerPodPhase.Failed, message: `Container ${containerName} is in terminated state`, details: containerState.terminated }
140
+ return {
141
+ phase: GameServerPodPhase.Failed,
142
+ message: `Container ${containerName} is in terminated state`,
143
+ details: containerState.terminated,
144
+ }
92
145
  }
93
146
 
94
147
  // Unable to determine launch failure reason, just return previous launch
95
- return { phase: GameServerPodPhase.Failed, message: `Container ${containerName} previous launch failed with exitCode=${lastState.terminated.exitCode} and reason=${lastState.terminated.reason}`, details: lastState.terminated }
148
+ return {
149
+ phase: GameServerPodPhase.Failed,
150
+ message: `Container ${containerName} previous launch failed with exitCode=${lastState.terminated.exitCode} and reason=${lastState.terminated.reason}`,
151
+ details: lastState.terminated,
152
+ }
96
153
  }
97
154
 
98
155
  // \todo handle containerState.running states (including various initialization states)
@@ -103,45 +160,102 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
103
160
  return { phase: GameServerPodPhase.Unknown, message: 'Container in unknown state', details: containerState }
104
161
  }
105
162
 
106
- function resolvePodStatusConditions (pod: V1Pod): GameServerPodStatus {
163
+ function resolveRunningPodStatusConditions(pod: V1Pod): GameServerPodStatus {
107
164
  const conditions = pod.status?.conditions
108
165
  if (!conditions || conditions.length === 0) {
109
- return { phase: GameServerPodPhase.Unknown, message: 'Unable to determine pod status: pod.status.conditions is empty', details: pod.status }
166
+ return {
167
+ phase: GameServerPodPhase.Unknown,
168
+ message: 'Unable to determine pod status: pod.status.conditions is empty',
169
+ details: pod.status,
170
+ }
110
171
  }
111
172
 
112
173
  // Bail if 'PodScheduled' is not yet true
113
- const condPodScheduled = conditions.find(cond => cond.type === 'PodScheduled')
174
+ const condPodScheduled = conditions.find((cond) => cond.type === 'PodScheduled')
114
175
  if (condPodScheduled?.status !== 'True') {
115
- return { phase: GameServerPodPhase.Pending, message: `Pod has not yet been scheduled on a node: ${condPodScheduled?.message}`, details: condPodScheduled }
176
+ return {
177
+ phase: GameServerPodPhase.Pending,
178
+ message: `Pod has not yet been scheduled on a node: ${condPodScheduled?.message}`,
179
+ details: condPodScheduled,
180
+ }
116
181
  }
117
182
 
118
183
  // Bail if 'Initialized' not is yet true
119
- const condInitialized = conditions.find(cond => cond.type === 'Initialized')
184
+ const condInitialized = conditions.find((cond) => cond.type === 'Initialized')
120
185
  if (condInitialized?.status !== 'True') {
121
- return { phase: GameServerPodPhase.Pending, message: `Pod has not yet been initialized: ${condInitialized?.message}`, details: condInitialized }
186
+ return {
187
+ phase: GameServerPodPhase.Pending,
188
+ message: `Pod has not yet been initialized: ${condInitialized?.message}`,
189
+ details: condInitialized,
190
+ }
122
191
  }
123
192
 
124
193
  // Bail if 'ContainersReady' is not yet true
125
- const condContainersReady = conditions.find(cond => cond.type === 'ContainersReady')
194
+ const condContainersReady = conditions.find((cond) => cond.type === 'ContainersReady')
126
195
  if (condContainersReady?.status !== 'True') {
127
196
  if (condContainersReady?.reason === 'ContainersNotReady') {
128
197
  return resolvePodContainersConditions(pod)
129
198
  }
130
199
 
131
- return { phase: GameServerPodPhase.Pending, message: `Pod containers are not yet ready: ${condContainersReady?.message}`, details: condContainersReady }
200
+ return {
201
+ phase: GameServerPodPhase.Pending,
202
+ message: `Pod containers are not yet ready: ${condContainersReady?.message}`,
203
+ details: condContainersReady,
204
+ }
132
205
  }
133
206
 
134
207
  // Bail if 'Ready' is not yet true
135
- const condReady = conditions.find(cond => cond.type === 'Ready')
208
+ const condReady = conditions.find((cond) => cond.type === 'Ready')
136
209
  if (condReady?.status !== 'True') {
137
- return { phase: GameServerPodPhase.Pending, message: `Pod is not yet ready: ${condReady?.message}`, details: condReady }
210
+ return {
211
+ phase: GameServerPodPhase.Pending,
212
+ message: `Pod is not yet ready: ${condReady?.message}`,
213
+ details: condReady,
214
+ }
138
215
  }
139
216
 
140
217
  // resolvePodContainersConditions(pod) // DEBUG DEBUG enable to print container state for running pods
141
218
  return { phase: GameServerPodPhase.Ready, message: 'Pod is ready to serve traffic' }
142
219
  }
143
220
 
144
- function resolvePodStatus (pod: V1Pod): GameServerPodStatus {
221
+ /**
222
+ * Returns the reason why the pod's PodScheduled condition is false. If that is not the case
223
+ * or reason cannot be determined, returns null.
224
+ */
225
+ function resolvePodScheduledConditionFailureReason(pod: V1Pod): string | null {
226
+ // We are only interested in status when PodScheduled hasn't completed yet.
227
+ const condPodScheduled = pod.status?.conditions?.find((cond) => cond.type === 'PodScheduled')
228
+ if (condPodScheduled?.status !== 'False') {
229
+ return null
230
+ }
231
+
232
+ return condPodScheduled?.reason ?? null
233
+ }
234
+
235
+ function resolvePendingPodStatusConditions(pod: V1Pod): GameServerPodStatus {
236
+ const pendingReason = resolvePodScheduledConditionFailureReason(pod)
237
+ if (pendingReason) {
238
+ return { phase: GameServerPodPhase.Pending, message: `Pod is still in Pending phase. Reason: ${pendingReason}` }
239
+ } else {
240
+ return { phase: GameServerPodPhase.Pending, message: 'Pod is still in Pending phase' }
241
+ }
242
+ }
243
+
244
+ function resolvePodGameServerImageTag(pod: V1Pod): string | null {
245
+ // Find the shard-server spec from the pod and extract image tag from spec
246
+ const containerSpec = pod.spec?.containers?.find((containerSpec) => containerSpec.name === 'shard-server')
247
+ if (!containerSpec) {
248
+ return null
249
+ }
250
+ if (!containerSpec.image) {
251
+ return null
252
+ }
253
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
254
+ const [_, imageTag] = containerSpec.image.split(':')
255
+ return imageTag
256
+ }
257
+
258
+ function resolvePodStatus(pod: V1Pod, requiredImageTag: string | null): GameServerPodStatus {
145
259
  // logger.debug('resolvePodStatus(): pod =', JSON.stringify(pod, undefined, 2))
146
260
 
147
261
  if (!pod) {
@@ -152,26 +266,49 @@ function resolvePodStatus (pod: V1Pod): GameServerPodStatus {
152
266
  return { phase: GameServerPodPhase.Unknown, message: 'Unable to access pod.status from Kubernetes' }
153
267
  }
154
268
 
269
+ // Check the pod has the expected image.
270
+ if (requiredImageTag !== null) {
271
+ const podImageTag = resolvePodGameServerImageTag(pod)
272
+ if (podImageTag !== requiredImageTag) {
273
+ return {
274
+ phase: GameServerPodPhase.Unknown,
275
+ message: `Image tag is not (yet?) updated. Pod image is ${podImageTag ?? 'unknown'}, expecting ${requiredImageTag}.`,
276
+ }
277
+ }
278
+ }
279
+
155
280
  // Handle status.phase
156
281
  const podPhase = pod.status?.phase
157
282
  switch (podPhase) {
158
283
  case 'Pending':
159
284
  // Pod not yet scheduled
160
- return { phase: GameServerPodPhase.Pending, message: 'Pod is still in Pending phase' }
285
+ return resolvePendingPodStatusConditions(pod)
161
286
 
162
287
  case 'Running':
163
288
  // Pod has been scheduled and start -- note that the containers may have failed!
164
- return resolvePodStatusConditions(pod)
289
+ return resolveRunningPodStatusConditions(pod)
290
+
291
+ case 'Succeeded':
292
+ // Should not happen, the game server pods should never stop
293
+ return {
294
+ phase: GameServerPodPhase.Unknown,
295
+ message: 'Pod has unexpectedly terminated (with a clean exit status)',
296
+ }
297
+
298
+ case 'Failed':
299
+ // Should not happen, the game server pods should never stop
300
+ return {
301
+ phase: GameServerPodPhase.Unknown,
302
+ message: 'Pod has unexpectedly terminated (with a failure exit status)',
303
+ }
165
304
 
166
- case 'Succeeded': // Should not happen, the game server pods should never stop
167
- case 'Failed': // Should not happen, the game server pods should never stop
168
305
  case 'Unknown':
169
306
  default:
170
307
  return { phase: GameServerPodPhase.Unknown, message: `Invalid pod.status.phase: ${podPhase}` }
171
308
  }
172
309
  }
173
310
 
174
- async function fetchPodLogs (k8sApi: CoreV1Api, pod: V1Pod) {
311
+ async function fetchPodLogs(k8sApi: CoreV1Api, pod: V1Pod): Promise<string> {
175
312
  const podName = pod.metadata?.name
176
313
  logger.debug(`Fetching logs for pod '${podName}'..`)
177
314
  const namespace = pod.metadata?.namespace
@@ -187,7 +324,7 @@ async function fetchPodLogs (k8sApi: CoreV1Api, pod: V1Pod) {
187
324
  pretty: 'true',
188
325
  previous: false,
189
326
  tailLines: 100,
190
- timestamps: true
327
+ timestamps: true,
191
328
  }
192
329
 
193
330
  try {
@@ -200,11 +337,15 @@ async function fetchPodLogs (k8sApi: CoreV1Api, pod: V1Pod) {
200
337
  }
201
338
  }
202
339
 
203
- async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod) {
340
+ async function checkGameServerPod(
341
+ k8sApi: CoreV1Api,
342
+ pod: V1Pod,
343
+ requiredImageTag: string | null
344
+ ): Promise<GameServerPodStatus> {
204
345
  // console.log('Pod:', JSON.stringify(pod, undefined, 2))
205
346
 
206
347
  // Classify game server status
207
- const podStatus = resolvePodStatus(pod)
348
+ const podStatus = resolvePodStatus(pod, requiredImageTag)
208
349
  const podName = pod.metadata?.name ?? '<unable to resolve name>'
209
350
 
210
351
  // If game server launch failed, get the error logs
@@ -217,19 +358,23 @@ async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod) {
217
358
  return podStatus
218
359
  }
219
360
 
220
- async function delay (ms: number): Promise<void> {
221
- return await new Promise<void>(resolve => setTimeout(resolve, ms))
361
+ async function delay(ms: number): Promise<void> {
362
+ await new Promise<void>((resolve) => setTimeout(resolve, ms))
222
363
  }
223
364
 
224
- function anyPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
225
- return podStatuses.some(status => status.phase === phase)
365
+ function anyPodsInPhase(podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
366
+ return podStatuses.some((status) => status.phase === phase)
226
367
  }
227
368
 
228
- function allPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
229
- return podStatuses.every(status => status.phase === phase)
369
+ function allPodsInPhase(podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
370
+ return podStatuses.every((status) => status.phase === phase)
230
371
  }
231
372
 
232
- export async function checkGameServerDeployment (namespace: string, kubeconfig: KubeConfig): Promise<number> {
373
+ export async function checkGameServerDeployment(
374
+ namespace: string,
375
+ kubeconfig: KubeConfig,
376
+ requiredImageTag: string | null
377
+ ): Promise<number> {
233
378
  const k8sApi = kubeconfig.makeApiClient(CoreV1Api)
234
379
 
235
380
  // Figure out when to stop
@@ -242,8 +387,10 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
242
387
  logger.debug(`Found ${pods?.length} pod(s) deployed in Kubernetes`)
243
388
  if (pods.length > 0) {
244
389
  // Resolve status for all pods
245
- const podStatuses = await Promise.all(pods.map(async pod => await checkGameServerPod(k8sApi, pod)))
246
- logger.debug(`Pod phases: ${podStatuses.map(status => status.phase)}`)
390
+ const podStatuses = await Promise.all(
391
+ pods.map(async (pod) => await checkGameServerPod(k8sApi, pod, requiredImageTag))
392
+ )
393
+ logger.debug(`Pod phases: ${podStatuses.map((status) => status.phase).toString()}`)
247
394
 
248
395
  // Handle state of the deployment
249
396
  if (anyPodsInPhase(podStatuses, GameServerPodPhase.Failed)) {
@@ -251,15 +398,19 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
251
398
  console.log('Gameserver failed to start due to the pods not starting properly! See above for details.')
252
399
  for (let ndx = 0; ndx < pods.length; ndx += 1) {
253
400
  const status = podStatuses[ndx]
254
- const suffix = (status.phase !== GameServerPodPhase.Ready) ? ` -- ${status.message}` : ''
401
+ const suffix = status.phase !== GameServerPodPhase.Ready ? ` -- ${status.message}` : ''
255
402
  console.log(` ${pods[ndx].metadata?.name}: ${status.phase}${suffix}`)
256
403
  }
257
404
  return 1
258
- } else if (anyPodsInPhase(podStatuses, GameServerPodPhase.Unknown) || anyPodsInPhase(podStatuses, GameServerPodPhase.Pending) || anyPodsInPhase(podStatuses, GameServerPodPhase.Running)) {
405
+ } else if (
406
+ anyPodsInPhase(podStatuses, GameServerPodPhase.Unknown) ||
407
+ anyPodsInPhase(podStatuses, GameServerPodPhase.Pending) ||
408
+ anyPodsInPhase(podStatuses, GameServerPodPhase.Running)
409
+ ) {
259
410
  console.log('Waiting for gameserver(s) to be ready...')
260
411
  for (let ndx = 0; ndx < pods.length; ndx += 1) {
261
412
  const status = podStatuses[ndx]
262
- const suffix = (status.phase !== GameServerPodPhase.Ready) ? ` -- ${status.message}` : ''
413
+ const suffix = status.phase !== GameServerPodPhase.Ready ? ` -- ${status.message}` : ''
263
414
  console.log(` ${pods[ndx].metadata?.name}: ${status.phase}${suffix}`)
264
415
  }
265
416
  } else if (allPodsInPhase(podStatuses, GameServerPodPhase.Ready)) {
@@ -270,7 +421,7 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
270
421
  console.log('Deployment in inconsistent state, waiting...')
271
422
  for (let ndx = 0; ndx < pods.length; ndx += 1) {
272
423
  const status = podStatuses[ndx]
273
- const suffix = (status.phase !== GameServerPodPhase.Ready) ? ` -- ${status.message}` : ''
424
+ const suffix = status.phase !== GameServerPodPhase.Ready ? ` -- ${status.message}` : ''
274
425
  console.log(` ${pods[ndx].metadata?.name}: ${status.phase}${suffix}`)
275
426
  }
276
427
  }
@@ -285,3 +436,85 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
285
436
  await delay(2000)
286
437
  }
287
438
  }
439
+
440
+ export async function debugGameServer(targetEnv: TargetEnvironment, targetPodName?: string): Promise<void> {
441
+ // Initialize kubeconfig for target environment
442
+ logger.debug('Get kubeconfig')
443
+ let kubeconfigPayload: string
444
+ const kubeconfig = new KubeConfig()
445
+ try {
446
+ kubeconfigPayload = await targetEnv.getKubeConfigWithEmbeddedCredentials()
447
+ kubeconfig.loadFromString(kubeconfigPayload)
448
+ } catch (error) {
449
+ const errMessage = error instanceof Error ? error.message : String(error)
450
+ console.error(`Failed to fetch kubeconfig for environment: ${errMessage}`)
451
+ exit(1)
452
+ }
453
+
454
+ // Fetch environment details
455
+ logger.debug('Get environment details')
456
+ const envInfo = await targetEnv.getEnvironmentDetails()
457
+ const kubernetesNamespace = envInfo.deployment.kubernetes_namespace
458
+
459
+ // Resolve which pods are running in the target environment
460
+ logger.debug('Get running game server pods')
461
+ const k8sApi = kubeconfig.makeApiClient(CoreV1Api)
462
+ const gameServerPods = await fetchGameServerPods(k8sApi, kubernetesNamespace)
463
+
464
+ // If no pod name is specified, automaticalyl target a singleton pod or fail if there are multiple
465
+ if (!targetPodName) {
466
+ if (gameServerPods.length === 1) {
467
+ const metadata = gameServerPods[0].metadata
468
+ if (!metadata?.name) {
469
+ throw new Error('Unable to resolve name for the Kubernetes pod!')
470
+ }
471
+ targetPodName = metadata.name
472
+ } else {
473
+ const podNames = gameServerPods.map((pod) => pod.metadata?.name).join(', ')
474
+ console.error(`Multiple game server pods running: ${podNames}\nPlease specify which you want to debug.`)
475
+ exit(1)
476
+ }
477
+ }
478
+
479
+ // Execute 'kubectl debug ..'
480
+ const tmpKubeconfigPath = path.join(os.tmpdir(), `temp-kubeconfig-${+Date.now()}`)
481
+ try {
482
+ // Write kubeconfig to a file
483
+ logger.debug(`Write temporary kubeconfig to ${tmpKubeconfigPath}`)
484
+ await writeFile(tmpKubeconfigPath, kubeconfigPayload)
485
+
486
+ // Run kubctl
487
+ // const args = [`--kubeconfig=${tmpKubeconfigPath}`, `--namespace=${kubernetesNamespace}`, 'get', 'pods']
488
+ const args = [
489
+ `--kubeconfig=${tmpKubeconfigPath}`,
490
+ `--namespace=${kubernetesNamespace}`,
491
+ 'debug',
492
+ targetPodName,
493
+ '-it',
494
+ '--profile=general',
495
+ '--image=metaplay/diagnostics:latest',
496
+ '--target=shard-server',
497
+ ]
498
+ console.log(`Execute: kubectl ${args.join(' ')}`)
499
+ const execResult = await executeCommand('kubectl', args, { inheritStdio: true })
500
+
501
+ // Remove temporary kubeconfig
502
+ logger.debug('Delete temporary kubeconfig')
503
+ await unlink(tmpKubeconfigPath)
504
+
505
+ // Forward exit code
506
+ if (execResult?.exitCode !== 0) {
507
+ console.error(`kubectl failed failed with exit code ${execResult.exitCode}`)
508
+ exit(execResult.exitCode)
509
+ }
510
+ } catch (err) {
511
+ const errMessage = err instanceof Error ? err.message : String(err)
512
+ console.error(
513
+ `Failed to execute 'kubectl': ${errMessage}. You need to have kubectl installed to debug a game server with metaplay-auth.`
514
+ )
515
+
516
+ // Remove temporary kubeconfig
517
+ logger.debug('Delete temporary kubeconfig')
518
+ await unlink(tmpKubeconfigPath)
519
+ }
520
+ }
package/src/logging.ts CHANGED
@@ -5,15 +5,15 @@ import { Logger } from 'tslog'
5
5
  */
6
6
  export const logger = new Logger({
7
7
  overwrite: {
8
- transportFormatted (logMetaMarkup, logArgs, logErrors, settings) {
8
+ transportFormatted(logMetaMarkup, logArgs, logErrors): void {
9
9
  console.error(`${logMetaMarkup} ${logArgs.join('')} ${logErrors.join('')}`)
10
- }
11
- }
10
+ },
11
+ },
12
12
  })
13
13
 
14
14
  /**
15
15
  * Set the log level. We use 0 and 10.
16
16
  */
17
- export function setLogLevel (level: number): void {
17
+ export function setLogLevel(level: number): void {
18
18
  logger.settings.minLevel = level
19
19
  }
@@ -11,7 +11,7 @@ const salt: string = process.env.METAPLAY_AUTH_SALT ?? 'saltysalt'
11
11
 
12
12
  type Secrets = Record<string, string>
13
13
 
14
- async function loadSecrets (): Promise<Secrets> {
14
+ async function loadSecrets(): Promise<Secrets> {
15
15
  try {
16
16
  logger.debug(`Loading secrets from ${filePath}...`)
17
17
 
@@ -37,11 +37,12 @@ async function loadSecrets (): Promise<Secrets> {
37
37
  if (password.length === 0) {
38
38
  throw new Error('The file is encrypted. Please set the METAPLAY_AUTH_PASSWORD environment variable.')
39
39
  }
40
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
40
41
  throw error
41
42
  }
42
43
  }
43
44
 
44
- function encrypt (text: string): string {
45
+ function encrypt(text: string): string {
45
46
  const key = scryptSync(password, salt, 32)
46
47
  const iv = randomBytes(16) // Generate a 16-byte IV
47
48
  const cipher = createCipheriv(algorithm, key, iv)
@@ -49,13 +50,14 @@ function encrypt (text: string): string {
49
50
  return iv.toString('hex') + ':' + encrypted.toString('hex') // Store IV with the encrypted data
50
51
  }
51
52
 
52
- function decrypt (text: string): string {
53
+ function decrypt(text: string): string {
53
54
  const textParts = text.split(':')
54
55
  if (textParts.length !== 2) {
55
56
  throw new Error('Invalid encrypted data format.')
56
57
  }
57
58
  const iv = Buffer.from(textParts[0], 'hex')
58
- if (iv.length !== 16) { // Ensure the IV is 16 bytes
59
+ if (iv.length !== 16) {
60
+ // Ensure the IV is 16 bytes
59
61
  throw new Error('Invalid IV length.')
60
62
  }
61
63
  const encryptedText = Buffer.from(textParts[1], 'hex')
@@ -65,7 +67,7 @@ function decrypt (text: string): string {
65
67
  return decrypted.toString()
66
68
  }
67
69
 
68
- export async function setSecret (key: string, value: any): Promise<void> {
70
+ export async function setSecret(key: string, value: any): Promise<void> {
69
71
  logger.debug(`Setting secret ${key}...`)
70
72
 
71
73
  const secrets = await loadSecrets()
@@ -75,17 +77,18 @@ export async function setSecret (key: string, value: any): Promise<void> {
75
77
  await fs.writeFile(filePath, content)
76
78
  }
77
79
 
78
- export async function getSecret (key: string): Promise<any | undefined> {
80
+ export async function getSecret(key: string): Promise<any | undefined> {
79
81
  logger.debug(`Getting secret ${key}...`)
80
82
 
81
83
  const secrets = await loadSecrets()
82
84
  return secrets[key]
83
85
  }
84
86
 
85
- export async function removeSecret (key: string): Promise<void> {
87
+ export async function removeSecret(key: string): Promise<void> {
86
88
  logger.debug(`Removing secret ${key}...`)
87
89
 
88
90
  const currentSecrets = await loadSecrets()
91
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
89
92
  const { [key]: _, ...secrets } = currentSecrets // remove key from secrets
90
93
 
91
94
  const secretJson = JSON.stringify(secrets)