codify-plugin-lib 1.0.38 → 1.0.39
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/entities/plan-types.d.ts +2 -2
- package/dist/entities/plan.d.ts +1 -1
- package/dist/entities/plan.js +1 -1
- package/dist/entities/resource.d.ts +1 -1
- package/dist/entities/resource.js +14 -5
- package/package.json +1 -1
- package/src/entities/plan-types.ts +2 -2
- package/src/entities/plan.ts +4 -4
- package/src/entities/resource-parameters.test.ts +159 -0
- package/src/entities/resource.test.ts +26 -109
- package/src/entities/resource.ts +30 -18
|
@@ -5,7 +5,7 @@ export interface ParameterConfiguration {
|
|
|
5
5
|
isArrayElementEqual?: (a: any, b: any) => boolean;
|
|
6
6
|
isStatefulParameter?: boolean;
|
|
7
7
|
}
|
|
8
|
-
export interface PlanConfiguration {
|
|
8
|
+
export interface PlanConfiguration<T> {
|
|
9
9
|
statefulMode: boolean;
|
|
10
|
-
parameterConfigurations?: Record<
|
|
10
|
+
parameterConfigurations?: Record<keyof T, ParameterConfiguration>;
|
|
11
11
|
}
|
package/dist/entities/plan.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare class Plan<T extends StringIndexedObject> {
|
|
|
6
6
|
changeSet: ChangeSet<T>;
|
|
7
7
|
resourceMetadata: ResourceConfig;
|
|
8
8
|
constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig);
|
|
9
|
-
static create<T extends StringIndexedObject>(desiredConfig: Partial<T> & ResourceConfig, currentConfig: Partial<T> & ResourceConfig | null, configuration: PlanConfiguration): Plan<T>;
|
|
9
|
+
static create<T extends StringIndexedObject>(desiredConfig: Partial<T> & ResourceConfig, currentConfig: Partial<T> & ResourceConfig | null, configuration: PlanConfiguration<T>): Plan<T>;
|
|
10
10
|
getResourceType(): string;
|
|
11
11
|
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T>;
|
|
12
12
|
get desiredConfig(): T;
|
package/dist/entities/plan.js
CHANGED
|
@@ -17,7 +17,7 @@ export class Plan {
|
|
|
17
17
|
.filter(([k, v]) => v.isStatefulParameter)
|
|
18
18
|
.map(([k, v]) => k));
|
|
19
19
|
const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
|
|
20
|
-
const { parameters: currentParameters } = currentConfig ? splitUserConfig(currentConfig) : { parameters:
|
|
20
|
+
const { parameters: currentParameters } = currentConfig != null ? splitUserConfig(currentConfig) : { parameters: null };
|
|
21
21
|
const parameterChangeSet = ChangeSet.calculateParameterChangeSet(desiredParameters, currentParameters, { statefulMode: configuration.statefulMode, parameterConfigurations });
|
|
22
22
|
let resourceOperation;
|
|
23
23
|
if (!currentConfig && desiredConfig) {
|
|
@@ -7,7 +7,7 @@ export declare abstract class Resource<T extends StringIndexedObject> {
|
|
|
7
7
|
readonly typeId: string;
|
|
8
8
|
readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
|
|
9
9
|
readonly dependencies: Resource<any>[];
|
|
10
|
-
readonly parameterConfigurations: Record<
|
|
10
|
+
readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>;
|
|
11
11
|
private readonly options;
|
|
12
12
|
protected constructor(configuration: ResourceConfiguration<T>);
|
|
13
13
|
getDependencyTypeIds(): string[];
|
|
@@ -28,18 +28,24 @@ export class Resource {
|
|
|
28
28
|
const resourceParameters = Object.fromEntries([
|
|
29
29
|
...Object.entries(desiredParameters).filter(([key]) => !this.statefulParameters.has(key)),
|
|
30
30
|
]);
|
|
31
|
+
const statefulParameters = [...this.statefulParameters.values()]
|
|
32
|
+
.filter((sp) => desiredParameters[sp.name] !== undefined);
|
|
31
33
|
const keysToRefresh = new Set(Object.keys(resourceParameters));
|
|
32
34
|
const currentParameters = await this.refresh(keysToRefresh);
|
|
33
|
-
if (
|
|
35
|
+
if (currentParameters == null && statefulParameters.length === 0) {
|
|
34
36
|
return Plan.create(desiredConfig, null, planConfiguration);
|
|
35
37
|
}
|
|
36
38
|
this.validateRefreshResults(currentParameters, keysToRefresh);
|
|
37
|
-
const
|
|
38
|
-
.filter((sp) => desiredParameters[sp.name] !== undefined);
|
|
39
|
+
const currentStatefulParameters = {};
|
|
39
40
|
for (const statefulParameter of statefulParameters) {
|
|
40
|
-
|
|
41
|
+
const desiredValue = desiredParameters[statefulParameter.name];
|
|
42
|
+
let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
|
|
43
|
+
if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
|
|
44
|
+
currentValue = currentValue.filter((p) => desiredValue?.includes(p));
|
|
45
|
+
}
|
|
46
|
+
currentStatefulParameters[statefulParameter.name] = currentValue;
|
|
41
47
|
}
|
|
42
|
-
return Plan.create(desiredConfig, { ...currentParameters, ...resourceMetadata }, planConfiguration);
|
|
48
|
+
return Plan.create(desiredConfig, { ...currentParameters, ...currentStatefulParameters, ...resourceMetadata }, planConfiguration);
|
|
43
49
|
}
|
|
44
50
|
async apply(plan) {
|
|
45
51
|
if (plan.getResourceType() !== this.typeId) {
|
|
@@ -140,6 +146,9 @@ export class Resource {
|
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
148
|
validateRefreshResults(refresh, desiredKeys) {
|
|
149
|
+
if (!refresh) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
143
152
|
const refreshKeys = new Set(Object.keys(refresh));
|
|
144
153
|
if (!setsEqual(desiredKeys, refreshKeys)) {
|
|
145
154
|
throw new Error(`Resource ${this.options.type}
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@ export interface ParameterConfiguration {
|
|
|
20
20
|
isStatefulParameter?: boolean;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export interface PlanConfiguration {
|
|
23
|
+
export interface PlanConfiguration<T> {
|
|
24
24
|
statefulMode: boolean;
|
|
25
|
-
parameterConfigurations?: Record<
|
|
25
|
+
parameterConfigurations?: Record<keyof T, ParameterConfiguration>;
|
|
26
26
|
}
|
package/src/entities/plan.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
StringIndexedObject,
|
|
9
9
|
} from 'codify-schemas';
|
|
10
10
|
import { randomUUID } from 'crypto';
|
|
11
|
-
import { PlanConfiguration } from './plan-types.js';
|
|
11
|
+
import { ParameterConfiguration, PlanConfiguration } from './plan-types.js';
|
|
12
12
|
import { splitUserConfig } from '../utils/utils.js';
|
|
13
13
|
|
|
14
14
|
export class Plan<T extends StringIndexedObject> {
|
|
@@ -25,9 +25,9 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
25
25
|
static create<T extends StringIndexedObject>(
|
|
26
26
|
desiredConfig: Partial<T> & ResourceConfig,
|
|
27
27
|
currentConfig: Partial<T> & ResourceConfig | null,
|
|
28
|
-
configuration: PlanConfiguration
|
|
28
|
+
configuration: PlanConfiguration<T>
|
|
29
29
|
): Plan<T> {
|
|
30
|
-
const parameterConfigurations = configuration.parameterConfigurations ?? {}
|
|
30
|
+
const parameterConfigurations = configuration.parameterConfigurations ?? {} as Record<keyof T, ParameterConfiguration>;
|
|
31
31
|
const statefulParameterNames = new Set(
|
|
32
32
|
[...Object.entries(parameterConfigurations)]
|
|
33
33
|
.filter(([k, v]) => v.isStatefulParameter)
|
|
@@ -35,7 +35,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
|
|
38
|
-
const { parameters: currentParameters } = currentConfig ? splitUserConfig(currentConfig) : { parameters:
|
|
38
|
+
const { parameters: currentParameters } = currentConfig != null ? splitUserConfig(currentConfig) : { parameters: null };
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
// TODO: After adding in state files, need to calculate deletes here
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { StatefulParameter, StatefulParameterConfiguration } from './stateful-parameter.js';
|
|
3
|
+
import { Plan } from './plan.js';
|
|
4
|
+
import { spy } from 'sinon';
|
|
5
|
+
import { ResourceOperation } from 'codify-schemas';
|
|
6
|
+
import { TestConfig, TestResource } from './resource.test.js';
|
|
7
|
+
|
|
8
|
+
class TestParameter extends StatefulParameter<TestConfig, string> {
|
|
9
|
+
constructor(configuration?: StatefulParameterConfiguration<TestConfig>) {
|
|
10
|
+
super(configuration ?? {
|
|
11
|
+
name: 'propA'
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
applyAdd(valueToAdd: string, plan: Plan<TestConfig>): Promise<void> {
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}
|
|
18
|
+
applyModify(newValue: string, previousValue: string, allowDeletes: boolean, plan: Plan<TestConfig>): Promise<void> {
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
}
|
|
21
|
+
applyRemove(valueToRemove: string, plan: Plan<TestConfig>): Promise<void> {
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
async refresh(): Promise<string | null> {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('Resource parameters tests', () => {
|
|
30
|
+
it('supports the creation of stateful parameters', async () => {
|
|
31
|
+
|
|
32
|
+
const statefulParameter = new class extends TestParameter {
|
|
33
|
+
async refresh(): Promise<string | null> {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
39
|
+
|
|
40
|
+
const resource = new class extends TestResource {
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
super({
|
|
44
|
+
type: 'resource',
|
|
45
|
+
statefulParameters: [statefulParameterSpy],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resourceSpy = spy(resource);
|
|
51
|
+
const result = await resourceSpy.apply(
|
|
52
|
+
Plan.create<TestConfig>(
|
|
53
|
+
{ type: 'resource', propA: 'a', propB: 0, propC: 'b' },
|
|
54
|
+
null,
|
|
55
|
+
{ statefulMode: false },
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(statefulParameterSpy.applyAdd.calledOnce).to.be.true;
|
|
60
|
+
expect(resourceSpy.applyCreate.calledOnce).to.be.true;
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('supports the modification of stateful parameters', async () => {
|
|
64
|
+
const statefulParameter = new class extends TestParameter {
|
|
65
|
+
async refresh(): Promise<string | null> {
|
|
66
|
+
return 'b';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
71
|
+
|
|
72
|
+
const resource = new class extends TestResource {
|
|
73
|
+
|
|
74
|
+
constructor() {
|
|
75
|
+
super({
|
|
76
|
+
type: 'resource',
|
|
77
|
+
statefulParameters: [statefulParameterSpy],
|
|
78
|
+
parameterConfigurations: {
|
|
79
|
+
propB: { planOperation: ResourceOperation.MODIFY },
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
85
|
+
return { propB: -1, propC: 'b' }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const plan = await resource.plan({ type: 'resource', propA: 'a', propB: 0, propC: 'b' })
|
|
90
|
+
|
|
91
|
+
const resourceSpy = spy(resource);
|
|
92
|
+
const result = await resourceSpy.apply(plan);
|
|
93
|
+
|
|
94
|
+
expect(statefulParameterSpy.applyModify.calledOnce).to.be.true;
|
|
95
|
+
expect(resourceSpy.applyModify.calledOnce).to.be.true;
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('Filters array results in stateless mode to prevent modify from being called', async () => {
|
|
99
|
+
const statefulParameter = new class extends TestParameter {
|
|
100
|
+
async refresh(): Promise<any | null> {
|
|
101
|
+
return ['a', 'b', 'c', 'd']
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
106
|
+
|
|
107
|
+
const resource = new class extends TestResource {
|
|
108
|
+
constructor() {
|
|
109
|
+
super({
|
|
110
|
+
type: 'resource',
|
|
111
|
+
statefulParameters: [statefulParameterSpy],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const plan = await resource.plan({ type: 'resource', propA: ['a', 'b'] } as any)
|
|
121
|
+
|
|
122
|
+
expect(plan).toMatchObject({
|
|
123
|
+
changeSet: {
|
|
124
|
+
operation: ResourceOperation.NOOP,
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('Filters array results in stateless mode to prevent modify from being called', async () => {
|
|
130
|
+
const statefulParameter = new class extends TestParameter {
|
|
131
|
+
async refresh(): Promise<any | null> {
|
|
132
|
+
return ['a', 'b']
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const statefulParameterSpy = spy(statefulParameter);
|
|
137
|
+
|
|
138
|
+
const resource = new class extends TestResource {
|
|
139
|
+
constructor() {
|
|
140
|
+
super({
|
|
141
|
+
type: 'resource',
|
|
142
|
+
statefulParameters: [statefulParameterSpy],
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const plan = await resource.plan({ type: 'resource', propA: ['a', 'b', 'c', 'd'] } as any)
|
|
152
|
+
|
|
153
|
+
expect(plan).toMatchObject({
|
|
154
|
+
changeSet: {
|
|
155
|
+
operation: ResourceOperation.MODIFY,
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { Resource } from './resource.js';
|
|
2
2
|
import { ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
3
|
-
import { ParameterChange } from './change-set.js';
|
|
4
3
|
import { spy } from 'sinon';
|
|
5
4
|
import { Plan } from './plan.js';
|
|
6
|
-
import { StatefulParameter } from './stateful-parameter.js';
|
|
7
5
|
import { describe, expect, it } from 'vitest'
|
|
8
|
-
import { ResourceConfiguration } from './resource-types.js';
|
|
6
|
+
import { ResourceConfiguration, ValidationResult } from './resource-types.js';
|
|
9
7
|
|
|
10
|
-
interface TestConfig extends StringIndexedObject {
|
|
8
|
+
export interface TestConfig extends StringIndexedObject {
|
|
11
9
|
propA: string;
|
|
12
10
|
propB: number;
|
|
13
11
|
propC?: string;
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
class TestResource extends Resource<TestConfig> {
|
|
14
|
+
export class TestResource extends Resource<TestConfig> {
|
|
17
15
|
constructor(options: ResourceConfiguration<TestConfig>) {
|
|
18
16
|
super(options);
|
|
19
17
|
}
|
|
@@ -34,8 +32,10 @@ class TestResource extends Resource<TestConfig> {
|
|
|
34
32
|
};
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
async validate(config: unknown): Promise<
|
|
38
|
-
return
|
|
35
|
+
async validate(config: unknown): Promise<ValidationResult> {
|
|
36
|
+
return {
|
|
37
|
+
isValid: true
|
|
38
|
+
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -89,10 +89,6 @@ describe('Resource tests', () => {
|
|
|
89
89
|
super({ type: 'type' });
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
calculateOperation(change: ParameterChange<TestConfig>): ResourceOperation.RECREATE | ResourceOperation.MODIFY {
|
|
93
|
-
return ResourceOperation.MODIFY;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
92
|
async refresh(): Promise<TestConfig | null> {
|
|
97
93
|
return null;
|
|
98
94
|
}
|
|
@@ -111,6 +107,24 @@ describe('Resource tests', () => {
|
|
|
111
107
|
expect(result.changeSet.parameterChanges.length).to.eq(3);
|
|
112
108
|
})
|
|
113
109
|
|
|
110
|
+
it('handles empty parameters', async () => {
|
|
111
|
+
const resource = new class extends TestResource {
|
|
112
|
+
constructor() {
|
|
113
|
+
super({ type: 'type' });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const resourceSpy = spy(resource);
|
|
122
|
+
const result = await resourceSpy.plan({ type: 'type' })
|
|
123
|
+
|
|
124
|
+
expect(result.changeSet.operation).to.eq(ResourceOperation.CREATE);
|
|
125
|
+
expect(result.changeSet.parameterChanges.length).to.eq(0);
|
|
126
|
+
})
|
|
127
|
+
|
|
114
128
|
it('chooses the create apply properly', async () => {
|
|
115
129
|
const resource = new class extends TestResource {
|
|
116
130
|
constructor() {
|
|
@@ -195,101 +209,4 @@ describe('Resource tests', () => {
|
|
|
195
209
|
|
|
196
210
|
expect(resourceSpy.applyModify.calledTwice).to.be.true;
|
|
197
211
|
})
|
|
198
|
-
|
|
199
|
-
it('supports the creation of stateful parameters', async () => {
|
|
200
|
-
const statefulParameter = new class extends StatefulParameter<TestConfig, string> {
|
|
201
|
-
constructor() {
|
|
202
|
-
super({
|
|
203
|
-
name: 'propA',
|
|
204
|
-
})
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
applyAdd(valueToAdd: string, plan: Plan<TestConfig>): Promise<void> {
|
|
208
|
-
return Promise.resolve();
|
|
209
|
-
}
|
|
210
|
-
applyModify(newValue: string, previousValue: string, allowDeletes: boolean, plan: Plan<TestConfig>): Promise<void> {
|
|
211
|
-
return Promise.resolve();
|
|
212
|
-
}
|
|
213
|
-
applyRemove(valueToRemove: string, plan: Plan<TestConfig>): Promise<void> {
|
|
214
|
-
return Promise.resolve();
|
|
215
|
-
}
|
|
216
|
-
async refresh(): Promise<string | null> {
|
|
217
|
-
return '';
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const statefulParameterSpy = spy(statefulParameter);
|
|
222
|
-
|
|
223
|
-
const resource = new class extends TestResource {
|
|
224
|
-
|
|
225
|
-
constructor() {
|
|
226
|
-
super({
|
|
227
|
-
type: 'resource',
|
|
228
|
-
statefulParameters: [statefulParameterSpy],
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const resourceSpy = spy(resource);
|
|
234
|
-
const result = await resourceSpy.apply(
|
|
235
|
-
Plan.create<TestConfig>(
|
|
236
|
-
{ type: 'resource', propA: 'a', propB: 0, propC: 'b' },
|
|
237
|
-
null,
|
|
238
|
-
{ statefulMode: false },
|
|
239
|
-
)
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
expect(statefulParameterSpy.applyAdd.calledOnce).to.be.true;
|
|
243
|
-
expect(resourceSpy.applyCreate.calledOnce).to.be.true;
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
it('supports the modification of stateful parameters', async () => {
|
|
247
|
-
const statefulParameter = new class extends StatefulParameter<TestConfig, string> {
|
|
248
|
-
constructor() {
|
|
249
|
-
super({
|
|
250
|
-
name: 'propA',
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
applyAdd(valueToAdd: string, plan: Plan<TestConfig>): Promise<void> {
|
|
255
|
-
return Promise.resolve();
|
|
256
|
-
}
|
|
257
|
-
applyModify(newValue: string, previousValue: string, allowDeletes: false, plan: Plan<TestConfig>): Promise<void> {
|
|
258
|
-
return Promise.resolve();
|
|
259
|
-
}
|
|
260
|
-
applyRemove(valueToRemove: string, plan: Plan<TestConfig>): Promise<void> {
|
|
261
|
-
return Promise.resolve();
|
|
262
|
-
}
|
|
263
|
-
async refresh(): Promise<string | null> {
|
|
264
|
-
return 'b';
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const statefulParameterSpy = spy(statefulParameter);
|
|
269
|
-
|
|
270
|
-
const resource = new class extends TestResource {
|
|
271
|
-
|
|
272
|
-
constructor() {
|
|
273
|
-
super({
|
|
274
|
-
type: 'resource',
|
|
275
|
-
statefulParameters: [statefulParameterSpy],
|
|
276
|
-
parameterConfigurations: {
|
|
277
|
-
propB: { planOperation: ResourceOperation.MODIFY },
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async refresh(): Promise<Partial<TestConfig> | null> {
|
|
283
|
-
return { propB: -1, propC: 'b' }
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const plan = await resource.plan({ type: 'resource', propA: 'a', propB: 0, propC: 'b' })
|
|
288
|
-
|
|
289
|
-
const resourceSpy = spy(resource);
|
|
290
|
-
const result = await resourceSpy.apply(plan);
|
|
291
|
-
|
|
292
|
-
expect(statefulParameterSpy.applyModify.calledOnce).to.be.true;
|
|
293
|
-
expect(resourceSpy.applyModify.calledOnce).to.be.true;
|
|
294
|
-
})
|
|
295
|
-
})
|
|
212
|
+
});
|
package/src/entities/resource.ts
CHANGED
|
@@ -18,7 +18,7 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
18
18
|
readonly typeId: string;
|
|
19
19
|
readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
|
|
20
20
|
readonly dependencies: Resource<any>[]; // TODO: Change this to a string
|
|
21
|
-
readonly parameterConfigurations: Record<
|
|
21
|
+
readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>
|
|
22
22
|
|
|
23
23
|
private readonly options: ResourceConfiguration<T>;
|
|
24
24
|
|
|
@@ -45,23 +45,26 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
45
45
|
async plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>> {
|
|
46
46
|
|
|
47
47
|
// Explanation: these are settings for how the plan will be generated
|
|
48
|
-
const planConfiguration: PlanConfiguration = {
|
|
48
|
+
const planConfiguration: PlanConfiguration<T> = {
|
|
49
49
|
statefulMode: false,
|
|
50
50
|
parameterConfigurations: this.parameterConfigurations,
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
|
|
54
54
|
|
|
55
|
-
// Refresh resource parameters
|
|
56
|
-
// This refreshes the parameters that configure the resource itself
|
|
57
|
-
|
|
58
55
|
const resourceParameters = Object.fromEntries([
|
|
59
56
|
...Object.entries(desiredParameters).filter(([key]) => !this.statefulParameters.has(key)),
|
|
60
57
|
]) as Partial<T>;
|
|
61
58
|
|
|
59
|
+
const statefulParameters = [...this.statefulParameters.values()]
|
|
60
|
+
.filter((sp) => desiredParameters[sp.name] !== undefined) // Checking for undefined is fine here because JSONs can only have null.
|
|
61
|
+
|
|
62
|
+
// Refresh resource parameters
|
|
63
|
+
// This refreshes the parameters that configure the resource itself
|
|
62
64
|
const keysToRefresh = new Set(Object.keys(resourceParameters));
|
|
63
65
|
const currentParameters = await this.refresh(keysToRefresh);
|
|
64
|
-
|
|
66
|
+
|
|
67
|
+
if (currentParameters == null && statefulParameters.length === 0) {
|
|
65
68
|
return Plan.create(desiredConfig, null, planConfiguration);
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -69,19 +72,24 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
69
72
|
|
|
70
73
|
// Refresh stateful parameters
|
|
71
74
|
// This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
|
|
72
|
-
|
|
73
|
-
const statefulParameters = [...this.statefulParameters.values()]
|
|
74
|
-
.filter((sp) => desiredParameters[sp.name] !== undefined) // Checking for undefined is fine here because JSONs can only have null.
|
|
75
|
+
const currentStatefulParameters = {} as Partial<T>;
|
|
75
76
|
|
|
76
77
|
for(const statefulParameter of statefulParameters) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
) ?? undefined;
|
|
78
|
+
const desiredValue = desiredParameters[statefulParameter.name];
|
|
79
|
+
|
|
80
|
+
let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
|
|
81
|
+
|
|
82
|
+
// In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
|
|
83
|
+
if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
|
|
84
|
+
currentValue = currentValue.filter((p) => desiredValue?.includes(p)) as any;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
currentStatefulParameters[statefulParameter.name] = currentValue;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
return Plan.create(
|
|
83
91
|
desiredConfig,
|
|
84
|
-
{ ...currentParameters, ...resourceMetadata } as Partial<T> & ResourceConfig,
|
|
92
|
+
{ ...currentParameters, ...currentStatefulParameters, ...resourceMetadata } as Partial<T> & ResourceConfig,
|
|
85
93
|
planConfiguration,
|
|
86
94
|
)
|
|
87
95
|
}
|
|
@@ -172,13 +180,13 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
172
180
|
|
|
173
181
|
private generateParameterConfigurations(
|
|
174
182
|
resourceConfiguration: ResourceConfiguration<T>
|
|
175
|
-
): Record<
|
|
176
|
-
const resourceParameters
|
|
183
|
+
): Record<keyof T, ParameterConfiguration> {
|
|
184
|
+
const resourceParameters = Object.fromEntries(
|
|
177
185
|
Object.entries(resourceConfiguration.parameterConfigurations ?? {})
|
|
178
186
|
?.map(([name, value]) => ([name, { ...value, isStatefulParameter: false }]))
|
|
179
|
-
)
|
|
187
|
+
) as Record<keyof T, ParameterConfiguration>
|
|
180
188
|
|
|
181
|
-
const statefulParameters
|
|
189
|
+
const statefulParameters = resourceConfiguration.statefulParameters
|
|
182
190
|
?.reduce((obj, sp) => {
|
|
183
191
|
return {
|
|
184
192
|
...obj,
|
|
@@ -209,7 +217,11 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
209
217
|
}
|
|
210
218
|
}
|
|
211
219
|
|
|
212
|
-
private validateRefreshResults(refresh: Partial<T
|
|
220
|
+
private validateRefreshResults(refresh: Partial<T> | null, desiredKeys: Set<keyof T>) {
|
|
221
|
+
if (!refresh) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
213
225
|
const refreshKeys = new Set(Object.keys(refresh)) as Set<keyof T>;
|
|
214
226
|
|
|
215
227
|
if (!setsEqual(desiredKeys, refreshKeys)) {
|