@highstate/k8s 0.19.1 → 0.21.1
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/dist/chunk-23vn2rdc.js +11 -0
- package/dist/chunk-2pfx13ay.js +11 -0
- package/dist/chunk-46ntav0c.js +299 -0
- package/dist/chunk-556pc9e6.js +155 -0
- package/dist/chunk-7kgjgcft.js +170 -0
- package/dist/{chunk-LGHFSXNT.js → chunk-9hs97f1q.js} +23 -17
- package/dist/chunk-aame3x1b.js +11 -0
- package/dist/chunk-b05q6fm2.js +37 -0
- package/dist/chunk-bmvc9d2d.js +11 -0
- package/dist/chunk-de82bbp2.js +7 -0
- package/dist/chunk-facs31cb.js +624 -0
- package/dist/chunk-h1b79v66.js +1425 -0
- package/dist/chunk-k4w9zpn5.js +215 -0
- package/dist/chunk-pqc6w52f.js +352 -0
- package/dist/chunk-qyshvz32.js +176 -0
- package/dist/chunk-tpfyj6fe.js +199 -0
- package/dist/chunk-z6bmpnm7.js +180 -0
- package/dist/highstate.manifest.json +3 -2
- package/dist/impl/dynamic-endpoint-resolver.js +91 -0
- package/dist/impl/gateway-route.js +226 -166
- package/dist/impl/tls-certificate.js +31 -31
- package/dist/index.js +293 -166
- package/dist/units/cert-manager/index.js +19 -14
- package/dist/units/cluster-patch/index.js +14 -13
- package/dist/units/dns01-issuer/index.js +82 -42
- package/dist/units/existing-cluster/index.js +59 -26
- package/dist/units/gateway-api/index.js +15 -16
- package/dist/units/reduced-access-cluster/index.js +32 -36
- package/package.json +23 -21
- package/src/cluster.ts +12 -8
- package/src/config-map.ts +15 -5
- package/src/container.ts +4 -2
- package/src/cron-job.ts +51 -5
- package/src/deployment.ts +49 -18
- package/src/gateway/backend.ts +3 -3
- package/src/gateway/gateway.ts +12 -56
- package/src/helm.ts +354 -22
- package/src/impl/dynamic-endpoint-resolver.ts +109 -0
- package/src/impl/gateway-route.ts +231 -57
- package/src/impl/tls-certificate.ts +8 -3
- package/src/index.ts +1 -0
- package/src/job.ts +38 -6
- package/src/kubectl.ts +166 -0
- package/src/namespace.ts +47 -3
- package/src/network-policy.ts +1 -1
- package/src/pvc.ts +12 -2
- package/src/rbac.ts +28 -5
- package/src/scripting/bundle.ts +21 -98
- package/src/scripting/environment.ts +4 -10
- package/src/secret.ts +15 -5
- package/src/service.ts +28 -6
- package/src/shared.ts +31 -3
- package/src/stateful-set.ts +49 -18
- package/src/tls.ts +31 -5
- package/src/units/cluster-patch/index.ts +5 -5
- package/src/units/dns01-issuer/index.ts +56 -12
- package/src/units/existing-cluster/index.ts +36 -15
- package/src/units/reduced-access-cluster/index.ts +6 -3
- package/src/worker.ts +4 -2
- package/src/workload.ts +474 -217
- package/LICENSE +0 -21
- package/dist/chunk-4G6LLC2X.js +0 -240
- package/dist/chunk-4G6LLC2X.js.map +0 -1
- package/dist/chunk-BR2CLUUD.js +0 -230
- package/dist/chunk-BR2CLUUD.js.map +0 -1
- package/dist/chunk-DCUMJSO6.js +0 -427
- package/dist/chunk-DCUMJSO6.js.map +0 -1
- package/dist/chunk-FE4SHRAJ.js +0 -286
- package/dist/chunk-FE4SHRAJ.js.map +0 -1
- package/dist/chunk-HH2JJELM.js +0 -13
- package/dist/chunk-HH2JJELM.js.map +0 -1
- package/dist/chunk-KMLRI5UZ.js +0 -155
- package/dist/chunk-KMLRI5UZ.js.map +0 -1
- package/dist/chunk-LGHFSXNT.js.map +0 -1
- package/dist/chunk-MIC2BHGS.js +0 -301
- package/dist/chunk-MIC2BHGS.js.map +0 -1
- package/dist/chunk-OBDQONMV.js +0 -401
- package/dist/chunk-OBDQONMV.js.map +0 -1
- package/dist/chunk-P2VOUU7E.js +0 -1626
- package/dist/chunk-P2VOUU7E.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js +0 -9
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-RVB4WWZZ.js +0 -267
- package/dist/chunk-RVB4WWZZ.js.map +0 -1
- package/dist/chunk-TWBMG6TD.js +0 -315
- package/dist/chunk-TWBMG6TD.js.map +0 -1
- package/dist/chunk-VCXWCZ43.js +0 -279
- package/dist/chunk-VCXWCZ43.js.map +0 -1
- package/dist/chunk-YIJUVPU2.js +0 -297
- package/dist/chunk-YIJUVPU2.js.map +0 -1
- package/dist/cron-job-NX4HD4FI.js +0 -8
- package/dist/cron-job-NX4HD4FI.js.map +0 -1
- package/dist/deployment-O2LJ5WR5.js +0 -8
- package/dist/deployment-O2LJ5WR5.js.map +0 -1
- package/dist/impl/gateway-route.js.map +0 -1
- package/dist/impl/tls-certificate.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/job-SYME6Y43.js +0 -8
- package/dist/job-SYME6Y43.js.map +0 -1
- package/dist/stateful-set-VJYKTQ72.js +0 -8
- package/dist/stateful-set-VJYKTQ72.js.map +0 -1
- package/dist/units/cert-manager/index.js.map +0 -1
- package/dist/units/cluster-patch/index.js.map +0 -1
- package/dist/units/dns01-issuer/index.js.map +0 -1
- package/dist/units/existing-cluster/index.js.map +0 -1
- package/dist/units/gateway-api/index.js.map +0 -1
- package/dist/units/reduced-access-cluster/index.js.map +0 -1
package/src/helm.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import type { UnitTerminal } from "@highstate/contract"
|
|
2
|
+
import type { common } from "@highstate/library"
|
|
3
|
+
import type { DistributedOmit } from "type-fest"
|
|
2
4
|
import type { Namespace } from "./namespace"
|
|
3
5
|
import type { Workload, WorkloadTerminalArgs } from "./workload"
|
|
4
6
|
import { mkdir, readFile, unlink } from "node:fs/promises"
|
|
5
7
|
import { resolve } from "node:path"
|
|
6
|
-
import { AccessPointRoute, type AccessPointRouteArgs } from "@highstate/common"
|
|
7
8
|
import {
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "@highstate/pulumi"
|
|
9
|
+
AccessPointRoute,
|
|
10
|
+
type AccessPointRouteArgs,
|
|
11
|
+
type GatewayHttpRuleArgs,
|
|
12
|
+
type GatewayRuleArgs,
|
|
13
|
+
} from "@highstate/common"
|
|
14
|
+
import { type InputRecord, normalize, normalizeInputs, toPromise } from "@highstate/pulumi"
|
|
14
15
|
import { local } from "@pulumi/command"
|
|
15
16
|
import { apps, core, helm, type types } from "@pulumi/kubernetes"
|
|
16
17
|
import {
|
|
@@ -30,6 +31,112 @@ import { createServiceSpec, Service, type ServiceArgs } from "./service"
|
|
|
30
31
|
import { getNamespaceName, getProvider, type NamespaceLike } from "./shared"
|
|
31
32
|
import { StatefulSet } from "./stateful-set"
|
|
32
33
|
|
|
34
|
+
type ChartRouteBaseArgs = DistributedOmit<
|
|
35
|
+
AccessPointRouteArgs,
|
|
36
|
+
"accessPoint" | "backend" | "backends" | "rules" | "type" | "path" | "paths"
|
|
37
|
+
> & {
|
|
38
|
+
/**
|
|
39
|
+
* The name of the service to route to.
|
|
40
|
+
*
|
|
41
|
+
* If not specified, it will route to the main service of the chart.
|
|
42
|
+
*/
|
|
43
|
+
serviceName?: string
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The service port to route to.
|
|
47
|
+
*
|
|
48
|
+
* Can be either a numeric port or a named port from the matched service.
|
|
49
|
+
*
|
|
50
|
+
* If not specified, it first falls back to the servicePort of the chart,
|
|
51
|
+
* then to the first port of the matched service.
|
|
52
|
+
*/
|
|
53
|
+
servicePort?: Input<number | string>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The access point to use for the route.
|
|
57
|
+
*
|
|
58
|
+
* If not specified, it will use the access point provided at the chart level, or throw an error if the chart level access point is not provided.
|
|
59
|
+
*/
|
|
60
|
+
accessPoint?: Input<common.AccessPoint>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The protocol type for the route.
|
|
64
|
+
*/
|
|
65
|
+
type: "http" | "tcp" | "udp"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type ChartHttpGatewayRuleArgs = DistributedOmit<GatewayHttpRuleArgs, "backend" | "backends"> & {
|
|
69
|
+
/**
|
|
70
|
+
* The name of the service to route to.
|
|
71
|
+
*
|
|
72
|
+
* If not specified, it first falls back to the serviceName of the route, then to the main service of the chart.
|
|
73
|
+
*/
|
|
74
|
+
serviceName?: string
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The service port to route to.
|
|
78
|
+
*
|
|
79
|
+
* Can be either a numeric port or a named port from the matched service.
|
|
80
|
+
*
|
|
81
|
+
* If not specified, it first falls back to the servicePort of the route,
|
|
82
|
+
* then to the servicePort of the chart,
|
|
83
|
+
* then to the first port of the matched service.
|
|
84
|
+
*/
|
|
85
|
+
servicePort?: Input<number | string>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type ChartTcpUdpGatewayRuleArgs = DistributedOmit<GatewayRuleArgs, "backend" | "backends"> & {
|
|
89
|
+
/**
|
|
90
|
+
* The name of the service to route to.
|
|
91
|
+
*
|
|
92
|
+
* If not specified, it first falls back to the serviceName of the route, then to the main service of the chart.
|
|
93
|
+
*/
|
|
94
|
+
serviceName?: string
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The service port to route to.
|
|
98
|
+
*
|
|
99
|
+
* Can be either a numeric port or a named port from the matched service.
|
|
100
|
+
*
|
|
101
|
+
* If not specified, it first falls back to the servicePort of the route,
|
|
102
|
+
* then to the servicePort of the chart,
|
|
103
|
+
* then to the first port of the matched service.
|
|
104
|
+
*/
|
|
105
|
+
servicePort?: Input<number | string>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type ChartGatewayRuleArgs = ChartHttpGatewayRuleArgs | ChartTcpUdpGatewayRuleArgs
|
|
109
|
+
|
|
110
|
+
export type ChartRouteArgs = ChartRouteBaseArgs &
|
|
111
|
+
(
|
|
112
|
+
| {
|
|
113
|
+
type: "http"
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The path to match for the `default` rule of the listener.
|
|
117
|
+
*/
|
|
118
|
+
path?: Input<string>
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The paths to match for the `default` rule of the listener.
|
|
122
|
+
*/
|
|
123
|
+
paths?: Input<string[]>
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The rules of the route.
|
|
127
|
+
*/
|
|
128
|
+
rules?: InputRecord<ChartHttpGatewayRuleArgs>
|
|
129
|
+
}
|
|
130
|
+
| {
|
|
131
|
+
type: "tcp" | "udp"
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The rules of the route.
|
|
135
|
+
*/
|
|
136
|
+
rules?: InputRecord<ChartTcpUdpGatewayRuleArgs>
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
33
140
|
export type ChartArgs = Omit<
|
|
34
141
|
helm.v4.ChartArgs,
|
|
35
142
|
"chart" | "version" | "repositoryOpts" | "namespace"
|
|
@@ -46,6 +153,15 @@ export type ChartArgs = Omit<
|
|
|
46
153
|
*/
|
|
47
154
|
serviceName?: string
|
|
48
155
|
|
|
156
|
+
/**
|
|
157
|
+
* The service port to route to by default.
|
|
158
|
+
*
|
|
159
|
+
* Can be either a numeric port or a named port from the matched service.
|
|
160
|
+
*
|
|
161
|
+
* Can be overridden by route.servicePort and rule.servicePort.
|
|
162
|
+
*/
|
|
163
|
+
servicePort?: Input<number | string>
|
|
164
|
+
|
|
49
165
|
/**
|
|
50
166
|
* The extra args to pass to the main service of the chart.
|
|
51
167
|
*
|
|
@@ -68,12 +184,17 @@ export type ChartArgs = Omit<
|
|
|
68
184
|
/**
|
|
69
185
|
* The configuration for the access point route to create.
|
|
70
186
|
*/
|
|
71
|
-
route?: Input<
|
|
187
|
+
route?: Input<ChartRouteArgs>
|
|
72
188
|
|
|
73
189
|
/**
|
|
74
190
|
* The configuration for the access point routes to create.
|
|
75
191
|
*/
|
|
76
|
-
routes?:
|
|
192
|
+
routes?: InputRecord<ChartRouteArgs>
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The access point to use for the routes.
|
|
196
|
+
*/
|
|
197
|
+
accessPoint?: Input<common.AccessPoint>
|
|
77
198
|
|
|
78
199
|
/**
|
|
79
200
|
* The network policy to apply to the chart.
|
|
@@ -158,7 +279,9 @@ export class Chart extends ComponentResource {
|
|
|
158
279
|
...resourceArgs.props,
|
|
159
280
|
spec: {
|
|
160
281
|
...spec,
|
|
161
|
-
...
|
|
282
|
+
...(serviceSpec.ports?.length !== 0
|
|
283
|
+
? serviceSpec
|
|
284
|
+
: omit(serviceSpec, ["ports"])),
|
|
162
285
|
},
|
|
163
286
|
},
|
|
164
287
|
opts: resourceArgs.opts,
|
|
@@ -172,23 +295,101 @@ export class Chart extends ComponentResource {
|
|
|
172
295
|
)
|
|
173
296
|
})
|
|
174
297
|
|
|
175
|
-
this.routes = output(
|
|
298
|
+
this.routes = output(
|
|
299
|
+
normalizeInputs(
|
|
300
|
+
args.route ? { name: "default", route: args.route } : undefined,
|
|
301
|
+
args.routes
|
|
302
|
+
? Object.entries(args.routes).map(([name, route]) => ({ name, route }))
|
|
303
|
+
: undefined,
|
|
304
|
+
),
|
|
305
|
+
).apply(async routes => {
|
|
176
306
|
if (routes.length === 0) {
|
|
177
307
|
return []
|
|
178
308
|
}
|
|
179
309
|
|
|
180
310
|
return await Promise.all(
|
|
181
|
-
routes.map(async route => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
311
|
+
routes.map(async ({ name: routeName, route }) => {
|
|
312
|
+
const { serviceName: _serviceName, rules: _rules, ...baseRoute } = route
|
|
313
|
+
const accessPoint = route.accessPoint ?? args.accessPoint
|
|
314
|
+
|
|
315
|
+
if (!accessPoint) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Access point is required for chart route "${name}-${routeName}". Set it on the route or on Chart args.accessPoint`,
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const namespace = await toPromise(args.namespace)
|
|
322
|
+
|
|
323
|
+
const routeRules = (await toPromise(route.rules)) as Record<string, ChartGatewayRuleArgs>
|
|
324
|
+
const routeRuleValues = Object.values(routeRules ?? {})
|
|
325
|
+
|
|
326
|
+
const defaultServiceName = route.serviceName ?? args.serviceName ?? name
|
|
327
|
+
const defaultServicePort = route.servicePort ?? args.servicePort
|
|
328
|
+
const needsDefaultBackend = routeRuleValues.length === 0
|
|
329
|
+
|
|
330
|
+
const defaultService = needsDefaultBackend
|
|
331
|
+
? await this.getService(defaultServiceName)
|
|
332
|
+
: undefined
|
|
333
|
+
|
|
334
|
+
const defaultServiceEndpoints =
|
|
335
|
+
needsDefaultBackend && defaultService
|
|
336
|
+
? await this.resolveServiceEndpoints(
|
|
337
|
+
defaultService,
|
|
338
|
+
defaultServiceName,
|
|
339
|
+
defaultServicePort,
|
|
340
|
+
`${name}-${routeName}`,
|
|
341
|
+
)
|
|
342
|
+
: undefined
|
|
343
|
+
|
|
344
|
+
const resolvedRules = routeRules
|
|
345
|
+
? await Promise.all(
|
|
346
|
+
Object.entries(routeRules).map(async ([ruleName, rule]) => {
|
|
347
|
+
const ruleServiceName = rule.serviceName ?? defaultServiceName
|
|
348
|
+
const ruleService = await this.getService(ruleServiceName)
|
|
349
|
+
const ruleServicePort = rule.servicePort ?? route.servicePort ?? args.servicePort
|
|
350
|
+
const ruleServiceEndpoints = await this.resolveServiceEndpoints(
|
|
351
|
+
ruleService,
|
|
352
|
+
ruleServiceName,
|
|
353
|
+
ruleServicePort,
|
|
354
|
+
`${name}-${routeName}:${ruleName}`,
|
|
355
|
+
)
|
|
186
356
|
|
|
187
|
-
|
|
357
|
+
return [
|
|
358
|
+
ruleName,
|
|
359
|
+
[
|
|
360
|
+
{
|
|
361
|
+
...omit(rule, ["serviceName", "servicePort"]),
|
|
362
|
+
backend: {
|
|
363
|
+
endpoints: ruleServiceEndpoints,
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
] as const
|
|
368
|
+
}),
|
|
369
|
+
)
|
|
370
|
+
: undefined
|
|
371
|
+
|
|
372
|
+
const resolvedRulesInput = resolvedRules
|
|
373
|
+
? (Object.fromEntries(resolvedRules) as unknown as InputRecord<GatewayRuleArgs[]>)
|
|
374
|
+
: undefined
|
|
188
375
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
376
|
+
return new AccessPointRoute(
|
|
377
|
+
`${name}-${routeName}`,
|
|
378
|
+
{
|
|
379
|
+
...baseRoute,
|
|
380
|
+
accessPoint,
|
|
381
|
+
...(defaultService
|
|
382
|
+
? {
|
|
383
|
+
backend: {
|
|
384
|
+
endpoints: defaultServiceEndpoints,
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
: {}),
|
|
388
|
+
rules: resolvedRulesInput,
|
|
389
|
+
metadata: {
|
|
390
|
+
...(route.metadata ?? {}),
|
|
391
|
+
"k8s.namespace": namespace,
|
|
392
|
+
},
|
|
192
393
|
},
|
|
193
394
|
{ ...opts, parent: this },
|
|
194
395
|
)
|
|
@@ -254,19 +455,102 @@ export class Chart extends ComponentResource {
|
|
|
254
455
|
|
|
255
456
|
private set service(_value: never) {}
|
|
256
457
|
|
|
257
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: for pulumi which for some reason tries to copy all properties
|
|
258
458
|
private set terminals(_value: never) {}
|
|
259
459
|
|
|
260
460
|
get service(): Output<Service> {
|
|
261
461
|
return this.getServiceOutput(undefined)
|
|
262
462
|
}
|
|
263
463
|
|
|
464
|
+
get deployment(): Output<Deployment> {
|
|
465
|
+
return this.getDeploymentOutput(this.name)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
get statefulSet(): Output<StatefulSet> {
|
|
469
|
+
return this.getStatefulSetOutput(this.name)
|
|
470
|
+
}
|
|
471
|
+
|
|
264
472
|
get terminals(): Output<UnitTerminal[]> {
|
|
265
|
-
return this.workloads.apply(workloads =>
|
|
473
|
+
return this.workloads.apply(workloads => {
|
|
474
|
+
const terminalsByWorkload = workloads.map(workload =>
|
|
475
|
+
output({ terminals: workload.terminals, workloadName: workload.metadata.name }),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
return output(terminalsByWorkload).apply(workloadTerminals => {
|
|
479
|
+
const hasMultipleWorkloads = workloadTerminals.length > 1
|
|
480
|
+
|
|
481
|
+
return workloadTerminals.flatMap(({ terminals, workloadName }) => {
|
|
482
|
+
if (!hasMultipleWorkloads) {
|
|
483
|
+
return terminals
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return terminals.map(terminal => ({
|
|
487
|
+
...terminal,
|
|
488
|
+
meta: {
|
|
489
|
+
...terminal.meta,
|
|
490
|
+
title: `${terminal.meta.title} | ${workloadName}`,
|
|
491
|
+
},
|
|
492
|
+
}))
|
|
493
|
+
})
|
|
494
|
+
})
|
|
495
|
+
})
|
|
266
496
|
}
|
|
267
497
|
|
|
268
498
|
private readonly services = new Map<string, Service>()
|
|
269
499
|
|
|
500
|
+
private async resolveServiceEndpoints(
|
|
501
|
+
service: Service,
|
|
502
|
+
serviceName: string,
|
|
503
|
+
servicePort: Input<number | string> | undefined,
|
|
504
|
+
routeName: string,
|
|
505
|
+
) {
|
|
506
|
+
const endpoints = await toPromise(service.endpoints)
|
|
507
|
+
const servicePorts = await toPromise(service.spec.ports)
|
|
508
|
+
|
|
509
|
+
if (endpoints.length === 0) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
`No endpoints found for service "${serviceName}" in chart route "${routeName}"`,
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let resolvedServicePort: number | undefined
|
|
516
|
+
|
|
517
|
+
if (servicePort != null) {
|
|
518
|
+
const requestedServicePort = await toPromise(servicePort)
|
|
519
|
+
|
|
520
|
+
if (typeof requestedServicePort === "string") {
|
|
521
|
+
const namedPort = servicePorts?.find(port => port.name === requestedServicePort)
|
|
522
|
+
|
|
523
|
+
if (!namedPort) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`Named port "${requestedServicePort}" not found for service "${serviceName}" in chart route "${routeName}"`,
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
resolvedServicePort = namedPort.port
|
|
530
|
+
} else {
|
|
531
|
+
resolvedServicePort = requestedServicePort
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
resolvedServicePort = endpoints[0]?.port
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (resolvedServicePort == null) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`Unable to resolve service port for service "${serviceName}" in chart route "${routeName}"`,
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const filteredEndpoints = endpoints.filter(endpoint => endpoint.port === resolvedServicePort)
|
|
544
|
+
|
|
545
|
+
if (filteredEndpoints.length === 0) {
|
|
546
|
+
throw new Error(
|
|
547
|
+
`No endpoints with port ${resolvedServicePort} found for service "${serviceName}" in chart route "${routeName}"`,
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return filteredEndpoints
|
|
552
|
+
}
|
|
553
|
+
|
|
270
554
|
getServiceOutput(name: string | undefined): Output<Service> {
|
|
271
555
|
return output({ args: this.args, chart: this.chart }).apply(({ args, chart }) => {
|
|
272
556
|
const resolvedName = name ?? args.serviceName ?? this.name
|
|
@@ -289,9 +573,57 @@ export class Chart extends ComponentResource {
|
|
|
289
573
|
})
|
|
290
574
|
}
|
|
291
575
|
|
|
576
|
+
getWorkloadOutput(name: string): Output<Workload> {
|
|
577
|
+
return this.workloads.apply(async workloads => {
|
|
578
|
+
const workloadsWithNames = await toPromise(
|
|
579
|
+
workloads.map(workload => output({ workload, name: workload.metadata.name })),
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
const item = workloadsWithNames.find(w => w.name === name)
|
|
583
|
+
|
|
584
|
+
if (!item) {
|
|
585
|
+
throw new Error(`Workload with name '${name}' not found in the chart workloads`)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return item.workload
|
|
589
|
+
})
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
getDeploymentOutput(name: string): Output<Deployment> {
|
|
593
|
+
return this.getWorkloadOutput(name).apply(workload => {
|
|
594
|
+
if (workload instanceof Deployment) {
|
|
595
|
+
return workload
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
throw new Error(`Workload with name '${name}' is not a Deployment`)
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
getStatefulSetOutput(name: string): Output<StatefulSet> {
|
|
603
|
+
return this.getWorkloadOutput(name).apply(workload => {
|
|
604
|
+
if (workload instanceof StatefulSet) {
|
|
605
|
+
return workload
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
throw new Error(`Workload with name '${name}' is not a StatefulSet`)
|
|
609
|
+
})
|
|
610
|
+
}
|
|
611
|
+
|
|
292
612
|
getService(name?: string): Promise<Service> {
|
|
293
613
|
return toPromise(this.getServiceOutput(name))
|
|
294
614
|
}
|
|
615
|
+
|
|
616
|
+
getWorkload(name: string): Promise<Workload> {
|
|
617
|
+
return toPromise(this.getWorkloadOutput(name))
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
getDeployment(name: string): Promise<Deployment> {
|
|
621
|
+
return toPromise(this.getDeploymentOutput(name))
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
getStatefulSet(name: string): Promise<StatefulSet> {
|
|
625
|
+
return toPromise(this.getStatefulSetOutput(name))
|
|
626
|
+
}
|
|
295
627
|
}
|
|
296
628
|
|
|
297
629
|
export type RenderedChartArgs = {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { crc32 } from "node:zlib"
|
|
2
|
+
import {
|
|
3
|
+
dynamicEndpointResolverMediator,
|
|
4
|
+
endpointToString,
|
|
5
|
+
MaterializedFile,
|
|
6
|
+
parseEndpoint,
|
|
7
|
+
rebaseEndpoint,
|
|
8
|
+
} from "@highstate/common"
|
|
9
|
+
import { z } from "@highstate/contract"
|
|
10
|
+
import { k8s } from "@highstate/library"
|
|
11
|
+
import { getUnitStateId } from "@highstate/pulumi"
|
|
12
|
+
import spawn, { type Subprocess, SubprocessError } from "nano-spawn"
|
|
13
|
+
import { isEndpointFromCluster } from "../service"
|
|
14
|
+
|
|
15
|
+
export const resolveDynamicEndpoint = dynamicEndpointResolverMediator.implement(
|
|
16
|
+
z.object({ cluster: k8s.clusterEntity.schema }),
|
|
17
|
+
async ({ endpoint }, { cluster }) => {
|
|
18
|
+
if (!isEndpointFromCluster(endpoint, cluster)) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Endpoint "${endpointToString(endpoint)}" is not from cluster "${cluster.id}"`,
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { name, namespace } = endpoint.metadata["k8s.service"]
|
|
25
|
+
|
|
26
|
+
const localPort = getStablePort(`${cluster.id}:${namespace}:${name}`)
|
|
27
|
+
|
|
28
|
+
let subprocess: Subprocess
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
endpoint: rebaseEndpoint(endpoint, parseEndpoint(`localhost:${localPort}`)),
|
|
32
|
+
|
|
33
|
+
setup: async () => {
|
|
34
|
+
console.log(
|
|
35
|
+
`[port-forward] starting port-forward for service "${name}" in namespace "${namespace}" on local port ${localPort}`,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const config = MaterializedFile.for(cluster.kubeconfig)
|
|
39
|
+
await using _ = await config.open()
|
|
40
|
+
|
|
41
|
+
subprocess = spawn("kubectl", [
|
|
42
|
+
"--kubeconfig",
|
|
43
|
+
config.path,
|
|
44
|
+
"port-forward",
|
|
45
|
+
`service/${name}`,
|
|
46
|
+
"-n",
|
|
47
|
+
namespace,
|
|
48
|
+
`${localPort}:${endpoint.port}`,
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
// catch the error when the process is killed to prevent unhandled promise rejection
|
|
52
|
+
subprocess.catch(err => {
|
|
53
|
+
if (err instanceof SubprocessError && err.signalName === "SIGTERM") {
|
|
54
|
+
// correct termination
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw err
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const nodeProcess = await subprocess.nodeChildProcess
|
|
62
|
+
|
|
63
|
+
await new Promise<void>((resolve, reject) => {
|
|
64
|
+
nodeProcess.stdout?.once("data", (data: Buffer) => {
|
|
65
|
+
const output = data.toString()
|
|
66
|
+
|
|
67
|
+
if (output.includes("Forwarding from")) {
|
|
68
|
+
resolve()
|
|
69
|
+
} else {
|
|
70
|
+
reject(new Error(`Failed to start port-forward: ${output}`))
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
nodeProcess.stderr?.once("data", (data: Buffer) => {
|
|
75
|
+
reject(new Error(`Failed to start port-forward: ${data.toString()}`))
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
console.log(
|
|
80
|
+
`[port-forward] port-forward is ready for service "${name}" in namespace "${namespace}" on local port ${localPort}`,
|
|
81
|
+
)
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
dispose: async () => {
|
|
85
|
+
console.log(
|
|
86
|
+
`[port-forward] stopping port-forward for service "${name}" in namespace "${namespace}" on local port ${localPort}`,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const nodeProcess = await subprocess.nodeChildProcess
|
|
90
|
+
nodeProcess.kill()
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Return a stable port number based on the given id.
|
|
98
|
+
* This is important because Pulumi stores the resolved endpoint in the state,
|
|
99
|
+
* and uses it in destroy operations.
|
|
100
|
+
*/
|
|
101
|
+
function getStablePort(id: string): number {
|
|
102
|
+
// also add state ID to ensure different ports for the same service in different states which may run in parallel
|
|
103
|
+
const hash = crc32(`${getUnitStateId()}:${id}`)
|
|
104
|
+
|
|
105
|
+
const minPort = 30000
|
|
106
|
+
const maxPort = 60000
|
|
107
|
+
|
|
108
|
+
return (Math.abs(hash) % (maxPort - minPort)) + minPort
|
|
109
|
+
}
|