@metaplay/metaplay-auth 1.4.2 → 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 +69 -28
- package/dist/index.cjs +292 -0
- package/dist/sshcrypto-OMBCGRSN.node +0 -0
- package/eslint.config.js +3 -0
- package/index.ts +763 -437
- package/package.json +22 -21
- package/prettier.config.js +3 -0
- package/src/auth.ts +121 -80
- package/src/buildCommand.ts +267 -0
- package/src/config.ts +12 -0
- package/src/deployment.ts +285 -52
- package/src/logging.ts +4 -4
- package/src/secret_store.ts +10 -7
- package/src/stackapi.ts +5 -381
- package/src/targetenvironment.ts +311 -0
- package/src/utils.ts +162 -31
- 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/stackapi.ts
CHANGED
|
@@ -1,89 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
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'
|
|
6
|
-
|
|
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
|
-
}
|
|
80
|
-
|
|
1
|
+
// \note Unused right now but will likely need this back later
|
|
81
2
|
export class StackAPI {
|
|
82
3
|
private readonly accessToken: string
|
|
4
|
+
private readonly stackApiBaseUrl: string
|
|
83
5
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
constructor (accessToken: string, stackApiBaseUrl: string | undefined) {
|
|
6
|
+
constructor(accessToken: string, stackApiBaseUrl: string | undefined) {
|
|
87
7
|
if (accessToken == null) {
|
|
88
8
|
throw new Error('accessToken must be provided')
|
|
89
9
|
}
|
|
@@ -91,305 +11,9 @@ export class StackAPI {
|
|
|
91
11
|
this.accessToken = accessToken
|
|
92
12
|
|
|
93
13
|
if (stackApiBaseUrl) {
|
|
94
|
-
this.
|
|
95
|
-
} else {
|
|
96
|
-
this._stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
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`
|
|
14
|
+
this.stackApiBaseUrl = stackApiBaseUrl.replace(/\/$/, '') // Remove trailing slash
|
|
142
15
|
} else {
|
|
143
|
-
|
|
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}`)
|
|
16
|
+
this.stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
|
|
164
17
|
}
|
|
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}...`)
|
|
338
|
-
const response = await fetch(url, {
|
|
339
|
-
method: 'GET',
|
|
340
|
-
headers: {
|
|
341
|
-
Authorization: `Bearer ${this.accessToken}`,
|
|
342
|
-
'Content-Type': 'application/json'
|
|
343
|
-
}
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
if (response.status !== 200) {
|
|
347
|
-
throw new Error(`Failed to fetch environment details: ${response.statusText}`)
|
|
348
|
-
}
|
|
349
|
-
|
|
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')
|
|
379
|
-
}
|
|
380
|
-
|
|
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
|
|
394
18
|
}
|
|
395
19
|
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { logger } from './logging.js'
|
|
2
|
+
import { dump } from 'js-yaml'
|
|
3
|
+
import { getUserinfo } from './auth.js'
|
|
4
|
+
import { isRunningUnderNpx } from './utils.js'
|
|
5
|
+
import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr'
|
|
6
|
+
|
|
7
|
+
interface AwsCredentialsResponse {
|
|
8
|
+
AccessKeyId: string
|
|
9
|
+
SecretAccessKey: string
|
|
10
|
+
SessionToken: string
|
|
11
|
+
Expiration: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DeploymentDetails {
|
|
15
|
+
server_hostname: string
|
|
16
|
+
kubernetes_namespace: string
|
|
17
|
+
ecr_repo: string
|
|
18
|
+
aws_region: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EnvironmentDetails {
|
|
22
|
+
deployment: DeploymentDetails
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface KubeConfig {
|
|
26
|
+
apiVersion?: string
|
|
27
|
+
kind: string
|
|
28
|
+
'current-context': string
|
|
29
|
+
clusters: KubeConfigCluster[]
|
|
30
|
+
contexts: KubeConfigContext[]
|
|
31
|
+
users: KubeConfigUser[]
|
|
32
|
+
preferences?: any
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface KubeConfigCluster {
|
|
36
|
+
cluster: {
|
|
37
|
+
'certificate-authority-data': string
|
|
38
|
+
server: string
|
|
39
|
+
}
|
|
40
|
+
name: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface KubeConfigContext {
|
|
44
|
+
context: {
|
|
45
|
+
cluster: string
|
|
46
|
+
user: string
|
|
47
|
+
namespace?: string
|
|
48
|
+
}
|
|
49
|
+
name: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface KubeConfigUser {
|
|
53
|
+
name: string
|
|
54
|
+
user: {
|
|
55
|
+
token?: string
|
|
56
|
+
exec?: {
|
|
57
|
+
command: string
|
|
58
|
+
args: string[]
|
|
59
|
+
apiVersion: string
|
|
60
|
+
interactiveMode: string
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface KubeExecCredential {
|
|
66
|
+
apiVersion: string
|
|
67
|
+
kind: string
|
|
68
|
+
spec: {
|
|
69
|
+
cluster?: {
|
|
70
|
+
server: string
|
|
71
|
+
certificateAuthorityData: string
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
status: {
|
|
75
|
+
token: string
|
|
76
|
+
expirationTimestamp: string
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DockerCredentials {
|
|
81
|
+
username: string
|
|
82
|
+
password: string
|
|
83
|
+
registryUrl: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class TargetEnvironment {
|
|
87
|
+
private readonly accessToken: string
|
|
88
|
+
private readonly humanId: string
|
|
89
|
+
private readonly stackApiBaseUrl: string
|
|
90
|
+
|
|
91
|
+
constructor(accessToken: string, humanId: string, stackApiBaseUrl: string) {
|
|
92
|
+
this.accessToken = accessToken
|
|
93
|
+
this.humanId = humanId
|
|
94
|
+
this.stackApiBaseUrl = stackApiBaseUrl
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async fetchJson<T>(url: string, method: string): Promise<T> {
|
|
98
|
+
const response = await fetch(url, {
|
|
99
|
+
method,
|
|
100
|
+
headers: {
|
|
101
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
102
|
+
'Content-Type': 'application/json',
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if (response.status !== 200) {
|
|
107
|
+
throw new Error(`Failed to fetch ${method} ${url}: ${response.statusText}, response code=${response.status}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (await response.json()) as T
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async fetchText(url: string, method: string): Promise<string> {
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations
|
|
115
|
+
let response
|
|
116
|
+
try {
|
|
117
|
+
response = await fetch(url, {
|
|
118
|
+
method,
|
|
119
|
+
headers: {
|
|
120
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
logger.error(`Failed to fetch ${method} ${url}:`, error)
|
|
126
|
+
if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Failed to to fetch ${url}: SSL certificate validation failed. Is someone trying to tamper with your internet connection?`
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Failed to fetch ${url}: ${error}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (response.status !== 200) {
|
|
135
|
+
throw new Error(`Failed to fetch ${method} ${url}: ${response.statusText}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return await response.text()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get AWS credentials for the target environment from Metaplay cloud.
|
|
143
|
+
* @returns The AWS credentials.
|
|
144
|
+
*/
|
|
145
|
+
async getAwsCredentials(): Promise<AwsCredentialsResponse> {
|
|
146
|
+
const url = `${this.stackApiBaseUrl}/v0/credentials/${this.humanId}/aws`
|
|
147
|
+
logger.debug(`Fetching AWS credentials from ${url}...`)
|
|
148
|
+
return await this.fetchJson<AwsCredentialsResponse>(url, 'POST')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get a short-lived kubeconfig with the access credentials embedded in the kubeconfig file.
|
|
153
|
+
* @returns The kubeconfig YAML.
|
|
154
|
+
*/
|
|
155
|
+
async getKubeConfigWithEmbeddedCredentials(): Promise<string> {
|
|
156
|
+
const url = `${this.stackApiBaseUrl}/v0/credentials/${this.humanId}/k8s`
|
|
157
|
+
logger.debug(`Getting KubeConfig from ${url}...`)
|
|
158
|
+
return await this.fetchText(url, 'POST')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get a `kubeconfig` payload which invokes `metaplay-auth get-kubernetes-execcredential` to get the actual
|
|
163
|
+
* access credentials each time the kubeconfig is used.
|
|
164
|
+
* @returns The kubeconfig YAML.
|
|
165
|
+
*/
|
|
166
|
+
async getKubeConfigWithExecCredential(): Promise<string> {
|
|
167
|
+
const url = `${this.stackApiBaseUrl}/v0/credentials/${this.humanId}/k8s?type=execcredential`
|
|
168
|
+
logger.debug(`Getting Kubernetes KubeConfig from ${url}...`)
|
|
169
|
+
|
|
170
|
+
// get the execcredential and morph it into a kubeconfig which calls metaplay-auth for the token
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations
|
|
172
|
+
let kubeExecCredential: KubeExecCredential
|
|
173
|
+
try {
|
|
174
|
+
kubeExecCredential = await this.fetchJson<KubeExecCredential>(url, 'POST')
|
|
175
|
+
} catch {
|
|
176
|
+
throw new Error(`Failed to fetch Kubernetes KubeConfig from ${url}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!kubeExecCredential.spec.cluster) {
|
|
180
|
+
throw new Error('Received kubeExecCredential with missing spec.cluster')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Fetch environment namespace
|
|
184
|
+
const environment = await this.getEnvironmentDetails()
|
|
185
|
+
const namespace = environment.deployment?.kubernetes_namespace
|
|
186
|
+
if (!namespace) {
|
|
187
|
+
throw new Error('Environment details did not contain a valid Kubernetes namespace')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Fetch user id
|
|
191
|
+
const userinfo = await getUserinfo(this.accessToken)
|
|
192
|
+
const userId = userinfo.email
|
|
193
|
+
|
|
194
|
+
// Resolve invocation for getting the kubeconfig exec credential
|
|
195
|
+
let execCmd = 'metaplay-auth'
|
|
196
|
+
let execArgs = ['get-kubernetes-execcredential']
|
|
197
|
+
.concat(['--stack-api', this.stackApiBaseUrl]) // include URL to avoid lookups from portal
|
|
198
|
+
.concat([this.humanId]) // use the immutable humanId to tolerate slug renaming
|
|
199
|
+
|
|
200
|
+
// If running under npx, use npx also for getting the kube exec credential
|
|
201
|
+
if (isRunningUnderNpx()) {
|
|
202
|
+
execCmd = 'npx'
|
|
203
|
+
execArgs = [
|
|
204
|
+
'--yes', // For NPX to silently install or update
|
|
205
|
+
'@metaplay/metaplay-auth@latest',
|
|
206
|
+
...execArgs,
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const kubeConfig: KubeConfig = {
|
|
211
|
+
apiVersion: 'v1',
|
|
212
|
+
kind: 'Config',
|
|
213
|
+
'current-context': this.humanId, // use immutable humanId
|
|
214
|
+
clusters: [
|
|
215
|
+
{
|
|
216
|
+
cluster: {
|
|
217
|
+
'certificate-authority-data': kubeExecCredential.spec.cluster.certificateAuthorityData,
|
|
218
|
+
server: kubeExecCredential.spec.cluster.server,
|
|
219
|
+
},
|
|
220
|
+
name: kubeExecCredential.spec.cluster.server,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
contexts: [
|
|
224
|
+
{
|
|
225
|
+
context: {
|
|
226
|
+
cluster: kubeExecCredential.spec.cluster.server,
|
|
227
|
+
namespace,
|
|
228
|
+
user: userId,
|
|
229
|
+
},
|
|
230
|
+
name: this.humanId, // use immutable humanId
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
users: [
|
|
234
|
+
{
|
|
235
|
+
name: userId,
|
|
236
|
+
user: {
|
|
237
|
+
exec: {
|
|
238
|
+
apiVersion: 'client.authentication.k8s.io/v1beta1',
|
|
239
|
+
command: execCmd,
|
|
240
|
+
args: execArgs,
|
|
241
|
+
interactiveMode: 'Never',
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// return as yaml for easier consumption
|
|
249
|
+
return dump(kubeConfig)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async getKubeExecCredential(): Promise<string> {
|
|
253
|
+
const url = `${this.stackApiBaseUrl}/v0/credentials/${this.humanId}/k8s?type=execcredential`
|
|
254
|
+
logger.debug(`Getting Kubernetes ExecCredential from ${url}...`)
|
|
255
|
+
return await this.fetchText(url, 'POST')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async getEnvironmentDetails(): Promise<EnvironmentDetails> {
|
|
259
|
+
const url = `${this.stackApiBaseUrl}/v0/deployments/${this.humanId}`
|
|
260
|
+
logger.debug(`Getting environment details from ${url}...`)
|
|
261
|
+
return await this.fetchJson<EnvironmentDetails>(url, 'GET')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async getDockerCredentials(): Promise<DockerCredentials> {
|
|
265
|
+
// Get environment info (for AWS region)
|
|
266
|
+
logger.debug('Get environment info')
|
|
267
|
+
const envInfo = await this.getEnvironmentDetails()
|
|
268
|
+
|
|
269
|
+
// Fetch AWS credentials from Metaplay cloud
|
|
270
|
+
logger.debug('Get AWS credentials from Metaplay')
|
|
271
|
+
const awsCredentials = await this.getAwsCredentials()
|
|
272
|
+
|
|
273
|
+
// Create ECR client with credentials
|
|
274
|
+
logger.debug('Create ECR client')
|
|
275
|
+
const client = new ECRClient({
|
|
276
|
+
credentials: {
|
|
277
|
+
accessKeyId: awsCredentials.AccessKeyId,
|
|
278
|
+
secretAccessKey: awsCredentials.SecretAccessKey,
|
|
279
|
+
sessionToken: awsCredentials.SessionToken,
|
|
280
|
+
},
|
|
281
|
+
region: envInfo.deployment.aws_region,
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// Fetch the ECR docker authentication token
|
|
285
|
+
logger.debug('Fetch ECR login credentials from AWS')
|
|
286
|
+
const command = new GetAuthorizationTokenCommand({})
|
|
287
|
+
const response = await client.send(command)
|
|
288
|
+
if (
|
|
289
|
+
!response.authorizationData ||
|
|
290
|
+
response.authorizationData.length === 0 ||
|
|
291
|
+
!response.authorizationData[0].authorizationToken ||
|
|
292
|
+
!response.authorizationData[0].proxyEndpoint
|
|
293
|
+
) {
|
|
294
|
+
throw new Error('Received an empty authorization token response for ECR repository')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Parse username and password from the response (separated by a ':')
|
|
298
|
+
logger.debug('Parse ECR response')
|
|
299
|
+
const registryUrl = response.authorizationData[0].proxyEndpoint
|
|
300
|
+
const authorization64 = response.authorizationData[0].authorizationToken
|
|
301
|
+
const authorization = Buffer.from(authorization64, 'base64').toString()
|
|
302
|
+
const [username, password] = authorization.split(':')
|
|
303
|
+
logger.debug(`ECR: username=${username}, proxyEndpoint=${registryUrl}`)
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
username,
|
|
307
|
+
password,
|
|
308
|
+
registryUrl,
|
|
309
|
+
} satisfies DockerCredentials
|
|
310
|
+
}
|
|
311
|
+
}
|