@kustodian/cli 1.0.0
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/package.json +50 -0
- package/src/bin.ts +71 -0
- package/src/command.ts +76 -0
- package/src/commands/apply.ts +473 -0
- package/src/commands/init.ts +299 -0
- package/src/commands/update.ts +274 -0
- package/src/commands/validate.ts +112 -0
- package/src/container.ts +105 -0
- package/src/index.ts +9 -0
- package/src/middleware.ts +401 -0
- package/src/runner.ts +213 -0
package/src/container.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency injection container for the CLI.
|
|
3
|
+
* Provides a simple service locator pattern.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Service identifier type.
|
|
8
|
+
*/
|
|
9
|
+
export type ServiceIdType<T> = symbol & { __type?: T };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a typed service identifier.
|
|
13
|
+
*/
|
|
14
|
+
export function create_service_id<T>(name: string): ServiceIdType<T> {
|
|
15
|
+
return Symbol(name) as ServiceIdType<T>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Factory function type for creating services.
|
|
20
|
+
*/
|
|
21
|
+
export type FactoryType<T> = (container: ContainerType) => T;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Container interface for dependency injection.
|
|
25
|
+
*/
|
|
26
|
+
export interface ContainerType {
|
|
27
|
+
/**
|
|
28
|
+
* Registers a singleton service.
|
|
29
|
+
*/
|
|
30
|
+
register_singleton<T>(id: ServiceIdType<T>, factory: FactoryType<T>): void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Registers a transient service (new instance per resolve).
|
|
34
|
+
*/
|
|
35
|
+
register_transient<T>(id: ServiceIdType<T>, factory: FactoryType<T>): void;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Registers an instance directly.
|
|
39
|
+
*/
|
|
40
|
+
register_instance<T>(id: ServiceIdType<T>, instance: T): void;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolves a service from the container.
|
|
44
|
+
*/
|
|
45
|
+
resolve<T>(id: ServiceIdType<T>): T;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Checks if a service is registered.
|
|
49
|
+
*/
|
|
50
|
+
has<T>(id: ServiceIdType<T>): boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface RegistrationType<T> {
|
|
54
|
+
factory: FactoryType<T>;
|
|
55
|
+
singleton: boolean;
|
|
56
|
+
instance?: T;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a new dependency injection container.
|
|
61
|
+
*/
|
|
62
|
+
export function create_container(): ContainerType {
|
|
63
|
+
const registrations = new Map<symbol, RegistrationType<unknown>>();
|
|
64
|
+
|
|
65
|
+
const container: ContainerType = {
|
|
66
|
+
register_singleton<T>(id: ServiceIdType<T>, factory: FactoryType<T>): void {
|
|
67
|
+
registrations.set(id, { factory, singleton: true });
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
register_transient<T>(id: ServiceIdType<T>, factory: FactoryType<T>): void {
|
|
71
|
+
registrations.set(id, { factory, singleton: false });
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
register_instance<T>(id: ServiceIdType<T>, instance: T): void {
|
|
75
|
+
registrations.set(id, {
|
|
76
|
+
factory: () => instance,
|
|
77
|
+
singleton: true,
|
|
78
|
+
instance,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
resolve<T>(id: ServiceIdType<T>): T {
|
|
83
|
+
const registration = registrations.get(id) as RegistrationType<T> | undefined;
|
|
84
|
+
|
|
85
|
+
if (!registration) {
|
|
86
|
+
throw new Error(`Service not registered: ${id.toString()}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (registration.singleton) {
|
|
90
|
+
if (registration.instance === undefined) {
|
|
91
|
+
registration.instance = registration.factory(container);
|
|
92
|
+
}
|
|
93
|
+
return registration.instance;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return registration.factory(container);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
has<T>(id: ServiceIdType<T>): boolean {
|
|
100
|
+
return registrations.has(id);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return container;
|
|
105
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './container.js';
|
|
2
|
+
export * from './middleware.js';
|
|
3
|
+
export * from './command.js';
|
|
4
|
+
export * from './runner.js';
|
|
5
|
+
|
|
6
|
+
// Commands for programmatic use
|
|
7
|
+
export { apply_command } from './commands/apply.js';
|
|
8
|
+
export { validate_command } from './commands/validate.js';
|
|
9
|
+
export { init_command } from './commands/init.js';
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { type ResultType, failure } from '@kustodian/core';
|
|
2
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
3
|
+
import ora, { type Ora } from 'ora';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Log levels for configurable verbosity.
|
|
7
|
+
*/
|
|
8
|
+
export type LogLevelType = 'silent' | 'normal' | 'verbose' | 'debug';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Logger interface for middleware.
|
|
12
|
+
*/
|
|
13
|
+
export interface LoggerType {
|
|
14
|
+
level: LogLevelType;
|
|
15
|
+
debug(message: string, ...args: unknown[]): void;
|
|
16
|
+
info(message: string, ...args: unknown[]): void;
|
|
17
|
+
warn(message: string, ...args: unknown[]): void;
|
|
18
|
+
error(message: string, ...args: unknown[]): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Progress tracker interface.
|
|
23
|
+
*/
|
|
24
|
+
export interface ProgressType {
|
|
25
|
+
start(text: string): void;
|
|
26
|
+
update(text: string): void;
|
|
27
|
+
succeed(text?: string): void;
|
|
28
|
+
fail(text?: string): void;
|
|
29
|
+
stop(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Context passed through the middleware pipeline.
|
|
34
|
+
*/
|
|
35
|
+
export interface ContextType {
|
|
36
|
+
/**
|
|
37
|
+
* Command arguments.
|
|
38
|
+
*/
|
|
39
|
+
args: string[];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parsed options.
|
|
43
|
+
*/
|
|
44
|
+
options: Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Custom data added by middleware.
|
|
48
|
+
*/
|
|
49
|
+
data: Record<string, unknown>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether running in dry-run mode.
|
|
53
|
+
*/
|
|
54
|
+
dry_run?: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Logger instance (added by logging middleware).
|
|
58
|
+
*/
|
|
59
|
+
logger?: LoggerType;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Progress tracker (added by progress middleware).
|
|
63
|
+
*/
|
|
64
|
+
progress?: ProgressType;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Timing data (added by timing middleware).
|
|
68
|
+
*/
|
|
69
|
+
timing?: {
|
|
70
|
+
start_time: number;
|
|
71
|
+
duration_ms?: number;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Next function in the middleware chain.
|
|
77
|
+
*/
|
|
78
|
+
export type NextType = () => Promise<ResultType<void, KustodianErrorType>>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Middleware function type.
|
|
82
|
+
*/
|
|
83
|
+
export type MiddlewareType = (
|
|
84
|
+
ctx: ContextType,
|
|
85
|
+
next: NextType,
|
|
86
|
+
) => Promise<ResultType<void, KustodianErrorType>>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Creates a middleware pipeline.
|
|
90
|
+
*/
|
|
91
|
+
export function create_pipeline(middleware: MiddlewareType[]): MiddlewareType {
|
|
92
|
+
return async (ctx, next) => {
|
|
93
|
+
let index = -1;
|
|
94
|
+
|
|
95
|
+
const dispatch = async (i: number): Promise<ResultType<void, KustodianErrorType>> => {
|
|
96
|
+
if (i <= index) {
|
|
97
|
+
throw new Error('next() called multiple times');
|
|
98
|
+
}
|
|
99
|
+
index = i;
|
|
100
|
+
|
|
101
|
+
const fn = i < middleware.length ? middleware[i] : next;
|
|
102
|
+
if (!fn) {
|
|
103
|
+
return { success: true, value: undefined };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return fn(ctx, () => dispatch(i + 1));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return dispatch(0);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates a new empty context.
|
|
115
|
+
*/
|
|
116
|
+
export function create_context(
|
|
117
|
+
args: string[] = [],
|
|
118
|
+
options: Record<string, unknown> = {},
|
|
119
|
+
): ContextType {
|
|
120
|
+
return {
|
|
121
|
+
args,
|
|
122
|
+
options,
|
|
123
|
+
data: {},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Logger Implementation
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Creates a logger with the specified verbosity level.
|
|
133
|
+
*/
|
|
134
|
+
export function create_logger(level: LogLevelType = 'normal'): LoggerType {
|
|
135
|
+
const should_log = (target_level: LogLevelType): boolean => {
|
|
136
|
+
const levels: LogLevelType[] = ['silent', 'normal', 'verbose', 'debug'];
|
|
137
|
+
return levels.indexOf(level) >= levels.indexOf(target_level);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
level,
|
|
142
|
+
debug(message: string, ...args: unknown[]) {
|
|
143
|
+
if (should_log('debug')) {
|
|
144
|
+
console.log(`[DEBUG] ${message}`, ...args);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
info(message: string, ...args: unknown[]) {
|
|
148
|
+
if (should_log('normal')) {
|
|
149
|
+
console.log(message, ...args);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
warn(message: string, ...args: unknown[]) {
|
|
153
|
+
if (should_log('normal')) {
|
|
154
|
+
console.warn(`⚠ ${message}`, ...args);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
error(message: string, ...args: unknown[]) {
|
|
158
|
+
// Always log errors except in silent mode
|
|
159
|
+
if (level !== 'silent') {
|
|
160
|
+
console.error(`✗ ${message}`, ...args);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============================================================================
|
|
167
|
+
// Progress Tracker Implementation
|
|
168
|
+
// ============================================================================
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Creates a progress tracker using ora spinner.
|
|
172
|
+
*/
|
|
173
|
+
export function create_progress(enabled = true): ProgressType {
|
|
174
|
+
let spinner: Ora | null = null;
|
|
175
|
+
|
|
176
|
+
if (!enabled) {
|
|
177
|
+
// No-op implementation when progress is disabled
|
|
178
|
+
return {
|
|
179
|
+
start: () => {},
|
|
180
|
+
update: () => {},
|
|
181
|
+
succeed: () => {},
|
|
182
|
+
fail: () => {},
|
|
183
|
+
stop: () => {},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
start(text: string) {
|
|
189
|
+
spinner = ora({ text, color: 'cyan' }).start();
|
|
190
|
+
},
|
|
191
|
+
update(text: string) {
|
|
192
|
+
if (spinner) {
|
|
193
|
+
spinner.text = text;
|
|
194
|
+
} else {
|
|
195
|
+
spinner = ora({ text, color: 'cyan' }).start();
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
succeed(text?: string) {
|
|
199
|
+
if (spinner) {
|
|
200
|
+
spinner.succeed(text);
|
|
201
|
+
spinner = null;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
fail(text?: string) {
|
|
205
|
+
if (spinner) {
|
|
206
|
+
spinner.fail(text);
|
|
207
|
+
spinner = null;
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
stop() {
|
|
211
|
+
if (spinner) {
|
|
212
|
+
spinner.stop();
|
|
213
|
+
spinner = null;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Built-in Middleware
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Dry-run middleware - sets dry_run flag on context based on --dry-run option.
|
|
225
|
+
*/
|
|
226
|
+
export function dry_run_middleware(): MiddlewareType {
|
|
227
|
+
return async (ctx, next) => {
|
|
228
|
+
ctx.dry_run = ctx.options['dry-run'] === true;
|
|
229
|
+
|
|
230
|
+
if (ctx.dry_run && ctx.logger) {
|
|
231
|
+
ctx.logger.info('[DRY RUN] Preview mode - no changes will be made');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const result = await next();
|
|
235
|
+
|
|
236
|
+
if (ctx.dry_run && ctx.logger) {
|
|
237
|
+
ctx.logger.info('[DRY RUN] No changes were made');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Logging middleware - adds logger to context based on verbosity options.
|
|
246
|
+
*/
|
|
247
|
+
export function logging_middleware(): MiddlewareType {
|
|
248
|
+
return async (ctx, next) => {
|
|
249
|
+
let level: LogLevelType = 'normal';
|
|
250
|
+
|
|
251
|
+
if (ctx.options['silent'] === true) {
|
|
252
|
+
level = 'silent';
|
|
253
|
+
} else if (ctx.options['debug'] === true) {
|
|
254
|
+
level = 'debug';
|
|
255
|
+
} else if (ctx.options['verbose'] === true) {
|
|
256
|
+
level = 'verbose';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
ctx.logger = create_logger(level);
|
|
260
|
+
ctx.logger.debug('Starting command execution');
|
|
261
|
+
ctx.logger.debug('Options:', ctx.options);
|
|
262
|
+
|
|
263
|
+
const result = await next();
|
|
264
|
+
|
|
265
|
+
ctx.logger.debug('Command execution completed');
|
|
266
|
+
return result;
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Progress middleware - adds progress tracker to context.
|
|
272
|
+
*/
|
|
273
|
+
export function progress_middleware(): MiddlewareType {
|
|
274
|
+
return async (ctx, next) => {
|
|
275
|
+
const enabled = ctx.options['no-progress'] !== true;
|
|
276
|
+
ctx.progress = create_progress(enabled);
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
return await next();
|
|
280
|
+
} finally {
|
|
281
|
+
// Ensure spinner is stopped even if there's an error
|
|
282
|
+
ctx.progress.stop();
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Timing middleware - measures command execution duration.
|
|
289
|
+
*/
|
|
290
|
+
export function timing_middleware(): MiddlewareType {
|
|
291
|
+
return async (ctx, next) => {
|
|
292
|
+
const start_time = Date.now();
|
|
293
|
+
ctx.timing = { start_time };
|
|
294
|
+
|
|
295
|
+
const result = await next();
|
|
296
|
+
|
|
297
|
+
const duration_ms = Date.now() - start_time;
|
|
298
|
+
ctx.timing.duration_ms = duration_ms;
|
|
299
|
+
|
|
300
|
+
if (ctx.logger && ctx.logger.level !== 'silent') {
|
|
301
|
+
const seconds = (duration_ms / 1000).toFixed(1);
|
|
302
|
+
ctx.logger.info(`Completed in ${seconds}s`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return result;
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Error handling middleware - provides consistent error handling and recovery suggestions.
|
|
311
|
+
*/
|
|
312
|
+
export function error_handling_middleware(): MiddlewareType {
|
|
313
|
+
return async (ctx, next) => {
|
|
314
|
+
try {
|
|
315
|
+
const result = await next();
|
|
316
|
+
|
|
317
|
+
if (!result.success) {
|
|
318
|
+
const error = result.error;
|
|
319
|
+
|
|
320
|
+
if (ctx.logger) {
|
|
321
|
+
ctx.logger.error(`Error: ${error.message}`);
|
|
322
|
+
|
|
323
|
+
if (ctx.logger.level === 'debug' || ctx.logger.level === 'verbose') {
|
|
324
|
+
ctx.logger.debug(`Error code: ${error.code}`);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
console.error(`Error: ${error.message}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Provide recovery suggestions based on error code
|
|
331
|
+
const suggestions = get_error_suggestions(error.code);
|
|
332
|
+
if (suggestions.length > 0 && ctx.logger) {
|
|
333
|
+
ctx.logger.info('Suggestions:');
|
|
334
|
+
for (const suggestion of suggestions) {
|
|
335
|
+
ctx.logger.info(` - ${suggestion}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return result;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const err = error as Error;
|
|
343
|
+
|
|
344
|
+
if (ctx.logger) {
|
|
345
|
+
ctx.logger.error(`Unexpected error: ${err.message}`);
|
|
346
|
+
if (ctx.logger.level === 'debug') {
|
|
347
|
+
ctx.logger.debug('Stack trace:', err.stack);
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
console.error(`Unexpected error: ${err.message}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return failure({
|
|
354
|
+
code: 'UNEXPECTED_ERROR',
|
|
355
|
+
message: err.message,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Returns recovery suggestions for known error codes.
|
|
363
|
+
*/
|
|
364
|
+
function get_error_suggestions(code: string): string[] {
|
|
365
|
+
const suggestions: Record<string, string[]> = {
|
|
366
|
+
COMMAND_NOT_FOUND: ['Run "kustodian --help" to see available commands'],
|
|
367
|
+
NOT_FOUND: ['Check that the resource exists', 'Verify the name is spelled correctly'],
|
|
368
|
+
MISSING_DEPENDENCY: [
|
|
369
|
+
'Install required dependencies',
|
|
370
|
+
'Check that kubectl and flux CLIs are installed',
|
|
371
|
+
],
|
|
372
|
+
DEPENDENCY_VALIDATION_ERROR: [
|
|
373
|
+
'Check for circular dependencies between templates',
|
|
374
|
+
'Verify all depends_on references are valid',
|
|
375
|
+
],
|
|
376
|
+
FILE_NOT_FOUND: ['Verify the file path is correct', 'Check file permissions'],
|
|
377
|
+
FILE_WRITE_ERROR: ['Check directory permissions', 'Ensure disk has available space'],
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
return suggestions[code] ?? [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Creates the default middleware stack for CLI operations.
|
|
385
|
+
*
|
|
386
|
+
* Middleware order (outermost to innermost):
|
|
387
|
+
* 1. timing - measures total execution time
|
|
388
|
+
* 2. logging - configures logger
|
|
389
|
+
* 3. error_handling - catches and formats errors
|
|
390
|
+
* 4. dry_run - sets dry-run mode
|
|
391
|
+
* 5. progress - provides spinner for operations
|
|
392
|
+
*/
|
|
393
|
+
export function create_default_middleware(): MiddlewareType[] {
|
|
394
|
+
return [
|
|
395
|
+
timing_middleware(),
|
|
396
|
+
logging_middleware(),
|
|
397
|
+
error_handling_middleware(),
|
|
398
|
+
dry_run_middleware(),
|
|
399
|
+
progress_middleware(),
|
|
400
|
+
];
|
|
401
|
+
}
|