@highstate/pulumi 0.19.1 → 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.
- package/dist/index.js +206 -117
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/entity.ts +108 -0
- package/src/file.ts +87 -49
- package/src/index.ts +2 -0
- package/src/resource-hooks.ts +14 -0
- package/src/unit.ts +150 -110
package/src/unit.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/** biome-ignore-all lint/suspicious/noExplicitAny: здесь орать запрещено */
|
|
2
2
|
|
|
3
3
|
import type { IsEmptyObject } from "type-fest"
|
|
4
|
+
import { join } from "node:path"
|
|
4
5
|
import { pathToFileURL } from "node:url"
|
|
5
6
|
import {
|
|
6
7
|
type ComponentInput,
|
|
7
8
|
type ComponentInputSpec,
|
|
8
9
|
camelCaseToHumanReadable,
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
type EntityValue,
|
|
11
|
+
type EntityValueInput,
|
|
11
12
|
HighstateConfigKey,
|
|
12
|
-
|
|
13
|
+
HighstateSignature,
|
|
13
14
|
type InstanceStatusField,
|
|
14
15
|
type InstanceStatusFieldValue,
|
|
15
16
|
type PartialKeys,
|
|
@@ -19,8 +20,7 @@ import {
|
|
|
19
20
|
type TriggerInvocation,
|
|
20
21
|
type Unit,
|
|
21
22
|
type UnitArtifact,
|
|
22
|
-
type
|
|
23
|
-
type UnitInputReference,
|
|
23
|
+
type UnitInputValue,
|
|
24
24
|
type UnitPage,
|
|
25
25
|
type UnitTerminal,
|
|
26
26
|
type UnitTrigger,
|
|
@@ -36,10 +36,10 @@ import {
|
|
|
36
36
|
type Output,
|
|
37
37
|
output,
|
|
38
38
|
secret as pulumiSecret,
|
|
39
|
-
StackReference,
|
|
40
39
|
type Unwrap,
|
|
41
40
|
} from "@pulumi/pulumi"
|
|
42
|
-
import { mapValues } from "remeda"
|
|
41
|
+
import { isPlainObject, mapValues } from "remeda"
|
|
42
|
+
import { getHasResourceHooks } from "./resource-hooks"
|
|
43
43
|
import { type DeepInput, toPromise } from "./utils"
|
|
44
44
|
|
|
45
45
|
type StatusField<TArgName extends string = string> = Omit<
|
|
@@ -92,6 +92,7 @@ interface UnitContext<
|
|
|
92
92
|
> {
|
|
93
93
|
args: TArgs
|
|
94
94
|
instanceId: string
|
|
95
|
+
stateId: string
|
|
95
96
|
type: string
|
|
96
97
|
name: string
|
|
97
98
|
|
|
@@ -112,6 +113,12 @@ interface UnitContext<
|
|
|
112
113
|
factory: () => Input<NonNullable<TSecrets[K]>>,
|
|
113
114
|
): Output<NonNullable<TSecrets[K]>>
|
|
114
115
|
|
|
116
|
+
setSecret<K extends keyof TSecrets>(
|
|
117
|
+
this: void,
|
|
118
|
+
name: K,
|
|
119
|
+
value: Input<NonNullable<TSecrets[K]>>,
|
|
120
|
+
): void
|
|
121
|
+
|
|
115
122
|
inputs: TInputs
|
|
116
123
|
invokedTriggers: TriggerInvocation[]
|
|
117
124
|
|
|
@@ -123,24 +130,22 @@ interface UnitContext<
|
|
|
123
130
|
|
|
124
131
|
// z.output since the values are validated/transformed and passed to the user
|
|
125
132
|
type InputSpecToWrappedValue<T extends ComponentInputSpec> = T[2] extends true
|
|
126
|
-
?
|
|
127
|
-
Output<NonNullable<z.output<T[0]["schema"]>>[]>
|
|
133
|
+
? NonNullable<EntityValue<T[0]>>[]
|
|
128
134
|
: T[1] extends true
|
|
129
|
-
?
|
|
130
|
-
:
|
|
135
|
+
? NonNullable<EntityValue<T[0]>>
|
|
136
|
+
: NonNullable<EntityValue<T[0]>> | undefined
|
|
131
137
|
|
|
132
138
|
// z.input since the values are passed from the user and should be validated/transformed before returning from the unit
|
|
133
139
|
type OutputSpecToValue<T extends ComponentInputSpec> = T[2] extends true
|
|
134
140
|
? T[1] extends true
|
|
135
|
-
? NonNullable<
|
|
136
|
-
: NonNullable<
|
|
141
|
+
? NonNullable<EntityValueInput<T[0]>>[]
|
|
142
|
+
: NonNullable<EntityValueInput<T[0]>>[] | undefined
|
|
137
143
|
: T[1] extends true
|
|
138
|
-
? NonNullable<
|
|
139
|
-
: NonNullable<
|
|
140
|
-
|
|
141
|
-
const stackRefMap = new Map<string, StackReference>()
|
|
144
|
+
? NonNullable<EntityValueInput<T[0]>>
|
|
145
|
+
: NonNullable<EntityValueInput<T[0]>> | undefined
|
|
142
146
|
|
|
143
147
|
let instanceId: string | undefined
|
|
148
|
+
let stateId: string | undefined
|
|
144
149
|
let instanceName: string | undefined
|
|
145
150
|
let importBaseUrl: URL | undefined
|
|
146
151
|
|
|
@@ -157,6 +162,20 @@ export function getUnitInstanceId(): string {
|
|
|
157
162
|
return instanceId
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Returns the current unit instance state id.
|
|
167
|
+
*
|
|
168
|
+
* The state id is provided by the runner via Pulumi config.
|
|
169
|
+
* Only available after calling `forUnit` function.
|
|
170
|
+
*/
|
|
171
|
+
export function getUnitStateId(): string {
|
|
172
|
+
if (!stateId) {
|
|
173
|
+
throw new Error(`State id is not set. Did you call "forUnit" function?`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return stateId
|
|
177
|
+
}
|
|
178
|
+
|
|
160
179
|
/**
|
|
161
180
|
* Returns the current unit instance name.
|
|
162
181
|
*/
|
|
@@ -183,78 +202,38 @@ export function getImportBaseUrl(): URL {
|
|
|
183
202
|
* Returns a comment that can be used in resources to indicate that they are managed by Highstate.
|
|
184
203
|
*/
|
|
185
204
|
export function getResourceComment(): string {
|
|
186
|
-
return `Managed by Highstate
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function getStackRef(config: UnitConfig, input: InstanceInput) {
|
|
190
|
-
const [instanceType] = parseInstanceId(input.instanceId)
|
|
191
|
-
const stateId = config.stateIdMap[input.instanceId]
|
|
192
|
-
if (!stateId) {
|
|
193
|
-
throw new Error(`State ID for instance "${input.instanceId}" not found in the unit config.`)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const key = `organization/${instanceType}/${stateId}`
|
|
197
|
-
let stackRef = stackRefMap.get(key)
|
|
198
|
-
|
|
199
|
-
if (!stackRef) {
|
|
200
|
-
stackRef = new StackReference(key)
|
|
201
|
-
stackRefMap.set(key, stackRef)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return stackRef
|
|
205
|
+
return `Managed by Highstate [${getUnitStateId()}]`
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
function
|
|
208
|
-
config: UnitConfig,
|
|
208
|
+
function getInputValue(
|
|
209
209
|
unit: Unit,
|
|
210
210
|
inputName: string,
|
|
211
211
|
input: ComponentInput,
|
|
212
|
-
|
|
212
|
+
entries: UnitInputValue[],
|
|
213
213
|
) {
|
|
214
214
|
const entity = unit.entities.get(input.type)
|
|
215
215
|
if (!entity) {
|
|
216
216
|
throw new Error(`Entity "${input.type}" not found in the unit "${unit.model.type}".`)
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
const
|
|
220
|
-
const value =
|
|
219
|
+
const values = entries.flatMap(entry => {
|
|
220
|
+
const value = parseArgumentValue(entry.value)
|
|
221
|
+
const schema = Array.isArray(value) ? entity.schema.array() : entity.schema
|
|
222
|
+
const result = schema.safeParse(value)
|
|
221
223
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
throw new Error(
|
|
226
|
-
`Cannot extract field "${ref.inclusion.field}" from non-object output "${ref.output}" of instance "${ref.instanceId}".`,
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
value = (value as Record<string, unknown>)[ref.inclusion.field]
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const schema = Array.isArray(value) ? entity.schema.array() : entity.schema
|
|
234
|
-
const result = schema.safeParse(value)
|
|
235
|
-
|
|
236
|
-
if (!result.success) {
|
|
237
|
-
throw new Error(
|
|
238
|
-
`Invalid output "${ref.output}" from "${ref.instanceId}" for input "${inputName}": ${result.error.message}`,
|
|
239
|
-
)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (Array.isArray(value)) {
|
|
243
|
-
return value
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return input.multiple ? [value] : value
|
|
247
|
-
})
|
|
248
|
-
}
|
|
224
|
+
if (!result.success) {
|
|
225
|
+
throw new Error(`Invalid value for input "${inputName}": ${z.prettifyError(result.error)}`)
|
|
226
|
+
}
|
|
249
227
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
228
|
+
if (Array.isArray(result.data)) {
|
|
229
|
+
return result.data
|
|
230
|
+
}
|
|
253
231
|
|
|
254
|
-
|
|
232
|
+
return input.multiple ? [result.data] : [result.data]
|
|
233
|
+
})
|
|
255
234
|
|
|
256
235
|
if (!input.multiple) {
|
|
257
|
-
return values
|
|
236
|
+
return values[0]
|
|
258
237
|
}
|
|
259
238
|
|
|
260
239
|
return values
|
|
@@ -278,10 +257,6 @@ export function forUnit<
|
|
|
278
257
|
const rawHSConfig = config.requireObject(HighstateConfigKey.Config)
|
|
279
258
|
const hsConfig = unitConfigSchema.parse(rawHSConfig)
|
|
280
259
|
|
|
281
|
-
const rawHsSecrets = config
|
|
282
|
-
.requireSecretObject(HighstateConfigKey.Secrets)
|
|
283
|
-
.apply(secrets => z.record(z.string(), z.unknown()).parse(secrets))
|
|
284
|
-
|
|
285
260
|
const args = mapValues(unit.model.args, (arg, argName) => {
|
|
286
261
|
const value = parseArgumentValue(hsConfig.args[argName])
|
|
287
262
|
const result = arg[runtimeSchema]!.safeParse(value)
|
|
@@ -294,7 +269,7 @@ export function forUnit<
|
|
|
294
269
|
})
|
|
295
270
|
|
|
296
271
|
const secrets = mapValues(unit.model.secrets, (secret, secretName) => {
|
|
297
|
-
const hasValue = hsConfig.
|
|
272
|
+
const hasValue = secretName in hsConfig.secretValues
|
|
298
273
|
|
|
299
274
|
if (!hasValue && !secret.required) {
|
|
300
275
|
return secret.schema.default ? pulumiSecret(secret.schema.default) : undefined
|
|
@@ -304,16 +279,15 @@ export function forUnit<
|
|
|
304
279
|
throw new Error(`Secret "${secretName}" is required but not provided.`)
|
|
305
280
|
}
|
|
306
281
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
282
|
+
const rawValue = hsConfig.secretValues[secretName]
|
|
283
|
+
const value = parseArgumentValue(rawValue)
|
|
284
|
+
const result = secret[runtimeSchema]!.safeParse(value)
|
|
310
285
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
286
|
+
if (!result.success) {
|
|
287
|
+
throw new Error(`Invalid secret "${secretName}": ${z.prettifyError(result.error)}`)
|
|
288
|
+
}
|
|
314
289
|
|
|
315
|
-
|
|
316
|
-
})
|
|
290
|
+
return pulumiSecret(result.data)
|
|
317
291
|
})
|
|
318
292
|
|
|
319
293
|
const inputs = mapValues(unit.model.inputs, (input, inputName) => {
|
|
@@ -321,23 +295,25 @@ export function forUnit<
|
|
|
321
295
|
|
|
322
296
|
if (!value) {
|
|
323
297
|
if (input.multiple) {
|
|
324
|
-
return
|
|
298
|
+
return []
|
|
325
299
|
}
|
|
326
300
|
|
|
327
301
|
return undefined
|
|
328
302
|
}
|
|
329
303
|
|
|
330
|
-
return
|
|
304
|
+
return getInputValue(unit as unknown as Unit, inputName, input, value)
|
|
331
305
|
})
|
|
332
306
|
|
|
333
307
|
const [type, name] = parseInstanceId(hsConfig.instanceId)
|
|
334
308
|
|
|
335
309
|
instanceId = hsConfig.instanceId
|
|
310
|
+
stateId = hsConfig.stateId
|
|
336
311
|
instanceName = name
|
|
337
312
|
importBaseUrl = pathToFileURL(hsConfig.importBasePath)
|
|
338
313
|
|
|
339
314
|
return {
|
|
340
315
|
instanceId: hsConfig.instanceId,
|
|
316
|
+
stateId: hsConfig.stateId,
|
|
341
317
|
type,
|
|
342
318
|
name,
|
|
343
319
|
|
|
@@ -360,26 +336,32 @@ export function forUnit<
|
|
|
360
336
|
return value
|
|
361
337
|
}) as any,
|
|
362
338
|
|
|
339
|
+
setSecret: ((name: keyof TSecrets, value: Input<NonNullable<TSecrets[keyof TSecrets]>>) => {
|
|
340
|
+
secrets[name as string] = pulumiSecret(value)
|
|
341
|
+
}) as any,
|
|
342
|
+
|
|
363
343
|
outputs: async (outputs: any = {}) => {
|
|
364
|
-
const
|
|
344
|
+
const resolvedOutputs = await toPromise(outputs)
|
|
345
|
+
|
|
346
|
+
const result: any = mapValues(resolvedOutputs, (outputValue, outputName) => {
|
|
365
347
|
if (outputName === "$statusFields") {
|
|
366
|
-
return
|
|
348
|
+
return mapStatusFields(outputValue)
|
|
367
349
|
}
|
|
368
350
|
|
|
369
351
|
if (outputName === "$pages") {
|
|
370
|
-
return
|
|
352
|
+
return mapPages(outputValue)
|
|
371
353
|
}
|
|
372
354
|
|
|
373
355
|
if (outputName === "$terminals") {
|
|
374
|
-
return
|
|
356
|
+
return mapTerminals(outputValue)
|
|
375
357
|
}
|
|
376
358
|
|
|
377
359
|
if (outputName === "$triggers") {
|
|
378
|
-
return
|
|
360
|
+
return mapTriggers(outputValue)
|
|
379
361
|
}
|
|
380
362
|
|
|
381
363
|
if (outputName === "$workers") {
|
|
382
|
-
return
|
|
364
|
+
return mapWorkers(outputValue)
|
|
383
365
|
}
|
|
384
366
|
|
|
385
367
|
if (outputName.startsWith("$")) {
|
|
@@ -400,25 +382,20 @@ export function forUnit<
|
|
|
400
382
|
)
|
|
401
383
|
}
|
|
402
384
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const result = schema.safeParse(value)
|
|
385
|
+
const schema = outputModel.multiple ? entity.schema.array() : entity.schema
|
|
386
|
+
const result = schema.safeParse(outputValue)
|
|
406
387
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
388
|
+
if (!result.success) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Invalid value for output "${outputName}" of type "${outputModel.type}": ${z.prettifyError(
|
|
391
|
+
result.error,
|
|
392
|
+
)}`,
|
|
393
|
+
)
|
|
394
|
+
}
|
|
414
395
|
|
|
415
|
-
|
|
416
|
-
})
|
|
396
|
+
return result.data
|
|
417
397
|
})
|
|
418
398
|
|
|
419
|
-
// wait for all outputs to resolve before collecting secrets and artifacts
|
|
420
|
-
await Promise.all(Object.values(result).map(o => toPromise(o)))
|
|
421
|
-
|
|
422
399
|
result.$secrets = secrets
|
|
423
400
|
|
|
424
401
|
// collect artifacts from all outputs
|
|
@@ -435,11 +412,64 @@ export function forUnit<
|
|
|
435
412
|
result.$artifacts = artifactsMap
|
|
436
413
|
}
|
|
437
414
|
|
|
438
|
-
|
|
415
|
+
result.$hasResourceHooks = getHasResourceHooks()
|
|
416
|
+
|
|
417
|
+
return wrapHighstateSecretValues(result)
|
|
439
418
|
},
|
|
440
419
|
}
|
|
441
420
|
}
|
|
442
421
|
|
|
422
|
+
function wrapHighstateSecretValues<T>(data: T): T {
|
|
423
|
+
const cache = new WeakMap<object, unknown>()
|
|
424
|
+
|
|
425
|
+
const traverse = (value: unknown): unknown => {
|
|
426
|
+
if (value === null || value === undefined || typeof value !== "object") {
|
|
427
|
+
return value
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (Array.isArray(value)) {
|
|
431
|
+
const cached = cache.get(value)
|
|
432
|
+
if (cached) {
|
|
433
|
+
return cached
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const mapped: unknown[] = []
|
|
437
|
+
cache.set(value, mapped)
|
|
438
|
+
|
|
439
|
+
for (const item of value) {
|
|
440
|
+
mapped.push(traverse(item))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return mapped
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!isPlainObject(value)) {
|
|
447
|
+
return value
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const cached = cache.get(value)
|
|
451
|
+
if (cached) {
|
|
452
|
+
return cached
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const record = value as Record<string, unknown>
|
|
456
|
+
const mapped: Record<string, unknown> = {}
|
|
457
|
+
cache.set(value, mapped)
|
|
458
|
+
|
|
459
|
+
for (const [key, nestedValue] of Object.entries(record)) {
|
|
460
|
+
mapped[key] = traverse(nestedValue)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (record[HighstateSignature.Secret] === true && "value" in record) {
|
|
464
|
+
return pulumiSecret(mapped)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return mapped
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return traverse(data) as T
|
|
471
|
+
}
|
|
472
|
+
|
|
443
473
|
function mapStatusFields(status: Unwrap<ExtraOutputs["$statusFields"]>): InstanceStatusField[] {
|
|
444
474
|
if (!status) {
|
|
445
475
|
return []
|
|
@@ -600,3 +630,13 @@ function extractObjectsFromValue<TSchema extends z.ZodType>(
|
|
|
600
630
|
traverse(data)
|
|
601
631
|
return result
|
|
602
632
|
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Returns a temporary file path for the current unit instance.
|
|
636
|
+
*
|
|
637
|
+
* The format is `/tmp/highstate/{stateId}`.
|
|
638
|
+
* This directory does not change between different runs of the same unit instance.
|
|
639
|
+
*/
|
|
640
|
+
export function getUnitTempPath(): string {
|
|
641
|
+
return join("/tmp/highstate", getUnitStateId())
|
|
642
|
+
}
|