@metaplay/metaplay-auth 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/index.js +319 -17
- package/dist/index.js.map +1 -1
- package/dist/src/auth.js +19 -7
- package/dist/src/auth.js.map +1 -1
- package/dist/src/deployment.js +72 -53
- package/dist/src/deployment.js.map +1 -1
- package/dist/src/stackapi.js +35 -13
- package/dist/src/stackapi.js.map +1 -1
- package/dist/src/utils.js +21 -0
- package/dist/src/utils.js.map +1 -1
- package/index.ts +353 -19
- package/package.json +9 -3
- package/src/auth.ts +18 -7
- package/src/deployment.ts +70 -57
- package/src/stackapi.ts +33 -13
- package/src/utils.ts +37 -0
package/src/deployment.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { existsSync } from 'fs'
|
|
2
1
|
import { KubeConfig, CoreV1Api, V1Pod } from '@kubernetes/client-node'
|
|
3
2
|
import { logger } from './logging.js'
|
|
4
3
|
import type { CoreV1ApiListNamespacedPodRequest, CoreV1ApiReadNamespacedPodLogRequest } from '@kubernetes/client-node'
|
|
5
4
|
|
|
6
5
|
enum GameServerPodPhase {
|
|
7
|
-
Pending = 'Pending', // Still being deployed
|
|
8
6
|
Ready = 'Ready', // Successfully started and ready to accept traffic
|
|
9
|
-
|
|
7
|
+
Running = 'Running', // Started but not yet serving traffic (eg, waiting for peers to connect)
|
|
8
|
+
Pending = 'Pending', // Still being deployed
|
|
10
9
|
Unknown = 'Unknown', // Unknown status -- treat like Pending
|
|
10
|
+
Failed = 'Failed', // Failed to start
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
interface GameServerPodStatus {
|
|
@@ -18,6 +18,7 @@ interface GameServerPodStatus {
|
|
|
18
18
|
|
|
19
19
|
async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string) {
|
|
20
20
|
// Define label selector for gameserver
|
|
21
|
+
logger.debug(`Fetching game server pods from Kubernetes: namespace=${namespace}`)
|
|
21
22
|
const param: CoreV1ApiListNamespacedPodRequest = {
|
|
22
23
|
namespace,
|
|
23
24
|
labelSelector: 'app=metaplay-server'
|
|
@@ -42,14 +43,14 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
|
|
|
42
43
|
return { phase: GameServerPodPhase.Unknown, message: 'Unable to determine pod container statuses: pod.status.containerStatuses is empty' }
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
// Find the shard-server container from the pod (ignore others)
|
|
47
|
+
const containerStatus = containerStatuses.find(status => status.name === 'shard-server')
|
|
48
|
+
if (!containerStatus) {
|
|
49
|
+
return { phase: GameServerPodPhase.Unknown, message: 'Unable to find container shard-server from the pod' }
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
// Handle missing container state
|
|
51
|
-
|
|
52
|
-
console.log('Container status:', containerStatus)
|
|
53
|
+
logger.debug(`Container status for pod ${pod.metadata?.name ?? '<unnamed>'}: ${JSON.stringify(containerStatus, undefined, 2)}`)
|
|
53
54
|
const containerState = containerStatus.state
|
|
54
55
|
if (!containerState) {
|
|
55
56
|
return { phase: GameServerPodPhase.Unknown, message: 'Unable to get container state' }
|
|
@@ -57,8 +58,12 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
|
|
|
57
58
|
|
|
58
59
|
// Check if container running & ready
|
|
59
60
|
const containerName = containerStatus.name
|
|
60
|
-
if (
|
|
61
|
-
|
|
61
|
+
if (containerState.running) {
|
|
62
|
+
if (containerStatus.ready) {
|
|
63
|
+
return { phase: GameServerPodPhase.Ready, message: `Container ${containerName} is in ready phase`, details: containerState.running }
|
|
64
|
+
} else {
|
|
65
|
+
return { phase: GameServerPodPhase.Running, message: `Container ${containerName} is in running phase`, details: containerState.running }
|
|
66
|
+
}
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
// \note these may not be complete (or completely accurate)
|
|
@@ -94,7 +99,7 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
|
|
|
94
99
|
// \todo handle containerState.terminated states (what do these even mean)
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
|
|
102
|
+
logger.debug('Game server pod container in unknown state:', containerState)
|
|
98
103
|
return { phase: GameServerPodPhase.Unknown, message: 'Container in unknown state', details: containerState }
|
|
99
104
|
}
|
|
100
105
|
|
|
@@ -137,7 +142,11 @@ function resolvePodStatusConditions (pod: V1Pod): GameServerPodStatus {
|
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
function resolvePodStatus (pod: V1Pod): GameServerPodStatus {
|
|
140
|
-
//
|
|
145
|
+
// logger.debug('resolvePodStatus(): pod =', JSON.stringify(pod, undefined, 2))
|
|
146
|
+
|
|
147
|
+
if (!pod) {
|
|
148
|
+
return { phase: GameServerPodPhase.Unknown, message: 'Received empty pod from Kubernetes' }
|
|
149
|
+
}
|
|
141
150
|
|
|
142
151
|
if (!pod.status) {
|
|
143
152
|
return { phase: GameServerPodPhase.Unknown, message: 'Unable to access pod.status from Kubernetes' }
|
|
@@ -163,12 +172,12 @@ function resolvePodStatus (pod: V1Pod): GameServerPodStatus {
|
|
|
163
172
|
}
|
|
164
173
|
|
|
165
174
|
async function fetchPodLogs (k8sApi: CoreV1Api, pod: V1Pod) {
|
|
166
|
-
console.log('Fetching logs for pod..')
|
|
167
175
|
const podName = pod.metadata?.name
|
|
176
|
+
logger.debug(`Fetching logs for pod '${podName}'..`)
|
|
168
177
|
const namespace = pod.metadata?.namespace
|
|
169
178
|
const containerName = pod.spec?.containers[0].name // \todo Handle multiple containers?
|
|
170
179
|
if (!podName || !namespace || !containerName) {
|
|
171
|
-
throw new Error('Unable to determine pod metadata')
|
|
180
|
+
throw new Error('Unable to determine pod and container metadata')
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
const params: CoreV1ApiReadNamespacedPodLogRequest = {
|
|
@@ -196,14 +205,15 @@ async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod) {
|
|
|
196
205
|
|
|
197
206
|
// Classify game server status
|
|
198
207
|
const podStatus = resolvePodStatus(pod)
|
|
208
|
+
const podName = pod.metadata?.name ?? '<unable to resolve name>'
|
|
199
209
|
|
|
200
210
|
// If game server launch failed, get the error logs
|
|
201
211
|
if (podStatus.phase === GameServerPodPhase.Failed) {
|
|
202
212
|
const logs = await fetchPodLogs(k8sApi, pod)
|
|
203
|
-
console.log('
|
|
213
|
+
console.log(`Logs from pod '${podName}':\n${logs}`)
|
|
204
214
|
}
|
|
205
215
|
|
|
206
|
-
|
|
216
|
+
logger.debug(`Pod ${podName} status: ${JSON.stringify(podStatus, undefined, 2)}`)
|
|
207
217
|
return podStatus
|
|
208
218
|
}
|
|
209
219
|
|
|
@@ -211,29 +221,16 @@ async function delay (ms: number): Promise<void> {
|
|
|
211
221
|
return await new Promise<void>(resolve => setTimeout(resolve, ms))
|
|
212
222
|
}
|
|
213
223
|
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
function anyPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
|
|
225
|
+
return podStatuses.some(status => status.phase === phase)
|
|
226
|
+
}
|
|
216
227
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
throw new Error('The KUBECONFIG environment variable must be specified')
|
|
221
|
-
}
|
|
228
|
+
function allPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
|
|
229
|
+
return podStatuses.every(status => status.phase === phase)
|
|
230
|
+
}
|
|
222
231
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
throw new Error(`The environment variable KUBECONFIG points to a file '${kubeconfigPath}' that doesn't exist`)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Create Kubernetes API instance (with default kubeconfig)
|
|
229
|
-
const kc = new KubeConfig()
|
|
230
|
-
// Loda kubeconfig from file and throw error if validation fails.
|
|
231
|
-
try {
|
|
232
|
-
kc.loadFromFile(kubeconfigPath)
|
|
233
|
-
} catch (error) {
|
|
234
|
-
throw new Error(`Failed to load or validate kubeconfig. ${error}`)
|
|
235
|
-
}
|
|
236
|
-
const k8sApi = kc.makeApiClient(CoreV1Api)
|
|
232
|
+
export async function checkGameServerDeployment (namespace: string, kubeconfig: KubeConfig): Promise<number> {
|
|
233
|
+
const k8sApi = kubeconfig.makeApiClient(CoreV1Api)
|
|
237
234
|
|
|
238
235
|
// Figure out when to stop
|
|
239
236
|
const startTime = Date.now()
|
|
@@ -242,33 +239,49 @@ export async function checkGameServerDeployment (namespace: string): Promise<num
|
|
|
242
239
|
while (true) {
|
|
243
240
|
// Check pod states
|
|
244
241
|
const pods = await fetchGameServerPods(k8sApi, namespace)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
242
|
+
logger.debug(`Found ${pods?.length} pod(s) deployed in Kubernetes`)
|
|
243
|
+
if (pods.length > 0) {
|
|
244
|
+
// 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)}`)
|
|
247
|
+
|
|
248
|
+
// Handle state of the deployment
|
|
249
|
+
if (anyPodsInPhase(podStatuses, GameServerPodPhase.Failed)) {
|
|
250
|
+
logger.error(`Gameserver start failed with pod statuses: ${JSON.stringify(podStatuses, undefined, 2)}`)
|
|
251
|
+
console.log('Gameserver failed to start due to the pods not starting properly! See above for details.')
|
|
252
|
+
for (let ndx = 0; ndx < pods.length; ndx += 1) {
|
|
253
|
+
const status = podStatuses[ndx]
|
|
254
|
+
const suffix = (status.phase !== GameServerPodPhase.Ready) ? ` -- ${status.message}` : ''
|
|
255
|
+
console.log(` ${pods[ndx].metadata?.name}: ${status.phase}${suffix}`)
|
|
256
|
+
}
|
|
257
|
+
return 1
|
|
258
|
+
} else if (anyPodsInPhase(podStatuses, GameServerPodPhase.Unknown) || anyPodsInPhase(podStatuses, GameServerPodPhase.Pending) || anyPodsInPhase(podStatuses, GameServerPodPhase.Running)) {
|
|
259
|
+
console.log('Waiting for gameserver(s) to be ready...')
|
|
260
|
+
for (let ndx = 0; ndx < pods.length; ndx += 1) {
|
|
261
|
+
const status = podStatuses[ndx]
|
|
262
|
+
const suffix = (status.phase !== GameServerPodPhase.Ready) ? ` -- ${status.message}` : ''
|
|
263
|
+
console.log(` ${pods[ndx].metadata?.name}: ${status.phase}${suffix}`)
|
|
264
|
+
}
|
|
265
|
+
} else if (allPodsInPhase(podStatuses, GameServerPodPhase.Ready)) {
|
|
266
|
+
console.log('Gameserver is up and ready to serve!')
|
|
250
267
|
// \todo add further readiness checks -- ping endpoint, ping dashboard, other checks?
|
|
251
268
|
return 0
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
case GameServerPodPhase.Unknown:
|
|
262
|
-
default:
|
|
263
|
-
console.log('Gameserver in unknown state')
|
|
264
|
-
break
|
|
269
|
+
} else {
|
|
270
|
+
console.log('Deployment in inconsistent state, waiting...')
|
|
271
|
+
for (let ndx = 0; ndx < pods.length; ndx += 1) {
|
|
272
|
+
const status = podStatuses[ndx]
|
|
273
|
+
const suffix = (status.phase !== GameServerPodPhase.Ready) ? ` -- ${status.message}` : ''
|
|
274
|
+
console.log(` ${pods[ndx].metadata?.name}: ${status.phase}${suffix}`)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
265
277
|
}
|
|
266
278
|
|
|
267
279
|
if (Date.now() >= timeoutAt) {
|
|
268
|
-
|
|
280
|
+
console.log('Deployment failed! Timeout while waiting for gameserver to initialize.')
|
|
269
281
|
return 124 // timeout
|
|
270
282
|
}
|
|
271
283
|
|
|
272
|
-
|
|
284
|
+
// Sleep a bit to avoid spamming the log
|
|
285
|
+
await delay(2000)
|
|
273
286
|
}
|
|
274
287
|
}
|
package/src/stackapi.ts
CHANGED
|
@@ -138,13 +138,23 @@ export class StackAPI {
|
|
|
138
138
|
|
|
139
139
|
logger.debug(`Getting KubeConfig from ${url}...`)
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
let response
|
|
142
|
+
try {
|
|
143
|
+
response = await fetch(url, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
147
|
+
'Content-Type': 'application/json'
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
} catch (error: any) {
|
|
151
|
+
logger.error(`Failed to fetch kubeconfig from ${url}`)
|
|
152
|
+
logger.error('Fetch error details:', error)
|
|
153
|
+
if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
154
|
+
throw new Error(`Failed to to fetch kubeconfig: SSL certificate validation failed for ${url}. Is someone trying to tamper with your internet connection?`)
|
|
146
155
|
}
|
|
147
|
-
|
|
156
|
+
throw new Error(`Failed to fetch kubeconfig from ${url}: ${error}`)
|
|
157
|
+
}
|
|
148
158
|
|
|
149
159
|
if (response.status !== 200) {
|
|
150
160
|
throw new Error(`Failed to fetch KubeConfigs: ${response.statusText}`)
|
|
@@ -171,20 +181,30 @@ export class StackAPI {
|
|
|
171
181
|
} else if (gs.organization != null && gs.project != null && gs.environment != null) {
|
|
172
182
|
url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s?type=execcredential`
|
|
173
183
|
gsSlug = `${gs.organization}.${gs.project}.${gs.environment}`
|
|
174
|
-
execArgs.push(
|
|
184
|
+
execArgs.push(`${gs.organization}-${gs.project}-${gs.environment}`)
|
|
175
185
|
} else {
|
|
176
186
|
throw new Error('Invalid arguments for getKubeConfigExecCredential')
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
logger.debug(`Getting Kubernetes KubeConfig from ${url}...`)
|
|
180
190
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
let response
|
|
192
|
+
try {
|
|
193
|
+
response = await fetch(url, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
197
|
+
'Content-Type': 'application/json'
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
} catch (error: any) {
|
|
201
|
+
logger.error(`Failed to fetch kubeconfig from ${url}`)
|
|
202
|
+
logger.error('Fetch error details:', error)
|
|
203
|
+
if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
204
|
+
throw new Error(`Failed to to fetch kubeconfig: SSL certificate validation failed for ${url}. Is someone trying to tamper with your internet connection?`)
|
|
186
205
|
}
|
|
187
|
-
|
|
206
|
+
throw new Error(`Failed to fetch kubeconfig from ${url}: ${error}`)
|
|
207
|
+
}
|
|
188
208
|
|
|
189
209
|
if (response.status !== 200) {
|
|
190
210
|
throw new Error(`Failed to fetch Kubernetes KubeConfig: ${response.statusText}`)
|
package/src/utils.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
|
|
1
3
|
export function stackApiBaseFromOptions (arg: string, options: Record<string, any>): string {
|
|
2
4
|
return ''
|
|
3
5
|
}
|
|
@@ -46,3 +48,38 @@ export function getGameserverAdminUrl (uri: string): string {
|
|
|
46
48
|
|
|
47
49
|
return `${hostname}-admin.${domain}`
|
|
48
50
|
}
|
|
51
|
+
|
|
52
|
+
interface CommandResult {
|
|
53
|
+
code: number | null
|
|
54
|
+
stdout: any[]
|
|
55
|
+
stderr: any[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function executeCommand (command: string, args: string[]): Promise<CommandResult> {
|
|
59
|
+
return await new Promise((resolve, reject) => {
|
|
60
|
+
const childProcess = spawn(command, args) //, { stdio: 'inherit' }) -- this pipes stdout & stderr to our output
|
|
61
|
+
let stdout: any[] = []
|
|
62
|
+
let stderr: any[] = []
|
|
63
|
+
|
|
64
|
+
childProcess.stdout?.on('data', (data) => {
|
|
65
|
+
stdout += data
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
childProcess.stderr?.on('data', (data) => {
|
|
69
|
+
stderr += data
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// If program runs to completion, resolve promise with success
|
|
73
|
+
childProcess.on('close', (code) => {
|
|
74
|
+
resolve({ code, stdout, stderr })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
childProcess.on('error', (err) => {
|
|
78
|
+
reject(
|
|
79
|
+
new Error(
|
|
80
|
+
`Failed to execute command '${command} ${args.join(' ')}': ${err.message}`
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
}
|