@highstate/common 0.9.16 → 0.9.19

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 (48) hide show
  1. package/dist/{chunk-HZBJ6LLS.js → chunk-WDYIUWYZ.js} +659 -267
  2. package/dist/chunk-WDYIUWYZ.js.map +1 -0
  3. package/dist/highstate.manifest.json +12 -8
  4. package/dist/index.js +1 -1
  5. package/dist/units/access-point/index.js +16 -0
  6. package/dist/units/access-point/index.js.map +1 -0
  7. package/dist/units/databases/existing-mariadb/index.js +17 -0
  8. package/dist/units/databases/existing-mariadb/index.js.map +1 -0
  9. package/dist/units/databases/existing-mongodb/index.js +17 -0
  10. package/dist/units/databases/existing-mongodb/index.js.map +1 -0
  11. package/dist/units/databases/existing-postgresql/index.js +17 -0
  12. package/dist/units/databases/existing-postgresql/index.js.map +1 -0
  13. package/dist/units/dns/record-set/index.js +22 -11
  14. package/dist/units/dns/record-set/index.js.map +1 -1
  15. package/dist/units/existing-server/index.js +12 -12
  16. package/dist/units/existing-server/index.js.map +1 -1
  17. package/dist/units/network/l3-endpoint/index.js +1 -1
  18. package/dist/units/network/l3-endpoint/index.js.map +1 -1
  19. package/dist/units/network/l4-endpoint/index.js +1 -1
  20. package/dist/units/network/l4-endpoint/index.js.map +1 -1
  21. package/dist/units/script/index.js +1 -1
  22. package/dist/units/script/index.js.map +1 -1
  23. package/dist/units/server-dns/index.js +1 -1
  24. package/dist/units/server-dns/index.js.map +1 -1
  25. package/dist/units/server-patch/index.js +1 -1
  26. package/dist/units/server-patch/index.js.map +1 -1
  27. package/dist/units/ssh/key-pair/index.js +6 -6
  28. package/dist/units/ssh/key-pair/index.js.map +1 -1
  29. package/package.json +61 -8
  30. package/src/shared/access-point.ts +110 -0
  31. package/src/shared/command.ts +310 -69
  32. package/src/shared/dns.ts +150 -90
  33. package/src/shared/files.ts +34 -34
  34. package/src/shared/gateway.ts +117 -0
  35. package/src/shared/impl-ref.ts +123 -0
  36. package/src/shared/index.ts +4 -0
  37. package/src/shared/network.ts +41 -27
  38. package/src/shared/passwords.ts +38 -2
  39. package/src/shared/ssh.ts +261 -126
  40. package/src/shared/tls.ts +123 -0
  41. package/src/units/access-point/index.ts +12 -0
  42. package/src/units/databases/existing-mariadb/index.ts +14 -0
  43. package/src/units/databases/existing-mongodb/index.ts +14 -0
  44. package/src/units/databases/existing-postgresql/index.ts +14 -0
  45. package/src/units/dns/record-set/index.ts +21 -11
  46. package/src/units/existing-server/index.ts +12 -17
  47. package/src/units/ssh/key-pair/index.ts +6 -6
  48. package/dist/chunk-HZBJ6LLS.js.map +0 -1
package/src/shared/dns.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import type { ArrayPatchMode, dns, network } from "@highstate/library"
2
+ import { getOrCreate, z } from "@highstate/contract"
2
3
  import {
3
4
  ComponentResource,
4
- normalize,
5
+ interpolate,
6
+ normalizeInputsAndMap,
5
7
  output,
6
- Output,
7
- Resource,
8
+ type Output,
8
9
  toPromise,
9
10
  type Input,
10
11
  type InputArray,
@@ -12,7 +13,7 @@ import {
12
13
  type ResourceOptions,
13
14
  type Unwrap,
14
15
  } from "@highstate/pulumi"
15
- import { capitalize, groupBy, uniqueBy } from "remeda"
16
+ import { flat, groupBy, uniqueBy } from "remeda"
16
17
  import { Command, type CommandHost } from "./command"
17
18
  import {
18
19
  filterEndpoints,
@@ -21,6 +22,13 @@ import {
21
22
  parseL3Endpoint,
22
23
  type InputL3Endpoint,
23
24
  } from "./network"
25
+ import { ImplementationMediator } from "./impl-ref"
26
+
27
+ export const dnsRecordMediator = new ImplementationMediator(
28
+ "dns-record",
29
+ z.object({ name: z.string(), args: z.custom<ResolvedDnsRecordArgs>() }),
30
+ z.instanceof(ComponentResource),
31
+ )
24
32
 
25
33
  export type DnsRecordArgs = {
26
34
  /**
@@ -74,23 +82,23 @@ export type DnsRecordArgs = {
74
82
  waitAt?: InputOrArray<CommandHost>
75
83
  }
76
84
 
77
- export type ResolvedDnsRecordArgs = Unwrap<Omit<DnsRecordArgs, "value" | "type">> & {
85
+ export type ResolvedDnsRecordArgs = Pick<DnsRecordArgs, "name" | "priority" | "ttl" | "proxied"> & {
78
86
  /**
79
87
  * The value of the DNS record.
80
88
  */
81
- value: string
89
+ value: Input<string>
82
90
 
83
91
  /**
84
92
  * The type of the DNS record.
85
93
  */
86
- type: string
94
+ type: Input<string>
87
95
  }
88
96
 
89
97
  export type DnsRecordSetArgs = Omit<DnsRecordArgs, "provider" | "value"> & {
90
98
  /**
91
99
  * The DNS providers to use to create the DNS records.
92
100
  *
93
- * If multiple providers matched the specified domain, multiple DNS records will be created.
101
+ * If multiple providers matched the specified domain, records will be created for each of them.
94
102
  */
95
103
  providers: Input<dns.Provider[]>
96
104
 
@@ -116,125 +124,177 @@ function getTypeByEndpoint(endpoint: network.L3Endpoint): string {
116
124
  }
117
125
  }
118
126
 
119
- export abstract class DnsRecord extends ComponentResource {
127
+ /**
128
+ * Creates a DNS record for the specified value and waits for it to be resolved.
129
+ *
130
+ * Uses the specified DNS provider to create the record.
131
+ */
132
+ export class DnsRecord extends ComponentResource {
120
133
  /**
121
134
  * The underlying dns record resource.
122
135
  */
123
- public readonly dnsRecord: Output<Resource>
136
+ readonly dnsRecord: Output<ComponentResource>
124
137
 
125
138
  /**
126
- * The wait commands to be executed after the DNS record is created/updated.
139
+ * The commands to be executed after the DNS record is created/updated.
127
140
  *
128
- * Use this field as a dependency for other resources.
141
+ * These commands will wait for the DNS record to be resolved to the specified value.
129
142
  */
130
- public readonly waitCommands: Output<Command[]>
143
+ readonly waitCommands: Output<Command[]>
131
144
 
132
- protected constructor(name: string, args: DnsRecordArgs, opts?: ResourceOptions) {
145
+ constructor(name: string, args: DnsRecordArgs, opts?: ResourceOptions) {
133
146
  super("highstate:common:DnsRecord", name, args, opts)
134
147
 
135
- this.dnsRecord = output(args).apply(args => {
136
- const l3Endpoint = parseL3Endpoint(args.value)
137
- const type = args.type ?? getTypeByEndpoint(l3Endpoint)
138
-
139
- return output(
140
- this.create(
141
- name,
142
- {
143
- ...args,
144
- type,
145
- value: l3EndpointToString(l3Endpoint),
146
- },
147
- { ...opts, parent: this },
148
- ),
149
- )
148
+ const l3Endpoint = output(args.value).apply(value => parseL3Endpoint(value))
149
+ const resolvedValue = l3Endpoint.apply(l3EndpointToString)
150
+ const resolvedType = args.type ? output(args.type) : l3Endpoint.apply(getTypeByEndpoint)
151
+
152
+ this.dnsRecord = output(args.provider).apply(provider => {
153
+ return dnsRecordMediator.call(provider.implRef, {
154
+ name,
155
+ args: {
156
+ name: args.name,
157
+ priority: args.priority,
158
+ ttl: args.ttl,
159
+ value: resolvedValue,
160
+ type: resolvedType,
161
+ },
162
+ })
150
163
  })
151
164
 
152
- this.waitCommands = output(args).apply(args => {
153
- const waitAt = args.waitAt ? (Array.isArray(args.waitAt) ? args.waitAt : [args.waitAt]) : []
154
-
155
- return waitAt.map(host => {
165
+ this.waitCommands = output({
166
+ waitAt: args.waitAt,
167
+ resolvedType,
168
+ proxied: args.proxied,
169
+ }).apply(({ waitAt, resolvedType, proxied }) => {
170
+ if (resolvedType === "CNAME") {
171
+ // TODO: handle CNAME records
172
+ return []
173
+ }
174
+
175
+ const resolvedHosts = waitAt ? [waitAt].flat() : []
176
+
177
+ if (proxied) {
178
+ // for proxied records do not verify the value since we do not know the actual IP addressa
179
+
180
+ return (resolvedHosts as Unwrap<CommandHost>[]).map(host => {
181
+ const hostname = host === "local" ? "local" : host.hostname
182
+
183
+ return new Command(
184
+ `${name}.wait-for-dns.${hostname}`,
185
+ {
186
+ host,
187
+ create: [
188
+ interpolate`while ! getent hosts "${args.name}";`,
189
+ interpolate`do echo "Waiting for DNS record \"${args.name}\" to be available...";`,
190
+ `sleep 5;`,
191
+ `done`,
192
+ ],
193
+ },
194
+ { parent: this },
195
+ )
196
+ })
197
+ }
198
+
199
+ return (resolvedHosts as Unwrap<CommandHost>[]).map(host => {
156
200
  const hostname = host === "local" ? "local" : host.hostname
157
201
 
158
202
  return new Command(
159
- `${name}-wait-${hostname}`,
203
+ `${name}.wait-for-dns.${hostname}`,
160
204
  {
161
205
  host,
162
- create: `while ! getent hosts ${args.name} >/dev/null; do echo "Waiting for DNS record ${args.name} to be created"; sleep 5; done`,
163
- triggers: [args.type, args.ttl, args.priority, args.proxied],
206
+ create: [
207
+ interpolate`while ! getent hosts "${args.name}" | grep "${resolvedValue}";`,
208
+ interpolate`do echo "Waiting for DNS record \"${args.name}" to resolve to "${resolvedValue}"...";`,
209
+ `sleep 5;`,
210
+ `done`,
211
+ ],
164
212
  },
165
213
  { parent: this },
166
214
  )
167
215
  })
168
216
  })
169
217
  }
170
-
171
- protected abstract create(
172
- name: string,
173
- args: ResolvedDnsRecordArgs,
174
- opts?: ResourceOptions,
175
- ): Input<Resource>
176
-
177
- static create(name: string, args: DnsRecordArgs, opts?: ResourceOptions): Output<DnsRecord> {
178
- return output(args).apply(async args => {
179
- const providerType = args.provider.type
180
- const implName = `${capitalize(providerType)}DnsRecord`
181
- const implModule = (await import(`@highstate/${providerType}`)) as Record<string, unknown>
182
-
183
- const implClass = implModule[implName] as new (
184
- name: string,
185
- args: Unwrap<DnsRecordArgs>,
186
- opts?: ResourceOptions,
187
- ) => DnsRecord
188
-
189
- return new implClass(name, args, opts)
190
- })
191
- }
192
218
  }
193
219
 
220
+ /**
221
+ * Creates a set of DNS records for the specified values and waits for them to be resolved.
222
+ */
194
223
  export class DnsRecordSet extends ComponentResource {
195
224
  /**
196
225
  * The underlying dns record resources.
197
226
  */
198
- public readonly dnsRecords: Output<DnsRecord[]>
227
+ readonly dnsRecords: Output<DnsRecord[]>
199
228
 
200
229
  /**
201
- * The wait commands to be executed after the DNS records are created/updated.
230
+ * The flat list of all wait commands for the DNS records.
202
231
  */
203
- public readonly waitCommands: Output<Command[]>
232
+ readonly waitCommands: Output<Command[]>
204
233
 
205
- private constructor(name: string, records: Output<DnsRecord[]>, opts?: ResourceOptions) {
206
- super("highstate:common:DnsRecordSet", name, records, opts)
234
+ constructor(name: string, args: DnsRecordSetArgs, opts?: ResourceOptions) {
235
+ super("highstate:common:DnsRecordSet", name, args, opts)
207
236
 
208
- this.dnsRecords = records
237
+ const matchedProviders = output({
238
+ providers: args.providers,
239
+ name: args.name ?? name,
240
+ }).apply(({ providers }) => {
241
+ const matchedProviders = providers.filter(provider => name.endsWith(provider.domain))
209
242
 
210
- this.waitCommands = records.apply(records =>
211
- records.flatMap(record => record.waitCommands),
212
- ) as unknown as Output<Command[]>
213
- }
243
+ if (matchedProviders.length === 0) {
244
+ throw new Error(`No DNS provider matched the domain "${name}"`)
245
+ }
214
246
 
215
- static create(name: string, args: DnsRecordSetArgs, opts?: ResourceOptions): DnsRecordSet {
216
- const records = output(args).apply(args => {
217
- const recordName = args.name ?? name
218
- const values = normalize(args.value, args.values)
219
-
220
- return output(
221
- args.providers
222
- .filter(provider => recordName.endsWith(provider.domain))
223
- .flatMap(provider => {
224
- return values.map(value => {
225
- const l3Endpoint = parseL3Endpoint(value)
226
-
227
- return DnsRecord.create(
228
- `${provider.type}-from-${recordName}-to-${l3EndpointToString(l3Endpoint)}`,
229
- { name: recordName, ...args, value: l3Endpoint, provider },
230
- opts,
231
- )
232
- })
233
- }),
234
- )
247
+ return matchedProviders
235
248
  })
236
249
 
237
- return new DnsRecordSet(name, records, opts)
250
+ this.dnsRecords = normalizeInputsAndMap(args.value, args.values, value => {
251
+ return output({
252
+ name: args.name ?? name,
253
+ providers: matchedProviders,
254
+ }).apply(({ name, providers }) => {
255
+ return providers.flatMap(provider => {
256
+ const l3Endpoint = parseL3Endpoint(value)
257
+
258
+ return new DnsRecord(
259
+ `${name}.${provider.id}.${l3EndpointToString(l3Endpoint)}`,
260
+ {
261
+ name,
262
+ provider,
263
+ value: l3Endpoint,
264
+ type: args.type ?? getTypeByEndpoint(l3Endpoint),
265
+ proxied: args.proxied,
266
+ ttl: args.ttl,
267
+ priority: args.priority,
268
+ waitAt: args.waitAt,
269
+ },
270
+ { parent: this },
271
+ )
272
+ })
273
+ })
274
+ }).apply(flat)
275
+
276
+ this.waitCommands = this.dnsRecords
277
+ .apply(records => records.flatMap(record => record.waitCommands))
278
+ .apply(flat)
279
+ }
280
+
281
+ private static readonly dnsRecordSetCache = new Map<string, DnsRecordSet>()
282
+
283
+ /**
284
+ * Creates a DNS record set for the specified endpoints and waits for it to be resolved.
285
+ *
286
+ * If a DNS record set with the same name already exists, it will be reused.
287
+ *
288
+ * @param name The name of the DNS record set.
289
+ * @param args The arguments for the DNS record set.
290
+ * @param opts The options for the resource.
291
+ */
292
+ static createOnce(name: string, args: DnsRecordSetArgs, opts?: ResourceOptions): DnsRecordSet {
293
+ return getOrCreate(
294
+ DnsRecordSet.dnsRecordSetCache,
295
+ name,
296
+ () => new DnsRecordSet(name, args, opts),
297
+ )
238
298
  }
239
299
  }
240
300
 
@@ -268,7 +328,7 @@ export async function updateEndpointsWithFqdn<TEndpoint extends network.L34Endpo
268
328
 
269
329
  const filteredEndpoints = filterEndpoints(resolvedEndpoints, fqdnEndpointFilter)
270
330
 
271
- const dnsRecordSet = DnsRecordSet.create(fqdn, {
331
+ const dnsRecordSet = new DnsRecordSet(fqdn, {
272
332
  providers: dnsProviders,
273
333
  values: filteredEndpoints,
274
334
  waitAt: "local",
@@ -1,18 +1,17 @@
1
1
  import type { common, network } from "@highstate/library"
2
- import { tmpdir } from "node:os"
3
- import { cp, mkdir, mkdtemp, rm, writeFile, rename, stat } from "node:fs/promises"
4
- import { join, extname, basename, dirname } from "node:path"
2
+ import { createHash } from "node:crypto"
5
3
  import { createReadStream } from "node:fs"
6
- import { pipeline } from "node:stream/promises"
4
+ import { cp, mkdir, mkdtemp, rename, rm, stat, writeFile } from "node:fs/promises"
5
+ import { tmpdir } from "node:os"
6
+ import { basename, dirname, extname, join } from "node:path"
7
7
  import { Readable } from "node:stream"
8
- import { createHash } from "node:crypto"
8
+ import { pipeline } from "node:stream/promises"
9
+ import { type CommonObjectMeta, type File, HighstateSignature } from "@highstate/contract"
10
+ import { asset, toPromise } from "@highstate/pulumi"
9
11
  import { minimatch } from "minimatch"
10
-
11
- import { HighstateSignature, type File } from "@highstate/contract"
12
- import { asset, toPromise, type ObjectMeta } from "@highstate/pulumi"
13
12
  import * as tar from "tar"
14
13
  import unzipper from "unzipper"
15
- import { l7EndpointToString, parseL7Endpoint, type InputL7Endpoint } from "./network"
14
+ import { type InputL7Endpoint, l7EndpointToString, parseL7Endpoint } from "./network"
16
15
 
17
16
  export type FolderPackOptions = {
18
17
  /**
@@ -50,7 +49,7 @@ export function assetFromFile(file: common.File): asset.Asset {
50
49
  )
51
50
  }
52
51
 
53
- if (file.meta.isBinary) {
52
+ if (file.content.isBinary) {
54
53
  throw new Error(
55
54
  "Cannot create asset from inline binary file content. Please open an issue if you need this feature.",
56
55
  )
@@ -171,12 +170,16 @@ export class MaterializedFile implements AsyncDisposable {
171
170
  private _path!: string
172
171
  private _disposed = false
173
172
 
174
- readonly artifactMeta: ObjectMeta = {}
173
+ readonly artifactMeta: CommonObjectMeta
175
174
 
176
175
  constructor(
177
176
  readonly entity: common.File,
178
177
  readonly parent?: MaterializedFolder,
179
- ) {}
178
+ ) {
179
+ this.artifactMeta = {
180
+ title: `Materialized file "${entity.meta.name}"`,
181
+ }
182
+ }
180
183
 
181
184
  get path(): string {
182
185
  return this._path
@@ -196,7 +199,7 @@ export class MaterializedFile implements AsyncDisposable {
196
199
 
197
200
  switch (this.entity.content.type) {
198
201
  case "embedded": {
199
- const content = this.entity.meta.isBinary
202
+ const content = this.entity.content.isBinary
200
203
  ? Buffer.from(this.entity.content.value, "base64")
201
204
  : this.entity.content.value
202
205
 
@@ -208,7 +211,7 @@ export class MaterializedFile implements AsyncDisposable {
208
211
  break
209
212
  }
210
213
  case "remote": {
211
- const response = await load(l7EndpointToString(this.entity.content.endpoint))
214
+ const response = await fetch(l7EndpointToString(this.entity.content.endpoint))
212
215
  if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`)
213
216
 
214
217
  const arrayBuffer = await response.arrayBuffer()
@@ -217,16 +220,14 @@ export class MaterializedFile implements AsyncDisposable {
217
220
  break
218
221
  }
219
222
  case "artifact": {
220
- const artifactData = this.entity.content[HighstateSignature.Artifact]
221
223
  const artifactPath = process.env.HIGHSTATE_ARTIFACT_READ_PATH
222
-
223
224
  if (!artifactPath) {
224
225
  throw new Error(
225
226
  "HIGHSTATE_ARTIFACT_READ_PATH environment variable is not set but required for artifact content",
226
227
  )
227
228
  }
228
229
 
229
- const tgzPath = join(artifactPath, `${artifactData.hash}.tgz`)
230
+ const tgzPath = join(artifactPath, `${this.entity.content.hash}.tgz`)
230
231
 
231
232
  // extract the tgz file directly to the target path
232
233
  const readStream = createReadStream(tgzPath)
@@ -303,7 +304,6 @@ export class MaterializedFile implements AsyncDisposable {
303
304
  name: this.entity.meta.name,
304
305
  mode: fileStats.mode & 0o777, // extract only permission bits
305
306
  size: fileStats.size,
306
- isBinary: this.entity.meta.isBinary, // keep original binary flag as we can't reliably detect this from filesystem
307
307
  }
308
308
 
309
309
  // return file entity with artifact content using actual filesystem stats
@@ -311,10 +311,9 @@ export class MaterializedFile implements AsyncDisposable {
311
311
  meta: newMeta,
312
312
  content: {
313
313
  type: "artifact",
314
- [HighstateSignature.Artifact]: {
315
- hash: hashValue,
316
- meta: await toPromise(this.artifactMeta),
317
- },
314
+ [HighstateSignature.Artifact]: true,
315
+ hash: hashValue,
316
+ meta: await toPromise(this.artifactMeta),
318
317
  },
319
318
  }
320
319
  } finally {
@@ -341,7 +340,6 @@ export class MaterializedFile implements AsyncDisposable {
341
340
  name,
342
341
  mode,
343
342
  size: 0,
344
- isBinary: false,
345
343
  },
346
344
  content: {
347
345
  type: "embedded",
@@ -392,12 +390,16 @@ export class MaterializedFolder implements AsyncDisposable {
392
390
 
393
391
  private readonly _disposables: AsyncDisposable[] = []
394
392
 
395
- readonly artifactMeta: ObjectMeta = {}
393
+ readonly artifactMeta: CommonObjectMeta
396
394
 
397
395
  constructor(
398
396
  readonly entity: common.Folder,
399
397
  readonly parent?: MaterializedFolder,
400
- ) {}
398
+ ) {
399
+ this.artifactMeta = {
400
+ title: `Materialized folder "${entity.meta.name}"`,
401
+ }
402
+ }
401
403
 
402
404
  get path(): string {
403
405
  return this._path
@@ -451,7 +453,7 @@ export class MaterializedFolder implements AsyncDisposable {
451
453
  break
452
454
  }
453
455
  case "remote": {
454
- const response = await load(l7EndpointToString(this.entity.content.endpoint))
456
+ const response = await fetch(l7EndpointToString(this.entity.content.endpoint))
455
457
  if (!response.ok) throw new Error(`Failed to fetch: ${response.statusText}`)
456
458
  if (!response.body) throw new Error("Response body is empty")
457
459
 
@@ -491,7 +493,6 @@ export class MaterializedFolder implements AsyncDisposable {
491
493
  break
492
494
  }
493
495
  case "artifact": {
494
- const artifactData = this.entity.content[HighstateSignature.Artifact]
495
496
  const artifactPath = process.env.HIGHSTATE_ARTIFACT_READ_PATH
496
497
 
497
498
  if (!artifactPath) {
@@ -500,7 +501,7 @@ export class MaterializedFolder implements AsyncDisposable {
500
501
  )
501
502
  }
502
503
 
503
- const tgzPath = join(artifactPath, `${artifactData.hash}.tgz`)
504
+ const tgzPath = join(artifactPath, `${this.entity.content.hash}.tgz`)
504
505
 
505
506
  // extract the tgz file directly to the target path
506
507
  const readStream = createReadStream(tgzPath)
@@ -615,11 +616,10 @@ export class MaterializedFolder implements AsyncDisposable {
615
616
  return {
616
617
  meta: newMeta,
617
618
  content: {
619
+ [HighstateSignature.Artifact]: true,
618
620
  type: "artifact",
619
- [HighstateSignature.Artifact]: {
620
- hash: hashValue,
621
- meta: await toPromise(this.artifactMeta),
622
- },
621
+ hash: hashValue,
622
+ meta: await toPromise(this.artifactMeta),
623
623
  },
624
624
  }
625
625
  } finally {
@@ -701,7 +701,7 @@ export async function fetchFileSize(endpoint: network.L7Endpoint): Promise<numbe
701
701
  }
702
702
 
703
703
  const url = l7EndpointToString(endpoint)
704
- const response = await load(url, { method: "HEAD" })
704
+ const response = await fetch(url, { method: "HEAD" })
705
705
 
706
706
  if (!response.ok) {
707
707
  throw new Error(`Failed to fetch file size: ${response.statusText}`)
@@ -713,7 +713,7 @@ export async function fetchFileSize(endpoint: network.L7Endpoint): Promise<numbe
713
713
  }
714
714
 
715
715
  const size = parseInt(contentLength, 10)
716
- if (isNaN(size)) {
716
+ if (Number.isNaN(size)) {
717
717
  throw new Error(`Invalid Content-Length value: ${contentLength}`)
718
718
  }
719
719
 
@@ -0,0 +1,117 @@
1
+ import type { TlsCertificate } from "./tls"
2
+ import { z } from "@highstate/contract"
3
+ import { type common, network } from "@highstate/library"
4
+ import {
5
+ ComponentResource,
6
+ type ComponentResourceOptions,
7
+ type Input,
8
+ type Output,
9
+ output,
10
+ Resource,
11
+ } from "@highstate/pulumi"
12
+ import { ImplementationMediator } from "./impl-ref"
13
+
14
+ export const gatewayRouteMediator = new ImplementationMediator(
15
+ "gateway-route",
16
+ z.object({
17
+ name: z.string(),
18
+ spec: z.custom<GatewayRouteSpec>(),
19
+ opts: z.custom<ComponentResourceOptions>().optional(),
20
+ }),
21
+ z.object({
22
+ resource: z.instanceof(Resource),
23
+ endpoints: network.l3EndpointEntity.schema.array(),
24
+ }),
25
+ )
26
+
27
+ export type GatewayRouteSpec = {
28
+ /**
29
+ * The FQDN to expose the workload on.
30
+ */
31
+ fqdn?: Input<string>
32
+
33
+ /**
34
+ * The endpoints of the backend workload to route traffic to.
35
+ */
36
+ endpoints: Input<network.L4Endpoint[]>
37
+
38
+ /**
39
+ * The native data to pass to the implementation.
40
+ *
41
+ * This is used for data which implementation may natively understand,
42
+ * such as Kubernetes `Service` resources.
43
+ *
44
+ * Implementations may use this data to create more efficient routes
45
+ * using native resources.
46
+ */
47
+ nativeData?: Input<unknown>
48
+
49
+ /**
50
+ * The TLS certificate to use for the route.
51
+ */
52
+ tlsCertificate?: Input<TlsCertificate | undefined>
53
+ } & (
54
+ | {
55
+ type: "http"
56
+
57
+ /**
58
+ * Whether to expose the workload over plain HTTP.
59
+ *
60
+ * By default, the workload will be exposed over HTTPS.
61
+ */
62
+ insecure?: Input<boolean>
63
+
64
+ /**
65
+ * The relative path to expose the workload on.
66
+ *
67
+ * By default, the workload will be exposed on the root path (`/`).
68
+ */
69
+ path?: Input<string>
70
+ }
71
+ | {
72
+ type: "l3"
73
+
74
+ /**
75
+ * The ports to request for the workload.
76
+ *
77
+ * The order must match the order of the ports in the service.
78
+ *
79
+ * If not provided, random ports will be assigned.
80
+ */
81
+ ports?: number
82
+ }
83
+ )
84
+
85
+ export type GatewayRouteArgs = GatewayRouteSpec & {
86
+ /**
87
+ * The gateway to attach the route to.
88
+ */
89
+ gateway: Input<common.Gateway>
90
+ }
91
+
92
+ export class GatewayRoute extends ComponentResource {
93
+ /**
94
+ * The underlying resource created by the implementation.
95
+ */
96
+ readonly resource: Output<Resource>
97
+
98
+ /**
99
+ * The endpoints of the gateway which serve this route.
100
+ *
101
+ * In most cases, this will be a single endpoint of the gateway shared for all routes.
102
+ */
103
+ readonly endpoints: Output<network.L3Endpoint[]>
104
+
105
+ constructor(name: string, args: GatewayRouteArgs, opts?: ComponentResourceOptions) {
106
+ super("highstate:common:GatewayRoute", name, args, opts)
107
+
108
+ const { resource, endpoints } = gatewayRouteMediator.callOutput(output(args.gateway).implRef, {
109
+ name,
110
+ spec: args,
111
+ opts: { ...opts, parent: this },
112
+ })
113
+
114
+ this.resource = resource
115
+ this.endpoints = endpoints
116
+ }
117
+ }