@terraforge/core 0.0.5 → 0.0.7
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/README.md +3 -3
- package/dist/index.d.ts +63 -4
- package/dist/index.js +309 -16
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The most used IaC solutions are slow & don't effectively leverage diffing to spe
|
|
|
12
12
|
Install with (NPM):
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
npm i @terraforge/core @terraforge/aws
|
|
15
|
+
npm i @terraforge/core @terraforge/terraform @terraforge/aws
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Example
|
|
@@ -52,12 +52,12 @@ This example illustrates how simple it is to define multi-stack resources withou
|
|
|
52
52
|
```ts
|
|
53
53
|
const app = new App('todo-app')
|
|
54
54
|
const storage = new Stack(app, 'storage')
|
|
55
|
-
const list = new aws.s3.Bucket(storage, 'list', {
|
|
55
|
+
const list = new aws.s3.Bucket(storage, 'todo-list', {
|
|
56
56
|
bucket: 'your-bucket-name'
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
const items = new Stack(app, 'items')
|
|
60
|
-
const todo = new aws.s3.BucketObject(items, 'item', {
|
|
60
|
+
const todo = new aws.s3.BucketObject(items, 'todo-item', {
|
|
61
61
|
bucket: list.bucket,
|
|
62
62
|
key: 'item-1',
|
|
63
63
|
content: JSON.stringify({
|
package/dist/index.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ declare const interpolate: (literals: TemplateStringsArray, ...placeholders: Inp
|
|
|
42
42
|
type URN = `urn:${string}`;
|
|
43
43
|
|
|
44
44
|
declare const nodeMetaSymbol: unique symbol;
|
|
45
|
-
type Node<T extends Tag = Tag, I extends State = State, O extends State =
|
|
45
|
+
type Node<T extends Tag = Tag, I extends State = State, O extends State = State, C extends Config = Config> = {
|
|
46
46
|
readonly [nodeMetaSymbol]: Meta<T, I, O, C>;
|
|
47
47
|
readonly urn: URN;
|
|
48
48
|
} & O;
|
|
@@ -58,12 +58,13 @@ declare const isDataSource: (obj: object) => obj is DataSource;
|
|
|
58
58
|
type ResourceConfig = Config & {
|
|
59
59
|
/** Import an existing resource instead of creating a new resource. */
|
|
60
60
|
import?: string;
|
|
61
|
-
/** If true the resource will be retained in the backing cloud provider during a
|
|
61
|
+
/** If true the resource will be retained in the backing cloud provider during a delete operation. */
|
|
62
62
|
retainOnDelete?: boolean;
|
|
63
|
-
/** Override the default create-after-delete behavior when replacing a resource. */
|
|
64
63
|
/** If set, the provider’s Delete method will not be called for this resource if the specified resource is being deleted as well. */
|
|
65
64
|
/** Declare that changes to certain properties should be treated as forcing a replacement. */
|
|
66
65
|
replaceOnChanges?: string[];
|
|
66
|
+
/** If true, create the replacement before deleting the existing resource. */
|
|
67
|
+
createBeforeReplace?: boolean;
|
|
67
68
|
};
|
|
68
69
|
type ResourceMeta<I extends State = State, O extends State = State> = Meta<'resource', I, O, ResourceConfig>;
|
|
69
70
|
type Resource<I extends State = State, O extends State = State> = O & {
|
|
@@ -187,6 +188,11 @@ type DeleteProps<T = State> = {
|
|
|
187
188
|
state: T;
|
|
188
189
|
idempotantToken?: string;
|
|
189
190
|
};
|
|
191
|
+
type PlanProps<T = State> = {
|
|
192
|
+
type: string;
|
|
193
|
+
priorState: T;
|
|
194
|
+
proposedState: T;
|
|
195
|
+
};
|
|
190
196
|
type GetProps<T = State> = {
|
|
191
197
|
type: string;
|
|
192
198
|
state: T;
|
|
@@ -210,12 +216,60 @@ interface Provider {
|
|
|
210
216
|
state: State;
|
|
211
217
|
}>;
|
|
212
218
|
deleteResource(props: DeleteProps): Promise<void>;
|
|
219
|
+
planResourceChange?(props: PlanProps): Promise<{
|
|
220
|
+
version: number;
|
|
221
|
+
state: State;
|
|
222
|
+
requiresReplacement: boolean;
|
|
223
|
+
}>;
|
|
213
224
|
getData?(props: GetDataProps): Promise<{
|
|
214
225
|
state: State;
|
|
215
226
|
}>;
|
|
216
227
|
destroy?(): Promise<void>;
|
|
217
228
|
}
|
|
218
229
|
|
|
230
|
+
type ResourceEvent = {
|
|
231
|
+
urn: URN;
|
|
232
|
+
type: string;
|
|
233
|
+
};
|
|
234
|
+
type BeforeResourceCreateEvent = ResourceEvent & {
|
|
235
|
+
resource: Resource;
|
|
236
|
+
newInput: State;
|
|
237
|
+
};
|
|
238
|
+
type AfterResourceCreateEvent = ResourceEvent & {
|
|
239
|
+
resource: Resource;
|
|
240
|
+
newInput: State;
|
|
241
|
+
newOutput: State;
|
|
242
|
+
};
|
|
243
|
+
type BeforeResourceUpdateEvent = ResourceEvent & {
|
|
244
|
+
resource: Resource;
|
|
245
|
+
oldInput: State;
|
|
246
|
+
newInput: State;
|
|
247
|
+
oldOutput: State;
|
|
248
|
+
};
|
|
249
|
+
type AfterResourceUpdateEvent = ResourceEvent & {
|
|
250
|
+
resource: Resource;
|
|
251
|
+
oldInput: State;
|
|
252
|
+
newInput: State;
|
|
253
|
+
oldOutput: State;
|
|
254
|
+
newOutput: State;
|
|
255
|
+
};
|
|
256
|
+
type BeforeResourceDeleteEvent = ResourceEvent & {
|
|
257
|
+
oldInput: State;
|
|
258
|
+
oldOutput: State;
|
|
259
|
+
};
|
|
260
|
+
type AfterResourceDeleteEvent = ResourceEvent & {
|
|
261
|
+
oldInput: State;
|
|
262
|
+
oldOutput: State;
|
|
263
|
+
};
|
|
264
|
+
type Hooks = {
|
|
265
|
+
beforeResourceCreate?: (event: BeforeResourceCreateEvent) => Promise<void> | void;
|
|
266
|
+
beforeResourceUpdate?: (event: BeforeResourceUpdateEvent) => Promise<void> | void;
|
|
267
|
+
beforeResourceDelete?: (event: BeforeResourceDeleteEvent) => Promise<void> | void;
|
|
268
|
+
afterResourceCreate?: (event: AfterResourceCreateEvent) => Promise<void> | void;
|
|
269
|
+
afterResourceUpdate?: (event: AfterResourceUpdateEvent) => Promise<void> | void;
|
|
270
|
+
afterResourceDelete?: (event: AfterResourceDeleteEvent) => Promise<void> | void;
|
|
271
|
+
};
|
|
272
|
+
|
|
219
273
|
type ProcedureOptions = {
|
|
220
274
|
filters?: string[];
|
|
221
275
|
idempotentToken?: UUID;
|
|
@@ -227,6 +281,7 @@ type WorkSpaceOptions = {
|
|
|
227
281
|
state: StateBackend;
|
|
228
282
|
lock: LockBackend;
|
|
229
283
|
};
|
|
284
|
+
hooks?: Hooks;
|
|
230
285
|
};
|
|
231
286
|
declare class WorkSpace {
|
|
232
287
|
protected props: WorkSpaceOptions;
|
|
@@ -356,7 +411,11 @@ type CustomResourceProvider = Partial<{
|
|
|
356
411
|
createResource?(props: Omit<CreateProps, 'type'>): Promise<State>;
|
|
357
412
|
deleteResource?(props: Omit<DeleteProps, 'type'>): Promise<void>;
|
|
358
413
|
getData?(props: Omit<GetDataProps, 'type'>): Promise<State>;
|
|
414
|
+
planResourceChange?(props: Omit<PlanProps, 'type'>): Promise<{
|
|
415
|
+
state: State;
|
|
416
|
+
requiresReplacement: boolean;
|
|
417
|
+
}>;
|
|
359
418
|
}>;
|
|
360
419
|
declare const createCustomProvider: (providerId: string, resourceProviders: Record<string, CustomResourceProvider>) => Provider;
|
|
361
420
|
|
|
362
|
-
export { App, AppError, type Config, type CreateProps, type CustomResourceProvider, type DataSource, type DataSourceFunction, type DataSourceMeta, type DeleteProps, DynamoLockBackend, FileLockBackend, FileStateBackend, Future, type GetDataProps, type GetProps, Group, type Input, type LockBackend, MemoryLockBackend, MemoryStateBackend, type Meta, type Node, type OptionalInput, type OptionalOutput, Output, type ProcedureOptions, type Provider, type Resource, ResourceAlreadyExists, type ResourceClass, type ResourceConfig, ResourceError, type ResourceMeta, ResourceNotFound, S3StateBackend, Stack, type State, type StateBackend, type Tag, type URN, type UpdateProps, WorkSpace, type WorkSpaceOptions, createCustomProvider, createCustomResourceClass, createDebugger, createMeta, deferredOutput, enableDebug, findInputDeps, getMeta, isDataSource, isNode, isResource, nodeMetaSymbol, output, resolveInputs };
|
|
421
|
+
export { App, AppError, type Config, type CreateProps, type CustomResourceProvider, type DataSource, type DataSourceFunction, type DataSourceMeta, type DeleteProps, DynamoLockBackend, FileLockBackend, FileStateBackend, Future, type GetDataProps, type GetProps, Group, type Input, type LockBackend, MemoryLockBackend, MemoryStateBackend, type Meta, type Node, type OptionalInput, type OptionalOutput, Output, type PlanProps, type ProcedureOptions, type Provider, type Resource, ResourceAlreadyExists, type ResourceClass, type ResourceConfig, ResourceError, type ResourceMeta, ResourceNotFound, S3StateBackend, Stack, type State, type StateBackend, type Tag, type URN, type UpdateProps, WorkSpace, type WorkSpaceOptions, createCustomProvider, createCustomResourceClass, createDebugger, createMeta, deferredOutput, enableDebug, findInputDeps, getMeta, isDataSource, isNode, isResource, nodeMetaSymbol, output, resolveInputs };
|
package/dist/index.js
CHANGED
|
@@ -420,6 +420,9 @@ var DependencyGraph = class {
|
|
|
420
420
|
this.callbacks.set(urn, callback);
|
|
421
421
|
this.graph.mergeNode(urn);
|
|
422
422
|
for (const dep of deps) {
|
|
423
|
+
if (!dep) {
|
|
424
|
+
throw new Error(`Resource ${urn} has an undefined dependency.`);
|
|
425
|
+
}
|
|
423
426
|
if (willCreateCycle(this.graph, dep, urn)) {
|
|
424
427
|
throw new Error(`There is a circular dependency between ${urn} -> ${dep}`);
|
|
425
428
|
}
|
|
@@ -610,11 +613,23 @@ var deleteResource = async (appToken, urn, state, opt) => {
|
|
|
610
613
|
const idempotantToken = createIdempotantToken(appToken, urn, "delete");
|
|
611
614
|
const provider = findProvider(opt.providers, state.provider);
|
|
612
615
|
try {
|
|
616
|
+
await opt.hooks?.beforeResourceDelete?.({
|
|
617
|
+
urn,
|
|
618
|
+
type: state.type,
|
|
619
|
+
oldInput: state.input,
|
|
620
|
+
oldOutput: state.output
|
|
621
|
+
});
|
|
613
622
|
await provider.deleteResource({
|
|
614
623
|
type: state.type,
|
|
615
624
|
state: state.output,
|
|
616
625
|
idempotantToken
|
|
617
626
|
});
|
|
627
|
+
await opt.hooks?.afterResourceDelete?.({
|
|
628
|
+
urn,
|
|
629
|
+
type: state.type,
|
|
630
|
+
oldInput: state.input,
|
|
631
|
+
oldOutput: state.output
|
|
632
|
+
});
|
|
618
633
|
} catch (error) {
|
|
619
634
|
if (error instanceof ResourceNotFound) {
|
|
620
635
|
debug(state.type, "already deleted");
|
|
@@ -699,11 +714,24 @@ var createResource = async (resource, appToken, input, opt) => {
|
|
|
699
714
|
debug2(input);
|
|
700
715
|
let result;
|
|
701
716
|
try {
|
|
717
|
+
await opt.hooks?.beforeResourceCreate?.({
|
|
718
|
+
urn: resource.urn,
|
|
719
|
+
type: meta.type,
|
|
720
|
+
resource,
|
|
721
|
+
newInput: input
|
|
722
|
+
});
|
|
702
723
|
result = await provider.createResource({
|
|
703
724
|
type: meta.type,
|
|
704
725
|
state: input,
|
|
705
726
|
idempotantToken
|
|
706
727
|
});
|
|
728
|
+
await opt.hooks?.afterResourceCreate?.({
|
|
729
|
+
urn: resource.urn,
|
|
730
|
+
type: meta.type,
|
|
731
|
+
resource,
|
|
732
|
+
newInput: input,
|
|
733
|
+
newOutput: result.state
|
|
734
|
+
});
|
|
707
735
|
} catch (error) {
|
|
708
736
|
throw ResourceError.wrap(meta.urn, meta.type, "create", error);
|
|
709
737
|
}
|
|
@@ -774,7 +802,7 @@ var importResource = async (resource, input, opt) => {
|
|
|
774
802
|
|
|
775
803
|
// src/workspace/procedure/replace-resource.ts
|
|
776
804
|
var debug5 = createDebugger("Replace");
|
|
777
|
-
var replaceResource = async (resource, appToken,
|
|
805
|
+
var replaceResource = async (resource, appToken, priorInputState, priorOutputState, proposedState, opt) => {
|
|
778
806
|
const meta = getMeta(resource);
|
|
779
807
|
const urn = meta.urn;
|
|
780
808
|
const type = meta.type;
|
|
@@ -786,11 +814,23 @@ var replaceResource = async (resource, appToken, priorState, proposedState, opt)
|
|
|
786
814
|
debug5("retain", type);
|
|
787
815
|
} else {
|
|
788
816
|
try {
|
|
817
|
+
await opt.hooks?.beforeResourceDelete?.({
|
|
818
|
+
urn,
|
|
819
|
+
type,
|
|
820
|
+
oldInput: priorInputState,
|
|
821
|
+
oldOutput: priorOutputState
|
|
822
|
+
});
|
|
789
823
|
await provider.deleteResource({
|
|
790
824
|
type,
|
|
791
|
-
state:
|
|
825
|
+
state: priorOutputState,
|
|
792
826
|
idempotantToken
|
|
793
827
|
});
|
|
828
|
+
await opt.hooks?.afterResourceDelete?.({
|
|
829
|
+
urn,
|
|
830
|
+
type,
|
|
831
|
+
oldInput: priorInputState,
|
|
832
|
+
oldOutput: priorOutputState
|
|
833
|
+
});
|
|
794
834
|
} catch (error) {
|
|
795
835
|
if (error instanceof ResourceNotFound) {
|
|
796
836
|
debug5(type, "already deleted");
|
|
@@ -801,11 +841,24 @@ var replaceResource = async (resource, appToken, priorState, proposedState, opt)
|
|
|
801
841
|
}
|
|
802
842
|
let result;
|
|
803
843
|
try {
|
|
844
|
+
await opt.hooks?.beforeResourceCreate?.({
|
|
845
|
+
urn,
|
|
846
|
+
type,
|
|
847
|
+
resource,
|
|
848
|
+
newInput: proposedState
|
|
849
|
+
});
|
|
804
850
|
result = await provider.createResource({
|
|
805
851
|
type,
|
|
806
852
|
state: proposedState,
|
|
807
853
|
idempotantToken
|
|
808
854
|
});
|
|
855
|
+
await opt.hooks?.afterResourceCreate?.({
|
|
856
|
+
urn,
|
|
857
|
+
type,
|
|
858
|
+
resource,
|
|
859
|
+
newInput: proposedState,
|
|
860
|
+
newOutput: result.state
|
|
861
|
+
});
|
|
809
862
|
} catch (error) {
|
|
810
863
|
throw ResourceError.wrap(urn, type, "replace", error);
|
|
811
864
|
}
|
|
@@ -817,7 +870,7 @@ var replaceResource = async (resource, appToken, priorState, proposedState, opt)
|
|
|
817
870
|
|
|
818
871
|
// src/workspace/procedure/update-resource.ts
|
|
819
872
|
var debug6 = createDebugger("Update");
|
|
820
|
-
var updateResource = async (resource, appToken,
|
|
873
|
+
var updateResource = async (resource, appToken, priorInputState, priorOutputState, proposedState, opt) => {
|
|
821
874
|
const meta = getMeta(resource);
|
|
822
875
|
const provider = findProvider(opt.providers, meta.provider);
|
|
823
876
|
const idempotantToken = createIdempotantToken(appToken, meta.urn, "update");
|
|
@@ -825,12 +878,29 @@ var updateResource = async (resource, appToken, priorState, proposedState, opt)
|
|
|
825
878
|
debug6(meta.type);
|
|
826
879
|
debug6(proposedState);
|
|
827
880
|
try {
|
|
881
|
+
await opt.hooks?.beforeResourceUpdate?.({
|
|
882
|
+
urn: resource.urn,
|
|
883
|
+
type: meta.type,
|
|
884
|
+
resource,
|
|
885
|
+
newInput: proposedState,
|
|
886
|
+
oldInput: priorInputState,
|
|
887
|
+
oldOutput: priorOutputState
|
|
888
|
+
});
|
|
828
889
|
result = await provider.updateResource({
|
|
829
890
|
type: meta.type,
|
|
830
|
-
priorState,
|
|
891
|
+
priorState: priorOutputState,
|
|
831
892
|
proposedState,
|
|
832
893
|
idempotantToken
|
|
833
894
|
});
|
|
895
|
+
await opt.hooks?.afterResourceUpdate?.({
|
|
896
|
+
urn: resource.urn,
|
|
897
|
+
type: meta.type,
|
|
898
|
+
resource,
|
|
899
|
+
newInput: proposedState,
|
|
900
|
+
oldInput: priorInputState,
|
|
901
|
+
newOutput: result.state,
|
|
902
|
+
oldOutput: priorOutputState
|
|
903
|
+
});
|
|
834
904
|
} catch (error) {
|
|
835
905
|
throw ResourceError.wrap(meta.urn, meta.type, "update", error);
|
|
836
906
|
}
|
|
@@ -842,6 +912,89 @@ var updateResource = async (resource, appToken, priorState, proposedState, opt)
|
|
|
842
912
|
|
|
843
913
|
// src/workspace/procedure/deploy-app.ts
|
|
844
914
|
var debug7 = createDebugger("Deploy App");
|
|
915
|
+
var findDependencyPaths = (value, dependencyUrn, path = []) => {
|
|
916
|
+
const paths = [];
|
|
917
|
+
const visit = (current, currentPath) => {
|
|
918
|
+
if (current instanceof Output) {
|
|
919
|
+
for (const dep of current.dependencies) {
|
|
920
|
+
if (dep.urn === dependencyUrn) {
|
|
921
|
+
paths.push(currentPath);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (Array.isArray(current)) {
|
|
928
|
+
current.forEach((item, index) => {
|
|
929
|
+
visit(item, [...currentPath, index]);
|
|
930
|
+
});
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (current && typeof current === "object") {
|
|
934
|
+
for (const [key, item] of Object.entries(current)) {
|
|
935
|
+
visit(item, [...currentPath, key]);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
visit(value, path);
|
|
940
|
+
return paths;
|
|
941
|
+
};
|
|
942
|
+
var cloneState = (value) => JSON.parse(JSON.stringify(value));
|
|
943
|
+
var removeAtPath = (target, path) => {
|
|
944
|
+
if (path.length === 0) return;
|
|
945
|
+
let parent = target;
|
|
946
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
947
|
+
if (parent == null) return;
|
|
948
|
+
parent = parent[path[i]];
|
|
949
|
+
}
|
|
950
|
+
const last = path[path.length - 1];
|
|
951
|
+
if (Array.isArray(parent) && typeof last === "number") {
|
|
952
|
+
if (last >= 0 && last < parent.length) {
|
|
953
|
+
parent.splice(last, 1);
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (parent && typeof parent === "object") {
|
|
958
|
+
delete parent[last];
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
var stripDependencyInputs = (input, metaInput, dependencyUrn) => {
|
|
962
|
+
const paths = findDependencyPaths(metaInput, dependencyUrn);
|
|
963
|
+
if (paths.length === 0) {
|
|
964
|
+
return input;
|
|
965
|
+
}
|
|
966
|
+
const detached = cloneState(input);
|
|
967
|
+
const sortedPaths = [...paths].sort((a, b) => {
|
|
968
|
+
if (a.length !== b.length) return b.length - a.length;
|
|
969
|
+
const aLast = a[a.length - 1];
|
|
970
|
+
const bLast = b[b.length - 1];
|
|
971
|
+
if (typeof aLast === "number" && typeof bLast === "number") {
|
|
972
|
+
return bLast - aLast;
|
|
973
|
+
}
|
|
974
|
+
return 0;
|
|
975
|
+
});
|
|
976
|
+
for (const path of sortedPaths) {
|
|
977
|
+
removeAtPath(detached, path);
|
|
978
|
+
}
|
|
979
|
+
return detached;
|
|
980
|
+
};
|
|
981
|
+
var allowsDependentReplace = (replaceOnChanges, dependencyPaths) => {
|
|
982
|
+
if (!replaceOnChanges || replaceOnChanges.length === 0) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
for (const path of dependencyPaths) {
|
|
986
|
+
const base = typeof path[0] === "string" ? path[0] : void 0;
|
|
987
|
+
if (!base) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
for (const replacePath of replaceOnChanges) {
|
|
991
|
+
if (replacePath === base || replacePath.startsWith(`${base}.`) || replacePath.startsWith(`${base}[`) || replacePath.startsWith(`${base}.*`)) {
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return false;
|
|
997
|
+
};
|
|
845
998
|
var deployApp = async (app, opt) => {
|
|
846
999
|
debug7(app.name, "start");
|
|
847
1000
|
const latestState = await opt.backend.state.get(app.urn);
|
|
@@ -864,8 +1017,23 @@ var deployApp = async (app, opt) => {
|
|
|
864
1017
|
stacks = app.stacks.filter((stack) => opt.filters.includes(stack.name));
|
|
865
1018
|
filteredOutStacks = app.stacks.filter((stack) => !opt.filters.includes(stack.name));
|
|
866
1019
|
}
|
|
1020
|
+
const nodeByUrn = /* @__PURE__ */ new Map();
|
|
1021
|
+
const stackStates = /* @__PURE__ */ new Map();
|
|
1022
|
+
const plannedDependents = /* @__PURE__ */ new Set();
|
|
1023
|
+
const forcedUpdateDependents = /* @__PURE__ */ new Set();
|
|
1024
|
+
for (const stack of stacks) {
|
|
1025
|
+
const stackState = appState.stacks[stack.urn] = appState.stacks[stack.urn] ?? {
|
|
1026
|
+
name: stack.name,
|
|
1027
|
+
nodes: {}
|
|
1028
|
+
};
|
|
1029
|
+
stackStates.set(stack.urn, stackState);
|
|
1030
|
+
for (const node of stack.nodes) {
|
|
1031
|
+
nodeByUrn.set(getMeta(node).urn, node);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
867
1034
|
const queue = createConcurrencyQueue(opt.concurrency ?? 10);
|
|
868
1035
|
const graph = new DependencyGraph();
|
|
1036
|
+
const replacementDeletes = /* @__PURE__ */ new Map();
|
|
869
1037
|
const allNodes = {};
|
|
870
1038
|
for (const stackState of Object.values(appState.stacks)) {
|
|
871
1039
|
for (const [urn, nodeState] of entries(stackState.nodes)) {
|
|
@@ -912,10 +1080,7 @@ var deployApp = async (app, opt) => {
|
|
|
912
1080
|
}
|
|
913
1081
|
}
|
|
914
1082
|
for (const stack of stacks) {
|
|
915
|
-
const stackState =
|
|
916
|
-
name: stack.name,
|
|
917
|
-
nodes: {}
|
|
918
|
-
};
|
|
1083
|
+
const stackState = stackStates.get(stack.urn);
|
|
919
1084
|
for (const [urn, nodeState] of entries(stackState.nodes)) {
|
|
920
1085
|
const resource = stack.nodes.find((r) => getMeta(r).urn === urn);
|
|
921
1086
|
if (!resource) {
|
|
@@ -986,6 +1151,7 @@ var deployApp = async (app, opt) => {
|
|
|
986
1151
|
const newResourceState = await updateResource(
|
|
987
1152
|
node,
|
|
988
1153
|
appState.idempotentToken,
|
|
1154
|
+
importedState.input,
|
|
989
1155
|
importedState.output,
|
|
990
1156
|
input,
|
|
991
1157
|
opt
|
|
@@ -1013,22 +1179,121 @@ var deployApp = async (app, opt) => {
|
|
|
1013
1179
|
!compareState(nodeState.input, input)
|
|
1014
1180
|
) {
|
|
1015
1181
|
let newResourceState;
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1182
|
+
const ignoreReplace = forcedUpdateDependents.has(meta2.urn);
|
|
1183
|
+
if (!ignoreReplace && requiresReplacement(nodeState.input, input, meta2.config?.replaceOnChanges ?? [])) {
|
|
1184
|
+
if (meta2.config?.createBeforeReplace) {
|
|
1185
|
+
const priorState = { ...nodeState };
|
|
1186
|
+
newResourceState = await createResource(node, appState.idempotentToken, input, opt);
|
|
1187
|
+
if (!meta2.config?.retainOnDelete) {
|
|
1188
|
+
replacementDeletes.set(meta2.urn, priorState);
|
|
1189
|
+
}
|
|
1190
|
+
} else {
|
|
1191
|
+
for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
|
|
1192
|
+
if (!isResource(dependentNode)) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const dependentMeta = getMeta(dependentNode);
|
|
1196
|
+
if (!dependentMeta.dependencies.has(meta2.urn)) {
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (plannedDependents.has(dependentUrn)) {
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
const dependentStackState = stackStates.get(dependentMeta.stack.urn);
|
|
1203
|
+
const dependentState = dependentStackState?.nodes[dependentUrn];
|
|
1204
|
+
if (!dependentStackState || !dependentState) {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
const dependencyPaths = findDependencyPaths(dependentMeta.input, meta2.urn);
|
|
1208
|
+
if (dependencyPaths.length === 0) {
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const detachedInput = stripDependencyInputs(
|
|
1212
|
+
dependentState.input,
|
|
1213
|
+
dependentMeta.input,
|
|
1214
|
+
meta2.urn
|
|
1215
|
+
);
|
|
1216
|
+
if (compareState(dependentState.input, detachedInput)) {
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
plannedDependents.add(dependentUrn);
|
|
1220
|
+
let dependentRequiresReplacement = false;
|
|
1221
|
+
const dependentProvider = findProvider(opt.providers, dependentMeta.provider);
|
|
1222
|
+
if (dependentProvider.planResourceChange) {
|
|
1223
|
+
try {
|
|
1224
|
+
const dependentPlan = await dependentProvider.planResourceChange({
|
|
1225
|
+
type: dependentMeta.type,
|
|
1226
|
+
priorState: dependentState.output,
|
|
1227
|
+
proposedState: detachedInput
|
|
1228
|
+
});
|
|
1229
|
+
dependentRequiresReplacement = dependentPlan.requiresReplacement;
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
throw ResourceError.wrap(
|
|
1232
|
+
dependentMeta.urn,
|
|
1233
|
+
dependentMeta.type,
|
|
1234
|
+
"update",
|
|
1235
|
+
error
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (dependentRequiresReplacement) {
|
|
1240
|
+
if (!allowsDependentReplace(
|
|
1241
|
+
dependentMeta.config?.replaceOnChanges,
|
|
1242
|
+
dependencyPaths
|
|
1243
|
+
)) {
|
|
1244
|
+
throw ResourceError.wrap(
|
|
1245
|
+
dependentMeta.urn,
|
|
1246
|
+
dependentMeta.type,
|
|
1247
|
+
"update",
|
|
1248
|
+
new Error(
|
|
1249
|
+
`Replacing ${meta2.urn} requires ${dependentMeta.urn} to set replaceOnChanges for its dependency fields.`
|
|
1250
|
+
)
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
await deleteResource(
|
|
1254
|
+
appState.idempotentToken,
|
|
1255
|
+
dependentUrn,
|
|
1256
|
+
dependentState,
|
|
1257
|
+
opt
|
|
1258
|
+
);
|
|
1259
|
+
delete dependentStackState.nodes[dependentUrn];
|
|
1260
|
+
} else {
|
|
1261
|
+
const updated = await updateResource(
|
|
1262
|
+
dependentNode,
|
|
1263
|
+
appState.idempotentToken,
|
|
1264
|
+
dependentState.input,
|
|
1265
|
+
dependentState.output,
|
|
1266
|
+
detachedInput,
|
|
1267
|
+
opt
|
|
1268
|
+
);
|
|
1269
|
+
Object.assign(dependentState, {
|
|
1270
|
+
input: detachedInput,
|
|
1271
|
+
...updated
|
|
1272
|
+
});
|
|
1273
|
+
forcedUpdateDependents.add(dependentUrn);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
newResourceState = await replaceResource(
|
|
1277
|
+
node,
|
|
1278
|
+
appState.idempotentToken,
|
|
1279
|
+
nodeState.input,
|
|
1280
|
+
nodeState.output,
|
|
1281
|
+
input,
|
|
1282
|
+
opt
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1024
1285
|
} else {
|
|
1025
1286
|
newResourceState = await updateResource(
|
|
1026
1287
|
node,
|
|
1027
1288
|
appState.idempotentToken,
|
|
1289
|
+
nodeState.input,
|
|
1028
1290
|
nodeState.output,
|
|
1029
1291
|
input,
|
|
1030
1292
|
opt
|
|
1031
1293
|
);
|
|
1294
|
+
if (ignoreReplace) {
|
|
1295
|
+
forcedUpdateDependents.delete(meta2.urn);
|
|
1296
|
+
}
|
|
1032
1297
|
}
|
|
1033
1298
|
Object.assign(nodeState, {
|
|
1034
1299
|
input,
|
|
@@ -1047,6 +1312,19 @@ var deployApp = async (app, opt) => {
|
|
|
1047
1312
|
}
|
|
1048
1313
|
}
|
|
1049
1314
|
const errors = await graph.run();
|
|
1315
|
+
if (errors.length === 0 && replacementDeletes.size > 0) {
|
|
1316
|
+
for (const [urn, nodeState] of replacementDeletes.entries()) {
|
|
1317
|
+
try {
|
|
1318
|
+
await deleteResource(appState.idempotentToken, urn, nodeState, opt);
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
if (error instanceof Error) {
|
|
1321
|
+
errors.push(error);
|
|
1322
|
+
} else {
|
|
1323
|
+
errors.push(new Error(`${error}`));
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1050
1328
|
removeEmptyStackStates(appState);
|
|
1051
1329
|
delete appState.idempotentToken;
|
|
1052
1330
|
await opt.backend.state.update(app.urn, appState);
|
|
@@ -1514,6 +1792,21 @@ var createCustomProvider = (providerId, resourceProviders) => {
|
|
|
1514
1792
|
async deleteResource({ type, ...props }) {
|
|
1515
1793
|
await getProvider(type).deleteResource?.(props);
|
|
1516
1794
|
},
|
|
1795
|
+
async planResourceChange({ type, ...props }) {
|
|
1796
|
+
const provider = getProvider(type);
|
|
1797
|
+
if (!provider.planResourceChange) {
|
|
1798
|
+
return {
|
|
1799
|
+
version,
|
|
1800
|
+
state: props.proposedState,
|
|
1801
|
+
requiresReplacement: false
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
const result = await provider.planResourceChange(props);
|
|
1805
|
+
return {
|
|
1806
|
+
version,
|
|
1807
|
+
...result
|
|
1808
|
+
};
|
|
1809
|
+
},
|
|
1517
1810
|
async getData({ type, ...props }) {
|
|
1518
1811
|
return {
|
|
1519
1812
|
version,
|
package/package.json
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terraforge/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/terraforge-js/terraforge.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/terraforge-js/terraforge/issues"
|
|
11
|
+
},
|
|
5
12
|
"module": "./dist/index.js",
|
|
6
13
|
"types": "./dist/index.d.ts",
|
|
7
14
|
"exports": {
|
|
@@ -12,7 +19,7 @@
|
|
|
12
19
|
},
|
|
13
20
|
"scripts": {
|
|
14
21
|
"build": "tsup src/index.ts --format esm --dts --clean --out-dir ./dist",
|
|
15
|
-
"prepublishOnly": "bun run build",
|
|
22
|
+
"prepublishOnly": "if bun run test; then bun run build; else exit; fi",
|
|
16
23
|
"test": "bun test"
|
|
17
24
|
},
|
|
18
25
|
"dependencies": {
|