codify-plugin-lib 1.0.144 → 1.0.146
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/messages/handlers.js +6 -1
- package/dist/plan/plan.js +8 -24
- package/dist/plugin/plugin.d.ts +2 -1
- package/dist/plugin/plugin.js +25 -3
- package/dist/resource/parsed-resource-settings.d.ts +2 -1
- package/dist/resource/parsed-resource-settings.js +8 -6
- package/dist/resource/resource-settings.d.ts +2 -1
- package/dist/resource/resource-settings.js +34 -0
- package/package.json +2 -2
- package/src/messages/handlers.ts +7 -0
- package/src/plan/plan.ts +9 -30
- package/src/plugin/plugin.test.ts +91 -9
- package/src/plugin/plugin.ts +35 -4
- package/src/resource/parsed-resource-settings.ts +10 -6
- package/src/resource/resource-settings.test.ts +65 -0
- package/src/resource/resource-settings.ts +45 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Ajv } from 'ajv';
|
|
2
2
|
import addFormats from 'ajv-formats';
|
|
3
|
-
import { ApplyRequestDataSchema, ApplyResponseDataSchema, GetResourceInfoRequestDataSchema, GetResourceInfoResponseDataSchema, ImportRequestDataSchema, ImportResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, IpcMessageV2Schema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
|
|
3
|
+
import { ApplyRequestDataSchema, ApplyResponseDataSchema, GetResourceInfoRequestDataSchema, GetResourceInfoResponseDataSchema, ImportRequestDataSchema, ImportResponseDataSchema, InitializeRequestDataSchema, InitializeResponseDataSchema, IpcMessageSchema, IpcMessageV2Schema, MatchRequestDataSchema, MatchResponseDataSchema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, ResourceSchema, ValidateRequestDataSchema, ValidateResponseDataSchema } from 'codify-schemas';
|
|
4
4
|
import { SudoError } from '../errors.js';
|
|
5
5
|
const SupportedRequests = {
|
|
6
6
|
'initialize': {
|
|
@@ -18,6 +18,11 @@ const SupportedRequests = {
|
|
|
18
18
|
requestValidator: GetResourceInfoRequestDataSchema,
|
|
19
19
|
responseValidator: GetResourceInfoResponseDataSchema
|
|
20
20
|
},
|
|
21
|
+
'match': {
|
|
22
|
+
handler: async (plugin, data) => plugin.match(data),
|
|
23
|
+
requestValidator: MatchRequestDataSchema,
|
|
24
|
+
responseValidator: MatchResponseDataSchema
|
|
25
|
+
},
|
|
21
26
|
'import': {
|
|
22
27
|
handler: async (plugin, data) => plugin.import(data),
|
|
23
28
|
requestValidator: ImportRequestDataSchema,
|
package/dist/plan/plan.js
CHANGED
|
@@ -156,30 +156,14 @@ export class Plan {
|
|
|
156
156
|
if (!currentArray) {
|
|
157
157
|
return null;
|
|
158
158
|
}
|
|
159
|
-
const matcher
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (!currentParameter) {
|
|
168
|
-
console.warn(`Unable to find required parameter for current ${currentParameter}`);
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
if (!desiredParameter) {
|
|
172
|
-
console.warn(`Unable to find required parameter for current ${currentParameter}`);
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
return currentParameter === desiredParameter;
|
|
176
|
-
}));
|
|
177
|
-
if (matched.length > 1) {
|
|
178
|
-
console.warn(`Required parameters did not uniquely identify a resource: ${currentArray}. Defaulting to the first one`);
|
|
179
|
-
}
|
|
180
|
-
return matched[0];
|
|
181
|
-
})
|
|
182
|
-
: settings.allowMultiple.matcher;
|
|
159
|
+
const { matcher: parameterMatcher, id } = settings;
|
|
160
|
+
const matcher = (desired, currentArray) => {
|
|
161
|
+
const matched = currentArray.filter((c) => parameterMatcher(desired, c));
|
|
162
|
+
if (matched.length > 0) {
|
|
163
|
+
console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`);
|
|
164
|
+
}
|
|
165
|
+
return matched[0];
|
|
166
|
+
};
|
|
183
167
|
if (isStateful) {
|
|
184
168
|
return state
|
|
185
169
|
? matcher(state, currentArray) ?? null
|
package/dist/plugin/plugin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApplyRequestData, GetResourceInfoRequestData, GetResourceInfoResponseData, ImportRequestData, ImportResponseData, InitializeResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ResourceJson, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
|
|
1
|
+
import { ApplyRequestData, GetResourceInfoRequestData, GetResourceInfoResponseData, ImportRequestData, ImportResponseData, InitializeResponseData, MatchRequestData, MatchResponseData, PlanRequestData, PlanResponseData, ResourceConfig, ResourceJson, ValidateRequestData, ValidateResponseData } from 'codify-schemas';
|
|
2
2
|
import { Plan } from '../plan/plan.js';
|
|
3
3
|
import { BackgroundPty } from '../pty/background-pty.js';
|
|
4
4
|
import { Resource } from '../resource/resource.js';
|
|
@@ -12,6 +12,7 @@ export declare class Plugin {
|
|
|
12
12
|
static create(name: string, resources: Resource<any>[]): Plugin;
|
|
13
13
|
initialize(): Promise<InitializeResponseData>;
|
|
14
14
|
getResourceInfo(data: GetResourceInfoRequestData): Promise<GetResourceInfoResponseData>;
|
|
15
|
+
match(data: MatchRequestData): Promise<MatchResponseData>;
|
|
15
16
|
import(data: ImportRequestData): Promise<ImportResponseData>;
|
|
16
17
|
validate(data: ValidateRequestData): Promise<ValidateResponseData>;
|
|
17
18
|
plan(data: PlanRequestData): Promise<PlanResponseData>;
|
package/dist/plugin/plugin.js
CHANGED
|
@@ -42,9 +42,7 @@ export class Plugin {
|
|
|
42
42
|
?? schema?.required
|
|
43
43
|
?? undefined);
|
|
44
44
|
const allowMultiple = resource.settings.allowMultiple !== undefined
|
|
45
|
-
|
|
46
|
-
? { identifyingParameters: schema?.required ?? [] }
|
|
47
|
-
: { identifyingParameters: resource.settings.allowMultiple.identifyingParameters ?? schema?.required ?? [] }) : undefined;
|
|
45
|
+
&& resource.settings.allowMultiple !== false;
|
|
48
46
|
return {
|
|
49
47
|
plugin: this.name,
|
|
50
48
|
type: data.type,
|
|
@@ -60,6 +58,30 @@ export class Plugin {
|
|
|
60
58
|
allowMultiple
|
|
61
59
|
};
|
|
62
60
|
}
|
|
61
|
+
async match(data) {
|
|
62
|
+
const { resource: resourceConfig, array } = data;
|
|
63
|
+
const resource = this.resourceControllers.get(resourceConfig.core.type);
|
|
64
|
+
if (!resource) {
|
|
65
|
+
throw new Error(`Resource of type ${resourceConfig.core.type} could not be found for match`);
|
|
66
|
+
}
|
|
67
|
+
if (!resource.settings.allowMultiple) {
|
|
68
|
+
return { match: array.find((r) => r.core.type === resourceConfig.core.type) };
|
|
69
|
+
}
|
|
70
|
+
const parameterMatcher = resource?.parsedSettings.matcher;
|
|
71
|
+
const match = array.find((r) => {
|
|
72
|
+
if (resourceConfig.core.type !== r.core.type) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
// If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource
|
|
76
|
+
if (resourceConfig.core.name === r.core.name
|
|
77
|
+
&& resourceConfig.core.name
|
|
78
|
+
&& Number.isInteger(Number.parseInt(resourceConfig.core.name, 10))) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return parameterMatcher(resourceConfig.parameters, r.parameters);
|
|
82
|
+
});
|
|
83
|
+
return { match };
|
|
84
|
+
}
|
|
63
85
|
async import(data) {
|
|
64
86
|
const { core, parameters } = data;
|
|
65
87
|
if (!this.resourceControllers.has(core.type)) {
|
|
@@ -20,7 +20,7 @@ export declare class ParsedResourceSettings<T extends StringIndexedObject> imple
|
|
|
20
20
|
id: string;
|
|
21
21
|
schema?: Partial<JSONSchemaType<T | any>>;
|
|
22
22
|
allowMultiple?: {
|
|
23
|
-
matcher?: (desired: Partial<T>, current: Partial<T>
|
|
23
|
+
matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
24
24
|
requiredParameters?: string[];
|
|
25
25
|
} | boolean;
|
|
26
26
|
removeStatefulParametersBeforeDestroy?: boolean | undefined;
|
|
@@ -34,6 +34,7 @@ export declare class ParsedResourceSettings<T extends StringIndexedObject> imple
|
|
|
34
34
|
get defaultValues(): Partial<Record<keyof T, unknown>>;
|
|
35
35
|
get inputTransformations(): Partial<Record<keyof T, InputTransformation>>;
|
|
36
36
|
get statefulParameterOrder(): Map<keyof T, number>;
|
|
37
|
+
get matcher(): (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
37
38
|
private validateSettings;
|
|
38
39
|
private validateParameterEqualsFn;
|
|
39
40
|
private getFromCacheOrCreate;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StatefulParameterController } from '../stateful-parameter/stateful-parameter-controller.js';
|
|
2
|
-
import { resolveElementEqualsFn, resolveEqualsFn, resolveParameterTransformFn } from './resource-settings.js';
|
|
2
|
+
import { resolveElementEqualsFn, resolveEqualsFn, resolveMatcher, resolveParameterTransformFn } from './resource-settings.js';
|
|
3
3
|
export class ParsedResourceSettings {
|
|
4
4
|
cache = new Map();
|
|
5
5
|
id;
|
|
@@ -45,7 +45,6 @@ export class ParsedResourceSettings {
|
|
|
45
45
|
...v,
|
|
46
46
|
controller: spController,
|
|
47
47
|
nestedSettings: spController?.parsedSettings,
|
|
48
|
-
definition: undefined,
|
|
49
48
|
};
|
|
50
49
|
return [k, parsed];
|
|
51
50
|
}
|
|
@@ -101,6 +100,9 @@ export class ParsedResourceSettings {
|
|
|
101
100
|
return new Map(resultArray.map((key, idx) => [key, idx]));
|
|
102
101
|
});
|
|
103
102
|
}
|
|
103
|
+
get matcher() {
|
|
104
|
+
return resolveMatcher(this);
|
|
105
|
+
}
|
|
104
106
|
validateSettings() {
|
|
105
107
|
// validate parameter settings
|
|
106
108
|
if (this.settings.parameterSettings) {
|
|
@@ -111,10 +113,10 @@ export class ParsedResourceSettings {
|
|
|
111
113
|
this.validateParameterEqualsFn(v, k);
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
|
-
if (this.allowMultiple
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
116
|
+
// if (this.allowMultiple
|
|
117
|
+
// && Object.values(this.parameterSettings).some((v) => v.type === 'stateful')) {
|
|
118
|
+
// throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`)
|
|
119
|
+
// }
|
|
118
120
|
const schema = this.settings.schema;
|
|
119
121
|
if (!this.settings.importAndDestroy && (schema?.oneOf
|
|
120
122
|
&& Array.isArray(schema.oneOf)
|
|
@@ -42,7 +42,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
42
42
|
*
|
|
43
43
|
* @return The matched resource.
|
|
44
44
|
*/
|
|
45
|
-
matcher?: (desired: Partial<T>, current: Partial<T>
|
|
45
|
+
matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
46
46
|
} | boolean;
|
|
47
47
|
/**
|
|
48
48
|
* If true, {@link StatefulParameter} remove() will be called before resource destruction. This is useful
|
|
@@ -255,3 +255,4 @@ export declare function resolveEqualsFn(parameter: ParameterSetting): (desired:
|
|
|
255
255
|
export declare function resolveElementEqualsFn(parameter: ArrayParameterSetting): (desired: unknown, current: unknown) => boolean;
|
|
256
256
|
export declare function resolveFnFromEqualsFnOrString(fnOrString: ((a: unknown, b: unknown) => boolean) | ParameterSettingType | undefined): ((a: unknown, b: unknown) => boolean) | undefined;
|
|
257
257
|
export declare function resolveParameterTransformFn(parameter: ParameterSetting): InputTransformation | undefined;
|
|
258
|
+
export declare function resolveMatcher<T extends StringIndexedObject>(settings: ResourceSettings<T>): (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
@@ -85,3 +85,37 @@ export function resolveParameterTransformFn(parameter) {
|
|
|
85
85
|
}
|
|
86
86
|
return parameter.transformation ?? ParameterTransformationDefaults[parameter.type] ?? undefined;
|
|
87
87
|
}
|
|
88
|
+
export function resolveMatcher(settings) {
|
|
89
|
+
return typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple?.matcher
|
|
90
|
+
? ((desired, current) => {
|
|
91
|
+
if (!desired || !current) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (!settings.allowMultiple) {
|
|
95
|
+
throw new Error(`Matching only works when allow multiple is enabled. Type: ${settings.id}`);
|
|
96
|
+
}
|
|
97
|
+
const requiredParameters = typeof settings.allowMultiple === 'object'
|
|
98
|
+
? settings.allowMultiple?.identifyingParameters ?? settings.schema?.required ?? []
|
|
99
|
+
: settings.schema?.required ?? [];
|
|
100
|
+
return requiredParameters.every((key) => {
|
|
101
|
+
const currentParameter = current[key];
|
|
102
|
+
const desiredParameter = desired[key];
|
|
103
|
+
// If both desired and current don't have a certain parameter then we assume they are the same
|
|
104
|
+
if (!currentParameter && !desiredParameter) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (!currentParameter) {
|
|
108
|
+
console.warn(`Unable to find required parameter for current ${currentParameter}`);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (!desiredParameter) {
|
|
112
|
+
console.warn(`Unable to find required parameter for current ${currentParameter}`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const parameterSetting = settings.parameterSettings?.[key];
|
|
116
|
+
const isEq = parameterSetting ? resolveEqualsFn(parameterSetting) : null;
|
|
117
|
+
return isEq?.(desiredParameter, currentParameter) ?? currentParameter === desiredParameter;
|
|
118
|
+
});
|
|
119
|
+
})
|
|
120
|
+
: settings.allowMultiple.matcher;
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codify-plugin-lib",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.146",
|
|
4
4
|
"description": "Library plugin library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "dist/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"ajv": "^8.12.0",
|
|
18
18
|
"ajv-formats": "^2.1.1",
|
|
19
|
-
"codify-schemas": "1.0.
|
|
19
|
+
"codify-schemas": "1.0.73",
|
|
20
20
|
"@npmcli/promise-spawn": "^7.0.1",
|
|
21
21
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5",
|
|
22
22
|
"uuid": "^10.0.0",
|
package/src/messages/handlers.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
IpcMessageSchema,
|
|
14
14
|
IpcMessageV2,
|
|
15
15
|
IpcMessageV2Schema,
|
|
16
|
+
MatchRequestDataSchema,
|
|
17
|
+
MatchResponseDataSchema,
|
|
16
18
|
MessageStatus,
|
|
17
19
|
PlanRequestDataSchema,
|
|
18
20
|
PlanResponseDataSchema,
|
|
@@ -40,6 +42,11 @@ const SupportedRequests: Record<string, { handler: (plugin: Plugin, data: any) =
|
|
|
40
42
|
requestValidator: GetResourceInfoRequestDataSchema,
|
|
41
43
|
responseValidator: GetResourceInfoResponseDataSchema
|
|
42
44
|
},
|
|
45
|
+
'match': {
|
|
46
|
+
handler: async (plugin: Plugin, data: any) => plugin.match(data),
|
|
47
|
+
requestValidator: MatchRequestDataSchema,
|
|
48
|
+
responseValidator: MatchResponseDataSchema
|
|
49
|
+
},
|
|
43
50
|
'import': {
|
|
44
51
|
handler: async (plugin: Plugin, data: any) => plugin.import(data),
|
|
45
52
|
requestValidator: ImportRequestDataSchema,
|
package/src/plan/plan.ts
CHANGED
|
@@ -241,7 +241,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
241
241
|
desired: Partial<T> | null,
|
|
242
242
|
currentArray: Partial<T>[] | null,
|
|
243
243
|
state: Partial<T> | null,
|
|
244
|
-
settings:
|
|
244
|
+
settings: ParsedResourceSettings<T>,
|
|
245
245
|
isStateful: boolean,
|
|
246
246
|
}): Partial<T> | null {
|
|
247
247
|
const {
|
|
@@ -260,36 +260,15 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
260
260
|
return null;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
const matcher
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const matched = currentArr.filter((c) => requiredParameters.every((key) => {
|
|
270
|
-
const currentParameter = c[key];
|
|
271
|
-
const desiredParameter = desired[key];
|
|
272
|
-
|
|
273
|
-
if (!currentParameter) {
|
|
274
|
-
console.warn(`Unable to find required parameter for current ${currentParameter}`)
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (!desiredParameter) {
|
|
279
|
-
console.warn(`Unable to find required parameter for current ${currentParameter}`)
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return currentParameter === desiredParameter;
|
|
284
|
-
}))
|
|
285
|
-
|
|
286
|
-
if (matched.length > 1) {
|
|
287
|
-
console.warn(`Required parameters did not uniquely identify a resource: ${currentArray}. Defaulting to the first one`);
|
|
288
|
-
}
|
|
263
|
+
const { matcher: parameterMatcher, id } = settings;
|
|
264
|
+
const matcher = (desired: Partial<T>, currentArray: Partial<T>[]): Partial<T> | undefined => {
|
|
265
|
+
const matched = currentArray.filter((c) => parameterMatcher(desired, c))
|
|
266
|
+
if (matched.length > 0) {
|
|
267
|
+
console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`)
|
|
268
|
+
}
|
|
289
269
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
: settings.allowMultiple.matcher
|
|
270
|
+
return matched[0];
|
|
271
|
+
}
|
|
293
272
|
|
|
294
273
|
if (isStateful) {
|
|
295
274
|
return state
|
|
@@ -5,7 +5,7 @@ import { Resource } from '../resource/resource.js';
|
|
|
5
5
|
import { Plan } from '../plan/plan.js';
|
|
6
6
|
import { spy } from 'sinon';
|
|
7
7
|
import { ResourceSettings } from '../resource/resource-settings.js';
|
|
8
|
-
import { TestConfig } from '../utils/test-utils.test.js';
|
|
8
|
+
import { TestConfig, TestStatefulParameter } from '../utils/test-utils.test.js';
|
|
9
9
|
import { getPty } from '../pty/index.js';
|
|
10
10
|
|
|
11
11
|
interface TestConfig extends StringIndexedObject {
|
|
@@ -343,27 +343,109 @@ describe('Plugin tests', () => {
|
|
|
343
343
|
type: 'testResource',
|
|
344
344
|
})
|
|
345
345
|
|
|
346
|
-
expect(resourceInfo.allowMultiple
|
|
347
|
-
'path', 'paths'
|
|
348
|
-
])
|
|
346
|
+
expect(resourceInfo.allowMultiple).to.be.true;
|
|
349
347
|
})
|
|
350
348
|
|
|
351
|
-
it('
|
|
349
|
+
it('Can match resources together', async () => {
|
|
352
350
|
const resource = spy(new class extends TestResource {
|
|
353
351
|
getSettings(): ResourceSettings<TestConfig> {
|
|
354
352
|
return {
|
|
355
353
|
...super.getSettings(),
|
|
356
|
-
|
|
354
|
+
parameterSettings: {
|
|
355
|
+
path: { type: 'directory' },
|
|
356
|
+
paths: { type: 'array', itemType: 'directory' }
|
|
357
|
+
},
|
|
358
|
+
allowMultiple: {
|
|
359
|
+
identifyingParameters: ['path', 'paths']
|
|
360
|
+
}
|
|
357
361
|
}
|
|
358
362
|
}
|
|
359
363
|
})
|
|
360
364
|
|
|
361
365
|
const testPlugin = Plugin.create('testPlugin', [resource as any]);
|
|
362
366
|
|
|
363
|
-
const
|
|
364
|
-
|
|
367
|
+
const { match } = await testPlugin.match({
|
|
368
|
+
resource: {
|
|
369
|
+
core: { type: 'testResource' },
|
|
370
|
+
parameters: { path: '/my/path', propA: 'abc' },
|
|
371
|
+
},
|
|
372
|
+
array: [
|
|
373
|
+
{
|
|
374
|
+
core: { type: 'testResource' },
|
|
375
|
+
parameters: { path: '/my/other/path', propA: 'abc' },
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
core: { type: 'testResource' },
|
|
379
|
+
parameters: { paths: ['/my/path'], propA: 'def' },
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
core: { type: 'testResource' },
|
|
383
|
+
parameters: { path: '/my/path', propA: 'hig' },
|
|
384
|
+
},
|
|
385
|
+
]
|
|
386
|
+
})
|
|
387
|
+
expect(match).toMatchObject({
|
|
388
|
+
core: { type: 'testResource' },
|
|
389
|
+
parameters: { path: '/my/path', propA: 'hig' },
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
const match2 = await testPlugin.match({
|
|
393
|
+
resource: {
|
|
394
|
+
core: { type: 'testResource' },
|
|
395
|
+
parameters: { path: '/my/path', propA: 'abc' },
|
|
396
|
+
},
|
|
397
|
+
array: []
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
expect(match2).toMatchObject({
|
|
401
|
+
match: undefined,
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('Can match resources together 2', { timeout: 3000000 }, async () => {
|
|
406
|
+
const resource = spy(new class extends TestResource {
|
|
407
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
408
|
+
return {
|
|
409
|
+
id: 'ssh-config',
|
|
410
|
+
parameterSettings: {
|
|
411
|
+
hosts: { type: 'stateful', definition: new TestStatefulParameter() }
|
|
412
|
+
},
|
|
413
|
+
importAndDestroy: {
|
|
414
|
+
refreshKeys: ['hosts'],
|
|
415
|
+
defaultRefreshValues: { hosts: [] },
|
|
416
|
+
requiredParameters: []
|
|
417
|
+
},
|
|
418
|
+
dependencies: ['ssh-key'],
|
|
419
|
+
allowMultiple: {
|
|
420
|
+
matcher: (a, b) => a.hosts === b.hosts
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const testPlugin = Plugin.create('testPlugin', [resource as any]);
|
|
427
|
+
|
|
428
|
+
const { match } = await testPlugin.match({
|
|
429
|
+
resource: {
|
|
430
|
+
core: { type: 'ssh-config' },
|
|
431
|
+
parameters: { hosts: 'a' },
|
|
432
|
+
},
|
|
433
|
+
array: [
|
|
434
|
+
{
|
|
435
|
+
core: { type: 'ssh-config' },
|
|
436
|
+
parameters: { hosts: 'b' },
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
core: { type: 'ssh-config' },
|
|
440
|
+
parameters: { hosts: 'a' },
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
core: { type: 'ssh-config' },
|
|
444
|
+
parameters: { hosts: 'c' },
|
|
445
|
+
},
|
|
446
|
+
]
|
|
365
447
|
})
|
|
366
448
|
|
|
367
|
-
|
|
449
|
+
console.log(match)
|
|
368
450
|
})
|
|
369
451
|
});
|
package/src/plugin/plugin.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
ImportRequestData,
|
|
7
7
|
ImportResponseData,
|
|
8
8
|
InitializeResponseData,
|
|
9
|
+
MatchRequestData,
|
|
10
|
+
MatchResponseData,
|
|
9
11
|
PlanRequestData,
|
|
10
12
|
PlanResponseData,
|
|
11
13
|
ResourceConfig,
|
|
@@ -73,10 +75,7 @@ export class Plugin {
|
|
|
73
75
|
) as any;
|
|
74
76
|
|
|
75
77
|
const allowMultiple = resource.settings.allowMultiple !== undefined
|
|
76
|
-
|
|
77
|
-
? { identifyingParameters: schema?.required ?? [] }
|
|
78
|
-
: { identifyingParameters: resource.settings.allowMultiple.identifyingParameters ?? schema?.required ?? [] }
|
|
79
|
-
) : undefined
|
|
78
|
+
&& resource.settings.allowMultiple !== false;
|
|
80
79
|
|
|
81
80
|
return {
|
|
82
81
|
plugin: this.name,
|
|
@@ -94,6 +93,38 @@ export class Plugin {
|
|
|
94
93
|
}
|
|
95
94
|
}
|
|
96
95
|
|
|
96
|
+
async match(data: MatchRequestData): Promise<MatchResponseData> {
|
|
97
|
+
const { resource: resourceConfig, array } = data;
|
|
98
|
+
|
|
99
|
+
const resource = this.resourceControllers.get(resourceConfig.core.type);
|
|
100
|
+
if (!resource) {
|
|
101
|
+
throw new Error(`Resource of type ${resourceConfig.core.type} could not be found for match`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!resource.settings.allowMultiple) {
|
|
105
|
+
return { match: array.find((r) => r.core.type === resourceConfig.core.type) }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const parameterMatcher = resource?.parsedSettings.matcher;
|
|
109
|
+
const match = array.find((r) => {
|
|
110
|
+
if (resourceConfig.core.type !== r.core.type) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource
|
|
115
|
+
if (resourceConfig.core.name === r.core.name
|
|
116
|
+
&& resourceConfig.core.name
|
|
117
|
+
&& Number.isInteger(Number.parseInt(resourceConfig.core.name, 10))
|
|
118
|
+
) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return parameterMatcher(resourceConfig.parameters, r.parameters);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return { match }
|
|
126
|
+
}
|
|
127
|
+
|
|
97
128
|
async import(data: ImportRequestData): Promise<ImportResponseData> {
|
|
98
129
|
const { core, parameters } = data;
|
|
99
130
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
ParameterSetting,
|
|
10
10
|
resolveElementEqualsFn,
|
|
11
11
|
resolveEqualsFn,
|
|
12
|
+
resolveMatcher,
|
|
12
13
|
resolveParameterTransformFn,
|
|
13
14
|
ResourceSettings,
|
|
14
15
|
StatefulParameterSetting
|
|
@@ -38,7 +39,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
38
39
|
id!: string;
|
|
39
40
|
schema?: Partial<JSONSchemaType<T | any>>;
|
|
40
41
|
allowMultiple?: {
|
|
41
|
-
matcher?: (desired: Partial<T>, current: Partial<T>
|
|
42
|
+
matcher?: (desired: Partial<T>, current: Partial<T>) => boolean;
|
|
42
43
|
requiredParameters?: string[]
|
|
43
44
|
} | boolean;
|
|
44
45
|
|
|
@@ -91,7 +92,6 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
91
92
|
...v,
|
|
92
93
|
controller: spController,
|
|
93
94
|
nestedSettings: spController?.parsedSettings,
|
|
94
|
-
definition: undefined,
|
|
95
95
|
};
|
|
96
96
|
|
|
97
97
|
return [k, parsed as ParsedStatefulParameterSetting];
|
|
@@ -172,6 +172,10 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
get matcher(): (desired: Partial<T>, current: Partial<T>) => boolean {
|
|
176
|
+
return resolveMatcher(this);
|
|
177
|
+
}
|
|
178
|
+
|
|
175
179
|
private validateSettings(): void {
|
|
176
180
|
// validate parameter settings
|
|
177
181
|
if (this.settings.parameterSettings) {
|
|
@@ -184,10 +188,10 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
190
|
|
|
187
|
-
if (this.allowMultiple
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
+
// if (this.allowMultiple
|
|
192
|
+
// && Object.values(this.parameterSettings).some((v) => v.type === 'stateful')) {
|
|
193
|
+
// throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`)
|
|
194
|
+
// }
|
|
191
195
|
|
|
192
196
|
const schema = this.settings.schema as JSONSchemaType<any>;
|
|
193
197
|
if (!this.settings.importAndDestroy && (schema?.oneOf
|
|
@@ -1054,4 +1054,69 @@ describe('Resource parameter tests', () => {
|
|
|
1054
1054
|
operation: ResourceOperation.NOOP,
|
|
1055
1055
|
})
|
|
1056
1056
|
})
|
|
1057
|
+
|
|
1058
|
+
it('Supports matching using the identfying parameters', async () => {
|
|
1059
|
+
const home = os.homedir()
|
|
1060
|
+
const testPath = path.join(home, 'test/folder');
|
|
1061
|
+
|
|
1062
|
+
const resource = new class extends TestResource {
|
|
1063
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
1064
|
+
return {
|
|
1065
|
+
id: 'resourceType',
|
|
1066
|
+
parameterSettings: {
|
|
1067
|
+
propA: { type: 'array', itemType: 'directory' }
|
|
1068
|
+
},
|
|
1069
|
+
allowMultiple: {
|
|
1070
|
+
identifyingParameters: ['propA']
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
const controller = new ResourceController(resource);
|
|
1077
|
+
expect(controller.parsedSettings.matcher({
|
|
1078
|
+
propA: [testPath],
|
|
1079
|
+
propB: 'random1',
|
|
1080
|
+
}, {
|
|
1081
|
+
propA: [testPath],
|
|
1082
|
+
propB: 'random2',
|
|
1083
|
+
})).to.be.true;
|
|
1084
|
+
|
|
1085
|
+
expect(controller.parsedSettings.matcher({
|
|
1086
|
+
propA: [testPath],
|
|
1087
|
+
propB: 'random1',
|
|
1088
|
+
}, {
|
|
1089
|
+
propA: [testPath, testPath],
|
|
1090
|
+
propB: 'random2',
|
|
1091
|
+
})).to.be.false;
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
it('Supports matching using custom matcher', async () => {
|
|
1095
|
+
const home = os.homedir()
|
|
1096
|
+
const testPath = path.join(home, 'test/folder');
|
|
1097
|
+
|
|
1098
|
+
const resource = new class extends TestResource {
|
|
1099
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
1100
|
+
return {
|
|
1101
|
+
id: 'resourceType',
|
|
1102
|
+
parameterSettings: {
|
|
1103
|
+
propA: { type: 'array', itemType: 'directory' }
|
|
1104
|
+
},
|
|
1105
|
+
allowMultiple: {
|
|
1106
|
+
identifyingParameters: ['propA'],
|
|
1107
|
+
matcher: () => false,
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
const controller = new ResourceController(resource);
|
|
1114
|
+
expect(controller.parsedSettings.matcher({
|
|
1115
|
+
propA: [testPath],
|
|
1116
|
+
propB: 'random1',
|
|
1117
|
+
}, {
|
|
1118
|
+
propA: [testPath],
|
|
1119
|
+
propB: 'random2',
|
|
1120
|
+
})).to.be.false;
|
|
1121
|
+
})
|
|
1057
1122
|
})
|
|
@@ -53,7 +53,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
53
53
|
*
|
|
54
54
|
* @return The matched resource.
|
|
55
55
|
*/
|
|
56
|
-
matcher?: (desired: Partial<T>, current: Partial<T>
|
|
56
|
+
matcher?: (desired: Partial<T>, current: Partial<T>) => boolean
|
|
57
57
|
} | boolean
|
|
58
58
|
|
|
59
59
|
/**
|
|
@@ -408,3 +408,47 @@ export function resolveParameterTransformFn(
|
|
|
408
408
|
|
|
409
409
|
return parameter.transformation ?? ParameterTransformationDefaults[parameter.type as ParameterSettingType] ?? undefined;
|
|
410
410
|
}
|
|
411
|
+
|
|
412
|
+
export function resolveMatcher<T extends StringIndexedObject>(
|
|
413
|
+
settings: ResourceSettings<T>
|
|
414
|
+
): (desired: Partial<T>, current: Partial<T>) => boolean {
|
|
415
|
+
return typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple?.matcher
|
|
416
|
+
? ((desired: Partial<T>, current: Partial<T>) => {
|
|
417
|
+
if (!desired || !current) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!settings.allowMultiple) {
|
|
422
|
+
throw new Error(`Matching only works when allow multiple is enabled. Type: ${settings.id}`)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const requiredParameters = typeof settings.allowMultiple === 'object'
|
|
426
|
+
? settings.allowMultiple?.identifyingParameters ?? (settings.schema?.required as string[]) ?? []
|
|
427
|
+
: (settings.schema?.required as string[]) ?? []
|
|
428
|
+
|
|
429
|
+
return requiredParameters.every((key) => {
|
|
430
|
+
const currentParameter = current[key];
|
|
431
|
+
const desiredParameter = desired[key];
|
|
432
|
+
|
|
433
|
+
// If both desired and current don't have a certain parameter then we assume they are the same
|
|
434
|
+
if (!currentParameter && !desiredParameter) {
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!currentParameter) {
|
|
439
|
+
console.warn(`Unable to find required parameter for current ${currentParameter}`)
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!desiredParameter) {
|
|
444
|
+
console.warn(`Unable to find required parameter for current ${currentParameter}`)
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const parameterSetting = settings.parameterSettings?.[key];
|
|
449
|
+
const isEq = parameterSetting ? resolveEqualsFn(parameterSetting) : null
|
|
450
|
+
return isEq?.(desiredParameter, currentParameter) ?? currentParameter === desiredParameter;
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
: settings.allowMultiple.matcher
|
|
454
|
+
}
|