codify-plugin-lib 1.0.76 → 1.0.77
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/.eslintrc.json +11 -4
- package/.github/workflows/release.yaml +19 -0
- package/.github/workflows/unit-test-ci.yaml +19 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +7 -0
- package/dist/index.d.ts +10 -10
- package/dist/index.js +9 -9
- package/dist/messages/handlers.d.ts +1 -1
- package/dist/messages/handlers.js +2 -1
- package/dist/plan/change-set.d.ts +37 -0
- package/dist/plan/change-set.js +146 -0
- package/dist/plan/plan-types.d.ts +23 -0
- package/dist/plan/plan-types.js +1 -0
- package/dist/plan/plan.d.ts +59 -0
- package/dist/plan/plan.js +228 -0
- package/dist/plugin/plugin.d.ts +17 -0
- package/dist/plugin/plugin.js +83 -0
- package/dist/resource/config-parser.d.ts +14 -0
- package/dist/resource/config-parser.js +48 -0
- package/dist/resource/parsed-resource-settings.d.ts +26 -0
- package/dist/resource/parsed-resource-settings.js +126 -0
- package/dist/resource/resource-controller.d.ts +30 -0
- package/dist/resource/resource-controller.js +247 -0
- package/dist/resource/resource-settings.d.ts +149 -0
- package/dist/resource/resource-settings.js +9 -0
- package/dist/resource/resource.d.ts +137 -0
- package/dist/resource/resource.js +44 -0
- package/dist/resource/stateful-parameter.d.ts +164 -0
- package/dist/resource/stateful-parameter.js +94 -0
- package/dist/utils/utils.d.ts +19 -3
- package/dist/utils/utils.js +52 -3
- package/package.json +5 -3
- package/src/index.ts +10 -11
- package/src/messages/handlers.test.ts +10 -37
- package/src/messages/handlers.ts +2 -2
- package/src/plan/change-set.test.ts +220 -0
- package/src/plan/change-set.ts +225 -0
- package/src/plan/plan-types.ts +27 -0
- package/src/{entities → plan}/plan.test.ts +35 -29
- package/src/plan/plan.ts +353 -0
- package/src/{entities → plugin}/plugin.test.ts +14 -13
- package/src/{entities → plugin}/plugin.ts +28 -24
- package/src/resource/config-parser.ts +77 -0
- package/src/{entities/resource-options.test.ts → resource/parsed-resource-settings.test.ts} +8 -7
- package/src/resource/parsed-resource-settings.ts +179 -0
- package/src/{entities/resource-stateful-mode.test.ts → resource/resource-controller-stateful-mode.test.ts} +36 -39
- package/src/{entities/resource.test.ts → resource/resource-controller.test.ts} +116 -176
- package/src/resource/resource-controller.ts +340 -0
- package/src/resource/resource-settings.test.ts +494 -0
- package/src/resource/resource-settings.ts +192 -0
- package/src/resource/resource.ts +149 -0
- package/src/resource/stateful-parameter.test.ts +93 -0
- package/src/resource/stateful-parameter.ts +217 -0
- package/src/utils/test-utils.test.ts +87 -0
- package/src/utils/utils.test.ts +2 -2
- package/src/utils/utils.ts +51 -5
- package/tsconfig.json +0 -1
- package/vitest.config.ts +10 -0
- package/src/entities/change-set.test.ts +0 -155
- package/src/entities/change-set.ts +0 -244
- package/src/entities/plan-types.ts +0 -44
- package/src/entities/plan.ts +0 -178
- package/src/entities/resource-options.ts +0 -155
- package/src/entities/resource-parameters.test.ts +0 -604
- package/src/entities/resource-types.ts +0 -31
- package/src/entities/resource.ts +0 -470
- package/src/entities/stateful-parameter.test.ts +0 -114
- package/src/entities/stateful-parameter.ts +0 -92
- package/src/entities/transform-parameter.ts +0 -13
- /package/src/{entities/errors.ts → errors.ts} +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
|
|
3
|
+
import { Plan } from './plan.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A narrower type for plans for CREATE operations. Only desiredConfig is not null.
|
|
7
|
+
*/
|
|
8
|
+
export interface CreatePlan<T extends StringIndexedObject> extends Plan<T> {
|
|
9
|
+
desiredConfig: T;
|
|
10
|
+
currentConfig: null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A narrower type for plans for DESTROY operations. Only currentConfig is not null.
|
|
15
|
+
*/
|
|
16
|
+
export interface DestroyPlan<T extends StringIndexedObject> extends Plan<T> {
|
|
17
|
+
desiredConfig: null;
|
|
18
|
+
currentConfig: T;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A narrower type for plans for MODIFY and RE-CREATE operations.
|
|
23
|
+
*/
|
|
24
|
+
export interface ModifyPlan<T extends StringIndexedObject> extends Plan<T> {
|
|
25
|
+
desiredConfig: T;
|
|
26
|
+
currentConfig: T;
|
|
27
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { Plan } from './plan.js';
|
|
3
|
-
import { TestResource } from './resource.test.js';
|
|
4
3
|
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
5
|
-
import {
|
|
4
|
+
import { TestConfig, TestResource } from '../utils/test-utils.test.js';
|
|
5
|
+
import { ResourceController } from '../resource/resource-controller.js';
|
|
6
|
+
import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
|
|
7
|
+
import { ResourceSettings } from '../resource/resource-settings.js';
|
|
6
8
|
|
|
7
9
|
describe('Plan entity tests', () => {
|
|
8
10
|
it('Adds default values properly when plan is parsed from request (Create)', () => {
|
|
9
|
-
const resource =
|
|
11
|
+
const resource = createTestResource()
|
|
12
|
+
const controller = new ResourceController(resource);
|
|
10
13
|
|
|
11
14
|
const plan = Plan.fromResponse({
|
|
12
15
|
operation: ResourceOperation.CREATE,
|
|
@@ -17,7 +20,7 @@ describe('Plan entity tests', () => {
|
|
|
17
20
|
previousValue: null,
|
|
18
21
|
newValue: 'propBValue'
|
|
19
22
|
}]
|
|
20
|
-
},
|
|
23
|
+
}, controller.parsedSettings.defaultValues);
|
|
21
24
|
|
|
22
25
|
expect(plan.currentConfig).to.be.null;
|
|
23
26
|
|
|
@@ -33,7 +36,8 @@ describe('Plan entity tests', () => {
|
|
|
33
36
|
})
|
|
34
37
|
|
|
35
38
|
it('Adds default values properly when plan is parsed from request (Destroy)', () => {
|
|
36
|
-
const resource =
|
|
39
|
+
const resource = createTestResource()
|
|
40
|
+
const controller = new ResourceController(resource);
|
|
37
41
|
|
|
38
42
|
const plan = Plan.fromResponse({
|
|
39
43
|
operation: ResourceOperation.DESTROY,
|
|
@@ -44,7 +48,7 @@ describe('Plan entity tests', () => {
|
|
|
44
48
|
previousValue: 'propBValue',
|
|
45
49
|
newValue: null,
|
|
46
50
|
}]
|
|
47
|
-
},
|
|
51
|
+
}, controller.parsedSettings.defaultValues);
|
|
48
52
|
|
|
49
53
|
expect(plan.currentConfig).toMatchObject({
|
|
50
54
|
type: 'type',
|
|
@@ -60,7 +64,8 @@ describe('Plan entity tests', () => {
|
|
|
60
64
|
})
|
|
61
65
|
|
|
62
66
|
it('Adds default values properly when plan is parsed from request (No-op)', () => {
|
|
63
|
-
const resource =
|
|
67
|
+
const resource = createTestResource()
|
|
68
|
+
const controller = new ResourceController(resource);
|
|
64
69
|
|
|
65
70
|
const plan = Plan.fromResponse({
|
|
66
71
|
operation: ResourceOperation.NOOP,
|
|
@@ -71,7 +76,7 @@ describe('Plan entity tests', () => {
|
|
|
71
76
|
previousValue: 'propBValue',
|
|
72
77
|
newValue: 'propBValue',
|
|
73
78
|
}]
|
|
74
|
-
},
|
|
79
|
+
}, controller.parsedSettings.defaultValues);
|
|
75
80
|
|
|
76
81
|
expect(plan.currentConfig).toMatchObject({
|
|
77
82
|
type: 'type',
|
|
@@ -91,7 +96,8 @@ describe('Plan entity tests', () => {
|
|
|
91
96
|
})
|
|
92
97
|
|
|
93
98
|
it('Does not add default value if a value has already been specified', () => {
|
|
94
|
-
const resource =
|
|
99
|
+
const resource = createTestResource()
|
|
100
|
+
const controller = new ResourceController(resource);
|
|
95
101
|
|
|
96
102
|
const plan = Plan.fromResponse({
|
|
97
103
|
operation: ResourceOperation.CREATE,
|
|
@@ -107,7 +113,7 @@ describe('Plan entity tests', () => {
|
|
|
107
113
|
previousValue: null,
|
|
108
114
|
newValue: 'propAValue',
|
|
109
115
|
}]
|
|
110
|
-
},
|
|
116
|
+
}, controller.parsedSettings.defaultValues);
|
|
111
117
|
|
|
112
118
|
expect(plan.currentConfig).to.be.null
|
|
113
119
|
|
|
@@ -123,19 +129,17 @@ describe('Plan entity tests', () => {
|
|
|
123
129
|
})
|
|
124
130
|
|
|
125
131
|
it('Returns the original resource names', () => {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
propA: 'propA2',
|
|
134
|
-
},
|
|
135
|
-
{
|
|
132
|
+
const plan = Plan.calculate<TestConfig>({
|
|
133
|
+
desiredParameters: { propA: 'propA' },
|
|
134
|
+
currentParametersArray: [{ propA: 'propA2' }],
|
|
135
|
+
stateParameters: null,
|
|
136
|
+
coreParameters: {
|
|
136
137
|
type: 'type',
|
|
137
138
|
name: 'name1'
|
|
138
|
-
},
|
|
139
|
+
},
|
|
140
|
+
settings: new ParsedResourceSettings<TestConfig>({ id: 'type' }),
|
|
141
|
+
statefulMode: false,
|
|
142
|
+
});
|
|
139
143
|
|
|
140
144
|
expect(plan.toResponse()).toMatchObject({
|
|
141
145
|
resourceType: 'type',
|
|
@@ -145,15 +149,17 @@ describe('Plan entity tests', () => {
|
|
|
145
149
|
})
|
|
146
150
|
})
|
|
147
151
|
|
|
148
|
-
function
|
|
152
|
+
function createTestResource() {
|
|
149
153
|
return new class extends TestResource {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
propA: {
|
|
154
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
155
|
+
return {
|
|
156
|
+
id: 'type',
|
|
157
|
+
parameterSettings: {
|
|
158
|
+
propA: {
|
|
159
|
+
default: 'defaultA'
|
|
160
|
+
}
|
|
155
161
|
}
|
|
156
|
-
}
|
|
162
|
+
}
|
|
157
163
|
}
|
|
158
|
-
}
|
|
164
|
+
};
|
|
159
165
|
}
|
package/src/plan/plan.ts
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApplyRequestData,
|
|
3
|
+
ParameterOperation,
|
|
4
|
+
PlanResponseData,
|
|
5
|
+
ResourceConfig,
|
|
6
|
+
ResourceOperation,
|
|
7
|
+
StringIndexedObject,
|
|
8
|
+
} from 'codify-schemas';
|
|
9
|
+
import { v4 as uuidV4 } from 'uuid';
|
|
10
|
+
|
|
11
|
+
import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
|
|
12
|
+
import { ArrayParameterSetting, ResourceSettings, StatefulParameterSetting } from '../resource/resource-settings.js';
|
|
13
|
+
import { ChangeSet } from './change-set.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A plan represents a set of actions that after taken will turn the current resource into the desired one.
|
|
17
|
+
* A plan consists of list of parameter level changes (ADD, REMOVE, MODIFY or NO-OP) as well as a resource level
|
|
18
|
+
* operation (CREATE, DESTROY, MODIFY, RE-CREATE, NO-OP).
|
|
19
|
+
*/
|
|
20
|
+
export class Plan<T extends StringIndexedObject> {
|
|
21
|
+
id: string;
|
|
22
|
+
changeSet: ChangeSet<T>;
|
|
23
|
+
coreParameters: ResourceConfig
|
|
24
|
+
|
|
25
|
+
constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig) {
|
|
26
|
+
this.id = id;
|
|
27
|
+
this.changeSet = changeSet;
|
|
28
|
+
this.coreParameters = resourceMetadata;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The desired config that a plan will achieve after executing all the actions.
|
|
33
|
+
*/
|
|
34
|
+
get desiredConfig(): T | null {
|
|
35
|
+
if (this.changeSet.operation === ResourceOperation.DESTROY) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...this.coreParameters,
|
|
41
|
+
...this.changeSet.desiredParameters,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The current config that the plan is changing.
|
|
47
|
+
*/
|
|
48
|
+
get currentConfig(): T | null {
|
|
49
|
+
if (this.changeSet.operation === ResourceOperation.CREATE) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...this.coreParameters,
|
|
55
|
+
...this.changeSet.currentParameters,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* When multiples of the same resource are allowed, this matching function will match a given config with one of the
|
|
61
|
+
* existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
|
|
62
|
+
* the application name and location to match it to our desired configs name and location.
|
|
63
|
+
*
|
|
64
|
+
* @param params
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
private static matchCurrentParameters<T extends StringIndexedObject>(params: {
|
|
68
|
+
desiredParameters: Partial<T> | null,
|
|
69
|
+
currentParametersArray: Partial<T>[] | null,
|
|
70
|
+
stateParameters: Partial<T> | null,
|
|
71
|
+
settings: ResourceSettings<T>,
|
|
72
|
+
statefulMode: boolean,
|
|
73
|
+
}): Partial<T> | null {
|
|
74
|
+
const {
|
|
75
|
+
desiredParameters,
|
|
76
|
+
currentParametersArray,
|
|
77
|
+
stateParameters,
|
|
78
|
+
settings,
|
|
79
|
+
statefulMode
|
|
80
|
+
} = params;
|
|
81
|
+
|
|
82
|
+
if (!settings.allowMultiple) {
|
|
83
|
+
return currentParametersArray?.[0] ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!currentParametersArray) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (statefulMode) {
|
|
91
|
+
return stateParameters
|
|
92
|
+
? settings.allowMultiple.matcher(stateParameters, currentParametersArray)
|
|
93
|
+
: null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return settings.allowMultiple.matcher(desiredParameters!, currentParametersArray);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The type (id) of the resource
|
|
101
|
+
*
|
|
102
|
+
* @return string
|
|
103
|
+
*/
|
|
104
|
+
getResourceType(): string {
|
|
105
|
+
return this.coreParameters.type
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static calculate<T extends StringIndexedObject>(params: {
|
|
109
|
+
desiredParameters: Partial<T> | null,
|
|
110
|
+
currentParametersArray: Partial<T>[] | null,
|
|
111
|
+
stateParameters: Partial<T> | null,
|
|
112
|
+
coreParameters: ResourceConfig,
|
|
113
|
+
settings: ParsedResourceSettings<T>,
|
|
114
|
+
statefulMode: boolean,
|
|
115
|
+
}): Plan<T> {
|
|
116
|
+
const {
|
|
117
|
+
desiredParameters,
|
|
118
|
+
currentParametersArray,
|
|
119
|
+
stateParameters,
|
|
120
|
+
coreParameters,
|
|
121
|
+
settings,
|
|
122
|
+
statefulMode
|
|
123
|
+
} = params
|
|
124
|
+
|
|
125
|
+
const currentParameters = Plan.matchCurrentParameters<T>({
|
|
126
|
+
desiredParameters,
|
|
127
|
+
currentParametersArray,
|
|
128
|
+
stateParameters,
|
|
129
|
+
settings,
|
|
130
|
+
statefulMode
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const filteredCurrentParameters = Plan.filterCurrentParams<T>({
|
|
134
|
+
desiredParameters,
|
|
135
|
+
currentParameters,
|
|
136
|
+
stateParameters,
|
|
137
|
+
settings,
|
|
138
|
+
statefulMode
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Empty
|
|
142
|
+
if (!filteredCurrentParameters && !desiredParameters) {
|
|
143
|
+
return new Plan(
|
|
144
|
+
uuidV4(),
|
|
145
|
+
ChangeSet.empty<T>(),
|
|
146
|
+
coreParameters,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// CREATE
|
|
151
|
+
if (!filteredCurrentParameters && desiredParameters) {
|
|
152
|
+
return new Plan(
|
|
153
|
+
uuidV4(),
|
|
154
|
+
ChangeSet.create(desiredParameters),
|
|
155
|
+
coreParameters
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// DESTROY
|
|
160
|
+
if (filteredCurrentParameters && !desiredParameters) {
|
|
161
|
+
return new Plan(
|
|
162
|
+
uuidV4(),
|
|
163
|
+
ChangeSet.destroy(filteredCurrentParameters),
|
|
164
|
+
coreParameters
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// NO-OP, MODIFY or RE-CREATE
|
|
169
|
+
const changeSet = ChangeSet.calculateModification(
|
|
170
|
+
desiredParameters!,
|
|
171
|
+
filteredCurrentParameters!,
|
|
172
|
+
settings.parameterSettings,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return new Plan(
|
|
176
|
+
uuidV4(),
|
|
177
|
+
changeSet,
|
|
178
|
+
coreParameters,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Only keep relevant params for the plan. We don't want to change settings that were not already
|
|
184
|
+
* defined.
|
|
185
|
+
*
|
|
186
|
+
* 1. In stateless mode, filter current by desired. We only want to know about settings that the user has specified
|
|
187
|
+
* 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
|
|
188
|
+
* or wants to set. If a parameter is not specified then it's not managed by Codify.
|
|
189
|
+
*/
|
|
190
|
+
private static filterCurrentParams<T extends StringIndexedObject>(params: {
|
|
191
|
+
desiredParameters: Partial<T> | null,
|
|
192
|
+
currentParameters: Partial<T> | null,
|
|
193
|
+
stateParameters: Partial<T> | null,
|
|
194
|
+
settings: ResourceSettings<T>,
|
|
195
|
+
statefulMode: boolean,
|
|
196
|
+
}): Partial<T> | null {
|
|
197
|
+
const {
|
|
198
|
+
desiredParameters: desired,
|
|
199
|
+
currentParameters: current,
|
|
200
|
+
stateParameters: state,
|
|
201
|
+
settings,
|
|
202
|
+
statefulMode
|
|
203
|
+
} = params;
|
|
204
|
+
|
|
205
|
+
if (!current) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const filteredCurrent = filterCurrent()
|
|
210
|
+
if (!filteredCurrent) {
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// For stateful mode, we're done after filtering by the keys of desired + state. Stateless mode
|
|
215
|
+
// requires additional filtering for stateful parameter arrays and objects.
|
|
216
|
+
if (statefulMode) {
|
|
217
|
+
return filteredCurrent;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// TODO: Add object handling here in addition to arrays in the future
|
|
221
|
+
const arrayStatefulParameters = Object.fromEntries(
|
|
222
|
+
Object.entries(filteredCurrent)
|
|
223
|
+
.filter(([k, v]) => isArrayStatefulParameter(k, v))
|
|
224
|
+
.map(([k, v]) => [k, filterArrayStatefulParameter(k, v)])
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return { ...filteredCurrent, ...arrayStatefulParameters }
|
|
228
|
+
|
|
229
|
+
function filterCurrent(): Partial<T> | null {
|
|
230
|
+
if (!current) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (statefulMode) {
|
|
235
|
+
const keys = new Set([...Object.keys(state ?? {}), ...Object.keys(desired ?? {})]);
|
|
236
|
+
return Object.fromEntries(
|
|
237
|
+
Object.entries(current)
|
|
238
|
+
.filter(([k]) => keys.has(k))
|
|
239
|
+
) as Partial<T>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Stateless mode
|
|
243
|
+
const keys = new Set(Object.keys(desired ?? {}));
|
|
244
|
+
return Object.fromEntries(
|
|
245
|
+
Object.entries(current)
|
|
246
|
+
.filter(([k]) => keys.has(k))
|
|
247
|
+
) as Partial<T>;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isArrayStatefulParameter(k: string, v: T[keyof T]): boolean {
|
|
251
|
+
return settings.parameterSettings?.[k]?.type === 'stateful'
|
|
252
|
+
&& (settings.parameterSettings[k] as StatefulParameterSetting).definition.getSettings().type === 'array'
|
|
253
|
+
&& Array.isArray(v)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function filterArrayStatefulParameter(k: string, v: unknown[]): unknown[] {
|
|
257
|
+
const desiredArray = desired![k] as unknown[];
|
|
258
|
+
const matcher = ((settings.parameterSettings![k] as StatefulParameterSetting)
|
|
259
|
+
.definition
|
|
260
|
+
.getSettings() as ArrayParameterSetting)
|
|
261
|
+
.isElementEqual;
|
|
262
|
+
|
|
263
|
+
return v.filter((cv) =>
|
|
264
|
+
desiredArray.find((dv) => (matcher ?? ((a: any, b: any) => a === b))(dv, cv))
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// TODO: This needs to be revisited. I don't think this is valid anymore.
|
|
270
|
+
// 1. For all scenarios, there shouldn't be an apply without a plan beforehand
|
|
271
|
+
// 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted
|
|
272
|
+
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T> {
|
|
273
|
+
if (!data) {
|
|
274
|
+
throw new Error('Data is empty');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
addDefaultValues();
|
|
278
|
+
|
|
279
|
+
return new Plan(
|
|
280
|
+
uuidV4(),
|
|
281
|
+
new ChangeSet<T>(
|
|
282
|
+
data.operation,
|
|
283
|
+
data.parameters
|
|
284
|
+
),
|
|
285
|
+
{
|
|
286
|
+
type: data.resourceType,
|
|
287
|
+
name: data.resourceName,
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
function addDefaultValues(): void {
|
|
292
|
+
Object.entries(defaultValues ?? {})
|
|
293
|
+
.forEach(([key, defaultValue]) => {
|
|
294
|
+
const configValueExists = data!
|
|
295
|
+
.parameters
|
|
296
|
+
.some((p) => p.name === key);
|
|
297
|
+
|
|
298
|
+
// Only set default values if the value does not exist in the config
|
|
299
|
+
if (configValueExists) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
switch (data!.operation) {
|
|
304
|
+
case ResourceOperation.CREATE: {
|
|
305
|
+
data!.parameters.push({
|
|
306
|
+
name: key,
|
|
307
|
+
operation: ParameterOperation.ADD,
|
|
308
|
+
previousValue: null,
|
|
309
|
+
newValue: defaultValue,
|
|
310
|
+
});
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
case ResourceOperation.DESTROY: {
|
|
315
|
+
data!.parameters.push({
|
|
316
|
+
name: key,
|
|
317
|
+
operation: ParameterOperation.REMOVE,
|
|
318
|
+
previousValue: defaultValue,
|
|
319
|
+
newValue: null,
|
|
320
|
+
});
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
case ResourceOperation.MODIFY:
|
|
325
|
+
case ResourceOperation.RECREATE:
|
|
326
|
+
case ResourceOperation.NOOP: {
|
|
327
|
+
data!.parameters.push({
|
|
328
|
+
name: key,
|
|
329
|
+
operation: ParameterOperation.NOOP,
|
|
330
|
+
previousValue: defaultValue,
|
|
331
|
+
newValue: defaultValue,
|
|
332
|
+
});
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Convert the plan to a JSON response object
|
|
343
|
+
*/
|
|
344
|
+
toResponse(): PlanResponseData {
|
|
345
|
+
return {
|
|
346
|
+
planId: this.id,
|
|
347
|
+
operation: this.changeSet.operation,
|
|
348
|
+
resourceName: this.coreParameters.name,
|
|
349
|
+
resourceType: this.coreParameters.type,
|
|
350
|
+
parameters: this.changeSet.parameterChanges,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { Plugin } from './plugin.js';
|
|
3
3
|
import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
4
|
-
import { Resource } from '
|
|
5
|
-
import { Plan } from '
|
|
4
|
+
import { Resource } from '../resource/resource.js';
|
|
5
|
+
import { Plan } from '../plan/plan.js';
|
|
6
6
|
import { spy } from 'sinon';
|
|
7
|
+
import { ResourceSettings } from '../resource/resource-settings.js';
|
|
7
8
|
|
|
8
9
|
interface TestConfig extends StringIndexedObject {
|
|
9
10
|
propA: string;
|
|
@@ -12,17 +13,17 @@ interface TestConfig extends StringIndexedObject {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
class TestResource extends Resource<TestConfig> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
16
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
17
|
+
return {
|
|
18
|
+
id: 'testResource'
|
|
19
|
+
};
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
create(plan: Plan<TestConfig>): Promise<void> {
|
|
22
23
|
return Promise.resolve(undefined);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
destroy(plan: Plan<TestConfig>): Promise<void> {
|
|
26
27
|
return Promise.resolve(undefined);
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -49,7 +50,7 @@ describe('Plugin tests', () => {
|
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
await plugin.apply({ plan });
|
|
52
|
-
expect(resource.
|
|
53
|
+
expect(resource.create.calledOnce).to.be.true;
|
|
53
54
|
});
|
|
54
55
|
|
|
55
56
|
it('Can destroy resource', async () => {
|
|
@@ -65,7 +66,7 @@ describe('Plugin tests', () => {
|
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
await testPlugin.apply({ plan })
|
|
68
|
-
expect(resource.
|
|
69
|
+
expect(resource.destroy.calledOnce).to.be.true;
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
it('Can re-create resource', async () => {
|
|
@@ -81,8 +82,8 @@ describe('Plugin tests', () => {
|
|
|
81
82
|
};
|
|
82
83
|
|
|
83
84
|
await testPlugin.apply({ plan })
|
|
84
|
-
expect(resource.
|
|
85
|
-
expect(resource.
|
|
85
|
+
expect(resource.destroy.calledOnce).to.be.true;
|
|
86
|
+
expect(resource.create.calledOnce).to.be.true;
|
|
86
87
|
});
|
|
87
88
|
|
|
88
89
|
it('Can modify resource', async () => {
|
|
@@ -98,6 +99,6 @@ describe('Plugin tests', () => {
|
|
|
98
99
|
};
|
|
99
100
|
|
|
100
101
|
await testPlugin.apply({ plan })
|
|
101
|
-
expect(resource.
|
|
102
|
+
expect(resource.modify.calledOnce).to.be.true;
|
|
102
103
|
});
|
|
103
104
|
});
|
|
@@ -8,35 +8,39 @@ import {
|
|
|
8
8
|
ValidateResponseData
|
|
9
9
|
} from 'codify-schemas';
|
|
10
10
|
|
|
11
|
+
import { Plan } from '../plan/plan.js';
|
|
12
|
+
import { Resource } from '../resource/resource.js';
|
|
13
|
+
import { ResourceController } from '../resource/resource-controller.js';
|
|
11
14
|
import { splitUserConfig } from '../utils/utils.js';
|
|
12
|
-
import { Plan } from './plan.js';
|
|
13
|
-
import { Resource } from './resource.js';
|
|
14
15
|
|
|
15
16
|
export class Plugin {
|
|
16
17
|
planStorage: Map<string, Plan<any>>;
|
|
17
18
|
|
|
18
|
-
static create(name: string, resources: Resource<any>[]) {
|
|
19
|
-
const resourceMap = new Map<string, Resource<any>>(
|
|
20
|
-
resources.map((r) => [r.typeId, r] as const)
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
return new Plugin(name, resourceMap);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
19
|
constructor(
|
|
27
20
|
public name: string,
|
|
28
|
-
public
|
|
21
|
+
public resourceControllers: Map<string, ResourceController<ResourceConfig>>
|
|
29
22
|
) {
|
|
30
23
|
this.planStorage = new Map();
|
|
31
24
|
}
|
|
32
25
|
|
|
26
|
+
static create(name: string, resources: Resource<any>[]) {
|
|
27
|
+
const controllers = resources
|
|
28
|
+
.map((resource) => new ResourceController(resource))
|
|
29
|
+
|
|
30
|
+
const controllersMap = new Map<string, ResourceController<any>>(
|
|
31
|
+
controllers.map((r) => [r.typeId, r] as const)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return new Plugin(name, controllersMap);
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
async initialize(): Promise<InitializeResponseData> {
|
|
34
|
-
for (const
|
|
35
|
-
await
|
|
38
|
+
for (const controller of this.resourceControllers.values()) {
|
|
39
|
+
await controller.initialize();
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
return {
|
|
39
|
-
resourceDefinitions: [...this.
|
|
43
|
+
resourceDefinitions: [...this.resourceControllers.values()]
|
|
40
44
|
.map((r) => ({
|
|
41
45
|
dependencies: r.dependencies,
|
|
42
46
|
type: r.typeId,
|
|
@@ -47,14 +51,14 @@ export class Plugin {
|
|
|
47
51
|
async validate(data: ValidateRequestData): Promise<ValidateResponseData> {
|
|
48
52
|
const validationResults = [];
|
|
49
53
|
for (const config of data.configs) {
|
|
50
|
-
if (!this.
|
|
54
|
+
if (!this.resourceControllers.has(config.type)) {
|
|
51
55
|
throw new Error(`Resource type not found: ${config.type}`);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
const { parameters,
|
|
55
|
-
const validation = await this.
|
|
58
|
+
const { parameters, coreParameters } = splitUserConfig(config);
|
|
59
|
+
const validation = await this.resourceControllers
|
|
56
60
|
.get(config.type)!
|
|
57
|
-
.validate(parameters,
|
|
61
|
+
.validate(parameters, coreParameters);
|
|
58
62
|
|
|
59
63
|
validationResults.push(validation);
|
|
60
64
|
}
|
|
@@ -68,11 +72,11 @@ export class Plugin {
|
|
|
68
72
|
async plan(data: PlanRequestData): Promise<PlanResponseData> {
|
|
69
73
|
const type = data.desired?.type ?? data.state?.type
|
|
70
74
|
|
|
71
|
-
if (!type || !this.
|
|
75
|
+
if (!type || !this.resourceControllers.has(type)) {
|
|
72
76
|
throw new Error(`Resource type not found: ${type}`);
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
const plan = await this.
|
|
79
|
+
const plan = await this.resourceControllers.get(type)!.plan(
|
|
76
80
|
data.desired ?? null,
|
|
77
81
|
data.state ?? null,
|
|
78
82
|
data.isStateful
|
|
@@ -89,7 +93,7 @@ export class Plugin {
|
|
|
89
93
|
|
|
90
94
|
const plan = this.resolvePlan(data);
|
|
91
95
|
|
|
92
|
-
const resource = this.
|
|
96
|
+
const resource = this.resourceControllers.get(plan.getResourceType());
|
|
93
97
|
if (!resource) {
|
|
94
98
|
throw new Error('Malformed plan with resource that cannot be found');
|
|
95
99
|
}
|
|
@@ -108,12 +112,12 @@ export class Plugin {
|
|
|
108
112
|
return this.planStorage.get(planId)!
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
if (!planRequest?.resourceType || !this.
|
|
115
|
+
if (!planRequest?.resourceType || !this.resourceControllers.has(planRequest.resourceType)) {
|
|
112
116
|
throw new Error('Malformed plan. Resource type must be supplied or resource type was not found');
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
const resource = this.
|
|
116
|
-
return Plan.fromResponse(planRequest, resource.defaultValues);
|
|
119
|
+
const resource = this.resourceControllers.get(planRequest.resourceType)!;
|
|
120
|
+
return Plan.fromResponse(planRequest, resource.parsedSettings.defaultValues);
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
protected async crossValidateResources(configs: ResourceConfig[]): Promise<void> {}
|