@metaplay/metaplay-auth 1.5.0 → 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 +58 -40
- package/dist/index.cjs +125 -125
- package/eslint.config.js +3 -0
- package/index.ts +717 -439
- package/package.json +18 -15
- package/prettier.config.js +3 -0
- package/src/auth.ts +115 -80
- package/src/buildCommand.ts +267 -0
- package/src/config.ts +12 -0
- package/src/deployment.ts +182 -59
- package/src/logging.ts +4 -4
- package/src/secret_store.ts +10 -7
- package/src/stackapi.ts +2 -33
- package/src/targetenvironment.ts +61 -57
- package/src/utils.ts +120 -29
- package/src/version.ts +1 -1
package/src/targetenvironment.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { dump } from 'js-yaml'
|
|
|
3
3
|
import { getUserinfo } from './auth.js'
|
|
4
4
|
import { isRunningUnderNpx } from './utils.js'
|
|
5
5
|
import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr'
|
|
6
|
-
import { defaultStackApiBaseUrl } from './stackapi.js'
|
|
7
6
|
|
|
8
7
|
interface AwsCredentialsResponse {
|
|
9
8
|
AccessKeyId: string
|
|
@@ -58,6 +57,7 @@ interface KubeConfigUser {
|
|
|
58
57
|
command: string
|
|
59
58
|
args: string[]
|
|
60
59
|
apiVersion: string
|
|
60
|
+
interactiveMode: string
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -85,53 +85,48 @@ export interface DockerCredentials {
|
|
|
85
85
|
|
|
86
86
|
export class TargetEnvironment {
|
|
87
87
|
private readonly accessToken: string
|
|
88
|
-
private readonly
|
|
88
|
+
private readonly humanId: string
|
|
89
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
90
|
|
|
95
|
-
constructor
|
|
91
|
+
constructor(accessToken: string, humanId: string, stackApiBaseUrl: string) {
|
|
96
92
|
this.accessToken = accessToken
|
|
97
|
-
this.
|
|
93
|
+
this.humanId = humanId
|
|
98
94
|
this.stackApiBaseUrl = stackApiBaseUrl
|
|
99
|
-
this.environmentApiBaseUrl = environmentApiBaseUrl
|
|
100
|
-
this.organization = organization
|
|
101
|
-
this.project = project
|
|
102
|
-
this.environment = environment
|
|
103
95
|
}
|
|
104
96
|
|
|
105
|
-
async fetchJson<T>
|
|
97
|
+
async fetchJson<T>(url: string, method: string): Promise<T> {
|
|
106
98
|
const response = await fetch(url, {
|
|
107
99
|
method,
|
|
108
100
|
headers: {
|
|
109
101
|
Authorization: `Bearer ${this.accessToken}`,
|
|
110
|
-
'Content-Type': 'application/json'
|
|
111
|
-
}
|
|
102
|
+
'Content-Type': 'application/json',
|
|
103
|
+
},
|
|
112
104
|
})
|
|
113
105
|
|
|
114
106
|
if (response.status !== 200) {
|
|
115
107
|
throw new Error(`Failed to fetch ${method} ${url}: ${response.statusText}, response code=${response.status}`)
|
|
116
108
|
}
|
|
117
109
|
|
|
118
|
-
return await response.json() as T
|
|
110
|
+
return (await response.json()) as T
|
|
119
111
|
}
|
|
120
112
|
|
|
121
|
-
async fetchText
|
|
113
|
+
async fetchText(url: string, method: string): Promise<string> {
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations
|
|
122
115
|
let response
|
|
123
116
|
try {
|
|
124
117
|
response = await fetch(url, {
|
|
125
118
|
method,
|
|
126
119
|
headers: {
|
|
127
120
|
Authorization: `Bearer ${this.accessToken}`,
|
|
128
|
-
'Content-Type': 'application/json'
|
|
129
|
-
}
|
|
121
|
+
'Content-Type': 'application/json',
|
|
122
|
+
},
|
|
130
123
|
})
|
|
131
124
|
} catch (error: any) {
|
|
132
125
|
logger.error(`Failed to fetch ${method} ${url}:`, error)
|
|
133
126
|
if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
134
|
-
throw new Error(
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Failed to to fetch ${url}: SSL certificate validation failed. Is someone trying to tamper with your internet connection?`
|
|
129
|
+
)
|
|
135
130
|
}
|
|
136
131
|
throw new Error(`Failed to fetch ${url}: ${error}`)
|
|
137
132
|
}
|
|
@@ -143,14 +138,22 @@ export class TargetEnvironment {
|
|
|
143
138
|
return await response.text()
|
|
144
139
|
}
|
|
145
140
|
|
|
146
|
-
|
|
147
|
-
|
|
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`
|
|
148
147
|
logger.debug(`Fetching AWS credentials from ${url}...`)
|
|
149
148
|
return await this.fetchJson<AwsCredentialsResponse>(url, 'POST')
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
|
|
153
|
-
|
|
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`
|
|
154
157
|
logger.debug(`Getting KubeConfig from ${url}...`)
|
|
155
158
|
return await this.fetchText(url, 'POST')
|
|
156
159
|
}
|
|
@@ -160,16 +163,17 @@ export class TargetEnvironment {
|
|
|
160
163
|
* access credentials each time the kubeconfig is used.
|
|
161
164
|
* @returns The kubeconfig YAML.
|
|
162
165
|
*/
|
|
163
|
-
async
|
|
164
|
-
const url = `${this.
|
|
166
|
+
async getKubeConfigWithExecCredential(): Promise<string> {
|
|
167
|
+
const url = `${this.stackApiBaseUrl}/v0/credentials/${this.humanId}/k8s?type=execcredential`
|
|
165
168
|
logger.debug(`Getting Kubernetes KubeConfig from ${url}...`)
|
|
166
169
|
|
|
167
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
|
|
168
172
|
let kubeExecCredential: KubeExecCredential
|
|
169
173
|
try {
|
|
170
174
|
kubeExecCredential = await this.fetchJson<KubeExecCredential>(url, 'POST')
|
|
171
175
|
} catch {
|
|
172
|
-
throw new Error(
|
|
176
|
+
throw new Error(`Failed to fetch Kubernetes KubeConfig from ${url}`)
|
|
173
177
|
}
|
|
174
178
|
|
|
175
179
|
if (!kubeExecCredential.spec.cluster) {
|
|
@@ -187,15 +191,11 @@ export class TargetEnvironment {
|
|
|
187
191
|
const userinfo = await getUserinfo(this.accessToken)
|
|
188
192
|
const userId = userinfo.email
|
|
189
193
|
|
|
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
194
|
// Resolve invocation for getting the kubeconfig exec credential
|
|
194
195
|
let execCmd = 'metaplay-auth'
|
|
195
|
-
let execArgs =
|
|
196
|
-
['
|
|
197
|
-
|
|
198
|
-
.concat([envId])
|
|
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
199
|
|
|
200
200
|
// If running under npx, use npx also for getting the kube exec credential
|
|
201
201
|
if (isRunningUnderNpx()) {
|
|
@@ -203,32 +203,32 @@ export class TargetEnvironment {
|
|
|
203
203
|
execArgs = [
|
|
204
204
|
'--yes', // For NPX to silently install or update
|
|
205
205
|
'@metaplay/metaplay-auth@latest',
|
|
206
|
-
...execArgs
|
|
206
|
+
...execArgs,
|
|
207
207
|
]
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
const kubeConfig: KubeConfig = {
|
|
211
211
|
apiVersion: 'v1',
|
|
212
212
|
kind: 'Config',
|
|
213
|
-
'current-context':
|
|
213
|
+
'current-context': this.humanId, // use immutable humanId
|
|
214
214
|
clusters: [
|
|
215
215
|
{
|
|
216
216
|
cluster: {
|
|
217
217
|
'certificate-authority-data': kubeExecCredential.spec.cluster.certificateAuthorityData,
|
|
218
|
-
server: kubeExecCredential.spec.cluster.server
|
|
218
|
+
server: kubeExecCredential.spec.cluster.server,
|
|
219
219
|
},
|
|
220
|
-
name: kubeExecCredential.spec.cluster.server
|
|
221
|
-
}
|
|
220
|
+
name: kubeExecCredential.spec.cluster.server,
|
|
221
|
+
},
|
|
222
222
|
],
|
|
223
223
|
contexts: [
|
|
224
224
|
{
|
|
225
225
|
context: {
|
|
226
226
|
cluster: kubeExecCredential.spec.cluster.server,
|
|
227
227
|
namespace,
|
|
228
|
-
user: userId
|
|
228
|
+
user: userId,
|
|
229
229
|
},
|
|
230
|
-
name:
|
|
231
|
-
}
|
|
230
|
+
name: this.humanId, // use immutable humanId
|
|
231
|
+
},
|
|
232
232
|
],
|
|
233
233
|
users: [
|
|
234
234
|
{
|
|
@@ -237,10 +237,11 @@ export class TargetEnvironment {
|
|
|
237
237
|
exec: {
|
|
238
238
|
apiVersion: 'client.authentication.k8s.io/v1beta1',
|
|
239
239
|
command: execCmd,
|
|
240
|
-
args: execArgs
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
args: execArgs,
|
|
241
|
+
interactiveMode: 'Never',
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
244
245
|
],
|
|
245
246
|
}
|
|
246
247
|
|
|
@@ -248,21 +249,19 @@ export class TargetEnvironment {
|
|
|
248
249
|
return dump(kubeConfig)
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
async getKubeExecCredential
|
|
252
|
-
const url = `${this.
|
|
252
|
+
async getKubeExecCredential(): Promise<string> {
|
|
253
|
+
const url = `${this.stackApiBaseUrl}/v0/credentials/${this.humanId}/k8s?type=execcredential`
|
|
253
254
|
logger.debug(`Getting Kubernetes ExecCredential from ${url}...`)
|
|
254
255
|
return await this.fetchText(url, 'POST')
|
|
255
256
|
}
|
|
256
257
|
|
|
257
|
-
async getEnvironmentDetails
|
|
258
|
-
|
|
259
|
-
const urlSuffix = this.environmentApiBaseUrl.endsWith('.infra') ? '/environment' : ''
|
|
260
|
-
const url = this.environmentApiBaseUrl + urlSuffix
|
|
258
|
+
async getEnvironmentDetails(): Promise<EnvironmentDetails> {
|
|
259
|
+
const url = `${this.stackApiBaseUrl}/v0/deployments/${this.humanId}`
|
|
261
260
|
logger.debug(`Getting environment details from ${url}...`)
|
|
262
261
|
return await this.fetchJson<EnvironmentDetails>(url, 'GET')
|
|
263
262
|
}
|
|
264
263
|
|
|
265
|
-
async getDockerCredentials
|
|
264
|
+
async getDockerCredentials(): Promise<DockerCredentials> {
|
|
266
265
|
// Get environment info (for AWS region)
|
|
267
266
|
logger.debug('Get environment info')
|
|
268
267
|
const envInfo = await this.getEnvironmentDetails()
|
|
@@ -277,16 +276,21 @@ export class TargetEnvironment {
|
|
|
277
276
|
credentials: {
|
|
278
277
|
accessKeyId: awsCredentials.AccessKeyId,
|
|
279
278
|
secretAccessKey: awsCredentials.SecretAccessKey,
|
|
280
|
-
sessionToken: awsCredentials.SessionToken
|
|
279
|
+
sessionToken: awsCredentials.SessionToken,
|
|
281
280
|
},
|
|
282
|
-
region: envInfo.deployment.aws_region
|
|
281
|
+
region: envInfo.deployment.aws_region,
|
|
283
282
|
})
|
|
284
283
|
|
|
285
284
|
// Fetch the ECR docker authentication token
|
|
286
285
|
logger.debug('Fetch ECR login credentials from AWS')
|
|
287
286
|
const command = new GetAuthorizationTokenCommand({})
|
|
288
287
|
const response = await client.send(command)
|
|
289
|
-
if (
|
|
288
|
+
if (
|
|
289
|
+
!response.authorizationData ||
|
|
290
|
+
response.authorizationData.length === 0 ||
|
|
291
|
+
!response.authorizationData[0].authorizationToken ||
|
|
292
|
+
!response.authorizationData[0].proxyEndpoint
|
|
293
|
+
) {
|
|
290
294
|
throw new Error('Received an empty authorization token response for ECR repository')
|
|
291
295
|
}
|
|
292
296
|
|
|
@@ -301,7 +305,7 @@ export class TargetEnvironment {
|
|
|
301
305
|
return {
|
|
302
306
|
username,
|
|
303
307
|
password,
|
|
304
|
-
registryUrl
|
|
308
|
+
registryUrl,
|
|
305
309
|
} satisfies DockerCredentials
|
|
306
310
|
}
|
|
307
311
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { spawn } from 'child_process'
|
|
2
|
+
import { logger } from './logging.js'
|
|
2
3
|
import path from 'path'
|
|
4
|
+
import yaml from 'js-yaml'
|
|
5
|
+
import * as semver from 'semver'
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Checks if the given string is a fully qualified domain name (FQDN).
|
|
@@ -7,7 +10,7 @@ import path from 'path'
|
|
|
7
10
|
* @param domain The domain name to check.
|
|
8
11
|
* @returns true if the domain is a valid FQDN, false otherwise.
|
|
9
12
|
*/
|
|
10
|
-
export function isValidFQDN
|
|
13
|
+
export function isValidFQDN(domain: string): boolean {
|
|
11
14
|
const fqdnRegex = /^(?=.{1,253}$)(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,})$/
|
|
12
15
|
|
|
13
16
|
return fqdnRegex.test(domain)
|
|
@@ -19,7 +22,12 @@ export function isValidFQDN (domain: string): boolean {
|
|
|
19
22
|
* @param urlString The HTTP URL string to be split.
|
|
20
23
|
* @returns An object containing the scheme, hostname, port, and subpaths.
|
|
21
24
|
*/
|
|
22
|
-
export function splitUrlComponents
|
|
25
|
+
export function splitUrlComponents(urlString: string): {
|
|
26
|
+
scheme: string
|
|
27
|
+
hostname: string
|
|
28
|
+
port: string | null
|
|
29
|
+
subpaths: string | null
|
|
30
|
+
} {
|
|
23
31
|
try {
|
|
24
32
|
const url = new URL(urlString)
|
|
25
33
|
|
|
@@ -32,50 +40,55 @@ export function splitUrlComponents (urlString: string): { scheme: string, hostna
|
|
|
32
40
|
const subpaths = url.pathname.slice(1) ?? null
|
|
33
41
|
|
|
34
42
|
return { scheme, hostname, port, subpaths }
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
35
44
|
} catch (error) {
|
|
36
45
|
throw new Error('Invalid URL')
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
export function getGameserverAdminUrl (uri: string): string {
|
|
41
|
-
const parts = uri.split('.')
|
|
42
|
-
|
|
43
|
-
const hostname = parts[0]
|
|
44
|
-
const domain = uri.substring(hostname.length + 1)
|
|
45
|
-
|
|
46
|
-
return `${hostname}-admin.${domain}`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
49
|
export interface ExecuteCommandResult {
|
|
50
50
|
exitCode: number | null
|
|
51
51
|
stdout: any[]
|
|
52
52
|
stderr: any[]
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export interface ExecuteCommandOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Should the stdin/stdout/stderr be inherited from the parent process? If false, the outputs are buffered for reading later.
|
|
58
|
+
*/
|
|
59
|
+
inheritStdio?: boolean
|
|
60
|
+
/**
|
|
61
|
+
* Environment variables to pass in. Does not inherit parent process env. Use current process env if not specified.
|
|
62
|
+
*/
|
|
63
|
+
env?: NodeJS.ProcessEnv
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
/**
|
|
56
67
|
* Run a child process and return an awaitable Promise.
|
|
57
68
|
*
|
|
58
69
|
* @param command Name of the command/binary to execute.
|
|
59
|
-
* @param args List of
|
|
60
|
-
* @param inheritStdio Should the stdin/stdout/stderr be inherited from the parent process? If false, the outputs are buffered for reading later.
|
|
61
|
-
* @param env Environment variables to pass in. Does not inherit parent process env. Use current process env if not specified.
|
|
70
|
+
* @param args List of arguments to pass to the child process.
|
|
62
71
|
* @returns Awaitable promise with the result of the execution.
|
|
63
72
|
*/
|
|
64
|
-
export async function executeCommand
|
|
73
|
+
export async function executeCommand(
|
|
74
|
+
command: string,
|
|
75
|
+
args: string[],
|
|
76
|
+
options?: { inheritStdio?: boolean; env?: NodeJS.ProcessEnv }
|
|
77
|
+
): Promise<ExecuteCommandResult> {
|
|
65
78
|
return await new Promise((resolve, reject) => {
|
|
66
79
|
const childProcess = spawn(command, args, {
|
|
67
|
-
stdio: inheritStdio ? 'inherit' : undefined,
|
|
68
|
-
env,
|
|
80
|
+
stdio: options?.inheritStdio ? 'inherit' : undefined,
|
|
81
|
+
env: options?.env,
|
|
69
82
|
})
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
const stdout: string[] = []
|
|
84
|
+
const stderr: string[] = []
|
|
72
85
|
|
|
73
|
-
childProcess.stdout?.on('data', (data) => {
|
|
74
|
-
stdout
|
|
86
|
+
childProcess.stdout?.on('data', (data: string) => {
|
|
87
|
+
stdout.push(data)
|
|
75
88
|
})
|
|
76
89
|
|
|
77
|
-
childProcess.stderr?.on('data', (data) => {
|
|
78
|
-
stderr
|
|
90
|
+
childProcess.stderr?.on('data', (data: string) => {
|
|
91
|
+
stderr.push(data)
|
|
79
92
|
})
|
|
80
93
|
|
|
81
94
|
// If program runs to completion, resolve promise with success
|
|
@@ -84,16 +97,12 @@ export async function executeCommand (command: string, args: string[], inheritSt
|
|
|
84
97
|
})
|
|
85
98
|
|
|
86
99
|
childProcess.on('error', (err) => {
|
|
87
|
-
reject(
|
|
88
|
-
new Error(
|
|
89
|
-
`Failed to execute command '${command} ${args.join(' ')}': ${err.message}`
|
|
90
|
-
)
|
|
91
|
-
)
|
|
100
|
+
reject(new Error(`Failed to execute command '${command} ${args.join(' ')}': ${err.message}`))
|
|
92
101
|
})
|
|
93
102
|
})
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
export function isRunningUnderNpx
|
|
105
|
+
export function isRunningUnderNpx(): boolean {
|
|
97
106
|
// console.log('process.argv:', process.argv)
|
|
98
107
|
const matchBinary = (binaryPath: string, binaryName: string): boolean => {
|
|
99
108
|
const binary = path.basename(binaryPath, path.extname(binaryPath)) // drop path and extension
|
|
@@ -123,3 +132,85 @@ export function isRunningUnderNpx () {
|
|
|
123
132
|
|
|
124
133
|
return false
|
|
125
134
|
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Join a set of path segments and return result with Unix path separators ('/').
|
|
138
|
+
* @param paths Individual path segments to join
|
|
139
|
+
* @returns Joined path
|
|
140
|
+
*/
|
|
141
|
+
export function pathJoin(...paths: string[]): string {
|
|
142
|
+
return path.join(...paths).replaceAll('\\', '/')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Remove a trailing slash from a URL.
|
|
147
|
+
* @param url URL to remove the trailing slash from.
|
|
148
|
+
* @returns URL without a trailing slash.
|
|
149
|
+
*/
|
|
150
|
+
export function removeTrailingSlash(url: string): string {
|
|
151
|
+
return url.replace(/\/+$/, '')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Entry in the Helm chart repository index.
|
|
156
|
+
*/
|
|
157
|
+
interface HelmChartEntry {
|
|
158
|
+
name: string
|
|
159
|
+
version: string
|
|
160
|
+
description?: string
|
|
161
|
+
apiVersion?: string
|
|
162
|
+
appVersion?: string
|
|
163
|
+
urls: string[]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The index of the Helm chart repository. Contains an entry for each of published chart version.
|
|
168
|
+
*/
|
|
169
|
+
interface HelmChartRepoIndex {
|
|
170
|
+
apiVersion: string
|
|
171
|
+
entries: Record<string, HelmChartEntry[]>
|
|
172
|
+
generated: string
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Fetch the available versions of a specific Helm chart from a repository.
|
|
177
|
+
* @param repository URL of the Helm chart repository. No trailing slash allowed.
|
|
178
|
+
* @param chartName Name of the Helm chart.
|
|
179
|
+
* @returns List of available Helm chart versions.
|
|
180
|
+
*/
|
|
181
|
+
export async function fetchHelmChartVersions(repository: string, chartName: string): Promise<string[]> {
|
|
182
|
+
// Fetch the index.yaml file from the repository
|
|
183
|
+
logger.debug(`Fetching Helm chart versions from '${repository}'...`)
|
|
184
|
+
const response: Response = await fetch(`${repository}/index.yaml`)
|
|
185
|
+
const indexYaml: string = await response.text()
|
|
186
|
+
|
|
187
|
+
// Parse the YAML index file
|
|
188
|
+
const repoIndex: HelmChartRepoIndex = yaml.load(indexYaml) as HelmChartRepoIndex
|
|
189
|
+
|
|
190
|
+
// Grab all versions >= 0.5.0 -- older are considered legacy
|
|
191
|
+
const chartVersions = repoIndex.entries[chartName]
|
|
192
|
+
.map((entry) => entry.version)
|
|
193
|
+
.filter((version) => semver.gte(version, '0.5.0'))
|
|
194
|
+
return chartVersions
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Resolve the best matching version from a list of versions that satisfy a semver range.
|
|
199
|
+
* @param versions List of versions to resolve from.
|
|
200
|
+
* @param range Semver range to satisfy.
|
|
201
|
+
* @returns The best (latest) matching version or null if no versions satisfy the range.
|
|
202
|
+
*/
|
|
203
|
+
export function resolveBestMatchingVersion(versions: string[], range: semver.Range | null): string | null {
|
|
204
|
+
// Filter versions that satisfy the range -- range==null means 'latest' == include all versions
|
|
205
|
+
const satisfyingVersions = range ? versions.filter((version) => semver.satisfies(version, range)) : versions
|
|
206
|
+
logger.debug(`Satisfying versions: ${satisfyingVersions.join(', ')}`)
|
|
207
|
+
|
|
208
|
+
// If no versions satisfy the range, return null
|
|
209
|
+
if (satisfyingVersions.length === 0) {
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Sort the versions to get the highest one that matches the range
|
|
214
|
+
const sortedVersions = satisfyingVersions.sort(semver.rcompare)
|
|
215
|
+
return sortedVersions[0]
|
|
216
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION =
|
|
1
|
+
export const PACKAGE_VERSION = "1.6.0"
|