@highstate/k8s 0.9.4 → 0.9.5

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.
Files changed (63) hide show
  1. package/dist/chunk-DQSCJM5S.js +183 -0
  2. package/dist/chunk-DQSCJM5S.js.map +1 -0
  3. package/dist/chunk-FKNHHKOL.js +260 -0
  4. package/dist/chunk-FKNHHKOL.js.map +1 -0
  5. package/dist/chunk-HW3NS3MC.js +347 -0
  6. package/dist/chunk-HW3NS3MC.js.map +1 -0
  7. package/dist/chunk-OQ7UXASD.js +193 -0
  8. package/dist/chunk-OQ7UXASD.js.map +1 -0
  9. package/dist/chunk-QGHMLKTW.js +1123 -0
  10. package/dist/chunk-QGHMLKTW.js.map +1 -0
  11. package/dist/chunk-UNVSWG6D.js +214 -0
  12. package/dist/chunk-UNVSWG6D.js.map +1 -0
  13. package/dist/deployment-ZP3ASKPT.js +10 -0
  14. package/dist/deployment-ZP3ASKPT.js.map +1 -0
  15. package/dist/highstate.manifest.json +8 -6
  16. package/dist/index.js +291 -954
  17. package/dist/index.js.map +1 -1
  18. package/dist/stateful-set-2AH7RAF7.js +10 -0
  19. package/dist/stateful-set-2AH7RAF7.js.map +1 -0
  20. package/dist/units/access-point/index.js +6 -1
  21. package/dist/units/access-point/index.js.map +1 -1
  22. package/dist/units/cert-manager/index.js +19 -24
  23. package/dist/units/cert-manager/index.js.map +1 -1
  24. package/dist/units/cluster-dns/index.js +36 -0
  25. package/dist/units/cluster-dns/index.js.map +1 -0
  26. package/dist/units/cluster-patch/index.js +34 -0
  27. package/dist/units/cluster-patch/index.js.map +1 -0
  28. package/dist/units/dns01-issuer/index.js +2 -2
  29. package/dist/units/dns01-issuer/index.js.map +1 -1
  30. package/dist/units/existing-cluster/index.js +22 -14
  31. package/dist/units/existing-cluster/index.js.map +1 -1
  32. package/dist/units/gateway-api/index.js +1 -1
  33. package/package.json +12 -10
  34. package/src/access-point.ts +44 -39
  35. package/src/container.ts +54 -5
  36. package/src/cron-job.ts +14 -30
  37. package/src/deployment.ts +170 -127
  38. package/src/gateway/http-route.ts +7 -5
  39. package/src/helm.ts +57 -8
  40. package/src/index.ts +11 -4
  41. package/src/job.ts +14 -32
  42. package/src/namespace.ts +241 -0
  43. package/src/network-policy.ts +371 -87
  44. package/src/network.ts +41 -0
  45. package/src/pvc.ts +43 -25
  46. package/src/scripting/bundle.ts +125 -22
  47. package/src/scripting/container.ts +16 -11
  48. package/src/scripting/environment.ts +56 -6
  49. package/src/secret.ts +195 -0
  50. package/src/service.ts +209 -89
  51. package/src/shared.ts +42 -51
  52. package/src/stateful-set.ts +193 -88
  53. package/src/units/access-point/index.ts +8 -1
  54. package/src/units/cert-manager/index.ts +15 -20
  55. package/src/units/cluster-dns/index.ts +37 -0
  56. package/src/units/cluster-patch/index.ts +35 -0
  57. package/src/units/dns01-issuer/index.ts +1 -1
  58. package/src/units/existing-cluster/index.ts +24 -14
  59. package/src/workload.ts +342 -44
  60. package/dist/chunk-K4WKJ4L5.js +0 -455
  61. package/dist/chunk-K4WKJ4L5.js.map +0 -1
  62. package/dist/chunk-T5Z2M4JE.js +0 -103
  63. package/dist/chunk-T5Z2M4JE.js.map +0 -1
@@ -2,6 +2,12 @@ import { k8s } from "@highstate/library"
2
2
  import { forUnit, secret, toPromise } from "@highstate/pulumi"
3
3
  import { core, Provider } from "@pulumi/kubernetes"
4
4
  import { KubeConfig, AppsV1Api } from "@kubernetes/client-node"
5
+ import {
6
+ l3EndpointToString,
7
+ l4EndpointToString,
8
+ parseL3Endpoint,
9
+ parseL4Endpoint,
10
+ } from "@highstate/common"
5
11
  import { createK8sTerminal, detectExternalIps } from "../../cluster"
6
12
 
7
13
  const { name, args, secrets, outputs } = forUnit(k8s.existingCluster)
@@ -10,7 +16,7 @@ const kubeconfigContent = await toPromise(secrets.kubeconfig.apply(JSON.stringif
10
16
 
11
17
  const provider = new Provider(name, { kubeconfig: kubeconfigContent })
12
18
 
13
- let cni: string | undefined
19
+ let cni: k8s.CNI = "other"
14
20
 
15
21
  const kubeConfig = new KubeConfig()
16
22
  kubeConfig.loadFromString(kubeconfigContent)
@@ -29,28 +35,32 @@ if (isCilium) {
29
35
  const externalIps =
30
36
  args.externalIps ?? (await detectExternalIps(kubeConfig, args.internalIpsPolicy))
31
37
 
38
+ const endpoints = externalIps.map(parseL3Endpoint)
39
+ const apiEndpoints = [parseL4Endpoint(kubeConfig.clusters[0].server.replace("https://", ""))]
40
+
32
41
  const kubeSystem = core.v1.Namespace.get("kube-system", "kube-system", { provider })
33
42
 
34
43
  export default outputs({
35
- cluster: {
36
- info: {
37
- id: kubeSystem.metadata.uid,
38
- name,
39
- cni,
40
- externalIps,
41
- tunDevicePolicy: args.tunDevicePolicy,
42
- },
44
+ k8sCluster: {
45
+ id: kubeSystem.metadata.uid,
46
+ name,
47
+ cni,
48
+ externalIps,
49
+ endpoints,
50
+ apiEndpoints,
51
+ quirks: args.quirks,
43
52
  kubeconfig: secret(kubeconfigContent),
44
53
  },
45
54
 
55
+ endpoints,
56
+ apiEndpoints,
57
+
46
58
  $terminals: [createK8sTerminal(kubeconfigContent)],
47
59
 
48
60
  $status: {
49
61
  clusterId: kubeSystem.metadata.uid,
50
- cni: cni ?? "unknown",
51
- externalIps: {
52
- value: externalIps.join(", "),
53
- complementaryTo: "externalIps",
54
- },
62
+ cni,
63
+ endpoints: endpoints.map(l3EndpointToString),
64
+ apiEndpoints: apiEndpoints.map(l4EndpointToString),
55
65
  },
56
66
  })
package/src/workload.ts CHANGED
@@ -1,9 +1,24 @@
1
- import type { types } from "@pulumi/kubernetes"
2
1
  import type { k8s } from "@highstate/library"
3
- import { normalize, type ComponentResourceOptions, type InputArray } from "@highstate/pulumi"
4
- import { ComponentResource, output, type Input, type Output } from "@pulumi/pulumi"
2
+ import type { DeploymentArgs } from "./deployment"
3
+ import type { StatefulSetArgs } from "./stateful-set"
4
+ import type { types } from "@pulumi/kubernetes"
5
+ import {
6
+ normalize,
7
+ type ComponentResourceOptions,
8
+ type InputArray,
9
+ type InstanceTerminal,
10
+ } from "@highstate/pulumi"
11
+ import {
12
+ ComponentResource,
13
+ interpolate,
14
+ Output,
15
+ output,
16
+ type CustomResourceOptions,
17
+ type Input,
18
+ } from "@pulumi/pulumi"
5
19
  import { uniqueBy } from "remeda"
6
- import { commonExtraArgs, type CommonArgs } from "./shared"
20
+ import { deepmerge } from "deepmerge-ts"
21
+ import { commonExtraArgs, getProvider, type CommonArgs } from "./shared"
7
22
  import { mapContainerPortToServicePort, Service, type ServiceArgs } from "./service"
8
23
  import { HttpRoute, type HttpRouteArgs } from "./gateway"
9
24
  import {
@@ -12,54 +27,77 @@ import {
12
27
  type Container,
13
28
  type WorkloadVolume,
14
29
  } from "./container"
30
+ import { NetworkPolicy } from "./network-policy"
31
+ import { podSpecDefaults } from "./pod"
15
32
 
16
33
  export type WorkloadArgs = CommonArgs & {
17
34
  container?: Input<Container>
18
35
  containers?: InputArray<Container>
19
36
 
20
37
  /**
21
- * The cluster to create the resource in.
38
+ * The shell to use in the terminal.
39
+ *
40
+ * By default, `bash` is used.
22
41
  */
23
- cluster: Input<k8s.Cluster>
42
+ terminalShell?: string
24
43
  }
25
44
 
26
45
  export const workloadExtraArgs = [...commonExtraArgs, "container", "containers"] as const
27
46
 
28
- export class WorkloadBase<TArgs extends WorkloadArgs> extends ComponentResource {
29
- protected readonly containers: Output<types.input.core.v1.Container[]>
30
- protected readonly volumes: Output<types.input.core.v1.Volume[]>
47
+ export type ExposableWorkloadArgs = WorkloadArgs & {
48
+ service?: Input<Omit<ServiceArgs, "cluster" | "namespace">>
49
+ httpRoute?: Input<Omit<HttpRouteArgs, "cluster" | "namespace">>
31
50
 
32
- constructor(type: string, name: string, args: TArgs, opts: ComponentResourceOptions | undefined) {
33
- super(type, name, args, opts)
51
+ /**
52
+ * The existing workload to patch.
53
+ */
54
+ existing?: Input<k8s.ExposableWorkload>
55
+ }
34
56
 
35
- const containers = output(args).apply(args => normalize(args.container, args.containers))
57
+ export const exposableWorkloadExtraArgs = [...workloadExtraArgs, "service", "httpRoute"] as const
36
58
 
37
- this.containers = containers.apply(containers =>
38
- containers.map(container => mapContainerToRaw(container, name)),
39
- )
59
+ export type ExposableWorkloadType = "Deployment" | "StatefulSet"
40
60
 
41
- this.volumes = containers.apply(containers =>
42
- containers
43
- .flatMap(container => normalize(container.volume, container.volumes))
44
- .map(mapWorkloadVolume),
45
- )
46
- }
47
- }
61
+ export type GenericExposableWorkloadArgs = Omit<ExposableWorkloadArgs, "existing"> & {
62
+ /**
63
+ * The type of workload to create.
64
+ *
65
+ * Will be ignored if the `existing` argument is provided.
66
+ */
67
+ type: ExposableWorkloadType
48
68
 
49
- export type PublicWorkloadArgs = WorkloadArgs & {
50
- service?: Input<Omit<ServiceArgs, "cluster" | "namespace">>
51
- httpRoute?: Input<Omit<HttpRouteArgs, "cluster" | "namespace">>
52
- patch?: Input<k8s.Deployment | k8s.StatefulSet>
53
- } & Partial<types.input.apps.v1.StatefulSetSpec>
69
+ /**
70
+ * The existing workload to patch.
71
+ */
72
+ existing: Input<k8s.ExposableWorkload | undefined>
73
+
74
+ /**
75
+ * The args specific to the "Deployment" workload type.
76
+ *
77
+ * Will be ignored for other workload types.
78
+ */
79
+ deployment?: Input<DeploymentArgs>
54
80
 
55
- export const publicWorkloadExtraArgs = [...workloadExtraArgs, "service", "httpRoute"] as const
81
+ /**
82
+ * The args specific to the "StatefulSet" workload type.
83
+ *
84
+ * Will be ignored for other workload types.
85
+ */
86
+ statefulSet?: Input<StatefulSetArgs>
87
+ }
56
88
 
57
- export function getWorkloadComponents(name: string, args: WorkloadArgs) {
89
+ export function getWorkloadComponents(
90
+ name: string,
91
+ args: WorkloadArgs,
92
+ parent: () => ComponentResource,
93
+ opts: ComponentResourceOptions | undefined,
94
+ ) {
58
95
  const labels = {
59
96
  "app.kubernetes.io/name": name,
60
97
  }
61
98
 
62
99
  const containers = output(args).apply(args => normalize(args.container, args.containers))
100
+
63
101
  const volumes = containers.apply(containers => {
64
102
  const containerVolumes = containers
65
103
  .flatMap(container => normalize(container.volume, container.volumes))
@@ -75,30 +113,87 @@ export function getWorkloadComponents(name: string, args: WorkloadArgs) {
75
113
  })
76
114
  .map(mapWorkloadVolume)
77
115
 
78
- return uniqueBy([...containerVolumes, ...containerVolumeMounts], volume => volume.name)
116
+ return output([...containerVolumes, ...containerVolumeMounts]).apply(
117
+ uniqueBy(volume => volume.name),
118
+ )
119
+ })
120
+
121
+ const podSpec = output({ args, containers, volumes }).apply(({ args, containers, volumes }) => {
122
+ const spec = {
123
+ volumes,
124
+ containers: containers.map(container => mapContainerToRaw(container, args.cluster, name)),
125
+ ...podSpecDefaults,
126
+ } satisfies types.input.core.v1.PodSpec
127
+
128
+ if (
129
+ containers.some(container => container.enableTun) &&
130
+ args.cluster.quirks?.tunDevicePolicy?.type !== "plugin"
131
+ ) {
132
+ spec.volumes = output(spec.volumes).apply(volumes => [
133
+ ...(volumes ?? []),
134
+ {
135
+ name: "tun-device",
136
+ hostPath: {
137
+ path: "/dev/net/tun",
138
+ },
139
+ },
140
+ ])
141
+ }
142
+
143
+ return spec
79
144
  })
80
145
 
81
- return { labels, containers, volumes }
146
+ const podTemplate = podSpec.apply(podSpec => {
147
+ return {
148
+ metadata: { labels },
149
+ spec: podSpec,
150
+ } satisfies types.input.core.v1.PodTemplateSpec
151
+ })
152
+
153
+ const networkPolicy = containers.apply(containers => {
154
+ const allowedEndpoints = containers.flatMap(container => container.allowedEndpoints ?? [])
155
+
156
+ if (allowedEndpoints.length === 0) {
157
+ return undefined
158
+ }
159
+
160
+ return NetworkPolicy.create(
161
+ name,
162
+ {
163
+ cluster: args.cluster,
164
+ namespace: args.namespace,
165
+ selector: labels,
166
+
167
+ egressRule: {
168
+ toEndpoints: allowedEndpoints,
169
+ },
170
+ },
171
+ { ...opts, parent: parent() },
172
+ )
173
+ }) as Output<NetworkPolicy | undefined>
174
+
175
+ return { labels, containers, volumes, podSpec, podTemplate, networkPolicy }
82
176
  }
83
177
 
84
- export function getPublicWorkloadComponents(
178
+ export function getExposableWorkloadComponents(
85
179
  name: string,
86
- args: PublicWorkloadArgs,
180
+ args: ExposableWorkloadArgs,
87
181
  parent: () => ComponentResource,
88
- opts: ComponentResourceOptions,
182
+ opts: ComponentResourceOptions | undefined,
89
183
  ) {
90
- const { labels, containers, volumes } = getWorkloadComponents(name, args)
184
+ const { labels, containers, volumes, podSpec, podTemplate, networkPolicy } =
185
+ getWorkloadComponents(name, args, parent, opts)
91
186
 
92
- const service = output({ args, containers }).apply(({ args, containers }) => {
187
+ const service = output({ args, containers }).apply(async ({ args, containers }) => {
93
188
  if (!args.service && !args.httpRoute) {
94
189
  return undefined
95
190
  }
96
191
 
97
- if (args.patch?.service) {
98
- return Service.of(name, args.patch.service, { parent: parent(), ...opts })
192
+ if (args.existing?.service) {
193
+ return Service.of(name, args.existing.service, args.cluster, { ...opts, parent: parent() })
99
194
  }
100
195
 
101
- if (args.patch) {
196
+ if (args.existing) {
102
197
  return undefined
103
198
  }
104
199
 
@@ -118,19 +213,23 @@ export function getPublicWorkloadComponents(
118
213
  ? ports.map(mapContainerPortToServicePort)
119
214
  : args.service?.ports,
120
215
  },
121
- { parent: parent(), ...opts },
216
+ {
217
+ ...opts,
218
+ parent: parent(),
219
+ provider: await getProvider(args.cluster),
220
+ },
122
221
  )
123
222
  })
124
223
 
125
224
  const httpRoute = output({
126
225
  args,
127
226
  service,
128
- }).apply(({ args, service }) => {
227
+ }).apply(async ({ args, service }) => {
129
228
  if (!args.httpRoute || !service) {
130
229
  return undefined
131
230
  }
132
231
 
133
- if (args.patch) {
232
+ if (args.existing) {
134
233
  return undefined
135
234
  }
136
235
 
@@ -138,13 +237,212 @@ export function getPublicWorkloadComponents(
138
237
  name,
139
238
  {
140
239
  ...args.httpRoute,
240
+ cluster: args.cluster,
141
241
  rule: {
142
242
  backend: service,
143
243
  },
144
244
  },
145
- { parent: parent(), ...opts },
245
+ {
246
+ ...opts,
247
+ parent: parent(),
248
+ provider: await getProvider(args.cluster),
249
+ },
146
250
  )
147
251
  })
148
252
 
149
- return { labels, containers, volumes, service, httpRoute }
253
+ return { labels, containers, volumes, podSpec, podTemplate, networkPolicy, service, httpRoute }
254
+ }
255
+
256
+ export abstract class Workload extends ComponentResource {
257
+ protected constructor(
258
+ type: string,
259
+ protected readonly name: string,
260
+ private readonly args: WorkloadArgs,
261
+ opts: ComponentResourceOptions | undefined,
262
+
263
+ protected readonly resourceType: string,
264
+
265
+ /**
266
+ * The cluster where the workload is created.
267
+ */
268
+ readonly cluster: Output<k8s.Cluster>,
269
+
270
+ /**
271
+ * The metadata of the underlying Kubernetes workload.
272
+ */
273
+ readonly metadata: Output<types.output.meta.v1.ObjectMeta>,
274
+
275
+ /**
276
+ * The network policy associated with the workload.
277
+ *
278
+ * Will be created if one or more containers have `allowedEndpoints` defined.
279
+ */
280
+ readonly networkPolicy: Output<NetworkPolicy | undefined>,
281
+ ) {
282
+ super(type, name, args, opts)
283
+ }
284
+
285
+ /**
286
+ * The instance terminal to interact with the deployment.
287
+ */
288
+ get terminal(): Output<InstanceTerminal> {
289
+ const containerName = output(this.args).apply(args => {
290
+ const containers = normalize(args.container, args.containers)
291
+
292
+ return containers[0]?.name ?? this.name
293
+ })
294
+
295
+ return output({
296
+ name: this.metadata.name,
297
+ title: this.metadata.name,
298
+ image: "ghcr.io/exeteres/highstate/terminal-kubectl",
299
+ command: [
300
+ "exec",
301
+ "kubectl",
302
+ "exec",
303
+ "-it",
304
+ "-n",
305
+ this.metadata.namespace,
306
+ interpolate`${this.resourceType}/${this.metadata.name}`,
307
+ "-c",
308
+ containerName,
309
+ "--",
310
+ this.args.terminalShell ?? "bash",
311
+ ],
312
+ files: {
313
+ "/kubeconfig": this.cluster.kubeconfig,
314
+ },
315
+ env: {
316
+ KUBECONFIG: "/kubeconfig",
317
+ },
318
+ })
319
+ }
320
+ }
321
+
322
+ export abstract class ExposableWorkload extends Workload {
323
+ protected constructor(
324
+ type: string,
325
+ protected readonly name: string,
326
+ args: ExposableWorkloadArgs,
327
+ opts: ComponentResourceOptions | undefined,
328
+
329
+ resourceType: string,
330
+ cluster: Output<k8s.Cluster>,
331
+ metadata: Output<types.output.meta.v1.ObjectMeta>,
332
+ networkPolicy: Output<NetworkPolicy | undefined>,
333
+
334
+ protected readonly _service: Output<Service | undefined>,
335
+ protected readonly _httpRoute: Output<HttpRoute | undefined>,
336
+ ) {
337
+ super(type, name, args, opts, resourceType, cluster, metadata, networkPolicy)
338
+ }
339
+
340
+ /**
341
+ * The service associated with the workload.
342
+ */
343
+ get optionalService(): Output<Service | undefined> {
344
+ return this._service
345
+ }
346
+
347
+ /**
348
+ * The HTTP route associated with the workload.
349
+ */
350
+ get optionalHttpRoute(): Output<HttpRoute | undefined> {
351
+ return this._httpRoute
352
+ }
353
+
354
+ /**
355
+ * The service associated with the workload.
356
+ *
357
+ * Will throw an error if the service is not available.
358
+ */
359
+ get service(): Output<Service> {
360
+ return this._service.apply(service => {
361
+ if (!service) {
362
+ throw new Error(`The service of the workload "${this.name}" is not available.`)
363
+ }
364
+
365
+ return service
366
+ })
367
+ }
368
+
369
+ /**
370
+ * The HTTP route associated with the workload.
371
+ *
372
+ * Will throw an error if the HTTP route is not available.
373
+ */
374
+ get httpRoute(): Output<HttpRoute> {
375
+ return this._httpRoute.apply(httpRoute => {
376
+ if (!httpRoute) {
377
+ throw new Error(`The HTTP route of the workload "${this.name}" is not available.`)
378
+ }
379
+
380
+ return httpRoute
381
+ })
382
+ }
383
+
384
+ /**
385
+ * The entity of the workload.
386
+ */
387
+ abstract get entity(): Output<k8s.ExposableWorkload>
388
+
389
+ /**
390
+ * The sped of the underlying Kubernetes workload.
391
+ */
392
+ abstract get spec(): Output<
393
+ types.output.apps.v1.DeploymentSpec | types.output.apps.v1.StatefulSetSpec
394
+ >
395
+
396
+ /**
397
+ * Creates a generic workload or patches the existing one.
398
+ */
399
+ static createOrPatchGeneric(
400
+ name: string,
401
+ args: GenericExposableWorkloadArgs,
402
+ opts?: CustomResourceOptions,
403
+ ): Output<ExposableWorkload> {
404
+ return output(args).apply(async args => {
405
+ if (args.existing?.type === "k8s.deployment") {
406
+ const { Deployment } = await import("./deployment")
407
+
408
+ return Deployment.patch(
409
+ name,
410
+ {
411
+ ...deepmerge(args, args.deployment),
412
+ name: args.existing.metadata.name,
413
+ namespace: args.existing.metadata.namespace,
414
+ },
415
+ opts,
416
+ )
417
+ }
418
+
419
+ if (args.existing?.type === "k8s.stateful-set") {
420
+ const { StatefulSet } = await import("./stateful-set")
421
+
422
+ return StatefulSet.patch(
423
+ name,
424
+ {
425
+ ...deepmerge(args, args.statefulSet),
426
+ name: args.existing.metadata.name,
427
+ namespace: args.existing.metadata.namespace,
428
+ },
429
+ opts,
430
+ )
431
+ }
432
+
433
+ if (args.type === "Deployment") {
434
+ const { Deployment } = await import("./deployment")
435
+
436
+ return Deployment.create(name, deepmerge(args, args.deployment), opts)
437
+ }
438
+
439
+ if (args.type === "StatefulSet") {
440
+ const { StatefulSet } = await import("./stateful-set")
441
+
442
+ return StatefulSet.create(name, deepmerge(args, args.statefulSet), opts)
443
+ }
444
+
445
+ throw new Error(`Unknown workload type: ${args.type as string}`)
446
+ })
447
+ }
150
448
  }