@metaplay/metaplay-auth 1.9.1 → 1.9.3-next.878
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 +14 -0
- package/dist/index.cjs +146 -219
- package/dist/sshcrypto-U57V5JZV.node +0 -0
- package/index.ts +51 -27
- package/package.json +18 -19
- package/src/auth.ts +59 -28
- package/src/buildCommand.ts +3 -3
- package/src/deployment.ts +13 -10
- package/src/secret_store.ts +5 -5
- package/src/stackapi.ts +1 -1
- package/src/targetenvironment.ts +5 -5
- package/src/utils.ts +61 -8
- package/src/version.ts +1 -1
- package/dist/sshcrypto-47ANNZZ5.node +0 -0
|
Binary file
|
package/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander'
|
|
3
3
|
import Docker from 'dockerode'
|
|
4
|
-
import { writeFile } from 'fs/promises'
|
|
5
|
-
import { exit } from 'process'
|
|
4
|
+
import { writeFile } from 'node:fs/promises'
|
|
5
|
+
import { exit } from 'node:process'
|
|
6
6
|
import * as semver from 'semver'
|
|
7
7
|
|
|
8
8
|
import { KubeConfig } from '@kubernetes/client-node'
|
|
@@ -13,7 +13,7 @@ import { portalBaseUrl, setPortalBaseUrl } from './src/config.js'
|
|
|
13
13
|
import { checkGameServerDeployment, debugGameServer } from './src/deployment.js'
|
|
14
14
|
import { logger, setLogLevel } from './src/logging.js'
|
|
15
15
|
import { TargetEnvironment } from './src/targetenvironment.js'
|
|
16
|
-
import { isValidFQDN, removeTrailingSlash, fetchHelmChartVersions, resolveBestMatchingVersion, executeHelmCommand, getGameServerHelmRelease } from './src/utils.js'
|
|
16
|
+
import { isValidFQDN, removeTrailingSlash, fetchHelmChartVersions, resolveBestMatchingVersion, checkHelmVersion, executeHelmCommand, getGameServerHelmRelease } from './src/utils.js'
|
|
17
17
|
import { PACKAGE_VERSION } from './src/version.js'
|
|
18
18
|
|
|
19
19
|
/** Stack API base url override, specified with the '--stack-api' global flag. */
|
|
@@ -250,6 +250,10 @@ program
|
|
|
250
250
|
setLogLevel(10)
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
// Show a deprecation warning.
|
|
254
|
+
console.warn('Warning: The metaplay-auth CLI is deprecated and will only receive critical updates.')
|
|
255
|
+
console.warn(' Please upgrade to Metaplay SDK release 32 or newer and the new Metaplay CLI (https://github.com/metaplay/cli).')
|
|
256
|
+
|
|
253
257
|
// Store the portal base URL for accessing globally
|
|
254
258
|
const overridePortalUrl: string | undefined = opts.portalBaseUrl ?? process.env.AUTHCLI_PORTAL_BASEURL
|
|
255
259
|
if (overridePortalUrl) {
|
|
@@ -275,29 +279,34 @@ program
|
|
|
275
279
|
.description('login to the Metaplay cloud using a machine account (using credentials in environment variable METAPLAY_CREDENTIALS)')
|
|
276
280
|
.option('--dev-credentials', 'machine user credentials to use, only for dev purposes, use METAPLAY_CREDENTIALS env variable for better safety!')
|
|
277
281
|
.action(async (options) => {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
282
|
+
try {
|
|
283
|
+
// Get credentials from command line or from METAPLAY_CREDENTIALS environment variable
|
|
284
|
+
let credentials: string
|
|
285
|
+
if (options.devCredentials) {
|
|
286
|
+
credentials = options.devCredentials
|
|
287
|
+
} else {
|
|
288
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
289
|
+
credentials = process.env.METAPLAY_CREDENTIALS!
|
|
290
|
+
if (!credentials || credentials === '') {
|
|
291
|
+
throw new Error('Unable to find the credentials, the environment variable METAPLAY_CREDENTIALS is not defined!')
|
|
292
|
+
}
|
|
287
293
|
}
|
|
288
|
-
}
|
|
289
294
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
// Parse the clientId and clientSecret from the credentials (separate by a '+' character)
|
|
296
|
+
// \note We can't be certain that the secret does not contain pluses so split at the first occurrence
|
|
297
|
+
const splitOffset = credentials.indexOf('+')
|
|
298
|
+
if (splitOffset === -1) {
|
|
299
|
+
throw new Error('Invalid format for credentials, you should copy-paste the value from the developer portal verbatim')
|
|
300
|
+
}
|
|
301
|
+
const clientId = credentials.substring(0, splitOffset)
|
|
302
|
+
const clientSecret = credentials.substring(splitOffset + 1)
|
|
298
303
|
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
// Login with machine user and save the tokens
|
|
305
|
+
await machineLoginAndSaveTokens(clientId, clientSecret)
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error(`Error during machine login: ${error instanceof Error ? error.message : String(error)}`)
|
|
308
|
+
exit(1)
|
|
309
|
+
}
|
|
301
310
|
})
|
|
302
311
|
|
|
303
312
|
program
|
|
@@ -716,7 +725,10 @@ program
|
|
|
716
725
|
})
|
|
717
726
|
} catch (error) {
|
|
718
727
|
const errMessage = error instanceof Error ? error.message : String(error)
|
|
719
|
-
throw new Error(
|
|
728
|
+
throw new Error(
|
|
729
|
+
`Failed to fetch docker image ${imageName} from target environment registry: ${errMessage}`,
|
|
730
|
+
{ cause: error }
|
|
731
|
+
)
|
|
720
732
|
}
|
|
721
733
|
|
|
722
734
|
// Resolve the labels
|
|
@@ -726,7 +738,10 @@ program
|
|
|
726
738
|
imageLabels = (await localDockerImage.inspect()).Config.Labels || {}
|
|
727
739
|
} catch (error) {
|
|
728
740
|
const errMessage = error instanceof Error ? error.message : String(error)
|
|
729
|
-
throw new Error(
|
|
741
|
+
throw new Error(
|
|
742
|
+
`Failed to resolve docker image metadata from pulled image ${imageName}: ${errMessage}`,
|
|
743
|
+
{ cause: error }
|
|
744
|
+
)
|
|
730
745
|
}
|
|
731
746
|
}
|
|
732
747
|
|
|
@@ -763,7 +778,10 @@ program
|
|
|
763
778
|
try {
|
|
764
779
|
helmChartRange = new semver.Range(helmChartVersionSpec)
|
|
765
780
|
} catch (error) {
|
|
766
|
-
throw new Error(
|
|
781
|
+
throw new Error(
|
|
782
|
+
`Helm chart version '${helmChartVersionSpec}' is not a valid SemVer range: ${String(error)}`,
|
|
783
|
+
{ cause: error }
|
|
784
|
+
)
|
|
767
785
|
}
|
|
768
786
|
}
|
|
769
787
|
|
|
@@ -783,6 +801,9 @@ program
|
|
|
783
801
|
}
|
|
784
802
|
logger.debug('Resolved Helm chart version: ', resolvedHelmChartVersion)
|
|
785
803
|
|
|
804
|
+
// Check that the Helm version is compatible
|
|
805
|
+
await checkHelmVersion()
|
|
806
|
+
|
|
786
807
|
// Fetch kubeconfig and write it to a temporary file
|
|
787
808
|
// \todo allow passing a custom kubeconfig file?
|
|
788
809
|
const kubeconfigPayload = await targetEnv.getKubeConfigWithEmbeddedCredentials()
|
|
@@ -866,6 +887,9 @@ program
|
|
|
866
887
|
exit(0)
|
|
867
888
|
}
|
|
868
889
|
|
|
890
|
+
// Check that the Helm version is compatible
|
|
891
|
+
await checkHelmVersion()
|
|
892
|
+
|
|
869
893
|
// Construct Helm invocation.
|
|
870
894
|
const deploymentName = gameServerHelmRelease.name
|
|
871
895
|
const helmArgs = ['uninstall', '--wait'] // \note waits for resources to be deleted before returning
|
|
@@ -915,7 +939,7 @@ program
|
|
|
915
939
|
kubeconfig.loadFromString(kubeconfigPayload)
|
|
916
940
|
} catch (error) {
|
|
917
941
|
const errMessage = error instanceof Error ? error.message : String(error)
|
|
918
|
-
throw new Error(`Failed to load or validate kubeconfig. ${errMessage}
|
|
942
|
+
throw new Error(`Failed to load or validate kubeconfig. ${errMessage}`, { cause: error })
|
|
919
943
|
}
|
|
920
944
|
|
|
921
945
|
// Resolve Helm deployment
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaplay/metaplay-auth",
|
|
3
3
|
"description": "Utility CLI for authenticating with the Metaplay Auth and making authenticated calls to infrastructure endpoints.",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.3-next.878",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
7
|
"homepage": "https://metaplay.io",
|
|
@@ -17,29 +17,28 @@
|
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@metaplay/eslint-config": "workspace:*",
|
|
20
|
-
"@types/dockerode": "3.3.
|
|
21
|
-
"@types/express": "5.0.
|
|
22
|
-
"@types/js-yaml": "4.0.9",
|
|
23
|
-
"@types/jsonwebtoken": "9.0.
|
|
20
|
+
"@types/dockerode": "3.3.44",
|
|
21
|
+
"@types/express": "5.0.3",
|
|
22
|
+
"@types/js-yaml": "^4.0.9",
|
|
23
|
+
"@types/jsonwebtoken": "9.0.10",
|
|
24
24
|
"@types/jwk-to-pem": "2.0.3",
|
|
25
|
-
"@types/node": "
|
|
26
|
-
"@types/semver": "7.7.
|
|
27
|
-
"esbuild": "0.25.
|
|
28
|
-
"tsx": "4.
|
|
29
|
-
"typescript": "5.
|
|
30
|
-
"vitest": "3.
|
|
31
|
-
"@aws-sdk/client-ecr": "3.
|
|
32
|
-
"@kubernetes/client-node": "1.
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"js-yaml": "4.1.0",
|
|
25
|
+
"@types/node": "24.5.2",
|
|
26
|
+
"@types/semver": "7.7.1",
|
|
27
|
+
"esbuild": "0.25.10",
|
|
28
|
+
"tsx": "4.20.5",
|
|
29
|
+
"typescript": "5.9.2",
|
|
30
|
+
"vitest": "3.2.4",
|
|
31
|
+
"@aws-sdk/client-ecr": "3.891.0",
|
|
32
|
+
"@kubernetes/client-node": "1.3.0",
|
|
33
|
+
"commander": "14.0.1",
|
|
34
|
+
"dockerode": "4.0.8",
|
|
35
|
+
"h3": "1.15.4",
|
|
36
|
+
"js-yaml": "^4.1.0",
|
|
38
37
|
"jsonwebtoken": "9.0.2",
|
|
39
38
|
"jwk-to-pem": "2.0.7",
|
|
40
39
|
"open": "8.4.2",
|
|
41
40
|
"semver": "7.7.2",
|
|
42
41
|
"tslog": "4.9.3",
|
|
43
|
-
"eslint": "9.
|
|
42
|
+
"eslint": "9.35.0"
|
|
44
43
|
}
|
|
45
44
|
}
|
package/src/auth.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
|
1
2
|
|
|
2
3
|
import { toNodeListener, createApp, defineEventHandler, getQuery, sendError } from 'h3'
|
|
3
4
|
import jwt, { JwtPayload } from 'jsonwebtoken'
|
|
@@ -6,8 +7,6 @@ import { randomBytes, createHash } from 'node:crypto'
|
|
|
6
7
|
import { createServer } from 'node:http'
|
|
7
8
|
import open from 'open'
|
|
8
9
|
|
|
9
|
-
import { Configuration, OidcApi } from '@ory/client'
|
|
10
|
-
|
|
11
10
|
import { portalBaseUrl } from './config.js'
|
|
12
11
|
import { logger } from './logging.js'
|
|
13
12
|
import { setSecret, getSecret, removeSecret } from './secret_store.js'
|
|
@@ -23,11 +22,6 @@ const clientId = 'c16ea663-ced3-46c6-8f85-38c9681fe1f0'
|
|
|
23
22
|
const baseURL = 'https://auth.metaplay.dev'
|
|
24
23
|
const authorizationEndpoint = `${baseURL}/oauth2/auth`
|
|
25
24
|
const tokenEndpoint = `${baseURL}/oauth2/token`
|
|
26
|
-
const oidcApi = new OidcApi(
|
|
27
|
-
new Configuration({
|
|
28
|
-
basePath: baseURL,
|
|
29
|
-
})
|
|
30
|
-
)
|
|
31
25
|
|
|
32
26
|
/**
|
|
33
27
|
* A helper function which generates a code verifier and challenge for exchanging code from Ory server.
|
|
@@ -45,16 +39,9 @@ function generateCodeVerifierAndChallenge(): { verifier: string; challenge: stri
|
|
|
45
39
|
* @returns An object containing the user's info.
|
|
46
40
|
*/
|
|
47
41
|
export async function getUserinfo(token: string): Promise<any> {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const userinfoEndpoint = oidcRes.data?.userinfo_endpoint
|
|
52
|
-
if (!userinfoEndpoint) {
|
|
53
|
-
throw new Error('No userinfo endpoint found in OIDC configuration')
|
|
54
|
-
}
|
|
55
|
-
logger.debug(`Found userinfo endpoint: ${userinfoEndpoint}`)
|
|
56
|
-
|
|
57
|
-
const userinfoRes = await fetch(userinfoEndpoint, {
|
|
42
|
+
// To relieve pressure on Ory endpoints, get the userinfo directly from portal,
|
|
43
|
+
// instead of discovering OIDC endpoints from Ory first. Portal is where Ory would redirect the request anyway.
|
|
44
|
+
const userinfoRes = await fetch(`${portalBaseUrl}/api/external/userinfo`, {
|
|
58
45
|
headers: {
|
|
59
46
|
Authorization: `Bearer ${token}`,
|
|
60
47
|
},
|
|
@@ -201,8 +188,30 @@ export async function machineLoginAndSaveTokens(clientId: string, clientSecret:
|
|
|
201
188
|
body: params.toString(),
|
|
202
189
|
})
|
|
203
190
|
|
|
204
|
-
//
|
|
205
|
-
|
|
191
|
+
// Improved error handling for response
|
|
192
|
+
let tokens: { access_token: string; token_type: string; expires_in: number; scope: string }
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
let message: string | undefined
|
|
195
|
+
try {
|
|
196
|
+
const errorBody: any = await res.json()
|
|
197
|
+
message = errorBody && (errorBody.error_description || errorBody.error || errorBody.message)
|
|
198
|
+
} catch (_error) {
|
|
199
|
+
try {
|
|
200
|
+
message = await res.text()
|
|
201
|
+
} catch (_error) {
|
|
202
|
+
message = undefined
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
throw new Error(`Machine login failed: ${message || `HTTP ${res.status}`}`)
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
tokens = (await res.json()) as { access_token: string; token_type: string; expires_in: number; scope: string }
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
"Invalid response from authentication server. Please check your network connection and credentials and try again.",
|
|
212
|
+
{ cause: error }
|
|
213
|
+
)
|
|
214
|
+
}
|
|
206
215
|
|
|
207
216
|
logger.debug('Received machine authentication tokens, saving them for future use...')
|
|
208
217
|
|
|
@@ -214,7 +223,29 @@ export async function machineLoginAndSaveTokens(clientId: string, clientSecret:
|
|
|
214
223
|
},
|
|
215
224
|
})
|
|
216
225
|
|
|
217
|
-
|
|
226
|
+
let userInfo: { given_name: string; family_name: string }
|
|
227
|
+
if (!userInfoResponse.ok) {
|
|
228
|
+
let message: string | undefined
|
|
229
|
+
try {
|
|
230
|
+
const errorBody: any = await userInfoResponse.json()
|
|
231
|
+
message = errorBody && (errorBody.error_description || errorBody.error || errorBody.message)
|
|
232
|
+
} catch (_error) {
|
|
233
|
+
try {
|
|
234
|
+
message = await userInfoResponse.text()
|
|
235
|
+
} catch (_error) {
|
|
236
|
+
message = undefined
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
throw new Error(`Failed to fetch user info: ${message || `HTTP ${userInfoResponse.status}`}`)
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
userInfo = (await userInfoResponse.json()) as { given_name: string; family_name: string }
|
|
243
|
+
} catch (error) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
"Invalid response from user info endpoint. Please check your network connection and try again.",
|
|
246
|
+
{ cause: error }
|
|
247
|
+
)
|
|
248
|
+
}
|
|
218
249
|
|
|
219
250
|
console.log(
|
|
220
251
|
`You are now logged in with machine user ${userInfo.given_name} ${userInfo.family_name} (clientId=${clientId}) and can execute the other commands.`
|
|
@@ -267,7 +298,6 @@ export async function extendCurrentSession(): Promise<void> {
|
|
|
267
298
|
async function extendCurrentSessionWithRefreshToken(
|
|
268
299
|
refreshToken: string
|
|
269
300
|
): Promise<{ id_token: string; access_token: string; refresh_token: string }> {
|
|
270
|
-
// TODO: similar to the todo task in getTokensWithAuthorizationCode, http request can be handled by ory/client.
|
|
271
301
|
const params = new URLSearchParams({
|
|
272
302
|
grant_type: 'refresh_token',
|
|
273
303
|
refresh_token: refreshToken,
|
|
@@ -292,9 +322,12 @@ async function extendCurrentSessionWithRefreshToken(
|
|
|
292
322
|
logger.error(`Failed to refresh tokens via endpoint ${tokenEndpoint}`)
|
|
293
323
|
logger.error('Fetch error details:', error)
|
|
294
324
|
if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
295
|
-
throw new Error(
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Failed to refresh tokens: SSL certificate validation failed for ${tokenEndpoint}. Is someone trying to tamper with your internet connection?`,
|
|
327
|
+
{ cause: error }
|
|
328
|
+
)
|
|
296
329
|
}
|
|
297
|
-
throw new Error(`Failed to refresh tokens via ${tokenEndpoint}: ${error}
|
|
330
|
+
throw new Error(`Failed to refresh tokens via ${tokenEndpoint}: ${error}`, { cause: error })
|
|
298
331
|
}
|
|
299
332
|
|
|
300
333
|
// Check if the response is OK
|
|
@@ -323,14 +356,12 @@ async function extendCurrentSessionWithRefreshToken(
|
|
|
323
356
|
* @param code
|
|
324
357
|
* @returns
|
|
325
358
|
*/
|
|
326
|
-
|
|
327
359
|
async function getTokensWithAuthorizationCode(
|
|
328
360
|
state: string,
|
|
329
361
|
redirectUri: string,
|
|
330
362
|
verifier: string,
|
|
331
363
|
code: string
|
|
332
364
|
): Promise<{ id_token: string; access_token: string; refresh_token: string }> {
|
|
333
|
-
// TODO: the authorization code exchange flow might be better to be handled by ory/client, could check if there's any useful tools there.
|
|
334
365
|
try {
|
|
335
366
|
const response = await fetch(tokenEndpoint, {
|
|
336
367
|
method: 'POST',
|
|
@@ -364,7 +395,7 @@ export async function loadTokens(): Promise<TokenSet> {
|
|
|
364
395
|
return tokens
|
|
365
396
|
} catch (error) {
|
|
366
397
|
if (error instanceof Error) {
|
|
367
|
-
throw new Error(`Error loading tokens: ${error.message}
|
|
398
|
+
throw new Error(`Error loading tokens: ${error.message}`, { cause: error })
|
|
368
399
|
}
|
|
369
400
|
|
|
370
401
|
throw error
|
|
@@ -393,7 +424,7 @@ export async function saveTokens(tokens: Record<string, string>): Promise<void>
|
|
|
393
424
|
showTokenInfo(tokens.access_token)
|
|
394
425
|
} catch (error) {
|
|
395
426
|
if (error instanceof Error) {
|
|
396
|
-
throw new Error(`Failed to save tokens: ${error.message}
|
|
427
|
+
throw new Error(`Failed to save tokens: ${error.message}`, { cause: error })
|
|
397
428
|
}
|
|
398
429
|
|
|
399
430
|
throw error
|
|
@@ -409,7 +440,7 @@ export async function removeTokens(): Promise<void> {
|
|
|
409
440
|
logger.debug('Removed tokens.')
|
|
410
441
|
} catch (error) {
|
|
411
442
|
if (error instanceof Error) {
|
|
412
|
-
throw new Error(`Error removing tokens: ${error.message}
|
|
443
|
+
throw new Error(`Error removing tokens: ${error.message}`, { cause: error })
|
|
413
444
|
}
|
|
414
445
|
|
|
415
446
|
throw error
|
package/src/buildCommand.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
|
-
import { readFileSync, existsSync } from 'fs'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { exit } from 'process'
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { exit } from 'node:process'
|
|
5
5
|
import * as semver from 'semver'
|
|
6
6
|
|
|
7
7
|
import { pathJoin, executeCommand, ExecuteCommandResult } from './utils.js'
|
package/src/deployment.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { unlink, writeFile } from 'fs/promises'
|
|
2
|
-
import os from 'os'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { exit } from 'process'
|
|
1
|
+
import { unlink, writeFile } from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { exit } from 'node:process'
|
|
5
5
|
import { EnvironmentDetails, TargetEnvironment } from 'targetenvironment.js'
|
|
6
|
-
import { promises as dns } from 'dns'
|
|
7
|
-
import tls from 'tls'
|
|
6
|
+
import { promises as dns } from 'node:dns'
|
|
7
|
+
import tls from 'node:tls'
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
KubeConfig,
|
|
@@ -48,7 +48,7 @@ async function fetchGameServerPods(k8sApi: CoreV1Api, namespace: string): Promis
|
|
|
48
48
|
} catch (error) {
|
|
49
49
|
// \todo Better error handling ..
|
|
50
50
|
console.error('Failed to fetch pods from Kubernetes:', error)
|
|
51
|
-
throw new Error('Failed to fetch pods from Kubernetes')
|
|
51
|
+
throw new Error('Failed to fetch pods from Kubernetes', { cause: error })
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -337,7 +337,7 @@ async function fetchPodLogs(k8sApi: CoreV1Api, pod: V1Pod): Promise<string> {
|
|
|
337
337
|
} catch (error) {
|
|
338
338
|
// \todo Better error handling ..
|
|
339
339
|
console.log('Failed to fetch pod logs from Kubernetes:', error)
|
|
340
|
-
throw new Error('Failed to fetch pod logs from Kubernetes')
|
|
340
|
+
throw new Error('Failed to fetch pod logs from Kubernetes', { cause: error })
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
|
|
@@ -363,7 +363,9 @@ async function checkGameServerPod(
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
async function delay(ms: number): Promise<void> {
|
|
366
|
-
await new Promise<void>((resolve) =>
|
|
366
|
+
await new Promise<void>((resolve) => {
|
|
367
|
+
setTimeout(resolve, ms)
|
|
368
|
+
})
|
|
367
369
|
}
|
|
368
370
|
|
|
369
371
|
function anyPodsInPhase(podStatuses: GameServerPodStatus[], phase: GameServerPodPhase): boolean {
|
|
@@ -457,7 +459,7 @@ async function waitForDomainResolution(hostname: string): Promise<void> {
|
|
|
457
459
|
return
|
|
458
460
|
} catch (err) {
|
|
459
461
|
if (Date.now() > timeoutAt) {
|
|
460
|
-
throw new Error(`Could not resolve domain ${hostname} before timeout
|
|
462
|
+
throw new Error(`Could not resolve domain ${hostname} before timeout.`, { cause: err })
|
|
461
463
|
}
|
|
462
464
|
|
|
463
465
|
if (err instanceof Error && (err as NodeJS.ErrnoException).code === 'ENOTFOUND') {
|
|
@@ -587,6 +589,7 @@ export async function debugGameServer(targetEnv: TargetEnvironment, targetPodNam
|
|
|
587
589
|
if (!metadata?.name) {
|
|
588
590
|
throw new Error('Unable to resolve name for the Kubernetes pod!')
|
|
589
591
|
}
|
|
592
|
+
// eslint-disable-next-line no-param-reassign
|
|
590
593
|
targetPodName = metadata.name
|
|
591
594
|
} else {
|
|
592
595
|
const podNames = gameServerPods.map((pod) => pod.metadata?.name).join(', ')
|
package/src/secret_store.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'crypto'
|
|
2
|
-
import { promises as fs } from 'fs'
|
|
3
|
-
import { homedir } from 'os'
|
|
4
|
-
import { join } from 'path'
|
|
1
|
+
import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from 'node:crypto'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
5
|
|
|
6
6
|
import { logger } from './logging.js'
|
|
7
7
|
|
|
@@ -36,7 +36,7 @@ async function loadSecrets(): Promise<Secrets> {
|
|
|
36
36
|
return {}
|
|
37
37
|
}
|
|
38
38
|
if (password.length === 0) {
|
|
39
|
-
throw new Error('The file is encrypted. Please set the METAPLAY_AUTH_PASSWORD environment variable.')
|
|
39
|
+
throw new Error('The file is encrypted. Please set the METAPLAY_AUTH_PASSWORD environment variable.', { cause: error })
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
throw error
|
package/src/stackapi.ts
CHANGED
package/src/targetenvironment.ts
CHANGED
|
@@ -133,9 +133,9 @@ export class TargetEnvironment {
|
|
|
133
133
|
} catch (error: any) {
|
|
134
134
|
logger.error(`Failed to fetch ${method} ${url}:`, error)
|
|
135
135
|
if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
136
|
-
throw new Error(`Failed to to fetch ${url}: SSL certificate validation failed. Is someone trying to tamper with your internet connection
|
|
136
|
+
throw new Error(`Failed to to fetch ${url}: SSL certificate validation failed. Is someone trying to tamper with your internet connection?`, { cause: error })
|
|
137
137
|
}
|
|
138
|
-
throw new Error(`Failed to fetch ${url}: ${error}
|
|
138
|
+
throw new Error(`Failed to fetch ${url}: ${error}`, { cause: error })
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
if (response.status !== 200) {
|
|
@@ -183,13 +183,13 @@ export class TargetEnvironment {
|
|
|
183
183
|
// User-friendly error messages for well-known HTTP errors.
|
|
184
184
|
if (error instanceof FetchJsonHttpError) {
|
|
185
185
|
if (error.response.status === 404) {
|
|
186
|
-
throw new Error(`Environment ${this.humanId} does not exist
|
|
186
|
+
throw new Error(`Environment ${this.humanId} does not exist.`, { cause: error })
|
|
187
187
|
} else if (error.response.status === 403) {
|
|
188
|
-
throw new Error(`Your account does not have permissions to access environment ${this.humanId}
|
|
188
|
+
throw new Error(`Your account does not have permissions to access environment ${this.humanId}.`, { cause: error })
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
const errorMessage = (error instanceof Error) ? error.message : String(error)
|
|
192
|
-
throw new Error(`Failed to fetch Kubernetes KubeConfig: ${errorMessage}
|
|
192
|
+
throw new Error(`Failed to fetch Kubernetes KubeConfig: ${errorMessage}`, { cause: error })
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
if (!kubeExecCredential.spec.cluster) {
|
package/src/utils.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { spawn } from 'child_process'
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
2
|
import yaml from 'js-yaml'
|
|
3
|
-
import path from 'path'
|
|
3
|
+
import path from 'node:path'
|
|
4
4
|
import * as semver from 'semver'
|
|
5
5
|
|
|
6
6
|
import { logger } from './logging.js'
|
|
7
|
-
import { tmpdir } from 'os'
|
|
8
|
-
import { randomBytes } from 'crypto'
|
|
9
|
-
import { unlink, writeFile } from 'fs/promises'
|
|
7
|
+
import { tmpdir } from 'node:os'
|
|
8
|
+
import { randomBytes } from 'node:crypto'
|
|
9
|
+
import { unlink, writeFile } from 'node:fs/promises'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Checks if the given string is a fully qualified domain name (FQDN).
|
|
@@ -44,9 +44,9 @@ export function splitUrlComponents(urlString: string): {
|
|
|
44
44
|
const subpaths = url.pathname.slice(1) ?? null
|
|
45
45
|
|
|
46
46
|
return { scheme, hostname, port, subpaths }
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
} catch (error) {
|
|
49
|
-
throw new Error('Invalid URL')
|
|
49
|
+
throw new Error('Invalid URL', { cause: error })
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -220,6 +220,58 @@ export function resolveBestMatchingVersion(versions: string[], range: semver.Ran
|
|
|
220
220
|
return sortedVersions[0]
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Check that the installed Helm version is compatible with the required version range.
|
|
225
|
+
* Validates that Helm version is >= 3.18.0 and < 3.18.5.
|
|
226
|
+
*
|
|
227
|
+
* Version 3.18.5 (and recent other patch versions) tightened up the Helm chart validation logic
|
|
228
|
+
* and we can't retroactively fix the charts. The path forward is to upgrade to more recent Metaplay
|
|
229
|
+
* SDK versions and switch to the new CLI (https://github.com/metaplay/cli).
|
|
230
|
+
*
|
|
231
|
+
* @throws Error if Helm is not installed, version cannot be determined, or version is incompatible
|
|
232
|
+
*/
|
|
233
|
+
export async function checkHelmVersion(): Promise<void> {
|
|
234
|
+
try {
|
|
235
|
+
// Execute 'helm version' command to get version information
|
|
236
|
+
const result = await executeCommand('helm', ['version', '--short'])
|
|
237
|
+
|
|
238
|
+
if (result.exitCode !== 0) {
|
|
239
|
+
throw new Error(`Helm version command failed with exit code ${result.exitCode}: ${result.stderr.join('')}`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Parse version from output (format: "v3.18.0+g123abc")
|
|
243
|
+
const versionOutput = result.stdout.join('').trim()
|
|
244
|
+
const versionRegex = /v(\d+\.\d+\.\d+)/
|
|
245
|
+
const versionMatch = versionRegex.exec(versionOutput)
|
|
246
|
+
|
|
247
|
+
if (!versionMatch) {
|
|
248
|
+
throw new Error(`Could not parse Helm version from output: ${versionOutput}`)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const helmVersion = versionMatch[1]
|
|
252
|
+
logger.debug(`Detected Helm version: ${helmVersion}`)
|
|
253
|
+
|
|
254
|
+
// Check version constraints: >= 3.18.0 and <= 3.18.4
|
|
255
|
+
const minVersion = '3.18.0'
|
|
256
|
+
const maxVersion = '3.18.4'
|
|
257
|
+
|
|
258
|
+
if (!semver.gte(helmVersion, minVersion)) {
|
|
259
|
+
throw new Error(`Locally installed Helm version ${helmVersion} is too old. Use Helm v${maxVersion} which is the latest compatible version`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!semver.lte(helmVersion, maxVersion)) {
|
|
263
|
+
throw new Error(`Locally installed Helm version ${helmVersion} is too new. Use Helm v${maxVersion} which is the latest compatible version`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
logger.debug(`Helm version ${helmVersion} is compatible`)
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof Error && error.message.includes('ENOENT')) {
|
|
269
|
+
throw new Error('Helm is not installed or not found in PATH. Please install Helm version >= 3.18.0 and < 3.18.5', { cause: error })
|
|
270
|
+
}
|
|
271
|
+
throw error
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
223
275
|
/**
|
|
224
276
|
* Execute a Helm command by invoking the Helm CLI client with the given kubeconfig and arguments to `helm`.
|
|
225
277
|
* The provided kubeconfig is written to a temporary file and cleaned up after, and passed to `helm` with
|
|
@@ -230,6 +282,7 @@ export async function executeHelmCommand(kubeconfigPayload: string, args: string
|
|
|
230
282
|
const kubeconfigPath = pathJoin(tmpdir(), randomBytes(20).toString('hex'))
|
|
231
283
|
logger.debug(`Write temporary kubeconfig in ${kubeconfigPath}`)
|
|
232
284
|
await writeFile(kubeconfigPath, kubeconfigPayload, { mode: 0o600 })
|
|
285
|
+
// eslint-disable-next-line no-param-reassign
|
|
233
286
|
args = args.concat(['--kubeconfig', kubeconfigPath])
|
|
234
287
|
|
|
235
288
|
// Use try-finally to remove the temp kubeconfig at the end
|
|
@@ -241,7 +294,7 @@ export async function executeHelmCommand(kubeconfigPayload: string, args: string
|
|
|
241
294
|
// \todo output something from Helm result?
|
|
242
295
|
} catch (error) {
|
|
243
296
|
const errMessage = error instanceof Error ? error.message : String(error)
|
|
244
|
-
throw new Error(`Failed to execute 'helm': ${errMessage}. You need to have Helm v3 installed to deploy a game server with metaplay-auth
|
|
297
|
+
throw new Error(`Failed to execute 'helm': ${errMessage}. You need to have Helm v3 installed to deploy a game server with metaplay-auth.`, { cause: error })
|
|
245
298
|
}
|
|
246
299
|
|
|
247
300
|
// Throw on Helm non-success exit code
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "1.9.
|
|
1
|
+
export const PACKAGE_VERSION = "1.9.3-next.878"
|
|
Binary file
|