@kustodian/cli 1.0.0 → 1.1.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/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @kustodian/cli
2
+
3
+ Command-line interface for Kustodian - a GitOps templating framework for Kubernetes with Flux CD.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add -g @kustodian/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ kustodian <command> [options]
15
+ ```
16
+
17
+ ## Commands
18
+
19
+ ### `init <name>`
20
+
21
+ Initialize a new Kustodian project with example templates and cluster configuration.
22
+
23
+ ```bash
24
+ kustodian init my-project
25
+ ```
26
+
27
+ Options:
28
+ - `--force, -f` - Overwrite existing files
29
+
30
+ ### `validate`
31
+
32
+ Validate cluster and template configurations, including dependency graph validation.
33
+
34
+ ```bash
35
+ kustodian validate
36
+ kustodian validate --cluster production
37
+ ```
38
+
39
+ Options:
40
+ - `--cluster, -c <name>` - Validate a specific cluster only
41
+ - `--project, -p <path>` - Path to project root (defaults to current directory)
42
+
43
+ ### `apply`
44
+
45
+ Apply full cluster configuration: bootstrap nodes, install Flux CD, and deploy templates.
46
+
47
+ ```bash
48
+ kustodian apply --cluster production
49
+ kustodian apply --cluster local --dry-run
50
+ ```
51
+
52
+ Options:
53
+ - `--cluster, -c <name>` - Cluster name to apply (required)
54
+ - `--provider, -P <name>` - Cluster provider for bootstrap (default: k0s)
55
+ - `--project, -p <path>` - Path to project root
56
+ - `--dry-run, -d` - Preview changes without applying
57
+ - `--skip-bootstrap` - Skip cluster bootstrap (use existing cluster)
58
+ - `--skip-flux` - Skip Flux CD installation
59
+ - `--skip-templates` - Skip template deployment
60
+
61
+ ### `update`
62
+
63
+ Check and update image version substitutions from container registries.
64
+
65
+ ```bash
66
+ kustodian update --cluster production
67
+ kustodian update --cluster production --dry-run
68
+ ```
69
+
70
+ Options:
71
+ - `--cluster, -c <name>` - Cluster to update values for (required)
72
+ - `--project, -p <path>` - Path to project root
73
+ - `--dry-run, -d` - Show what would be updated without making changes
74
+ - `--json` - Output results as JSON
75
+ - `--substitution, -s <name>` - Only update specific substitution(s)
76
+
77
+ ## Global Options
78
+
79
+ - `--help, -h` - Show help
80
+ - `--version, -v` - Show version
81
+
82
+ ## Links
83
+
84
+ - [Repository](https://github.com/lucasilverentand/kustodian)
85
+
86
+ ## License
87
+
88
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kustodian/cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "CLI framework with DI and middleware for Kustodian",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -45,6 +45,8 @@
45
45
  "@kustodian/plugins": "workspace:*",
46
46
  "@kustodian/registry": "workspace:*",
47
47
  "@kustodian/schema": "workspace:*",
48
- "ora": "^9.0.0"
48
+ "@kustodian/sources": "workspace:*",
49
+ "ora": "^9.0.0",
50
+ "yaml": "^2.8.2"
49
51
  }
50
52
  }
package/src/bin.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { apply_command } from './commands/apply.js';
4
4
  import { init_command } from './commands/init.js';
5
+ import { sources_command } from './commands/sources.js';
5
6
  import { update_command } from './commands/update.js';
6
7
  import { validate_command } from './commands/validate.js';
7
8
  import { create_container } from './container.js';
@@ -29,7 +30,8 @@ async function main() {
29
30
  console.log(
30
31
  ' apply Apply full cluster configuration (generates, pushes OCI, deploys)',
31
32
  );
32
- console.log(' update Check and update image version substitutions\n');
33
+ console.log(' update Check and update image version substitutions');
34
+ console.log(' sources Manage template sources (fetch, list, cache)\n');
33
35
  console.log('Options:');
34
36
  console.log(' --help, -h Show help');
35
37
  console.log(' --version, -v Show version\n');
@@ -52,6 +54,7 @@ async function main() {
52
54
  cli.command(validate_command);
53
55
  cli.command(apply_command);
54
56
  cli.command(update_command);
57
+ cli.command(sources_command);
55
58
 
56
59
  // Create container
57
60
  const container = create_container();
@@ -93,7 +93,7 @@ export const apply_command = define_command({
93
93
  console.log('\n[1/3] Loading project configuration...');
94
94
 
95
95
  const root_result = await find_project_root(project_path);
96
- if (!root_result.success) {
96
+ if (!is_success(root_result)) {
97
97
  console.error(` ✗ Error: ${root_result.error.message}`);
98
98
  return root_result;
99
99
  }
@@ -102,7 +102,7 @@ export const apply_command = define_command({
102
102
  console.log(` → Project root: ${project_root}`);
103
103
 
104
104
  const project_result = await load_project(project_root);
105
- if (!project_result.success) {
105
+ if (!is_success(project_result)) {
106
106
  console.error(` ✗ Error: ${project_result.error.message}`);
107
107
  return project_result;
108
108
  }
@@ -296,7 +296,7 @@ export const apply_command = define_command({
296
296
  {},
297
297
  );
298
298
 
299
- if (!gen_result.success) {
299
+ if (!is_success(gen_result)) {
300
300
  console.error(` ✗ Generation failed: ${gen_result.error.message}`);
301
301
  return gen_result;
302
302
  }
@@ -248,7 +248,7 @@ jobs:
248
248
  - name: Install kustodian
249
249
  run: |
250
250
  # TODO: Replace with actual installation method
251
- # npm install -g @kustodian/cli
251
+ # bun install -g @kustodian/cli
252
252
  echo "Install kustodian CLI here"
253
253
 
254
254
  - name: Validate configuration
@@ -0,0 +1,439 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { failure, success } from '@kustodian/core';
4
+ import { find_project_root } from '@kustodian/loader';
5
+ import type { TemplateSourceType } from '@kustodian/schema';
6
+ import {
7
+ DEFAULT_CACHE_DIR,
8
+ create_cache_manager,
9
+ get_fetcher_for_source,
10
+ load_templates_from_sources,
11
+ } from '@kustodian/sources';
12
+ import { parse } from 'yaml';
13
+
14
+ import { define_command } from '../command.js';
15
+
16
+ /**
17
+ * Project configuration with template sources.
18
+ */
19
+ interface ProjectConfigType {
20
+ spec?: {
21
+ template_sources?: TemplateSourceType[];
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Reads template sources from project config.
27
+ */
28
+ async function get_template_sources(project_root: string): Promise<TemplateSourceType[]> {
29
+ const config_path = path.join(project_root, 'kustodian.yaml');
30
+ try {
31
+ const content = await fs.readFile(config_path, 'utf-8');
32
+ const config = parse(content) as ProjectConfigType;
33
+ return config?.spec?.template_sources ?? [];
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Formats bytes to human-readable size.
41
+ */
42
+ function format_bytes(bytes: number): string {
43
+ if (bytes < 1024) return `${bytes} B`;
44
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
45
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
46
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
47
+ }
48
+
49
+ /**
50
+ * Sources command - manage template sources.
51
+ */
52
+ export const sources_command = define_command({
53
+ name: 'sources',
54
+ description: 'Manage template sources',
55
+ subcommands: [
56
+ // sources fetch
57
+ {
58
+ name: 'fetch',
59
+ description: 'Fetch or update template sources',
60
+ options: [
61
+ {
62
+ name: 'force',
63
+ short: 'f',
64
+ description: 'Force refresh all sources (ignore cache)',
65
+ type: 'boolean',
66
+ },
67
+ {
68
+ name: 'source',
69
+ short: 's',
70
+ description: 'Fetch a specific source only',
71
+ type: 'string',
72
+ },
73
+ {
74
+ name: 'project',
75
+ short: 'p',
76
+ description: 'Path to project root',
77
+ type: 'string',
78
+ },
79
+ ],
80
+ handler: async (ctx) => {
81
+ const project_path = (ctx.options['project'] as string) || process.cwd();
82
+ const force_refresh = ctx.options['force'] as boolean;
83
+ const source_filter = ctx.options['source'] as string | undefined;
84
+
85
+ // Find project root
86
+ const root_result = await find_project_root(project_path);
87
+ if (!root_result.success) {
88
+ console.error(`Error: ${root_result.error.message}`);
89
+ return root_result;
90
+ }
91
+
92
+ const project_root = root_result.value;
93
+
94
+ // Get template sources from project config
95
+ let sources = await get_template_sources(project_root);
96
+
97
+ if (sources.length === 0) {
98
+ console.log('No template sources configured in kustodian.yaml');
99
+ return success(undefined);
100
+ }
101
+
102
+ // Filter if specific source requested
103
+ if (source_filter) {
104
+ sources = sources.filter((s) => s.name === source_filter);
105
+ if (sources.length === 0) {
106
+ console.error(`Source '${source_filter}' not found in configuration`);
107
+ return failure({ code: 'NOT_FOUND', message: `Source '${source_filter}' not found` });
108
+ }
109
+ }
110
+
111
+ console.log(`Fetching ${sources.length} template source(s)...`);
112
+ if (force_refresh) {
113
+ console.log('(force refresh enabled)\n');
114
+ } else {
115
+ console.log('');
116
+ }
117
+
118
+ const cache_dir = path.join(project_root, DEFAULT_CACHE_DIR);
119
+ const result = await load_templates_from_sources(sources, {
120
+ cache_dir,
121
+ force_refresh,
122
+ });
123
+
124
+ if (!result.success) {
125
+ console.error(`\nError: ${result.error.message}`);
126
+ return result;
127
+ }
128
+
129
+ // Report results
130
+ for (const resolved of result.value.resolved) {
131
+ const status = resolved.fetch_result.from_cache ? '(cached)' : '(fetched)';
132
+ console.log(` ✓ ${resolved.source.name} @ ${resolved.fetch_result.version} ${status}`);
133
+ }
134
+
135
+ console.log(`\n✓ Loaded ${result.value.templates.length} template(s) from sources`);
136
+ return success(undefined);
137
+ },
138
+ },
139
+
140
+ // sources list
141
+ {
142
+ name: 'list',
143
+ description: 'List configured template sources',
144
+ options: [
145
+ {
146
+ name: 'cached',
147
+ short: 'c',
148
+ description: 'Show cached sources only',
149
+ type: 'boolean',
150
+ },
151
+ {
152
+ name: 'project',
153
+ short: 'p',
154
+ description: 'Path to project root',
155
+ type: 'string',
156
+ },
157
+ ],
158
+ handler: async (ctx) => {
159
+ const project_path = (ctx.options['project'] as string) || process.cwd();
160
+ const show_cached = ctx.options['cached'] as boolean;
161
+
162
+ // Find project root
163
+ const root_result = await find_project_root(project_path);
164
+ if (!root_result.success) {
165
+ console.error(`Error: ${root_result.error.message}`);
166
+ return root_result;
167
+ }
168
+
169
+ const project_root = root_result.value;
170
+ const cache_dir = path.join(project_root, DEFAULT_CACHE_DIR);
171
+
172
+ if (show_cached) {
173
+ // Show cached sources
174
+ const cache = create_cache_manager(cache_dir);
175
+ const entries_result = await cache.list();
176
+
177
+ if (!entries_result.success) {
178
+ console.error(`Error: ${entries_result.error.message}`);
179
+ return entries_result;
180
+ }
181
+
182
+ const entries = entries_result.value;
183
+ if (entries.length === 0) {
184
+ console.log('No cached sources');
185
+ return success(undefined);
186
+ }
187
+
188
+ console.log('Cached sources:\n');
189
+ for (const entry of entries) {
190
+ const expired = entry.expires_at && entry.expires_at < new Date() ? ' (expired)' : '';
191
+ const mutable = entry.expires_at ? '(mutable)' : '(immutable)';
192
+ console.log(` ${entry.source_name} @ ${entry.version}`);
193
+ console.log(` Type: ${entry.source_type} ${mutable}${expired}`);
194
+ console.log(` Fetched: ${entry.fetched_at.toISOString()}`);
195
+ console.log('');
196
+ }
197
+ } else {
198
+ // Show configured sources
199
+ const sources = await get_template_sources(project_root);
200
+
201
+ if (sources.length === 0) {
202
+ console.log('No template sources configured');
203
+ return success(undefined);
204
+ }
205
+
206
+ console.log('Configured sources:\n');
207
+ for (const source of sources) {
208
+ const type = source.git ? 'git' : source.http ? 'http' : source.oci ? 'oci' : 'unknown';
209
+ console.log(` ${source.name} (${type})`);
210
+
211
+ if (source.git) {
212
+ const ref = source.git.ref.tag ?? source.git.ref.branch ?? source.git.ref.commit;
213
+ console.log(` URL: ${source.git.url}`);
214
+ console.log(` Ref: ${ref}`);
215
+ if (source.git.path) console.log(` Path: ${source.git.path}`);
216
+ }
217
+ if (source.http) {
218
+ console.log(` URL: ${source.http.url}`);
219
+ if (source.http.checksum) console.log(` Checksum: ${source.http.checksum}`);
220
+ }
221
+ if (source.oci) {
222
+ const ref = source.oci.digest ?? source.oci.tag;
223
+ console.log(` Registry: ${source.oci.registry}/${source.oci.repository}`);
224
+ console.log(` Tag: ${ref}`);
225
+ }
226
+ if (source.ttl) console.log(` TTL: ${source.ttl}`);
227
+ console.log('');
228
+ }
229
+ }
230
+
231
+ return success(undefined);
232
+ },
233
+ },
234
+
235
+ // sources cache
236
+ {
237
+ name: 'cache',
238
+ description: 'Manage template cache',
239
+ subcommands: [
240
+ // sources cache info
241
+ {
242
+ name: 'info',
243
+ description: 'Show cache statistics',
244
+ options: [
245
+ {
246
+ name: 'project',
247
+ short: 'p',
248
+ description: 'Path to project root',
249
+ type: 'string',
250
+ },
251
+ ],
252
+ handler: async (ctx) => {
253
+ const project_path = (ctx.options['project'] as string) || process.cwd();
254
+
255
+ const root_result = await find_project_root(project_path);
256
+ if (!root_result.success) {
257
+ console.error(`Error: ${root_result.error.message}`);
258
+ return root_result;
259
+ }
260
+
261
+ const project_root = root_result.value;
262
+ const cache_dir = path.join(project_root, DEFAULT_CACHE_DIR);
263
+ const cache = create_cache_manager(cache_dir);
264
+
265
+ const entries_result = await cache.list();
266
+ const size_result = await cache.size();
267
+
268
+ if (!entries_result.success || !size_result.success) {
269
+ console.log('Cache directory not found or empty');
270
+ return success(undefined);
271
+ }
272
+
273
+ const entries = entries_result.value;
274
+ const total_size = size_result.value;
275
+ const expired = entries.filter((e) => e.expires_at && e.expires_at < new Date()).length;
276
+
277
+ console.log('Cache statistics:\n');
278
+ console.log(` Location: ${cache_dir}`);
279
+ console.log(` Total entries: ${entries.length}`);
280
+ console.log(` Expired entries: ${expired}`);
281
+ console.log(` Total size: ${format_bytes(total_size)}`);
282
+
283
+ return success(undefined);
284
+ },
285
+ },
286
+
287
+ // sources cache prune
288
+ {
289
+ name: 'prune',
290
+ description: 'Remove expired cache entries',
291
+ options: [
292
+ {
293
+ name: 'project',
294
+ short: 'p',
295
+ description: 'Path to project root',
296
+ type: 'string',
297
+ },
298
+ ],
299
+ handler: async (ctx) => {
300
+ const project_path = (ctx.options['project'] as string) || process.cwd();
301
+
302
+ const root_result = await find_project_root(project_path);
303
+ if (!root_result.success) {
304
+ console.error(`Error: ${root_result.error.message}`);
305
+ return root_result;
306
+ }
307
+
308
+ const project_root = root_result.value;
309
+ const cache_dir = path.join(project_root, DEFAULT_CACHE_DIR);
310
+ const cache = create_cache_manager(cache_dir);
311
+
312
+ console.log('Pruning expired cache entries...');
313
+ const result = await cache.prune();
314
+
315
+ if (!result.success) {
316
+ console.error(`Error: ${result.error.message}`);
317
+ return result;
318
+ }
319
+
320
+ console.log(`✓ Removed ${result.value} expired entries`);
321
+ return success(undefined);
322
+ },
323
+ },
324
+
325
+ // sources cache clear
326
+ {
327
+ name: 'clear',
328
+ description: 'Clear all cached templates',
329
+ options: [
330
+ {
331
+ name: 'project',
332
+ short: 'p',
333
+ description: 'Path to project root',
334
+ type: 'string',
335
+ },
336
+ ],
337
+ handler: async (ctx) => {
338
+ const project_path = (ctx.options['project'] as string) || process.cwd();
339
+
340
+ const root_result = await find_project_root(project_path);
341
+ if (!root_result.success) {
342
+ console.error(`Error: ${root_result.error.message}`);
343
+ return root_result;
344
+ }
345
+
346
+ const project_root = root_result.value;
347
+ const cache_dir = path.join(project_root, DEFAULT_CACHE_DIR);
348
+ const cache = create_cache_manager(cache_dir);
349
+
350
+ console.log('Clearing template cache...');
351
+ const result = await cache.clear();
352
+
353
+ if (!result.success) {
354
+ console.error(`Error: ${result.error.message}`);
355
+ return result;
356
+ }
357
+
358
+ console.log('✓ Cache cleared');
359
+ return success(undefined);
360
+ },
361
+ },
362
+ ],
363
+ },
364
+
365
+ // sources versions
366
+ {
367
+ name: 'versions',
368
+ description: 'List available versions for a source',
369
+ arguments: [
370
+ {
371
+ name: 'source',
372
+ description: 'Source name',
373
+ required: true,
374
+ },
375
+ ],
376
+ options: [
377
+ {
378
+ name: 'project',
379
+ short: 'p',
380
+ description: 'Path to project root',
381
+ type: 'string',
382
+ },
383
+ ],
384
+ handler: async (ctx) => {
385
+ const project_path = (ctx.options['project'] as string) || process.cwd();
386
+ const source_name = ctx.args[0] as string | undefined;
387
+
388
+ if (!source_name) {
389
+ console.error('Error: Source name is required');
390
+ return failure({ code: 'INVALID_ARGUMENT', message: 'Source name is required' });
391
+ }
392
+
393
+ const root_result = await find_project_root(project_path);
394
+ if (!root_result.success) {
395
+ console.error(`Error: ${root_result.error.message}`);
396
+ return root_result;
397
+ }
398
+
399
+ const project_root = root_result.value;
400
+ const sources = await get_template_sources(project_root);
401
+ const source = sources.find((s) => s.name === source_name);
402
+
403
+ if (!source) {
404
+ console.error(`Source '${source_name}' not found in configuration`);
405
+ return failure({ code: 'NOT_FOUND', message: `Source '${source_name}' not found` });
406
+ }
407
+
408
+ const fetcher = get_fetcher_for_source(source);
409
+
410
+ console.log(`Fetching versions for '${source_name}'...`);
411
+ const versions_result = await fetcher.list_versions(source);
412
+
413
+ if (!versions_result.success) {
414
+ console.error(`Error: ${versions_result.error.message}`);
415
+ return versions_result;
416
+ }
417
+
418
+ const versions = versions_result.value;
419
+ if (versions.length === 0) {
420
+ console.log('No versions found (this source type may not support version listing)');
421
+ return success(undefined);
422
+ }
423
+
424
+ console.log(`\nAvailable versions (${versions.length}):\n`);
425
+ for (const v of versions.slice(0, 50)) {
426
+ // Limit to 50
427
+ const digest = v.digest ? ` (${v.digest.slice(0, 12)})` : '';
428
+ console.log(` ${v.version}${digest}`);
429
+ }
430
+
431
+ if (versions.length > 50) {
432
+ console.log(` ... and ${versions.length - 50} more`);
433
+ }
434
+
435
+ return success(undefined);
436
+ },
437
+ },
438
+ ],
439
+ });