@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/.prettierignore +2 -0
- package/CHANGELOG.md +58 -40
- package/dist/index.cjs +125 -125
- package/eslint.config.js +3 -0
- package/index.ts +717 -439
- package/package.json +18 -15
- package/prettier.config.js +3 -0
- package/src/auth.ts +115 -80
- package/src/buildCommand.ts +267 -0
- package/src/config.ts +12 -0
- package/src/deployment.ts +182 -59
- package/src/logging.ts +4 -4
- package/src/secret_store.ts +10 -7
- package/src/stackapi.ts +2 -33
- package/src/targetenvironment.ts +61 -57
- package/src/utils.ts +120 -29
- package/src/version.ts +1 -1
package/src/deployment.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
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
9
|
import { TargetEnvironment } from 'targetenvironment.js'
|
|
4
10
|
import { exit } from 'process'
|
|
5
11
|
import { unlink, writeFile } from 'fs/promises'
|
|
6
|
-
import { executeCommand
|
|
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
|
|
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
|
|
51
|
+
function resolvePodContainersConditions(pod: V1Pod): GameServerPodStatus {
|
|
46
52
|
const containerStatuses = pod.status?.containerStatuses
|
|
47
53
|
if (!containerStatuses || containerStatuses.length === 0) {
|
|
48
|
-
return {
|
|
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(
|
|
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 {
|
|
79
|
+
return {
|
|
80
|
+
phase: GameServerPodPhase.Ready,
|
|
81
|
+
message: `Container ${containerName} is in ready phase`,
|
|
82
|
+
details: containerState.running,
|
|
83
|
+
}
|
|
69
84
|
} else {
|
|
70
|
-
return {
|
|
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 = [
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
163
|
+
function resolveRunningPodStatusConditions(pod: V1Pod): GameServerPodStatus {
|
|
112
164
|
const conditions = pod.status?.conditions
|
|
113
165
|
if (!conditions || conditions.length === 0) {
|
|
114
|
-
return {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
289
|
+
return resolveRunningPodStatusConditions(pod)
|
|
191
290
|
|
|
192
291
|
case 'Succeeded':
|
|
193
292
|
// Should not happen, the game server pods should never stop
|
|
194
|
-
return {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
278
|
-
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
|
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.
|
|
446
|
+
kubeconfigPayload = await targetEnv.getKubeConfigWithEmbeddedCredentials()
|
|
328
447
|
kubeconfig.loadFromString(kubeconfigPayload)
|
|
329
448
|
} catch (error) {
|
|
330
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
package/src/stackapi.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
}
|