codify-plugin-lib 1.0.37 → 1.0.39

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.
@@ -1,134 +1,246 @@
1
- import { ParameterOperation, ResourceConfig, ResourceOperation } from 'codify-schemas';
2
- import { ChangeSet, ParameterChange } from './change-set.js';
1
+ import { ParameterOperation, ResourceConfig, ResourceOperation, StringIndexedObject, } from 'codify-schemas';
2
+ import { ParameterChange } from './change-set.js';
3
3
  import { Plan } from './plan.js';
4
4
  import { StatefulParameter } from './stateful-parameter.js';
5
+ import { ResourceConfiguration, ValidationResult } from './resource-types.js';
6
+ import { setsEqual, splitUserConfig } from '../utils/utils.js';
7
+ import { ParameterConfiguration, PlanConfiguration } from './plan-types.js';
8
+
9
+ /**
10
+ * Description of resource here
11
+ * Two main functions:
12
+ * - Plan
13
+ * - Apply
14
+ *
15
+ */
16
+ export abstract class Resource<T extends StringIndexedObject> {
17
+
18
+ readonly typeId: string;
19
+ readonly statefulParameters: Map<keyof T, StatefulParameter<T, T[keyof T]>>;
20
+ readonly dependencies: Resource<any>[]; // TODO: Change this to a string
21
+ readonly parameterConfigurations: Record<keyof T, ParameterConfiguration>
22
+
23
+ private readonly options: ResourceConfiguration<T>;
24
+
25
+ protected constructor(configuration: ResourceConfiguration<T>) {
26
+ this.validateResourceConfiguration(configuration);
27
+
28
+ this.typeId = configuration.type;
29
+ this.statefulParameters = new Map(configuration.statefulParameters?.map((sp) => [sp.name, sp]));
30
+ this.parameterConfigurations = this.generateParameterConfigurations(configuration);
31
+
32
+ this.dependencies = configuration.dependencies ?? [];
33
+ this.options = configuration;
34
+ }
5
35
 
6
- export type ErrorMessage = string;
36
+ getDependencyTypeIds(): string[] {
37
+ return this.dependencies.map((d) => d.typeId)
38
+ }
7
39
 
8
- export abstract class Resource<T extends ResourceConfig> {
40
+ async onInitialize(): Promise<void> {}
9
41
 
10
- private statefulParameters: Map<string, StatefulParameter<T, keyof T>> = new Map();
42
+ // TODO: Add state in later.
43
+ // Currently only calculating how to add things to reach desired state. Can't delete resources.
44
+ // Add previousConfig as a parameter for plan(desired, previous);
45
+ async plan(desiredConfig: Partial<T> & ResourceConfig): Promise<Plan<T>> {
46
+
47
+ // Explanation: these are settings for how the plan will be generated
48
+ const planConfiguration: PlanConfiguration<T> = {
49
+ statefulMode: false,
50
+ parameterConfigurations: this.parameterConfigurations,
51
+ }
11
52
 
12
- constructor(
13
- private dependencies: Resource<any>[] = [],
14
- ) {}
53
+ const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
15
54
 
16
- abstract getTypeId(): string;
55
+ const resourceParameters = Object.fromEntries([
56
+ ...Object.entries(desiredParameters).filter(([key]) => !this.statefulParameters.has(key)),
57
+ ]) as Partial<T>;
17
58
 
18
- getDependencyTypeIds(): string[] {
19
- return this.dependencies.map((d) => d.getTypeId())
20
- }
59
+ const statefulParameters = [...this.statefulParameters.values()]
60
+ .filter((sp) => desiredParameters[sp.name] !== undefined) // Checking for undefined is fine here because JSONs can only have null.
21
61
 
22
- async onInitialize(): Promise<void> {}
62
+ // Refresh resource parameters
63
+ // This refreshes the parameters that configure the resource itself
64
+ const keysToRefresh = new Set(Object.keys(resourceParameters));
65
+ const currentParameters = await this.refresh(keysToRefresh);
23
66
 
24
- // TODO: Add state in later.
25
- // Calculate change set from current config -> state -> desired in the future
26
- async plan(desiredConfig: T): Promise<Plan<T>> {
27
- const currentConfig = await this.getCurrentConfig(desiredConfig);
28
- if (!currentConfig) {
29
- return Plan.create(ChangeSet.createForNullCurrentConfig(desiredConfig), desiredConfig);
67
+ if (currentParameters == null && statefulParameters.length === 0) {
68
+ return Plan.create(desiredConfig, null, planConfiguration);
30
69
  }
31
70
 
32
- // Fetch the status of stateful parameters separately
33
- const desiredConfigStatefulParameters = [...this.statefulParameters.values()]
34
- .filter((sp) => desiredConfig[sp.name] !== undefined)
35
- for(const statefulParameter of desiredConfigStatefulParameters) {
36
- const parameterCurrentStatus = await statefulParameter.getCurrent(desiredConfig[statefulParameter.name]);
37
- if (parameterCurrentStatus) {
38
- currentConfig[statefulParameter.name] = parameterCurrentStatus;
71
+ this.validateRefreshResults(currentParameters, keysToRefresh);
72
+
73
+ // Refresh stateful parameters
74
+ // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
75
+ const currentStatefulParameters = {} as Partial<T>;
76
+
77
+ for(const statefulParameter of statefulParameters) {
78
+ const desiredValue = desiredParameters[statefulParameter.name];
79
+
80
+ let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
81
+
82
+ // In stateless mode, filter the refreshed parameters by the desired to ensure that no deletes happen
83
+ if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
84
+ currentValue = currentValue.filter((p) => desiredValue?.includes(p)) as any;
39
85
  }
40
- }
41
86
 
42
- // TODO: After adding in state files, need to calculate deletes here
43
- // Where current config exists and state config exists but desired config doesn't
44
-
45
- // Explanation: This calculates the change set of the parameters between the
46
- // two configs and then passes it to the subclass to calculate the overall
47
- // operation for the resource
48
- const parameterChangeSet = ChangeSet.calculateParameterChangeSet(currentConfig, desiredConfig);
49
- const resourceOperation = parameterChangeSet
50
- .filter((change) => change.operation !== ParameterOperation.NOOP)
51
- .reduce((operation: ResourceOperation, curr: ParameterChange) => {
52
- const newOperation = !this.statefulParameters.has(curr.name)
53
- ? this.calculateOperation(curr)
54
- : ResourceOperation.MODIFY; // All stateful parameters are modify only
55
- return ChangeSet.combineResourceOperations(operation, newOperation);
56
- }, ResourceOperation.NOOP);
87
+ currentStatefulParameters[statefulParameter.name] = currentValue;
88
+ }
57
89
 
58
90
  return Plan.create(
59
- new ChangeSet(resourceOperation, parameterChangeSet),
60
- desiredConfig
61
- );
91
+ desiredConfig,
92
+ { ...currentParameters, ...currentStatefulParameters, ...resourceMetadata } as Partial<T> & ResourceConfig,
93
+ planConfiguration,
94
+ )
62
95
  }
63
96
 
64
97
  async apply(plan: Plan<T>): Promise<void> {
65
- if (plan.getResourceType() !== this.getTypeId()) {
66
- throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.getTypeId()} but got: ${plan.getResourceType()}`);
98
+ if (plan.getResourceType() !== this.typeId) {
99
+ throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
67
100
  }
68
101
 
69
102
  switch (plan.changeSet.operation) {
103
+ case ResourceOperation.CREATE: {
104
+ return this._applyCreate(plan); // TODO: Add new parameters value so that apply
105
+ }
70
106
  case ResourceOperation.MODIFY: {
71
- const parameterChanges = plan.changeSet.parameterChanges
72
- .filter((c: ParameterChange) => c.operation !== ParameterOperation.NOOP);
107
+ return this._applyModify(plan);
108
+ }
109
+ case ResourceOperation.RECREATE: {
110
+ await this._applyDestroy(plan);
111
+ return this._applyCreate(plan);
112
+ }
113
+ case ResourceOperation.DESTROY: {
114
+ return this._applyDestroy(plan);
115
+ }
116
+ }
117
+ }
73
118
 
74
- const statelessParameterChanges = parameterChanges.filter((pc: ParameterChange) => !this.statefulParameters.has(pc.name))
75
- if (statelessParameterChanges.length > 0) {
76
- await this.applyModify(plan);
77
- }
119
+ private async _applyCreate(plan: Plan<T>): Promise<void> {
120
+ await this.applyCreate(plan);
78
121
 
79
- const statefulParameterChanges = parameterChanges.filter((pc: ParameterChange) => this.statefulParameters.has(pc.name))
80
- for (const parameterChange of statefulParameterChanges) {
81
- const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
82
- switch (parameterChange.operation) {
83
- case ParameterOperation.ADD: {
84
- await statefulParameter.applyAdd(parameterChange, plan);
85
- break;
86
- }
87
- case ParameterOperation.MODIFY: {
88
- await statefulParameter.applyModify(parameterChange, plan);
89
- break;
90
- }
91
- case ParameterOperation.REMOVE: {
92
- await statefulParameter.applyRemove(parameterChange, plan);
93
- break;
94
- }
95
- }
122
+ const statefulParameterChanges = plan.changeSet.parameterChanges
123
+ .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
124
+ for (const parameterChange of statefulParameterChanges) {
125
+ const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
126
+ await statefulParameter.applyAdd(parameterChange.newValue, plan);
127
+ }
128
+ }
129
+
130
+ private async _applyModify(plan: Plan<T>): Promise<void> {
131
+ const parameterChanges = plan
132
+ .changeSet
133
+ .parameterChanges
134
+ .filter((c: ParameterChange<T>) => c.operation !== ParameterOperation.NOOP);
135
+
136
+ const statelessParameterChanges = parameterChanges
137
+ .filter((pc: ParameterChange<T>) => !this.statefulParameters.has(pc.name))
138
+ for (const pc of statelessParameterChanges) {
139
+ // TODO: When stateful mode is added in the future. Dynamically choose if deletes are allowed
140
+ await this.applyModify(pc.name, pc.newValue, pc.previousValue, false, plan);
141
+ }
142
+
143
+ const statefulParameterChanges = parameterChanges
144
+ .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
145
+ for (const parameterChange of statefulParameterChanges) {
146
+ const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
147
+
148
+ switch (parameterChange.operation) {
149
+ case ParameterOperation.ADD: {
150
+ await statefulParameter.applyAdd(parameterChange.newValue, plan);
151
+ break;
152
+ }
153
+ case ParameterOperation.MODIFY: {
154
+ // TODO: When stateful mode is added in the future. Dynamically choose if deletes are allowed
155
+ await statefulParameter.applyModify(parameterChange.newValue, parameterChange.previousValue, false, plan);
156
+ break;
157
+ }
158
+ case ParameterOperation.REMOVE: {
159
+ await statefulParameter.applyRemove(parameterChange.previousValue, plan);
160
+ break;
96
161
  }
162
+ }
163
+ }
164
+ }
97
165
 
98
- return;
166
+ private async _applyDestroy(plan: Plan<T>): Promise<void> {
167
+ // If this option is set (defaults to false), then stateful parameters need to be destroyed
168
+ // as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
169
+ if (this.options.callStatefulParameterRemoveOnDestroy) {
170
+ const statefulParameterChanges = plan.changeSet.parameterChanges
171
+ .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
172
+ for (const parameterChange of statefulParameterChanges) {
173
+ const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
174
+ await statefulParameter.applyRemove(parameterChange.previousValue, plan);
99
175
  }
100
- case ResourceOperation.CREATE: {
101
- await this.applyCreate(plan);
102
- const statefulParameterChanges = plan.changeSet.parameterChanges
103
- .filter((pc: ParameterChange) => this.statefulParameters.has(pc.name))
176
+ }
177
+
178
+ await this.applyDestroy(plan);
179
+ }
104
180
 
105
- for (const parameterChange of statefulParameterChanges) {
106
- const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
107
- await statefulParameter.applyAdd(parameterChange, plan);
181
+ private generateParameterConfigurations(
182
+ resourceConfiguration: ResourceConfiguration<T>
183
+ ): Record<keyof T, ParameterConfiguration> {
184
+ const resourceParameters = Object.fromEntries(
185
+ Object.entries(resourceConfiguration.parameterConfigurations ?? {})
186
+ ?.map(([name, value]) => ([name, { ...value, isStatefulParameter: false }]))
187
+ ) as Record<keyof T, ParameterConfiguration>
188
+
189
+ const statefulParameters = resourceConfiguration.statefulParameters
190
+ ?.reduce((obj, sp) => {
191
+ return {
192
+ ...obj,
193
+ [sp.name]: {
194
+ ...sp.configuration,
195
+ isStatefulParameter: true,
196
+ }
108
197
  }
198
+ }, {}) ?? {}
109
199
 
110
- return;
111
- }
112
- case ResourceOperation.RECREATE: return this.applyRecreate(plan);
113
- case ResourceOperation.DESTROY: return this.applyDestroy(plan);
200
+ return {
201
+ ...resourceParameters,
202
+ ...statefulParameters,
114
203
  }
204
+
115
205
  }
116
206
 
117
- protected registerStatefulParameter(parameter: StatefulParameter<T, keyof T>) {
118
- this.statefulParameters.set(parameter.name as string, parameter);
207
+ private validateResourceConfiguration(data: ResourceConfiguration<T>) {
208
+ // A parameter cannot be both stateful and stateless
209
+ if (data.parameterConfigurations && data.statefulParameters) {
210
+ const parameters = [...Object.keys(data.parameterConfigurations)];
211
+ const statefulParameterSet = new Set(Object.keys(data.statefulParameters));
212
+
213
+ const intersection = parameters.some((p) => statefulParameterSet.has(p));
214
+ if (intersection) {
215
+ throw new Error(`Resource ${this.typeId} cannot declare a parameter as both stateful and non-stateful`);
216
+ }
217
+ }
119
218
  }
120
219
 
121
- abstract validate(config: unknown): Promise<ErrorMessage[] | undefined>;
220
+ private validateRefreshResults(refresh: Partial<T> | null, desiredKeys: Set<keyof T>) {
221
+ if (!refresh) {
222
+ return;
223
+ }
224
+
225
+ const refreshKeys = new Set(Object.keys(refresh)) as Set<keyof T>;
226
+
227
+ if (!setsEqual(desiredKeys, refreshKeys)) {
228
+ throw new Error(
229
+ `Resource ${this.options.type}
230
+ refresh() must return back exactly the keys that were provided
231
+ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
232
+ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
233
+ );
234
+ }
235
+ }
122
236
 
123
- abstract getCurrentConfig(desiredConfig: T): Promise<T | null>;
237
+ abstract validate(config: unknown): Promise<ValidationResult>;
124
238
 
125
- abstract calculateOperation(change: ParameterChange): ResourceOperation.MODIFY | ResourceOperation.RECREATE;
239
+ abstract refresh(keys: Set<keyof T>): Promise<Partial<T> | null>;
126
240
 
127
241
  abstract applyCreate(plan: Plan<T>): Promise<void>;
128
242
 
129
- abstract applyModify(plan: Plan<T>): Promise<void>;
130
-
131
- abstract applyRecreate(plan: Plan<T>): Promise<void>;
243
+ async applyModify(parameterName: keyof T, newValue: unknown, previousValue: unknown, allowDeletes: boolean, plan: Plan<T>): Promise<void> {};
132
244
 
133
- abstract applyDestroy(plan:Plan<T>): Promise<void>;
245
+ abstract applyDestroy(plan: Plan<T>): Promise<void>;
134
246
  }
@@ -1,13 +1,61 @@
1
- import { ParameterChange } from './change-set.js';
2
1
  import { Plan } from './plan.js';
3
- import { ResourceConfig } from 'codify-schemas';
2
+ import { StringIndexedObject } from 'codify-schemas';
4
3
 
5
- export abstract class StatefulParameter<T extends ResourceConfig, K extends keyof T> {
6
- abstract get name(): K;
4
+ export interface StatefulParameterConfiguration<T> {
5
+ name: keyof T;
6
+ isEqual?: (a: any, b: any) => boolean;
7
+ }
8
+
9
+ export abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
10
+ readonly name: keyof T;
11
+ readonly configuration: StatefulParameterConfiguration<T>;
12
+
13
+ protected constructor(configuration: StatefulParameterConfiguration<T>) {
14
+ this.name = configuration.name;
15
+ this.configuration = configuration
16
+ }
17
+
18
+ abstract refresh(previousValue: V | null): Promise<V | null>;
19
+
20
+ // TODO: Add an additional parameter here for what has actually changed.
21
+ abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
22
+ abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
23
+ abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
24
+ }
25
+
26
+ export abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any>{
27
+ protected constructor(configuration: StatefulParameterConfiguration<T>) {
28
+ super(configuration);
29
+ }
30
+
31
+ async applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void> {
32
+ for (const value of valuesToAdd) {
33
+ await this.applyAddItem(value, plan);
34
+ }
35
+ }
36
+
37
+ async applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void> {
38
+ const valuesToAdd = newValues.filter((n) => !previousValues.includes(n));
39
+ const valuesToRemove = previousValues.filter((n) => !newValues.includes(n));
40
+
41
+ for (const value of valuesToAdd) {
42
+ await this.applyAddItem(value, plan)
43
+ }
44
+
45
+ if (allowDeletes) {
46
+ for (const value of valuesToRemove) {
47
+ await this.applyRemoveItem(value, plan)
48
+ }
49
+ }
50
+ }
7
51
 
8
- abstract getCurrent(desiredValue: T[K]): Promise<T[K]>;
52
+ async applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void> {
53
+ for (const value of valuesToRemove) {
54
+ await this.applyRemoveItem(value as V, plan);
55
+ }
56
+ }
9
57
 
10
- abstract applyAdd(parameterChange: ParameterChange, plan: Plan<T>): Promise<void>;
11
- abstract applyModify(parameterChange: ParameterChange, plan: Plan<T>): Promise<void>;
12
- abstract applyRemove(parameterChange: ParameterChange, plan: Plan<T>): Promise<void>;
58
+ abstract refresh(previousValue: V[] | null): Promise<V[] | null>;
59
+ abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
60
+ abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
13
61
  }
package/src/index.ts CHANGED
@@ -2,9 +2,11 @@ import { Plugin } from './entities/plugin.js';
2
2
  import { MessageHandler } from './messages/handlers.js';
3
3
 
4
4
  export * from './entities/resource.js'
5
+ export * from './entities/resource-types.js'
5
6
  export * from './entities/plugin.js'
6
7
  export * from './entities/change-set.js'
7
8
  export * from './entities/plan.js'
9
+ export * from './entities/plan-types.js'
8
10
  export * from './entities/stateful-parameter.js'
9
11
 
10
12
  export * from './utils/test-utils.js'
@@ -14,3 +16,4 @@ export async function runPlugin(plugin: Plugin) {
14
16
  const messageHandler = new MessageHandler(plugin);
15
17
  process.on('message', (message) => messageHandler.onMessage(message))
16
18
  }
19
+ export { ErrorMessage } from './entities/resource-types.js';
@@ -1,5 +1,6 @@
1
1
  import promiseSpawn from '@npmcli/promise-spawn';
2
2
  import { SpawnOptions } from 'child_process';
3
+ import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
3
4
 
4
5
  export enum SpawnStatus {
5
6
  SUCCESS = 'success',
@@ -78,3 +79,23 @@ export async function codifySpawn(
78
79
  export function isDebug(): boolean {
79
80
  return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
80
81
  }
82
+
83
+ export function splitUserConfig<T extends StringIndexedObject>(
84
+ config: T & ResourceConfig
85
+ ): { parameters: T; resourceMetadata: ResourceConfig} {
86
+ const resourceMetadata = {
87
+ type: config.type,
88
+ ...(config.name && { name: config.name }),
89
+ ...(config.dependsOn && { dependsOn: config.dependsOn }),
90
+ };
91
+
92
+ const { type, name, dependsOn, ...parameters } = config;
93
+ return {
94
+ parameters: parameters as T,
95
+ resourceMetadata,
96
+ };
97
+ }
98
+
99
+ export function setsEqual(set1: Set<unknown>, set2: Set<unknown>): boolean {
100
+ return set1.size === set2.size && [...set1].every((v) => set2.has(v));
101
+ }