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
package/.eslintrc.json
CHANGED
|
@@ -9,13 +9,20 @@
|
|
|
9
9
|
"warn",
|
|
10
10
|
"always"
|
|
11
11
|
],
|
|
12
|
-
"perfectionist/sort-classes":
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
"perfectionist/sort-classes": "off",
|
|
13
|
+
"perfectionist/sort-interfaces": "off",
|
|
14
|
+
"perfectionist/sort-enums": "off",
|
|
15
|
+
"perfectionist/sort-objects": "off",
|
|
16
|
+
"perfectionist/sort-object-types": "off",
|
|
17
|
+
"unicorn/no-array-reduce": "off",
|
|
18
|
+
"unicorn/no-array-for-each": "off",
|
|
19
|
+
"unicorn/prefer-object-from-entries": "off",
|
|
20
|
+
"unicorn/prefer-type-error": "off",
|
|
15
21
|
"quotes": [
|
|
16
22
|
"error",
|
|
17
23
|
"single"
|
|
18
|
-
]
|
|
24
|
+
],
|
|
25
|
+
"no-await-in-loop": "off"
|
|
19
26
|
},
|
|
20
27
|
"ignorePatterns": [
|
|
21
28
|
"*.test.ts"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
|
3
|
+
|
|
4
|
+
name: Release
|
|
5
|
+
|
|
6
|
+
on: workflow_dispatch
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: '20.x'
|
|
16
|
+
cache: 'npm'
|
|
17
|
+
- run: npm ci
|
|
18
|
+
- run: tsc
|
|
19
|
+
- run: npm publish
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
|
3
|
+
|
|
4
|
+
name: Unit tests
|
|
5
|
+
|
|
6
|
+
on: [ push ]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: '20.x'
|
|
16
|
+
cache: 'npm'
|
|
17
|
+
- run: npm ci
|
|
18
|
+
- run: tsc
|
|
19
|
+
- run: npm run test
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { Plugin } from './
|
|
2
|
-
export * from './
|
|
3
|
-
export * from './
|
|
4
|
-
export * from './
|
|
5
|
-
export * from './
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
8
|
-
export * from './
|
|
9
|
-
export * from './
|
|
10
|
-
export * from './
|
|
1
|
+
import { Plugin } from './plugin/plugin.js';
|
|
2
|
+
export * from './errors.js';
|
|
3
|
+
export * from './plan/change-set.js';
|
|
4
|
+
export * from './plan/plan.js';
|
|
5
|
+
export * from './plan/plan-types.js';
|
|
6
|
+
export * from './plugin/plugin.js';
|
|
7
|
+
export * from './resource/parsed-resource-settings.js';
|
|
8
|
+
export * from './resource/resource.js';
|
|
9
|
+
export * from './resource/resource-settings.js';
|
|
10
|
+
export * from './resource/stateful-parameter.js';
|
|
11
11
|
export * from './utils/utils.js';
|
|
12
12
|
export declare function runPlugin(plugin: Plugin): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { MessageHandler } from './messages/handlers.js';
|
|
2
|
-
export * from './
|
|
3
|
-
export * from './
|
|
4
|
-
export * from './
|
|
5
|
-
export * from './
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
8
|
-
export * from './
|
|
9
|
-
export * from './
|
|
10
|
-
export * from './
|
|
2
|
+
export * from './errors.js';
|
|
3
|
+
export * from './plan/change-set.js';
|
|
4
|
+
export * from './plan/plan.js';
|
|
5
|
+
export * from './plan/plan-types.js';
|
|
6
|
+
export * from './plugin/plugin.js';
|
|
7
|
+
export * from './resource/parsed-resource-settings.js';
|
|
8
|
+
export * from './resource/resource.js';
|
|
9
|
+
export * from './resource/resource-settings.js';
|
|
10
|
+
export * from './resource/stateful-parameter.js';
|
|
11
11
|
export * from './utils/utils.js';
|
|
12
12
|
export async function runPlugin(plugin) {
|
|
13
13
|
const messageHandler = new MessageHandler(plugin);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Ajv } from 'ajv';
|
|
2
2
|
import addFormats from 'ajv-formats';
|
|
3
3
|
import { ApplyRequestDataSchema, ApplyResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
|
|
4
|
-
import { SudoError } from '../
|
|
4
|
+
import { SudoError } from '../errors.js';
|
|
5
5
|
const SupportedRequests = {
|
|
6
6
|
'apply': {
|
|
7
7
|
async handler(plugin, data) {
|
|
@@ -81,6 +81,7 @@ export class MessageHandler {
|
|
|
81
81
|
if (!message.hasOwnProperty('cmd')) {
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
|
+
// @ts-ignore
|
|
84
85
|
const cmd = message.cmd + '_Response';
|
|
85
86
|
if (e instanceof SudoError) {
|
|
86
87
|
return process.send?.({
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ParameterOperation, ResourceOperation, StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { ParameterSetting } from '../resource/resource-settings.js';
|
|
3
|
+
/**
|
|
4
|
+
* A parameter change describes a parameter level change to a resource.
|
|
5
|
+
*/
|
|
6
|
+
export interface ParameterChange<T extends StringIndexedObject> {
|
|
7
|
+
/**
|
|
8
|
+
* The name of the parameter
|
|
9
|
+
*/
|
|
10
|
+
name: keyof T & string;
|
|
11
|
+
/**
|
|
12
|
+
* The operation to be performed on the parameter.
|
|
13
|
+
*/
|
|
14
|
+
operation: ParameterOperation;
|
|
15
|
+
/**
|
|
16
|
+
* The previous value of the resource (the current value on the system)
|
|
17
|
+
*/
|
|
18
|
+
previousValue: any | null;
|
|
19
|
+
/**
|
|
20
|
+
* The new value of the resource (the desired value)
|
|
21
|
+
*/
|
|
22
|
+
newValue: any | null;
|
|
23
|
+
}
|
|
24
|
+
export declare class ChangeSet<T extends StringIndexedObject> {
|
|
25
|
+
operation: ResourceOperation;
|
|
26
|
+
parameterChanges: Array<ParameterChange<T>>;
|
|
27
|
+
constructor(operation: ResourceOperation, parameterChanges: Array<ParameterChange<T>>);
|
|
28
|
+
get desiredParameters(): T;
|
|
29
|
+
get currentParameters(): T;
|
|
30
|
+
static empty<T extends StringIndexedObject>(): ChangeSet<T>;
|
|
31
|
+
static create<T extends StringIndexedObject>(desired: Partial<T>): ChangeSet<T>;
|
|
32
|
+
static destroy<T extends StringIndexedObject>(current: Partial<T>): ChangeSet<T>;
|
|
33
|
+
static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParameterSetting>>): ChangeSet<T>;
|
|
34
|
+
private static calculateParameterChanges;
|
|
35
|
+
private static combineResourceOperations;
|
|
36
|
+
private static isSame;
|
|
37
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ParameterOperation, ResourceOperation } from 'codify-schemas';
|
|
2
|
+
import { areArraysEqual } from '../utils/utils.js';
|
|
3
|
+
// Change set will coerce undefined values to null because undefined is not valid JSON
|
|
4
|
+
export class ChangeSet {
|
|
5
|
+
operation;
|
|
6
|
+
parameterChanges;
|
|
7
|
+
constructor(operation, parameterChanges) {
|
|
8
|
+
this.operation = operation;
|
|
9
|
+
this.parameterChanges = parameterChanges;
|
|
10
|
+
}
|
|
11
|
+
get desiredParameters() {
|
|
12
|
+
return this.parameterChanges
|
|
13
|
+
.reduce((obj, pc) => ({
|
|
14
|
+
...obj,
|
|
15
|
+
[pc.name]: pc.newValue,
|
|
16
|
+
}), {});
|
|
17
|
+
}
|
|
18
|
+
get currentParameters() {
|
|
19
|
+
return this.parameterChanges
|
|
20
|
+
.reduce((obj, pc) => ({
|
|
21
|
+
...obj,
|
|
22
|
+
[pc.name]: pc.previousValue,
|
|
23
|
+
}), {});
|
|
24
|
+
}
|
|
25
|
+
static empty() {
|
|
26
|
+
return new ChangeSet(ResourceOperation.NOOP, []);
|
|
27
|
+
}
|
|
28
|
+
static create(desired) {
|
|
29
|
+
const parameterChanges = Object.entries(desired)
|
|
30
|
+
.map(([k, v]) => ({
|
|
31
|
+
name: k,
|
|
32
|
+
operation: ParameterOperation.ADD,
|
|
33
|
+
previousValue: null,
|
|
34
|
+
newValue: v ?? null,
|
|
35
|
+
}));
|
|
36
|
+
return new ChangeSet(ResourceOperation.CREATE, parameterChanges);
|
|
37
|
+
}
|
|
38
|
+
static destroy(current) {
|
|
39
|
+
const parameterChanges = Object.entries(current)
|
|
40
|
+
.map(([k, v]) => ({
|
|
41
|
+
name: k,
|
|
42
|
+
operation: ParameterOperation.REMOVE,
|
|
43
|
+
previousValue: v ?? null,
|
|
44
|
+
newValue: null,
|
|
45
|
+
}));
|
|
46
|
+
return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
|
|
47
|
+
}
|
|
48
|
+
static calculateModification(desired, current, parameterSettings = {}) {
|
|
49
|
+
const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
|
|
50
|
+
const statefulParameterKeys = new Set(Object.entries(parameterSettings)
|
|
51
|
+
.filter(([, v]) => v?.type === 'stateful')
|
|
52
|
+
.map(([k]) => k));
|
|
53
|
+
const resourceOperation = pc
|
|
54
|
+
.filter((change) => change.operation !== ParameterOperation.NOOP)
|
|
55
|
+
.reduce((operation, curr) => {
|
|
56
|
+
let newOperation;
|
|
57
|
+
if (statefulParameterKeys.has(curr.name)) {
|
|
58
|
+
newOperation = ResourceOperation.MODIFY; // All stateful parameters are modify only
|
|
59
|
+
}
|
|
60
|
+
else if (parameterSettings[curr.name]?.canModify) {
|
|
61
|
+
newOperation = ResourceOperation.MODIFY;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
newOperation = ResourceOperation.RECREATE; // Default to Re-create. Should handle the majority of use cases
|
|
65
|
+
}
|
|
66
|
+
return ChangeSet.combineResourceOperations(operation, newOperation);
|
|
67
|
+
}, ResourceOperation.NOOP);
|
|
68
|
+
return new ChangeSet(resourceOperation, pc);
|
|
69
|
+
}
|
|
70
|
+
static calculateParameterChanges(desiredParameters, currentParameters, parameterOptions) {
|
|
71
|
+
const parameterChangeSet = new Array();
|
|
72
|
+
// Filter out null and undefined values or else the diff below will not work
|
|
73
|
+
const desired = Object.fromEntries(Object.entries(desiredParameters).filter(([, v]) => v !== null && v !== undefined));
|
|
74
|
+
const current = Object.fromEntries(Object.entries(currentParameters).filter(([, v]) => v !== null && v !== undefined));
|
|
75
|
+
for (const [k, v] of Object.entries(current)) {
|
|
76
|
+
if (desired?.[k] === null || desired?.[k] === undefined) {
|
|
77
|
+
parameterChangeSet.push({
|
|
78
|
+
name: k,
|
|
79
|
+
previousValue: v ?? null,
|
|
80
|
+
newValue: null,
|
|
81
|
+
operation: ParameterOperation.REMOVE,
|
|
82
|
+
});
|
|
83
|
+
delete current[k];
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
|
|
87
|
+
parameterChangeSet.push({
|
|
88
|
+
name: k,
|
|
89
|
+
previousValue: v ?? null,
|
|
90
|
+
newValue: desired[k] ?? null,
|
|
91
|
+
operation: ParameterOperation.MODIFY,
|
|
92
|
+
});
|
|
93
|
+
delete current[k];
|
|
94
|
+
delete desired[k];
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
parameterChangeSet.push({
|
|
98
|
+
name: k,
|
|
99
|
+
previousValue: v ?? null,
|
|
100
|
+
newValue: desired[k] ?? null,
|
|
101
|
+
operation: ParameterOperation.NOOP,
|
|
102
|
+
});
|
|
103
|
+
delete current[k];
|
|
104
|
+
delete desired[k];
|
|
105
|
+
}
|
|
106
|
+
if (Object.keys(current).length > 0) {
|
|
107
|
+
throw new Error('Diff algorithm error');
|
|
108
|
+
}
|
|
109
|
+
for (const [k, v] of Object.entries(desired)) {
|
|
110
|
+
parameterChangeSet.push({
|
|
111
|
+
name: k,
|
|
112
|
+
previousValue: null,
|
|
113
|
+
newValue: v ?? null,
|
|
114
|
+
operation: ParameterOperation.ADD,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return parameterChangeSet;
|
|
118
|
+
}
|
|
119
|
+
static combineResourceOperations(prev, next) {
|
|
120
|
+
const orderOfOperations = [
|
|
121
|
+
ResourceOperation.NOOP,
|
|
122
|
+
ResourceOperation.MODIFY,
|
|
123
|
+
ResourceOperation.RECREATE,
|
|
124
|
+
ResourceOperation.CREATE,
|
|
125
|
+
ResourceOperation.DESTROY,
|
|
126
|
+
];
|
|
127
|
+
const indexPrev = orderOfOperations.indexOf(prev);
|
|
128
|
+
const indexNext = orderOfOperations.indexOf(next);
|
|
129
|
+
return orderOfOperations[Math.max(indexPrev, indexNext)];
|
|
130
|
+
}
|
|
131
|
+
static isSame(desired, current, setting) {
|
|
132
|
+
switch (setting?.type) {
|
|
133
|
+
case 'stateful': {
|
|
134
|
+
const statefulSetting = setting.definition.getSettings();
|
|
135
|
+
return ChangeSet.isSame(desired, current, statefulSetting);
|
|
136
|
+
}
|
|
137
|
+
case 'array': {
|
|
138
|
+
const arrayParameter = setting;
|
|
139
|
+
return areArraysEqual(arrayParameter, desired, current);
|
|
140
|
+
}
|
|
141
|
+
default: {
|
|
142
|
+
return (setting?.isEqual ?? ((a, b) => a === b))(desired, current);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { Plan } from './plan.js';
|
|
3
|
+
/**
|
|
4
|
+
* A narrower type for plans for CREATE operations. Only desiredConfig is not null.
|
|
5
|
+
*/
|
|
6
|
+
export interface CreatePlan<T extends StringIndexedObject> extends Plan<T> {
|
|
7
|
+
desiredConfig: T;
|
|
8
|
+
currentConfig: null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A narrower type for plans for DESTROY operations. Only currentConfig is not null.
|
|
12
|
+
*/
|
|
13
|
+
export interface DestroyPlan<T extends StringIndexedObject> extends Plan<T> {
|
|
14
|
+
desiredConfig: null;
|
|
15
|
+
currentConfig: T;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A narrower type for plans for MODIFY and RE-CREATE operations.
|
|
19
|
+
*/
|
|
20
|
+
export interface ModifyPlan<T extends StringIndexedObject> extends Plan<T> {
|
|
21
|
+
desiredConfig: T;
|
|
22
|
+
currentConfig: T;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ApplyRequestData, PlanResponseData, ResourceConfig, StringIndexedObject } from 'codify-schemas';
|
|
2
|
+
import { ParsedResourceSettings } from '../resource/parsed-resource-settings.js';
|
|
3
|
+
import { ChangeSet } from './change-set.js';
|
|
4
|
+
/**
|
|
5
|
+
* A plan represents a set of actions that after taken will turn the current resource into the desired one.
|
|
6
|
+
* A plan consists of list of parameter level changes (ADD, REMOVE, MODIFY or NO-OP) as well as a resource level
|
|
7
|
+
* operation (CREATE, DESTROY, MODIFY, RE-CREATE, NO-OP).
|
|
8
|
+
*/
|
|
9
|
+
export declare class Plan<T extends StringIndexedObject> {
|
|
10
|
+
id: string;
|
|
11
|
+
changeSet: ChangeSet<T>;
|
|
12
|
+
coreParameters: ResourceConfig;
|
|
13
|
+
constructor(id: string, changeSet: ChangeSet<T>, resourceMetadata: ResourceConfig);
|
|
14
|
+
/**
|
|
15
|
+
* The desired config that a plan will achieve after executing all the actions.
|
|
16
|
+
*/
|
|
17
|
+
get desiredConfig(): T | null;
|
|
18
|
+
/**
|
|
19
|
+
* The current config that the plan is changing.
|
|
20
|
+
*/
|
|
21
|
+
get currentConfig(): T | null;
|
|
22
|
+
/**
|
|
23
|
+
* When multiples of the same resource are allowed, this matching function will match a given config with one of the
|
|
24
|
+
* existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
|
|
25
|
+
* the application name and location to match it to our desired configs name and location.
|
|
26
|
+
*
|
|
27
|
+
* @param params
|
|
28
|
+
* @private
|
|
29
|
+
*/
|
|
30
|
+
private static matchCurrentParameters;
|
|
31
|
+
/**
|
|
32
|
+
* The type (id) of the resource
|
|
33
|
+
*
|
|
34
|
+
* @return string
|
|
35
|
+
*/
|
|
36
|
+
getResourceType(): string;
|
|
37
|
+
static calculate<T extends StringIndexedObject>(params: {
|
|
38
|
+
desiredParameters: Partial<T> | null;
|
|
39
|
+
currentParametersArray: Partial<T>[] | null;
|
|
40
|
+
stateParameters: Partial<T> | null;
|
|
41
|
+
coreParameters: ResourceConfig;
|
|
42
|
+
settings: ParsedResourceSettings<T>;
|
|
43
|
+
statefulMode: boolean;
|
|
44
|
+
}): Plan<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Only keep relevant params for the plan. We don't want to change settings that were not already
|
|
47
|
+
* defined.
|
|
48
|
+
*
|
|
49
|
+
* 1. In stateless mode, filter current by desired. We only want to know about settings that the user has specified
|
|
50
|
+
* 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
|
|
51
|
+
* or wants to set. If a parameter is not specified then it's not managed by Codify.
|
|
52
|
+
*/
|
|
53
|
+
private static filterCurrentParams;
|
|
54
|
+
static fromResponse<T extends ResourceConfig>(data: ApplyRequestData['plan'], defaultValues?: Partial<Record<keyof T, unknown>>): Plan<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Convert the plan to a JSON response object
|
|
57
|
+
*/
|
|
58
|
+
toResponse(): PlanResponseData;
|
|
59
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
|
|
2
|
+
import { v4 as uuidV4 } from 'uuid';
|
|
3
|
+
import { ChangeSet } from './change-set.js';
|
|
4
|
+
/**
|
|
5
|
+
* A plan represents a set of actions that after taken will turn the current resource into the desired one.
|
|
6
|
+
* A plan consists of list of parameter level changes (ADD, REMOVE, MODIFY or NO-OP) as well as a resource level
|
|
7
|
+
* operation (CREATE, DESTROY, MODIFY, RE-CREATE, NO-OP).
|
|
8
|
+
*/
|
|
9
|
+
export class Plan {
|
|
10
|
+
id;
|
|
11
|
+
changeSet;
|
|
12
|
+
coreParameters;
|
|
13
|
+
constructor(id, changeSet, resourceMetadata) {
|
|
14
|
+
this.id = id;
|
|
15
|
+
this.changeSet = changeSet;
|
|
16
|
+
this.coreParameters = resourceMetadata;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The desired config that a plan will achieve after executing all the actions.
|
|
20
|
+
*/
|
|
21
|
+
get desiredConfig() {
|
|
22
|
+
if (this.changeSet.operation === ResourceOperation.DESTROY) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
...this.coreParameters,
|
|
27
|
+
...this.changeSet.desiredParameters,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The current config that the plan is changing.
|
|
32
|
+
*/
|
|
33
|
+
get currentConfig() {
|
|
34
|
+
if (this.changeSet.operation === ResourceOperation.CREATE) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
...this.coreParameters,
|
|
39
|
+
...this.changeSet.currentParameters,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* When multiples of the same resource are allowed, this matching function will match a given config with one of the
|
|
44
|
+
* existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use
|
|
45
|
+
* the application name and location to match it to our desired configs name and location.
|
|
46
|
+
*
|
|
47
|
+
* @param params
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
static matchCurrentParameters(params) {
|
|
51
|
+
const { desiredParameters, currentParametersArray, stateParameters, settings, statefulMode } = params;
|
|
52
|
+
if (!settings.allowMultiple) {
|
|
53
|
+
return currentParametersArray?.[0] ?? null;
|
|
54
|
+
}
|
|
55
|
+
if (!currentParametersArray) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (statefulMode) {
|
|
59
|
+
return stateParameters
|
|
60
|
+
? settings.allowMultiple.matcher(stateParameters, currentParametersArray)
|
|
61
|
+
: null;
|
|
62
|
+
}
|
|
63
|
+
return settings.allowMultiple.matcher(desiredParameters, currentParametersArray);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The type (id) of the resource
|
|
67
|
+
*
|
|
68
|
+
* @return string
|
|
69
|
+
*/
|
|
70
|
+
getResourceType() {
|
|
71
|
+
return this.coreParameters.type;
|
|
72
|
+
}
|
|
73
|
+
static calculate(params) {
|
|
74
|
+
const { desiredParameters, currentParametersArray, stateParameters, coreParameters, settings, statefulMode } = params;
|
|
75
|
+
const currentParameters = Plan.matchCurrentParameters({
|
|
76
|
+
desiredParameters,
|
|
77
|
+
currentParametersArray,
|
|
78
|
+
stateParameters,
|
|
79
|
+
settings,
|
|
80
|
+
statefulMode
|
|
81
|
+
});
|
|
82
|
+
const filteredCurrentParameters = Plan.filterCurrentParams({
|
|
83
|
+
desiredParameters,
|
|
84
|
+
currentParameters,
|
|
85
|
+
stateParameters,
|
|
86
|
+
settings,
|
|
87
|
+
statefulMode
|
|
88
|
+
});
|
|
89
|
+
// Empty
|
|
90
|
+
if (!filteredCurrentParameters && !desiredParameters) {
|
|
91
|
+
return new Plan(uuidV4(), ChangeSet.empty(), coreParameters);
|
|
92
|
+
}
|
|
93
|
+
// CREATE
|
|
94
|
+
if (!filteredCurrentParameters && desiredParameters) {
|
|
95
|
+
return new Plan(uuidV4(), ChangeSet.create(desiredParameters), coreParameters);
|
|
96
|
+
}
|
|
97
|
+
// DESTROY
|
|
98
|
+
if (filteredCurrentParameters && !desiredParameters) {
|
|
99
|
+
return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters), coreParameters);
|
|
100
|
+
}
|
|
101
|
+
// NO-OP, MODIFY or RE-CREATE
|
|
102
|
+
const changeSet = ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
|
|
103
|
+
return new Plan(uuidV4(), changeSet, coreParameters);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Only keep relevant params for the plan. We don't want to change settings that were not already
|
|
107
|
+
* defined.
|
|
108
|
+
*
|
|
109
|
+
* 1. In stateless mode, filter current by desired. We only want to know about settings that the user has specified
|
|
110
|
+
* 2. In stateful mode, filter current by state and desired. We only know about the settings the user has previously set
|
|
111
|
+
* or wants to set. If a parameter is not specified then it's not managed by Codify.
|
|
112
|
+
*/
|
|
113
|
+
static filterCurrentParams(params) {
|
|
114
|
+
const { desiredParameters: desired, currentParameters: current, stateParameters: state, settings, statefulMode } = params;
|
|
115
|
+
if (!current) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const filteredCurrent = filterCurrent();
|
|
119
|
+
if (!filteredCurrent) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
// For stateful mode, we're done after filtering by the keys of desired + state. Stateless mode
|
|
123
|
+
// requires additional filtering for stateful parameter arrays and objects.
|
|
124
|
+
if (statefulMode) {
|
|
125
|
+
return filteredCurrent;
|
|
126
|
+
}
|
|
127
|
+
// TODO: Add object handling here in addition to arrays in the future
|
|
128
|
+
const arrayStatefulParameters = Object.fromEntries(Object.entries(filteredCurrent)
|
|
129
|
+
.filter(([k, v]) => isArrayStatefulParameter(k, v))
|
|
130
|
+
.map(([k, v]) => [k, filterArrayStatefulParameter(k, v)]));
|
|
131
|
+
return { ...filteredCurrent, ...arrayStatefulParameters };
|
|
132
|
+
function filterCurrent() {
|
|
133
|
+
if (!current) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (statefulMode) {
|
|
137
|
+
const keys = new Set([...Object.keys(state ?? {}), ...Object.keys(desired ?? {})]);
|
|
138
|
+
return Object.fromEntries(Object.entries(current)
|
|
139
|
+
.filter(([k]) => keys.has(k)));
|
|
140
|
+
}
|
|
141
|
+
// Stateless mode
|
|
142
|
+
const keys = new Set(Object.keys(desired ?? {}));
|
|
143
|
+
return Object.fromEntries(Object.entries(current)
|
|
144
|
+
.filter(([k]) => keys.has(k)));
|
|
145
|
+
}
|
|
146
|
+
function isArrayStatefulParameter(k, v) {
|
|
147
|
+
return settings.parameterSettings?.[k]?.type === 'stateful'
|
|
148
|
+
&& settings.parameterSettings[k].definition.getSettings().type === 'array'
|
|
149
|
+
&& Array.isArray(v);
|
|
150
|
+
}
|
|
151
|
+
function filterArrayStatefulParameter(k, v) {
|
|
152
|
+
const desiredArray = desired[k];
|
|
153
|
+
const matcher = settings.parameterSettings[k]
|
|
154
|
+
.definition
|
|
155
|
+
.getSettings()
|
|
156
|
+
.isElementEqual;
|
|
157
|
+
return v.filter((cv) => desiredArray.find((dv) => (matcher ?? ((a, b) => a === b))(dv, cv)));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// TODO: This needs to be revisited. I don't think this is valid anymore.
|
|
161
|
+
// 1. For all scenarios, there shouldn't be an apply without a plan beforehand
|
|
162
|
+
// 2. Even if there was (maybe for testing reasons), the plan values should not be adjusted
|
|
163
|
+
static fromResponse(data, defaultValues) {
|
|
164
|
+
if (!data) {
|
|
165
|
+
throw new Error('Data is empty');
|
|
166
|
+
}
|
|
167
|
+
addDefaultValues();
|
|
168
|
+
return new Plan(uuidV4(), new ChangeSet(data.operation, data.parameters), {
|
|
169
|
+
type: data.resourceType,
|
|
170
|
+
name: data.resourceName,
|
|
171
|
+
});
|
|
172
|
+
function addDefaultValues() {
|
|
173
|
+
Object.entries(defaultValues ?? {})
|
|
174
|
+
.forEach(([key, defaultValue]) => {
|
|
175
|
+
const configValueExists = data
|
|
176
|
+
.parameters
|
|
177
|
+
.some((p) => p.name === key);
|
|
178
|
+
// Only set default values if the value does not exist in the config
|
|
179
|
+
if (configValueExists) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
switch (data.operation) {
|
|
183
|
+
case ResourceOperation.CREATE: {
|
|
184
|
+
data.parameters.push({
|
|
185
|
+
name: key,
|
|
186
|
+
operation: ParameterOperation.ADD,
|
|
187
|
+
previousValue: null,
|
|
188
|
+
newValue: defaultValue,
|
|
189
|
+
});
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case ResourceOperation.DESTROY: {
|
|
193
|
+
data.parameters.push({
|
|
194
|
+
name: key,
|
|
195
|
+
operation: ParameterOperation.REMOVE,
|
|
196
|
+
previousValue: defaultValue,
|
|
197
|
+
newValue: null,
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case ResourceOperation.MODIFY:
|
|
202
|
+
case ResourceOperation.RECREATE:
|
|
203
|
+
case ResourceOperation.NOOP: {
|
|
204
|
+
data.parameters.push({
|
|
205
|
+
name: key,
|
|
206
|
+
operation: ParameterOperation.NOOP,
|
|
207
|
+
previousValue: defaultValue,
|
|
208
|
+
newValue: defaultValue,
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Convert the plan to a JSON response object
|
|
218
|
+
*/
|
|
219
|
+
toResponse() {
|
|
220
|
+
return {
|
|
221
|
+
planId: this.id,
|
|
222
|
+
operation: this.changeSet.operation,
|
|
223
|
+
resourceName: this.coreParameters.name,
|
|
224
|
+
resourceType: this.coreParameters.type,
|
|
225
|
+
parameters: this.changeSet.parameterChanges,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ApplyRequestData, InitializeResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
|
|
2
|
+
import { Plan } from '../plan/plan.js';
|
|
3
|
+
import { Resource } from '../resource/resource.js';
|
|
4
|
+
import { ResourceController } from '../resource/resource-controller.js';
|
|
5
|
+
export declare class Plugin {
|
|
6
|
+
name: string;
|
|
7
|
+
resourceControllers: Map<string, ResourceController<ResourceConfig>>;
|
|
8
|
+
planStorage: Map<string, Plan<any>>;
|
|
9
|
+
constructor(name: string, resourceControllers: Map<string, ResourceController<ResourceConfig>>);
|
|
10
|
+
static create(name: string, resources: Resource<any>[]): Plugin;
|
|
11
|
+
initialize(): Promise<InitializeResponseData>;
|
|
12
|
+
validate(data: ValidateRequestData): Promise<ValidateResponseData>;
|
|
13
|
+
plan(data: PlanRequestData): Promise<PlanResponseData>;
|
|
14
|
+
apply(data: ApplyRequestData): Promise<void>;
|
|
15
|
+
private resolvePlan;
|
|
16
|
+
protected crossValidateResources(configs: ResourceConfig[]): Promise<void>;
|
|
17
|
+
}
|