@metaplay/metaplay-auth 1.4.2 → 1.5.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,6 +1,11 @@
1
- import { KubeConfig, CoreV1Api, V1Pod } from '@kubernetes/client-node'
1
+ import { KubeConfig, CoreV1Api, type V1Pod, type CoreV1ApiListNamespacedPodRequest, type CoreV1ApiReadNamespacedPodLogRequest } from '@kubernetes/client-node'
2
2
  import { logger } from './logging.js'
3
- import type { CoreV1ApiListNamespacedPodRequest, CoreV1ApiReadNamespacedPodLogRequest } from '@kubernetes/client-node'
3
+ import { TargetEnvironment } from 'targetenvironment.js'
4
+ import { exit } from 'process'
5
+ import { unlink, writeFile } from 'fs/promises'
6
+ import { executeCommand, type ExecuteCommandResult } from './utils.js'
7
+ import os from 'os'
8
+ import path from 'path'
4
9
 
5
10
  enum GameServerPodPhase {
6
11
  Ready = 'Ready', // Successfully started and ready to accept traffic
@@ -16,7 +21,7 @@ interface GameServerPodStatus {
16
21
  details?: any
17
22
  }
18
23
 
19
- async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string) {
24
+ async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string): Promise<V1Pod[]> {
20
25
  // Define label selector for gameserver
21
26
  logger.debug(`Fetching game server pods from Kubernetes: namespace=${namespace}`)
22
27
  const param: CoreV1ApiListNamespacedPodRequest = {
@@ -32,7 +37,7 @@ async function fetchGameServerPods (k8sApi: CoreV1Api, namespace: string) {
32
37
  return response.items
33
38
  } catch (error) {
34
39
  // \todo Better error handling ..
35
- console.log('Failed to fetch pods from Kubernetes:', error)
40
+ console.error('Failed to fetch pods from Kubernetes:', error)
36
41
  throw new Error('Failed to fetch pods from Kubernetes')
37
42
  }
38
43
  }
@@ -77,9 +82,9 @@ function resolvePodContainersConditions (pod: V1Pod): GameServerPodStatus {
77
82
  // Try to detecth why the previous launch failed
78
83
  if (containerState.waiting) {
79
84
  const reason = containerState.waiting.reason
80
- if (knownContainerFailureReasons.includes(reason as string)) {
85
+ if (reason && knownContainerFailureReasons.includes(reason)) {
81
86
  return { phase: GameServerPodPhase.Failed, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
82
- } else if (knownContainerPendingReasons.includes(reason as string)) {
87
+ } else if (reason && knownContainerPendingReasons.includes(reason)) {
83
88
  return { phase: GameServerPodPhase.Pending, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
84
89
  } else {
85
90
  return { phase: GameServerPodPhase.Unknown, message: `Container ${containerName} is in waiting state, reason=${reason}`, details: containerState.waiting }
@@ -141,7 +146,20 @@ function resolvePodStatusConditions (pod: V1Pod): GameServerPodStatus {
141
146
  return { phase: GameServerPodPhase.Ready, message: 'Pod is ready to serve traffic' }
142
147
  }
143
148
 
144
- function resolvePodStatus (pod: V1Pod): GameServerPodStatus {
149
+ function resolvePodGameServerImageTag (pod: V1Pod): string | null {
150
+ // 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')
152
+ if (!containerSpec) {
153
+ return null
154
+ }
155
+ if (!containerSpec.image) {
156
+ return null
157
+ }
158
+ const [_, imageTag] = containerSpec.image.split(':')
159
+ return imageTag
160
+ }
161
+
162
+ function resolvePodStatus (pod: V1Pod, requiredImageTag: string | null): GameServerPodStatus {
145
163
  // logger.debug('resolvePodStatus(): pod =', JSON.stringify(pod, undefined, 2))
146
164
 
147
165
  if (!pod) {
@@ -152,6 +170,14 @@ function resolvePodStatus (pod: V1Pod): GameServerPodStatus {
152
170
  return { phase: GameServerPodPhase.Unknown, message: 'Unable to access pod.status from Kubernetes' }
153
171
  }
154
172
 
173
+ // Check the pod has the expected image.
174
+ if (requiredImageTag !== null) {
175
+ const podImageTag = resolvePodGameServerImageTag(pod)
176
+ if (podImageTag !== requiredImageTag) {
177
+ return { phase: GameServerPodPhase.Unknown, message: `Image tag is not (yet?) updated. Pod image is ${podImageTag ?? 'unknown'}, expecting ${requiredImageTag}.` }
178
+ }
179
+ }
180
+
155
181
  // Handle status.phase
156
182
  const podPhase = pod.status?.phase
157
183
  switch (podPhase) {
@@ -163,8 +189,14 @@ function resolvePodStatus (pod: V1Pod): GameServerPodStatus {
163
189
  // Pod has been scheduled and start -- note that the containers may have failed!
164
190
  return resolvePodStatusConditions(pod)
165
191
 
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
192
+ case 'Succeeded':
193
+ // 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)' }
195
+
196
+ case 'Failed':
197
+ // 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)' }
199
+
168
200
  case 'Unknown':
169
201
  default:
170
202
  return { phase: GameServerPodPhase.Unknown, message: `Invalid pod.status.phase: ${podPhase}` }
@@ -200,11 +232,11 @@ async function fetchPodLogs (k8sApi: CoreV1Api, pod: V1Pod) {
200
232
  }
201
233
  }
202
234
 
203
- async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod) {
235
+ async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod, requiredImageTag: string | null) {
204
236
  // console.log('Pod:', JSON.stringify(pod, undefined, 2))
205
237
 
206
238
  // Classify game server status
207
- const podStatus = resolvePodStatus(pod)
239
+ const podStatus = resolvePodStatus(pod, requiredImageTag)
208
240
  const podName = pod.metadata?.name ?? '<unable to resolve name>'
209
241
 
210
242
  // If game server launch failed, get the error logs
@@ -218,7 +250,7 @@ async function checkGameServerPod (k8sApi: CoreV1Api, pod: V1Pod) {
218
250
  }
219
251
 
220
252
  async function delay (ms: number): Promise<void> {
221
- return await new Promise<void>(resolve => setTimeout(resolve, ms))
253
+ await new Promise<void>(resolve => setTimeout(resolve, ms))
222
254
  }
223
255
 
224
256
  function anyPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
@@ -229,7 +261,7 @@ function allPodsInPhase (podStatuses: GameServerPodStatus[], phase: GameServerPo
229
261
  return podStatuses.every(status => status.phase === phase)
230
262
  }
231
263
 
232
- export async function checkGameServerDeployment (namespace: string, kubeconfig: KubeConfig): Promise<number> {
264
+ export async function checkGameServerDeployment (namespace: string, kubeconfig: KubeConfig, requiredImageTag: string | null): Promise<number> {
233
265
  const k8sApi = kubeconfig.makeApiClient(CoreV1Api)
234
266
 
235
267
  // Figure out when to stop
@@ -242,7 +274,7 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
242
274
  logger.debug(`Found ${pods?.length} pod(s) deployed in Kubernetes`)
243
275
  if (pods.length > 0) {
244
276
  // Resolve status for all pods
245
- const podStatuses = await Promise.all(pods.map(async pod => await checkGameServerPod(k8sApi, pod)))
277
+ const podStatuses = await Promise.all(pods.map(async pod => await checkGameServerPod(k8sApi, pod, requiredImageTag)))
246
278
  logger.debug(`Pod phases: ${podStatuses.map(status => status.phase)}`)
247
279
 
248
280
  // Handle state of the deployment
@@ -285,3 +317,81 @@ export async function checkGameServerDeployment (namespace: string, kubeconfig:
285
317
  await delay(2000)
286
318
  }
287
319
  }
320
+
321
+ export async function debugGameServer (targetEnv: TargetEnvironment, targetPodName?: string) {
322
+ // Initialize kubeconfig for target environment
323
+ logger.debug('Get kubeconfig')
324
+ let kubeconfigPayload
325
+ const kubeconfig = new KubeConfig()
326
+ try {
327
+ kubeconfigPayload = await targetEnv.getKubeConfig()
328
+ kubeconfig.loadFromString(kubeconfigPayload)
329
+ } catch (error) {
330
+ console.error(`Failed to fetch kubeconfig for environment: ${error}`)
331
+ exit(1)
332
+ }
333
+
334
+ // Fetch environment details
335
+ logger.debug('Get environment details')
336
+ const envInfo = await targetEnv.getEnvironmentDetails()
337
+ const kubernetesNamespace = envInfo.deployment.kubernetes_namespace
338
+
339
+ // Resolve which pods are running in the target environment
340
+ logger.debug('Get running game server pods')
341
+ const k8sApi = kubeconfig.makeApiClient(CoreV1Api)
342
+ const gameServerPods = await fetchGameServerPods(k8sApi, kubernetesNamespace)
343
+
344
+ // If no pod name is specified, automaticalyl target a singleton pod or fail if there are multiple
345
+ if (!targetPodName) {
346
+ if (gameServerPods.length === 1) {
347
+ const metadata = gameServerPods[0].metadata
348
+ if (!metadata?.name) {
349
+ throw new Error('Unable to resolve name for the Kubernetes pod!')
350
+ }
351
+ targetPodName = metadata.name
352
+ } else {
353
+ const podNames = gameServerPods.map(pod => pod.metadata?.name).join(', ')
354
+ console.error(`Multiple game server pods running: ${podNames}\nPlease specify which you want to debug.`)
355
+ exit(1)
356
+ }
357
+ }
358
+
359
+ // Execute 'kubectl debug ..'
360
+ const tmpKubeconfigPath = path.join(os.tmpdir(), `temp-kubeconfig-${+Date.now()}`)
361
+ try {
362
+ // Write kubeconfig to a file
363
+ logger.debug(`Write temporary kubeconfig to ${tmpKubeconfigPath}`)
364
+ await writeFile(tmpKubeconfigPath, kubeconfigPayload)
365
+
366
+ // Run kubctl
367
+ // const args = [`--kubeconfig=${tmpKubeconfigPath}`, `--namespace=${kubernetesNamespace}`, 'get', 'pods']
368
+ const args = [
369
+ `--kubeconfig=${tmpKubeconfigPath}`,
370
+ `--namespace=${kubernetesNamespace}`,
371
+ 'debug',
372
+ targetPodName,
373
+ '-it',
374
+ '--profile=general',
375
+ '--image=metaplay/diagnostics:latest',
376
+ '--target=shard-server'
377
+ ]
378
+ console.log(`Execute: kubectl ${args.join(' ')}`)
379
+ const execResult = await executeCommand('kubectl', args, true)
380
+
381
+ // Remove temporary kubeconfig
382
+ logger.debug('Delete temporary kubeconfig')
383
+ await unlink(tmpKubeconfigPath)
384
+
385
+ // Forward exit code
386
+ if (execResult?.exitCode !== 0) {
387
+ console.error(`kubectl failed failed with exit code ${execResult.exitCode}`)
388
+ exit(execResult.exitCode)
389
+ }
390
+ } catch (err) {
391
+ console.error(`Failed to execute 'kubectl': ${err}. You need to have kubectl installed to debug a game server with metaplay-auth.`)
392
+
393
+ // Remove temporary kubeconfig
394
+ logger.debug('Delete temporary kubeconfig')
395
+ await unlink(tmpKubeconfigPath)
396
+ }
397
+ }
package/src/stackapi.ts CHANGED
@@ -1,87 +1,11 @@
1
- import { isValidFQDN, getGameserverAdminUrl } from './utils.js'
2
1
  import { logger } from './logging.js'
3
- import { dump } from 'js-yaml'
4
- import { getUserinfo } from './auth.js'
5
- import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr'
2
+ import { type EnvironmentDetails } from './targetenvironment.js'
6
3
 
7
- interface AwsCredentialsResponse {
8
- AccessKeyId: string
9
- SecretAccessKey: string
10
- SessionToken: string
11
- Expiration: string
12
- }
13
-
14
- export interface GameserverId {
15
- gameserver?: string
16
- organization?: string
17
- project?: string
18
- environment?: string
19
- }
20
-
21
- interface KubeConfig {
22
- apiVersion?: string
23
- kind: string
24
- 'current-context': string
25
- clusters: KubeConfigCluster[]
26
- contexts: KubeConfigContext[]
27
- users: KubeConfigUser[]
28
- preferences?: any
29
- }
30
-
31
- interface KubeConfigCluster {
32
- cluster: {
33
- 'certificate-authority-data': string
34
- server: string
35
- }
36
- name: string
37
- }
38
-
39
- interface KubeConfigContext {
40
- context: {
41
- cluster: string
42
- user: string
43
- namespace?: string
44
- }
45
- name: string
46
- }
47
-
48
- interface KubeConfigUser {
49
- name: string
50
- user: {
51
- token?: string
52
- exec?: {
53
- command: string
54
- args: string[]
55
- apiVersion: string
56
- }
57
- }
58
- }
59
-
60
- interface KubeExecCredential {
61
- apiVersion: string
62
- kind: string
63
- spec: {
64
- cluster?: {
65
- server: string
66
- certificateAuthorityData: string
67
- }
68
- }
69
- status: {
70
- token: string
71
- expirationTimestamp: string
72
- }
73
- }
74
-
75
- export interface DockerCredentials {
76
- username: string
77
- password: string
78
- registryUrl: string
79
- }
4
+ export const defaultStackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
80
5
 
81
6
  export class StackAPI {
82
7
  private readonly accessToken: string
83
-
84
- private readonly _stackApiBaseUrl: string
8
+ private readonly stackApiBaseUrl: string
85
9
 
86
10
  constructor (accessToken: string, stackApiBaseUrl: string | undefined) {
87
11
  if (accessToken == null) {
@@ -91,250 +15,15 @@ export class StackAPI {
91
15
  this.accessToken = accessToken
92
16
 
93
17
  if (stackApiBaseUrl) {
94
- this._stackApiBaseUrl = stackApiBaseUrl.replace(/\/$/, '') // Remove trailing slash
18
+ this.stackApiBaseUrl = stackApiBaseUrl.replace(/\/$/, '') // Remove trailing slash
95
19
  } else {
96
- this._stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
20
+ this.stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
97
21
  }
98
22
  }
99
23
 
100
- async getAwsCredentials (gs: GameserverId): Promise<AwsCredentialsResponse> {
101
- let url = ''
102
- if (gs.gameserver != null) {
103
- if (isValidFQDN(gs.gameserver)) {
104
- const adminUrl = getGameserverAdminUrl(gs.gameserver)
105
- url = `https://${adminUrl}/.infra/credentials/aws`
106
- } else {
107
- url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/aws`
108
- }
109
- } else if (gs.organization != null && gs.project != null && gs.environment != null) {
110
- url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/aws`
111
- } else {
112
- throw new Error('Invalid arguments for getAwsCredentials')
113
- }
114
-
115
- logger.debug(`Getting AWS credentials from ${url}...`)
116
- const response = await fetch(url, {
117
- method: 'POST',
118
- headers: {
119
- Authorization: `Bearer ${this.accessToken}`,
120
- 'Content-Type': 'application/json'
121
- }
122
- })
123
-
124
- if (response.status !== 200) {
125
- throw new Error(`Failed to fetch AWS credentials: ${response.statusText}, response code=${response.status}`)
126
- }
127
-
128
- return await response.json()
129
- }
130
-
131
- async getKubeConfig (gs: GameserverId): Promise<string> {
132
- let url = ''
133
- if (gs.gameserver != null) {
134
- if (isValidFQDN(gs.gameserver)) {
135
- const adminUrl = getGameserverAdminUrl(gs.gameserver)
136
- url = `https://${adminUrl}/.infra/credentials/k8s`
137
- } else {
138
- url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/k8s`
139
- }
140
- } else if (gs.organization != null && gs.project != null && gs.environment != null) {
141
- url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s`
142
- } else {
143
- throw new Error('Invalid arguments for getKubeConfig')
144
- }
145
-
146
- logger.debug(`Getting KubeConfig from ${url}...`)
147
-
148
- let response
149
- try {
150
- response = await fetch(url, {
151
- method: 'POST',
152
- headers: {
153
- Authorization: `Bearer ${this.accessToken}`,
154
- 'Content-Type': 'application/json'
155
- }
156
- })
157
- } catch (error: any) {
158
- logger.error(`Failed to fetch kubeconfig from ${url}`)
159
- logger.error('Fetch error details:', error)
160
- if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
161
- throw new Error(`Failed to to fetch kubeconfig: SSL certificate validation failed for ${url}. Is someone trying to tamper with your internet connection?`)
162
- }
163
- throw new Error(`Failed to fetch kubeconfig from ${url}: ${error}`)
164
- }
165
-
166
- if (response.status !== 200) {
167
- throw new Error(`Failed to fetch KubeConfigs: ${response.statusText}`)
168
- }
169
-
170
- return await response.text()
171
- }
172
-
173
- /**
174
- * Get a `kubeconfig` payload which invokes `metaplay-auth get-kubernetes-execcredential` to get the actual
175
- * access credentials each time the kubeconfig is used.
176
- * @param gs Game server environment to get credentials for.
177
- * @returns The kubeconfig YAML.
178
- */
179
- async getKubeConfigExecCredential (gs: GameserverId): Promise<string> {
180
- let url = ''
181
- let gsSlug = ''
182
- const execArgs = ['get-kubernetes-execcredential']
183
- if (gs.gameserver != null) {
184
- const adminUrl = getGameserverAdminUrl(gs.gameserver)
185
- url = `https://${adminUrl}/.infra/credentials/k8s?type=execcredential`
186
- gsSlug = gs.gameserver
187
- execArgs.push('--gameserver', gs.gameserver)
188
- } else if (gs.organization != null && gs.project != null && gs.environment != null) {
189
- url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s?type=execcredential`
190
- gsSlug = `${gs.organization}.${gs.project}.${gs.environment}`
191
- execArgs.push(`${gs.organization}-${gs.project}-${gs.environment}`)
192
- } else {
193
- throw new Error('Invalid arguments for getKubeConfigExecCredential')
194
- }
195
-
196
- logger.debug(`Getting Kubernetes KubeConfig from ${url}...`)
197
-
198
- let response
199
- try {
200
- response = await fetch(url, {
201
- method: 'POST',
202
- headers: {
203
- Authorization: `Bearer ${this.accessToken}`,
204
- 'Content-Type': 'application/json'
205
- }
206
- })
207
- } catch (error: any) {
208
- logger.error(`Failed to fetch kubeconfig from ${url}`)
209
- logger.error('Fetch error details:', error)
210
- if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
211
- throw new Error(`Failed to to fetch kubeconfig: SSL certificate validation failed for ${url}. Is someone trying to tamper with your internet connection?`)
212
- }
213
- throw new Error(`Failed to fetch kubeconfig from ${url}: ${error}`)
214
- }
215
-
216
- if (response.status !== 200) {
217
- throw new Error(`Failed to fetch Kubernetes KubeConfig: ${response.statusText}`)
218
- }
219
-
220
- // get the execcredential and morph it into a kubeconfig which calls metaplay-auth for the token
221
- let kubeExecCredential
222
- try {
223
- kubeExecCredential = await response.json() as KubeExecCredential
224
- } catch {
225
- throw new Error('Failed to fetch Kubernetes KubeConfig: the server response is not JSON')
226
- }
227
-
228
- if (!kubeExecCredential.spec.cluster) {
229
- throw new Error('Received kubeExecCredential with missing spec.cluster')
230
- }
231
-
232
- let namespace = 'default'
233
- let user = 'user'
234
-
235
- try {
236
- const environment = await this.getEnvironmentDetails(gs)
237
- namespace = environment.deployment?.kubernetes_namespace ?? namespace
238
- } catch (e) {
239
- logger.debug('Failed to get environment details, using defaults', e)
240
- }
241
-
242
- try {
243
- const userinfo = await getUserinfo(this.accessToken)
244
- user = userinfo.sub ?? user
245
- } catch (e) {
246
- logger.debug('Failed to get userinfo, using defaults', e)
247
- }
248
-
249
- const kubeConfig: KubeConfig = {
250
- apiVersion: 'v1',
251
- kind: 'Config',
252
- 'current-context': gsSlug,
253
- clusters: [
254
- {
255
- cluster: {
256
- 'certificate-authority-data': kubeExecCredential.spec.cluster.certificateAuthorityData,
257
- server: kubeExecCredential.spec.cluster.server
258
- },
259
- name: kubeExecCredential.spec.cluster.server
260
- }
261
- ],
262
- contexts: [
263
- {
264
- context: {
265
- cluster: kubeExecCredential.spec.cluster.server,
266
- namespace,
267
- user
268
- },
269
- name: gsSlug,
270
- }
271
- ],
272
- users: [
273
- {
274
- name: user,
275
- user: {
276
- exec: {
277
- apiVersion: 'client.authentication.k8s.io/v1beta1',
278
- command: 'metaplay-auth', // todo: figure out how to refer to metaplay-auth itself
279
- args: execArgs,
280
- }
281
- }
282
- }
283
- ],
284
- }
285
-
286
- // return as yaml for easier consumption
287
- return dump(kubeConfig)
288
- }
289
-
290
- async getKubeExecCredential (gs: GameserverId): Promise<string> {
291
- let url = ''
292
- if (gs.gameserver != null) {
293
- if (isValidFQDN(gs.gameserver)) {
294
- const adminUrl = getGameserverAdminUrl(gs.gameserver)
295
- url = `https://${adminUrl}/.infra/credentials/k8s?type=execcredential`
296
- } else {
297
- url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/k8s?type=execcredential`
298
- }
299
- } else if (gs.organization != null && gs.project != null && gs.environment != null) {
300
- url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s?type=execcredential`
301
- } else {
302
- throw new Error('Invalid arguments for getKubeConfig')
303
- }
304
-
305
- logger.debug(`Getting Kubernetes ExecCredential from ${url}...`)
306
-
307
- const response = await fetch(url, {
308
- method: 'POST',
309
- headers: {
310
- Authorization: `Bearer ${this.accessToken}`,
311
- 'Content-Type': 'application/json'
312
- }
313
- })
314
-
315
- if (response.status !== 200) {
316
- throw new Error(`Failed to fetch Kubernetes ExecCredential: ${response.statusText}`)
317
- }
318
-
319
- return await response.text()
320
- }
321
-
322
- async getEnvironmentDetails (gs: GameserverId): Promise<any> {
323
- let url = ''
324
- if (gs.gameserver != null) {
325
- if (isValidFQDN(gs.gameserver)) {
326
- const adminUrl = getGameserverAdminUrl(gs.gameserver)
327
- url = `https://${adminUrl}/.infra/environment`
328
- } else {
329
- url = `${this._stackApiBaseUrl}/v0/deployments/${gs.gameserver}`
330
- }
331
- } else if (gs.organization != null && gs.project != null && gs.environment != null) {
332
- url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}`
333
- } else {
334
- throw new Error('Invalid arguments for environment details')
335
- }
336
-
337
- logger.debug(`Getting environment details from ${url}...`)
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}...`)
338
27
  const response = await fetch(url, {
339
28
  method: 'GET',
340
29
  headers: {
@@ -343,53 +32,19 @@ export class StackAPI {
343
32
  }
344
33
  })
345
34
 
346
- if (response.status !== 200) {
347
- throw new Error(`Failed to fetch environment details: ${response.statusText}`)
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)}`)
348
39
  }
349
40
 
350
- return await response.json()
351
- }
352
-
353
- async getDockerCredentials (gameserverId: GameserverId): Promise<DockerCredentials> {
354
- // Get environment info (for AWS region)
355
- logger.debug('Get environment info')
356
- const envInfo = await this.getEnvironmentDetails(gameserverId)
357
-
358
- // Fetch AWS credentials from Metaplay cloud
359
- logger.debug('Get AWS credentials from Metaplay')
360
- const awsCredentials = await this.getAwsCredentials(gameserverId)
361
-
362
- // Create ECR client with credentials
363
- logger.debug('Create ECR client')
364
- const client = new ECRClient({
365
- credentials: {
366
- accessKeyId: awsCredentials.AccessKeyId,
367
- secretAccessKey: awsCredentials.SecretAccessKey,
368
- sessionToken: awsCredentials.SessionToken
369
- },
370
- region: envInfo.deployment.aws_region
371
- })
372
-
373
- // Fetch the ECR docker authentication token
374
- logger.debug('Fetch ECR login credentials from AWS')
375
- const command = new GetAuthorizationTokenCommand({})
376
- const response = await client.send(command)
377
- if (!response.authorizationData || response.authorizationData.length === 0 || !response.authorizationData[0].authorizationToken || !response.authorizationData[0].proxyEndpoint) {
378
- throw new Error('Received an empty authorization token response for ECR repository')
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)}`)
379
46
  }
380
47
 
381
- // Parse username and password from the response (separated by a ':')
382
- logger.debug('Parse ECR response')
383
- const registryUrl = response.authorizationData[0].proxyEndpoint
384
- const authorization64 = response.authorizationData[0].authorizationToken
385
- const authorization = Buffer.from(authorization64, 'base64').toString()
386
- const [username, password] = authorization.split(':')
387
- logger.debug(`ECR: username=${username}, proxyEndpoint=${registryUrl}`)
388
-
389
- return {
390
- username,
391
- password,
392
- registryUrl
393
- } satisfies DockerCredentials
48
+ return envDetails.deployment.server_hostname
394
49
  }
395
50
  }