@metaplay/metaplay-auth 1.4.1 → 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 +29 -0
- package/dist/index.cjs +292 -0
- package/dist/sshcrypto-OMBCGRSN.node +0 -0
- package/index.ts +208 -156
- package/package.json +12 -14
- package/src/auth.ts +11 -5
- package/src/deployment.ts +124 -14
- package/src/stackapi.ts +19 -314
- package/src/targetenvironment.ts +307 -0
- package/src/utils.ts +49 -9
- package/src/version.ts +1 -0
- package/dist/index.js +0 -638
- 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 -264
- 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,80 +1,11 @@
|
|
|
1
|
-
import { isValidFQDN, getGameserverAdminUrl } from './utils.js'
|
|
2
1
|
import { logger } from './logging.js'
|
|
3
|
-
import {
|
|
4
|
-
import { getUserinfo } from './auth.js'
|
|
2
|
+
import { type EnvironmentDetails } from './targetenvironment.js'
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
AccessKeyId: string
|
|
8
|
-
SecretAccessKey: string
|
|
9
|
-
SessionToken: string
|
|
10
|
-
Expiration: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface GameserverId {
|
|
14
|
-
gameserver?: string
|
|
15
|
-
organization?: string
|
|
16
|
-
project?: string
|
|
17
|
-
environment?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface KubeConfig {
|
|
21
|
-
apiVersion?: string
|
|
22
|
-
kind: string
|
|
23
|
-
'current-context': string
|
|
24
|
-
clusters: KubeConfigCluster[]
|
|
25
|
-
contexts: KubeConfigContext[]
|
|
26
|
-
users: KubeConfigUser[]
|
|
27
|
-
preferences?: any
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface KubeConfigCluster {
|
|
31
|
-
cluster: {
|
|
32
|
-
'certificate-authority-data': string
|
|
33
|
-
server: string
|
|
34
|
-
}
|
|
35
|
-
name: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface KubeConfigContext {
|
|
39
|
-
context: {
|
|
40
|
-
cluster: string
|
|
41
|
-
user: string
|
|
42
|
-
namespace?: string
|
|
43
|
-
}
|
|
44
|
-
name: string
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface KubeConfigUser {
|
|
48
|
-
name: string
|
|
49
|
-
user: {
|
|
50
|
-
token?: string
|
|
51
|
-
exec?: {
|
|
52
|
-
command: string
|
|
53
|
-
args: string[]
|
|
54
|
-
apiVersion: string
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface KubeExecCredential {
|
|
60
|
-
apiVersion: string
|
|
61
|
-
kind: string
|
|
62
|
-
spec: {
|
|
63
|
-
cluster?: {
|
|
64
|
-
server: string
|
|
65
|
-
certificateAuthorityData: string
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
status: {
|
|
69
|
-
token: string
|
|
70
|
-
expirationTimestamp: string
|
|
71
|
-
}
|
|
72
|
-
}
|
|
4
|
+
export const defaultStackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
|
|
73
5
|
|
|
74
6
|
export class StackAPI {
|
|
75
7
|
private readonly accessToken: string
|
|
76
|
-
|
|
77
|
-
private readonly _stackApiBaseUrl: string
|
|
8
|
+
private readonly stackApiBaseUrl: string
|
|
78
9
|
|
|
79
10
|
constructor (accessToken: string, stackApiBaseUrl: string | undefined) {
|
|
80
11
|
if (accessToken == null) {
|
|
@@ -84,262 +15,36 @@ export class StackAPI {
|
|
|
84
15
|
this.accessToken = accessToken
|
|
85
16
|
|
|
86
17
|
if (stackApiBaseUrl) {
|
|
87
|
-
this.
|
|
88
|
-
} else {
|
|
89
|
-
this._stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async getAwsCredentials (gs: GameserverId): Promise<AwsCredentialsResponse> {
|
|
94
|
-
let url = ''
|
|
95
|
-
if (gs.gameserver != null) {
|
|
96
|
-
if (isValidFQDN(gs.gameserver)) {
|
|
97
|
-
const adminUrl = getGameserverAdminUrl(gs.gameserver)
|
|
98
|
-
url = `https://${adminUrl}/.infra/credentials/aws`
|
|
99
|
-
} else {
|
|
100
|
-
url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/aws`
|
|
101
|
-
}
|
|
102
|
-
} else if (gs.organization != null && gs.project != null && gs.environment != null) {
|
|
103
|
-
url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/aws`
|
|
104
|
-
} else {
|
|
105
|
-
throw new Error('Invalid arguments for getAwsCredentials')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
logger.debug(`Getting AWS credentials from ${url}...`)
|
|
109
|
-
const response = await fetch(url, {
|
|
110
|
-
method: 'POST',
|
|
111
|
-
headers: {
|
|
112
|
-
Authorization: `Bearer ${this.accessToken}`,
|
|
113
|
-
'Content-Type': 'application/json'
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
if (response.status !== 200) {
|
|
118
|
-
throw new Error(`Failed to fetch AWS credentials: ${response.statusText}, response code=${response.status}`)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return await response.json()
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async getKubeConfig (gs: GameserverId): Promise<string> {
|
|
125
|
-
let url = ''
|
|
126
|
-
if (gs.gameserver != null) {
|
|
127
|
-
if (isValidFQDN(gs.gameserver)) {
|
|
128
|
-
const adminUrl = getGameserverAdminUrl(gs.gameserver)
|
|
129
|
-
url = `https://${adminUrl}/.infra/credentials/k8s`
|
|
130
|
-
} else {
|
|
131
|
-
url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/k8s`
|
|
132
|
-
}
|
|
133
|
-
} else if (gs.organization != null && gs.project != null && gs.environment != null) {
|
|
134
|
-
url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s`
|
|
135
|
-
} else {
|
|
136
|
-
throw new Error('Invalid arguments for getKubeConfig')
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
logger.debug(`Getting KubeConfig from ${url}...`)
|
|
140
|
-
|
|
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?`)
|
|
155
|
-
}
|
|
156
|
-
throw new Error(`Failed to fetch kubeconfig from ${url}: ${error}`)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (response.status !== 200) {
|
|
160
|
-
throw new Error(`Failed to fetch KubeConfigs: ${response.statusText}`)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return await response.text()
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Get a `kubeconfig` payload which invokes `metaplay-auth get-kubernetes-execcredential` to get the actual
|
|
168
|
-
* access credentials each time the kubeconfig is used.
|
|
169
|
-
* @param gs Game server environment to get credentials for.
|
|
170
|
-
* @returns The kubeconfig YAML.
|
|
171
|
-
*/
|
|
172
|
-
async getKubeConfigExecCredential (gs: GameserverId): Promise<string> {
|
|
173
|
-
let url = ''
|
|
174
|
-
let gsSlug = ''
|
|
175
|
-
const execArgs = ['get-kubernetes-execcredential']
|
|
176
|
-
if (gs.gameserver != null) {
|
|
177
|
-
const adminUrl = getGameserverAdminUrl(gs.gameserver)
|
|
178
|
-
url = `https://${adminUrl}/.infra/credentials/k8s?type=execcredential`
|
|
179
|
-
gsSlug = gs.gameserver
|
|
180
|
-
execArgs.push('--gameserver', gs.gameserver)
|
|
181
|
-
} else if (gs.organization != null && gs.project != null && gs.environment != null) {
|
|
182
|
-
url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s?type=execcredential`
|
|
183
|
-
gsSlug = `${gs.organization}.${gs.project}.${gs.environment}`
|
|
184
|
-
execArgs.push(`${gs.organization}-${gs.project}-${gs.environment}`)
|
|
18
|
+
this.stackApiBaseUrl = stackApiBaseUrl.replace(/\/$/, '') // Remove trailing slash
|
|
185
19
|
} else {
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
logger.debug(`Getting Kubernetes KubeConfig from ${url}...`)
|
|
190
|
-
|
|
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?`)
|
|
205
|
-
}
|
|
206
|
-
throw new Error(`Failed to fetch kubeconfig from ${url}: ${error}`)
|
|
20
|
+
this.stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
|
|
207
21
|
}
|
|
208
|
-
|
|
209
|
-
if (response.status !== 200) {
|
|
210
|
-
throw new Error(`Failed to fetch Kubernetes KubeConfig: ${response.statusText}`)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// get the execcredential and morph it into a kubeconfig which calls metaplay-auth for the token
|
|
214
|
-
let kubeExecCredential
|
|
215
|
-
try {
|
|
216
|
-
kubeExecCredential = await response.json() as KubeExecCredential
|
|
217
|
-
} catch {
|
|
218
|
-
throw new Error('Failed to fetch Kubernetes KubeConfig: the server response is not JSON')
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (!kubeExecCredential.spec.cluster) {
|
|
222
|
-
throw new Error('Received kubeExecCredential with missing spec.cluster')
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
let namespace = 'default'
|
|
226
|
-
let user = 'user'
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
const environment = await this.getEnvironmentDetails(gs)
|
|
230
|
-
namespace = environment.deployment?.kubernetes_namespace ?? namespace
|
|
231
|
-
} catch (e) {
|
|
232
|
-
logger.debug('Failed to get environment details, using defaults', e)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const userinfo = await getUserinfo(this.accessToken)
|
|
237
|
-
user = userinfo.sub ?? user
|
|
238
|
-
} catch (e) {
|
|
239
|
-
logger.debug('Failed to get userinfo, using defaults', e)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const kubeConfig: KubeConfig = {
|
|
243
|
-
apiVersion: 'v1',
|
|
244
|
-
kind: 'Config',
|
|
245
|
-
'current-context': gsSlug,
|
|
246
|
-
clusters: [
|
|
247
|
-
{
|
|
248
|
-
cluster: {
|
|
249
|
-
'certificate-authority-data': kubeExecCredential.spec.cluster.certificateAuthorityData,
|
|
250
|
-
server: kubeExecCredential.spec.cluster.server
|
|
251
|
-
},
|
|
252
|
-
name: kubeExecCredential.spec.cluster.server
|
|
253
|
-
}
|
|
254
|
-
],
|
|
255
|
-
contexts: [
|
|
256
|
-
{
|
|
257
|
-
context: {
|
|
258
|
-
cluster: kubeExecCredential.spec.cluster.server,
|
|
259
|
-
namespace,
|
|
260
|
-
user
|
|
261
|
-
},
|
|
262
|
-
name: gsSlug,
|
|
263
|
-
}
|
|
264
|
-
],
|
|
265
|
-
users: [
|
|
266
|
-
{
|
|
267
|
-
name: user,
|
|
268
|
-
user: {
|
|
269
|
-
exec: {
|
|
270
|
-
apiVersion: 'client.authentication.k8s.io/v1beta1',
|
|
271
|
-
command: 'metaplay-auth', // todo: figure out how to refer to metaplay-auth itself
|
|
272
|
-
args: execArgs,
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
],
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// return as yaml for easier consumption
|
|
280
|
-
return dump(kubeConfig)
|
|
281
22
|
}
|
|
282
23
|
|
|
283
|
-
async
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (isValidFQDN(gs.gameserver)) {
|
|
287
|
-
const adminUrl = getGameserverAdminUrl(gs.gameserver)
|
|
288
|
-
url = `https://${adminUrl}/.infra/credentials/k8s?type=execcredential`
|
|
289
|
-
} else {
|
|
290
|
-
url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/k8s?type=execcredential`
|
|
291
|
-
}
|
|
292
|
-
} else if (gs.organization != null && gs.project != null && gs.environment != null) {
|
|
293
|
-
url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s?type=execcredential`
|
|
294
|
-
} else {
|
|
295
|
-
throw new Error('Invalid arguments for getKubeConfig')
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
logger.debug(`Getting Kubernetes ExecCredential from ${url}...`)
|
|
299
|
-
|
|
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}...`)
|
|
300
27
|
const response = await fetch(url, {
|
|
301
|
-
method: '
|
|
28
|
+
method: 'GET',
|
|
302
29
|
headers: {
|
|
303
30
|
Authorization: `Bearer ${this.accessToken}`,
|
|
304
31
|
'Content-Type': 'application/json'
|
|
305
32
|
}
|
|
306
33
|
})
|
|
307
34
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return await response.text()
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async getEnvironmentDetails (gs: GameserverId): Promise<any> {
|
|
316
|
-
let url = ''
|
|
317
|
-
if (gs.gameserver != null) {
|
|
318
|
-
if (isValidFQDN(gs.gameserver)) {
|
|
319
|
-
const adminUrl = getGameserverAdminUrl(gs.gameserver)
|
|
320
|
-
url = `https://${adminUrl}/.infra/environment`
|
|
321
|
-
} else {
|
|
322
|
-
url = `${this._stackApiBaseUrl}/v0/deployments/${gs.gameserver}`
|
|
323
|
-
}
|
|
324
|
-
} else if (gs.organization != null && gs.project != null && gs.environment != null) {
|
|
325
|
-
url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}`
|
|
326
|
-
} else {
|
|
327
|
-
throw new Error('Invalid arguments for environment details')
|
|
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)}`)
|
|
328
39
|
}
|
|
329
40
|
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
'Content-Type': 'application/json'
|
|
336
|
-
}
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
if (response.status !== 200) {
|
|
340
|
-
throw new Error(`Failed to fetch environment details: ${response.statusText}`)
|
|
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)}`)
|
|
341
46
|
}
|
|
342
47
|
|
|
343
|
-
return
|
|
48
|
+
return envDetails.deployment.server_hostname
|
|
344
49
|
}
|
|
345
50
|
}
|
|
@@ -0,0 +1,307 @@
|
|
|
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
|
+
import { defaultStackApiBaseUrl } from './stackapi.js'
|
|
7
|
+
|
|
8
|
+
interface AwsCredentialsResponse {
|
|
9
|
+
AccessKeyId: string
|
|
10
|
+
SecretAccessKey: string
|
|
11
|
+
SessionToken: string
|
|
12
|
+
Expiration: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DeploymentDetails {
|
|
16
|
+
server_hostname: string
|
|
17
|
+
kubernetes_namespace: string
|
|
18
|
+
ecr_repo: string
|
|
19
|
+
aws_region: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EnvironmentDetails {
|
|
23
|
+
deployment: DeploymentDetails
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface KubeConfig {
|
|
27
|
+
apiVersion?: string
|
|
28
|
+
kind: string
|
|
29
|
+
'current-context': string
|
|
30
|
+
clusters: KubeConfigCluster[]
|
|
31
|
+
contexts: KubeConfigContext[]
|
|
32
|
+
users: KubeConfigUser[]
|
|
33
|
+
preferences?: any
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface KubeConfigCluster {
|
|
37
|
+
cluster: {
|
|
38
|
+
'certificate-authority-data': string
|
|
39
|
+
server: string
|
|
40
|
+
}
|
|
41
|
+
name: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface KubeConfigContext {
|
|
45
|
+
context: {
|
|
46
|
+
cluster: string
|
|
47
|
+
user: string
|
|
48
|
+
namespace?: string
|
|
49
|
+
}
|
|
50
|
+
name: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface KubeConfigUser {
|
|
54
|
+
name: string
|
|
55
|
+
user: {
|
|
56
|
+
token?: string
|
|
57
|
+
exec?: {
|
|
58
|
+
command: string
|
|
59
|
+
args: string[]
|
|
60
|
+
apiVersion: 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 environmentDomain: string
|
|
89
|
+
private readonly stackApiBaseUrl: string
|
|
90
|
+
private readonly environmentApiBaseUrl: string
|
|
91
|
+
private readonly organization?: string
|
|
92
|
+
private readonly project?: string
|
|
93
|
+
private readonly environment?: string
|
|
94
|
+
|
|
95
|
+
constructor (accessToken: string, environmentDomain: string, stackApiBaseUrl: string, environmentApiBaseUrl: string, organization?: string, project?: string, environment?: string) {
|
|
96
|
+
this.accessToken = accessToken
|
|
97
|
+
this.environmentDomain = environmentDomain
|
|
98
|
+
this.stackApiBaseUrl = stackApiBaseUrl
|
|
99
|
+
this.environmentApiBaseUrl = environmentApiBaseUrl
|
|
100
|
+
this.organization = organization
|
|
101
|
+
this.project = project
|
|
102
|
+
this.environment = environment
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async fetchJson<T> (url: string, method: string): Promise<T> {
|
|
106
|
+
const response = await fetch(url, {
|
|
107
|
+
method,
|
|
108
|
+
headers: {
|
|
109
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
110
|
+
'Content-Type': 'application/json'
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (response.status !== 200) {
|
|
115
|
+
throw new Error(`Failed to fetch ${method} ${url}: ${response.statusText}, response code=${response.status}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return await response.json() as T
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async fetchText (url: string, method: string): Promise<string> {
|
|
122
|
+
let response
|
|
123
|
+
try {
|
|
124
|
+
response = await fetch(url, {
|
|
125
|
+
method,
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
128
|
+
'Content-Type': 'application/json'
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
logger.error(`Failed to fetch ${method} ${url}:`, error)
|
|
133
|
+
if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
134
|
+
throw new Error(`Failed to to fetch ${url}: SSL certificate validation failed. Is someone trying to tamper with your internet connection?`)
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Failed to fetch ${url}: ${error}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (response.status !== 200) {
|
|
140
|
+
throw new Error(`Failed to fetch ${method} ${url}: ${response.statusText}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return await response.text()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getAwsCredentials (): Promise<AwsCredentialsResponse> {
|
|
147
|
+
const url = `${this.environmentApiBaseUrl}/credentials/aws`
|
|
148
|
+
logger.debug(`Fetching AWS credentials from ${url}...`)
|
|
149
|
+
return await this.fetchJson<AwsCredentialsResponse>(url, 'POST')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getKubeConfig (): Promise<string> {
|
|
153
|
+
const url = `${this.environmentApiBaseUrl}/credentials/k8s`
|
|
154
|
+
logger.debug(`Getting KubeConfig from ${url}...`)
|
|
155
|
+
return await this.fetchText(url, 'POST')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get a `kubeconfig` payload which invokes `metaplay-auth get-kubernetes-execcredential` to get the actual
|
|
160
|
+
* access credentials each time the kubeconfig is used.
|
|
161
|
+
* @returns The kubeconfig YAML.
|
|
162
|
+
*/
|
|
163
|
+
async getKubeConfigExecCredential (): Promise<string> {
|
|
164
|
+
const url = `${this.environmentApiBaseUrl}/credentials/k8s?type=execcredential`
|
|
165
|
+
logger.debug(`Getting Kubernetes KubeConfig from ${url}...`)
|
|
166
|
+
|
|
167
|
+
// get the execcredential and morph it into a kubeconfig which calls metaplay-auth for the token
|
|
168
|
+
let kubeExecCredential: KubeExecCredential
|
|
169
|
+
try {
|
|
170
|
+
kubeExecCredential = await this.fetchJson<KubeExecCredential>(url, 'POST')
|
|
171
|
+
} catch {
|
|
172
|
+
throw new Error('Failed to fetch Kubernetes KubeConfig')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!kubeExecCredential.spec.cluster) {
|
|
176
|
+
throw new Error('Received kubeExecCredential with missing spec.cluster')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Fetch environment namespace
|
|
180
|
+
const environment = await this.getEnvironmentDetails()
|
|
181
|
+
const namespace = environment.deployment?.kubernetes_namespace
|
|
182
|
+
if (!namespace) {
|
|
183
|
+
throw new Error('Environment details did not contain a valid Kubernetes namespace')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fetch user id
|
|
187
|
+
const userinfo = await getUserinfo(this.accessToken)
|
|
188
|
+
const userId = userinfo.email
|
|
189
|
+
|
|
190
|
+
// Resolve the environment identity (prefer org-proj-env or fall back to FQDN)
|
|
191
|
+
const envId = this.organization ? `${this.organization}-${this.project}-${this.environment}` : this.environmentDomain
|
|
192
|
+
|
|
193
|
+
// Resolve invocation for getting the kubeconfig exec credential
|
|
194
|
+
let execCmd = 'metaplay-auth'
|
|
195
|
+
let execArgs =
|
|
196
|
+
['get-kubernetes-execcredential']
|
|
197
|
+
.concat(this.stackApiBaseUrl !== defaultStackApiBaseUrl ? ['--stack-api', this.stackApiBaseUrl] : [])
|
|
198
|
+
.concat([envId])
|
|
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': envId,
|
|
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: envId,
|
|
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
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// return as yaml for easier consumption
|
|
248
|
+
return dump(kubeConfig)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async getKubeExecCredential (): Promise<string> {
|
|
252
|
+
const url = `${this.environmentApiBaseUrl}/credentials/k8s?type=execcredential`
|
|
253
|
+
logger.debug(`Getting Kubernetes ExecCredential from ${url}...`)
|
|
254
|
+
return await this.fetchText(url, 'POST')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async getEnvironmentDetails (): Promise<EnvironmentDetails> {
|
|
258
|
+
// \note If invoking StackAPI via the https://<environment>/.infra, we need to add '/environment' to the path
|
|
259
|
+
const urlSuffix = this.environmentApiBaseUrl.endsWith('.infra') ? '/environment' : ''
|
|
260
|
+
const url = this.environmentApiBaseUrl + urlSuffix
|
|
261
|
+
logger.debug(`Getting environment details from ${url}...`)
|
|
262
|
+
return await this.fetchJson<EnvironmentDetails>(url, 'GET')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async getDockerCredentials (): Promise<DockerCredentials> {
|
|
266
|
+
// Get environment info (for AWS region)
|
|
267
|
+
logger.debug('Get environment info')
|
|
268
|
+
const envInfo = await this.getEnvironmentDetails()
|
|
269
|
+
|
|
270
|
+
// Fetch AWS credentials from Metaplay cloud
|
|
271
|
+
logger.debug('Get AWS credentials from Metaplay')
|
|
272
|
+
const awsCredentials = await this.getAwsCredentials()
|
|
273
|
+
|
|
274
|
+
// Create ECR client with credentials
|
|
275
|
+
logger.debug('Create ECR client')
|
|
276
|
+
const client = new ECRClient({
|
|
277
|
+
credentials: {
|
|
278
|
+
accessKeyId: awsCredentials.AccessKeyId,
|
|
279
|
+
secretAccessKey: awsCredentials.SecretAccessKey,
|
|
280
|
+
sessionToken: awsCredentials.SessionToken
|
|
281
|
+
},
|
|
282
|
+
region: envInfo.deployment.aws_region
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Fetch the ECR docker authentication token
|
|
286
|
+
logger.debug('Fetch ECR login credentials from AWS')
|
|
287
|
+
const command = new GetAuthorizationTokenCommand({})
|
|
288
|
+
const response = await client.send(command)
|
|
289
|
+
if (!response.authorizationData || response.authorizationData.length === 0 || !response.authorizationData[0].authorizationToken || !response.authorizationData[0].proxyEndpoint) {
|
|
290
|
+
throw new Error('Received an empty authorization token response for ECR repository')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Parse username and password from the response (separated by a ':')
|
|
294
|
+
logger.debug('Parse ECR response')
|
|
295
|
+
const registryUrl = response.authorizationData[0].proxyEndpoint
|
|
296
|
+
const authorization64 = response.authorizationData[0].authorizationToken
|
|
297
|
+
const authorization = Buffer.from(authorization64, 'base64').toString()
|
|
298
|
+
const [username, password] = authorization.split(':')
|
|
299
|
+
logger.debug(`ECR: username=${username}, proxyEndpoint=${registryUrl}`)
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
username,
|
|
303
|
+
password,
|
|
304
|
+
registryUrl
|
|
305
|
+
} satisfies DockerCredentials
|
|
306
|
+
}
|
|
307
|
+
}
|