@highstate/k8s 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/dist/{chunk-FE4SHRAJ.js → chunk-23X5SXQG.js} +22 -7
  2. package/dist/chunk-23X5SXQG.js.map +1 -0
  3. package/dist/{chunk-LGHFSXNT.js → chunk-ADHZK6V2.js} +14 -10
  4. package/dist/chunk-ADHZK6V2.js.map +1 -0
  5. package/dist/{chunk-VCXWCZ43.js → chunk-BTAEFJ5N.js} +27 -15
  6. package/dist/chunk-BTAEFJ5N.js.map +1 -0
  7. package/dist/{chunk-BR2CLUUD.js → chunk-IXE3OKB4.js} +27 -8
  8. package/dist/chunk-IXE3OKB4.js.map +1 -0
  9. package/dist/{chunk-TWBMG6TD.js → chunk-OG2OPX7B.js} +30 -12
  10. package/dist/chunk-OG2OPX7B.js.map +1 -0
  11. package/dist/{chunk-DCUMJSO6.js → chunk-P26SQ2ZB.js} +17 -51
  12. package/dist/chunk-P26SQ2ZB.js.map +1 -0
  13. package/dist/{chunk-MIC2BHGS.js → chunk-PG27ZY2H.js} +25 -7
  14. package/dist/chunk-PG27ZY2H.js.map +1 -0
  15. package/dist/chunk-PZYGZSN5.js +54 -0
  16. package/dist/{chunk-PZ5AY32C.js.map → chunk-PZYGZSN5.js.map} +1 -1
  17. package/dist/{chunk-YIJUVPU2.js → chunk-S77TE7UC.js} +27 -15
  18. package/dist/chunk-S77TE7UC.js.map +1 -0
  19. package/dist/{chunk-P2VOUU7E.js → chunk-SZKOAHNX.js} +383 -205
  20. package/dist/chunk-SZKOAHNX.js.map +1 -0
  21. package/dist/chunk-TOLFVF4S.js +889 -0
  22. package/dist/chunk-TOLFVF4S.js.map +1 -0
  23. package/dist/{chunk-RVB4WWZZ.js → chunk-TVKT3ZYX.js} +174 -18
  24. package/dist/chunk-TVKT3ZYX.js.map +1 -0
  25. package/dist/cron-job-RKB2HYTO.js +7 -0
  26. package/dist/{cron-job-NX4HD4FI.js.map → cron-job-RKB2HYTO.js.map} +1 -1
  27. package/dist/deployment-T35TUOL2.js +7 -0
  28. package/dist/{deployment-O2LJ5WR5.js.map → deployment-T35TUOL2.js.map} +1 -1
  29. package/dist/highstate.manifest.json +3 -2
  30. package/dist/impl/dynamic-endpoint-resolver.js +90 -0
  31. package/dist/impl/dynamic-endpoint-resolver.js.map +1 -0
  32. package/dist/impl/gateway-route.js +159 -62
  33. package/dist/impl/gateway-route.js.map +1 -1
  34. package/dist/impl/tls-certificate.js +6 -5
  35. package/dist/impl/tls-certificate.js.map +1 -1
  36. package/dist/index.js +106 -23
  37. package/dist/index.js.map +1 -1
  38. package/dist/job-PE4AKOHB.js +7 -0
  39. package/dist/job-PE4AKOHB.js.map +1 -0
  40. package/dist/stateful-set-LUIRHQJY.js +7 -0
  41. package/dist/{stateful-set-VJYKTQ72.js.map → stateful-set-LUIRHQJY.js.map} +1 -1
  42. package/dist/units/cert-manager/index.js +7 -8
  43. package/dist/units/cert-manager/index.js.map +1 -1
  44. package/dist/units/cluster-patch/index.js +6 -6
  45. package/dist/units/cluster-patch/index.js.map +1 -1
  46. package/dist/units/dns01-issuer/index.js +52 -15
  47. package/dist/units/dns01-issuer/index.js.map +1 -1
  48. package/dist/units/existing-cluster/index.js +39 -18
  49. package/dist/units/existing-cluster/index.js.map +1 -1
  50. package/dist/units/gateway-api/index.js +2 -2
  51. package/dist/units/reduced-access-cluster/index.js +8 -8
  52. package/dist/units/reduced-access-cluster/index.js.map +1 -1
  53. package/package.json +9 -7
  54. package/src/cluster.ts +12 -8
  55. package/src/config-map.ts +15 -5
  56. package/src/container.ts +4 -2
  57. package/src/cron-job.ts +25 -4
  58. package/src/deployment.ts +32 -17
  59. package/src/gateway/backend.ts +3 -3
  60. package/src/gateway/gateway.ts +12 -56
  61. package/src/helm.ts +354 -22
  62. package/src/impl/dynamic-endpoint-resolver.ts +109 -0
  63. package/src/impl/gateway-route.ts +231 -57
  64. package/src/impl/tls-certificate.ts +8 -3
  65. package/src/index.ts +1 -0
  66. package/src/job.ts +23 -5
  67. package/src/kubectl.ts +166 -0
  68. package/src/namespace.ts +47 -3
  69. package/src/network-policy.ts +1 -1
  70. package/src/pvc.ts +12 -2
  71. package/src/rbac.ts +28 -5
  72. package/src/scripting/environment.ts +3 -2
  73. package/src/secret.ts +15 -5
  74. package/src/service.ts +28 -6
  75. package/src/shared.ts +30 -2
  76. package/src/stateful-set.ts +32 -17
  77. package/src/tls.ts +31 -5
  78. package/src/units/cluster-patch/index.ts +5 -5
  79. package/src/units/dns01-issuer/index.ts +56 -12
  80. package/src/units/existing-cluster/index.ts +36 -15
  81. package/src/units/reduced-access-cluster/index.ts +6 -3
  82. package/src/worker.ts +4 -2
  83. package/src/workload.ts +453 -213
  84. package/dist/chunk-4G6LLC2X.js +0 -240
  85. package/dist/chunk-4G6LLC2X.js.map +0 -1
  86. package/dist/chunk-BR2CLUUD.js.map +0 -1
  87. package/dist/chunk-DCUMJSO6.js.map +0 -1
  88. package/dist/chunk-FE4SHRAJ.js.map +0 -1
  89. package/dist/chunk-KMLRI5UZ.js +0 -155
  90. package/dist/chunk-KMLRI5UZ.js.map +0 -1
  91. package/dist/chunk-LGHFSXNT.js.map +0 -1
  92. package/dist/chunk-MIC2BHGS.js.map +0 -1
  93. package/dist/chunk-OBDQONMV.js +0 -401
  94. package/dist/chunk-OBDQONMV.js.map +0 -1
  95. package/dist/chunk-P2VOUU7E.js.map +0 -1
  96. package/dist/chunk-PZ5AY32C.js +0 -9
  97. package/dist/chunk-RVB4WWZZ.js.map +0 -1
  98. package/dist/chunk-TWBMG6TD.js.map +0 -1
  99. package/dist/chunk-VCXWCZ43.js.map +0 -1
  100. package/dist/chunk-YIJUVPU2.js.map +0 -1
  101. package/dist/cron-job-NX4HD4FI.js +0 -8
  102. package/dist/deployment-O2LJ5WR5.js +0 -8
  103. package/dist/job-SYME6Y43.js +0 -8
  104. package/dist/job-SYME6Y43.js.map +0 -1
  105. package/dist/stateful-set-VJYKTQ72.js +0 -8
@@ -1,23 +1,20 @@
1
1
  import type { Secret } from "../secret"
2
- import { type GatewayRouteSpec, gatewayRouteMediator, type TlsCertificate } from "@highstate/common"
3
- import { k8s, type network } from "@highstate/library"
2
+ import { gatewayRouteMediator, type TlsCertificate } from "@highstate/common"
3
+ import { type common, k8s, type network } from "@highstate/library"
4
4
  import { type ComponentResourceOptions, type Input, toPromise } from "@highstate/pulumi"
5
5
  import { core } from "@pulumi/kubernetes"
6
6
  import { Gateway, HttpRoute, TcpRoute, UdpRoute } from "../gateway"
7
7
  import { Namespace } from "../namespace"
8
- import { l4EndpointToServicePort, Service } from "../service"
8
+ import { isEndpointFromCluster, l4EndpointToServicePort, Service } from "../service"
9
9
  import { getProvider, mapMetadata } from "../shared"
10
10
  import { Certificate } from "../tls"
11
11
 
12
12
  export const createGatewayRoute = gatewayRouteMediator.implement(
13
13
  k8s.gatewayDataSchema,
14
- async ({ name, spec, opts }, data) => {
15
- const namespace =
16
- spec.nativeData instanceof Service
17
- ? await toPromise(spec.nativeData.namespace)
18
- : Namespace.for(data.namespace, data.cluster)
14
+ async ({ name, args: spec, opts }, data) => {
15
+ const namespace = resolveRouteNamespace(spec, data)
19
16
 
20
- const certSecret = await getCertificateSecret(name, namespace, spec.tlsCertificate)
17
+ const certSecret = await getCertificateSecret(name, namespace, spec.certificate)
21
18
 
22
19
  const certificateRef = certSecret
23
20
  ? {
@@ -27,7 +24,9 @@ export const createGatewayRoute = gatewayRouteMediator.implement(
27
24
  }
28
25
  : undefined
29
26
 
30
- if (spec.type === "http") {
27
+ const routeProtocol = resolveRouteProtocol(spec)
28
+
29
+ if (routeProtocol === "http") {
31
30
  return await createHttpGatewayRoute({
32
31
  name,
33
32
  spec,
@@ -38,25 +37,48 @@ export const createGatewayRoute = gatewayRouteMediator.implement(
38
37
  })
39
38
  }
40
39
 
41
- const protocol = spec.type === "tcp" ? "TCP" : "UDP"
42
-
43
40
  return await createL4GatewayRoute({
44
41
  name,
45
42
  spec,
46
43
  opts,
47
44
  data,
48
45
  namespace,
49
- protocol,
46
+ protocol: routeProtocol === "tcp" ? "TCP" : "UDP",
50
47
  })
51
48
  },
52
49
  )
53
50
 
54
- type HttpGatewayRouteSpec = Extract<GatewayRouteSpec, { type: "http" }>
55
- type L4GatewayRouteSpec = Extract<GatewayRouteSpec, { type: "tcp" | "udp" }>
51
+ type GatewayRouteRuleSpec = {
52
+ type: "http" | "tcp" | "udp"
53
+ paths: string[]
54
+ backends: {
55
+ endpoints: network.L4Endpoint[]
56
+ weight?: number
57
+ }[]
58
+ }
59
+
60
+ function resolveRouteNamespace(spec: GatewayRouteSpec, data: k8s.GatewayData): Namespace {
61
+ const metadataNamespace = spec.metadata["k8s.namespace"]
62
+
63
+ if (metadataNamespace instanceof Namespace) {
64
+ return metadataNamespace
65
+ }
66
+
67
+ return Namespace.for(data.namespace, data.cluster)
68
+ }
69
+
70
+ type GatewayRouteSpec = {
71
+ gateway: common.Gateway
72
+ metadata: Record<string, unknown>
73
+ fqdns: string[]
74
+ certificate?: TlsCertificate
75
+ port?: number
76
+ rules: Record<string, GatewayRouteRuleSpec>
77
+ }
56
78
 
57
79
  type CreateHttpGatewayRouteArgs = {
58
80
  name: string
59
- spec: HttpGatewayRouteSpec
81
+ spec: GatewayRouteSpec
60
82
  opts: ComponentResourceOptions | undefined
61
83
  data: k8s.GatewayData
62
84
  namespace: Namespace
@@ -77,17 +99,16 @@ async function createHttpGatewayRoute({
77
99
  namespace,
78
100
  certificateRef,
79
101
  }: CreateHttpGatewayRouteArgs) {
80
- const backendService =
81
- spec.nativeData instanceof Service
82
- ? spec.nativeData
83
- : (await createServiceFromEndpoints(name, namespace, spec.endpoints, data.cluster, opts))
84
- .service
102
+ const listenerPort = spec.port ?? (certificateRef ? data.httpsPort : data.httpPort)
103
+ const listenerProtocol = certificateRef ? "HTTPS" : "HTTP"
104
+ const listenerHostname = spec.fqdns[0]
85
105
 
86
106
  const listeners = [
87
107
  {
88
- name: "https",
89
- port: data.httpsPort,
90
- protocol: "HTTPS",
108
+ name: `${listenerProtocol.toLowerCase()}-${listenerPort}`,
109
+ port: listenerPort,
110
+ protocol: listenerProtocol,
111
+ hostname: listenerHostname,
91
112
  tls: {
92
113
  mode: "Terminate",
93
114
  certificateRefs: certificateRef ? [certificateRef] : undefined,
@@ -95,9 +116,9 @@ async function createHttpGatewayRoute({
95
116
  },
96
117
  ]
97
118
 
98
- const gateway = await Gateway.createOnce(
119
+ const gateway = Gateway.create(
120
+ name,
99
121
  {
100
- name: data.className,
101
122
  namespace,
102
123
  gatewayClassName: data.className,
103
124
  listeners,
@@ -105,13 +126,50 @@ async function createHttpGatewayRoute({
105
126
  opts,
106
127
  )
107
128
 
129
+ const ruleSpecs = Object.values(spec.rules)
130
+
131
+ const rules = await Promise.all(
132
+ ruleSpecs.map(async (ruleSpec, ruleIndex) => {
133
+ const backendRefs = await Promise.all(
134
+ ruleSpec.backends.map(async (backend, backendIndex) => {
135
+ const backendData = await resolveBackendFromEndpoints({
136
+ routeName: name,
137
+ backendName: `${ruleIndex}-${backendIndex}`,
138
+ endpoints: backend.endpoints,
139
+ namespace,
140
+ cluster: data.cluster,
141
+ opts,
142
+ })
143
+
144
+ const backendPort = await selectBackendPort({
145
+ ports: backendData.ports,
146
+ protocol: "TCP",
147
+ targetPort: backendData.preferredTargetPort,
148
+ serviceName: backendData.serviceName,
149
+ routeName: name,
150
+ })
151
+
152
+ return {
153
+ name: backendData.serviceName,
154
+ namespace: backendData.serviceNamespace,
155
+ port: backendPort.port,
156
+ }
157
+ }),
158
+ )
159
+
160
+ return {
161
+ matches: ruleSpec.paths,
162
+ backends: backendRefs,
163
+ }
164
+ }),
165
+ )
166
+
108
167
  const httpRoute = new HttpRoute(
109
168
  name,
110
169
  {
111
170
  gateway,
112
- rule: {
113
- backend: backendService,
114
- },
171
+ hostnames: spec.fqdns,
172
+ rules,
115
173
  },
116
174
  opts,
117
175
  )
@@ -124,7 +182,7 @@ async function createHttpGatewayRoute({
124
182
 
125
183
  type CreateL4GatewayRouteArgs = {
126
184
  name: string
127
- spec: L4GatewayRouteSpec
185
+ spec: GatewayRouteSpec
128
186
  opts: ComponentResourceOptions | undefined
129
187
  data: k8s.GatewayData
130
188
  namespace: Namespace
@@ -139,21 +197,28 @@ async function createL4GatewayRoute({
139
197
  namespace,
140
198
  protocol,
141
199
  }: CreateL4GatewayRouteArgs) {
142
- const serviceData =
143
- spec.nativeData instanceof Service
144
- ? {
145
- service: spec.nativeData,
146
- ports: await getServicePorts(spec.nativeData),
147
- }
148
- : await createServiceFromEndpoints(name, namespace, spec.endpoints, data.cluster, opts)
200
+ const backends = Object.values(spec.rules).flatMap(rule => rule.backends)
201
+
202
+ if (backends.length === 0) {
203
+ throw new Error(`Gateway route "${name}" has no backends to expose.`)
204
+ }
149
205
 
150
- const serviceName = await toPromise(serviceData.service.metadata.name)
206
+ const firstBackend = backends[0]
207
+
208
+ const backendData = await resolveBackendFromEndpoints({
209
+ routeName: name,
210
+ backendName: "0",
211
+ endpoints: firstBackend.endpoints,
212
+ namespace,
213
+ cluster: data.cluster,
214
+ opts,
215
+ })
151
216
 
152
217
  const backendPort = await selectBackendPort({
153
- ports: serviceData.ports,
218
+ ports: backendData.ports,
154
219
  protocol,
155
- targetPort: spec.targetPort,
156
- serviceName,
220
+ targetPort: backendData.preferredTargetPort,
221
+ serviceName: backendData.serviceName,
157
222
  routeName: name,
158
223
  })
159
224
 
@@ -166,9 +231,13 @@ async function createL4GatewayRoute({
166
231
 
167
232
  const listenerName = `${protocol.toLowerCase()}-${listenerPort}`
168
233
 
169
- const gateway = await Gateway.createOnce(
234
+ const gatewayName = name
235
+ const gatewayResourceName = name
236
+
237
+ const gateway = Gateway.create(
238
+ gatewayResourceName,
170
239
  {
171
- name: data.className,
240
+ name: gatewayName,
172
241
  namespace,
173
242
  gatewayClassName: data.className,
174
243
  listeners: [
@@ -182,19 +251,11 @@ async function createL4GatewayRoute({
182
251
  opts,
183
252
  )
184
253
 
185
- const backendRef = serviceData.service.metadata.apply(metadata => {
186
- if (!metadata?.name) {
187
- throw new Error(
188
- `Service "${serviceName}" referenced by gateway route "${name}" does not have a name.`,
189
- )
190
- }
191
-
192
- return {
193
- name: metadata.name,
194
- namespace: metadata.namespace,
195
- port: backendPort.port,
196
- }
197
- })
254
+ const backendRef = {
255
+ name: backendData.serviceName,
256
+ namespace: backendData.serviceNamespace,
257
+ port: backendPort.port,
258
+ }
198
259
 
199
260
  const routeOpts = { ...opts, parent: gateway }
200
261
 
@@ -254,6 +315,119 @@ async function getCertificateSecret(
254
315
  )
255
316
  }
256
317
 
318
+ function resolveRouteProtocol(spec: GatewayRouteSpec): GatewayRouteRuleSpec["type"] {
319
+ const rules = Object.values(spec.rules)
320
+
321
+ if (rules.length === 0) {
322
+ throw new Error("Gateway route must contain at least one rule")
323
+ }
324
+
325
+ const type = rules[0].type
326
+
327
+ if (rules.some(rule => rule.type !== type)) {
328
+ throw new Error("Gateway route rules must use the same protocol type")
329
+ }
330
+
331
+ return type
332
+ }
333
+
334
+ async function resolveBackendFromEndpoints({
335
+ routeName,
336
+ backendName,
337
+ endpoints,
338
+ namespace,
339
+ cluster,
340
+ opts,
341
+ }: {
342
+ routeName: string
343
+ backendName: string
344
+ endpoints: network.L4Endpoint[]
345
+ namespace: Namespace
346
+ cluster: k8s.Cluster
347
+ opts: ComponentResourceOptions | undefined
348
+ }): Promise<{
349
+ serviceName: string
350
+ serviceNamespace: string
351
+ ports: ServicePortInfo[]
352
+ preferredTargetPort?: number | string
353
+ }> {
354
+ const serviceMeta = getServiceMetadataFromEndpoints(endpoints, cluster)
355
+
356
+ if (serviceMeta) {
357
+ const metadataPorts: ServicePortInfo[] = endpoints.map(endpoint => ({
358
+ name: typeof serviceMeta.targetPort === "string" ? serviceMeta.targetPort : undefined,
359
+ port: endpoint.port,
360
+ protocol: endpoint.protocol.toUpperCase() as "TCP" | "UDP",
361
+ targetPort: serviceMeta.targetPort,
362
+ }))
363
+
364
+ return {
365
+ serviceName: serviceMeta.name,
366
+ serviceNamespace: serviceMeta.namespace,
367
+ ports: metadataPorts,
368
+ preferredTargetPort: serviceMeta.targetPort,
369
+ }
370
+ }
371
+
372
+ const syntheticService = await createServiceFromEndpoints(
373
+ `${routeName}-${backendName}`,
374
+ namespace,
375
+ endpoints,
376
+ cluster,
377
+ opts,
378
+ )
379
+
380
+ const syntheticServiceName = await toPromise(syntheticService.service.metadata.name)
381
+ const syntheticServiceNamespace = await toPromise(syntheticService.service.metadata.namespace)
382
+
383
+ if (!syntheticServiceNamespace) {
384
+ throw new Error(
385
+ `Synthetic backend service "${syntheticServiceName}" for gateway route "${routeName}" has no namespace.`,
386
+ )
387
+ }
388
+
389
+ return {
390
+ serviceName: syntheticServiceName,
391
+ serviceNamespace: syntheticServiceNamespace,
392
+ ports: syntheticService.ports,
393
+ }
394
+ }
395
+
396
+ function getServiceMetadataFromEndpoints(
397
+ endpoints: network.L4Endpoint[],
398
+ cluster: k8s.Cluster,
399
+ ):
400
+ | {
401
+ name: string
402
+ namespace: string
403
+ targetPort?: number | string
404
+ }
405
+ | undefined {
406
+ const serviceEndpoints = endpoints.filter(endpoint => isEndpointFromCluster(endpoint, cluster))
407
+
408
+ if (serviceEndpoints.length === 0 || serviceEndpoints.length !== endpoints.length) {
409
+ return undefined
410
+ }
411
+
412
+ const first = serviceEndpoints[0].metadata["k8s.service"]
413
+
414
+ if (
415
+ serviceEndpoints.some(
416
+ endpoint =>
417
+ endpoint.metadata["k8s.service"].name !== first.name ||
418
+ endpoint.metadata["k8s.service"].namespace !== first.namespace,
419
+ )
420
+ ) {
421
+ return undefined
422
+ }
423
+
424
+ return {
425
+ name: first.name,
426
+ namespace: first.namespace,
427
+ targetPort: first.targetPort,
428
+ }
429
+ }
430
+
257
431
  type ServicePortInfo = {
258
432
  name: string | undefined
259
433
  port: number
@@ -331,7 +505,7 @@ async function createServiceFromEndpoints(
331
505
  }
332
506
  }
333
507
 
334
- async function getServicePorts(service: Service): Promise<ServicePortInfo[]> {
508
+ async function _getServicePorts(service: Service): Promise<ServicePortInfo[]> {
335
509
  const spec = await toPromise(service.spec)
336
510
  const ports = spec.ports ?? []
337
511
 
@@ -9,10 +9,15 @@ export const createCertificate = tlsCertificateMediator.implement(
9
9
  ({ name, spec, opts }, data) => {
10
10
  const provider = getProvider(data.cluster)
11
11
 
12
+ const metadata = spec.metadata as Record<string, unknown> | undefined
13
+ const metadataNamespace = metadata?.["k8s.namespace"]
14
+
12
15
  const namespace =
13
- spec.nativeData instanceof Namespace
14
- ? spec.nativeData
15
- : Namespace.get("cert-manager", { name: "cert-manager", cluster: data.cluster })
16
+ metadataNamespace instanceof Namespace
17
+ ? metadataNamespace
18
+ : metadataNamespace
19
+ ? Namespace.for(metadataNamespace as k8s.Namespace, data.cluster)
20
+ : Namespace.get("cert-manager", { name: "cert-manager", cluster: data.cluster })
16
21
 
17
22
  return Certificate.create(
18
23
  name,
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./dns01-solver"
7
7
  export * from "./gateway"
8
8
  export * from "./helm"
9
9
  export * from "./job"
10
+ export * from "./kubectl"
10
11
  export * from "./namespace"
11
12
  export * from "./network"
12
13
  export * from "./network-policy"
package/src/job.ts CHANGED
@@ -1,12 +1,13 @@
1
- import type { k8s } from "@highstate/library"
2
1
  import type { Container } from "./container"
3
2
  import type { NetworkPolicy } from "./network-policy"
4
3
  import { getOrCreate, type UnitTerminal } from "@highstate/contract"
4
+ import { k8s } from "@highstate/library"
5
5
  import {
6
6
  type ComponentResourceOptions,
7
7
  type Input,
8
8
  type Inputs,
9
9
  interpolate,
10
+ makeEntityOutput,
10
11
  type Output,
11
12
  output,
12
13
  toPromise,
@@ -18,6 +19,7 @@ import { omit } from "remeda"
18
19
  import { Namespace } from "./namespace"
19
20
  import { commonExtraArgs, getProvider, mapMetadata, type ScopedResourceArgs } from "./shared"
20
21
  import {
22
+ filterPatchOwnedContainersInTemplate,
21
23
  getWorkloadComponents,
22
24
  Workload,
23
25
  type WorkloadArgs,
@@ -90,7 +92,17 @@ export abstract class Job extends Workload {
90
92
  * The Highstate job entity.
91
93
  */
92
94
  get entity(): Output<k8s.Job> {
93
- return output(this.entityBase)
95
+ return makeEntityOutput({
96
+ entity: k8s.jobEntity,
97
+ identity: this.metadata.uid,
98
+ meta: {
99
+ title: this.metadata.name,
100
+ },
101
+ value: {
102
+ ...this.entityBase,
103
+ spec: this.spec,
104
+ },
105
+ })
94
106
  }
95
107
 
96
108
  protected getTerminalMeta(): Output<UnitTerminal["meta"]> {
@@ -298,10 +310,16 @@ class JobPatch extends Job {
298
310
  {
299
311
  metadata: mapMetadata(args, name),
300
312
  spec: output({ args, podTemplate }).apply(({ args, podTemplate }) => {
301
- return deepmerge(
313
+ const spec = deepmerge(
302
314
  { template: podTemplate } satisfies types.input.batch.v1.JobSpec,
303
- omit(args, jobExtraArgs) as types.input.batch.v1.JobSpec,
304
- )
315
+ omit(args, jobExtraArgs),
316
+ ) as Unwrap<types.input.batch.v1.JobSpec>
317
+
318
+ if (spec.template) {
319
+ spec.template = filterPatchOwnedContainersInTemplate(spec.template, podTemplate)
320
+ }
321
+
322
+ return spec
305
323
  }),
306
324
  },
307
325
  { ...opts, parent: this, provider: getProvider(cluster) },
package/src/kubectl.ts ADDED
@@ -0,0 +1,166 @@
1
+ import type { k8s } from "@highstate/library"
2
+ import type { InputOrArray } from "@highstate/pulumi"
3
+ import type { Namespace } from "./namespace"
4
+ import type { Workload } from "./workload"
5
+ import { Command, MaterializedFile } from "@highstate/common"
6
+ import {
7
+ ComponentResource,
8
+ type ComponentResourceOptions,
9
+ type Input,
10
+ type Output,
11
+ output,
12
+ } from "@pulumi/pulumi"
13
+ import { images } from "./shared"
14
+
15
+ export type KubeCommandArgs = {
16
+ /**
17
+ * The kubernetes cluster to run the command against.
18
+ */
19
+ cluster: Input<k8s.Cluster>
20
+
21
+ /**
22
+ * The namespace to run the command in, if any.
23
+ */
24
+ namespace?: Input<string>
25
+
26
+ /**
27
+ * The create command to run.
28
+ */
29
+ create: InputOrArray<string>
30
+
31
+ /**
32
+ * The update command to run.
33
+ */
34
+ update?: InputOrArray<string>
35
+
36
+ /**
37
+ * The delete command to run.
38
+ */
39
+ delete?: InputOrArray<string>
40
+ }
41
+
42
+ export type NamespaceKubeCommandArgs = Omit<KubeCommandArgs, "cluster" | "namespace"> & {
43
+ /**
44
+ * The namespace to run the command in.
45
+ */
46
+ namespace: Input<Namespace>
47
+ }
48
+
49
+ export type ExecKubeCommandArgs = Omit<KubeCommandArgs, "cluster" | "namespace"> & {
50
+ /**
51
+ * The workload to exec into.
52
+ */
53
+ workload: Input<Workload>
54
+ }
55
+
56
+ function createCommand(command: string | string[]): string {
57
+ if (Array.isArray(command)) {
58
+ return command.join(" ")
59
+ }
60
+
61
+ return command
62
+ }
63
+
64
+ function buildKubeCommand(
65
+ command: InputOrArray<string>,
66
+ namespace?: Input<string>,
67
+ ): Output<string> {
68
+ if (namespace) {
69
+ return output([command, namespace]).apply(
70
+ ([cmd, ns]) => `kubectl -n ${ns} ${createCommand(cmd)}`,
71
+ )
72
+ }
73
+
74
+ return output(command).apply(cmd => `kubectl ${createCommand(cmd)}`)
75
+ }
76
+
77
+ function buildWorkloadExecCommand(
78
+ command: InputOrArray<string>,
79
+ workload: Input<Workload>,
80
+ ): Output<string> {
81
+ return output({
82
+ command,
83
+ kind: output(workload).kind,
84
+ name: output(workload).metadata.name,
85
+ }).apply(({ command, kind, name }) => {
86
+ const type = kind.toLowerCase()
87
+
88
+ return `exec -it ${type}/${name} -- ${createCommand(command)}`
89
+ })
90
+ }
91
+
92
+ export class KubeCommand extends ComponentResource {
93
+ /**
94
+ * The underlying command that will be executed when this unit is invoked.
95
+ */
96
+ readonly command: Output<Command>
97
+
98
+ /**
99
+ * The standard output of the command.
100
+ */
101
+ readonly stdout: Output<string>
102
+
103
+ /**
104
+ * The standard error of the command.
105
+ */
106
+ readonly stderr: Output<string>
107
+
108
+ constructor(name: string, args: KubeCommandArgs, opts?: ComponentResourceOptions) {
109
+ super("highstate:k8s:KubeCommand", name, args, opts)
110
+
111
+ this.command = output(args.cluster).apply(cluster => {
112
+ const kubeconfig = MaterializedFile.for(cluster.kubeconfig)
113
+
114
+ return new Command(`kubectl-${name}`, {
115
+ host: "local",
116
+ create: buildKubeCommand(args.create, args.namespace),
117
+ update: args.update ? buildKubeCommand(args.update, args.namespace) : undefined,
118
+ delete: args.delete ? buildKubeCommand(args.delete, args.namespace) : undefined,
119
+ files: [kubeconfig],
120
+ image: images["terminal-kubectl"].image,
121
+ containerShell: "bash",
122
+ environment: {
123
+ KUBECONFIG: kubeconfig.path,
124
+ },
125
+ })
126
+ })
127
+
128
+ this.stdout = this.command.stdout
129
+ this.stderr = this.command.stderr
130
+ }
131
+
132
+ static forNamespace(
133
+ name: string,
134
+ args: NamespaceKubeCommandArgs,
135
+ opts?: ComponentResourceOptions,
136
+ ): KubeCommand {
137
+ return new KubeCommand(
138
+ name,
139
+ {
140
+ cluster: output(args.namespace).cluster,
141
+ create: args.create,
142
+ update: args.update,
143
+ delete: args.delete,
144
+ namespace: output(args.namespace).metadata.name,
145
+ },
146
+ opts,
147
+ )
148
+ }
149
+
150
+ static execInto(
151
+ name: string,
152
+ args: ExecKubeCommandArgs,
153
+ opts?: ComponentResourceOptions,
154
+ ): KubeCommand {
155
+ return KubeCommand.forNamespace(
156
+ name,
157
+ {
158
+ namespace: output(args.workload).namespace,
159
+ create: buildWorkloadExecCommand(args.create, args.workload),
160
+ update: args.update ? buildWorkloadExecCommand(args.update, args.workload) : undefined,
161
+ delete: args.delete ? buildWorkloadExecCommand(args.delete, args.workload) : undefined,
162
+ },
163
+ opts,
164
+ )
165
+ }
166
+ }