codify-plugin-lib 1.0.46 → 1.0.48
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.d.ts +1 -1
- package/dist/entities/plan.js +44 -6
- package/dist/entities/plugin.js +5 -1
- package/dist/entities/resource-types.d.ts +1 -0
- package/dist/entities/resource.d.ts +5 -2
- package/dist/entities/resource.js +27 -7
- package/dist/entities/stateful-parameter.d.ts +2 -2
- package/package.json +2 -2
- package/src/entities/plan.test.ts +151 -0
- package/src/entities/plan.ts +50 -9
- package/src/entities/plugin.ts +6 -1
- package/src/entities/resource-types.ts +4 -0
- package/src/entities/resource.test.ts +57 -0
- package/src/entities/resource.ts +37 -10
- package/src/entities/stateful-parameter.ts +2 -2
package/dist/entities/plan.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare class Plan<T extends StringIndexedObject> {
|
|
|
8
8
|
constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig);
|
|
9
9
|
static create<T extends StringIndexedObject>(desiredParameters: Partial<T> | null, currentParameters: Partial<T> | null, resourceMetadata: ResourceConfig, configuration: PlanConfiguration<T>): Plan<T>;
|
|
10
10
|
getResourceType(): string;
|
|
11
|
-
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T>;
|
|
11
|
+
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues: Partial<Record<keyof T, unknown>>): Plan<T>;
|
|
12
12
|
get desiredConfig(): T;
|
|
13
13
|
get currentConfig(): T;
|
|
14
14
|
toResponse(): PlanResponseData;
|
package/dist/entities/plan.js
CHANGED
|
@@ -45,18 +45,56 @@ export class Plan {
|
|
|
45
45
|
getResourceType() {
|
|
46
46
|
return this.resourceMetadata.type;
|
|
47
47
|
}
|
|
48
|
-
static fromResponse(data) {
|
|
48
|
+
static fromResponse(data, defaultValues) {
|
|
49
49
|
if (!data) {
|
|
50
50
|
throw new Error('Data is empty');
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
previousValue: null,
|
|
55
|
-
}))), {
|
|
52
|
+
addDefaultValues();
|
|
53
|
+
return new Plan(randomUUID(), new ChangeSet(data.operation, data.parameters), {
|
|
56
54
|
type: data.resourceType,
|
|
57
55
|
name: data.resourceName,
|
|
58
|
-
...(data.parameters.reduce((prev, { name, newValue }) => Object.assign(prev, { [name]: newValue }), {}))
|
|
59
56
|
});
|
|
57
|
+
function addDefaultValues() {
|
|
58
|
+
Object.entries(defaultValues)
|
|
59
|
+
.forEach(([key, defaultValue]) => {
|
|
60
|
+
const configValueExists = data
|
|
61
|
+
?.parameters
|
|
62
|
+
.find((p) => p.name === key) !== undefined;
|
|
63
|
+
if (!configValueExists) {
|
|
64
|
+
switch (data?.operation) {
|
|
65
|
+
case ResourceOperation.CREATE: {
|
|
66
|
+
data?.parameters.push({
|
|
67
|
+
name: key,
|
|
68
|
+
operation: ParameterOperation.ADD,
|
|
69
|
+
previousValue: null,
|
|
70
|
+
newValue: defaultValue,
|
|
71
|
+
});
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case ResourceOperation.DESTROY: {
|
|
75
|
+
data?.parameters.push({
|
|
76
|
+
name: key,
|
|
77
|
+
operation: ParameterOperation.REMOVE,
|
|
78
|
+
previousValue: defaultValue,
|
|
79
|
+
newValue: null,
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case ResourceOperation.MODIFY:
|
|
84
|
+
case ResourceOperation.RECREATE:
|
|
85
|
+
case ResourceOperation.NOOP: {
|
|
86
|
+
data?.parameters.push({
|
|
87
|
+
name: key,
|
|
88
|
+
operation: ParameterOperation.NOOP,
|
|
89
|
+
previousValue: defaultValue,
|
|
90
|
+
newValue: defaultValue,
|
|
91
|
+
});
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
60
98
|
}
|
|
61
99
|
get desiredConfig() {
|
|
62
100
|
return {
|
package/dist/entities/plugin.js
CHANGED
|
@@ -65,7 +65,11 @@ export class Plugin {
|
|
|
65
65
|
}
|
|
66
66
|
return this.planStorage.get(planId);
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
if (!planRequest?.resourceName || !this.resources.has(planRequest.resourceName)) {
|
|
69
|
+
throw new Error('Malformed plan. Resource name must be supplied');
|
|
70
|
+
}
|
|
71
|
+
const resource = this.resources.get(planRequest.resourceName);
|
|
72
|
+
return Plan.fromResponse(data.plan, resource?.defaultValues);
|
|
69
73
|
}
|
|
70
74
|
async crossValidateResources(configs) { }
|
|
71
75
|
}
|
|
@@ -4,6 +4,7 @@ export type ErrorMessage = string;
|
|
|
4
4
|
export interface ResourceParameterConfiguration {
|
|
5
5
|
planOperation?: ResourceOperation.MODIFY | ResourceOperation.RECREATE;
|
|
6
6
|
isEqual?: (desired: any, current: any) => boolean;
|
|
7
|
+
defaultValue?: unknown;
|
|
7
8
|
}
|
|
8
9
|
export interface ResourceConfiguration<T extends StringIndexedObject> {
|
|
9
10
|
type: string;
|
|
@@ -9,6 +9,7 @@ export declare abstract class Resource<T extends StringIndexedObject> {
|
|
|
9
9
|
readonly dependencies: string[];
|
|
10
10
|
readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>;
|
|
11
11
|
readonly configuration: ResourceConfiguration<T>;
|
|
12
|
+
readonly defaultValues: Partial<Record<keyof T, unknown>>;
|
|
12
13
|
protected constructor(configuration: ResourceConfiguration<T>);
|
|
13
14
|
onInitialize(): Promise<void>;
|
|
14
15
|
plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>>;
|
|
@@ -16,11 +17,13 @@ export declare abstract class Resource<T extends StringIndexedObject> {
|
|
|
16
17
|
private _applyCreate;
|
|
17
18
|
private _applyModify;
|
|
18
19
|
private _applyDestroy;
|
|
19
|
-
private
|
|
20
|
+
private initializeParameterConfigurations;
|
|
21
|
+
private initializeDefaultValues;
|
|
20
22
|
private validateResourceConfiguration;
|
|
21
23
|
private validateRefreshResults;
|
|
24
|
+
private addDefaultValues;
|
|
22
25
|
abstract validate(parameters: unknown): Promise<ValidationResult>;
|
|
23
|
-
abstract refresh(keys:
|
|
26
|
+
abstract refresh(keys: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
|
|
24
27
|
abstract applyCreate(plan: Plan<T>): Promise<void>;
|
|
25
28
|
applyModify(parameterName: keyof T, newValue: unknown, previousValue: unknown, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
|
|
26
29
|
abstract applyDestroy(plan: Plan<T>): Promise<void>;
|
|
@@ -7,11 +7,13 @@ export class Resource {
|
|
|
7
7
|
dependencies;
|
|
8
8
|
parameterConfigurations;
|
|
9
9
|
configuration;
|
|
10
|
+
defaultValues;
|
|
10
11
|
constructor(configuration) {
|
|
11
12
|
this.validateResourceConfiguration(configuration);
|
|
12
13
|
this.typeId = configuration.type;
|
|
13
14
|
this.statefulParameters = new Map(configuration.statefulParameters?.map((sp) => [sp.name, sp]));
|
|
14
|
-
this.parameterConfigurations = this.
|
|
15
|
+
this.parameterConfigurations = this.initializeParameterConfigurations(configuration);
|
|
16
|
+
this.defaultValues = this.initializeDefaultValues(configuration);
|
|
15
17
|
this.dependencies = configuration.dependencies ?? [];
|
|
16
18
|
this.configuration = configuration;
|
|
17
19
|
}
|
|
@@ -22,20 +24,21 @@ export class Resource {
|
|
|
22
24
|
parameterConfigurations: this.parameterConfigurations,
|
|
23
25
|
};
|
|
24
26
|
const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
|
|
27
|
+
this.addDefaultValues(desiredParameters);
|
|
25
28
|
const resourceParameters = Object.fromEntries([
|
|
26
29
|
...Object.entries(desiredParameters).filter(([key]) => !this.statefulParameters.has(key)),
|
|
27
30
|
]);
|
|
28
31
|
const statefulParameters = [...this.statefulParameters.values()]
|
|
29
32
|
.filter((sp) => desiredParameters[sp.name] !== undefined);
|
|
30
|
-
const
|
|
31
|
-
const currentParameters = await this.refresh(
|
|
33
|
+
const entriesToRefresh = new Map(Object.entries(resourceParameters));
|
|
34
|
+
const currentParameters = await this.refresh(entriesToRefresh);
|
|
32
35
|
if (currentParameters == null) {
|
|
33
36
|
return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
|
|
34
37
|
}
|
|
35
|
-
this.validateRefreshResults(currentParameters,
|
|
38
|
+
this.validateRefreshResults(currentParameters, entriesToRefresh);
|
|
36
39
|
for (const statefulParameter of statefulParameters) {
|
|
37
40
|
const desiredValue = desiredParameters[statefulParameter.name];
|
|
38
|
-
let currentValue = await statefulParameter.refresh() ?? undefined;
|
|
41
|
+
let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
|
|
39
42
|
if (Array.isArray(currentValue)
|
|
40
43
|
&& Array.isArray(desiredValue)
|
|
41
44
|
&& !planConfiguration.statefulMode
|
|
@@ -122,7 +125,7 @@ export class Resource {
|
|
|
122
125
|
}
|
|
123
126
|
await this.applyDestroy(plan);
|
|
124
127
|
}
|
|
125
|
-
|
|
128
|
+
initializeParameterConfigurations(resourceConfiguration) {
|
|
126
129
|
const resourceParameters = Object.fromEntries(Object.entries(resourceConfiguration.parameterConfigurations ?? {})
|
|
127
130
|
?.map(([name, value]) => ([name, { ...value, isStatefulParameter: false }])));
|
|
128
131
|
const statefulParameters = resourceConfiguration.statefulParameters
|
|
@@ -140,6 +143,14 @@ export class Resource {
|
|
|
140
143
|
...statefulParameters,
|
|
141
144
|
};
|
|
142
145
|
}
|
|
146
|
+
initializeDefaultValues(resourceConfiguration) {
|
|
147
|
+
if (!resourceConfiguration.parameterConfigurations) {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
return Object.fromEntries(Object.entries(resourceConfiguration.parameterConfigurations)
|
|
151
|
+
.filter((p) => p[1]?.defaultValue !== undefined)
|
|
152
|
+
.map((config) => [config[0], config[1].defaultValue]));
|
|
153
|
+
}
|
|
143
154
|
validateResourceConfiguration(data) {
|
|
144
155
|
if (data.parameterConfigurations && data.statefulParameters) {
|
|
145
156
|
const parameters = [...Object.keys(data.parameterConfigurations)];
|
|
@@ -150,10 +161,11 @@ export class Resource {
|
|
|
150
161
|
}
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
|
-
validateRefreshResults(refresh,
|
|
164
|
+
validateRefreshResults(refresh, desiredMap) {
|
|
154
165
|
if (!refresh) {
|
|
155
166
|
return;
|
|
156
167
|
}
|
|
168
|
+
const desiredKeys = new Set(desiredMap.keys());
|
|
157
169
|
const refreshKeys = new Set(Object.keys(refresh));
|
|
158
170
|
if (!setsEqual(desiredKeys, refreshKeys)) {
|
|
159
171
|
throw new Error(`Resource ${this.configuration.type}
|
|
@@ -162,6 +174,14 @@ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
|
|
|
162
174
|
Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
|
|
163
175
|
}
|
|
164
176
|
}
|
|
177
|
+
addDefaultValues(desired) {
|
|
178
|
+
Object.entries(this.defaultValues)
|
|
179
|
+
.forEach(([key, defaultValue]) => {
|
|
180
|
+
if (defaultValue !== undefined && desired[key] === undefined) {
|
|
181
|
+
desired[key] = defaultValue;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
165
185
|
async applyModify(parameterName, newValue, previousValue, allowDeletes, plan) { }
|
|
166
186
|
;
|
|
167
187
|
}
|
|
@@ -13,7 +13,7 @@ export declare abstract class StatefulParameter<T extends StringIndexedObject, V
|
|
|
13
13
|
readonly name: keyof T;
|
|
14
14
|
readonly configuration: StatefulParameterConfiguration<T>;
|
|
15
15
|
protected constructor(configuration: StatefulParameterConfiguration<T>);
|
|
16
|
-
abstract refresh(): Promise<V | null>;
|
|
16
|
+
abstract refresh(desired: V | null): Promise<V | null>;
|
|
17
17
|
abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
|
|
18
18
|
abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
|
|
19
19
|
abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
|
|
@@ -24,7 +24,7 @@ export declare abstract class ArrayStatefulParameter<T extends StringIndexedObje
|
|
|
24
24
|
applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void>;
|
|
25
25
|
applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void>;
|
|
26
26
|
applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void>;
|
|
27
|
-
abstract refresh(): Promise<V[] | null>;
|
|
27
|
+
abstract refresh(desired: V[] | null): Promise<V[] | null>;
|
|
28
28
|
abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
|
|
29
29
|
abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
|
|
30
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codify-plugin-lib",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.48",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"ajv": "^8.12.0",
|
|
16
16
|
"ajv-formats": "^2.1.1",
|
|
17
|
-
"codify-schemas": "1.0.
|
|
17
|
+
"codify-schemas": "1.0.33",
|
|
18
18
|
"@npmcli/promise-spawn": "^7.0.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Plan } from './plan.js';
|
|
3
|
+
import { TestResource } from './resource.test.js';
|
|
4
|
+
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
5
|
+
import { Resource } from './resource.js';
|
|
6
|
+
|
|
7
|
+
describe('Plan entity tests', () => {
|
|
8
|
+
it('Adds default values properly when plan is parsed from request (Create)', () => {
|
|
9
|
+
const resource = createResource();
|
|
10
|
+
|
|
11
|
+
const plan = Plan.fromResponse({
|
|
12
|
+
operation: ResourceOperation.CREATE,
|
|
13
|
+
resourceType: 'type',
|
|
14
|
+
parameters: [{
|
|
15
|
+
name: 'propB',
|
|
16
|
+
operation: ParameterOperation.ADD,
|
|
17
|
+
previousValue: null,
|
|
18
|
+
newValue: 'propBValue'
|
|
19
|
+
}]
|
|
20
|
+
}, resource.defaultValues);
|
|
21
|
+
|
|
22
|
+
expect(plan.currentConfig).toMatchObject({
|
|
23
|
+
type: 'type',
|
|
24
|
+
propA: null,
|
|
25
|
+
propB: null,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(plan.desiredConfig).toMatchObject({
|
|
29
|
+
type: 'type',
|
|
30
|
+
propA: 'defaultA',
|
|
31
|
+
propB: 'propBValue',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect(plan.changeSet.parameterChanges
|
|
35
|
+
.every((pc) => pc.operation === ParameterOperation.ADD)
|
|
36
|
+
).to.be.true;
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('Adds default values properly when plan is parsed from request (Destroy)', () => {
|
|
40
|
+
const resource = createResource();
|
|
41
|
+
|
|
42
|
+
const plan = Plan.fromResponse({
|
|
43
|
+
operation: ResourceOperation.DESTROY,
|
|
44
|
+
resourceType: 'type',
|
|
45
|
+
parameters: [{
|
|
46
|
+
name: 'propB',
|
|
47
|
+
operation: ParameterOperation.REMOVE,
|
|
48
|
+
previousValue: 'propBValue',
|
|
49
|
+
newValue: null,
|
|
50
|
+
}]
|
|
51
|
+
}, resource.defaultValues);
|
|
52
|
+
|
|
53
|
+
expect(plan.currentConfig).toMatchObject({
|
|
54
|
+
type: 'type',
|
|
55
|
+
propA: 'defaultA',
|
|
56
|
+
propB: 'propBValue',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
expect(plan.desiredConfig).toMatchObject({
|
|
60
|
+
type: 'type',
|
|
61
|
+
propA: null,
|
|
62
|
+
propB: null,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(plan.changeSet.parameterChanges
|
|
66
|
+
.every((pc) => pc.operation === ParameterOperation.REMOVE)
|
|
67
|
+
).to.be.true;
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('Adds default values properly when plan is parsed from request (No-op)', () => {
|
|
71
|
+
const resource = createResource();
|
|
72
|
+
|
|
73
|
+
const plan = Plan.fromResponse({
|
|
74
|
+
operation: ResourceOperation.NOOP,
|
|
75
|
+
resourceType: 'type',
|
|
76
|
+
parameters: [{
|
|
77
|
+
name: 'propB',
|
|
78
|
+
operation: ParameterOperation.NOOP,
|
|
79
|
+
previousValue: 'propBValue',
|
|
80
|
+
newValue: 'propBValue',
|
|
81
|
+
}]
|
|
82
|
+
}, resource.defaultValues);
|
|
83
|
+
|
|
84
|
+
expect(plan.currentConfig).toMatchObject({
|
|
85
|
+
type: 'type',
|
|
86
|
+
propA: 'defaultA',
|
|
87
|
+
propB: 'propBValue',
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
expect(plan.desiredConfig).toMatchObject({
|
|
91
|
+
type: 'type',
|
|
92
|
+
propA: 'defaultA',
|
|
93
|
+
propB: 'propBValue',
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
expect(plan.changeSet.parameterChanges
|
|
97
|
+
.every((pc) => pc.operation === ParameterOperation.NOOP)
|
|
98
|
+
).to.be.true;
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('Does not add default value if a value has already been specified', () => {
|
|
102
|
+
const resource = createResource();
|
|
103
|
+
|
|
104
|
+
const plan = Plan.fromResponse({
|
|
105
|
+
operation: ResourceOperation.CREATE,
|
|
106
|
+
resourceType: 'type',
|
|
107
|
+
parameters: [{
|
|
108
|
+
name: 'propB',
|
|
109
|
+
operation: ParameterOperation.ADD,
|
|
110
|
+
previousValue: null,
|
|
111
|
+
newValue: 'propBValue',
|
|
112
|
+
}, {
|
|
113
|
+
name: 'propA',
|
|
114
|
+
operation: ParameterOperation.ADD,
|
|
115
|
+
previousValue: null,
|
|
116
|
+
newValue: 'propAValue',
|
|
117
|
+
}]
|
|
118
|
+
}, resource.defaultValues);
|
|
119
|
+
|
|
120
|
+
expect(plan.currentConfig).toMatchObject({
|
|
121
|
+
type: 'type',
|
|
122
|
+
propA: null,
|
|
123
|
+
propB: null,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(plan.desiredConfig).toMatchObject({
|
|
127
|
+
type: 'type',
|
|
128
|
+
propA: 'propAValue',
|
|
129
|
+
propB: 'propBValue',
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
expect(plan.changeSet.parameterChanges
|
|
133
|
+
.every((pc) => pc.operation === ParameterOperation.ADD)
|
|
134
|
+
).to.be.true;
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
function createResource(): Resource<any> {
|
|
139
|
+
return new class extends TestResource {
|
|
140
|
+
constructor() {
|
|
141
|
+
super({
|
|
142
|
+
type: 'type',
|
|
143
|
+
parameterConfigurations: {
|
|
144
|
+
propA: {
|
|
145
|
+
defaultValue: 'defaultA'
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/entities/plan.ts
CHANGED
|
@@ -75,29 +75,70 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
75
75
|
return this.resourceMetadata.type
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan']): Plan<T> {
|
|
78
|
+
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues: Partial<Record<keyof T, unknown>>): Plan<T> {
|
|
79
79
|
if (!data) {
|
|
80
80
|
throw new Error('Data is empty');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
addDefaultValues();
|
|
84
|
+
|
|
83
85
|
return new Plan(
|
|
84
86
|
randomUUID(),
|
|
85
87
|
new ChangeSet<T>(
|
|
86
88
|
data.operation,
|
|
87
|
-
data.parameters
|
|
88
|
-
...value,
|
|
89
|
-
previousValue: null,
|
|
90
|
-
})),
|
|
89
|
+
data.parameters
|
|
91
90
|
),
|
|
92
91
|
{
|
|
93
92
|
type: data.resourceType,
|
|
94
93
|
name: data.resourceName,
|
|
95
|
-
...(data.parameters.reduce(
|
|
96
|
-
(prev, { name, newValue }) => Object.assign(prev, { [name]: newValue }),
|
|
97
|
-
{}
|
|
98
|
-
))
|
|
99
94
|
},
|
|
100
95
|
);
|
|
96
|
+
|
|
97
|
+
function addDefaultValues(): void {
|
|
98
|
+
Object.entries(defaultValues)
|
|
99
|
+
.forEach(([key, defaultValue]) => {
|
|
100
|
+
const configValueExists = data
|
|
101
|
+
?.parameters
|
|
102
|
+
.find((p) => p.name === key) !== undefined;
|
|
103
|
+
|
|
104
|
+
if (!configValueExists) {
|
|
105
|
+
switch (data?.operation) {
|
|
106
|
+
case ResourceOperation.CREATE: {
|
|
107
|
+
data?.parameters.push({
|
|
108
|
+
name: key,
|
|
109
|
+
operation: ParameterOperation.ADD,
|
|
110
|
+
previousValue: null,
|
|
111
|
+
newValue: defaultValue,
|
|
112
|
+
});
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case ResourceOperation.DESTROY: {
|
|
117
|
+
data?.parameters.push({
|
|
118
|
+
name: key,
|
|
119
|
+
operation: ParameterOperation.REMOVE,
|
|
120
|
+
previousValue: defaultValue,
|
|
121
|
+
newValue: null,
|
|
122
|
+
});
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case ResourceOperation.MODIFY:
|
|
127
|
+
case ResourceOperation.RECREATE:
|
|
128
|
+
case ResourceOperation.NOOP: {
|
|
129
|
+
data?.parameters.push({
|
|
130
|
+
name: key,
|
|
131
|
+
operation: ParameterOperation.NOOP,
|
|
132
|
+
previousValue: defaultValue,
|
|
133
|
+
newValue: defaultValue,
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
101
142
|
}
|
|
102
143
|
|
|
103
144
|
get desiredConfig(): T {
|
package/src/entities/plugin.ts
CHANGED
|
@@ -94,7 +94,12 @@ export class Plugin {
|
|
|
94
94
|
return this.planStorage.get(planId)!
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
if (!planRequest?.resourceName || !this.resources.has(planRequest.resourceName)) {
|
|
98
|
+
throw new Error('Malformed plan. Resource name must be supplied');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const resource = this.resources.get(planRequest.resourceName);
|
|
102
|
+
return Plan.fromResponse(data.plan, resource?.defaultValues!);
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
protected async crossValidateResources(configs: ResourceConfig[]): Promise<void> {}
|
|
@@ -17,6 +17,10 @@ export interface ResourceParameterConfiguration {
|
|
|
17
17
|
* @param current
|
|
18
18
|
*/
|
|
19
19
|
isEqual?: (desired: any, current: any) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Default value for the parameter. If a value is not provided in the config, the library will use this value.
|
|
22
|
+
*/
|
|
23
|
+
defaultValue?: unknown,
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/**
|
|
@@ -293,4 +293,61 @@ describe('Resource tests', () => {
|
|
|
293
293
|
}).to.not.throw;
|
|
294
294
|
})
|
|
295
295
|
|
|
296
|
+
it('Allows default values to be added', async () => {
|
|
297
|
+
const resource = new class extends TestResource {
|
|
298
|
+
constructor() {
|
|
299
|
+
super({
|
|
300
|
+
type: 'type',
|
|
301
|
+
parameterConfigurations: {
|
|
302
|
+
propA: { defaultValue: 'propADefault' }
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// @ts-ignore
|
|
308
|
+
async refresh(desired: Map<string, unknown>): Promise<Partial<TestConfig>> {
|
|
309
|
+
expect(desired.has('propA')).to.be.true;
|
|
310
|
+
expect(desired.get('propA')).to.be.eq('propADefault');
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
propA: 'propAAfter'
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const plan = await resource.plan({ type: 'resource'})
|
|
319
|
+
expect(plan.currentConfig.propA).to.eq('propAAfter');
|
|
320
|
+
expect(plan.desiredConfig.propA).to.eq('propADefault');
|
|
321
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.RECREATE);
|
|
322
|
+
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('Allows default values to be added (ignore default value if already present)', async () => {
|
|
326
|
+
const resource = new class extends TestResource {
|
|
327
|
+
constructor() {
|
|
328
|
+
super({
|
|
329
|
+
type: 'type',
|
|
330
|
+
parameterConfigurations: {
|
|
331
|
+
propA: { defaultValue: 'propADefault' }
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// @ts-ignore
|
|
337
|
+
async refresh(desired: Map<string, unknown>): Promise<Partial<TestConfig>> {
|
|
338
|
+
expect(desired.has('propA')).to.be.true;
|
|
339
|
+
expect(desired.get('propA')).to.be.eq('propA');
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
propA: 'propAAfter'
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const plan = await resource.plan({ type: 'resource', propA: 'propA'})
|
|
348
|
+
expect(plan.currentConfig.propA).to.eq('propAAfter');
|
|
349
|
+
expect(plan.desiredConfig.propA).to.eq('propA');
|
|
350
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.RECREATE);
|
|
351
|
+
|
|
352
|
+
})
|
|
296
353
|
});
|
package/src/entities/resource.ts
CHANGED
|
@@ -20,13 +20,15 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
20
20
|
readonly dependencies: string[]; // TODO: Change this to a string
|
|
21
21
|
readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>
|
|
22
22
|
readonly configuration: ResourceConfiguration<T>;
|
|
23
|
+
readonly defaultValues: Partial<Record<keyof T, unknown>>;
|
|
23
24
|
|
|
24
25
|
protected constructor(configuration: ResourceConfiguration<T>) {
|
|
25
26
|
this.validateResourceConfiguration(configuration);
|
|
26
27
|
|
|
27
28
|
this.typeId = configuration.type;
|
|
28
29
|
this.statefulParameters = new Map(configuration.statefulParameters?.map((sp) => [sp.name, sp]));
|
|
29
|
-
this.parameterConfigurations = this.
|
|
30
|
+
this.parameterConfigurations = this.initializeParameterConfigurations(configuration);
|
|
31
|
+
this.defaultValues = this.initializeDefaultValues(configuration);
|
|
30
32
|
|
|
31
33
|
this.dependencies = configuration.dependencies ?? [];
|
|
32
34
|
this.configuration = configuration;
|
|
@@ -47,6 +49,8 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
47
49
|
|
|
48
50
|
const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
|
|
49
51
|
|
|
52
|
+
this.addDefaultValues(desiredParameters);
|
|
53
|
+
|
|
50
54
|
const resourceParameters = Object.fromEntries([
|
|
51
55
|
...Object.entries(desiredParameters).filter(([key]) => !this.statefulParameters.has(key)),
|
|
52
56
|
]) as Partial<T>;
|
|
@@ -56,22 +60,22 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
56
60
|
|
|
57
61
|
// Refresh resource parameters
|
|
58
62
|
// This refreshes the parameters that configure the resource itself
|
|
59
|
-
const
|
|
60
|
-
const currentParameters = await this.refresh(
|
|
63
|
+
const entriesToRefresh = new Map(Object.entries(resourceParameters));
|
|
64
|
+
const currentParameters = await this.refresh(entriesToRefresh);
|
|
61
65
|
|
|
62
66
|
// Short circuit here. If resource is non-existent, then there's no point checking stateful parameters
|
|
63
67
|
if (currentParameters == null) {
|
|
64
68
|
return Plan.create(desiredParameters, null, resourceMetadata, planConfiguration);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
this.validateRefreshResults(currentParameters,
|
|
71
|
+
this.validateRefreshResults(currentParameters, entriesToRefresh);
|
|
68
72
|
|
|
69
73
|
// Refresh stateful parameters
|
|
70
74
|
// This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
|
|
71
75
|
for(const statefulParameter of statefulParameters) {
|
|
72
76
|
const desiredValue = desiredParameters[statefulParameter.name];
|
|
73
77
|
|
|
74
|
-
let currentValue = await statefulParameter.refresh() ?? undefined;
|
|
78
|
+
let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
|
|
75
79
|
|
|
76
80
|
// In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
|
|
77
81
|
if (Array.isArray(currentValue)
|
|
@@ -183,7 +187,7 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
183
187
|
await this.applyDestroy(plan);
|
|
184
188
|
}
|
|
185
189
|
|
|
186
|
-
private
|
|
190
|
+
private initializeParameterConfigurations(
|
|
187
191
|
resourceConfiguration: ResourceConfiguration<T>
|
|
188
192
|
): Record<keyof T, ParameterConfiguration> {
|
|
189
193
|
const resourceParameters = Object.fromEntries(
|
|
@@ -209,6 +213,20 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
209
213
|
|
|
210
214
|
}
|
|
211
215
|
|
|
216
|
+
private initializeDefaultValues(
|
|
217
|
+
resourceConfiguration: ResourceConfiguration<T>
|
|
218
|
+
): Partial<Record<keyof T, unknown>> {
|
|
219
|
+
if (!resourceConfiguration.parameterConfigurations) {
|
|
220
|
+
return {};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return Object.fromEntries(
|
|
224
|
+
Object.entries(resourceConfiguration.parameterConfigurations)
|
|
225
|
+
.filter((p) => p[1]?.defaultValue !== undefined)
|
|
226
|
+
.map((config) => [config[0], config[1]!.defaultValue])
|
|
227
|
+
) as Partial<Record<keyof T, unknown>>;
|
|
228
|
+
}
|
|
229
|
+
|
|
212
230
|
private validateResourceConfiguration(data: ResourceConfiguration<T>) {
|
|
213
231
|
// Stateful parameters are configured within the object not in the resource.
|
|
214
232
|
if (data.parameterConfigurations && data.statefulParameters) {
|
|
@@ -220,15 +238,14 @@ export abstract class Resource<T extends StringIndexedObject> {
|
|
|
220
238
|
throw new Error(`Resource ${this.typeId} cannot declare a parameter as both stateful and non-stateful`);
|
|
221
239
|
}
|
|
222
240
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
241
|
}
|
|
226
242
|
|
|
227
|
-
private validateRefreshResults(refresh: Partial<T> | null,
|
|
243
|
+
private validateRefreshResults(refresh: Partial<T> | null, desiredMap: Map<keyof T, T[keyof T]>) {
|
|
228
244
|
if (!refresh) {
|
|
229
245
|
return;
|
|
230
246
|
}
|
|
231
247
|
|
|
248
|
+
const desiredKeys = new Set<keyof T>(desiredMap.keys());
|
|
232
249
|
const refreshKeys = new Set(Object.keys(refresh)) as Set<keyof T>;
|
|
233
250
|
|
|
234
251
|
if (!setsEqual(desiredKeys, refreshKeys)) {
|
|
@@ -241,9 +258,19 @@ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
|
|
|
241
258
|
}
|
|
242
259
|
}
|
|
243
260
|
|
|
261
|
+
private addDefaultValues(desired: Partial<T>): void {
|
|
262
|
+
Object.entries(this.defaultValues)
|
|
263
|
+
.forEach(([key, defaultValue]) => {
|
|
264
|
+
if (defaultValue !== undefined && desired[key as any] === undefined) {
|
|
265
|
+
// @ts-ignore
|
|
266
|
+
desired[key] = defaultValue;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
244
271
|
abstract validate(parameters: unknown): Promise<ValidationResult>;
|
|
245
272
|
|
|
246
|
-
abstract refresh(keys:
|
|
273
|
+
abstract refresh(keys: Map<keyof T, T[keyof T]>): Promise<Partial<T> | null>;
|
|
247
274
|
|
|
248
275
|
abstract applyCreate(plan: Plan<T>): Promise<void>;
|
|
249
276
|
|
|
@@ -32,7 +32,7 @@ export abstract class StatefulParameter<T extends StringIndexedObject, V extends
|
|
|
32
32
|
this.configuration = configuration
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
abstract refresh(): Promise<V | null>;
|
|
35
|
+
abstract refresh(desired: V | null): Promise<V | null>;
|
|
36
36
|
|
|
37
37
|
// TODO: Add an additional parameter here for what has actually changed.
|
|
38
38
|
abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
|
|
@@ -88,7 +88,7 @@ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> e
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
abstract refresh(): Promise<V[] | null>;
|
|
91
|
+
abstract refresh(desired: V[] | null): Promise<V[] | null>;
|
|
92
92
|
abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
|
|
93
93
|
abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
|
|
94
94
|
}
|