@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/src/deployment.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
51
|
+
function resolvePodContainersConditions(pod: V1Pod): GameServerPodStatus {
|
|
41
52
|
const containerStatuses = pod.status?.containerStatuses
|
|
42
53
|
if (!containerStatuses || containerStatuses.length === 0) {
|
|
43
|
-
return {
|
|
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(
|
|
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 {
|
|
79
|
+
return {
|
|
80
|
+
phase: GameServerPodPhase.Ready,
|
|
81
|
+
message: `Container ${containerName} is in ready phase`,
|
|
82
|
+
details: containerState.running,
|
|
83
|
+
}
|
|
64
84
|
} else {
|
|
65
|
-
return {
|
|
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 = [
|
|
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
|
|
81
|
-
return {
|
|
82
|
-
|
|
83
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
163
|
+
function resolveRunningPodStatusConditions(pod: V1Pod): GameServerPodStatus {
|
|
107
164
|
const conditions = pod.status?.conditions
|
|
108
165
|
if (!conditions || conditions.length === 0) {
|
|
109
|
-
return {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
221
|
-
|
|
361
|
+
async function delay(ms: number): Promise<void> {
|
|
362
|
+
await new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
222
363
|
}
|
|
223
364
|
|
|
224
|
-
function anyPodsInPhase
|
|
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
|
|
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
|
|
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(
|
|
246
|
-
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
17
|
+
export function setLogLevel(level: number): void {
|
|
18
18
|
logger.settings.minLevel = level
|
|
19
19
|
}
|
package/src/secret_store.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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) {
|
|
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
|
|
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
|
|
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
|
|
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)
|