@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/CHANGELOG.md +23 -0
- package/dist/index.cjs +292 -0
- package/dist/sshcrypto-OMBCGRSN.node +0 -0
- package/index.ts +142 -94
- package/package.json +12 -14
- package/src/auth.ts +11 -5
- package/src/deployment.ts +124 -14
- package/src/stackapi.ts +18 -363
- package/src/targetenvironment.ts +307 -0
- package/src/utils.ts +49 -9
- 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,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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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':
|
|
167
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
18
|
+
this.stackApiBaseUrl = stackApiBaseUrl.replace(/\/$/, '') // Remove trailing slash
|
|
95
19
|
} else {
|
|
96
|
-
this.
|
|
20
|
+
this.stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
|
|
97
21
|
}
|
|
98
22
|
}
|
|
99
23
|
|
|
100
|
-
async
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
}
|