@highstate/k8s 0.7.2 → 0.7.3
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/{helm-wPTgVV1N.js → chunk-K4WKJ4L5.js} +89 -47
- package/dist/chunk-K4WKJ4L5.js.map +1 -0
- package/dist/{shared-Clzbl5K-.js → chunk-T5Z2M4JE.js} +21 -7
- package/dist/chunk-T5Z2M4JE.js.map +1 -0
- package/dist/highstate.manifest.json +9 -0
- package/dist/index.js +304 -154
- package/dist/index.js.map +1 -0
- package/dist/units/access-point/index.js +9 -7
- package/dist/units/access-point/index.js.map +1 -0
- package/dist/units/cert-manager/index.js +29 -29
- package/dist/units/cert-manager/index.js.map +1 -0
- package/dist/units/dns01-issuer/index.js +22 -14
- package/dist/units/dns01-issuer/index.js.map +1 -0
- package/dist/units/existing-cluster/index.js +49 -21
- package/dist/units/existing-cluster/index.js.map +1 -0
- package/package.json +15 -16
- package/src/access-point.ts +185 -0
- package/src/container.ts +271 -0
- package/src/cron-job.ts +77 -0
- package/src/deployment.ts +210 -0
- package/src/gateway/backend.ts +61 -0
- package/src/gateway/http-route.ts +139 -0
- package/src/gateway/index.ts +2 -0
- package/src/helm.ts +298 -0
- package/src/index.ts +61 -0
- package/src/job.ts +66 -0
- package/src/network-policy.ts +732 -0
- package/src/pod.ts +5 -0
- package/src/pvc.ts +178 -0
- package/src/scripting/bundle.ts +244 -0
- package/src/scripting/container.ts +44 -0
- package/src/scripting/environment.ts +79 -0
- package/src/scripting/index.ts +3 -0
- package/src/service.ts +279 -0
- package/src/shared.ts +150 -0
- package/src/stateful-set.ts +159 -0
- package/src/units/access-point/index.ts +12 -0
- package/src/units/cert-manager/index.ts +37 -0
- package/src/units/dns01-issuer/index.ts +41 -0
- package/src/units/dns01-issuer/solver.ts +23 -0
- package/src/units/existing-cluster/index.ts +107 -0
- package/src/workload.ts +150 -0
- package/assets/charts.json +0 -8
- package/dist/index.d.ts +0 -1036
@@ -0,0 +1,210 @@
|
|
1
|
+
import type { k8s } from "@highstate/library"
|
2
|
+
import type { HttpRoute } from "./gateway"
|
3
|
+
import type { Service } from "./service"
|
4
|
+
import {
|
5
|
+
output,
|
6
|
+
type ComponentResourceOptions,
|
7
|
+
Output,
|
8
|
+
type Inputs,
|
9
|
+
ComponentResource,
|
10
|
+
type InputArray,
|
11
|
+
Resource,
|
12
|
+
type Input,
|
13
|
+
type InstanceTerminal,
|
14
|
+
interpolate,
|
15
|
+
} from "@highstate/pulumi"
|
16
|
+
import { apps, types } from "@pulumi/kubernetes"
|
17
|
+
import { omit } from "remeda"
|
18
|
+
import { deepmerge } from "deepmerge-ts"
|
19
|
+
import { trimIndentation } from "@highstate/contract"
|
20
|
+
import { mapMetadata, verifyProvider } from "./shared"
|
21
|
+
import { mapContainerToRaw } from "./container"
|
22
|
+
import {
|
23
|
+
getPublicWorkloadComponents,
|
24
|
+
publicWorkloadExtraArgs,
|
25
|
+
type PublicWorkloadArgs,
|
26
|
+
} from "./workload"
|
27
|
+
|
28
|
+
export type DeploymentArgs = Omit<PublicWorkloadArgs, "patch"> & {
|
29
|
+
patch?: Input<k8s.Deployment>
|
30
|
+
|
31
|
+
/**
|
32
|
+
* The shell to use in the terminal.
|
33
|
+
*
|
34
|
+
* By default, `bash` is used.
|
35
|
+
*/
|
36
|
+
terminalShell?: string
|
37
|
+
} & Omit<Partial<types.input.apps.v1.DeploymentSpec>, "template"> & {
|
38
|
+
template?: {
|
39
|
+
metadata?: types.input.meta.v1.ObjectMeta
|
40
|
+
spec?: Partial<types.input.core.v1.PodSpec>
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
export abstract class Deployment extends ComponentResource {
|
45
|
+
protected constructor(
|
46
|
+
type: string,
|
47
|
+
name: string,
|
48
|
+
private readonly args: Inputs,
|
49
|
+
opts: ComponentResourceOptions,
|
50
|
+
|
51
|
+
/**
|
52
|
+
* The cluster where the deployment is created.
|
53
|
+
*/
|
54
|
+
readonly cluster: Output<k8s.Cluster>,
|
55
|
+
|
56
|
+
/**
|
57
|
+
* The metadata of the underlying Kubernetes deployment.
|
58
|
+
*/
|
59
|
+
readonly metadata: Output<types.output.meta.v1.ObjectMeta>,
|
60
|
+
|
61
|
+
/**
|
62
|
+
* The spec of the underlying Kubernetes deployment.
|
63
|
+
*/
|
64
|
+
readonly spec: Output<types.output.apps.v1.DeploymentSpec>,
|
65
|
+
|
66
|
+
/**
|
67
|
+
* The status of the underlying Kubernetes deployment.
|
68
|
+
*/
|
69
|
+
readonly status: Output<types.output.apps.v1.DeploymentStatus>,
|
70
|
+
|
71
|
+
private readonly _service: Output<Service | undefined>,
|
72
|
+
private readonly _httpRoute: Output<HttpRoute | undefined>,
|
73
|
+
|
74
|
+
/**
|
75
|
+
* The resources associated with the deployment.
|
76
|
+
*/
|
77
|
+
readonly resources: InputArray<Resource>,
|
78
|
+
) {
|
79
|
+
super(type, name, args, opts)
|
80
|
+
}
|
81
|
+
|
82
|
+
/**
|
83
|
+
* The Highstate deployment entity.
|
84
|
+
*/
|
85
|
+
get entity(): Output<k8s.Deployment> {
|
86
|
+
return output({
|
87
|
+
type: "k8s.deployment",
|
88
|
+
clusterInfo: this.cluster.info,
|
89
|
+
metadata: this.metadata,
|
90
|
+
spec: this.spec,
|
91
|
+
service: this._service.apply(service => service?.entity),
|
92
|
+
})
|
93
|
+
}
|
94
|
+
|
95
|
+
get optionalService(): Output<Service | undefined> {
|
96
|
+
return this._service
|
97
|
+
}
|
98
|
+
|
99
|
+
/**
|
100
|
+
* The service associated with the deployment.
|
101
|
+
*/
|
102
|
+
get service(): Output<Service> {
|
103
|
+
return this._service.apply(service => {
|
104
|
+
if (!service) {
|
105
|
+
throw new Error("The service is not available.")
|
106
|
+
}
|
107
|
+
|
108
|
+
return service
|
109
|
+
})
|
110
|
+
}
|
111
|
+
|
112
|
+
/**
|
113
|
+
* The HTTP route associated with the deployment.
|
114
|
+
*/
|
115
|
+
get httpRoute(): Output<HttpRoute> {
|
116
|
+
return this._httpRoute.apply(httpRoute => {
|
117
|
+
if (!httpRoute) {
|
118
|
+
throw new Error("The HTTP route is not available.")
|
119
|
+
}
|
120
|
+
|
121
|
+
return httpRoute
|
122
|
+
})
|
123
|
+
}
|
124
|
+
|
125
|
+
/**
|
126
|
+
* The instance terminal to interact with the deployment.
|
127
|
+
*/
|
128
|
+
get terminal(): Output<InstanceTerminal> {
|
129
|
+
return output({
|
130
|
+
name: this.metadata.name,
|
131
|
+
title: this.metadata.name,
|
132
|
+
image: "ghcr.io/exeteres/highstate/terminal-kubectl",
|
133
|
+
command: ["script", "-q", "-c", "/enter-container.sh", "/dev/null"],
|
134
|
+
files: {
|
135
|
+
"/kubeconfig": this.cluster.kubeconfig,
|
136
|
+
|
137
|
+
"/enter-container.sh": {
|
138
|
+
mode: 0o755,
|
139
|
+
content: interpolate`
|
140
|
+
#!/bin/bash
|
141
|
+
|
142
|
+
exec kubectl exec -it -n ${this.metadata.namespace} deployment/${this.metadata.name} -- ${this.args.terminalShell ?? "bash"}
|
143
|
+
`.apply(trimIndentation),
|
144
|
+
},
|
145
|
+
},
|
146
|
+
env: {
|
147
|
+
KUBECONFIG: "/kubeconfig",
|
148
|
+
},
|
149
|
+
})
|
150
|
+
}
|
151
|
+
|
152
|
+
static create(name: string, args: DeploymentArgs, opts: ComponentResourceOptions): Deployment {
|
153
|
+
return new CreatedDeployment(name, args, opts)
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
class CreatedDeployment extends Deployment {
|
158
|
+
constructor(name: string, args: DeploymentArgs, opts: ComponentResourceOptions) {
|
159
|
+
const { labels, containers, volumes, service, httpRoute } = getPublicWorkloadComponents(
|
160
|
+
name,
|
161
|
+
args,
|
162
|
+
() => this,
|
163
|
+
opts,
|
164
|
+
)
|
165
|
+
|
166
|
+
const deployment = output({ args, containers, volumes }).apply(
|
167
|
+
async ({ args, containers, volumes }) => {
|
168
|
+
await verifyProvider(opts.provider, args.cluster.info)
|
169
|
+
|
170
|
+
return new (args.patch ? apps.v1.DeploymentPatch : apps.v1.Deployment)(
|
171
|
+
name,
|
172
|
+
{
|
173
|
+
metadata: mapMetadata(args.patch?.metadata ?? args, name),
|
174
|
+
spec: deepmerge(
|
175
|
+
{
|
176
|
+
template: {
|
177
|
+
metadata: !args.patch ? { labels } : undefined,
|
178
|
+
spec: {
|
179
|
+
containers: containers.map(container => mapContainerToRaw(container, name)),
|
180
|
+
volumes,
|
181
|
+
},
|
182
|
+
},
|
183
|
+
selector: !args.patch ? { matchLabels: labels } : undefined,
|
184
|
+
},
|
185
|
+
omit(args, publicWorkloadExtraArgs),
|
186
|
+
),
|
187
|
+
},
|
188
|
+
{ parent: this, ...opts },
|
189
|
+
)
|
190
|
+
},
|
191
|
+
)
|
192
|
+
|
193
|
+
super(
|
194
|
+
"highstate:k8s:Deployment",
|
195
|
+
name,
|
196
|
+
args,
|
197
|
+
opts,
|
198
|
+
|
199
|
+
output(args.cluster),
|
200
|
+
deployment.metadata,
|
201
|
+
deployment.spec,
|
202
|
+
deployment.status,
|
203
|
+
|
204
|
+
service,
|
205
|
+
httpRoute,
|
206
|
+
|
207
|
+
[deployment],
|
208
|
+
)
|
209
|
+
}
|
210
|
+
}
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import { core } from "@pulumi/kubernetes"
|
2
|
+
import { type Input, output, Output, type Unwrap } from "@highstate/pulumi"
|
3
|
+
import { Service } from "../service"
|
4
|
+
|
5
|
+
export interface FullBackendRef {
|
6
|
+
/**
|
7
|
+
* The name of the resource being referenced.
|
8
|
+
*/
|
9
|
+
name: Input<string>
|
10
|
+
|
11
|
+
/**
|
12
|
+
* The namespace of the resource being referenced.
|
13
|
+
* May be undefined if the resource is not in a namespace.
|
14
|
+
*/
|
15
|
+
namespace?: Input<string | undefined>
|
16
|
+
|
17
|
+
/**
|
18
|
+
* The port of the resource being referenced.
|
19
|
+
*/
|
20
|
+
port: Input<number>
|
21
|
+
}
|
22
|
+
|
23
|
+
export interface ServiceBackendRef {
|
24
|
+
/**
|
25
|
+
* The name of the service being referenced.
|
26
|
+
*/
|
27
|
+
service: Input<core.v1.Service>
|
28
|
+
|
29
|
+
/**
|
30
|
+
* The port of the service being referenced.
|
31
|
+
*/
|
32
|
+
port: Input<number>
|
33
|
+
}
|
34
|
+
|
35
|
+
export type BackendRef = FullBackendRef | ServiceBackendRef | Service
|
36
|
+
|
37
|
+
export function resolveBackendRef(ref: BackendRef): Output<Unwrap<FullBackendRef>> {
|
38
|
+
if (Service.isInstance(ref)) {
|
39
|
+
return output({
|
40
|
+
name: ref.metadata.name,
|
41
|
+
namespace: ref.metadata.namespace,
|
42
|
+
port: ref.spec.ports[0].port,
|
43
|
+
})
|
44
|
+
}
|
45
|
+
|
46
|
+
if ("service" in ref) {
|
47
|
+
const service = output(ref.service)
|
48
|
+
|
49
|
+
return output({
|
50
|
+
name: service.metadata.name,
|
51
|
+
namespace: service.metadata.namespace,
|
52
|
+
port: ref.port,
|
53
|
+
})
|
54
|
+
}
|
55
|
+
|
56
|
+
return output({
|
57
|
+
name: ref.name,
|
58
|
+
namespace: ref.namespace,
|
59
|
+
port: ref.port,
|
60
|
+
})
|
61
|
+
}
|
@@ -0,0 +1,139 @@
|
|
1
|
+
import {
|
2
|
+
ComponentResource,
|
3
|
+
normalize,
|
4
|
+
output,
|
5
|
+
Output,
|
6
|
+
type ComponentResourceOptions,
|
7
|
+
type Input,
|
8
|
+
type InputArray,
|
9
|
+
} from "@highstate/pulumi"
|
10
|
+
import { gateway, types } from "@highstate/gateway-api"
|
11
|
+
import { map, pipe } from "remeda"
|
12
|
+
import { mapMetadata, type CommonArgs } from "../shared"
|
13
|
+
import { resolveBackendRef, type BackendRef } from "./backend"
|
14
|
+
|
15
|
+
export type HttpRouteArgs = Omit<CommonArgs, "namespace"> & {
|
16
|
+
/**
|
17
|
+
* The gateway to associate with the route.
|
18
|
+
*/
|
19
|
+
gateway: Input<gateway.v1.Gateway>
|
20
|
+
|
21
|
+
/**
|
22
|
+
* The alias for `hostnames: [hostname]`.
|
23
|
+
*/
|
24
|
+
hostname?: Input<string>
|
25
|
+
|
26
|
+
/**
|
27
|
+
* The rule of the route.
|
28
|
+
*/
|
29
|
+
rule?: Input<HttpRouteRuleArgs>
|
30
|
+
|
31
|
+
/**
|
32
|
+
* The rules of the route.
|
33
|
+
*/
|
34
|
+
rules?: InputArray<HttpRouteRuleArgs>
|
35
|
+
} & Omit<Partial<types.input.gateway.v1.HTTPRouteSpec>, "rules">
|
36
|
+
|
37
|
+
export type HttpRouteRuleArgs = Omit<
|
38
|
+
types.input.gateway.v1.HTTPRouteSpecRules,
|
39
|
+
"matches" | "filters" | "backendRefs"
|
40
|
+
> & {
|
41
|
+
/**
|
42
|
+
* The conditions of the rule.
|
43
|
+
* Can be specified as string to match on the path.
|
44
|
+
*/
|
45
|
+
matches?: InputArray<HttpRouteRuleMatchOptions>
|
46
|
+
|
47
|
+
/**
|
48
|
+
* The condition of the rule.
|
49
|
+
* Can be specified as string to match on the path.
|
50
|
+
*/
|
51
|
+
match?: Input<HttpRouteRuleMatchOptions>
|
52
|
+
|
53
|
+
/**
|
54
|
+
* The filters of the rule.
|
55
|
+
*/
|
56
|
+
filters?: InputArray<types.input.gateway.v1.HTTPRouteSpecRulesFilters>
|
57
|
+
|
58
|
+
/**
|
59
|
+
* The filter of the rule.
|
60
|
+
*/
|
61
|
+
filter?: Input<types.input.gateway.v1.HTTPRouteSpecRulesFilters>
|
62
|
+
|
63
|
+
/**
|
64
|
+
* The service to route to.
|
65
|
+
*/
|
66
|
+
backend?: Input<BackendRef>
|
67
|
+
}
|
68
|
+
|
69
|
+
export type HttpRouteRuleMatchOptions = types.input.gateway.v1.HTTPRouteSpecRulesMatches | string
|
70
|
+
|
71
|
+
export class HttpRoute extends ComponentResource {
|
72
|
+
/**
|
73
|
+
* The underlying Kubernetes resource.
|
74
|
+
*/
|
75
|
+
public readonly route: Output<gateway.v1.HTTPRoute>
|
76
|
+
|
77
|
+
constructor(name: string, args: HttpRouteArgs, opts?: ComponentResourceOptions) {
|
78
|
+
super("highstate:k8s:HttpRoute", name, args, opts)
|
79
|
+
|
80
|
+
this.route = output({
|
81
|
+
args,
|
82
|
+
gatewayNamespace: output(args.gateway).metadata.namespace,
|
83
|
+
}).apply(({ args, gatewayNamespace }) => {
|
84
|
+
return new gateway.v1.HTTPRoute(
|
85
|
+
name,
|
86
|
+
{
|
87
|
+
metadata: mapMetadata(
|
88
|
+
{
|
89
|
+
...args,
|
90
|
+
namespace: gatewayNamespace as string,
|
91
|
+
},
|
92
|
+
name,
|
93
|
+
),
|
94
|
+
spec: {
|
95
|
+
hostnames: normalize(args.hostname, args.hostnames),
|
96
|
+
|
97
|
+
parentRefs: [
|
98
|
+
{
|
99
|
+
name: args.gateway.metadata.name as Output<string>,
|
100
|
+
},
|
101
|
+
],
|
102
|
+
|
103
|
+
rules: normalize(args.rule, args.rules).map(rule => ({
|
104
|
+
timeouts: rule.timeouts,
|
105
|
+
|
106
|
+
matches: pipe(
|
107
|
+
normalize(rule.match, rule.matches),
|
108
|
+
map(mapHttpRouteRuleMatch),
|
109
|
+
addDefaultPathMatch,
|
110
|
+
),
|
111
|
+
|
112
|
+
filters: normalize(rule.filter, rule.filters),
|
113
|
+
backendRefs: rule.backend ? [resolveBackendRef(rule.backend)] : undefined,
|
114
|
+
})),
|
115
|
+
} satisfies types.input.gateway.v1.HTTPRouteSpec,
|
116
|
+
},
|
117
|
+
{ parent: this, ...opts },
|
118
|
+
)
|
119
|
+
})
|
120
|
+
|
121
|
+
this.registerOutputs({ route: this.route })
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
function addDefaultPathMatch(
|
126
|
+
matches: types.input.gateway.v1.HTTPRouteSpecRulesMatches[],
|
127
|
+
): types.input.gateway.v1.HTTPRouteSpecRulesMatches[] {
|
128
|
+
return matches.length ? matches : [{ path: { type: "PathPrefix", value: "/" } }]
|
129
|
+
}
|
130
|
+
|
131
|
+
export function mapHttpRouteRuleMatch(
|
132
|
+
match: HttpRouteRuleMatchOptions,
|
133
|
+
): types.input.gateway.v1.HTTPRouteSpecRulesMatches {
|
134
|
+
if (typeof match === "string") {
|
135
|
+
return { path: { type: "PathPrefix", value: match } }
|
136
|
+
}
|
137
|
+
|
138
|
+
return match
|
139
|
+
}
|
package/src/helm.ts
ADDED
@@ -0,0 +1,298 @@
|
|
1
|
+
import type { k8s } from "@highstate/library"
|
2
|
+
import { resolve } from "node:path"
|
3
|
+
import { mkdir, readFile, unlink } from "node:fs/promises"
|
4
|
+
import { toPromise, type InputMap } from "@highstate/pulumi"
|
5
|
+
import { core, helm } from "@pulumi/kubernetes"
|
6
|
+
import {
|
7
|
+
ComponentResource,
|
8
|
+
output,
|
9
|
+
type ComponentResourceOptions,
|
10
|
+
type Input,
|
11
|
+
type Output,
|
12
|
+
} from "@pulumi/pulumi"
|
13
|
+
import spawn from "nano-spawn"
|
14
|
+
import { sha256 } from "crypto-hash"
|
15
|
+
import { omit } from "remeda"
|
16
|
+
import { local } from "@pulumi/command"
|
17
|
+
import { glob } from "glob"
|
18
|
+
import { HttpRoute, type HttpRouteArgs } from "./gateway"
|
19
|
+
import { mapNamespaceLikeToNamespaceName, type NamespaceLike } from "./shared"
|
20
|
+
import { Service } from "./service"
|
21
|
+
|
22
|
+
export type ChartArgs = Omit<
|
23
|
+
helm.v4.ChartArgs,
|
24
|
+
"chart" | "version" | "repositoryOpts" | "namespace"
|
25
|
+
> & {
|
26
|
+
/**
|
27
|
+
* The namespace to deploy the chart into.
|
28
|
+
*/
|
29
|
+
namespace?: Input<NamespaceLike>
|
30
|
+
|
31
|
+
/**
|
32
|
+
* The custom name of the primary service exposed by the chart.
|
33
|
+
*
|
34
|
+
* By default, it is the same as the chart name.
|
35
|
+
*/
|
36
|
+
serviceName?: string
|
37
|
+
|
38
|
+
/**
|
39
|
+
* The manifest of the chart to resolve.
|
40
|
+
*/
|
41
|
+
chart: ChartManifest
|
42
|
+
|
43
|
+
/**
|
44
|
+
* The cluster to create the resource in.
|
45
|
+
*/
|
46
|
+
cluster: Input<k8s.Cluster>
|
47
|
+
|
48
|
+
/**
|
49
|
+
* The http route args to bind the service to.
|
50
|
+
*/
|
51
|
+
httpRoute?: Input<HttpRouteArgs>
|
52
|
+
}
|
53
|
+
|
54
|
+
export class Chart extends ComponentResource {
|
55
|
+
/**
|
56
|
+
* The underlying Helm chart.
|
57
|
+
*/
|
58
|
+
public readonly chart: Output<helm.v4.Chart>
|
59
|
+
|
60
|
+
/**
|
61
|
+
* The HTTP route associated with the deployment.
|
62
|
+
*/
|
63
|
+
public readonly httpRoute: Output<HttpRoute | undefined>
|
64
|
+
|
65
|
+
constructor(
|
66
|
+
private readonly name: string,
|
67
|
+
private readonly args: ChartArgs,
|
68
|
+
private readonly opts?: ComponentResourceOptions,
|
69
|
+
) {
|
70
|
+
super("highstate:k8s:Chart", name, args, opts)
|
71
|
+
|
72
|
+
this.chart = output(args).apply(args => {
|
73
|
+
return new helm.v4.Chart(
|
74
|
+
name,
|
75
|
+
omit(
|
76
|
+
{
|
77
|
+
...args,
|
78
|
+
chart: resolveHelmChart(args.chart),
|
79
|
+
namespace: args.namespace ? mapNamespaceLikeToNamespaceName(args.namespace) : undefined,
|
80
|
+
},
|
81
|
+
["httpRoute"],
|
82
|
+
),
|
83
|
+
{ parent: this, ...opts },
|
84
|
+
)
|
85
|
+
})
|
86
|
+
|
87
|
+
this.httpRoute = output(args.httpRoute).apply(httpRoute => {
|
88
|
+
if (!httpRoute) {
|
89
|
+
return undefined
|
90
|
+
}
|
91
|
+
|
92
|
+
return new HttpRoute(
|
93
|
+
name,
|
94
|
+
{
|
95
|
+
...httpRoute,
|
96
|
+
rule: {
|
97
|
+
backend: this.service,
|
98
|
+
},
|
99
|
+
},
|
100
|
+
{ ...opts, parent: this },
|
101
|
+
)
|
102
|
+
})
|
103
|
+
|
104
|
+
this.registerOutputs({ chart: this.chart })
|
105
|
+
}
|
106
|
+
|
107
|
+
get service(): Output<Service> {
|
108
|
+
return this.getServiceOutput(undefined)
|
109
|
+
}
|
110
|
+
|
111
|
+
private readonly services = new Map<string, Service>()
|
112
|
+
|
113
|
+
getServiceOutput(name: string | undefined): Output<Service> {
|
114
|
+
return output({ args: this.args, chart: this.chart }).apply(({ args, chart }) => {
|
115
|
+
const resolvedName = name ?? args.serviceName ?? this.name
|
116
|
+
const existingService = this.services.get(resolvedName)
|
117
|
+
|
118
|
+
if (existingService) {
|
119
|
+
return existingService
|
120
|
+
}
|
121
|
+
|
122
|
+
const service = getChartServiceOutput(chart, resolvedName)
|
123
|
+
|
124
|
+
const wrappedService = Service.wrap(
|
125
|
+
//
|
126
|
+
resolvedName,
|
127
|
+
service,
|
128
|
+
args.cluster.info,
|
129
|
+
{ parent: this, ...this.opts },
|
130
|
+
)
|
131
|
+
|
132
|
+
this.services.set(resolvedName, wrappedService)
|
133
|
+
return wrappedService
|
134
|
+
})
|
135
|
+
}
|
136
|
+
|
137
|
+
getService(name?: string): Promise<Service> {
|
138
|
+
return toPromise(this.getServiceOutput(name))
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
export type RenderedChartArgs = {
|
143
|
+
/**
|
144
|
+
* The namespace to deploy the chart into.
|
145
|
+
*/
|
146
|
+
namespace?: Input<NamespaceLike>
|
147
|
+
|
148
|
+
/**
|
149
|
+
* The manifest of the chart to resolve.
|
150
|
+
*/
|
151
|
+
chart: ChartManifest
|
152
|
+
|
153
|
+
/**
|
154
|
+
* The values to pass to the chart.
|
155
|
+
*/
|
156
|
+
values?: InputMap<string>
|
157
|
+
}
|
158
|
+
|
159
|
+
export class RenderedChart extends ComponentResource {
|
160
|
+
/**
|
161
|
+
* The rendered manifest of the Helm chart.
|
162
|
+
*/
|
163
|
+
public readonly manifest: Output<string>
|
164
|
+
|
165
|
+
/**
|
166
|
+
* The underlying command used to render the chart.
|
167
|
+
*/
|
168
|
+
public readonly command: Output<local.Command>
|
169
|
+
|
170
|
+
constructor(name: string, args: RenderedChartArgs, opts?: ComponentResourceOptions) {
|
171
|
+
super("highstate:k8s:RenderedChart", name, args, opts)
|
172
|
+
|
173
|
+
this.command = output(args).apply(args => {
|
174
|
+
const values = args.values
|
175
|
+
? Object.entries(args.values).flatMap(([key, value]) => ["--set", `${key}="${value}"`])
|
176
|
+
: []
|
177
|
+
|
178
|
+
return new local.Command(
|
179
|
+
name,
|
180
|
+
{
|
181
|
+
create: output([
|
182
|
+
"helm",
|
183
|
+
"template",
|
184
|
+
resolveHelmChart(args.chart),
|
185
|
+
|
186
|
+
...(args.namespace
|
187
|
+
? ["--namespace", mapNamespaceLikeToNamespaceName(args.namespace)]
|
188
|
+
: []),
|
189
|
+
|
190
|
+
...values,
|
191
|
+
]).apply(command => command.join(" ")),
|
192
|
+
|
193
|
+
logging: "stderr",
|
194
|
+
},
|
195
|
+
{ parent: this, ...opts },
|
196
|
+
)
|
197
|
+
})
|
198
|
+
|
199
|
+
this.manifest = this.command.stdout
|
200
|
+
|
201
|
+
this.registerOutputs({ manifest: this.manifest, command: this.command })
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
export type ChartManifest = {
|
206
|
+
repo: string
|
207
|
+
name: string
|
208
|
+
version: string
|
209
|
+
sha256: string
|
210
|
+
}
|
211
|
+
|
212
|
+
/**
|
213
|
+
* Downloads or reuses the Helm chart according to the charts.json file.
|
214
|
+
* Returns the full path to the chart's .tgz file.
|
215
|
+
*
|
216
|
+
* @param manifest The manifest of the Helm chart.
|
217
|
+
*/
|
218
|
+
export async function resolveHelmChart(manifest: ChartManifest): Promise<string> {
|
219
|
+
if (!process.env.HIGHSTATE_CACHE_DIR) {
|
220
|
+
throw new Error("Environment variable HIGHSTATE_CACHE_DIR is not set")
|
221
|
+
}
|
222
|
+
|
223
|
+
const chartsDir = resolve(process.env.HIGHSTATE_CACHE_DIR, "charts")
|
224
|
+
await mkdir(chartsDir, { recursive: true })
|
225
|
+
|
226
|
+
const globPattern = `${manifest.name}-*.tgz`
|
227
|
+
const targetFileName = `${manifest.name}-${manifest.version}.tgz`
|
228
|
+
|
229
|
+
// find all matching files
|
230
|
+
const files = await glob(globPattern, { cwd: chartsDir })
|
231
|
+
|
232
|
+
if (files.includes(targetFileName)) {
|
233
|
+
return resolve(chartsDir, targetFileName)
|
234
|
+
}
|
235
|
+
|
236
|
+
// delete old versions
|
237
|
+
for (const file of files) {
|
238
|
+
await unlink(resolve(chartsDir, file))
|
239
|
+
}
|
240
|
+
|
241
|
+
// download the chart
|
242
|
+
await spawn("helm", [
|
243
|
+
"pull",
|
244
|
+
manifest.name,
|
245
|
+
"--version",
|
246
|
+
manifest.version,
|
247
|
+
"--repo",
|
248
|
+
manifest.repo,
|
249
|
+
"--destination",
|
250
|
+
chartsDir,
|
251
|
+
])
|
252
|
+
|
253
|
+
// check the SHA256
|
254
|
+
const content = await readFile(resolve(chartsDir, targetFileName))
|
255
|
+
const actualSha256 = await sha256(content)
|
256
|
+
|
257
|
+
if (actualSha256 !== manifest.sha256) {
|
258
|
+
throw new Error(`SHA256 mismatch for chart '${manifest.name}'`)
|
259
|
+
}
|
260
|
+
|
261
|
+
return resolve(chartsDir, targetFileName)
|
262
|
+
}
|
263
|
+
|
264
|
+
/**
|
265
|
+
* Extracts the service with the given name from the chart resources.
|
266
|
+
* Throws an error if the service is not found.
|
267
|
+
*
|
268
|
+
* @param chart The Helm chart.
|
269
|
+
* @param name The name of the service.
|
270
|
+
*/
|
271
|
+
export function getChartServiceOutput(chart: helm.v4.Chart, name: string): Output<core.v1.Service> {
|
272
|
+
const services = chart.resources.apply(resources => {
|
273
|
+
return resources
|
274
|
+
.filter(r => core.v1.Service.isInstance(r))
|
275
|
+
.map(service => ({ name: service.metadata.name, service }))
|
276
|
+
})
|
277
|
+
|
278
|
+
return output(services).apply(services => {
|
279
|
+
const service = services.find(s => s.name === name)?.service
|
280
|
+
|
281
|
+
if (!service) {
|
282
|
+
throw new Error(`Service with name '${name}' not found in the chart resources`)
|
283
|
+
}
|
284
|
+
|
285
|
+
return service
|
286
|
+
})
|
287
|
+
}
|
288
|
+
|
289
|
+
/**
|
290
|
+
* Extracts the service with the given name from the chart resources.
|
291
|
+
* Throws an error if the service is not found.
|
292
|
+
*
|
293
|
+
* @param chart The Helm chart.
|
294
|
+
* @param name The name of the service.
|
295
|
+
*/
|
296
|
+
export function getChartService(chart: helm.v4.Chart, name: string): Promise<core.v1.Service> {
|
297
|
+
return toPromise(getChartServiceOutput(chart, name))
|
298
|
+
}
|