codify-plugin-lib 1.0.36 → 1.0.38

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,234 @@
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
-
6
- export type ErrorMessage = string;
7
-
8
- export abstract class Resource<T extends ResourceConfig> {
9
-
10
- private statefulParameters: Map<string, StatefulParameter<T, keyof T>> = new Map();
11
-
12
- constructor(
13
- private dependencies: Resource<any>[] = [],
14
- ) {}
15
-
16
- abstract getTypeId(): string;
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<string, 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
+ }
17
35
 
18
36
  getDependencyTypeIds(): string[] {
19
- return this.dependencies.map((d) => d.getTypeId())
37
+ return this.dependencies.map((d) => d.typeId)
20
38
  }
21
39
 
22
40
  async onInitialize(): Promise<void> {}
23
41
 
24
42
  // 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);
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 = {
49
+ statefulMode: false,
50
+ parameterConfigurations: this.parameterConfigurations,
30
51
  }
31
52
 
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;
39
- }
53
+ const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
54
+
55
+ // Refresh resource parameters
56
+ // This refreshes the parameters that configure the resource itself
57
+
58
+ const resourceParameters = Object.fromEntries([
59
+ ...Object.entries(desiredParameters).filter(([key]) => !this.statefulParameters.has(key)),
60
+ ]) as Partial<T>;
61
+
62
+ const keysToRefresh = new Set(Object.keys(resourceParameters));
63
+ const currentParameters = await this.refresh(keysToRefresh);
64
+ if (!currentParameters) {
65
+ return Plan.create(desiredConfig, null, planConfiguration);
40
66
  }
41
67
 
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);
68
+ this.validateRefreshResults(currentParameters, keysToRefresh);
69
+
70
+ // Refresh stateful parameters
71
+ // This refreshes parameters that are stateful (they can be added, deleted separately from the resource)
72
+
73
+ const statefulParameters = [...this.statefulParameters.values()]
74
+ .filter((sp) => desiredParameters[sp.name] !== undefined) // Checking for undefined is fine here because JSONs can only have null.
75
+
76
+ for(const statefulParameter of statefulParameters) {
77
+ currentParameters[statefulParameter.name] = await statefulParameter.refresh(
78
+ desiredParameters[statefulParameter.name] ?? null
79
+ ) ?? undefined;
80
+ }
57
81
 
58
82
  return Plan.create(
59
- new ChangeSet(resourceOperation, parameterChangeSet),
60
- desiredConfig
61
- );
83
+ desiredConfig,
84
+ { ...currentParameters, ...resourceMetadata } as Partial<T> & ResourceConfig,
85
+ planConfiguration,
86
+ )
62
87
  }
63
88
 
64
89
  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()}`);
90
+ if (plan.getResourceType() !== this.typeId) {
91
+ throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
67
92
  }
68
93
 
69
94
  switch (plan.changeSet.operation) {
95
+ case ResourceOperation.CREATE: {
96
+ return this._applyCreate(plan); // TODO: Add new parameters value so that apply
97
+ }
70
98
  case ResourceOperation.MODIFY: {
71
- const parameterChanges = plan.changeSet.parameterChanges
72
- .filter((c: ParameterChange) => c.operation !== ParameterOperation.NOOP);
99
+ return this._applyModify(plan);
100
+ }
101
+ case ResourceOperation.RECREATE: {
102
+ await this._applyDestroy(plan);
103
+ return this._applyCreate(plan);
104
+ }
105
+ case ResourceOperation.DESTROY: {
106
+ return this._applyDestroy(plan);
107
+ }
108
+ }
109
+ }
73
110
 
74
- const statelessParameterChanges = parameterChanges.filter((pc: ParameterChange) => !this.statefulParameters.has(pc.name))
75
- if (statelessParameterChanges.length > 0) {
76
- await this.applyModify(plan);
77
- }
111
+ private async _applyCreate(plan: Plan<T>): Promise<void> {
112
+ await this.applyCreate(plan);
78
113
 
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
- }
114
+ const statefulParameterChanges = plan.changeSet.parameterChanges
115
+ .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
116
+ for (const parameterChange of statefulParameterChanges) {
117
+ const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
118
+ await statefulParameter.applyAdd(parameterChange.newValue, plan);
119
+ }
120
+ }
121
+
122
+ private async _applyModify(plan: Plan<T>): Promise<void> {
123
+ const parameterChanges = plan
124
+ .changeSet
125
+ .parameterChanges
126
+ .filter((c: ParameterChange<T>) => c.operation !== ParameterOperation.NOOP);
127
+
128
+ const statelessParameterChanges = parameterChanges
129
+ .filter((pc: ParameterChange<T>) => !this.statefulParameters.has(pc.name))
130
+ for (const pc of statelessParameterChanges) {
131
+ // TODO: When stateful mode is added in the future. Dynamically choose if deletes are allowed
132
+ await this.applyModify(pc.name, pc.newValue, pc.previousValue, false, plan);
133
+ }
134
+
135
+ const statefulParameterChanges = parameterChanges
136
+ .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
137
+ for (const parameterChange of statefulParameterChanges) {
138
+ const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
139
+
140
+ switch (parameterChange.operation) {
141
+ case ParameterOperation.ADD: {
142
+ await statefulParameter.applyAdd(parameterChange.newValue, plan);
143
+ break;
144
+ }
145
+ case ParameterOperation.MODIFY: {
146
+ // TODO: When stateful mode is added in the future. Dynamically choose if deletes are allowed
147
+ await statefulParameter.applyModify(parameterChange.newValue, parameterChange.previousValue, false, plan);
148
+ break;
96
149
  }
150
+ case ParameterOperation.REMOVE: {
151
+ await statefulParameter.applyRemove(parameterChange.previousValue, plan);
152
+ break;
153
+ }
154
+ }
155
+ }
156
+ }
97
157
 
98
- return;
158
+ private async _applyDestroy(plan: Plan<T>): Promise<void> {
159
+ // If this option is set (defaults to false), then stateful parameters need to be destroyed
160
+ // as well. This means that the stateful parameter wouldn't have been normally destroyed with applyDestroy()
161
+ if (this.options.callStatefulParameterRemoveOnDestroy) {
162
+ const statefulParameterChanges = plan.changeSet.parameterChanges
163
+ .filter((pc: ParameterChange<T>) => this.statefulParameters.has(pc.name))
164
+ for (const parameterChange of statefulParameterChanges) {
165
+ const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
166
+ await statefulParameter.applyRemove(parameterChange.previousValue, plan);
99
167
  }
100
- case ResourceOperation.CREATE: {
101
- await this.applyCreate(plan);
102
- const statefulParameterChanges = plan.changeSet.parameterChanges
103
- .filter((pc: ParameterChange) => this.statefulParameters.has(pc.name))
168
+ }
104
169
 
105
- for (const parameterChange of statefulParameterChanges) {
106
- const statefulParameter = this.statefulParameters.get(parameterChange.name)!;
107
- await statefulParameter.applyAdd(parameterChange, plan);
170
+ await this.applyDestroy(plan);
171
+ }
172
+
173
+ private generateParameterConfigurations(
174
+ resourceConfiguration: ResourceConfiguration<T>
175
+ ): Record<string, ParameterConfiguration> {
176
+ const resourceParameters: Record<string, ParameterConfiguration> = Object.fromEntries(
177
+ Object.entries(resourceConfiguration.parameterConfigurations ?? {})
178
+ ?.map(([name, value]) => ([name, { ...value, isStatefulParameter: false }]))
179
+ )
180
+
181
+ const statefulParameters: Record<string, ParameterConfiguration> = resourceConfiguration.statefulParameters
182
+ ?.reduce((obj, sp) => {
183
+ return {
184
+ ...obj,
185
+ [sp.name]: {
186
+ ...sp.configuration,
187
+ isStatefulParameter: true,
188
+ }
108
189
  }
190
+ }, {}) ?? {}
109
191
 
110
- return;
111
- }
112
- case ResourceOperation.RECREATE: return this.applyRecreate(plan);
113
- case ResourceOperation.DESTROY: return this.applyDestroy(plan);
192
+ return {
193
+ ...resourceParameters,
194
+ ...statefulParameters,
114
195
  }
196
+
115
197
  }
116
198
 
117
- protected registerStatefulParameter(parameter: StatefulParameter<T, keyof T>) {
118
- this.statefulParameters.set(parameter.name as string, parameter);
199
+ private validateResourceConfiguration(data: ResourceConfiguration<T>) {
200
+ // A parameter cannot be both stateful and stateless
201
+ if (data.parameterConfigurations && data.statefulParameters) {
202
+ const parameters = [...Object.keys(data.parameterConfigurations)];
203
+ const statefulParameterSet = new Set(Object.keys(data.statefulParameters));
204
+
205
+ const intersection = parameters.some((p) => statefulParameterSet.has(p));
206
+ if (intersection) {
207
+ throw new Error(`Resource ${this.typeId} cannot declare a parameter as both stateful and non-stateful`);
208
+ }
209
+ }
119
210
  }
120
211
 
121
- abstract validate(config: unknown): Promise<ErrorMessage[] | undefined>;
212
+ private validateRefreshResults(refresh: Partial<T>, desiredKeys: Set<keyof T>) {
213
+ const refreshKeys = new Set(Object.keys(refresh)) as Set<keyof T>;
214
+
215
+ if (!setsEqual(desiredKeys, refreshKeys)) {
216
+ throw new Error(
217
+ `Resource ${this.options.type}
218
+ refresh() must return back exactly the keys that were provided
219
+ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
220
+ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`
221
+ );
222
+ }
223
+ }
122
224
 
123
- abstract getCurrentConfig(desiredConfig: T): Promise<T | null>;
225
+ abstract validate(config: unknown): Promise<ValidationResult>;
124
226
 
125
- abstract calculateOperation(change: ParameterChange): ResourceOperation.MODIFY | ResourceOperation.RECREATE;
227
+ abstract refresh(keys: Set<keyof T>): Promise<Partial<T> | null>;
126
228
 
127
229
  abstract applyCreate(plan: Plan<T>): Promise<void>;
128
230
 
129
- abstract applyModify(plan: Plan<T>): Promise<void>;
130
-
131
- abstract applyRecreate(plan: Plan<T>): Promise<void>;
231
+ async applyModify(parameterName: keyof T, newValue: unknown, previousValue: unknown, allowDeletes: boolean, plan: Plan<T>): Promise<void> {};
132
232
 
133
- abstract applyDestroy(plan:Plan<T>): Promise<void>;
233
+ abstract applyDestroy(plan: Plan<T>): Promise<void>;
134
234
  }
@@ -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
+ }