@highstate/pulumi 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/highstate.manifest.json +1 -1
- package/dist/index.js +188 -316
- package/dist/index.js.map +1 -1
- package/package.json +14 -8
- package/src/file.ts +68 -0
- package/src/index.ts +1 -1
- package/src/unit.ts +281 -445
- package/src/utils.ts +56 -141
- package/src/secret.ts +0 -30
package/src/unit.ts
CHANGED
@@ -1,140 +1,73 @@
|
|
1
|
-
|
2
|
-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
3
|
-
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
4
|
-
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
5
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
6
|
-
|
7
|
-
import type { DeepInput, InputArray, InputMap } from "./utils"
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: здесь орать запрещено */
|
8
2
|
import {
|
9
|
-
type ArgumentValue,
|
10
|
-
type ComponentInputSpec,
|
11
|
-
type Entity,
|
12
|
-
type Unit,
|
13
3
|
type ComponentInput,
|
4
|
+
type ComponentInputSpec,
|
5
|
+
camelCaseToHumanReadable,
|
6
|
+
HighstateConfigKey,
|
14
7
|
type InstanceInput,
|
8
|
+
type InstanceStatusField,
|
9
|
+
type InstanceStatusFieldValue,
|
10
|
+
type PartialKeys,
|
11
|
+
parseArgumentValue,
|
15
12
|
parseInstanceId,
|
16
|
-
|
17
|
-
|
18
|
-
type
|
19
|
-
type
|
20
|
-
|
21
|
-
|
13
|
+
runtimeSchema,
|
14
|
+
type TriggerInvocation,
|
15
|
+
type Unit,
|
16
|
+
type UnitArtifact,
|
17
|
+
type UnitConfig,
|
18
|
+
type UnitPage,
|
19
|
+
type UnitTerminal,
|
20
|
+
type UnitTrigger,
|
21
|
+
type UnitWorker,
|
22
|
+
unitArtifactSchema,
|
23
|
+
unitConfigSchema,
|
24
|
+
z,
|
22
25
|
} from "@highstate/contract"
|
23
|
-
import { Type, type Static } from "@sinclair/typebox"
|
24
|
-
import { mapValues, pickBy, pipe } from "remeda"
|
25
26
|
import {
|
26
27
|
Config,
|
27
|
-
|
28
|
-
Output,
|
28
|
+
type Input,
|
29
|
+
type Output,
|
29
30
|
output,
|
30
|
-
secret,
|
31
|
+
secret as pulumiSecret,
|
31
32
|
StackReference,
|
32
|
-
type Input,
|
33
33
|
type Unwrap,
|
34
34
|
} from "@pulumi/pulumi"
|
35
|
-
import {
|
36
|
-
import {
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
iconColor?: Input<string | undefined>
|
45
|
-
}
|
46
|
-
|
47
|
-
export type InstanceFileMeta = {
|
48
|
-
name: Input<string>
|
49
|
-
contentType?: Input<string>
|
50
|
-
size?: Input<number>
|
51
|
-
isBinary?: Input<boolean>
|
52
|
-
mode?: Input<number>
|
53
|
-
}
|
54
|
-
|
55
|
-
export type UnitArtifact = {
|
56
|
-
hash: Input<string>
|
57
|
-
meta?: Input<ObjectMeta>
|
58
|
-
}
|
59
|
-
|
60
|
-
export type InstanceFile = {
|
61
|
-
meta: Input<InstanceFileMeta>
|
62
|
-
content:
|
63
|
-
| { type: "embedded"; value: Input<string> }
|
64
|
-
| {
|
65
|
-
type: "artifact"
|
66
|
-
[HighstateSignature.Artifact]: UnitArtifact
|
67
|
-
}
|
68
|
-
}
|
69
|
-
|
70
|
-
export type InstanceTerminalSpec = {
|
71
|
-
image: Input<string>
|
72
|
-
command: InputArray<string>
|
73
|
-
cwd?: Input<string | undefined>
|
74
|
-
env?: InputMap<string | undefined>
|
75
|
-
files?: InputMap<InstanceFile | string | undefined>
|
76
|
-
}
|
77
|
-
|
78
|
-
export type InstanceTerminal = {
|
79
|
-
name: Input<string>
|
80
|
-
meta: Input<ObjectMeta>
|
81
|
-
spec: Input<InstanceTerminalSpec>
|
82
|
-
}
|
83
|
-
|
84
|
-
export type StatusFieldValue = string | number | boolean | string[]
|
85
|
-
|
86
|
-
export type StatusField<TArgName extends string = string> = {
|
87
|
-
name: Input<string>
|
88
|
-
meta?: Input<ObjectMeta>
|
89
|
-
complementaryTo?: Input<TArgName | undefined>
|
90
|
-
value?: Input<StatusFieldValue | undefined>
|
35
|
+
import { mapValues } from "remeda"
|
36
|
+
import { type DeepInput, toPromise } from "./utils"
|
37
|
+
|
38
|
+
type StatusField<TArgName extends string = string> = Omit<
|
39
|
+
InstanceStatusField,
|
40
|
+
"complementaryTo" | "meta"
|
41
|
+
> & {
|
42
|
+
meta?: PartialKeys<InstanceStatusField["meta"], "title">
|
43
|
+
complementaryTo?: TArgName
|
91
44
|
}
|
92
45
|
|
93
|
-
|
94
|
-
| { type: "markdown"; content: Input<string> }
|
95
|
-
| { type: "qr"; content: Input<string>; showContent?: boolean; language?: string }
|
96
|
-
| ({ type: "file" } & InstanceFile)
|
97
|
-
|
98
|
-
export type InstancePage = {
|
99
|
-
name: Input<string>
|
100
|
-
meta: Input<ObjectMeta>
|
101
|
-
content: InputArray<InstancePageBlock>
|
102
|
-
}
|
103
|
-
|
104
|
-
export type InstanceTriggerSpec =
|
105
|
-
| {
|
106
|
-
type: "before-destroy"
|
107
|
-
}
|
108
|
-
| {
|
109
|
-
type: "schedule"
|
110
|
-
schedule: string
|
111
|
-
}
|
112
|
-
|
113
|
-
export type InstanceTrigger = {
|
114
|
-
name: Input<string>
|
115
|
-
title: Input<string>
|
116
|
-
description?: Input<string>
|
117
|
-
spec: Input<InstanceTriggerSpec>
|
118
|
-
}
|
119
|
-
|
120
|
-
export type ExtraOutputs<TArgName extends string = string> = {
|
46
|
+
type ExtraOutputs<TArgName extends string = string> = {
|
121
47
|
$statusFields?:
|
122
|
-
|
|
123
|
-
|
48
|
+
| Input<
|
49
|
+
Record<
|
50
|
+
string,
|
51
|
+
DeepInput<Omit<StatusField<TArgName>, "name"> | InstanceStatusFieldValue | undefined>
|
52
|
+
>
|
53
|
+
>
|
54
|
+
| Input<DeepInput<StatusField<TArgName> | undefined>[]>
|
124
55
|
|
125
56
|
$terminals?:
|
126
|
-
|
|
127
|
-
|
|
57
|
+
| Input<Record<string, DeepInput<Omit<UnitTerminal, "name"> | undefined>>>
|
58
|
+
| Input<DeepInput<UnitTerminal | undefined>[]>
|
128
59
|
|
129
|
-
$pages?:
|
60
|
+
$pages?:
|
61
|
+
| Input<Record<string, DeepInput<Omit<UnitPage, "name"> | undefined>>>
|
62
|
+
| Input<DeepInput<UnitPage | undefined>[]>
|
130
63
|
|
131
64
|
$triggers?:
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
65
|
+
| Input<Record<string, DeepInput<Omit<UnitTrigger, "name"> | undefined>>>
|
66
|
+
| Input<DeepInput<UnitTrigger | undefined>[]>
|
135
67
|
|
136
|
-
|
137
|
-
|
68
|
+
$workers?:
|
69
|
+
| Input<Record<string, DeepInput<Omit<UnitWorker, "name"> | undefined>>>
|
70
|
+
| Input<DeepInput<UnitWorker | undefined>[]>
|
138
71
|
}
|
139
72
|
|
140
73
|
type OutputMapToDeepInputMap<T extends Record<string, unknown>, TArgName extends string> =
|
@@ -142,27 +75,36 @@ type OutputMapToDeepInputMap<T extends Record<string, unknown>, TArgName extends
|
|
142
75
|
? ExtraOutputs
|
143
76
|
: { [K in keyof T]: DeepInput<T[K]> } & ExtraOutputs<TArgName>
|
144
77
|
|
145
|
-
|
146
|
-
TArgs extends Record<string,
|
78
|
+
interface UnitContext<
|
79
|
+
TArgs extends Record<string, unknown>,
|
147
80
|
TInputs extends Record<string, unknown>,
|
148
81
|
TOutputs extends Record<string, unknown>,
|
149
|
-
TSecrets extends Record<string,
|
82
|
+
TSecrets extends Record<string, unknown>,
|
150
83
|
> {
|
151
84
|
args: TArgs
|
152
85
|
instanceId: string
|
153
86
|
type: string
|
154
87
|
name: string
|
155
|
-
secrets: Output<TSecrets>
|
156
|
-
|
157
|
-
inputs: TInputs extends Record<string, never>
|
158
|
-
? never
|
159
|
-
: {
|
160
|
-
[K in keyof TInputs]: undefined extends TInputs[K]
|
161
|
-
? Output<NonNullable<TInputs[K]>> | undefined
|
162
|
-
: Output<TInputs[K]>
|
163
|
-
}
|
164
88
|
|
165
|
-
|
89
|
+
secrets: {
|
90
|
+
[K in keyof TSecrets]: undefined extends TSecrets[K]
|
91
|
+
? Output<NonNullable<TSecrets[K]>> | undefined
|
92
|
+
: Output<TSecrets[K]>
|
93
|
+
}
|
94
|
+
|
95
|
+
getSecret<K extends keyof TSecrets>(
|
96
|
+
this: void,
|
97
|
+
name: K,
|
98
|
+
): Output<NonNullable<TSecrets[K]> | undefined>
|
99
|
+
|
100
|
+
getSecret<K extends keyof TSecrets>(
|
101
|
+
this: void,
|
102
|
+
name: K,
|
103
|
+
factory: () => Input<NonNullable<TSecrets[K]>>,
|
104
|
+
): Output<NonNullable<TSecrets[K]>>
|
105
|
+
|
106
|
+
inputs: TInputs
|
107
|
+
invokedTriggers: TriggerInvocation[]
|
166
108
|
|
167
109
|
outputs(
|
168
110
|
this: void,
|
@@ -170,68 +112,102 @@ export interface UnitContext<
|
|
170
112
|
): Promise<unknown>
|
171
113
|
}
|
172
114
|
|
173
|
-
|
174
|
-
|
115
|
+
// z.output since the values are validated/transformed and passed to the user
|
116
|
+
type InputSpecToWrappedValue<T extends ComponentInputSpec> = T[2] extends true
|
117
|
+
? // we have to wrap the array in Output since we don't know how many items will be returned by each multiple input
|
118
|
+
Output<NonNullable<z.output<T[0]["schema"]>>[]>
|
175
119
|
: T[1] extends true
|
176
|
-
?
|
177
|
-
:
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
120
|
+
? Output<NonNullable<z.output<T[0]["schema"]>>>
|
121
|
+
: Output<NonNullable<z.output<T[0]["schema"]>>> | undefined
|
122
|
+
|
123
|
+
// z.input since the values are passed from the user and should be validated/transformed before returning from the unit
|
124
|
+
type OutputSpecToValue<T extends ComponentInputSpec> = T[2] extends true
|
125
|
+
? T[1] extends true
|
126
|
+
? NonNullable<z.input<T[0]["schema"]>>[]
|
127
|
+
: NonNullable<z.input<T[0]["schema"]>>[] | undefined
|
128
|
+
: T[1] extends true
|
129
|
+
? NonNullable<z.input<T[0]["schema"]>>
|
130
|
+
: NonNullable<z.input<T[0]["schema"]>> | undefined
|
183
131
|
|
184
132
|
const stackRefMap = new Map<string, StackReference>()
|
185
|
-
const [projectId, instanceName] = getStack().split("_")
|
186
133
|
|
187
134
|
let instanceId: string | undefined
|
135
|
+
let instanceName: string | undefined
|
188
136
|
|
137
|
+
/**
|
138
|
+
* Returns the current unit instance id.
|
139
|
+
*
|
140
|
+
* Only available after calling `forUnit` function.
|
141
|
+
*/
|
189
142
|
export function getUnitInstanceId(): string {
|
190
143
|
if (!instanceId) {
|
191
|
-
throw new Error(
|
144
|
+
throw new Error(`Instance id is not set. Did you call "forUnit" function?`)
|
192
145
|
}
|
193
146
|
|
194
147
|
return instanceId
|
195
148
|
}
|
196
149
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
150
|
+
/**
|
151
|
+
* Returns the current unit instance name.
|
152
|
+
*/
|
201
153
|
export function getUnitInstanceName(): string {
|
154
|
+
if (!instanceName) {
|
155
|
+
throw new Error(`Instance name is not set. Did you call "forUnit" function?`)
|
156
|
+
}
|
157
|
+
|
202
158
|
return instanceName
|
203
159
|
}
|
204
160
|
|
205
|
-
|
206
|
-
|
207
|
-
|
161
|
+
/**
|
162
|
+
* Returns a comment that can be used in resources to indicate that they are managed by Highstate.
|
163
|
+
*/
|
164
|
+
export function getResourceComment(): string {
|
165
|
+
return `Managed by Highstate (${getUnitInstanceId()})`
|
166
|
+
}
|
167
|
+
|
168
|
+
function getStackRef(config: UnitConfig, input: InstanceInput) {
|
169
|
+
const [instanceType] = parseInstanceId(input.instanceId)
|
170
|
+
const stateId = config.stateIdMap[input.instanceId]
|
171
|
+
if (!stateId) {
|
172
|
+
throw new Error(`State ID for instance "${input.instanceId}" not found in the unit config.`)
|
173
|
+
}
|
174
|
+
|
175
|
+
const key = `organization/${instanceType}/${stateId}`
|
176
|
+
let stackRef = stackRefMap.get(key)
|
208
177
|
|
209
|
-
if (!
|
210
|
-
|
178
|
+
if (!stackRef) {
|
179
|
+
stackRef = new StackReference(key)
|
180
|
+
stackRefMap.set(key, stackRef)
|
211
181
|
}
|
212
182
|
|
213
|
-
return
|
183
|
+
return stackRef
|
214
184
|
}
|
215
185
|
|
216
|
-
function getOutput(unit: Unit, input: ComponentInput, refs: InstanceInput[]) {
|
186
|
+
function getOutput(config: UnitConfig, unit: Unit, input: ComponentInput, refs: InstanceInput[]) {
|
217
187
|
const entity = unit.entities.get(input.type)
|
218
188
|
if (!entity) {
|
219
|
-
throw new Error(`Entity
|
189
|
+
throw new Error(`Entity "${input.type}" not found in the unit "${unit.model.type}".`)
|
220
190
|
}
|
221
191
|
|
222
192
|
const _getOutput = (ref: InstanceInput) => {
|
223
|
-
const value = getStackRef(ref).requireOutput(ref.output)
|
193
|
+
const value = getStackRef(config, ref).requireOutput(ref.output)
|
224
194
|
|
225
195
|
return value.apply(value => {
|
226
196
|
if (Array.isArray(value)) {
|
227
197
|
for (const [index, item] of value.entries()) {
|
228
|
-
|
229
|
-
|
198
|
+
const result = entity.schema.safeParse(item)
|
199
|
+
|
200
|
+
if (!result.success) {
|
201
|
+
throw new Error(
|
202
|
+
`Invalid output for "${input.type}[${index}]": ${z.prettifyError(result.error)}`,
|
203
|
+
)
|
230
204
|
}
|
231
205
|
}
|
232
206
|
} else {
|
233
|
-
|
234
|
-
|
207
|
+
const result = entity.schema.safeParse(value)
|
208
|
+
|
209
|
+
if (!result.success) {
|
210
|
+
throw new Error(`Invalid output for "${input.type}": ${z.prettifyError(result.error)}`)
|
235
211
|
}
|
236
212
|
}
|
237
213
|
|
@@ -252,183 +228,105 @@ function getOutput(unit: Unit, input: ComponentInput, refs: InstanceInput[]) {
|
|
252
228
|
return values
|
253
229
|
}
|
254
230
|
|
255
|
-
function isAnyOfSchema(schema: ArgumentValueSchema, itemType: string): boolean {
|
256
|
-
if (schema.anyOf) {
|
257
|
-
return Object.values(schema.anyOf).every(schema =>
|
258
|
-
isAnyOfSchema(schema as ArgumentValueSchema, itemType),
|
259
|
-
)
|
260
|
-
}
|
261
|
-
|
262
|
-
return schema.type === itemType
|
263
|
-
}
|
264
|
-
|
265
|
-
function isStringSchema(schema: ArgumentValueSchema): boolean {
|
266
|
-
if (schema.type === "string") {
|
267
|
-
return true
|
268
|
-
}
|
269
|
-
|
270
|
-
if (isAnyOfSchema(schema, "string")) {
|
271
|
-
return true
|
272
|
-
}
|
273
|
-
|
274
|
-
return false
|
275
|
-
}
|
276
|
-
|
277
|
-
function isNumberSchema(schema: ArgumentValueSchema): boolean {
|
278
|
-
if (schema.type === "number") {
|
279
|
-
return true
|
280
|
-
}
|
281
|
-
|
282
|
-
if (isAnyOfSchema(schema, "number")) {
|
283
|
-
return true
|
284
|
-
}
|
285
|
-
|
286
|
-
return false
|
287
|
-
}
|
288
|
-
|
289
|
-
function isBooleanSchema(schema: ArgumentValueSchema): boolean {
|
290
|
-
if (schema.type === "boolean") {
|
291
|
-
return true
|
292
|
-
}
|
293
|
-
|
294
|
-
if (isAnyOfSchema(schema, "boolean")) {
|
295
|
-
return true
|
296
|
-
}
|
297
|
-
|
298
|
-
return false
|
299
|
-
}
|
300
|
-
|
301
231
|
export function forUnit<
|
302
|
-
TArgs extends Record<string,
|
232
|
+
TArgs extends Record<string, z.ZodType>,
|
303
233
|
TInputs extends Record<string, ComponentInputSpec>,
|
304
234
|
TOutputs extends Record<string, ComponentInputSpec>,
|
305
|
-
TSecrets extends Record<string,
|
235
|
+
TSecrets extends Record<string, z.ZodType>,
|
306
236
|
>(
|
307
237
|
unit: Unit<TArgs, TInputs, TOutputs, TSecrets>,
|
308
238
|
): UnitContext<
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
ComponentArgumentSpecToStatic<TSecrets>
|
239
|
+
{ [K in keyof TArgs]: z.output<TArgs[K]> },
|
240
|
+
{ [K in keyof TInputs]: InputSpecToWrappedValue<TInputs[K]> },
|
241
|
+
{ [K in keyof TOutputs]: OutputSpecToValue<TOutputs[K]> },
|
242
|
+
{ [K in keyof TSecrets]: z.output<TSecrets[K]> }
|
314
243
|
> {
|
315
244
|
const config = new Config()
|
245
|
+
const rawHSConfig = config.requireObject(HighstateConfigKey.Config)
|
246
|
+
const hsConfig = unitConfigSchema.parse(rawHSConfig)
|
316
247
|
|
317
|
-
const
|
318
|
-
|
319
|
-
|
320
|
-
if (arg.required) {
|
321
|
-
return config.require(argName)
|
322
|
-
}
|
323
|
-
|
324
|
-
// handle empty strings as undefined
|
325
|
-
return config.get(argName) || arg.schema.default
|
326
|
-
}
|
327
|
-
case isNumberSchema(arg.schema): {
|
328
|
-
if (arg.required) {
|
329
|
-
return config.requireNumber(argName)
|
330
|
-
}
|
331
|
-
|
332
|
-
// handle empty strings as undefined
|
333
|
-
const value = config.get(argName)
|
334
|
-
if (!value) {
|
335
|
-
return arg.schema.default
|
336
|
-
}
|
337
|
-
|
338
|
-
return config.getNumber(argName) ?? arg.schema.default
|
339
|
-
}
|
340
|
-
case isBooleanSchema(arg.schema): {
|
341
|
-
if (arg.required) {
|
342
|
-
return config.requireBoolean(argName)
|
343
|
-
}
|
248
|
+
const rawHsSecrets = config
|
249
|
+
.requireSecretObject(HighstateConfigKey.Secrets)
|
250
|
+
.apply(secrets => z.record(z.string(), z.unknown()).parse(secrets))
|
344
251
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
}
|
252
|
+
const args = mapValues(unit.model.args, (arg, argName) => {
|
253
|
+
const value = parseArgumentValue(hsConfig.args[argName])
|
254
|
+
// biome-ignore lint/style/noNonNullAssertion: runtime schema is there in runtime
|
255
|
+
const result = arg[runtimeSchema]!.safeParse(value)
|
350
256
|
|
351
|
-
|
352
|
-
}
|
353
|
-
|
354
|
-
if (!arg.required) {
|
355
|
-
const value = config.get(argName)
|
356
|
-
// handle empty strings as undefined
|
357
|
-
if (!value) {
|
358
|
-
return arg.schema.default
|
359
|
-
}
|
360
|
-
}
|
257
|
+
if (!result.success) {
|
258
|
+
throw new Error(`Invalid argument "${argName}": ${z.prettifyError(result.error)}`)
|
259
|
+
}
|
361
260
|
|
362
|
-
|
363
|
-
|
261
|
+
return result.data
|
262
|
+
})
|
364
263
|
|
365
|
-
|
366
|
-
|
367
|
-
}
|
264
|
+
const secrets = mapValues(unit.model.secrets, (secret, secretName) => {
|
265
|
+
const hasValue = hsConfig.secretNames.includes(secretName)
|
368
266
|
|
369
|
-
|
370
|
-
|
267
|
+
if (!hasValue && !secret.required) {
|
268
|
+
return secret.schema.default ? pulumiSecret(secret.schema.default) : undefined
|
371
269
|
}
|
372
|
-
}) as ComponentArgumentSpecToStatic<TArgs>
|
373
270
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
case isStringSchema(secret.schema): {
|
378
|
-
return secret.required ? config.requireSecret(secretName) : config.getSecret(secretName)
|
379
|
-
}
|
380
|
-
case isNumberSchema(secret.schema): {
|
381
|
-
return secret.required
|
382
|
-
? config.requireSecretNumber(secretName)
|
383
|
-
: config.getSecretNumber(secretName)
|
384
|
-
}
|
385
|
-
case isBooleanSchema(secret.schema): {
|
386
|
-
return secret.required
|
387
|
-
? config.requireSecretBoolean(secretName)
|
388
|
-
: config.getSecretBoolean(secretName)
|
389
|
-
}
|
390
|
-
default: {
|
391
|
-
const value = secret.required
|
392
|
-
? config.requireSecretObject(secretName)
|
393
|
-
: config.getSecretObject(secretName)
|
271
|
+
if (!hasValue && secret.required) {
|
272
|
+
throw new Error(`Secret "${secretName}" is required but not provided.`)
|
273
|
+
}
|
394
274
|
|
395
|
-
|
396
|
-
|
397
|
-
|
275
|
+
return rawHsSecrets[secretName].apply(rawValue => {
|
276
|
+
const value = parseArgumentValue(rawValue)
|
277
|
+
// biome-ignore lint/style/noNonNullAssertion: runtime schema is there in runtime
|
278
|
+
const result = secret[runtimeSchema]!.safeParse(value)
|
398
279
|
|
399
|
-
|
400
|
-
}
|
280
|
+
if (!result.success) {
|
281
|
+
throw new Error(`Invalid secret "${secretName}": ${z.prettifyError(result.error)}`)
|
401
282
|
}
|
402
|
-
|
403
|
-
|
283
|
+
|
284
|
+
return pulumiSecret(result.data)
|
285
|
+
})
|
286
|
+
})
|
404
287
|
|
405
288
|
const inputs = mapValues(unit.model.inputs, (input, inputName) => {
|
406
|
-
const value =
|
407
|
-
? config.requireObject<InstanceInput[]>(`input.${inputName}`)
|
408
|
-
: config.getObject<InstanceInput[]>(`input.${inputName}`)
|
289
|
+
const value = hsConfig.inputs[inputName]
|
409
290
|
|
410
291
|
if (!value) {
|
411
292
|
if (input.multiple) {
|
412
|
-
return
|
293
|
+
return []
|
413
294
|
}
|
414
295
|
|
415
296
|
return undefined
|
416
297
|
}
|
417
298
|
|
418
|
-
return getOutput(unit as unknown as Unit, input, value)
|
299
|
+
return getOutput(hsConfig, unit as unknown as Unit, input, value)
|
419
300
|
})
|
420
301
|
|
421
|
-
const type =
|
422
|
-
|
302
|
+
const [type, name] = parseInstanceId(hsConfig.instanceId)
|
303
|
+
|
304
|
+
instanceId = hsConfig.instanceId
|
305
|
+
instanceName = name
|
423
306
|
|
424
307
|
return {
|
425
|
-
|
426
|
-
instanceId,
|
308
|
+
instanceId: hsConfig.instanceId,
|
427
309
|
type,
|
428
|
-
name
|
429
|
-
|
310
|
+
name,
|
311
|
+
|
312
|
+
args: args as any,
|
313
|
+
secrets: secrets as any,
|
430
314
|
inputs: inputs as any,
|
431
|
-
invokedTriggers:
|
315
|
+
invokedTriggers: hsConfig.invokedTriggers,
|
316
|
+
|
317
|
+
getSecret: (<K extends keyof TSecrets>(
|
318
|
+
name: K,
|
319
|
+
factory?: () => Input<NonNullable<TSecrets[K]>>,
|
320
|
+
) => {
|
321
|
+
if (!factory) {
|
322
|
+
return secrets[name as string]
|
323
|
+
}
|
324
|
+
|
325
|
+
const value = secrets[name as string] ?? pulumiSecret(factory())
|
326
|
+
secrets[name as string] = value
|
327
|
+
|
328
|
+
return value
|
329
|
+
}) as any,
|
432
330
|
|
433
331
|
outputs: async (outputs: any = {}) => {
|
434
332
|
const result: any = mapValues(outputs, (outputValue, outputName) => {
|
@@ -448,56 +346,54 @@ export function forUnit<
|
|
448
346
|
return output(outputValue).apply(mapTriggers)
|
449
347
|
}
|
450
348
|
|
349
|
+
if (outputName === "$workers") {
|
350
|
+
return output(outputValue).apply(mapWorkers)
|
351
|
+
}
|
352
|
+
|
451
353
|
if (outputName.startsWith("$")) {
|
452
|
-
throw new Error(`Unknown extra output
|
354
|
+
throw new Error(`Unknown extra output "${outputName}".`)
|
453
355
|
}
|
454
356
|
|
455
357
|
const outputModel = unit.model.outputs[outputName]
|
456
358
|
if (!outputModel) {
|
457
|
-
throw new Error(
|
359
|
+
throw new Error(
|
360
|
+
`Output "${outputName}" not found in the unit "${unit.model.type}", but was passed to outputs(...).`,
|
361
|
+
)
|
458
362
|
}
|
459
363
|
|
460
364
|
const entity = unit.entities.get(outputModel.type)
|
461
365
|
if (!entity) {
|
462
366
|
throw new Error(
|
463
|
-
`Entity
|
367
|
+
`Entity "${outputModel.type}" not found in the unit "${unit.model.type}". It looks like a bug in the unit definition.`,
|
464
368
|
)
|
465
369
|
}
|
466
370
|
|
467
371
|
return output(outputValue).apply(value => {
|
468
|
-
|
469
|
-
|
470
|
-
throw new Error(`Output '${outputName}' is required.`)
|
471
|
-
}
|
472
|
-
|
473
|
-
return undefined
|
474
|
-
}
|
475
|
-
|
476
|
-
const schema = outputModel.multiple ? Type.Array(entity.schema) : entity.schema
|
372
|
+
const schema = outputModel.multiple ? entity.schema.array() : entity.schema
|
373
|
+
const result = schema.safeParse(value)
|
477
374
|
|
478
|
-
if (!
|
479
|
-
throw new Error(
|
375
|
+
if (!result.success) {
|
376
|
+
throw new Error(
|
377
|
+
`Invalid output "${outputName}" of type "${outputModel.type}": ${z.prettifyError(result.error)}`,
|
378
|
+
)
|
480
379
|
}
|
481
380
|
|
482
|
-
return
|
381
|
+
return result.data
|
483
382
|
})
|
484
|
-
})
|
383
|
+
})
|
485
384
|
|
486
|
-
|
385
|
+
// wait for all outputs to resolve before collecting secrets and artifacts
|
386
|
+
await Promise.all(Object.values(result).map(o => toPromise(o)))
|
487
387
|
|
488
|
-
|
489
|
-
result.$secrets = createdSecrets
|
490
|
-
}
|
388
|
+
result.$secrets = secrets
|
491
389
|
|
492
390
|
// collect artifacts from all outputs
|
493
391
|
const artifactsMap: Record<string, UnitArtifact[]> = {}
|
494
392
|
for (const [outputName, outputValue] of Object.entries(outputs)) {
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
artifactsMap[outputName] = artifacts
|
500
|
-
}
|
393
|
+
const resolvedValue = await toPromise(outputValue)
|
394
|
+
const artifacts = extractObjectsFromValue(unitArtifactSchema, resolvedValue)
|
395
|
+
if (artifacts.length > 0) {
|
396
|
+
artifactsMap[outputName] = artifacts
|
501
397
|
}
|
502
398
|
}
|
503
399
|
|
@@ -510,29 +406,22 @@ export function forUnit<
|
|
510
406
|
}
|
511
407
|
}
|
512
408
|
|
513
|
-
|
514
|
-
export type EntityInput<T extends Entity> = Output<EntityValue<T>>
|
515
|
-
|
516
|
-
function outputToPromise(o: unknown): Promise<unknown> {
|
517
|
-
return new Promise(resolve => (output(o) as Output<unknown>).apply(resolve))
|
518
|
-
}
|
519
|
-
|
520
|
-
function mapStatusFields(status: Unwrap<ExtraOutputs["$statusFields"]>): StatusField[] {
|
409
|
+
function mapStatusFields(status: Unwrap<ExtraOutputs["$statusFields"]>): InstanceStatusField[] {
|
521
410
|
if (!status) {
|
522
411
|
return []
|
523
412
|
}
|
524
413
|
|
525
414
|
if (Array.isArray(status)) {
|
526
415
|
return status
|
527
|
-
.filter(field =>
|
416
|
+
.filter((field): field is NonNullable<StatusField> => field?.value !== undefined)
|
528
417
|
.map(field => {
|
529
418
|
return {
|
530
|
-
name: field
|
419
|
+
name: field.name,
|
531
420
|
meta: {
|
532
|
-
title: field
|
421
|
+
title: field.meta?.title ?? camelCaseToHumanReadable(field.name),
|
533
422
|
},
|
534
|
-
value: field
|
535
|
-
}
|
423
|
+
value: field.value,
|
424
|
+
}
|
536
425
|
})
|
537
426
|
}
|
538
427
|
|
@@ -558,7 +447,7 @@ function mapStatusFields(status: Unwrap<ExtraOutputs["$statusFields"]>): StatusF
|
|
558
447
|
}
|
559
448
|
|
560
449
|
return {
|
561
|
-
...
|
450
|
+
...field,
|
562
451
|
meta: {
|
563
452
|
...field.meta,
|
564
453
|
title: field.meta?.title ?? camelCaseToHumanReadable(name),
|
@@ -566,65 +455,30 @@ function mapStatusFields(status: Unwrap<ExtraOutputs["$statusFields"]>): StatusF
|
|
566
455
|
name,
|
567
456
|
}
|
568
457
|
})
|
569
|
-
.filter(field =>
|
458
|
+
.filter((field): field is InstanceStatusField => field?.value !== undefined)
|
570
459
|
}
|
571
460
|
|
572
|
-
function mapPages(pages: Unwrap<ExtraOutputs["$pages"]>):
|
461
|
+
function mapPages(pages: Unwrap<ExtraOutputs["$pages"]>): Output<UnitPage[]> {
|
573
462
|
if (!pages) {
|
574
|
-
return []
|
463
|
+
return output([])
|
575
464
|
}
|
576
465
|
|
577
|
-
if (Array.isArray(pages)) {
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
.filter(([, page]) => !!page)
|
583
|
-
.map(([name, page]) => ({ ...page!, name }))
|
584
|
-
}
|
466
|
+
if (!Array.isArray(pages)) {
|
467
|
+
pages = Object.entries(pages).map(([name, page]) => {
|
468
|
+
if (!page) {
|
469
|
+
return undefined
|
470
|
+
}
|
585
471
|
|
586
|
-
|
587
|
-
|
588
|
-
content: string,
|
589
|
-
contentType = "text/plain",
|
590
|
-
isSecret = false,
|
591
|
-
): InstanceFile {
|
592
|
-
return {
|
593
|
-
meta: {
|
594
|
-
name,
|
595
|
-
contentType,
|
596
|
-
size: Buffer.byteLength(content, "utf8"),
|
597
|
-
},
|
598
|
-
content: {
|
599
|
-
type: "embedded",
|
600
|
-
value: isSecret ? secret(content) : content,
|
601
|
-
},
|
472
|
+
return { ...page, name }
|
473
|
+
})
|
602
474
|
}
|
603
|
-
}
|
604
475
|
|
605
|
-
|
606
|
-
name: string,
|
607
|
-
content: Buffer,
|
608
|
-
contentType = "application/octet-stream",
|
609
|
-
isSecret = false,
|
610
|
-
): InstanceFile {
|
611
|
-
return {
|
612
|
-
meta: {
|
613
|
-
name,
|
614
|
-
contentType,
|
615
|
-
size: content.byteLength,
|
616
|
-
isBinary: true,
|
617
|
-
},
|
618
|
-
content: {
|
619
|
-
type: "embedded",
|
620
|
-
value: isSecret ? secret(content.toString("base64")) : content.toString("base64"),
|
621
|
-
},
|
622
|
-
}
|
476
|
+
return output(pages.filter((page): page is NonNullable<UnitPage> => !!page))
|
623
477
|
}
|
624
478
|
|
625
|
-
function mapTerminals(terminals: Unwrap<ExtraOutputs["$terminals"]>):
|
479
|
+
function mapTerminals(terminals: Unwrap<ExtraOutputs["$terminals"]>): Output<UnitTerminal[]> {
|
626
480
|
if (!terminals) {
|
627
|
-
return []
|
481
|
+
return output([])
|
628
482
|
}
|
629
483
|
|
630
484
|
if (!Array.isArray(terminals)) {
|
@@ -637,65 +491,53 @@ function mapTerminals(terminals: Unwrap<ExtraOutputs["$terminals"]>): InstanceTe
|
|
637
491
|
})
|
638
492
|
}
|
639
493
|
|
640
|
-
return terminals
|
641
|
-
|
642
|
-
.map(terminal => {
|
643
|
-
if (!terminal.spec.files) {
|
644
|
-
return terminal
|
645
|
-
}
|
494
|
+
return output(terminals.filter((terminal): terminal is NonNullable<UnitTerminal> => !!terminal))
|
495
|
+
}
|
646
496
|
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
if (typeof file === "string") {
|
657
|
-
return {
|
658
|
-
meta: {
|
659
|
-
name: "content",
|
660
|
-
contentType: "text/plain",
|
661
|
-
size: Buffer.byteLength(file, "utf8"),
|
662
|
-
},
|
663
|
-
content: {
|
664
|
-
type: "embedded" as const,
|
665
|
-
value: file,
|
666
|
-
},
|
667
|
-
}
|
668
|
-
}
|
669
|
-
|
670
|
-
return file
|
671
|
-
}),
|
672
|
-
pickBy(value => !!value),
|
673
|
-
),
|
674
|
-
},
|
497
|
+
function mapTriggers(triggers: Unwrap<ExtraOutputs["$triggers"]>): Output<UnitTrigger[]> {
|
498
|
+
if (!triggers) {
|
499
|
+
return output([])
|
500
|
+
}
|
501
|
+
|
502
|
+
if (!Array.isArray(triggers)) {
|
503
|
+
triggers = Object.entries(triggers).map(([name, trigger]) => {
|
504
|
+
if (!trigger) {
|
505
|
+
return undefined
|
675
506
|
}
|
507
|
+
|
508
|
+
return { ...trigger, name }
|
676
509
|
})
|
510
|
+
}
|
511
|
+
|
512
|
+
return output(triggers.filter((trigger): trigger is NonNullable<UnitTrigger> => !!trigger))
|
677
513
|
}
|
678
514
|
|
679
|
-
function
|
680
|
-
if (!
|
681
|
-
return []
|
515
|
+
function mapWorkers(workers: Unwrap<ExtraOutputs["$workers"]>): Output<Unwrap<UnitWorker>[]> {
|
516
|
+
if (!workers) {
|
517
|
+
return output([])
|
682
518
|
}
|
683
519
|
|
684
|
-
if (Array.isArray(
|
685
|
-
|
520
|
+
if (!Array.isArray(workers)) {
|
521
|
+
workers = Object.entries(workers).map(([name, worker]) => {
|
522
|
+
if (!worker) {
|
523
|
+
return undefined
|
524
|
+
}
|
525
|
+
|
526
|
+
return { ...worker, name }
|
527
|
+
})
|
686
528
|
}
|
687
529
|
|
688
|
-
return
|
689
|
-
.filter(([, trigger]) => !!trigger)
|
690
|
-
.map(([name, trigger]) => ({ ...(trigger as InstanceTrigger), name }))
|
530
|
+
return output(workers.filter((worker): worker is NonNullable<Unwrap<UnitWorker>> => !!worker))
|
691
531
|
}
|
692
532
|
|
693
533
|
/**
|
694
|
-
* Extracts
|
695
|
-
* looking for properties with HighstateSignature.Artifact.
|
534
|
+
* Extracts all objects with the specified schema from a value.
|
696
535
|
*/
|
697
|
-
function
|
698
|
-
|
536
|
+
function extractObjectsFromValue<TSchema extends z.ZodType>(
|
537
|
+
schema: TSchema,
|
538
|
+
data: unknown,
|
539
|
+
): z.infer<TSchema>[] {
|
540
|
+
const result: z.infer<TSchema>[] = []
|
699
541
|
|
700
542
|
function traverse(obj: unknown): void {
|
701
543
|
if (obj === null || obj === undefined || typeof obj !== "object") {
|
@@ -709,24 +551,18 @@ function extractArtifactsFromValue(data: unknown): UnitArtifact[] {
|
|
709
551
|
return
|
710
552
|
}
|
711
553
|
|
712
|
-
const
|
713
|
-
|
714
|
-
|
715
|
-
if (HighstateSignature.Artifact in record) {
|
716
|
-
const artifactData = record[HighstateSignature.Artifact] as UnitArtifact
|
717
|
-
artifacts.push(artifactData)
|
718
|
-
|
719
|
-
// strip all other properties except the artifact hash
|
720
|
-
record[HighstateSignature.Artifact] = { hash: artifactData.hash }
|
554
|
+
const parseResult = schema.safeParse(obj)
|
555
|
+
if (parseResult.success) {
|
556
|
+
result.push(parseResult.data)
|
721
557
|
return
|
722
558
|
}
|
723
559
|
|
724
560
|
// recursively traverse all properties
|
725
|
-
for (const value of Object.values(
|
561
|
+
for (const value of Object.values(obj)) {
|
726
562
|
traverse(value)
|
727
563
|
}
|
728
564
|
}
|
729
565
|
|
730
566
|
traverse(data)
|
731
|
-
return
|
567
|
+
return result
|
732
568
|
}
|