codify-plugin-lib 1.0.77 → 1.0.79

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.
@@ -30,7 +30,17 @@ export declare class ChangeSet<T extends StringIndexedObject> {
30
30
  static empty<T extends StringIndexedObject>(): ChangeSet<T>;
31
31
  static create<T extends StringIndexedObject>(desired: Partial<T>): ChangeSet<T>;
32
32
  static destroy<T extends StringIndexedObject>(current: Partial<T>): ChangeSet<T>;
33
- static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParameterSetting>>): ChangeSet<T>;
33
+ static calculateModification<T extends StringIndexedObject>(desired: Partial<T>, current: Partial<T>, parameterSettings?: Partial<Record<keyof T, ParameterSetting>>): Promise<ChangeSet<T>>;
34
+ /**
35
+ * Calculates the differences between the desired and current parameters,
36
+ * and returns a list of parameter changes that describe what needs to be added,
37
+ * removed, or modified to match the desired state.
38
+ *
39
+ * @param {Partial<T>} desiredParameters - The desired target state of the parameters.
40
+ * @param {Partial<T>} currentParameters - The current state of the parameters.
41
+ * @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
42
+ * @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
43
+ */
34
44
  private static calculateParameterChanges;
35
45
  private static combineResourceOperations;
36
46
  private static isSame;
@@ -45,8 +45,8 @@ export class ChangeSet {
45
45
  }));
46
46
  return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
47
47
  }
48
- static calculateModification(desired, current, parameterSettings = {}) {
49
- const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
48
+ static async calculateModification(desired, current, parameterSettings = {}) {
49
+ const pc = await ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
50
50
  const statefulParameterKeys = new Set(Object.entries(parameterSettings)
51
51
  .filter(([, v]) => v?.type === 'stateful')
52
52
  .map(([k]) => k));
@@ -67,7 +67,17 @@ export class ChangeSet {
67
67
  }, ResourceOperation.NOOP);
68
68
  return new ChangeSet(resourceOperation, pc);
69
69
  }
70
- static calculateParameterChanges(desiredParameters, currentParameters, parameterOptions) {
70
+ /**
71
+ * Calculates the differences between the desired and current parameters,
72
+ * and returns a list of parameter changes that describe what needs to be added,
73
+ * removed, or modified to match the desired state.
74
+ *
75
+ * @param {Partial<T>} desiredParameters - The desired target state of the parameters.
76
+ * @param {Partial<T>} currentParameters - The current state of the parameters.
77
+ * @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
78
+ * @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
79
+ */
80
+ static async calculateParameterChanges(desiredParameters, currentParameters, parameterOptions) {
71
81
  const parameterChangeSet = new Array();
72
82
  // Filter out null and undefined values or else the diff below will not work
73
83
  const desired = Object.fromEntries(Object.entries(desiredParameters).filter(([, v]) => v !== null && v !== undefined));
@@ -83,7 +93,7 @@ export class ChangeSet {
83
93
  delete current[k];
84
94
  continue;
85
95
  }
86
- if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
96
+ if (!await ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
87
97
  parameterChangeSet.push({
88
98
  name: k,
89
99
  previousValue: v ?? null,
@@ -128,7 +138,7 @@ export class ChangeSet {
128
138
  const indexNext = orderOfOperations.indexOf(next);
129
139
  return orderOfOperations[Math.max(indexPrev, indexNext)];
130
140
  }
131
- static isSame(desired, current, setting) {
141
+ static async isSame(desired, current, setting) {
132
142
  switch (setting?.type) {
133
143
  case 'stateful': {
134
144
  const statefulSetting = setting.definition.getSettings();
@@ -41,7 +41,7 @@ export declare class Plan<T extends StringIndexedObject> {
41
41
  coreParameters: ResourceConfig;
42
42
  settings: ParsedResourceSettings<T>;
43
43
  statefulMode: boolean;
44
- }): Plan<T>;
44
+ }): Promise<Plan<T>>;
45
45
  /**
46
46
  * Only keep relevant params for the plan. We don't want to change settings that were not already
47
47
  * defined.
package/dist/plan/plan.js CHANGED
@@ -70,7 +70,7 @@ export class Plan {
70
70
  getResourceType() {
71
71
  return this.coreParameters.type;
72
72
  }
73
- static calculate(params) {
73
+ static async calculate(params) {
74
74
  const { desiredParameters, currentParametersArray, stateParameters, coreParameters, settings, statefulMode } = params;
75
75
  const currentParameters = Plan.matchCurrentParameters({
76
76
  desiredParameters,
@@ -99,7 +99,7 @@ export class Plan {
99
99
  return new Plan(uuidV4(), ChangeSet.destroy(filteredCurrentParameters), coreParameters);
100
100
  }
101
101
  // NO-OP, MODIFY or RE-CREATE
102
- const changeSet = ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
102
+ const changeSet = await ChangeSet.calculateModification(desiredParameters, filteredCurrentParameters, settings.parameterSettings);
103
103
  return new Plan(uuidV4(), changeSet, coreParameters);
104
104
  }
105
105
  /**
@@ -1,6 +1,7 @@
1
1
  import { Ajv } from 'ajv';
2
2
  import { ParameterOperation, ResourceOperation } from 'codify-schemas';
3
3
  import { Plan } from '../plan/plan.js';
4
+ import { splitUserConfig } from '../utils/utils.js';
4
5
  import { ConfigParser } from './config-parser.js';
5
6
  import { ParsedResourceSettings } from './parsed-resource-settings.js';
6
7
  export class ResourceController {
@@ -193,9 +194,10 @@ ${JSON.stringify(refresh, null, 2)}
193
194
  desired[key] = await inputTransformation(desired[key]);
194
195
  }
195
196
  if (this.settings.inputTransformation) {
196
- const transformed = await this.settings.inputTransformation(desired);
197
+ const { parameters, coreParameters } = splitUserConfig(desired);
198
+ const transformed = await this.settings.inputTransformation(parameters);
197
199
  Object.keys(desired).forEach((k) => delete desired[k]);
198
- Object.assign(desired, transformed);
200
+ Object.assign(desired, transformed, coreParameters);
199
201
  }
200
202
  }
201
203
  addDefaultValues(desired) {
@@ -86,7 +86,7 @@ export interface DefaultParameterSetting {
86
86
  *
87
87
  * @param input The original parameter value from the desired config.
88
88
  */
89
- inputTransformation?: (input: any) => Promise<any> | unknown;
89
+ inputTransformation?: (input: any) => Promise<any> | any;
90
90
  /**
91
91
  * Customize the equality comparison for a parameter. This is used in the diffing algorithm for generating the plan.
92
92
  * This value will override the pre-set equality function from the type. Return true if the desired value is
@@ -97,7 +97,7 @@ export interface DefaultParameterSetting {
97
97
  *
98
98
  * @return Return true if equal
99
99
  */
100
- isEqual?: (desired: any, current: any) => boolean;
100
+ isEqual?: (desired: any, current: any) => Promise<boolean> | boolean;
101
101
  /**
102
102
  * Chose if the resource can be modified instead of re-created when there is a change to this parameter.
103
103
  * Defaults to false (re-create).
@@ -123,7 +123,7 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
123
123
  *
124
124
  * @return Return true if desired is equivalent to current.
125
125
  */
126
- isElementEqual?: (desired: any, current: any) => boolean;
126
+ isElementEqual?: (desired: any, current: any) => Promise<boolean> | boolean;
127
127
  }
128
128
  /**
129
129
  * Stateful parameter type specific settings. A stateful parameter is a sub-resource that can hold its own
@@ -0,0 +1,5 @@
1
+ import { Shell } from 'zx';
2
+ export declare class ShellContext implements Shell {
3
+ zx: Shell;
4
+ static create(): ShellContext;
5
+ }
@@ -0,0 +1,7 @@
1
+ import { $ } from 'zx';
2
+ export class ShellContext {
3
+ zx = $({ shell: true });
4
+ static create() {
5
+ return new ShellContext();
6
+ }
7
+ }
@@ -0,0 +1,29 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import { SpawnOptions } from 'node:child_process';
3
+ export declare enum SpawnStatus {
4
+ SUCCESS = "success",
5
+ ERROR = "error"
6
+ }
7
+ export interface SpawnResult {
8
+ status: SpawnStatus;
9
+ data: string;
10
+ }
11
+ type CodifySpawnOptions = {
12
+ cwd?: string;
13
+ throws?: boolean;
14
+ requiresRoot?: boolean;
15
+ } & Omit<SpawnOptions, 'detached' | 'shell' | 'stdio'>;
16
+ /**
17
+ *
18
+ * @param cmd Command to run. Ex: `rm -rf`
19
+ * @param opts Standard options for node spawn. Additional argument:
20
+ * throws determines if a shell will throw a JS error. Defaults to true
21
+ *
22
+ * @see promiseSpawn
23
+ * @see spawn
24
+ *
25
+ * @returns SpawnResult { status: SUCCESS | ERROR; data: string }
26
+ */
27
+ export declare function $(cmd: string, opts?: CodifySpawnOptions): Promise<SpawnResult>;
28
+ export declare function isDebug(): boolean;
29
+ export {};
@@ -0,0 +1,124 @@
1
+ import { Ajv } from 'ajv';
2
+ import { MessageCmd, SudoRequestResponseDataSchema } from 'codify-schemas';
3
+ import { spawn } from 'node:child_process';
4
+ import { SudoError } from '../errors.js';
5
+ const ajv = new Ajv({
6
+ strict: true,
7
+ });
8
+ const validateSudoRequestResponse = ajv.compile(SudoRequestResponseDataSchema);
9
+ export var SpawnStatus;
10
+ (function (SpawnStatus) {
11
+ SpawnStatus["SUCCESS"] = "success";
12
+ SpawnStatus["ERROR"] = "error";
13
+ })(SpawnStatus || (SpawnStatus = {}));
14
+ /**
15
+ *
16
+ * @param cmd Command to run. Ex: `rm -rf`
17
+ * @param opts Standard options for node spawn. Additional argument:
18
+ * throws determines if a shell will throw a JS error. Defaults to true
19
+ *
20
+ * @see promiseSpawn
21
+ * @see spawn
22
+ *
23
+ * @returns SpawnResult { status: SUCCESS | ERROR; data: string }
24
+ */
25
+ export async function $(cmd, opts) {
26
+ const throws = opts?.throws ?? true;
27
+ console.log(`Running command: ${cmd}`);
28
+ try {
29
+ // TODO: Need to benchmark the effects of using sh vs zsh for shell.
30
+ // Seems like zsh shells run slower
31
+ let result;
32
+ if (!opts?.requiresRoot) {
33
+ result = await internalSpawn(cmd, opts ?? {});
34
+ }
35
+ else {
36
+ result = await externalSpawnWithSudo(cmd, opts);
37
+ }
38
+ if (result.status !== SpawnStatus.SUCCESS) {
39
+ throw new Error(result.data);
40
+ }
41
+ return result;
42
+ }
43
+ catch (error) {
44
+ if (isDebug()) {
45
+ console.error(`CodifySpawn error for command ${cmd}`, error);
46
+ }
47
+ if (error.message?.startsWith('sudo:')) {
48
+ throw new SudoError(cmd);
49
+ }
50
+ if (throws) {
51
+ throw error;
52
+ }
53
+ if (error instanceof Error) {
54
+ return {
55
+ status: SpawnStatus.ERROR,
56
+ data: error.message,
57
+ };
58
+ }
59
+ return {
60
+ status: SpawnStatus.ERROR,
61
+ data: error + '',
62
+ };
63
+ }
64
+ }
65
+ async function internalSpawn(cmd, opts) {
66
+ return new Promise((resolve, reject) => {
67
+ const output = [];
68
+ // Source start up shells to emulate a users environment vs. a non-interactive non-login shell script
69
+ // Ignore all stdin
70
+ const _process = spawn(`source ~/.zshrc; ${cmd}`, [], {
71
+ ...opts,
72
+ stdio: ['ignore', 'pipe', 'pipe'],
73
+ shell: 'zsh',
74
+ });
75
+ const { stdout, stderr, stdin } = _process;
76
+ stdout.setEncoding('utf8');
77
+ stderr.setEncoding('utf8');
78
+ stdout.on('data', (data) => {
79
+ output.push(data.toString());
80
+ });
81
+ stderr.on('data', (data) => {
82
+ output.push(data.toString());
83
+ });
84
+ _process.on('error', (data) => {
85
+ });
86
+ // please node that this is not a full replacement for 'inherit'
87
+ // the child process can and will detect if stdout is a pty and change output based on it
88
+ // the terminal context is lost & ansi information (coloring) etc will be lost
89
+ if (stdout && stderr) {
90
+ stdout.pipe(process.stdout);
91
+ stderr.pipe(process.stderr);
92
+ }
93
+ _process.on('close', (code) => {
94
+ resolve({
95
+ status: code === 0 ? SpawnStatus.SUCCESS : SpawnStatus.ERROR,
96
+ data: output.join('\n'),
97
+ });
98
+ });
99
+ });
100
+ }
101
+ async function externalSpawnWithSudo(cmd, opts) {
102
+ return await new Promise((resolve) => {
103
+ const listener = (data) => {
104
+ if (data.cmd === MessageCmd.SUDO_REQUEST + '_Response') {
105
+ process.removeListener('message', listener);
106
+ if (!validateSudoRequestResponse(data.data)) {
107
+ throw new Error(`Invalid response for sudo request: ${JSON.stringify(validateSudoRequestResponse.errors, null, 2)}`);
108
+ }
109
+ resolve(data.data);
110
+ }
111
+ };
112
+ process.on('message', listener);
113
+ process.send({
114
+ cmd: MessageCmd.SUDO_REQUEST,
115
+ data: {
116
+ command: cmd,
117
+ options: opts ?? {},
118
+ }
119
+ });
120
+ });
121
+ }
122
+ export function isDebug() {
123
+ return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
124
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.77",
3
+ "version": "1.0.79",
4
4
  "description": "Library plugin library",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -85,12 +85,12 @@ export class ChangeSet<T extends StringIndexedObject> {
85
85
  return new ChangeSet(ResourceOperation.DESTROY, parameterChanges);
86
86
  }
87
87
 
88
- static calculateModification<T extends StringIndexedObject>(
88
+ static async calculateModification<T extends StringIndexedObject>(
89
89
  desired: Partial<T>,
90
90
  current: Partial<T>,
91
91
  parameterSettings: Partial<Record<keyof T, ParameterSetting>> = {},
92
- ): ChangeSet<T> {
93
- const pc = ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
92
+ ): Promise<ChangeSet<T>> {
93
+ const pc = await ChangeSet.calculateParameterChanges(desired, current, parameterSettings);
94
94
 
95
95
  const statefulParameterKeys = new Set(
96
96
  Object.entries(parameterSettings)
@@ -116,11 +116,21 @@ export class ChangeSet<T extends StringIndexedObject> {
116
116
  return new ChangeSet<T>(resourceOperation, pc);
117
117
  }
118
118
 
119
- private static calculateParameterChanges<T extends StringIndexedObject>(
119
+ /**
120
+ * Calculates the differences between the desired and current parameters,
121
+ * and returns a list of parameter changes that describe what needs to be added,
122
+ * removed, or modified to match the desired state.
123
+ *
124
+ * @param {Partial<T>} desiredParameters - The desired target state of the parameters.
125
+ * @param {Partial<T>} currentParameters - The current state of the parameters.
126
+ * @param {Partial<Record<keyof T, ParameterSetting>>} [parameterOptions] - Optional settings used when comparing parameters.
127
+ * @return {ParameterChange<T>[]} A list of changes required to transition from the current state to the desired state.
128
+ */
129
+ private static async calculateParameterChanges<T extends StringIndexedObject>(
120
130
  desiredParameters: Partial<T>,
121
131
  currentParameters: Partial<T>,
122
132
  parameterOptions?: Partial<Record<keyof T, ParameterSetting>>,
123
- ): ParameterChange<T>[] {
133
+ ): Promise<ParameterChange<T>[]> {
124
134
  const parameterChangeSet = new Array<ParameterChange<T>>();
125
135
 
126
136
  // Filter out null and undefined values or else the diff below will not work
@@ -145,7 +155,7 @@ export class ChangeSet<T extends StringIndexedObject> {
145
155
  continue;
146
156
  }
147
157
 
148
- if (!ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
158
+ if (!await ChangeSet.isSame(desired[k], current[k], parameterOptions?.[k])) {
149
159
  parameterChangeSet.push({
150
160
  name: k,
151
161
  previousValue: v ?? null,
@@ -200,11 +210,11 @@ export class ChangeSet<T extends StringIndexedObject> {
200
210
  return orderOfOperations[Math.max(indexPrev, indexNext)];
201
211
  }
202
212
 
203
- private static isSame(
213
+ private static async isSame(
204
214
  desired: unknown,
205
215
  current: unknown,
206
216
  setting?: ParameterSetting,
207
- ): boolean {
217
+ ): Promise<boolean> {
208
218
  switch (setting?.type) {
209
219
  case 'stateful': {
210
220
  const statefulSetting = (setting as StatefulParameterSetting).definition.getSettings()
package/src/plan/plan.ts CHANGED
@@ -105,14 +105,14 @@ export class Plan<T extends StringIndexedObject> {
105
105
  return this.coreParameters.type
106
106
  }
107
107
 
108
- static calculate<T extends StringIndexedObject>(params: {
108
+ static async calculate<T extends StringIndexedObject>(params: {
109
109
  desiredParameters: Partial<T> | null,
110
110
  currentParametersArray: Partial<T>[] | null,
111
111
  stateParameters: Partial<T> | null,
112
112
  coreParameters: ResourceConfig,
113
113
  settings: ParsedResourceSettings<T>,
114
114
  statefulMode: boolean,
115
- }): Plan<T> {
115
+ }): Promise<Plan<T>> {
116
116
  const {
117
117
  desiredParameters,
118
118
  currentParametersArray,
@@ -166,7 +166,7 @@ export class Plan<T extends StringIndexedObject> {
166
166
  }
167
167
 
168
168
  // NO-OP, MODIFY or RE-CREATE
169
- const changeSet = ChangeSet.calculateModification(
169
+ const changeSet = await ChangeSet.calculateModification(
170
170
  desiredParameters!,
171
171
  filteredCurrentParameters!,
172
172
  settings.parameterSettings,
@@ -154,7 +154,7 @@ export class ParsedResourceSettings<T extends StringIndexedObject> implements Re
154
154
  // The rest of the types have defaults set already
155
155
  }
156
156
 
157
- private resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => boolean {
157
+ private resolveEqualsFn(parameter: ParameterSetting, key: string): (desired: unknown, current: unknown) => Promise<boolean> | boolean {
158
158
  if (parameter.type === 'array') {
159
159
  return parameter.isEqual ?? areArraysEqual.bind(areArraysEqual, parameter as ArrayParameterSetting)
160
160
  }
@@ -10,6 +10,7 @@ import {
10
10
  import { ParameterChange } from '../plan/change-set.js';
11
11
  import { Plan } from '../plan/plan.js';
12
12
  import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js';
13
+ import { splitUserConfig } from '../utils/utils.js';
13
14
  import { ConfigParser } from './config-parser.js';
14
15
  import { ParsedResourceSettings } from './parsed-resource-settings.js';
15
16
  import { Resource } from './resource.js';
@@ -250,7 +251,7 @@ ${JSON.stringify(refresh, null, 2)}
250
251
  }
251
252
  }
252
253
 
253
- private async applyTransformParameters(desired: Partial<T> | null): Promise<void> {
254
+ private async applyTransformParameters(desired: Partial<T> & ResourceConfig | null): Promise<void> {
254
255
  if (!desired) {
255
256
  return;
256
257
  }
@@ -264,9 +265,11 @@ ${JSON.stringify(refresh, null, 2)}
264
265
  }
265
266
 
266
267
  if (this.settings.inputTransformation) {
267
- const transformed = await this.settings.inputTransformation(desired)
268
+ const { parameters, coreParameters } = splitUserConfig(desired);
269
+
270
+ const transformed = await this.settings.inputTransformation(parameters)
268
271
  Object.keys(desired).forEach((k) => delete desired[k])
269
- Object.assign(desired, transformed);
272
+ Object.assign(desired, transformed, coreParameters);
270
273
  }
271
274
  }
272
275
 
@@ -115,7 +115,7 @@ export interface DefaultParameterSetting {
115
115
  *
116
116
  * @param input The original parameter value from the desired config.
117
117
  */
118
- inputTransformation?: (input: any) => Promise<any> | unknown;
118
+ inputTransformation?: (input: any) => Promise<any> | any;
119
119
 
120
120
  /**
121
121
  * Customize the equality comparison for a parameter. This is used in the diffing algorithm for generating the plan.
@@ -127,7 +127,7 @@ export interface DefaultParameterSetting {
127
127
  *
128
128
  * @return Return true if equal
129
129
  */
130
- isEqual?: (desired: any, current: any) => boolean;
130
+ isEqual?: (desired: any, current: any) => Promise<boolean> | boolean;
131
131
 
132
132
  /**
133
133
  * Chose if the resource can be modified instead of re-created when there is a change to this parameter.
@@ -156,7 +156,7 @@ export interface ArrayParameterSetting extends DefaultParameterSetting {
156
156
  *
157
157
  * @return Return true if desired is equivalent to current.
158
158
  */
159
- isElementEqual?: (desired: any, current: any) => boolean
159
+ isElementEqual?: (desired: any, current: any) => Promise<boolean> | boolean;
160
160
  }
161
161
 
162
162
  /**