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