codify-plugin-lib 1.0.98 → 1.0.100
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/plan/plan.js +13 -8
- package/dist/resource/parsed-resource-settings.js +20 -4
- package/dist/resource/resource-controller.js +4 -2
- package/dist/resource/resource-settings.d.ts +65 -1
- package/package.json +1 -1
- package/src/plan/plan.test.ts +73 -0
- package/src/plan/plan.ts +16 -8
- package/src/resource/parsed-resource-settings.test.ts +73 -5
- package/src/resource/parsed-resource-settings.ts +31 -5
- package/src/resource/resource-controller.ts +9 -7
- package/src/resource/resource-settings.test.ts +17 -0
- package/src/resource/resource-settings.ts +68 -1
- package/src/resource/stateful-parameter.test.ts +0 -2
package/dist/plan/plan.js
CHANGED
|
@@ -126,7 +126,7 @@ export class Plan {
|
|
|
126
126
|
}
|
|
127
127
|
// TODO: Add object handling here in addition to arrays in the future
|
|
128
128
|
const arrayStatefulParameters = Object.fromEntries(Object.entries(filteredCurrent)
|
|
129
|
-
.filter(([k, v]) =>
|
|
129
|
+
.filter(([k, v]) => isArrayParameterWithFiltering(k, v))
|
|
130
130
|
.map(([k, v]) => [k, filterArrayStatefulParameter(k, v)]));
|
|
131
131
|
return { ...filteredCurrent, ...arrayStatefulParameters };
|
|
132
132
|
function filterCurrent() {
|
|
@@ -143,18 +143,23 @@ export class Plan {
|
|
|
143
143
|
return Object.fromEntries(Object.entries(current)
|
|
144
144
|
.filter(([k]) => keys.has(k)));
|
|
145
145
|
}
|
|
146
|
-
function
|
|
147
|
-
return settings.parameterSettings?.[k]?.type === 'stateful'
|
|
148
|
-
&& settings.parameterSettings[k].definition.getSettings().type === 'array'
|
|
146
|
+
function isArrayParameterWithFiltering(k, v) {
|
|
147
|
+
return (((settings.parameterSettings?.[k]?.type === 'stateful'
|
|
148
|
+
&& settings.parameterSettings[k].definition.getSettings().type === 'array')
|
|
149
|
+
&& (settings.parameterSettings[k].definition.getSettings().filterInStatelessMode ?? true)) || (settings.parameterSettings?.[k]?.type === 'array'
|
|
150
|
+
&& ((settings.parameterSettings?.[k]).filterInStatelessMode ?? true)))
|
|
149
151
|
&& Array.isArray(v);
|
|
150
152
|
}
|
|
151
153
|
// For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
|
|
152
154
|
function filterArrayStatefulParameter(k, v) {
|
|
153
155
|
const desiredArray = desired[k];
|
|
154
|
-
const matcher = settings.parameterSettings[k]
|
|
155
|
-
.
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
const matcher = settings.parameterSettings[k].type === 'stateful'
|
|
157
|
+
? settings.parameterSettings[k]
|
|
158
|
+
.definition
|
|
159
|
+
.getSettings()
|
|
160
|
+
.isElementEqual ?? ((a, b) => a === b)
|
|
161
|
+
: settings.parameterSettings[k]
|
|
162
|
+
.isElementEqual ?? ((a, b) => a === b);
|
|
158
163
|
const desiredCopy = [...desiredArray];
|
|
159
164
|
const currentCopy = [...v];
|
|
160
165
|
const result = [];
|
|
@@ -115,10 +115,26 @@ export class ParsedResourceSettings {
|
|
|
115
115
|
'determine the prompt to ask users during imports. It can\'t parse which parameters are needed when ' +
|
|
116
116
|
'required is declared conditionally.');
|
|
117
117
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
if (this.settings.import) {
|
|
119
|
+
const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.import;
|
|
120
|
+
const requiredParametersNotInSchema = requiredParameters
|
|
121
|
+
?.filter((p) => schema && !(schema.properties[p]));
|
|
122
|
+
if (schema && requiredParametersNotInSchema && requiredParametersNotInSchema.length > 0) {
|
|
123
|
+
throw new Error(`The following properties were declared in settings.import.requiredParameters but were not found in the schema:
|
|
124
|
+
${JSON.stringify(requiredParametersNotInSchema, null, 2)}`);
|
|
125
|
+
}
|
|
126
|
+
const refreshKeyNotInSchema = refreshKeys
|
|
127
|
+
?.filter((k) => schema && !(schema.properties[k]));
|
|
128
|
+
if (schema && refreshKeyNotInSchema && refreshKeyNotInSchema.length > 0) {
|
|
129
|
+
throw new Error(`The following properties were declared in settings.import.refreshKeys but were not found in the schema:
|
|
130
|
+
${JSON.stringify(requiredParametersNotInSchema, null, 2)}`);
|
|
131
|
+
}
|
|
132
|
+
const refreshValueNotInRefreshKey = Object.entries(defaultRefreshValues ?? {})
|
|
133
|
+
.filter(([k]) => schema && !(schema.properties[k])).map(([k]) => k);
|
|
134
|
+
if (schema && refreshValueNotInRefreshKey.length > 0) {
|
|
135
|
+
throw new Error(`Properties declared in defaultRefreshValues must already be declared in refreshKeys:
|
|
136
|
+
${JSON.stringify(refreshValueNotInRefreshKey, null, 2)}`);
|
|
137
|
+
}
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
validateParameterEqualsFn(parameter, key) {
|
|
@@ -134,13 +134,15 @@ export class ResourceController {
|
|
|
134
134
|
this.addDefaultValues(config);
|
|
135
135
|
await this.applyTransformParameters(config);
|
|
136
136
|
// Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
|
|
137
|
-
const parametersToRefresh = this.settings.import?.
|
|
137
|
+
const parametersToRefresh = this.settings.import?.refreshKeys
|
|
138
138
|
? {
|
|
139
|
-
...Object.fromEntries(this.settings.import?.
|
|
139
|
+
...Object.fromEntries(this.settings.import?.refreshKeys.map((k) => [k, null])),
|
|
140
|
+
...this.settings.import?.defaultRefreshValues,
|
|
140
141
|
...config,
|
|
141
142
|
}
|
|
142
143
|
: {
|
|
143
144
|
...Object.fromEntries(this.getAllParameterKeys().map((k) => [k, null])),
|
|
145
|
+
...this.settings.import?.defaultRefreshValues,
|
|
144
146
|
...config,
|
|
145
147
|
};
|
|
146
148
|
// Parse data from the user supplied config
|
|
@@ -52,9 +52,54 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
52
52
|
* @param desired
|
|
53
53
|
*/
|
|
54
54
|
inputTransformation?: (desired: Partial<T>) => Promise<unknown> | unknown;
|
|
55
|
+
/**
|
|
56
|
+
* Customize the import behavior of the resource. By default, <code>codify import</code> will call `refresh()` with
|
|
57
|
+
* every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters
|
|
58
|
+
* in the schema and will prompt the user for these values before performing the import.
|
|
59
|
+
*
|
|
60
|
+
* <b>Example:</b><br>
|
|
61
|
+
* Resource `alias` with parameters
|
|
62
|
+
*
|
|
63
|
+
* ```
|
|
64
|
+
* { alias <b>(*required)</b>: string; value: string; }
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* When the user calls `codify import alias`, they will first be prompted to enter the value for `alias`. Refresh
|
|
68
|
+
* is then called with `refresh({ alias: 'user-input', value: null })`. The result returned to the user will then be:
|
|
69
|
+
*
|
|
70
|
+
* ```
|
|
71
|
+
* { type: 'alias', alias: 'user-input', value: 'git push' }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
55
74
|
import?: {
|
|
75
|
+
/**
|
|
76
|
+
* Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken
|
|
77
|
+
* from the JSON schema. The `requiredParameters` parameter must be declared if a complex required is declared in
|
|
78
|
+
* the schema (contains `oneOf`, `anyOf`, `allOf`, `if`, `then`, `else`).
|
|
79
|
+
* <br>
|
|
80
|
+
* The user will be prompted for the required parameters before the import starts. This is done because for most resources
|
|
81
|
+
* the required parameters change the behaviour of the refresh (for example for the `alias` resource, the `alias` parmaeter
|
|
82
|
+
* chooses which alias the resource is managing).
|
|
83
|
+
*
|
|
84
|
+
* See {@link import} for more information on how importing works.
|
|
85
|
+
*/
|
|
56
86
|
requiredParameters?: Array<Partial<keyof T>>;
|
|
57
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Customize which keys will be refreshed in the import. Typically, `refresh()` statements only refresh
|
|
89
|
+
* the parameters provided as the input. Use `refreshKeys` to control which parameter keys are passed in.
|
|
90
|
+
* <br>
|
|
91
|
+
* By default all parameters (except for {@link requiredParameters }) are passed in with the value `null`. The passed
|
|
92
|
+
* in value can be customized using {@link defaultRefreshValues}
|
|
93
|
+
*
|
|
94
|
+
* See {@link import} for more information on how importing works.
|
|
95
|
+
*/
|
|
96
|
+
refreshKeys?: Array<Partial<keyof T>>;
|
|
97
|
+
/**
|
|
98
|
+
* Customize the value that is passed into refresh when importing. This must only contain keys found in {@link refreshKeys}.
|
|
99
|
+
*
|
|
100
|
+
* See {@link import} for more information on how importing works.
|
|
101
|
+
*/
|
|
102
|
+
defaultRefreshValues?: Partial<T>;
|
|
58
103
|
};
|
|
59
104
|
}
|
|
60
105
|
/**
|
|
@@ -128,6 +173,25 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
|
128
173
|
* @return Return true if desired is equivalent to current.
|
|
129
174
|
*/
|
|
130
175
|
isElementEqual?: (desired: any, current: any) => boolean;
|
|
176
|
+
/**
|
|
177
|
+
* Filter the contents of the refreshed array by the desired. This way items currently on the system but not
|
|
178
|
+
* in desired don't show up in the plan.
|
|
179
|
+
*
|
|
180
|
+
* <b>For example, for the nvm resource:</b>
|
|
181
|
+
* <ul>
|
|
182
|
+
* <li>Desired (20.18.0, 18.9.0, 16.3.1)</li>
|
|
183
|
+
* <li>Current (20.18.0, 22.1.3, 12.1.0)</li>
|
|
184
|
+
* </ul>
|
|
185
|
+
*
|
|
186
|
+
* Without filtering the plan will be:
|
|
187
|
+
* (~20.18.0, +18.9.0, +16.3.1, -22.1.3, -12.1.0)<br>
|
|
188
|
+
* With filtering the plan is: (~20.18.0, +18.9.0, +16.3.1)
|
|
189
|
+
*
|
|
190
|
+
* As you can see, filtering prevents items currently installed on the system from being removed.
|
|
191
|
+
*
|
|
192
|
+
* Defaults to true.
|
|
193
|
+
*/
|
|
194
|
+
filterInStatelessMode?: boolean;
|
|
131
195
|
}
|
|
132
196
|
/**
|
|
133
197
|
* Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
|
package/package.json
CHANGED
package/src/plan/plan.test.ts
CHANGED
|
@@ -147,6 +147,79 @@ describe('Plan entity tests', () => {
|
|
|
147
147
|
operation: ResourceOperation.RECREATE
|
|
148
148
|
})
|
|
149
149
|
})
|
|
150
|
+
|
|
151
|
+
it('Filters array parameters in stateless mode (by default)', async () => {
|
|
152
|
+
const resource = new class extends TestResource {
|
|
153
|
+
getSettings(): ResourceSettings<any> {
|
|
154
|
+
return {
|
|
155
|
+
id: 'type',
|
|
156
|
+
parameterSettings: {
|
|
157
|
+
propZ: { type: 'array', isElementEqual: (a, b) => b.includes(a) }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async refresh(): Promise<Partial<any> | null> {
|
|
163
|
+
return {
|
|
164
|
+
propZ: [
|
|
165
|
+
'20.15.0',
|
|
166
|
+
'20.15.1'
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const controller = new ResourceController(resource);
|
|
173
|
+
const plan = await controller.plan({
|
|
174
|
+
propZ: ['20.15'],
|
|
175
|
+
} as any)
|
|
176
|
+
|
|
177
|
+
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('Doesn\'t filters array parameters if filtering is disabled', async () => {
|
|
181
|
+
const resource = new class extends TestResource {
|
|
182
|
+
getSettings(): ResourceSettings<any> {
|
|
183
|
+
return {
|
|
184
|
+
id: 'type',
|
|
185
|
+
parameterSettings: {
|
|
186
|
+
propZ: { type: 'array', canModify: true, isElementEqual: (a, b) => b.includes(a), filterInStatelessMode: false }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async refresh(): Promise<Partial<any> | null> {
|
|
192
|
+
return {
|
|
193
|
+
propZ: [
|
|
194
|
+
'20.15.0',
|
|
195
|
+
'20.15.1'
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const controller = new ResourceController(resource);
|
|
202
|
+
const plan = await controller.plan({
|
|
203
|
+
propZ: ['20.15'],
|
|
204
|
+
} as any)
|
|
205
|
+
|
|
206
|
+
expect(plan.changeSet).toMatchObject({
|
|
207
|
+
operation: ResourceOperation.MODIFY,
|
|
208
|
+
parameterChanges: expect.arrayContaining([
|
|
209
|
+
expect.objectContaining({
|
|
210
|
+
name: 'propZ',
|
|
211
|
+
previousValue: expect.arrayContaining([
|
|
212
|
+
'20.15.0',
|
|
213
|
+
'20.15.1'
|
|
214
|
+
]),
|
|
215
|
+
newValue: expect.arrayContaining([
|
|
216
|
+
'20.15'
|
|
217
|
+
]),
|
|
218
|
+
operation: 'modify'
|
|
219
|
+
})
|
|
220
|
+
])
|
|
221
|
+
})
|
|
222
|
+
})
|
|
150
223
|
})
|
|
151
224
|
|
|
152
225
|
function createTestResource() {
|
package/src/plan/plan.ts
CHANGED
|
@@ -220,7 +220,7 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
220
220
|
// TODO: Add object handling here in addition to arrays in the future
|
|
221
221
|
const arrayStatefulParameters = Object.fromEntries(
|
|
222
222
|
Object.entries(filteredCurrent)
|
|
223
|
-
.filter(([k, v]) =>
|
|
223
|
+
.filter(([k, v]) => isArrayParameterWithFiltering(k, v))
|
|
224
224
|
.map(([k, v]) => [k, filterArrayStatefulParameter(k, v)])
|
|
225
225
|
)
|
|
226
226
|
|
|
@@ -247,19 +247,27 @@ export class Plan<T extends StringIndexedObject> {
|
|
|
247
247
|
) as Partial<T>;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
function
|
|
251
|
-
return settings.parameterSettings?.[k]?.type === 'stateful'
|
|
252
|
-
|
|
250
|
+
function isArrayParameterWithFiltering(k: string, v: T[keyof T]): boolean {
|
|
251
|
+
return (((settings.parameterSettings?.[k]?.type === 'stateful'
|
|
252
|
+
&& (settings.parameterSettings[k] as StatefulParameterSetting).definition.getSettings().type === 'array')
|
|
253
|
+
&& (((settings.parameterSettings[k] as StatefulParameterSetting).definition.getSettings() as ArrayParameterSetting).filterInStatelessMode ?? true)
|
|
254
|
+
) || (
|
|
255
|
+
settings.parameterSettings?.[k]?.type === 'array'
|
|
256
|
+
&& ((settings.parameterSettings?.[k] as ArrayParameterSetting).filterInStatelessMode ?? true)
|
|
257
|
+
))
|
|
253
258
|
&& Array.isArray(v)
|
|
254
259
|
}
|
|
255
260
|
|
|
256
261
|
// For stateless mode, we must filter the current array so that the diff algorithm will not detect any deletes
|
|
257
262
|
function filterArrayStatefulParameter(k: string, v: unknown[]): unknown[] {
|
|
258
263
|
const desiredArray = desired![k] as unknown[];
|
|
259
|
-
const matcher =
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
const matcher = settings.parameterSettings![k]!.type === 'stateful'
|
|
265
|
+
? ((settings.parameterSettings![k] as StatefulParameterSetting)
|
|
266
|
+
.definition
|
|
267
|
+
.getSettings() as ArrayParameterSetting)
|
|
268
|
+
.isElementEqual ?? ((a, b) => a === b)
|
|
269
|
+
: (settings.parameterSettings![k] as ArrayParameterSetting)
|
|
270
|
+
.isElementEqual ?? ((a, b) => a === b)
|
|
263
271
|
|
|
264
272
|
const desiredCopy = [...desiredArray];
|
|
265
273
|
const currentCopy = [...v];
|
|
@@ -63,19 +63,16 @@ describe('Resource options parser tests', () => {
|
|
|
63
63
|
schema,
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
expect(() => new ParsedResourceSettings(option)).
|
|
66
|
+
expect(() => new ParsedResourceSettings(option)).toThrowError();
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
it('Throws an error when an import.requiredParameters is declared improperly', () => {
|
|
70
70
|
const schema = {
|
|
71
71
|
'$schema': 'http://json-schema.org/draft-07/schema',
|
|
72
|
-
'$id': 'https://www.codifycli.com/git-clone.json',
|
|
73
|
-
'title': 'Git-clone resource',
|
|
74
72
|
'type': 'object',
|
|
75
73
|
'properties': {
|
|
76
74
|
'remote': {
|
|
77
75
|
'type': 'string',
|
|
78
|
-
'description': 'Remote tracking url to clone repo from. Equivalent to repository and only one should be specified'
|
|
79
76
|
},
|
|
80
77
|
},
|
|
81
78
|
'additionalProperties': false,
|
|
@@ -89,6 +86,77 @@ describe('Resource options parser tests', () => {
|
|
|
89
86
|
}
|
|
90
87
|
}
|
|
91
88
|
|
|
92
|
-
expect(() => new ParsedResourceSettings(option)).
|
|
89
|
+
expect(() => new ParsedResourceSettings(option)).toThrowError();
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('Throws an error when an import.refreshKeys is declared improperly', () => {
|
|
93
|
+
const schema = {
|
|
94
|
+
'$schema': 'http://json-schema.org/draft-07/schema',
|
|
95
|
+
'type': 'object',
|
|
96
|
+
'properties': {
|
|
97
|
+
'remote': {
|
|
98
|
+
'type': 'string',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
'additionalProperties': false,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const option: ResourceSettings<TestConfig> = {
|
|
105
|
+
id: 'typeId',
|
|
106
|
+
schema,
|
|
107
|
+
import: {
|
|
108
|
+
refreshKeys: ['import-error']
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
expect(() => new ParsedResourceSettings(option)).toThrowError();
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('Doesn\'t throw an error when an import.refreshValues is declared properly', () => {
|
|
116
|
+
const schema = {
|
|
117
|
+
'$schema': 'http://json-schema.org/draft-07/schema',
|
|
118
|
+
'type': 'object',
|
|
119
|
+
'properties': {
|
|
120
|
+
'remote': {
|
|
121
|
+
'type': 'string',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
'additionalProperties': false,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const option: ResourceSettings<TestConfig> = {
|
|
128
|
+
id: 'typeId',
|
|
129
|
+
schema,
|
|
130
|
+
import: {
|
|
131
|
+
refreshKeys: ['remote'],
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
expect(() => new ParsedResourceSettings(option)).not.toThrowError()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('Throws an error if defaultRefreshValue is not found in refreshKeys', () => {
|
|
139
|
+
const schema = {
|
|
140
|
+
'$schema': 'http://json-schema.org/draft-07/schema',
|
|
141
|
+
'type': 'object',
|
|
142
|
+
'properties': {
|
|
143
|
+
'remote': {
|
|
144
|
+
'type': 'string',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
'additionalProperties': false,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const option: ResourceSettings<TestConfig> = {
|
|
151
|
+
id: 'typeId',
|
|
152
|
+
schema,
|
|
153
|
+
import: {
|
|
154
|
+
defaultRefreshValues: {
|
|
155
|
+
repository: 'abc'
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
expect(() => new ParsedResourceSettings(option)).toThrowError()
|
|
93
161
|
})
|
|
94
162
|
})
|
|
@@ -167,11 +167,37 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
|
|
|
167
167
|
)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
this.settings.import
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
if (this.settings.import) {
|
|
171
|
+
const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.import;
|
|
172
|
+
|
|
173
|
+
const requiredParametersNotInSchema = requiredParameters
|
|
174
|
+
?.filter(
|
|
175
|
+
(p) => schema && !(schema.properties[p])
|
|
176
|
+
)
|
|
177
|
+
if (schema && requiredParametersNotInSchema && requiredParametersNotInSchema.length > 0) {
|
|
178
|
+
throw new Error(`The following properties were declared in settings.import.requiredParameters but were not found in the schema:
|
|
179
|
+
${JSON.stringify(requiredParametersNotInSchema, null, 2)}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const refreshKeyNotInSchema = refreshKeys
|
|
183
|
+
?.filter(
|
|
184
|
+
(k) => schema && !(schema.properties[k])
|
|
185
|
+
)
|
|
186
|
+
if (schema && refreshKeyNotInSchema && refreshKeyNotInSchema.length > 0) {
|
|
187
|
+
throw new Error(`The following properties were declared in settings.import.refreshKeys but were not found in the schema:
|
|
188
|
+
${JSON.stringify(requiredParametersNotInSchema, null, 2)}`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const refreshValueNotInRefreshKey =
|
|
192
|
+
Object.entries(defaultRefreshValues ?? {})
|
|
193
|
+
.filter(
|
|
194
|
+
([k]) => schema && !(schema.properties[k])
|
|
195
|
+
).map(([k]) => k)
|
|
196
|
+
|
|
197
|
+
if (schema && refreshValueNotInRefreshKey.length > 0) {
|
|
198
|
+
throw new Error(`Properties declared in defaultRefreshValues must already be declared in refreshKeys:
|
|
199
|
+
${JSON.stringify(refreshValueNotInRefreshKey, null, 2)}`)
|
|
200
|
+
}
|
|
175
201
|
}
|
|
176
202
|
}
|
|
177
203
|
|
|
@@ -190,19 +190,21 @@ export class ResourceController<T extends StringIndexedObject> {
|
|
|
190
190
|
await this.applyTransformParameters(config);
|
|
191
191
|
|
|
192
192
|
// Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here
|
|
193
|
-
const parametersToRefresh = this.settings.import?.
|
|
193
|
+
const parametersToRefresh = this.settings.import?.refreshKeys
|
|
194
194
|
? {
|
|
195
195
|
...Object.fromEntries(
|
|
196
|
-
this.settings.import?.
|
|
196
|
+
this.settings.import?.refreshKeys.map((k) => [k, null])
|
|
197
197
|
),
|
|
198
|
+
...this.settings.import?.defaultRefreshValues,
|
|
198
199
|
...config,
|
|
199
200
|
}
|
|
200
201
|
: {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
...Object.fromEntries(
|
|
203
|
+
this.getAllParameterKeys().map((k) => [k, null])
|
|
204
|
+
),
|
|
205
|
+
...this.settings.import?.defaultRefreshValues,
|
|
206
|
+
...config,
|
|
207
|
+
};
|
|
206
208
|
|
|
207
209
|
// Parse data from the user supplied config
|
|
208
210
|
const parsedConfig = new ConfigParser(parametersToRefresh, null, this.parsedSettings.statefulParameters)
|
|
@@ -612,4 +612,21 @@ describe('Resource parameter tests', () => {
|
|
|
612
612
|
|
|
613
613
|
expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
|
|
614
614
|
})
|
|
615
|
+
|
|
616
|
+
it('Accepts an input parameters for imports', () => {
|
|
617
|
+
const resource = new class extends TestResource {
|
|
618
|
+
getSettings(): ResourceSettings<TestConfig> {
|
|
619
|
+
return {
|
|
620
|
+
id: 'resourceType',
|
|
621
|
+
import: {
|
|
622
|
+
requiredParameters: ['propA'],
|
|
623
|
+
refreshKeys: ['propB', 'propA'],
|
|
624
|
+
defaultRefreshValues: {
|
|
625
|
+
propB: 6,
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
})
|
|
615
632
|
})
|
|
@@ -65,10 +65,57 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
65
65
|
*/
|
|
66
66
|
inputTransformation?: (desired: Partial<T>) => Promise<unknown> | unknown;
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Customize the import behavior of the resource. By default, <code>codify import</code> will call `refresh()` with
|
|
70
|
+
* every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters
|
|
71
|
+
* in the schema and will prompt the user for these values before performing the import.
|
|
72
|
+
*
|
|
73
|
+
* <b>Example:</b><br>
|
|
74
|
+
* Resource `alias` with parameters
|
|
75
|
+
*
|
|
76
|
+
* ```
|
|
77
|
+
* { alias <b>(*required)</b>: string; value: string; }
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* When the user calls `codify import alias`, they will first be prompted to enter the value for `alias`. Refresh
|
|
81
|
+
* is then called with `refresh({ alias: 'user-input', value: null })`. The result returned to the user will then be:
|
|
82
|
+
*
|
|
83
|
+
* ```
|
|
84
|
+
* { type: 'alias', alias: 'user-input', value: 'git push' }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
68
87
|
import?: {
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken
|
|
91
|
+
* from the JSON schema. The `requiredParameters` parameter must be declared if a complex required is declared in
|
|
92
|
+
* the schema (contains `oneOf`, `anyOf`, `allOf`, `if`, `then`, `else`).
|
|
93
|
+
* <br>
|
|
94
|
+
* The user will be prompted for the required parameters before the import starts. This is done because for most resources
|
|
95
|
+
* the required parameters change the behaviour of the refresh (for example for the `alias` resource, the `alias` parmaeter
|
|
96
|
+
* chooses which alias the resource is managing).
|
|
97
|
+
*
|
|
98
|
+
* See {@link import} for more information on how importing works.
|
|
99
|
+
*/
|
|
69
100
|
requiredParameters?: Array<Partial<keyof T>>;
|
|
70
101
|
|
|
71
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Customize which keys will be refreshed in the import. Typically, `refresh()` statements only refresh
|
|
104
|
+
* the parameters provided as the input. Use `refreshKeys` to control which parameter keys are passed in.
|
|
105
|
+
* <br>
|
|
106
|
+
* By default all parameters (except for {@link requiredParameters }) are passed in with the value `null`. The passed
|
|
107
|
+
* in value can be customized using {@link defaultRefreshValues}
|
|
108
|
+
*
|
|
109
|
+
* See {@link import} for more information on how importing works.
|
|
110
|
+
*/
|
|
111
|
+
refreshKeys?: Array<Partial<keyof T>>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Customize the value that is passed into refresh when importing. This must only contain keys found in {@link refreshKeys}.
|
|
115
|
+
*
|
|
116
|
+
* See {@link import} for more information on how importing works.
|
|
117
|
+
*/
|
|
118
|
+
defaultRefreshValues?: Partial<T>
|
|
72
119
|
}
|
|
73
120
|
}
|
|
74
121
|
|
|
@@ -164,6 +211,26 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
|
|
|
164
211
|
* @return Return true if desired is equivalent to current.
|
|
165
212
|
*/
|
|
166
213
|
isElementEqual?: (desired: any, current: any) => boolean
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Filter the contents of the refreshed array by the desired. This way items currently on the system but not
|
|
217
|
+
* in desired don't show up in the plan.
|
|
218
|
+
*
|
|
219
|
+
* <b>For example, for the nvm resource:</b>
|
|
220
|
+
* <ul>
|
|
221
|
+
* <li>Desired (20.18.0, 18.9.0, 16.3.1)</li>
|
|
222
|
+
* <li>Current (20.18.0, 22.1.3, 12.1.0)</li>
|
|
223
|
+
* </ul>
|
|
224
|
+
*
|
|
225
|
+
* Without filtering the plan will be:
|
|
226
|
+
* (~20.18.0, +18.9.0, +16.3.1, -22.1.3, -12.1.0)<br>
|
|
227
|
+
* With filtering the plan is: (~20.18.0, +18.9.0, +16.3.1)
|
|
228
|
+
*
|
|
229
|
+
* As you can see, filtering prevents items currently installed on the system from being removed.
|
|
230
|
+
*
|
|
231
|
+
* Defaults to true.
|
|
232
|
+
*/
|
|
233
|
+
filterInStatelessMode?: boolean,
|
|
167
234
|
}
|
|
168
235
|
|
|
169
236
|
/**
|