@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.
- package/dist/{chunk-HZBJ6LLS.js → chunk-WDYIUWYZ.js} +659 -267
- package/dist/chunk-WDYIUWYZ.js.map +1 -0
- package/dist/highstate.manifest.json +12 -8
- package/dist/index.js +1 -1
- package/dist/units/access-point/index.js +16 -0
- package/dist/units/access-point/index.js.map +1 -0
- package/dist/units/databases/existing-mariadb/index.js +17 -0
- package/dist/units/databases/existing-mariadb/index.js.map +1 -0
- package/dist/units/databases/existing-mongodb/index.js +17 -0
- package/dist/units/databases/existing-mongodb/index.js.map +1 -0
- package/dist/units/databases/existing-postgresql/index.js +17 -0
- package/dist/units/databases/existing-postgresql/index.js.map +1 -0
- package/dist/units/dns/record-set/index.js +22 -11
- package/dist/units/dns/record-set/index.js.map +1 -1
- package/dist/units/existing-server/index.js +12 -12
- package/dist/units/existing-server/index.js.map +1 -1
- package/dist/units/network/l3-endpoint/index.js +1 -1
- package/dist/units/network/l3-endpoint/index.js.map +1 -1
- package/dist/units/network/l4-endpoint/index.js +1 -1
- package/dist/units/network/l4-endpoint/index.js.map +1 -1
- package/dist/units/script/index.js +1 -1
- package/dist/units/script/index.js.map +1 -1
- package/dist/units/server-dns/index.js +1 -1
- package/dist/units/server-dns/index.js.map +1 -1
- package/dist/units/server-patch/index.js +1 -1
- package/dist/units/server-patch/index.js.map +1 -1
- package/dist/units/ssh/key-pair/index.js +6 -6
- package/dist/units/ssh/key-pair/index.js.map +1 -1
- package/package.json +61 -8
- package/src/shared/access-point.ts +110 -0
- package/src/shared/command.ts +310 -69
- package/src/shared/dns.ts +150 -90
- package/src/shared/files.ts +34 -34
- package/src/shared/gateway.ts +117 -0
- package/src/shared/impl-ref.ts +123 -0
- package/src/shared/index.ts +4 -0
- package/src/shared/network.ts +41 -27
- package/src/shared/passwords.ts +38 -2
- package/src/shared/ssh.ts +261 -126
- package/src/shared/tls.ts +123 -0
- package/src/units/access-point/index.ts +12 -0
- package/src/units/databases/existing-mariadb/index.ts +14 -0
- package/src/units/databases/existing-mongodb/index.ts +14 -0
- package/src/units/databases/existing-postgresql/index.ts +14 -0
- package/src/units/dns/record-set/index.ts +21 -11
- package/src/units/existing-server/index.ts +12 -17
- package/src/units/ssh/key-pair/index.ts +6 -6
- 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
|
-
|
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 {
|
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 =
|
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,
|
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
|
-
|
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
|
-
|
136
|
+
readonly dnsRecord: Output<ComponentResource>
|
124
137
|
|
125
138
|
/**
|
126
|
-
* The
|
139
|
+
* The commands to be executed after the DNS record is created/updated.
|
127
140
|
*
|
128
|
-
*
|
141
|
+
* These commands will wait for the DNS record to be resolved to the specified value.
|
129
142
|
*/
|
130
|
-
|
143
|
+
readonly waitCommands: Output<Command[]>
|
131
144
|
|
132
|
-
|
145
|
+
constructor(name: string, args: DnsRecordArgs, opts?: ResourceOptions) {
|
133
146
|
super("highstate:common:DnsRecord", name, args, opts)
|
134
147
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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(
|
153
|
-
|
154
|
-
|
155
|
-
|
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}-
|
203
|
+
`${name}.wait-for-dns.${hostname}`,
|
160
204
|
{
|
161
205
|
host,
|
162
|
-
create:
|
163
|
-
|
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
|
-
|
227
|
+
readonly dnsRecords: Output<DnsRecord[]>
|
199
228
|
|
200
229
|
/**
|
201
|
-
* The
|
230
|
+
* The flat list of all wait commands for the DNS records.
|
202
231
|
*/
|
203
|
-
|
232
|
+
readonly waitCommands: Output<Command[]>
|
204
233
|
|
205
|
-
|
206
|
-
super("highstate:common:DnsRecordSet", name,
|
234
|
+
constructor(name: string, args: DnsRecordSetArgs, opts?: ResourceOptions) {
|
235
|
+
super("highstate:common:DnsRecordSet", name, args, opts)
|
207
236
|
|
208
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
}
|
243
|
+
if (matchedProviders.length === 0) {
|
244
|
+
throw new Error(`No DNS provider matched the domain "${name}"`)
|
245
|
+
}
|
214
246
|
|
215
|
-
|
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
|
-
|
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
|
331
|
+
const dnsRecordSet = new DnsRecordSet(fqdn, {
|
272
332
|
providers: dnsProviders,
|
273
333
|
values: filteredEndpoints,
|
274
334
|
waitAt: "local",
|
package/src/shared/files.ts
CHANGED
@@ -1,18 +1,17 @@
|
|
1
1
|
import type { common, network } from "@highstate/library"
|
2
|
-
import {
|
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 {
|
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 {
|
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
|
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.
|
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:
|
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.
|
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
|
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, `${
|
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
|
-
|
316
|
-
|
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:
|
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
|
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, `${
|
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
|
-
|
620
|
-
|
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
|
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
|
+
}
|