@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/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
- Failed = 'Failed', // Failed to start
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
- // Only one container allowed in the game server pod
46
- if (containerStatuses.length !== 1) {
47
- throw new Error(`Internal error: Expecting only one container in the game server pod, got ${containerStatuses.length}`)
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
- const containerStatus = containerStatuses[0]
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 (containerStatus.ready && containerState.running) {
61
- return { phase: GameServerPodPhase.Ready, message: `Container ${containerName} is in ready phase and was started at ${containerState.running.startedAt}`, details: containerState.running }
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
- console.log('Game server pod container in unknown state:', containerState)
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
- // console.log('Pod.status:', JSON.stringify(pod.status, undefined, 2))
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('Pod logs:\n' + logs)
213
+ console.log(`Logs from pod '${podName}':\n${logs}`)
204
214
  }
205
215
 
206
- console.log(`Pod ${pod.metadata?.name} status:`, podStatus)
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
- export async function checkGameServerDeployment (namespace: string): Promise<number> {
215
- logger.info(`Validating game server deployment in namespace ${namespace}`)
224
+ function anyPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
225
+ return podStatuses.some(status => status.phase === phase)
226
+ }
216
227
 
217
- // Check that the KUBECONFIG environment variable exists
218
- const kubeconfigPath = process.env.KUBECONFIG
219
- if (!kubeconfigPath) {
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
- // Check that the kubeconfig file exists
224
- if (!await existsSync(kubeconfigPath)) {
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
- const podStatus = await checkGameServerPod(k8sApi, pods[0]) // \todo Handle all pods
246
-
247
- switch (podStatus.phase) {
248
- case GameServerPodPhase.Ready:
249
- console.log('Gameserver successfully started')
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
- case GameServerPodPhase.Failed:
254
- console.log('Gameserver failed to start')
255
- return 1
256
-
257
- case GameServerPodPhase.Pending:
258
- console.log('Gameserver still starting')
259
- break
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
- logger.error('Timeout while waiting for gameserver to initialize')
280
+ console.log('Deployment failed! Timeout while waiting for gameserver to initialize.')
269
281
  return 124 // timeout
270
282
  }
271
283
 
272
- await delay(1000)
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
- const response = await fetch(url, {
142
- method: 'POST',
143
- headers: {
144
- Authorization: `Bearer ${this.accessToken}`,
145
- 'Content-Type': 'application/json'
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('--organization', gs.organization, '--project', gs.project, '--environment', gs.environment)
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
- const response = await fetch(url, {
182
- method: 'POST',
183
- headers: {
184
- Authorization: `Bearer ${this.accessToken}`,
185
- 'Content-Type': 'application/json'
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
+ }