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,83 @@
|
|
|
1
|
+
import { Plan } from '../plan/plan.js';
|
|
2
|
+
import { ResourceController } from '../resource/resource-controller.js';
|
|
3
|
+
import { splitUserConfig } from '../utils/utils.js';
|
|
4
|
+
export class Plugin {
|
|
5
|
+
name;
|
|
6
|
+
resourceControllers;
|
|
7
|
+
planStorage;
|
|
8
|
+
constructor(name, resourceControllers) {
|
|
9
|
+
this.name = name;
|
|
10
|
+
this.resourceControllers = resourceControllers;
|
|
11
|
+
this.planStorage = new Map();
|
|
12
|
+
}
|
|
13
|
+
static create(name, resources) {
|
|
14
|
+
const controllers = resources
|
|
15
|
+
.map((resource) => new ResourceController(resource));
|
|
16
|
+
const controllersMap = new Map(controllers.map((r) => [r.typeId, r]));
|
|
17
|
+
return new Plugin(name, controllersMap);
|
|
18
|
+
}
|
|
19
|
+
async initialize() {
|
|
20
|
+
for (const controller of this.resourceControllers.values()) {
|
|
21
|
+
await controller.initialize();
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
resourceDefinitions: [...this.resourceControllers.values()]
|
|
25
|
+
.map((r) => ({
|
|
26
|
+
dependencies: r.dependencies,
|
|
27
|
+
type: r.typeId,
|
|
28
|
+
}))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async validate(data) {
|
|
32
|
+
const validationResults = [];
|
|
33
|
+
for (const config of data.configs) {
|
|
34
|
+
if (!this.resourceControllers.has(config.type)) {
|
|
35
|
+
throw new Error(`Resource type not found: ${config.type}`);
|
|
36
|
+
}
|
|
37
|
+
const { parameters, coreParameters } = splitUserConfig(config);
|
|
38
|
+
const validation = await this.resourceControllers
|
|
39
|
+
.get(config.type)
|
|
40
|
+
.validate(parameters, coreParameters);
|
|
41
|
+
validationResults.push(validation);
|
|
42
|
+
}
|
|
43
|
+
await this.crossValidateResources(data.configs);
|
|
44
|
+
return {
|
|
45
|
+
resourceValidations: validationResults
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async plan(data) {
|
|
49
|
+
const type = data.desired?.type ?? data.state?.type;
|
|
50
|
+
if (!type || !this.resourceControllers.has(type)) {
|
|
51
|
+
throw new Error(`Resource type not found: ${type}`);
|
|
52
|
+
}
|
|
53
|
+
const plan = await this.resourceControllers.get(type).plan(data.desired ?? null, data.state ?? null, data.isStateful);
|
|
54
|
+
this.planStorage.set(plan.id, plan);
|
|
55
|
+
return plan.toResponse();
|
|
56
|
+
}
|
|
57
|
+
async apply(data) {
|
|
58
|
+
if (!data.planId && !data.plan) {
|
|
59
|
+
throw new Error('For applies either plan or planId must be supplied');
|
|
60
|
+
}
|
|
61
|
+
const plan = this.resolvePlan(data);
|
|
62
|
+
const resource = this.resourceControllers.get(plan.getResourceType());
|
|
63
|
+
if (!resource) {
|
|
64
|
+
throw new Error('Malformed plan with resource that cannot be found');
|
|
65
|
+
}
|
|
66
|
+
await resource.apply(plan);
|
|
67
|
+
}
|
|
68
|
+
resolvePlan(data) {
|
|
69
|
+
const { plan: planRequest, planId } = data;
|
|
70
|
+
if (planId) {
|
|
71
|
+
if (!this.planStorage.has(planId)) {
|
|
72
|
+
throw new Error(`Plan with id: ${planId} was not found`);
|
|
73
|
+
}
|
|
74
|
+
return this.planStorage.get(planId);
|
|
75
|
+
}
|
|
76
|
+
if (!planRequest?.resourceType || !this.resourceControllers.has(planRequest.resourceType)) {
|
|
77
|
+
throw new Error('Malformed plan. Resource type must be supplied or resource type was not found');
|
|
78
|
+
}
|
|
79
|
+
const resource = this.resourceControllers.get(planRequest.resourceType);
|
|
80
|
+
return Plan.fromResponse(planRequest, resource.parsedSettings.defaultValues);
|
|
81
|
+
}
|
|
82
|
+
async crossValidateResources(configs) { }
|
|
83
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { StatefulParameter } from './stateful-parameter.js';
|
|
3
|
+
export declare class ConfigParser<T extends StringIndexedObject> {
|
|
4
|
+
private readonly desiredConfig;
|
|
5
|
+
private readonly stateConfig;
|
|
6
|
+
private statefulParametersMap;
|
|
7
|
+
constructor(desiredConfig: Partial<T> & ResourceConfig | null, stateConfig: Partial<T> & ResourceConfig | null, statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>);
|
|
8
|
+
get coreParameters(): ResourceConfig;
|
|
9
|
+
get desiredParameters(): Partial<T> | null;
|
|
10
|
+
get stateParameters(): Partial<T> | null;
|
|
11
|
+
get allParameters(): Partial<T>;
|
|
12
|
+
get allNonStatefulParameters(): Partial<T>;
|
|
13
|
+
get allStatefulParameters(): Partial<T>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { splitUserConfig } from '../utils/utils.js';
|
|
2
|
+
export class ConfigParser {
|
|
3
|
+
desiredConfig;
|
|
4
|
+
stateConfig;
|
|
5
|
+
statefulParametersMap;
|
|
6
|
+
constructor(desiredConfig, stateConfig, statefulParameters) {
|
|
7
|
+
this.desiredConfig = desiredConfig;
|
|
8
|
+
this.stateConfig = stateConfig;
|
|
9
|
+
this.statefulParametersMap = statefulParameters;
|
|
10
|
+
}
|
|
11
|
+
get coreParameters() {
|
|
12
|
+
const desiredCoreParameters = this.desiredConfig ? splitUserConfig(this.desiredConfig).coreParameters : undefined;
|
|
13
|
+
const currentCoreParameters = this.stateConfig ? splitUserConfig(this.stateConfig).coreParameters : undefined;
|
|
14
|
+
if (!desiredCoreParameters && !currentCoreParameters) {
|
|
15
|
+
throw new Error(`Unable to parse resource core parameters from:
|
|
16
|
+
|
|
17
|
+
Desired: ${JSON.stringify(this.desiredConfig, null, 2)}
|
|
18
|
+
|
|
19
|
+
Current: ${JSON.stringify(this.stateConfig, null, 2)}`);
|
|
20
|
+
}
|
|
21
|
+
return desiredCoreParameters ?? currentCoreParameters;
|
|
22
|
+
}
|
|
23
|
+
get desiredParameters() {
|
|
24
|
+
if (!this.desiredConfig) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const { parameters } = splitUserConfig(this.desiredConfig);
|
|
28
|
+
return parameters;
|
|
29
|
+
}
|
|
30
|
+
get stateParameters() {
|
|
31
|
+
if (!this.stateConfig) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const { parameters } = splitUserConfig(this.stateConfig);
|
|
35
|
+
return parameters;
|
|
36
|
+
}
|
|
37
|
+
get allParameters() {
|
|
38
|
+
return { ...this.desiredParameters, ...this.stateParameters };
|
|
39
|
+
}
|
|
40
|
+
get allNonStatefulParameters() {
|
|
41
|
+
const { allParameters, statefulParametersMap, } = this;
|
|
42
|
+
return Object.fromEntries(Object.entries(allParameters).filter(([key]) => !statefulParametersMap.has(key)));
|
|
43
|
+
}
|
|
44
|
+
get allStatefulParameters() {
|
|
45
|
+
const { allParameters, statefulParametersMap } = this;
|
|
46
|
+
return Object.fromEntries(Object.entries(allParameters).filter(([key]) => statefulParametersMap.has(key)));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { ParameterSetting, ResourceSettings } from './resource-settings.js';
|
|
3
|
+
import { StatefulParameter as StatefulParameterImpl } from './stateful-parameter.js';
|
|
4
|
+
export declare class ParsedResourceSettings<T extends StringIndexedObject> implements ResourceSettings<T> {
|
|
5
|
+
private cache;
|
|
6
|
+
id: string;
|
|
7
|
+
schema?: unknown;
|
|
8
|
+
allowMultiple?: {
|
|
9
|
+
matcher: (desired: Partial<T>, current: Partial<T>[]) => Partial<T>;
|
|
10
|
+
} | undefined;
|
|
11
|
+
removeStatefulParametersBeforeDestroy?: boolean | undefined;
|
|
12
|
+
dependencies?: string[] | undefined;
|
|
13
|
+
inputTransformation?: ((desired: Partial<T>) => unknown) | undefined;
|
|
14
|
+
private settings;
|
|
15
|
+
constructor(settings: ResourceSettings<T>);
|
|
16
|
+
get typeId(): string;
|
|
17
|
+
get statefulParameters(): Map<keyof T, StatefulParameterImpl<T, T[keyof T]>>;
|
|
18
|
+
get parameterSettings(): Record<keyof T, ParameterSetting>;
|
|
19
|
+
get defaultValues(): Partial<Record<keyof T, unknown>>;
|
|
20
|
+
get inputTransformations(): Partial<Record<keyof T, (a: unknown) => unknown>>;
|
|
21
|
+
get statefulParameterOrder(): Map<keyof T, number>;
|
|
22
|
+
private validateSettings;
|
|
23
|
+
private validateParameterEqualsFn;
|
|
24
|
+
private resolveEqualsFn;
|
|
25
|
+
private getFromCacheOrCreate;
|
|
26
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { areArraysEqual } from '../utils/utils.js';
|
|
2
|
+
import { ParameterEqualsDefaults } from './resource-settings.js';
|
|
3
|
+
export class ParsedResourceSettings {
|
|
4
|
+
cache = new Map();
|
|
5
|
+
id;
|
|
6
|
+
schema;
|
|
7
|
+
allowMultiple;
|
|
8
|
+
removeStatefulParametersBeforeDestroy;
|
|
9
|
+
dependencies;
|
|
10
|
+
inputTransformation;
|
|
11
|
+
settings;
|
|
12
|
+
constructor(settings) {
|
|
13
|
+
this.settings = settings;
|
|
14
|
+
this.id = settings.id;
|
|
15
|
+
this.schema = settings.schema;
|
|
16
|
+
this.allowMultiple = settings.allowMultiple;
|
|
17
|
+
this.removeStatefulParametersBeforeDestroy = settings.removeStatefulParametersBeforeDestroy;
|
|
18
|
+
this.dependencies = settings.dependencies;
|
|
19
|
+
this.inputTransformation = settings.inputTransformation;
|
|
20
|
+
this.validateSettings();
|
|
21
|
+
}
|
|
22
|
+
get typeId() {
|
|
23
|
+
return this.id;
|
|
24
|
+
}
|
|
25
|
+
get statefulParameters() {
|
|
26
|
+
return this.getFromCacheOrCreate('statefulParameters', () => {
|
|
27
|
+
const statefulParameters = Object.entries(this.settings.parameterSettings ?? {})
|
|
28
|
+
.filter(([, p]) => p?.type === 'stateful')
|
|
29
|
+
.map(([k, v]) => [k, v.definition]);
|
|
30
|
+
return new Map(statefulParameters);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
get parameterSettings() {
|
|
34
|
+
return this.getFromCacheOrCreate('parameterSetting', () => {
|
|
35
|
+
const settings = Object.entries(this.settings.parameterSettings ?? {})
|
|
36
|
+
.map(([k, v]) => [k, v])
|
|
37
|
+
.map(([k, v]) => {
|
|
38
|
+
v.isEqual = this.resolveEqualsFn(v, k);
|
|
39
|
+
return [k, v];
|
|
40
|
+
});
|
|
41
|
+
return Object.fromEntries(settings);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
get defaultValues() {
|
|
45
|
+
return this.getFromCacheOrCreate('defaultValues', () => {
|
|
46
|
+
if (!this.settings.parameterSettings) {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
const defaultValues = Object.fromEntries(Object.entries(this.settings.parameterSettings)
|
|
50
|
+
.filter(([, v]) => v.default !== undefined)
|
|
51
|
+
.map(([k, v]) => [k, v.default]));
|
|
52
|
+
const statefulParameterDefaultValues = Object.fromEntries(Object.entries(this.settings.parameterSettings)
|
|
53
|
+
.filter(([, v]) => v?.type === 'stateful')
|
|
54
|
+
.filter(([, v]) => v.definition.getSettings().default !== undefined)
|
|
55
|
+
.map(([k, v]) => [k, v.definition.getSettings().default]));
|
|
56
|
+
return { ...defaultValues, ...statefulParameterDefaultValues };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
get inputTransformations() {
|
|
60
|
+
return this.getFromCacheOrCreate('inputTransformations', () => {
|
|
61
|
+
if (!this.settings.parameterSettings) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
return Object.fromEntries(Object.entries(this.settings.parameterSettings)
|
|
65
|
+
.filter(([, v]) => v.inputTransformation !== undefined)
|
|
66
|
+
.map(([k, v]) => [k, v.inputTransformation]));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
get statefulParameterOrder() {
|
|
70
|
+
return this.getFromCacheOrCreate('stateParameterOrder', () => {
|
|
71
|
+
const entries = Object.entries(this.settings.parameterSettings ?? {})
|
|
72
|
+
.filter(([, v]) => v?.type === 'stateful')
|
|
73
|
+
.map(([k, v]) => [k, v]);
|
|
74
|
+
const orderedEntries = entries.filter(([, v]) => v.order !== undefined);
|
|
75
|
+
const unorderedEntries = entries.filter(([, v]) => v.order === undefined);
|
|
76
|
+
orderedEntries.sort((a, b) => a[1].order - b[1].order);
|
|
77
|
+
const resultArray = [
|
|
78
|
+
...orderedEntries.map(([k]) => k),
|
|
79
|
+
...unorderedEntries.map(([k]) => k)
|
|
80
|
+
];
|
|
81
|
+
return new Map(resultArray.map((key, idx) => [key, idx]));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
validateSettings() {
|
|
85
|
+
// validate parameter settings
|
|
86
|
+
if (this.settings.parameterSettings) {
|
|
87
|
+
for (const [k, v] of Object.entries(this.settings.parameterSettings)) {
|
|
88
|
+
if (!v) {
|
|
89
|
+
throw new Error(`Resource: ${this.id}. Parameter setting ${k} was left undefined`);
|
|
90
|
+
}
|
|
91
|
+
this.validateParameterEqualsFn(v, k);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (this.allowMultiple
|
|
95
|
+
&& Object.values(this.parameterSettings).some((v) => v.type === 'stateful')) {
|
|
96
|
+
throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
validateParameterEqualsFn(parameter, key) {
|
|
100
|
+
if (parameter.type === 'stateful') {
|
|
101
|
+
const nestedSettings = parameter.definition.getSettings();
|
|
102
|
+
if (nestedSettings.type === 'stateful') {
|
|
103
|
+
throw new Error(`Nested stateful parameters are not allowed for ${key}`);
|
|
104
|
+
}
|
|
105
|
+
this.validateParameterEqualsFn(nestedSettings, key);
|
|
106
|
+
}
|
|
107
|
+
// The rest of the types have defaults set already
|
|
108
|
+
}
|
|
109
|
+
resolveEqualsFn(parameter, key) {
|
|
110
|
+
if (parameter.type === 'array') {
|
|
111
|
+
return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter);
|
|
112
|
+
}
|
|
113
|
+
if (parameter.type === 'stateful') {
|
|
114
|
+
return this.resolveEqualsFn(parameter.definition.getSettings(), key);
|
|
115
|
+
}
|
|
116
|
+
return parameter.isEqual ?? ParameterEqualsDefaults[parameter.type] ?? (((a, b) => a === b));
|
|
117
|
+
}
|
|
118
|
+
getFromCacheOrCreate(key, create) {
|
|
119
|
+
if (this.cache.has(key)) {
|
|
120
|
+
return this.cache.get(key);
|
|
121
|
+
}
|
|
122
|
+
const result = create();
|
|
123
|
+
this.cache.set(key, result);
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Ajv, ValidateFunction } from 'ajv';
|
|
2
|
+
import { ResourceConfig, StringIndexedObject, ValidateResponseData } from 'codify-schemas';
|
|
3
|
+
import { Plan } from '../plan/plan.js';
|
|
4
|
+
import { ParsedResourceSettings } from './parsed-resource-settings.js';
|
|
5
|
+
import { Resource } from './resource.js';
|
|
6
|
+
import { ResourceSettings } from './resource-settings.js';
|
|
7
|
+
export declare class ResourceController<T extends StringIndexedObject> {
|
|
8
|
+
readonly resource: Resource<T>;
|
|
9
|
+
readonly settings: ResourceSettings<T>;
|
|
10
|
+
readonly parsedSettings: ParsedResourceSettings<T>;
|
|
11
|
+
readonly typeId: string;
|
|
12
|
+
readonly dependencies: string[];
|
|
13
|
+
protected ajv?: Ajv;
|
|
14
|
+
protected schemaValidator?: ValidateFunction;
|
|
15
|
+
constructor(resource: Resource<T>);
|
|
16
|
+
initialize(): Promise<void>;
|
|
17
|
+
validate(parameters: Partial<T>, resourceMetaData: ResourceConfig): Promise<ValidateResponseData['resourceValidations'][0]>;
|
|
18
|
+
plan(desiredConfig: Partial<T> & ResourceConfig | null, stateConfig?: Partial<T> & ResourceConfig | null, statefulMode?: boolean): Promise<Plan<T>>;
|
|
19
|
+
apply(plan: Plan<T>): Promise<void>;
|
|
20
|
+
private applyCreate;
|
|
21
|
+
private applyModify;
|
|
22
|
+
private applyDestroy;
|
|
23
|
+
private validateRefreshResults;
|
|
24
|
+
private applyTransformParameters;
|
|
25
|
+
private addDefaultValues;
|
|
26
|
+
private refreshNonStatefulParameters;
|
|
27
|
+
private refreshStatefulParameters;
|
|
28
|
+
private validatePlanInputs;
|
|
29
|
+
private getSortedStatefulParameterChanges;
|
|
30
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { Ajv } from 'ajv';
|
|
2
|
+
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
3
|
+
import { Plan } from '../plan/plan.js';
|
|
4
|
+
import { ConfigParser } from './config-parser.js';
|
|
5
|
+
import { ParsedResourceSettings } from './parsed-resource-settings.js';
|
|
6
|
+
export class ResourceController {
|
|
7
|
+
resource;
|
|
8
|
+
settings;
|
|
9
|
+
parsedSettings;
|
|
10
|
+
typeId;
|
|
11
|
+
dependencies;
|
|
12
|
+
ajv;
|
|
13
|
+
schemaValidator;
|
|
14
|
+
constructor(resource) {
|
|
15
|
+
this.resource = resource;
|
|
16
|
+
this.settings = resource.getSettings();
|
|
17
|
+
this.typeId = this.settings.id;
|
|
18
|
+
this.dependencies = this.settings.dependencies ?? [];
|
|
19
|
+
if (this.settings.schema) {
|
|
20
|
+
this.ajv = new Ajv({
|
|
21
|
+
allErrors: true,
|
|
22
|
+
strict: true,
|
|
23
|
+
strictRequired: false,
|
|
24
|
+
});
|
|
25
|
+
this.schemaValidator = this.ajv.compile(this.settings.schema);
|
|
26
|
+
}
|
|
27
|
+
this.parsedSettings = new ParsedResourceSettings(this.settings);
|
|
28
|
+
}
|
|
29
|
+
async initialize() {
|
|
30
|
+
return this.resource.initialize();
|
|
31
|
+
}
|
|
32
|
+
async validate(parameters, resourceMetaData) {
|
|
33
|
+
if (this.schemaValidator) {
|
|
34
|
+
const isValid = this.schemaValidator(parameters);
|
|
35
|
+
if (!isValid) {
|
|
36
|
+
return {
|
|
37
|
+
isValid: false,
|
|
38
|
+
resourceName: resourceMetaData.name,
|
|
39
|
+
resourceType: resourceMetaData.type,
|
|
40
|
+
schemaValidationErrors: this.schemaValidator?.errors ?? [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let isValid = true;
|
|
45
|
+
let customValidationErrorMessage;
|
|
46
|
+
try {
|
|
47
|
+
await this.resource.validate(parameters);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
isValid = false;
|
|
51
|
+
customValidationErrorMessage = error.message;
|
|
52
|
+
}
|
|
53
|
+
if (!isValid) {
|
|
54
|
+
return {
|
|
55
|
+
customValidationErrorMessage,
|
|
56
|
+
isValid: false,
|
|
57
|
+
resourceName: resourceMetaData.name,
|
|
58
|
+
resourceType: resourceMetaData.type,
|
|
59
|
+
schemaValidationErrors: this.schemaValidator?.errors ?? [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
isValid: true,
|
|
64
|
+
resourceName: resourceMetaData.name,
|
|
65
|
+
resourceType: resourceMetaData.type,
|
|
66
|
+
schemaValidationErrors: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async plan(desiredConfig, stateConfig = null, statefulMode = false) {
|
|
70
|
+
this.validatePlanInputs(desiredConfig, stateConfig, statefulMode);
|
|
71
|
+
this.addDefaultValues(desiredConfig);
|
|
72
|
+
await this.applyTransformParameters(desiredConfig);
|
|
73
|
+
// Parse data from the user supplied config
|
|
74
|
+
const parsedConfig = new ConfigParser(desiredConfig, stateConfig, this.parsedSettings.statefulParameters);
|
|
75
|
+
const { coreParameters, desiredParameters, stateParameters, allNonStatefulParameters, allStatefulParameters, } = parsedConfig;
|
|
76
|
+
// Refresh resource parameters. This refreshes the parameters that configure the resource itself
|
|
77
|
+
const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters);
|
|
78
|
+
// Short circuit here. If the resource is non-existent, there's no point checking stateful parameters
|
|
79
|
+
if (currentParametersArray === null
|
|
80
|
+
|| currentParametersArray === undefined
|
|
81
|
+
|| this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true
|
|
82
|
+
|| currentParametersArray.length === 0
|
|
83
|
+
|| currentParametersArray.filter(Boolean).length === 0) {
|
|
84
|
+
return Plan.calculate({
|
|
85
|
+
desiredParameters,
|
|
86
|
+
currentParametersArray,
|
|
87
|
+
stateParameters,
|
|
88
|
+
coreParameters,
|
|
89
|
+
settings: this.parsedSettings,
|
|
90
|
+
statefulMode,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Refresh stateful parameters. These parameters have state external to the resource. allowMultiple
|
|
94
|
+
// does not work together with stateful parameters
|
|
95
|
+
const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters);
|
|
96
|
+
return Plan.calculate({
|
|
97
|
+
desiredParameters,
|
|
98
|
+
currentParametersArray: [{ ...currentParametersArray[0], ...statefulCurrentParameters }],
|
|
99
|
+
stateParameters,
|
|
100
|
+
coreParameters,
|
|
101
|
+
settings: this.parsedSettings,
|
|
102
|
+
statefulMode
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async apply(plan) {
|
|
106
|
+
if (plan.getResourceType() !== this.typeId) {
|
|
107
|
+
throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
|
|
108
|
+
}
|
|
109
|
+
switch (plan.changeSet.operation) {
|
|
110
|
+
case ResourceOperation.CREATE: {
|
|
111
|
+
return this.applyCreate(plan);
|
|
112
|
+
}
|
|
113
|
+
case ResourceOperation.MODIFY: {
|
|
114
|
+
return this.applyModify(plan);
|
|
115
|
+
}
|
|
116
|
+
case ResourceOperation.RECREATE: {
|
|
117
|
+
await this.applyDestroy(plan);
|
|
118
|
+
return this.applyCreate(plan);
|
|
119
|
+
}
|
|
120
|
+
case ResourceOperation.DESTROY: {
|
|
121
|
+
return this.applyDestroy(plan);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async applyCreate(plan) {
|
|
126
|
+
await this.resource.create(plan);
|
|
127
|
+
const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges);
|
|
128
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
129
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name);
|
|
130
|
+
await statefulParameter.add(parameterChange.newValue, plan);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async applyModify(plan) {
|
|
134
|
+
const parameterChanges = plan
|
|
135
|
+
.changeSet
|
|
136
|
+
.parameterChanges
|
|
137
|
+
.filter((c) => c.operation !== ParameterOperation.NOOP);
|
|
138
|
+
const statelessParameterChanges = parameterChanges
|
|
139
|
+
.filter((pc) => !this.parsedSettings.statefulParameters.has(pc.name));
|
|
140
|
+
for (const pc of statelessParameterChanges) {
|
|
141
|
+
await this.resource.modify(pc, plan);
|
|
142
|
+
}
|
|
143
|
+
const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges);
|
|
144
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
145
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name);
|
|
146
|
+
switch (parameterChange.operation) {
|
|
147
|
+
case ParameterOperation.ADD: {
|
|
148
|
+
await statefulParameter.add(parameterChange.newValue, plan);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case ParameterOperation.MODIFY: {
|
|
152
|
+
await statefulParameter.modify(parameterChange.newValue, parameterChange.previousValue, plan);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case ParameterOperation.REMOVE: {
|
|
156
|
+
await statefulParameter.remove(parameterChange.previousValue, plan);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async applyDestroy(plan) {
|
|
163
|
+
// If this option is set (defaults to false), then stateful parameters need to be destroyed
|
|
164
|
+
// as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
|
|
165
|
+
if (this.settings.removeStatefulParametersBeforeDestroy) {
|
|
166
|
+
const statefulParameterChanges = this.getSortedStatefulParameterChanges(plan.changeSet.parameterChanges);
|
|
167
|
+
for (const parameterChange of statefulParameterChanges) {
|
|
168
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(parameterChange.name);
|
|
169
|
+
await statefulParameter.remove(parameterChange.previousValue, plan);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await this.resource.destroy(plan);
|
|
173
|
+
}
|
|
174
|
+
validateRefreshResults(refresh) {
|
|
175
|
+
if (!refresh) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (!this.settings.allowMultiple && refresh.length > 1) {
|
|
179
|
+
throw new Error(`Resource: ${this.settings.id}. Allow multiple was set to false but multiple refresh results were returned.
|
|
180
|
+
|
|
181
|
+
${JSON.stringify(refresh, null, 2)}
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async applyTransformParameters(desired) {
|
|
186
|
+
if (!desired) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
for (const [key, inputTransformation] of Object.entries(this.parsedSettings.inputTransformations)) {
|
|
190
|
+
if (desired[key] === undefined || !inputTransformation) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
desired[key] = await inputTransformation(desired[key]);
|
|
194
|
+
}
|
|
195
|
+
if (this.settings.inputTransformation) {
|
|
196
|
+
const transformed = await this.settings.inputTransformation(desired);
|
|
197
|
+
Object.keys(desired).forEach((k) => delete desired[k]);
|
|
198
|
+
Object.assign(desired, transformed);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
addDefaultValues(desired) {
|
|
202
|
+
if (!desired) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const [key, defaultValue] of Object.entries(this.parsedSettings.defaultValues)) {
|
|
206
|
+
if (defaultValue !== undefined && (desired[key] === undefined || desired[key] === null)) {
|
|
207
|
+
desired[key] = defaultValue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async refreshNonStatefulParameters(resourceParameters) {
|
|
212
|
+
const result = await this.resource.refresh(resourceParameters);
|
|
213
|
+
const currentParametersArray = Array.isArray(result) || result === null
|
|
214
|
+
? result
|
|
215
|
+
: [result];
|
|
216
|
+
this.validateRefreshResults(currentParametersArray);
|
|
217
|
+
return currentParametersArray;
|
|
218
|
+
}
|
|
219
|
+
// Refresh stateful parameters
|
|
220
|
+
// This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
|
|
221
|
+
async refreshStatefulParameters(statefulParametersConfig) {
|
|
222
|
+
const result = {};
|
|
223
|
+
const sortedEntries = Object.entries(statefulParametersConfig)
|
|
224
|
+
.sort(([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1) - this.parsedSettings.statefulParameterOrder.get(key2));
|
|
225
|
+
for (const [key, desiredValue] of sortedEntries) {
|
|
226
|
+
const statefulParameter = this.parsedSettings.statefulParameters.get(key);
|
|
227
|
+
if (!statefulParameter) {
|
|
228
|
+
throw new Error(`Stateful parameter ${key} was not found`);
|
|
229
|
+
}
|
|
230
|
+
result[key] = await statefulParameter.refresh(desiredValue ?? null);
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
validatePlanInputs(desired, current, statefulMode) {
|
|
235
|
+
if (!desired && !current) {
|
|
236
|
+
throw new Error('Desired config and current config cannot both be missing');
|
|
237
|
+
}
|
|
238
|
+
if (!statefulMode && !desired) {
|
|
239
|
+
throw new Error('Desired config must be provided in non-stateful mode');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
getSortedStatefulParameterChanges(parameterChanges) {
|
|
243
|
+
return parameterChanges
|
|
244
|
+
.filter((pc) => this.parsedSettings.statefulParameters.has(pc.name))
|
|
245
|
+
.sort((a, b) => this.parsedSettings.statefulParameterOrder.get(a.name) - this.parsedSettings.statefulParameterOrder.get(b.name));
|
|
246
|
+
}
|
|
247
|
+
}
|