@metaplay/metaplay-auth 1.5.0 → 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,9 +1,15 @@
1
- import { KubeConfig, CoreV1Api, type V1Pod, type CoreV1ApiListNamespacedPodRequest, type CoreV1ApiReadNamespacedPodLogRequest } 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
9
  import { TargetEnvironment } from 'targetenvironment.js'
4
10
  import { exit } from 'process'
5
11
  import { unlink, writeFile } from 'fs/promises'
6
- import { executeCommand, type ExecuteCommandResult } from './utils.js'
12
+ import { executeCommand } from './utils.js'
7
13
  import os from 'os'
8
14
  import path from 'path'
9
15
 
@@ -21,12 +27,12 @@ interface GameServerPodStatus {
21
27
  details?: any
22
28
  }
23
29
 
24
- async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string): Promise<V1Pod[]> {
30
+ async function fetchGameServerPods(k8sApi: CoreV1Api, namespace: string): Promise<V1Pod[]> {
25
31
  // Define label selector for gameserver
26
32
  logger.debug(`Fetching game server pods from Kubernetes: namespace=${namespace}`)
27
33
  const param: CoreV1ApiListNamespacedPodRequest = {
28
34
  namespace,
29
- labelSelector: 'app=metaplay-server'
35
+ labelSelector: 'app=metaplay-server',
30
36
  }
31
37
 
32
38
  try {
@@ -42,20 +48,25 @@ async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string): Promi
42
48
  }
43
49
  }
44
50
 
45
- function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
51
+ function resolvePodContainersConditions(pod: V1Pod): GameServerPodStatus {
46
52
  const containerStatuses = pod.status?.containerStatuses
47
53
  if (!containerStatuses || containerStatuses.length === 0) {
48
- 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
+ }
49
58
  }
50
59
 
51
60
  // Find the shard-server container from the pod (ignore others)
52
- const containerStatus = containerStatuses.find(status => status.name === 'shard-server')
61
+ const containerStatus = containerStatuses.find((status) => status.name === 'shard-server')
53
62
  if (!containerStatus) {
54
63
  return { phase: GameServerPodPhase.Unknown, message: 'Unable to find container shard-server from the pod' }
55
64
  }
56
65
 
57
66
  // Handle missing container state
58
- 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
+ )
59
70
  const containerState = containerStatus.state
60
71
  if (!containerState) {
61
72
  return { phase: GameServerPodPhase.Unknown, message: 'Unable to get container state' }
@@ -65,14 +76,31 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
65
76
  const containerName = containerStatus.name
66
77
  if (containerState.running) {
67
78
  if (containerStatus.ready) {
68
- 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
+ }
69
84
  } else {
70
- 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
+ }
71
90
  }
72
91
  }
73
92
 
74
93
  // \note these may not be complete (or completely accurate)
75
- 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
+ ]
76
104
  const knownContainerPendingReasons = ['Init', 'Pending', 'PodInitializing']
77
105
 
78
106
  // Check if there's a previous terminated state (usually indicates a crash during server initialization)
@@ -83,21 +111,45 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
83
111
  if (containerState.waiting) {
84
112
  const reason = containerState.waiting.reason
85
113
  if (reason && knownContainerFailureReasons.includes(reason)) {
86
- return { phase: GameServerPodPhase.Failed, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
114
+ return {
115
+ phase: GameServerPodPhase.Failed,
116
+ message: `Container ${containerName} is in waiting state, reason=${reason}`,
117
+ details: containerState.waiting,
118
+ }
87
119
  } else if (reason && knownContainerPendingReasons.includes(reason)) {
88
- return { phase: GameServerPodPhase.Pending, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
120
+ return {
121
+ phase: GameServerPodPhase.Pending,
122
+ message: `Container ${containerName} is in waiting state, reason=${reason}`,
123
+ details: containerState.waiting,
124
+ }
89
125
  } else {
90
- 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
+ }
91
131
  }
92
132
  } else if (containerState.running) {
93
133
  // This happens when the container is still initializing
94
- 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
+ }
95
139
  } else if (containerState.terminated) {
96
- 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
+ }
97
145
  }
98
146
 
99
147
  // Unable to determine launch failure reason, just return previous launch
100
- 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
+ }
101
153
  }
102
154
 
103
155
  // \todo handle containerState.running states (including various initialization states)
@@ -108,58 +160,102 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
108
160
  return { phase: GameServerPodPhase.Unknown, message: 'Container in unknown state', details: containerState }
109
161
  }
110
162
 
111
- function resolvePodStatusConditions (pod: V1Pod): GameServerPodStatus {
163
+ function resolveRunningPodStatusConditions(pod: V1Pod): GameServerPodStatus {
112
164
  const conditions = pod.status?.conditions
113
165
  if (!conditions || conditions.length === 0) {
114
- 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
+ }
115
171
  }
116
172
 
117
173
  // Bail if 'PodScheduled' is not yet true
118
- const condPodScheduled = conditions.find(cond => cond.type === 'PodScheduled')
174
+ const condPodScheduled = conditions.find((cond) => cond.type === 'PodScheduled')
119
175
  if (condPodScheduled?.status !== 'True') {
120
- 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
+ }
121
181
  }
122
182
 
123
183
  // Bail if 'Initialized' not is yet true
124
- const condInitialized = conditions.find(cond => cond.type === 'Initialized')
184
+ const condInitialized = conditions.find((cond) => cond.type === 'Initialized')
125
185
  if (condInitialized?.status !== 'True') {
126
- 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
+ }
127
191
  }
128
192
 
129
193
  // Bail if 'ContainersReady' is not yet true
130
- const condContainersReady = conditions.find(cond => cond.type === 'ContainersReady')
194
+ const condContainersReady = conditions.find((cond) => cond.type === 'ContainersReady')
131
195
  if (condContainersReady?.status !== 'True') {
132
196
  if (condContainersReady?.reason === 'ContainersNotReady') {
133
197
  return resolvePodContainersConditions(pod)
134
198
  }
135
199
 
136
- 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
+ }
137
205
  }
138
206
 
139
207
  // Bail if 'Ready' is not yet true
140
- const condReady = conditions.find(cond => cond.type === 'Ready')
208
+ const condReady = conditions.find((cond) => cond.type === 'Ready')
141
209
  if (condReady?.status !== 'True') {
142
- 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
+ }
143
215
  }
144
216
 
145
217
  // resolvePodContainersConditions(pod) // DEBUG DEBUG enable to print container state for running pods
146
218
  return { phase: GameServerPodPhase.Ready, message: 'Pod is ready to serve traffic' }
147
219
  }
148
220
 
149
- function resolvePodGameServerImageTag (pod: V1Pod): string | null {
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 {
150
245
  // 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')
246
+ const containerSpec = pod.spec?.containers?.find((containerSpec) => containerSpec.name === 'shard-server')
152
247
  if (!containerSpec) {
153
248
  return null
154
249
  }
155
250
  if (!containerSpec.image) {
156
251
  return null
157
252
  }
253
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
158
254
  const [_, imageTag] = containerSpec.image.split(':')
159
255
  return imageTag
160
256
  }
161
257
 
162
- function resolvePodStatus (pod: V1Pod, requiredImageTag: string | null): GameServerPodStatus {
258
+ function resolvePodStatus(pod: V1Pod, requiredImageTag: string | null): GameServerPodStatus {
163
259
  // logger.debug('resolvePodStatus(): pod =', JSON.stringify(pod, undefined, 2))
164
260
 
165
261
  if (!pod) {
@@ -174,7 +270,10 @@ function resolvePodStatus (pod: V1Pod, requiredImageTag: string | null): GameSer
174
270
  if (requiredImageTag !== null) {
175
271
  const podImageTag = resolvePodGameServerImageTag(pod)
176
272
  if (podImageTag !== requiredImageTag) {
177
- return { phase: GameServerPodPhase.Unknown, message: `Image tag is not (yet?) updated. Pod image is ${podImageTag ?? 'unknown'}, expecting ${requiredImageTag}.` }
273
+ return {
274
+ phase: GameServerPodPhase.Unknown,
275
+ message: `Image tag is not (yet?) updated. Pod image is ${podImageTag ?? 'unknown'}, expecting ${requiredImageTag}.`,
276
+ }
178
277
  }
179
278
  }
180
279
 
@@ -183,19 +282,25 @@ function resolvePodStatus (pod: V1Pod, requiredImageTag: string | null): GameSer
183
282
  switch (podPhase) {
184
283
  case 'Pending':
185
284
  // Pod not yet scheduled
186
- return { phase: GameServerPodPhase.Pending, message: 'Pod is still in Pending phase' }
285
+ return resolvePendingPodStatusConditions(pod)
187
286
 
188
287
  case 'Running':
189
288
  // Pod has been scheduled and start -- note that the containers may have failed!
190
- return resolvePodStatusConditions(pod)
289
+ return resolveRunningPodStatusConditions(pod)
191
290
 
192
291
  case 'Succeeded':
193
292
  // 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)' }
293
+ return {
294
+ phase: GameServerPodPhase.Unknown,
295
+ message: 'Pod has unexpectedly terminated (with a clean exit status)',
296
+ }
195
297
 
196
298
  case 'Failed':
197
299
  // 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)' }
300
+ return {
301
+ phase: GameServerPodPhase.Unknown,
302
+ message: 'Pod has unexpectedly terminated (with a failure exit status)',
303
+ }
199
304
 
200
305
  case 'Unknown':
201
306
  default:
@@ -203,7 +308,7 @@ function resolvePodStatus (pod: V1Pod, requiredImageTag: string | null): GameSer
203
308
  }
204
309
  }
205
310
 
206
- async function fetchPodLogs (k8sApi: CoreV1Api, pod: V1Pod) {
311
+ async function fetchPodLogs(k8sApi: CoreV1Api, pod: V1Pod): Promise<string> {
207
312
  const podName = pod.metadata?.name
208
313
  logger.debug(`Fetching logs for pod '${podName}'..`)
209
314
  const namespace = pod.metadata?.namespace
@@ -219,7 +324,7 @@ async function fetchPodLogs (k8sApi: CoreV1Api, pod: V1Pod) {
219
324
  pretty: 'true',
220
325
  previous: false,
221
326
  tailLines: 100,
222
- timestamps: true
327
+ timestamps: true,
223
328
  }
224
329
 
225
330
  try {
@@ -232,7 +337,11 @@ async function fetchPodLogs (k8sApi: CoreV1Api, pod: V1Pod) {
232
337
  }
233
338
  }
234
339
 
235
- async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod, requiredImageTag: string | null) {
340
+ async function checkGameServerPod(
341
+ k8sApi: CoreV1Api,
342
+ pod: V1Pod,
343
+ requiredImageTag: string | null
344
+ ): Promise<GameServerPodStatus> {
236
345
  // console.log('Pod:', JSON.stringify(pod, undefined, 2))
237
346
 
238
347
  // Classify game server status
@@ -249,19 +358,23 @@ async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod, requiredImageT
249
358
  return podStatus
250
359
  }
251
360
 
252
- async function delay (ms: number): Promise<void> {
253
- 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))
254
363
  }
255
364
 
256
- function anyPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
257
- return podStatuses.some(status => status.phase === phase)
365
+ function anyPodsInPhase(podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
366
+ return podStatuses.some((status) => status.phase === phase)
258
367
  }
259
368
 
260
- function allPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
261
- return podStatuses.every(status => status.phase === phase)
369
+ function allPodsInPhase(podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
370
+ return podStatuses.every((status) => status.phase === phase)
262
371
  }
263
372
 
264
- export async function checkGameServerDeployment (namespace: string, kubeconfig: KubeConfig, requiredImageTag: string | null): Promise<number> {
373
+ export async function checkGameServerDeployment(
374
+ namespace: string,
375
+ kubeconfig: KubeConfig,
376
+ requiredImageTag: string | null
377
+ ): Promise<number> {
265
378
  const k8sApi = kubeconfig.makeApiClient(CoreV1Api)
266
379
 
267
380
  // Figure out when to stop
@@ -274,8 +387,10 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
274
387
  logger.debug(`Found ${pods?.length} pod(s) deployed in Kubernetes`)
275
388
  if (pods.length > 0) {
276
389
  // Resolve status for all pods
277
- const podStatuses = await Promise.all(pods.map(async pod => await checkGameServerPod(k8sApi, pod, requiredImageTag)))
278
- 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()}`)
279
394
 
280
395
  // Handle state of the deployment
281
396
  if (anyPodsInPhase(podStatuses, GameServerPodPhase.Failed)) {
@@ -283,15 +398,19 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
283
398
  console.log('Gameserver failed to start due to the pods not starting properly! See above for details.')
284
399
  for (let ndx = 0; ndx < pods.length; ndx += 1) {
285
400
  const status = podStatuses[ndx]
286
- const suffix = (status.phase !== GameServerPodPhase.Ready) ? ` -- ${status.message}` : ''
401
+ const suffix = status.phase !== GameServerPodPhase.Ready ? ` -- ${status.message}` : ''
287
402
  console.log(` ${pods[ndx].metadata?.name}: ${status.phase}${suffix}`)
288
403
  }
289
404
  return 1
290
- } 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
+ ) {
291
410
  console.log('Waiting for gameserver(s) to be ready...')
292
411
  for (let ndx = 0; ndx < pods.length; ndx += 1) {
293
412
  const status = podStatuses[ndx]
294
- const suffix = (status.phase !== GameServerPodPhase.Ready) ? ` -- ${status.message}` : ''
413
+ const suffix = status.phase !== GameServerPodPhase.Ready ? ` -- ${status.message}` : ''
295
414
  console.log(` ${pods[ndx].metadata?.name}: ${status.phase}${suffix}`)
296
415
  }
297
416
  } else if (allPodsInPhase(podStatuses, GameServerPodPhase.Ready)) {
@@ -302,7 +421,7 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
302
421
  console.log('Deployment in inconsistent state, waiting...')
303
422
  for (let ndx = 0; ndx < pods.length; ndx += 1) {
304
423
  const status = podStatuses[ndx]
305
- const suffix = (status.phase !== GameServerPodPhase.Ready) ? ` -- ${status.message}` : ''
424
+ const suffix = status.phase !== GameServerPodPhase.Ready ? ` -- ${status.message}` : ''
306
425
  console.log(` ${pods[ndx].metadata?.name}: ${status.phase}${suffix}`)
307
426
  }
308
427
  }
@@ -318,16 +437,17 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
318
437
  }
319
438
  }
320
439
 
321
- export async function debugGameServer (targetEnv: TargetEnvironment, targetPodName?: string) {
440
+ export async function debugGameServer(targetEnv: TargetEnvironment, targetPodName?: string): Promise<void> {
322
441
  // Initialize kubeconfig for target environment
323
442
  logger.debug('Get kubeconfig')
324
- let kubeconfigPayload
443
+ let kubeconfigPayload: string
325
444
  const kubeconfig = new KubeConfig()
326
445
  try {
327
- kubeconfigPayload = await targetEnv.getKubeConfig()
446
+ kubeconfigPayload = await targetEnv.getKubeConfigWithEmbeddedCredentials()
328
447
  kubeconfig.loadFromString(kubeconfigPayload)
329
448
  } catch (error) {
330
- console.error(`Failed to fetch kubeconfig for environment: ${error}`)
449
+ const errMessage = error instanceof Error ? error.message : String(error)
450
+ console.error(`Failed to fetch kubeconfig for environment: ${errMessage}`)
331
451
  exit(1)
332
452
  }
333
453
 
@@ -350,7 +470,7 @@ export async function debugGameServer (targetEnv: TargetEnvironment, targetPodNa
350
470
  }
351
471
  targetPodName = metadata.name
352
472
  } else {
353
- const podNames = gameServerPods.map(pod => pod.metadata?.name).join(', ')
473
+ const podNames = gameServerPods.map((pod) => pod.metadata?.name).join(', ')
354
474
  console.error(`Multiple game server pods running: ${podNames}\nPlease specify which you want to debug.`)
355
475
  exit(1)
356
476
  }
@@ -373,10 +493,10 @@ export async function debugGameServer (targetEnv: TargetEnvironment, targetPodNa
373
493
  '-it',
374
494
  '--profile=general',
375
495
  '--image=metaplay/diagnostics:latest',
376
- '--target=shard-server'
496
+ '--target=shard-server',
377
497
  ]
378
498
  console.log(`Execute: kubectl ${args.join(' ')}`)
379
- const execResult = await executeCommand('kubectl', args, true)
499
+ const execResult = await executeCommand('kubectl', args, { inheritStdio: true })
380
500
 
381
501
  // Remove temporary kubeconfig
382
502
  logger.debug('Delete temporary kubeconfig')
@@ -388,7 +508,10 @@ export async function debugGameServer (targetEnv: TargetEnvironment, targetPodNa
388
508
  exit(execResult.exitCode)
389
509
  }
390
510
  } catch (err) {
391
- console.error(`Failed to execute 'kubectl': ${err}. You need to have kubectl installed to debug a game server with metaplay-auth.`)
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
+ )
392
515
 
393
516
  // Remove temporary kubeconfig
394
517
  logger.debug('Delete temporary kubeconfig')
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)
package/src/stackapi.ts CHANGED
@@ -1,13 +1,9 @@
1
- import { logger } from './logging.js'
2
- import { type EnvironmentDetails } from './targetenvironment.js'
3
-
4
- export const defaultStackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
5
-
1
+ // \note Unused right now but will likely need this back later
6
2
  export class StackAPI {
7
3
  private readonly accessToken: string
8
4
  private readonly stackApiBaseUrl: string
9
5
 
10
- constructor (accessToken: string, stackApiBaseUrl: string | undefined) {
6
+ constructor(accessToken: string, stackApiBaseUrl: string | undefined) {
11
7
  if (accessToken == null) {
12
8
  throw new Error('accessToken must be provided')
13
9
  }
@@ -20,31 +16,4 @@ export class StackAPI {
20
16
  this.stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
21
17
  }
22
18
  }
23
-
24
- async resolveManagedEnvironmentFQDN (organization: string, project: string, environment: string): Promise<string> {
25
- const url = `${this.stackApiBaseUrl}/v1/servers/${organization}/${project}/${environment}`
26
- logger.debug(`Getting environment information from ${url}...`)
27
- const response = await fetch(url, {
28
- method: 'GET',
29
- headers: {
30
- Authorization: `Bearer ${this.accessToken}`,
31
- 'Content-Type': 'application/json'
32
- }
33
- })
34
-
35
- // Throw on server errors (eg, forbidden)
36
- if (!response.ok) {
37
- const errorData = await response.json()
38
- throw new Error(`Failed to fetch environment details with error ${response.status}: ${JSON.stringify(errorData)}`)
39
- }
40
-
41
- // Sanity check that response is of the right structure
42
- const envDetails = await response.json() as EnvironmentDetails
43
- logger.debug(`Received environment details: ${JSON.stringify(envDetails)}`)
44
- if (!envDetails.deployment) {
45
- throw new Error(`Received invalid environment details -- missing .deployment: ${JSON.stringify(envDetails)}`)
46
- }
47
-
48
- return envDetails.deployment.server_hostname
49
- }
50
19
  }