@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.
Files changed (107) hide show
  1. package/dist/chunk-23vn2rdc.js +11 -0
  2. package/dist/chunk-2pfx13ay.js +11 -0
  3. package/dist/chunk-46ntav0c.js +299 -0
  4. package/dist/chunk-556pc9e6.js +155 -0
  5. package/dist/chunk-7kgjgcft.js +170 -0
  6. package/dist/{chunk-LGHFSXNT.js → chunk-9hs97f1q.js} +23 -17
  7. package/dist/chunk-aame3x1b.js +11 -0
  8. package/dist/chunk-b05q6fm2.js +37 -0
  9. package/dist/chunk-bmvc9d2d.js +11 -0
  10. package/dist/chunk-de82bbp2.js +7 -0
  11. package/dist/chunk-facs31cb.js +624 -0
  12. package/dist/chunk-h1b79v66.js +1425 -0
  13. package/dist/chunk-k4w9zpn5.js +215 -0
  14. package/dist/chunk-pqc6w52f.js +352 -0
  15. package/dist/chunk-qyshvz32.js +176 -0
  16. package/dist/chunk-tpfyj6fe.js +199 -0
  17. package/dist/chunk-z6bmpnm7.js +180 -0
  18. package/dist/highstate.manifest.json +3 -2
  19. package/dist/impl/dynamic-endpoint-resolver.js +91 -0
  20. package/dist/impl/gateway-route.js +226 -166
  21. package/dist/impl/tls-certificate.js +31 -31
  22. package/dist/index.js +293 -166
  23. package/dist/units/cert-manager/index.js +19 -14
  24. package/dist/units/cluster-patch/index.js +14 -13
  25. package/dist/units/dns01-issuer/index.js +82 -42
  26. package/dist/units/existing-cluster/index.js +59 -26
  27. package/dist/units/gateway-api/index.js +15 -16
  28. package/dist/units/reduced-access-cluster/index.js +32 -36
  29. package/package.json +23 -21
  30. package/src/cluster.ts +12 -8
  31. package/src/config-map.ts +15 -5
  32. package/src/container.ts +4 -2
  33. package/src/cron-job.ts +51 -5
  34. package/src/deployment.ts +49 -18
  35. package/src/gateway/backend.ts +3 -3
  36. package/src/gateway/gateway.ts +12 -56
  37. package/src/helm.ts +354 -22
  38. package/src/impl/dynamic-endpoint-resolver.ts +109 -0
  39. package/src/impl/gateway-route.ts +231 -57
  40. package/src/impl/tls-certificate.ts +8 -3
  41. package/src/index.ts +1 -0
  42. package/src/job.ts +38 -6
  43. package/src/kubectl.ts +166 -0
  44. package/src/namespace.ts +47 -3
  45. package/src/network-policy.ts +1 -1
  46. package/src/pvc.ts +12 -2
  47. package/src/rbac.ts +28 -5
  48. package/src/scripting/bundle.ts +21 -98
  49. package/src/scripting/environment.ts +4 -10
  50. package/src/secret.ts +15 -5
  51. package/src/service.ts +28 -6
  52. package/src/shared.ts +31 -3
  53. package/src/stateful-set.ts +49 -18
  54. package/src/tls.ts +31 -5
  55. package/src/units/cluster-patch/index.ts +5 -5
  56. package/src/units/dns01-issuer/index.ts +56 -12
  57. package/src/units/existing-cluster/index.ts +36 -15
  58. package/src/units/reduced-access-cluster/index.ts +6 -3
  59. package/src/worker.ts +4 -2
  60. package/src/workload.ts +474 -217
  61. package/LICENSE +0 -21
  62. package/dist/chunk-4G6LLC2X.js +0 -240
  63. package/dist/chunk-4G6LLC2X.js.map +0 -1
  64. package/dist/chunk-BR2CLUUD.js +0 -230
  65. package/dist/chunk-BR2CLUUD.js.map +0 -1
  66. package/dist/chunk-DCUMJSO6.js +0 -427
  67. package/dist/chunk-DCUMJSO6.js.map +0 -1
  68. package/dist/chunk-FE4SHRAJ.js +0 -286
  69. package/dist/chunk-FE4SHRAJ.js.map +0 -1
  70. package/dist/chunk-HH2JJELM.js +0 -13
  71. package/dist/chunk-HH2JJELM.js.map +0 -1
  72. package/dist/chunk-KMLRI5UZ.js +0 -155
  73. package/dist/chunk-KMLRI5UZ.js.map +0 -1
  74. package/dist/chunk-LGHFSXNT.js.map +0 -1
  75. package/dist/chunk-MIC2BHGS.js +0 -301
  76. package/dist/chunk-MIC2BHGS.js.map +0 -1
  77. package/dist/chunk-OBDQONMV.js +0 -401
  78. package/dist/chunk-OBDQONMV.js.map +0 -1
  79. package/dist/chunk-P2VOUU7E.js +0 -1626
  80. package/dist/chunk-P2VOUU7E.js.map +0 -1
  81. package/dist/chunk-PZ5AY32C.js +0 -9
  82. package/dist/chunk-PZ5AY32C.js.map +0 -1
  83. package/dist/chunk-RVB4WWZZ.js +0 -267
  84. package/dist/chunk-RVB4WWZZ.js.map +0 -1
  85. package/dist/chunk-TWBMG6TD.js +0 -315
  86. package/dist/chunk-TWBMG6TD.js.map +0 -1
  87. package/dist/chunk-VCXWCZ43.js +0 -279
  88. package/dist/chunk-VCXWCZ43.js.map +0 -1
  89. package/dist/chunk-YIJUVPU2.js +0 -297
  90. package/dist/chunk-YIJUVPU2.js.map +0 -1
  91. package/dist/cron-job-NX4HD4FI.js +0 -8
  92. package/dist/cron-job-NX4HD4FI.js.map +0 -1
  93. package/dist/deployment-O2LJ5WR5.js +0 -8
  94. package/dist/deployment-O2LJ5WR5.js.map +0 -1
  95. package/dist/impl/gateway-route.js.map +0 -1
  96. package/dist/impl/tls-certificate.js.map +0 -1
  97. package/dist/index.js.map +0 -1
  98. package/dist/job-SYME6Y43.js +0 -8
  99. package/dist/job-SYME6Y43.js.map +0 -1
  100. package/dist/stateful-set-VJYKTQ72.js +0 -8
  101. package/dist/stateful-set-VJYKTQ72.js.map +0 -1
  102. package/dist/units/cert-manager/index.js.map +0 -1
  103. package/dist/units/cluster-patch/index.js.map +0 -1
  104. package/dist/units/dns01-issuer/index.js.map +0 -1
  105. package/dist/units/existing-cluster/index.js.map +0 -1
  106. package/dist/units/gateway-api/index.js.map +0 -1
  107. package/dist/units/reduced-access-cluster/index.js.map +0 -1
package/src/workload.ts CHANGED
@@ -1,18 +1,26 @@
1
1
  import type { k8s, network } from "@highstate/library"
2
2
  import type { types } from "@pulumi/kubernetes"
3
- import type { Except } from "type-fest"
3
+ import type { DistributedOmit, Except } from "type-fest"
4
4
  import type { DeploymentArgs } from "./deployment"
5
5
  import type { JobArgs } from "./job"
6
6
  import type { StatefulSetArgs } from "./stateful-set"
7
- import { AccessPointRoute, type AccessPointRouteArgs, mergeEndpoints } from "@highstate/common"
7
+ import {
8
+ AccessPointRoute,
9
+ type AccessPointRouteArgs,
10
+ type GatewayHttpRuleArgs,
11
+ type GatewayRuleArgs,
12
+ mergeEndpoints,
13
+ } from "@highstate/common"
8
14
  import { type TerminalSpec, trimIndentation, type UnitTerminal } from "@highstate/contract"
9
15
  import {
10
16
  type ComponentResourceOptions,
11
17
  type DeepInput,
12
- fileFromString,
13
18
  type InputArray,
19
+ type InputRecord,
20
+ makeFileOutput,
14
21
  normalize,
15
22
  normalizeInputs,
23
+ toPromise,
16
24
  } from "@highstate/pulumi"
17
25
  import {
18
26
  type ComponentResource,
@@ -26,7 +34,7 @@ import {
26
34
  } from "@pulumi/pulumi"
27
35
  import { sha256 } from "crypto-hash"
28
36
  import { deepmerge } from "deepmerge-ts"
29
- import { filter, flat, isNonNullish, unique, uniqueBy } from "remeda"
37
+ import { filter, flat, isNonNullish, omit, unique, uniqueBy } from "remeda"
30
38
  import {
31
39
  type Container,
32
40
  getFallbackContainerName,
@@ -39,7 +47,14 @@ import { Namespace } from "./namespace"
39
47
  import { NetworkPolicy, type NetworkPolicyArgs } from "./network-policy"
40
48
  import { podSpecDefaults } from "./pod"
41
49
  import { mapContainerPortToServicePort, Service, type ServiceArgs } from "./service"
42
- import { commonExtraArgs, images, NamespacedResource, type ScopedResourceArgs } from "./shared"
50
+ import {
51
+ commonExtraArgs,
52
+ getClusterKubeconfigContent,
53
+ images,
54
+ NamespacedResource,
55
+ type ScopedResourceArgs,
56
+ type SelectorLike,
57
+ } from "./shared"
43
58
 
44
59
  export type WorkloadTerminalArgs = {
45
60
  /**
@@ -84,31 +99,150 @@ export type WorkloadArgs = ScopedResourceArgs & {
84
99
 
85
100
  export const workloadExtraArgs = [...commonExtraArgs, "container", "containers"] as const
86
101
 
87
- export type ExposableWorkloadRouteArgs = Except<
88
- AccessPointRouteArgs,
89
- "endpoints" | "gatewayNativeData" | "tlsCertificateNativeData"
90
- >
102
+ /**
103
+ * Filters pod template containers for patch operations to include only containers owned by the patch args.
104
+ *
105
+ * This prevents patching containers from existing workload spec that are not declared via workload container args.
106
+ *
107
+ * @param template The merged pod template that will be sent to Kubernetes.
108
+ * @param ownedTemplate The pod template generated from workload container args.
109
+ * @returns The pod template with filtered container lists.
110
+ */
111
+ export function filterPatchOwnedContainersInTemplate(
112
+ template: Unwrap<types.input.core.v1.PodTemplateSpec>,
113
+ ownedTemplate: Unwrap<types.input.core.v1.PodTemplateSpec>,
114
+ ): Unwrap<types.input.core.v1.PodTemplateSpec> {
115
+ const ownedContainerNames = unique(
116
+ (ownedTemplate.spec?.containers ?? []).map(container => container.name).filter(isNonNullish),
117
+ )
118
+
119
+ const ownedInitContainerNames = unique(
120
+ (ownedTemplate.spec?.initContainers ?? [])
121
+ .map(container => container.name)
122
+ .filter(isNonNullish),
123
+ )
124
+
125
+ const filterByOwnedNames = <TContainer extends { name?: string }>(
126
+ source: TContainer[] | undefined,
127
+ ownedNames: string[],
128
+ ): TContainer[] | undefined => {
129
+ if (!source || source.length === 0 || ownedNames.length === 0) {
130
+ return undefined
131
+ }
132
+
133
+ const filtered = source.filter(container =>
134
+ container.name ? ownedNames.includes(container.name) : false,
135
+ )
136
+
137
+ return filtered.length > 0 ? filtered : undefined
138
+ }
139
+
140
+ const containers = filterByOwnedNames(template.spec?.containers, ownedContainerNames)
141
+ const initContainers = filterByOwnedNames(template.spec?.initContainers, ownedInitContainerNames)
142
+
143
+ const {
144
+ containers: _containers,
145
+ initContainers: _initContainers,
146
+ ...restSpec
147
+ } = template.spec ?? {}
148
+
149
+ const spec = {
150
+ ...restSpec,
151
+ ...(containers ? { containers } : {}),
152
+ ...(initContainers ? { initContainers } : {}),
153
+ } as Partial<Unwrap<types.input.core.v1.PodSpec>>
154
+
155
+ return {
156
+ ...template,
157
+ spec: spec as Unwrap<types.input.core.v1.PodSpec>,
158
+ }
159
+ }
160
+
161
+ type WorkloadHttpGatewayRuleArgs = DistributedOmit<GatewayHttpRuleArgs, "backend" | "backends"> & {
162
+ /**
163
+ * The service port to route to.
164
+ *
165
+ * Can be either a numeric port or a named port from the workload service.
166
+ *
167
+ * If not specified, it first falls back to the servicePort of the route,
168
+ * then to the first port of the workload service.
169
+ */
170
+ servicePort?: Input<number | string>
171
+ }
172
+
173
+ type WorkloadTcpUdpGatewayRuleArgs = DistributedOmit<GatewayRuleArgs, "backend" | "backends"> & {
174
+ /**
175
+ * The service port to route to.
176
+ *
177
+ * Can be either a numeric port or a named port from the workload service.
178
+ *
179
+ * If not specified, it first falls back to the servicePort of the route,
180
+ * then to the first port of the workload service.
181
+ */
182
+ servicePort?: Input<number | string>
183
+ }
91
184
 
92
- export type ExposableWorkloadArgs = WorkloadArgs & {
185
+ type WorkloadGatewayRuleArgs = WorkloadHttpGatewayRuleArgs | WorkloadTcpUdpGatewayRuleArgs
186
+
187
+ export type WorkloadRouteArgs = Except<AccessPointRouteArgs, "backend" | "backends" | "rules"> & {
188
+ /**
189
+ * The service port to route to by default.
190
+ *
191
+ * Can be either a numeric port or a named port from the workload service.
192
+ *
193
+ * Can be overridden by `rules.*.servicePort`.
194
+ * If omitted, the first port of the workload service is used.
195
+ */
196
+ servicePort?: Input<number | string>
197
+ } & (
198
+ | {
199
+ type: "http"
200
+
201
+ /**
202
+ * The path to match for the `default` rule of the listener.
203
+ */
204
+ path?: Input<string>
205
+
206
+ /**
207
+ * The paths to match for the `default` rule of the listener.
208
+ */
209
+ paths?: Input<string[]>
210
+
211
+ /**
212
+ * The rules of the route.
213
+ */
214
+ rules?: InputRecord<WorkloadHttpGatewayRuleArgs>
215
+ }
216
+ | {
217
+ type: "tcp" | "udp"
218
+
219
+ /**
220
+ * The rules of the route.
221
+ */
222
+ rules?: InputRecord<WorkloadTcpUdpGatewayRuleArgs>
223
+ }
224
+ )
225
+
226
+ export type WorkloadServiceArgs = WorkloadArgs & {
93
227
  service?: Input<Omit<ServiceArgs, "cluster" | "namespace">>
94
228
 
95
229
  /**
96
230
  * The configuration for the access point route to create.
97
231
  */
98
- route?: Input<ExposableWorkloadRouteArgs>
232
+ route?: Input<WorkloadRouteArgs>
99
233
 
100
234
  /**
101
235
  * The configuration for the access point routes to create.
102
236
  */
103
- routes?: InputArray<ExposableWorkloadRouteArgs>
237
+ routes?: InputArray<WorkloadRouteArgs>
104
238
 
105
239
  /**
106
240
  * The existing workload to patch.
107
241
  */
108
- existing?: Input<k8s.ExposableWorkload>
242
+ existing?: Input<k8s.Workload>
109
243
  }
110
244
 
111
- export const exposableWorkloadExtraArgs = [
245
+ export const workloadServiceExtraArgs = [
112
246
  ...workloadExtraArgs,
113
247
  "service",
114
248
  "route",
@@ -116,9 +250,8 @@ export const exposableWorkloadExtraArgs = [
116
250
  ] as const
117
251
 
118
252
  export type WorkloadType = "Deployment" | "StatefulSet" | "Job" | "CronJob"
119
- export type ExposableWorkloadType = "Deployment" | "StatefulSet"
120
253
 
121
- export type GenericWorkloadArgs = Omit<ExposableWorkloadArgs, "existing"> & {
254
+ export type GenericWorkloadArgs = Omit<WorkloadServiceArgs, "existing"> & {
122
255
  /**
123
256
  * The type of workload to create.
124
257
  *
@@ -136,14 +269,14 @@ export type GenericWorkloadArgs = Omit<ExposableWorkloadArgs, "existing"> & {
136
269
  *
137
270
  * Will be ignored for other workload types.
138
271
  */
139
- deployment?: Input<DeploymentArgs>
272
+ deployment?: Input<Omit<DeploymentArgs, "name" | "namespace">>
140
273
 
141
274
  /**
142
275
  * The args specific to the "StatefulSet" workload type.
143
276
  *
144
277
  * Will be ignored for other workload types.
145
278
  */
146
- statefulSet?: Input<StatefulSetArgs>
279
+ statefulSet?: Input<Omit<StatefulSetArgs, "name" | "namespace">>
147
280
 
148
281
  /**
149
282
  * The args specific to the "Job" workload type.
@@ -160,33 +293,14 @@ export type GenericWorkloadArgs = Omit<ExposableWorkloadArgs, "existing"> & {
160
293
  cronJob?: Input<JobArgs>
161
294
  }
162
295
 
163
- export type GenericExposableWorkloadArgs = Omit<ExposableWorkloadArgs, "existing"> & {
164
- /**
165
- * The type of workload to create.
166
- *
167
- * Will be ignored if the `existing` argument is provided.
168
- */
169
- defaultType: ExposableWorkloadType
170
-
171
- /**
172
- * The existing workload to patch.
173
- */
174
- existing: Input<k8s.ExposableWorkload | undefined>
175
-
176
- /**
177
- * The args specific to the "Deployment" workload type.
178
- *
179
- * Will be ignored for other workload types.
180
- */
181
- deployment?: Input<DeploymentArgs>
182
-
183
- /**
184
- * The args specific to the "StatefulSet" workload type.
185
- *
186
- * Will be ignored for other workload types.
187
- */
188
- statefulSet?: Input<StatefulSetArgs>
189
- }
296
+ const genericWorkloadExtraArgs = [
297
+ "defaultType",
298
+ "existing",
299
+ "deployment",
300
+ "statefulSet",
301
+ "job",
302
+ "cronJob",
303
+ ] as const
190
304
 
191
305
  export function getWorkloadComponents(
192
306
  name: string,
@@ -315,9 +429,9 @@ export function getWorkloadComponents(
315
429
  return { labels, containers, volumes, podSpec, podTemplate, networkPolicy }
316
430
  }
317
431
 
318
- export function getExposableWorkloadComponents(
432
+ export function getWorkloadServiceComponents(
319
433
  name: string,
320
- args: ExposableWorkloadArgs,
434
+ args: WorkloadServiceArgs,
321
435
  parent: () => ComponentResource,
322
436
  opts: ComponentResourceOptions | undefined,
323
437
  isForPatch?: boolean,
@@ -361,7 +475,7 @@ export function getExposableWorkloadComponents(
361
475
  routesArgs: normalizeInputs(args.route, args.routes),
362
476
  service,
363
477
  namespace: output(args.namespace),
364
- }).apply(({ routesArgs, service, namespace }) => {
478
+ }).apply(async ({ routesArgs, service, namespace }) => {
365
479
  if (!routesArgs.length || !service) {
366
480
  return []
367
481
  }
@@ -370,16 +484,114 @@ export function getExposableWorkloadComponents(
370
484
  return []
371
485
  }
372
486
 
373
- return routesArgs.map((routeArgs, index) => {
374
- return new AccessPointRoute(`${name}.${index}`, {
375
- ...routeArgs,
376
- endpoints: service.endpoints,
487
+ const serviceEndpoints = await toPromise(service.endpoints)
488
+ const servicePorts = await toPromise(service.spec.ports)
377
489
 
378
- // pass the native data to the route to allow implementation to use it
379
- gatewayNativeData: service,
380
- tlsCertificateNativeData: namespace,
381
- })
382
- })
490
+ const resolveServiceEndpoints = async (
491
+ servicePort: Input<number | string> | undefined,
492
+ routeName: string,
493
+ ): Promise<network.L4Endpoint[]> => {
494
+ if (serviceEndpoints.length === 0) {
495
+ throw new Error(`No endpoints found for workload service in route "${routeName}"`)
496
+ }
497
+
498
+ let resolvedServicePort: number | undefined
499
+
500
+ if (servicePort != null) {
501
+ const requestedServicePort = await toPromise(servicePort)
502
+
503
+ if (typeof requestedServicePort === "string") {
504
+ const namedPort = servicePorts?.find(port => port.name === requestedServicePort)
505
+
506
+ if (!namedPort) {
507
+ throw new Error(
508
+ `Named port "${requestedServicePort}" not found for workload service in route "${routeName}"`,
509
+ )
510
+ }
511
+
512
+ resolvedServicePort = namedPort.port
513
+ } else {
514
+ resolvedServicePort = requestedServicePort
515
+ }
516
+ } else {
517
+ resolvedServicePort = serviceEndpoints[0]?.port
518
+ }
519
+
520
+ if (resolvedServicePort == null) {
521
+ throw new Error(
522
+ `Unable to resolve service port for workload service in route "${routeName}"`,
523
+ )
524
+ }
525
+
526
+ const filteredEndpoints = serviceEndpoints.filter(
527
+ endpoint => endpoint.port === resolvedServicePort,
528
+ )
529
+
530
+ if (filteredEndpoints.length === 0) {
531
+ throw new Error(
532
+ `No endpoints with port ${resolvedServicePort} found for workload service in route "${routeName}"`,
533
+ )
534
+ }
535
+
536
+ return filteredEndpoints
537
+ }
538
+
539
+ return await Promise.all(
540
+ routesArgs.map(async (routeArgs, index) => {
541
+ const routeName = `${name}.${index}`
542
+ const routeRules = (await toPromise(routeArgs.rules)) as
543
+ | Record<string, WorkloadGatewayRuleArgs>
544
+ | undefined
545
+ const routeRuleValues = Object.values(routeRules ?? {})
546
+ const needsDefaultBackend =
547
+ routeRuleValues.length === 0 || routeRuleValues.some(rule => rule.servicePort == null)
548
+
549
+ const defaultServiceEndpoints = needsDefaultBackend
550
+ ? await resolveServiceEndpoints(routeArgs.servicePort, routeName)
551
+ : undefined
552
+
553
+ const resolvedRules = routeRules
554
+ ? await Promise.all(
555
+ Object.entries(routeRules).map(async ([ruleName, rule]) => {
556
+ const ruleServiceEndpoints = await resolveServiceEndpoints(
557
+ rule.servicePort ?? routeArgs.servicePort,
558
+ `${routeName}:${ruleName}`,
559
+ )
560
+
561
+ return [
562
+ ruleName,
563
+ {
564
+ ...omit(rule, ["servicePort"]),
565
+ backend: {
566
+ endpoints: ruleServiceEndpoints,
567
+ },
568
+ },
569
+ ] as const
570
+ }),
571
+ )
572
+ : undefined
573
+
574
+ const resolvedRulesInput = resolvedRules
575
+ ? (Object.fromEntries(resolvedRules) as unknown as InputRecord<GatewayRuleArgs>)
576
+ : undefined
577
+
578
+ return new AccessPointRoute(routeName, {
579
+ ...omit(routeArgs, ["servicePort", "rules"]),
580
+ ...(defaultServiceEndpoints
581
+ ? {
582
+ backend: {
583
+ endpoints: defaultServiceEndpoints,
584
+ },
585
+ }
586
+ : {}),
587
+ rules: resolvedRulesInput,
588
+ metadata: {
589
+ ...(routeArgs.metadata ?? {}),
590
+ "k8s.namespace": namespace,
591
+ },
592
+ })
593
+ }),
594
+ )
383
595
  })
384
596
 
385
597
  return { labels, containers, volumes, podSpec, podTemplate, networkPolicy, service, routes }
@@ -409,17 +621,81 @@ export abstract class Workload extends NamespacedResource {
409
621
  * Will be created if one or more containers have `allowedEndpoints` defined.
410
622
  */
411
623
  readonly networkPolicy: Output<NetworkPolicy | undefined>,
624
+
625
+ protected readonly _service: Output<Service | undefined> = output(undefined),
626
+
627
+ /**
628
+ * The access point routes associated with the workload.
629
+ */
630
+ readonly routes: Output<AccessPointRoute[]> = output([]),
412
631
  ) {
413
632
  super(type, name, args, opts, metadata, namespace)
414
633
  }
415
634
 
635
+ abstract get entity(): Output<k8s.Workload>
636
+
416
637
  protected abstract get templateMetadata(): Output<types.output.meta.v1.ObjectMeta>
417
638
 
418
639
  protected abstract getTerminalMeta(): Output<UnitTerminal["meta"]>
419
640
 
420
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for pulumi which for some reason tries to copy all properties
421
641
  private set terminal(_value: never) {}
422
642
 
643
+ private set logsTerminal(_value: never) {}
644
+
645
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for pulumi which for some reason tries to copy all properties
646
+ private set terminals(_value: never) {}
647
+
648
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for pulumi which for some reason tries to copy all properties
649
+ private set optionalService(_value: never) {}
650
+
651
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for pulumi which for some reason tries to copy all properties
652
+ private set service(_value: never) {}
653
+
654
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for pulumi which for some reason tries to copy all properties
655
+ private set selector(_value: never) {}
656
+
657
+ /**
658
+ * The service associated with the workload.
659
+ */
660
+ get optionalService(): Output<Service | undefined> {
661
+ return this._service
662
+ }
663
+
664
+ /**
665
+ * The service associated with the workload.
666
+ *
667
+ * Will throw an error if the service is not available.
668
+ */
669
+ get service(): Output<Service> {
670
+ return this._service.apply(service => {
671
+ if (!service) {
672
+ throw new Error(`The service of the workload "${this.name}" is not available.`)
673
+ }
674
+
675
+ return service
676
+ })
677
+ }
678
+
679
+ /**
680
+ * The merged and deduplicated L3 endpoints of all routes.
681
+ */
682
+ get endpoints(): Output<network.L3Endpoint[]> {
683
+ return this.routes.apply(routes =>
684
+ output(routes.map(route => route.route.endpoints))
685
+ .apply(endpoints => flat(endpoints))
686
+ .apply(mergeEndpoints),
687
+ )
688
+ }
689
+
690
+ /**
691
+ * The selector matching pods created from this workload's template labels.
692
+ */
693
+ get selector(): Output<SelectorLike> {
694
+ return this.podTemplate.apply(template => ({
695
+ matchLabels: template.metadata?.labels,
696
+ }))
697
+ }
698
+
423
699
  /**
424
700
  * The instance terminal to interact with the workload's pods.
425
701
  */
@@ -445,11 +721,15 @@ export abstract class Workload extends NamespacedResource {
445
721
  command: ["bash", "/welcome.sh"],
446
722
 
447
723
  files: {
448
- "/kubeconfig": fileFromString("kubeconfig", this.cluster.kubeconfig, { isSecret: true }),
449
-
450
- "/welcome.sh": fileFromString(
451
- "welcome.sh",
452
- interpolate`
724
+ "/kubeconfig": makeFileOutput({
725
+ name: "kubeconfig",
726
+ content: getClusterKubeconfigContent(this.cluster),
727
+ isSecret: true,
728
+ }),
729
+
730
+ "/welcome.sh": makeFileOutput({
731
+ name: "welcome.sh",
732
+ content: interpolate`
453
733
  #!/bin/bash
454
734
  set -euo pipefail
455
735
 
@@ -494,7 +774,98 @@ export abstract class Workload extends NamespacedResource {
494
774
  # execute into the selected pod
495
775
  exec kubectl exec -it -n "$NAMESPACE" "$SELECTED_POD" -c "$CONTAINER_NAME" -- "$SHELL"
496
776
  `.apply(trimIndentation),
497
- ),
777
+ }),
778
+ },
779
+
780
+ env: {
781
+ KUBECONFIG: "/kubeconfig",
782
+ },
783
+ },
784
+ })
785
+ }
786
+
787
+ /**
788
+ * The instance terminal to view the workload's logs.
789
+ */
790
+ get logsTerminal(): Output<UnitTerminal> {
791
+ const containerName = this.podTemplate.spec.containers.apply(containers => containers[0].name)
792
+
793
+ const podLabelSelector = this.templateMetadata
794
+ .apply(meta => meta.labels ?? {})
795
+ .apply(labels =>
796
+ Object.entries(labels)
797
+ .map(([key, value]) => `${key}=${value}`)
798
+ .join(","),
799
+ )
800
+
801
+ return output({
802
+ name: interpolate`${this.metadata.name}.logs`,
803
+
804
+ meta: output(this.getTerminalMeta()).apply(meta => ({
805
+ ...meta,
806
+ title: `${meta.title} Logs`,
807
+ globalTitle: `${meta.globalTitle} | Logs`,
808
+ description: `The logs of ${meta.title.toLowerCase()}.`,
809
+ })),
810
+
811
+ spec: {
812
+ image: images["terminal-kubectl"].image,
813
+ command: ["bash", "/welcome.sh"],
814
+
815
+ files: {
816
+ "/kubeconfig": makeFileOutput({
817
+ name: "kubeconfig",
818
+ content: getClusterKubeconfigContent(this.cluster),
819
+ isSecret: true,
820
+ }),
821
+
822
+ "/welcome.sh": makeFileOutput({
823
+ name: "welcome.sh",
824
+ content: interpolate`
825
+ #!/bin/bash
826
+ set -euo pipefail
827
+
828
+ NAMESPACE="${this.metadata.namespace}"
829
+ RESOURCE_TYPE="${this.kind.toLowerCase()}"
830
+ RESOURCE_NAME="${this.metadata.name}"
831
+ CONTAINER_NAME="${containerName}"
832
+ LABEL_SELECTOR="${podLabelSelector}"
833
+
834
+ echo "Connecting to logs of $RESOURCE_TYPE \"$RESOURCE_NAME\" in namespace \"$NAMESPACE\""
835
+
836
+ # get all pods for this workload
837
+ PODS=$(kubectl get pods -n "$NAMESPACE" -l "$LABEL_SELECTOR" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || echo "")
838
+
839
+ if [ -z "$PODS" ]; then
840
+ echo "No pods found"
841
+ exit 1
842
+ fi
843
+
844
+ # convert space-separated string to array
845
+ read -ra POD_ARRAY <<< "$PODS"
846
+
847
+ if [ \${#POD_ARRAY[@]} -eq 1 ]; then
848
+ # single pod found, connect directly
849
+ SELECTED_POD="\${POD_ARRAY[0]}"
850
+ echo "Found single pod: $SELECTED_POD"
851
+ else
852
+ # multiple pods found, use fzf for selection
853
+ echo "Found \${#POD_ARRAY[@]} pods. Please select one."
854
+
855
+ SELECTED_POD=$(printf '%s\n' "\${POD_ARRAY[@]}" | fzf --prompt="Select pod: " --height 10 --border --info=inline)
856
+
857
+ if [ -z "$SELECTED_POD" ]; then
858
+ echo "No pod selected"
859
+ exit 1
860
+ fi
861
+
862
+ echo "Selected pod: $SELECTED_POD"
863
+ fi
864
+
865
+ # stream logs for the selected pod
866
+ exec kubectl logs -f -n "$NAMESPACE" "$SELECTED_POD" -c "$CONTAINER_NAME"
867
+ `.apply(trimIndentation),
868
+ }),
498
869
  },
499
870
 
500
871
  env: {
@@ -504,6 +875,13 @@ export abstract class Workload extends NamespacedResource {
504
875
  })
505
876
  }
506
877
 
878
+ /**
879
+ * The instance terminals to interact with the workload's pods and view its logs.
880
+ */
881
+ get terminals(): Output<UnitTerminal>[] {
882
+ return [this.logsTerminal, this.terminal]
883
+ }
884
+
507
885
  /**
508
886
  * Creates a terminal with a custom command.
509
887
  *
@@ -517,9 +895,7 @@ export abstract class Workload extends NamespacedResource {
517
895
  command: InputArray<string>,
518
896
  spec?: { env?: DeepInput<TerminalSpec["env"]>; files?: DeepInput<TerminalSpec["files"]> },
519
897
  ): Output<UnitTerminal> {
520
- const containerName = output(this.containers).apply(containers => {
521
- return containers[0]?.name ?? this.name
522
- })
898
+ const containerName = this.podTemplate.spec.containers.apply(containers => containers[0].name)
523
899
 
524
900
  return output({
525
901
  name,
@@ -539,7 +915,7 @@ export abstract class Workload extends NamespacedResource {
539
915
  "-it",
540
916
  "-n",
541
917
  this.metadata.namespace,
542
- `${this.kind.toLowerCase()}/${this.metadata.name}`,
918
+ interpolate`${this.kind.toLowerCase()}/${this.metadata.name}`,
543
919
  "-c",
544
920
  containerName,
545
921
  "--",
@@ -547,7 +923,11 @@ export abstract class Workload extends NamespacedResource {
547
923
  ]),
548
924
 
549
925
  files: {
550
- "/kubeconfig": fileFromString("kubeconfig", this.cluster.kubeconfig, { isSecret: true }),
926
+ "/kubeconfig": makeFileOutput({
927
+ name: "kubeconfig",
928
+ content: getClusterKubeconfigContent(this.cluster),
929
+ isSecret: true,
930
+ }),
551
931
  ...spec?.files,
552
932
  },
553
933
 
@@ -568,13 +948,17 @@ export abstract class Workload extends NamespacedResource {
568
948
  opts?: CustomResourceOptions,
569
949
  ): Output<Workload> {
570
950
  return output(args).apply(async args => {
951
+ const baseArgs = omit(args, genericWorkloadExtraArgs)
952
+
571
953
  if (args.existing?.kind === "Deployment") {
572
954
  const { Deployment } = await import("./deployment")
573
955
 
956
+ const deploymentArgs = deepmerge(baseArgs, args.deployment ?? {}) as DeploymentArgs
957
+
574
958
  return Deployment.patch(
575
959
  name,
576
960
  {
577
- ...deepmerge(args, args.deployment),
961
+ ...deploymentArgs,
578
962
  name: args.existing.metadata.name,
579
963
  namespace: Namespace.forResourceAsync(args.existing, output(args.namespace).cluster),
580
964
  },
@@ -585,10 +969,12 @@ export abstract class Workload extends NamespacedResource {
585
969
  if (args.existing?.kind === "StatefulSet") {
586
970
  const { StatefulSet } = await import("./stateful-set")
587
971
 
972
+ const statefulSetArgs = deepmerge(baseArgs, args.statefulSet ?? {}) as StatefulSetArgs
973
+
588
974
  return StatefulSet.patch(
589
975
  name,
590
976
  {
591
- ...deepmerge(args, args.statefulSet),
977
+ ...statefulSetArgs,
592
978
  name: args.existing.metadata.name,
593
979
  namespace: Namespace.forResourceAsync(args.existing, output(args.namespace).cluster),
594
980
  },
@@ -599,10 +985,12 @@ export abstract class Workload extends NamespacedResource {
599
985
  if (args.existing?.kind === "Job") {
600
986
  const { Job } = await import("./job")
601
987
 
988
+ const jobArgs = deepmerge(baseArgs, args.job ?? {}) as JobArgs
989
+
602
990
  return Job.patch(
603
991
  name,
604
992
  {
605
- ...deepmerge(args, args.job),
993
+ ...jobArgs,
606
994
  name: args.existing.metadata.name,
607
995
  namespace: Namespace.forResourceAsync(args.existing, output(args.namespace).cluster),
608
996
  },
@@ -613,10 +1001,12 @@ export abstract class Workload extends NamespacedResource {
613
1001
  if (args.existing?.kind === "CronJob") {
614
1002
  const { CronJob } = await import("./cron-job")
615
1003
 
1004
+ const cronJobArgs = deepmerge(baseArgs, args.cronJob ?? {}) as JobArgs
1005
+
616
1006
  return CronJob.patch(
617
1007
  name,
618
1008
  {
619
- ...deepmerge(args, args.cronJob),
1009
+ ...cronJobArgs,
620
1010
  name: args.existing.metadata.name,
621
1011
  namespace: Namespace.forResourceAsync(args.existing, output(args.namespace).cluster),
622
1012
  },
@@ -627,166 +1017,33 @@ export abstract class Workload extends NamespacedResource {
627
1017
  if (args.defaultType === "Deployment") {
628
1018
  const { Deployment } = await import("./deployment")
629
1019
 
630
- return Deployment.create(name, deepmerge(args, args.deployment), opts)
1020
+ const deploymentArgs = deepmerge(baseArgs, args.deployment ?? {}) as DeploymentArgs
1021
+
1022
+ return Deployment.create(name, deploymentArgs, opts)
631
1023
  }
632
1024
 
633
1025
  if (args.defaultType === "StatefulSet") {
634
1026
  const { StatefulSet } = await import("./stateful-set")
635
1027
 
636
- return StatefulSet.create(name, deepmerge(args, args.statefulSet), opts)
1028
+ const statefulSetArgs = deepmerge(baseArgs, args.statefulSet ?? {}) as StatefulSetArgs
1029
+
1030
+ return StatefulSet.create(name, statefulSetArgs, opts)
637
1031
  }
638
1032
 
639
1033
  if (args.defaultType === "Job") {
640
1034
  const { Job } = await import("./job")
641
1035
 
642
- return Job.create(name, deepmerge(args, args.job), opts)
1036
+ const jobArgs = deepmerge(baseArgs, args.job ?? {}) as JobArgs
1037
+
1038
+ return Job.create(name, jobArgs, opts)
643
1039
  }
644
1040
 
645
1041
  if (args.defaultType === "CronJob") {
646
1042
  const { CronJob } = await import("./cron-job")
647
1043
 
648
- return CronJob.create(name, deepmerge(args, args.cronJob), opts)
649
- }
650
-
651
- throw new Error(`Unknown workload type: ${args.defaultType as string}`)
652
- })
653
- }
654
- }
655
-
656
- export abstract class ExposableWorkload extends Workload {
657
- protected constructor(
658
- type: string,
659
- name: string,
660
- args: Inputs,
661
- opts: ComponentResourceOptions | undefined,
662
-
663
- metadata: Output<types.output.meta.v1.ObjectMeta>,
664
- namespace: Output<Namespace>,
665
-
666
- terminalArgs: Output<Unwrap<WorkloadTerminalArgs>>,
667
- containers: Output<Container[]>,
668
- podTemplate: Output<types.output.core.v1.PodTemplateSpec>,
669
- networkPolicy: Output<NetworkPolicy | undefined>,
670
-
671
- protected readonly _service: Output<Service | undefined>,
672
-
673
- /**
674
- * The access point routes associated with the workload.
675
- */
676
- readonly routes: Output<AccessPointRoute[]>,
677
- ) {
678
- super(
679
- type,
680
- name,
681
- args,
682
- opts,
683
- metadata,
684
- namespace,
685
- terminalArgs,
686
- containers,
687
- podTemplate,
688
- networkPolicy,
689
- )
690
- }
691
-
692
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for pulumi which for some reason tries to copy all properties
693
- private set optionalService(_value: never) {}
694
-
695
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: for pulumi which for some reason tries to copy all properties
696
- private set service(_value: never) {}
697
-
698
- /**
699
- * The service associated with the workload.
700
- */
701
- get optionalService(): Output<Service | undefined> {
702
- return this._service
703
- }
704
-
705
- /**
706
- * The service associated with the workload.
707
- *
708
- * Will throw an error if the service is not available.
709
- */
710
- get service(): Output<Service> {
711
- return this._service.apply(service => {
712
- if (!service) {
713
- throw new Error(`The service of the workload "${this.name}" is not available.`)
714
- }
715
-
716
- return service
717
- })
718
- }
719
-
720
- /**
721
- * The merged and deduplicated L3 endpoints of all routes.
722
- */
723
- get endpoints(): Output<network.L3Endpoint[]> {
724
- return this.routes.apply(routes =>
725
- output(routes.map(route => route.route.endpoints))
726
- .apply(endpoints => flat(endpoints))
727
- .apply(mergeEndpoints),
728
- )
729
- }
730
-
731
- /**
732
- * The entity of the workload.
733
- */
734
- abstract get entity(): Output<k8s.ExposableWorkload>
735
-
736
- /**
737
- * The sped of the underlying Kubernetes workload.
738
- */
739
- abstract get spec(): Output<
740
- types.output.apps.v1.DeploymentSpec | types.output.apps.v1.StatefulSetSpec
741
- >
742
-
743
- /**
744
- * Creates a generic exposable workload or patches the existing one.
745
- */
746
- static createOrPatchGeneric(
747
- name: string,
748
- args: GenericExposableWorkloadArgs,
749
- opts?: CustomResourceOptions,
750
- ): Output<ExposableWorkload> {
751
- return output(args).apply(async args => {
752
- if (args.existing?.kind === "Deployment") {
753
- const { Deployment } = await import("./deployment")
754
-
755
- return Deployment.patch(
756
- name,
757
- {
758
- ...deepmerge(args, args.deployment),
759
- name: args.existing.metadata.name,
760
- namespace: Namespace.forResourceAsync(args.existing, output(args.namespace).cluster),
761
- },
762
- opts,
763
- )
764
- }
765
-
766
- if (args.existing?.kind === "StatefulSet") {
767
- const { StatefulSet } = await import("./stateful-set")
768
-
769
- return StatefulSet.patch(
770
- name,
771
- {
772
- ...deepmerge(args, args.statefulSet),
773
- name: args.existing.metadata.name,
774
- namespace: Namespace.forResourceAsync(args.existing, output(args.namespace).cluster),
775
- },
776
- opts,
777
- )
778
- }
779
-
780
- if (args.defaultType === "Deployment") {
781
- const { Deployment } = await import("./deployment")
782
-
783
- return Deployment.create(name, deepmerge(args, args.deployment), opts)
784
- }
785
-
786
- if (args.defaultType === "StatefulSet") {
787
- const { StatefulSet } = await import("./stateful-set")
1044
+ const cronJobArgs = deepmerge(baseArgs, args.cronJob ?? {}) as JobArgs
788
1045
 
789
- return StatefulSet.create(name, deepmerge(args, args.statefulSet), opts)
1046
+ return CronJob.create(name, cronJobArgs, opts)
790
1047
  }
791
1048
 
792
1049
  throw new Error(`Unknown workload type: ${args.defaultType as string}`)