@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
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { success } from '@kustodian/core';
|
|
3
|
+
import { file_exists, write_file, write_yaml_file } from '@kustodian/loader';
|
|
4
|
+
|
|
5
|
+
import { define_command } from '../command.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Init command - initializes a new Kustodian project.
|
|
9
|
+
*/
|
|
10
|
+
export const init_command = define_command({
|
|
11
|
+
name: 'init',
|
|
12
|
+
description: 'Initialize a new Kustodian project',
|
|
13
|
+
arguments: [
|
|
14
|
+
{
|
|
15
|
+
name: 'name',
|
|
16
|
+
description: 'Project name (creates directory)',
|
|
17
|
+
required: true,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
options: [
|
|
21
|
+
{
|
|
22
|
+
name: 'force',
|
|
23
|
+
short: 'f',
|
|
24
|
+
description: 'Overwrite existing files',
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
default_value: false,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
handler: async (ctx) => {
|
|
30
|
+
const project_name = ctx.args[0];
|
|
31
|
+
const force = ctx.options['force'] as boolean;
|
|
32
|
+
|
|
33
|
+
if (!project_name) {
|
|
34
|
+
console.error('Error: Project name is required');
|
|
35
|
+
return {
|
|
36
|
+
success: false as const,
|
|
37
|
+
error: { code: 'INVALID_ARGS', message: 'Project name is required' },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const project_dir = path.resolve(project_name);
|
|
42
|
+
|
|
43
|
+
// Check if directory exists
|
|
44
|
+
if (!force && (await file_exists(project_dir))) {
|
|
45
|
+
console.error(`Error: Directory '${project_name}' already exists. Use --force to overwrite.`);
|
|
46
|
+
return {
|
|
47
|
+
success: false as const,
|
|
48
|
+
error: { code: 'ALREADY_EXISTS', message: `Directory '${project_name}' already exists` },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(`Creating project: ${project_name}`);
|
|
53
|
+
|
|
54
|
+
// Create kustodian.yaml
|
|
55
|
+
const kustodian_config = {
|
|
56
|
+
apiVersion: 'kustodian.io/v1',
|
|
57
|
+
kind: 'Project',
|
|
58
|
+
metadata: {
|
|
59
|
+
name: project_name,
|
|
60
|
+
},
|
|
61
|
+
spec: {
|
|
62
|
+
flux_namespace: 'flux-system',
|
|
63
|
+
oci_repository: 'flux-system',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let result = await write_yaml_file(path.join(project_dir, 'kustodian.yaml'), kustodian_config);
|
|
68
|
+
if (!result.success) {
|
|
69
|
+
console.error(`Error creating kustodian.yaml: ${result.error.message}`);
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
console.log(' Created kustodian.yaml');
|
|
73
|
+
|
|
74
|
+
// Create example template
|
|
75
|
+
const example_template = {
|
|
76
|
+
apiVersion: 'kustodian.io/v1',
|
|
77
|
+
kind: 'Template',
|
|
78
|
+
metadata: {
|
|
79
|
+
name: 'example',
|
|
80
|
+
},
|
|
81
|
+
spec: {
|
|
82
|
+
kustomizations: [
|
|
83
|
+
{
|
|
84
|
+
name: 'app',
|
|
85
|
+
path: './app',
|
|
86
|
+
namespace: {
|
|
87
|
+
default: 'example',
|
|
88
|
+
},
|
|
89
|
+
substitutions: [
|
|
90
|
+
{
|
|
91
|
+
name: 'replicas',
|
|
92
|
+
default: '1',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
result = await write_yaml_file(
|
|
101
|
+
path.join(project_dir, 'templates', 'example', 'template.yaml'),
|
|
102
|
+
example_template,
|
|
103
|
+
);
|
|
104
|
+
if (!result.success) {
|
|
105
|
+
console.error(`Error creating template: ${result.error.message}`);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
console.log(' Created templates/example/template.yaml');
|
|
109
|
+
|
|
110
|
+
// Create example kustomization.yaml
|
|
111
|
+
const example_kustomization = `apiVersion: kustomize.config.k8s.io/v1beta1
|
|
112
|
+
kind: Kustomization
|
|
113
|
+
resources:
|
|
114
|
+
- deployment.yaml
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
result = await write_file(
|
|
118
|
+
path.join(project_dir, 'templates', 'example', 'app', 'kustomization.yaml'),
|
|
119
|
+
example_kustomization,
|
|
120
|
+
);
|
|
121
|
+
if (!result.success) {
|
|
122
|
+
console.error(`Error creating kustomization.yaml: ${result.error.message}`);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
console.log(' Created templates/example/app/kustomization.yaml');
|
|
126
|
+
|
|
127
|
+
// Create example deployment
|
|
128
|
+
const example_deployment = `apiVersion: apps/v1
|
|
129
|
+
kind: Deployment
|
|
130
|
+
metadata:
|
|
131
|
+
name: example
|
|
132
|
+
spec:
|
|
133
|
+
replicas: \${replicas}
|
|
134
|
+
selector:
|
|
135
|
+
matchLabels:
|
|
136
|
+
app: example
|
|
137
|
+
template:
|
|
138
|
+
metadata:
|
|
139
|
+
labels:
|
|
140
|
+
app: example
|
|
141
|
+
spec:
|
|
142
|
+
containers:
|
|
143
|
+
- name: example
|
|
144
|
+
image: nginx:latest
|
|
145
|
+
ports:
|
|
146
|
+
- containerPort: 80
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
result = await write_file(
|
|
150
|
+
path.join(project_dir, 'templates', 'example', 'app', 'deployment.yaml'),
|
|
151
|
+
example_deployment,
|
|
152
|
+
);
|
|
153
|
+
if (!result.success) {
|
|
154
|
+
console.error(`Error creating deployment.yaml: ${result.error.message}`);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
console.log(' Created templates/example/app/deployment.yaml');
|
|
158
|
+
|
|
159
|
+
// Create example cluster
|
|
160
|
+
const example_cluster = {
|
|
161
|
+
apiVersion: 'kustodian.io/v1',
|
|
162
|
+
kind: 'Cluster',
|
|
163
|
+
metadata: {
|
|
164
|
+
name: 'local',
|
|
165
|
+
},
|
|
166
|
+
spec: {
|
|
167
|
+
domain: 'local.example.com',
|
|
168
|
+
oci: {
|
|
169
|
+
registry: 'ghcr.io',
|
|
170
|
+
repository: `your-org/${project_name}`,
|
|
171
|
+
tag_strategy: 'git-sha',
|
|
172
|
+
secret_ref: 'ghcr-auth',
|
|
173
|
+
},
|
|
174
|
+
templates: [
|
|
175
|
+
{
|
|
176
|
+
name: 'example',
|
|
177
|
+
enabled: true,
|
|
178
|
+
values: {
|
|
179
|
+
replicas: '2',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
result = await write_yaml_file(
|
|
187
|
+
path.join(project_dir, 'clusters', 'local', 'cluster.yaml'),
|
|
188
|
+
example_cluster,
|
|
189
|
+
);
|
|
190
|
+
if (!result.success) {
|
|
191
|
+
console.error(`Error creating cluster: ${result.error.message}`);
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
console.log(' Created clusters/local/cluster.yaml');
|
|
195
|
+
|
|
196
|
+
// Create .gitignore
|
|
197
|
+
const gitignore = `# Output
|
|
198
|
+
output/
|
|
199
|
+
|
|
200
|
+
# Dependencies
|
|
201
|
+
node_modules/
|
|
202
|
+
|
|
203
|
+
# Build artifacts
|
|
204
|
+
dist/
|
|
205
|
+
*.tsbuildinfo
|
|
206
|
+
|
|
207
|
+
# OS files
|
|
208
|
+
.DS_Store
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
result = await write_file(path.join(project_dir, '.gitignore'), gitignore);
|
|
212
|
+
if (!result.success) {
|
|
213
|
+
console.error(`Error creating .gitignore: ${result.error.message}`);
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
console.log(' Created .gitignore');
|
|
217
|
+
|
|
218
|
+
// Create GitHub Actions workflow
|
|
219
|
+
const workflow_yaml = `name: Deploy
|
|
220
|
+
|
|
221
|
+
on:
|
|
222
|
+
push:
|
|
223
|
+
branches: [main]
|
|
224
|
+
pull_request:
|
|
225
|
+
branches: [main]
|
|
226
|
+
|
|
227
|
+
jobs:
|
|
228
|
+
deploy:
|
|
229
|
+
runs-on: ubuntu-latest
|
|
230
|
+
permissions:
|
|
231
|
+
contents: read
|
|
232
|
+
packages: write
|
|
233
|
+
|
|
234
|
+
steps:
|
|
235
|
+
- name: Checkout
|
|
236
|
+
uses: actions/checkout@v4
|
|
237
|
+
|
|
238
|
+
- name: Setup Flux CLI
|
|
239
|
+
uses: fluxcd/flux2/action@main
|
|
240
|
+
|
|
241
|
+
- name: Login to GitHub Container Registry
|
|
242
|
+
uses: docker/login-action@v3
|
|
243
|
+
with:
|
|
244
|
+
registry: ghcr.io
|
|
245
|
+
username: \${{ github.actor }}
|
|
246
|
+
password: \${{ secrets.GITHUB_TOKEN }}
|
|
247
|
+
|
|
248
|
+
- name: Install kustodian
|
|
249
|
+
run: |
|
|
250
|
+
# TODO: Replace with actual installation method
|
|
251
|
+
# npm install -g @kustodian/cli
|
|
252
|
+
echo "Install kustodian CLI here"
|
|
253
|
+
|
|
254
|
+
- name: Validate configuration
|
|
255
|
+
run: |
|
|
256
|
+
# TODO: Run validation once kustodian is installed
|
|
257
|
+
echo "Validation step"
|
|
258
|
+
|
|
259
|
+
- name: Push artifacts (on main branch)
|
|
260
|
+
if: github.ref == 'refs/heads/main'
|
|
261
|
+
run: |
|
|
262
|
+
# TODO: Replace with actual push command once kustodian is installed
|
|
263
|
+
# for cluster in clusters/*/cluster.yaml; do
|
|
264
|
+
# cluster_name=$(basename $(dirname $cluster))
|
|
265
|
+
# echo "Pushing $cluster_name..."
|
|
266
|
+
# kustodian push --cluster $cluster_name
|
|
267
|
+
# done
|
|
268
|
+
echo "Push step (configure after installing kustodian)"
|
|
269
|
+
`;
|
|
270
|
+
|
|
271
|
+
result = await write_file(
|
|
272
|
+
path.join(project_dir, '.github', 'workflows', 'deploy.yaml'),
|
|
273
|
+
workflow_yaml,
|
|
274
|
+
);
|
|
275
|
+
if (!result.success) {
|
|
276
|
+
console.error(`Error creating workflow: ${result.error.message}`);
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
console.log(' Created .github/workflows/deploy.yaml');
|
|
280
|
+
|
|
281
|
+
console.log(`\n✓ Project '${project_name}' created successfully`);
|
|
282
|
+
console.log('\nNext steps:');
|
|
283
|
+
console.log(` 1. cd ${project_name}`);
|
|
284
|
+
console.log(
|
|
285
|
+
' 2. Initialize git repository: git init && git add . && git commit -m "Initial commit"',
|
|
286
|
+
);
|
|
287
|
+
console.log(' 3. Update clusters/local/cluster.yaml with your OCI registry details');
|
|
288
|
+
console.log(' 4. Create registry authentication secret in your cluster:');
|
|
289
|
+
console.log(' kubectl create secret docker-registry ghcr-auth \\');
|
|
290
|
+
console.log(' --docker-server=ghcr.io \\');
|
|
291
|
+
console.log(' --docker-username=<your-username> \\');
|
|
292
|
+
console.log(' --docker-password=<your-token>');
|
|
293
|
+
console.log(' 5. Generate and push: kustodian push --cluster local --dry-run');
|
|
294
|
+
console.log(' 6. Review output, then: kustodian push --cluster local');
|
|
295
|
+
console.log(' 7. Apply Flux resources: kubectl apply -f output/local/');
|
|
296
|
+
|
|
297
|
+
return success(undefined);
|
|
298
|
+
},
|
|
299
|
+
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { failure, is_success, success } from '@kustodian/core';
|
|
4
|
+
import { read_yaml_file, write_yaml_file } from '@kustodian/loader';
|
|
5
|
+
import { find_project_root, load_project } from '@kustodian/loader';
|
|
6
|
+
import {
|
|
7
|
+
check_version_update,
|
|
8
|
+
create_client_for_image,
|
|
9
|
+
filter_semver_tags,
|
|
10
|
+
parse_image_reference,
|
|
11
|
+
} from '@kustodian/registry';
|
|
12
|
+
import { type VersionSubstitutionType, is_version_substitution } from '@kustodian/schema';
|
|
13
|
+
|
|
14
|
+
import { define_command } from '../command.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Update result for a single substitution.
|
|
18
|
+
*/
|
|
19
|
+
interface UpdateResultType {
|
|
20
|
+
cluster: string;
|
|
21
|
+
template: string;
|
|
22
|
+
substitution: string;
|
|
23
|
+
image: string;
|
|
24
|
+
current: string;
|
|
25
|
+
latest: string;
|
|
26
|
+
constraint?: string | undefined;
|
|
27
|
+
updated: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Update command - checks and updates version substitutions.
|
|
32
|
+
*/
|
|
33
|
+
export const update_command = define_command({
|
|
34
|
+
name: 'update',
|
|
35
|
+
description: 'Check and update image version substitutions',
|
|
36
|
+
options: [
|
|
37
|
+
{
|
|
38
|
+
name: 'cluster',
|
|
39
|
+
short: 'c',
|
|
40
|
+
description: 'Cluster to update values for',
|
|
41
|
+
type: 'string',
|
|
42
|
+
required: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'project',
|
|
46
|
+
short: 'p',
|
|
47
|
+
description: 'Path to project root',
|
|
48
|
+
type: 'string',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'dry-run',
|
|
52
|
+
short: 'd',
|
|
53
|
+
description: 'Show what would be updated without making changes',
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
default_value: false,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'json',
|
|
59
|
+
description: 'Output results as JSON',
|
|
60
|
+
type: 'boolean',
|
|
61
|
+
default_value: false,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'substitution',
|
|
65
|
+
short: 's',
|
|
66
|
+
description: 'Only update specific substitution(s)',
|
|
67
|
+
type: 'string',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
handler: async (ctx) => {
|
|
71
|
+
const cluster_name = ctx.options['cluster'] as string;
|
|
72
|
+
const project_path = (ctx.options['project'] as string) || process.cwd();
|
|
73
|
+
const dry_run = ctx.options['dry-run'] as boolean;
|
|
74
|
+
const json_output = ctx.options['json'] as boolean;
|
|
75
|
+
const substitution_filter = ctx.options['substitution'] as string | undefined;
|
|
76
|
+
|
|
77
|
+
// Load project
|
|
78
|
+
const root_result = await find_project_root(project_path);
|
|
79
|
+
if (!is_success(root_result)) {
|
|
80
|
+
return root_result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const project_result = await load_project(root_result.value);
|
|
84
|
+
if (!is_success(project_result)) {
|
|
85
|
+
return project_result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const project = project_result.value;
|
|
89
|
+
const loaded_cluster = project.clusters.find((c) => c.cluster.metadata.name === cluster_name);
|
|
90
|
+
|
|
91
|
+
if (!loaded_cluster) {
|
|
92
|
+
console.error(`Cluster '${cluster_name}' not found`);
|
|
93
|
+
return failure({
|
|
94
|
+
code: 'NOT_FOUND',
|
|
95
|
+
message: `Cluster '${cluster_name}' not found`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Collect all version substitutions from templates enabled in this cluster
|
|
100
|
+
const version_subs: Array<{
|
|
101
|
+
template_name: string;
|
|
102
|
+
kustomization_name: string;
|
|
103
|
+
substitution: VersionSubstitutionType;
|
|
104
|
+
current_value: string | undefined;
|
|
105
|
+
}> = [];
|
|
106
|
+
|
|
107
|
+
for (const loaded_template of project.templates) {
|
|
108
|
+
const template = loaded_template.template;
|
|
109
|
+
const template_config = loaded_cluster.cluster.spec.templates?.find(
|
|
110
|
+
(t) => t.name === template.metadata.name,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Skip disabled templates
|
|
114
|
+
if (template_config?.enabled === false) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const kustomization of template.spec.kustomizations) {
|
|
119
|
+
for (const sub of kustomization.substitutions ?? []) {
|
|
120
|
+
if (is_version_substitution(sub)) {
|
|
121
|
+
if (substitution_filter && sub.name !== substitution_filter) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
version_subs.push({
|
|
126
|
+
template_name: template.metadata.name,
|
|
127
|
+
kustomization_name: kustomization.name,
|
|
128
|
+
substitution: sub,
|
|
129
|
+
current_value: template_config?.values?.[sub.name] ?? sub.default,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (version_subs.length === 0) {
|
|
137
|
+
if (!json_output) {
|
|
138
|
+
console.log('No version substitutions found.');
|
|
139
|
+
} else {
|
|
140
|
+
console.log('[]');
|
|
141
|
+
}
|
|
142
|
+
return success(undefined);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!json_output) {
|
|
146
|
+
console.log(`Found ${version_subs.length} version substitution(s) to check\n`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check each version substitution
|
|
150
|
+
const results: UpdateResultType[] = [];
|
|
151
|
+
const updates_to_apply: Map<string, Record<string, string>> = new Map();
|
|
152
|
+
|
|
153
|
+
for (const { template_name, substitution, current_value } of version_subs) {
|
|
154
|
+
const image_ref = parse_image_reference(substitution.registry.image);
|
|
155
|
+
const client = create_client_for_image(image_ref);
|
|
156
|
+
|
|
157
|
+
if (!json_output) {
|
|
158
|
+
console.log(`Checking ${substitution.name} (${substitution.registry.image})...`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const tags_result = await client.list_tags(image_ref);
|
|
162
|
+
if (!is_success(tags_result)) {
|
|
163
|
+
if (!json_output) {
|
|
164
|
+
console.error(` Failed to fetch tags: ${tags_result.error.message}`);
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const versions = filter_semver_tags(tags_result.value, {
|
|
170
|
+
exclude_prerelease: substitution.exclude_prerelease ?? true,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (versions.length === 0) {
|
|
174
|
+
if (!json_output) {
|
|
175
|
+
console.log(' No valid semver tags found');
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const current = current_value ?? substitution.default ?? '0.0.0';
|
|
181
|
+
const check = check_version_update(current, versions, substitution.constraint);
|
|
182
|
+
|
|
183
|
+
const result: UpdateResultType = {
|
|
184
|
+
cluster: cluster_name,
|
|
185
|
+
template: template_name,
|
|
186
|
+
substitution: substitution.name,
|
|
187
|
+
image: substitution.registry.image,
|
|
188
|
+
current: check.current_version,
|
|
189
|
+
latest: check.latest_version,
|
|
190
|
+
constraint: substitution.constraint,
|
|
191
|
+
updated: false,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (check.has_update) {
|
|
195
|
+
result.updated = !dry_run;
|
|
196
|
+
|
|
197
|
+
if (!dry_run) {
|
|
198
|
+
// Queue update for this template
|
|
199
|
+
const existing = updates_to_apply.get(template_name) ?? {};
|
|
200
|
+
existing[substitution.name] = check.latest_version;
|
|
201
|
+
updates_to_apply.set(template_name, existing);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!json_output) {
|
|
205
|
+
const action = dry_run ? 'available' : 'will update';
|
|
206
|
+
console.log(` ${current} -> ${check.latest_version} (${action})`);
|
|
207
|
+
}
|
|
208
|
+
} else if (!json_output) {
|
|
209
|
+
console.log(` ${current} (up to date)`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
results.push(result);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Apply updates to cluster.yaml
|
|
216
|
+
if (!dry_run && updates_to_apply.size > 0) {
|
|
217
|
+
const cluster_path = path.join(loaded_cluster.path, 'cluster.yaml');
|
|
218
|
+
const cluster_yaml_result = await read_yaml_file<Record<string, unknown>>(cluster_path);
|
|
219
|
+
|
|
220
|
+
if (!is_success(cluster_yaml_result)) {
|
|
221
|
+
return cluster_yaml_result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const cluster_data = cluster_yaml_result.value;
|
|
225
|
+
const spec = cluster_data['spec'] as Record<string, unknown> | undefined;
|
|
226
|
+
const templates = (spec?.['templates'] as Array<Record<string, unknown>> | undefined) ?? [];
|
|
227
|
+
|
|
228
|
+
for (const [template_name, values] of updates_to_apply) {
|
|
229
|
+
const template_idx = templates.findIndex((t) => t['name'] === template_name);
|
|
230
|
+
|
|
231
|
+
if (template_idx >= 0) {
|
|
232
|
+
const template = templates[template_idx];
|
|
233
|
+
const existing_values = (template?.['values'] as Record<string, string>) ?? {};
|
|
234
|
+
if (template) {
|
|
235
|
+
template['values'] = { ...existing_values, ...values };
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
// Add new template config with values
|
|
239
|
+
templates.push({
|
|
240
|
+
name: template_name,
|
|
241
|
+
values,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Ensure spec.templates exists
|
|
247
|
+
if (spec) {
|
|
248
|
+
spec['templates'] = templates;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const write_result = await write_yaml_file(cluster_path, cluster_data);
|
|
252
|
+
if (!is_success(write_result)) {
|
|
253
|
+
return write_result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!json_output) {
|
|
257
|
+
console.log(`\nUpdated ${cluster_path}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Output results
|
|
262
|
+
if (json_output) {
|
|
263
|
+
console.log(JSON.stringify(results, null, 2));
|
|
264
|
+
} else {
|
|
265
|
+
const update_count = results.filter(
|
|
266
|
+
(r) => r.updated || (dry_run && r.current !== r.latest),
|
|
267
|
+
).length;
|
|
268
|
+
const action = dry_run ? 'available' : 'applied';
|
|
269
|
+
console.log(`\n${update_count} update(s) ${action}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return success(undefined);
|
|
273
|
+
},
|
|
274
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { failure, success } from '@kustodian/core';
|
|
2
|
+
import { validate_dependency_graph } from '@kustodian/generator';
|
|
3
|
+
import { find_project_root, load_project } from '@kustodian/loader';
|
|
4
|
+
|
|
5
|
+
import { define_command } from '../command.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate command - validates cluster and template configurations.
|
|
9
|
+
*/
|
|
10
|
+
export const validate_command = define_command({
|
|
11
|
+
name: 'validate',
|
|
12
|
+
description: 'Validate cluster and template configurations',
|
|
13
|
+
options: [
|
|
14
|
+
{
|
|
15
|
+
name: 'cluster',
|
|
16
|
+
short: 'c',
|
|
17
|
+
description: 'Validate a specific cluster only',
|
|
18
|
+
type: 'string',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'project',
|
|
22
|
+
short: 'p',
|
|
23
|
+
description: 'Path to project root (defaults to current directory)',
|
|
24
|
+
type: 'string',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
handler: async (ctx) => {
|
|
28
|
+
const project_path = (ctx.options['project'] as string) || process.cwd();
|
|
29
|
+
const cluster_filter = ctx.options['cluster'] as string | undefined;
|
|
30
|
+
|
|
31
|
+
// Find project root
|
|
32
|
+
console.log('Finding project root...');
|
|
33
|
+
const root_result = await find_project_root(project_path);
|
|
34
|
+
if (!root_result.success) {
|
|
35
|
+
console.error(`Error: ${root_result.error.message}`);
|
|
36
|
+
console.error(
|
|
37
|
+
'Make sure you are in a Kustodian project directory with a kustodian.yaml file.',
|
|
38
|
+
);
|
|
39
|
+
return root_result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const project_root = root_result.value;
|
|
43
|
+
console.log(`Project root: ${project_root}`);
|
|
44
|
+
|
|
45
|
+
// Load project
|
|
46
|
+
console.log('Loading project...');
|
|
47
|
+
const project_result = await load_project(project_root);
|
|
48
|
+
if (!project_result.success) {
|
|
49
|
+
console.error(`Validation failed: ${project_result.error.message}`);
|
|
50
|
+
return project_result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const project = project_result.value;
|
|
54
|
+
|
|
55
|
+
// Report templates
|
|
56
|
+
console.log(`\nTemplates: ${project.templates.length} found`);
|
|
57
|
+
for (const t of project.templates) {
|
|
58
|
+
console.log(` ✓ ${t.template.metadata.name}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Report clusters
|
|
62
|
+
const clusters = cluster_filter
|
|
63
|
+
? project.clusters.filter((c) => c.cluster.metadata.name === cluster_filter)
|
|
64
|
+
: project.clusters;
|
|
65
|
+
|
|
66
|
+
if (cluster_filter && clusters.length === 0) {
|
|
67
|
+
console.error(`\nError: Cluster '${cluster_filter}' not found`);
|
|
68
|
+
return {
|
|
69
|
+
success: false as const,
|
|
70
|
+
error: { code: 'NOT_FOUND', message: `Cluster '${cluster_filter}' not found` },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`\nClusters: ${clusters.length} found`);
|
|
75
|
+
for (const c of clusters) {
|
|
76
|
+
console.log(` ✓ ${c.cluster.metadata.name}`);
|
|
77
|
+
if (c.cluster.spec.templates) {
|
|
78
|
+
for (const t of c.cluster.spec.templates) {
|
|
79
|
+
const status = t.enabled === false ? '(disabled)' : '';
|
|
80
|
+
console.log(` - ${t.name} ${status}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Validate dependency graph
|
|
86
|
+
console.log('\nValidating dependency graph...');
|
|
87
|
+
const templates = project.templates.map((t) => t.template);
|
|
88
|
+
const graph_result = validate_dependency_graph(templates);
|
|
89
|
+
|
|
90
|
+
if (!graph_result.valid) {
|
|
91
|
+
console.error('\nDependency validation errors:');
|
|
92
|
+
for (const error of graph_result.errors) {
|
|
93
|
+
console.error(` ✗ ${error.message}`);
|
|
94
|
+
}
|
|
95
|
+
return failure({
|
|
96
|
+
code: 'DEPENDENCY_VALIDATION_ERROR',
|
|
97
|
+
message: 'Dependency validation failed',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Show deployment order if there are dependencies
|
|
102
|
+
if (graph_result.topological_order && graph_result.topological_order.length > 0) {
|
|
103
|
+
console.log(`\nDeployment order (${graph_result.topological_order.length} kustomizations):`);
|
|
104
|
+
graph_result.topological_order.forEach((id, index) => {
|
|
105
|
+
console.log(` ${index + 1}. ${id}`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log('\n✓ All configurations are valid');
|
|
110
|
+
return success(undefined);
|
|
111
|
+
},
|
|
112
|
+
});
|