@kustodian/cli 1.0.0 → 1.2.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 +88 -0
- package/package.json +12 -10
- package/src/bin.ts +4 -1
- package/src/commands/apply.ts +36 -3
- package/src/commands/init.ts +1 -1
- package/src/commands/sources.ts +439 -0
- package/src/commands/update.ts +72 -12
- package/src/commands/validate.ts +41 -1
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.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "CLI framework with DI and middleware for Kustodian",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -37,14 +37,16 @@
|
|
|
37
37
|
"registry": "https://npm.pkg.github.com"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@kustodian/core": "
|
|
41
|
-
"@kustodian/generator": "
|
|
42
|
-
"@kustodian/loader": "
|
|
43
|
-
"@kustodian/nodes": "
|
|
44
|
-
"@kustodian/plugin-k0s": "
|
|
45
|
-
"@kustodian/plugins": "
|
|
46
|
-
"@kustodian/registry": "
|
|
47
|
-
"@kustodian/schema": "
|
|
48
|
-
"
|
|
40
|
+
"@kustodian/core": "1.1.0",
|
|
41
|
+
"@kustodian/generator": "1.2.0",
|
|
42
|
+
"@kustodian/loader": "1.1.0",
|
|
43
|
+
"@kustodian/nodes": "1.1.0",
|
|
44
|
+
"@kustodian/plugin-k0s": "1.1.0",
|
|
45
|
+
"@kustodian/plugins": "1.0.1",
|
|
46
|
+
"@kustodian/registry": "1.1.0",
|
|
47
|
+
"@kustodian/schema": "1.3.0",
|
|
48
|
+
"@kustodian/sources": "1.3.0",
|
|
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
|
|
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();
|
package/src/commands/apply.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { exec } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { is_success, success } from '@kustodian/core';
|
|
4
|
+
import { validate_template_requirements } from '@kustodian/generator';
|
|
4
5
|
import { find_project_root, load_project } from '@kustodian/loader';
|
|
5
6
|
import type { NodeListType } from '@kustodian/nodes';
|
|
6
7
|
import type { ClusterType } from '@kustodian/schema';
|
|
@@ -93,7 +94,7 @@ export const apply_command = define_command({
|
|
|
93
94
|
console.log('\n[1/3] Loading project configuration...');
|
|
94
95
|
|
|
95
96
|
const root_result = await find_project_root(project_path);
|
|
96
|
-
if (!root_result
|
|
97
|
+
if (!is_success(root_result)) {
|
|
97
98
|
console.error(` ✗ Error: ${root_result.error.message}`);
|
|
98
99
|
return root_result;
|
|
99
100
|
}
|
|
@@ -102,7 +103,7 @@ export const apply_command = define_command({
|
|
|
102
103
|
console.log(` → Project root: ${project_root}`);
|
|
103
104
|
|
|
104
105
|
const project_result = await load_project(project_root);
|
|
105
|
-
if (!project_result
|
|
106
|
+
if (!is_success(project_result)) {
|
|
106
107
|
console.error(` ✗ Error: ${project_result.error.message}`);
|
|
107
108
|
return project_result;
|
|
108
109
|
}
|
|
@@ -278,6 +279,38 @@ export const apply_command = define_command({
|
|
|
278
279
|
if (!skip_templates) {
|
|
279
280
|
console.log('\n[4/4] Deploying templates...');
|
|
280
281
|
|
|
282
|
+
// Validate template requirements
|
|
283
|
+
console.log(' → Validating template requirements...');
|
|
284
|
+
const enabled_template_refs =
|
|
285
|
+
loaded_cluster.cluster.spec.templates?.filter((t) => t.enabled !== false) || [];
|
|
286
|
+
|
|
287
|
+
if (enabled_template_refs.length > 0) {
|
|
288
|
+
const enabled_templates = project.templates
|
|
289
|
+
.filter((t) => enabled_template_refs.some((ref) => ref.name === t.template.metadata.name))
|
|
290
|
+
.map((t) => t.template);
|
|
291
|
+
|
|
292
|
+
const requirements_result = validate_template_requirements(
|
|
293
|
+
enabled_templates,
|
|
294
|
+
loaded_cluster.nodes,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
if (!requirements_result.valid) {
|
|
298
|
+
console.error(' ✗ Template requirement validation failed:');
|
|
299
|
+
for (const error of requirements_result.errors) {
|
|
300
|
+
console.error(` - ${error.template}: ${error.message}`);
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
success: false as const,
|
|
304
|
+
error: {
|
|
305
|
+
code: 'REQUIREMENT_VALIDATION_ERROR',
|
|
306
|
+
message: 'Template requirements not met',
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log(' ✓ All template requirements satisfied');
|
|
312
|
+
}
|
|
313
|
+
|
|
281
314
|
if (loaded_cluster.cluster.spec.oci) {
|
|
282
315
|
// OCI Mode - generate in memory and apply directly
|
|
283
316
|
console.log(' → Cluster uses OCI deployment');
|
|
@@ -296,7 +329,7 @@ export const apply_command = define_command({
|
|
|
296
329
|
{},
|
|
297
330
|
);
|
|
298
331
|
|
|
299
|
-
if (!gen_result
|
|
332
|
+
if (!is_success(gen_result)) {
|
|
300
333
|
console.error(` ✗ Generation failed: ${gen_result.error.message}`);
|
|
301
334
|
return gen_result;
|
|
302
335
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/commands/update.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
|
|
3
|
-
import { failure, is_success, success } from '@kustodian/core';
|
|
3
|
+
import { type ResultType, failure, is_success, success } from '@kustodian/core';
|
|
4
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
4
5
|
import { read_yaml_file, write_yaml_file } from '@kustodian/loader';
|
|
5
6
|
import { find_project_root, load_project } from '@kustodian/loader';
|
|
6
7
|
import {
|
|
8
|
+
type ImageReferenceType,
|
|
9
|
+
type RegistryClientType,
|
|
10
|
+
type TagInfoType,
|
|
7
11
|
check_version_update,
|
|
8
12
|
create_client_for_image,
|
|
13
|
+
create_helm_client,
|
|
9
14
|
filter_semver_tags,
|
|
10
15
|
parse_image_reference,
|
|
11
16
|
} from '@kustodian/registry';
|
|
12
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
type HelmSubstitutionType,
|
|
19
|
+
type VersionSubstitutionType,
|
|
20
|
+
is_helm_substitution,
|
|
21
|
+
is_version_substitution,
|
|
22
|
+
} from '@kustodian/schema';
|
|
13
23
|
|
|
14
24
|
import { define_command } from '../command.js';
|
|
15
25
|
|
|
@@ -20,7 +30,8 @@ interface UpdateResultType {
|
|
|
20
30
|
cluster: string;
|
|
21
31
|
template: string;
|
|
22
32
|
substitution: string;
|
|
23
|
-
|
|
33
|
+
source: string; // Image name or Helm chart reference
|
|
34
|
+
source_type: 'image' | 'helm';
|
|
24
35
|
current: string;
|
|
25
36
|
latest: string;
|
|
26
37
|
constraint?: string | undefined;
|
|
@@ -96,12 +107,13 @@ export const update_command = define_command({
|
|
|
96
107
|
});
|
|
97
108
|
}
|
|
98
109
|
|
|
99
|
-
// Collect all version substitutions from templates enabled in this cluster
|
|
110
|
+
// Collect all version and helm substitutions from templates enabled in this cluster
|
|
100
111
|
const version_subs: Array<{
|
|
101
112
|
template_name: string;
|
|
102
113
|
kustomization_name: string;
|
|
103
|
-
substitution: VersionSubstitutionType;
|
|
114
|
+
substitution: VersionSubstitutionType | HelmSubstitutionType;
|
|
104
115
|
current_value: string | undefined;
|
|
116
|
+
type: 'version' | 'helm';
|
|
105
117
|
}> = [];
|
|
106
118
|
|
|
107
119
|
for (const loaded_template of project.templates) {
|
|
@@ -127,6 +139,19 @@ export const update_command = define_command({
|
|
|
127
139
|
kustomization_name: kustomization.name,
|
|
128
140
|
substitution: sub,
|
|
129
141
|
current_value: template_config?.values?.[sub.name] ?? sub.default,
|
|
142
|
+
type: 'version',
|
|
143
|
+
});
|
|
144
|
+
} else if (is_helm_substitution(sub)) {
|
|
145
|
+
if (substitution_filter && sub.name !== substitution_filter) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
version_subs.push({
|
|
150
|
+
template_name: template.metadata.name,
|
|
151
|
+
kustomization_name: kustomization.name,
|
|
152
|
+
substitution: sub,
|
|
153
|
+
current_value: template_config?.values?.[sub.name] ?? sub.default,
|
|
154
|
+
type: 'helm',
|
|
130
155
|
});
|
|
131
156
|
}
|
|
132
157
|
}
|
|
@@ -146,19 +171,53 @@ export const update_command = define_command({
|
|
|
146
171
|
console.log(`Found ${version_subs.length} version substitution(s) to check\n`);
|
|
147
172
|
}
|
|
148
173
|
|
|
149
|
-
// Check each version substitution
|
|
174
|
+
// Check each version and helm substitution
|
|
150
175
|
const results: UpdateResultType[] = [];
|
|
151
176
|
const updates_to_apply: Map<string, Record<string, string>> = new Map();
|
|
152
177
|
|
|
153
|
-
for (const { template_name, substitution, current_value } of version_subs) {
|
|
154
|
-
|
|
155
|
-
|
|
178
|
+
for (const { template_name, substitution, current_value, type } of version_subs) {
|
|
179
|
+
let source_name: string;
|
|
180
|
+
let client: RegistryClientType;
|
|
181
|
+
|
|
182
|
+
if (type === 'version') {
|
|
183
|
+
const version_sub = substitution as VersionSubstitutionType;
|
|
184
|
+
const image_ref = parse_image_reference(version_sub.registry.image);
|
|
185
|
+
source_name = version_sub.registry.image;
|
|
186
|
+
client = create_client_for_image(image_ref);
|
|
187
|
+
} else {
|
|
188
|
+
// helm type
|
|
189
|
+
const helm_sub = substitution as HelmSubstitutionType;
|
|
190
|
+
source_name = helm_sub.helm.oci || helm_sub.helm.repository || '';
|
|
191
|
+
source_name = `${source_name}/${helm_sub.helm.chart}`;
|
|
192
|
+
// Create helm config object to satisfy exactOptionalPropertyTypes
|
|
193
|
+
const helm_config: { repository?: string; oci?: string; chart: string } = {
|
|
194
|
+
chart: helm_sub.helm.chart,
|
|
195
|
+
};
|
|
196
|
+
if (helm_sub.helm.repository) {
|
|
197
|
+
helm_config.repository = helm_sub.helm.repository;
|
|
198
|
+
}
|
|
199
|
+
if (helm_sub.helm.oci) {
|
|
200
|
+
helm_config.oci = helm_sub.helm.oci;
|
|
201
|
+
}
|
|
202
|
+
client = create_helm_client(helm_config);
|
|
203
|
+
}
|
|
156
204
|
|
|
157
205
|
if (!json_output) {
|
|
158
|
-
console.log(`Checking ${substitution.name} (${
|
|
206
|
+
console.log(`Checking ${substitution.name} (${source_name})...`);
|
|
159
207
|
}
|
|
160
208
|
|
|
161
|
-
|
|
209
|
+
// For version type, we need to pass the image reference
|
|
210
|
+
// For helm type, the parameter is optional and ignored
|
|
211
|
+
let tags_result: ResultType<TagInfoType[], KustodianErrorType>;
|
|
212
|
+
if (type === 'version') {
|
|
213
|
+
tags_result = await client.list_tags(parse_image_reference(source_name));
|
|
214
|
+
} else {
|
|
215
|
+
// Helm client - the list_tags parameter is optional for helm clients
|
|
216
|
+
type ListTagsFn = (
|
|
217
|
+
ref?: ImageReferenceType,
|
|
218
|
+
) => Promise<ResultType<TagInfoType[], KustodianErrorType>>;
|
|
219
|
+
tags_result = await (client.list_tags as ListTagsFn)(undefined);
|
|
220
|
+
}
|
|
162
221
|
if (!is_success(tags_result)) {
|
|
163
222
|
if (!json_output) {
|
|
164
223
|
console.error(` Failed to fetch tags: ${tags_result.error.message}`);
|
|
@@ -184,7 +243,8 @@ export const update_command = define_command({
|
|
|
184
243
|
cluster: cluster_name,
|
|
185
244
|
template: template_name,
|
|
186
245
|
substitution: substitution.name,
|
|
187
|
-
|
|
246
|
+
source: source_name,
|
|
247
|
+
source_type: type === 'version' ? 'image' : 'helm',
|
|
188
248
|
current: check.current_version,
|
|
189
249
|
latest: check.latest_version,
|
|
190
250
|
constraint: substitution.constraint,
|
package/src/commands/validate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { failure, success } from '@kustodian/core';
|
|
2
|
-
import { validate_dependency_graph } from '@kustodian/generator';
|
|
2
|
+
import { validate_dependency_graph, validate_template_requirements } from '@kustodian/generator';
|
|
3
3
|
import { find_project_root, load_project } from '@kustodian/loader';
|
|
4
4
|
|
|
5
5
|
import { define_command } from '../command.js';
|
|
@@ -82,6 +82,46 @@ export const validate_command = define_command({
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Validate template requirements for each cluster
|
|
86
|
+
console.log('\nValidating template requirements...');
|
|
87
|
+
let has_requirement_errors = false;
|
|
88
|
+
|
|
89
|
+
for (const cluster_data of clusters) {
|
|
90
|
+
const cluster_name = cluster_data.cluster.metadata.name;
|
|
91
|
+
const enabled_template_refs =
|
|
92
|
+
cluster_data.cluster.spec.templates?.filter((t) => t.enabled !== false) || [];
|
|
93
|
+
|
|
94
|
+
if (enabled_template_refs.length === 0) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get enabled templates
|
|
99
|
+
const enabled_templates = project.templates
|
|
100
|
+
.filter((t) => enabled_template_refs.some((ref) => ref.name === t.template.metadata.name))
|
|
101
|
+
.map((t) => t.template);
|
|
102
|
+
|
|
103
|
+
// Validate requirements
|
|
104
|
+
const requirements_result = validate_template_requirements(
|
|
105
|
+
enabled_templates,
|
|
106
|
+
cluster_data.nodes,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (!requirements_result.valid) {
|
|
110
|
+
has_requirement_errors = true;
|
|
111
|
+
console.error(`\nRequirement validation errors for cluster '${cluster_name}':`);
|
|
112
|
+
for (const error of requirements_result.errors) {
|
|
113
|
+
console.error(` ✗ ${error.template}: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (has_requirement_errors) {
|
|
119
|
+
return failure({
|
|
120
|
+
code: 'REQUIREMENT_VALIDATION_ERROR',
|
|
121
|
+
message: 'Template requirement validation failed',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
85
125
|
// Validate dependency graph
|
|
86
126
|
console.log('\nValidating dependency graph...');
|
|
87
127
|
const templates = project.templates.map((t) => t.template);
|