@kustodian/loader 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 +43 -0
- package/src/file.ts +155 -0
- package/src/index.ts +4 -0
- package/src/profile.ts +124 -0
- package/src/project.ts +348 -0
- package/src/yaml.ts +50 -0
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kustodian/loader",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "YAML file loading and validation for Kustodian",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"test:watch": "bun test --watch",
|
|
20
|
+
"typecheck": "bun run tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"kustodian",
|
|
24
|
+
"loader",
|
|
25
|
+
"yaml"
|
|
26
|
+
],
|
|
27
|
+
"author": "Luca Silverentand <luca@onezero.company>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/lucasilverentand/kustodian.git",
|
|
32
|
+
"directory": "packages/loader"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"registry": "https://npm.pkg.github.com"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@kustodian/core": "workspace:*",
|
|
39
|
+
"@kustodian/schema": "workspace:*",
|
|
40
|
+
"yaml": "^2.8.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {}
|
|
43
|
+
}
|
package/src/file.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { Errors, type ResultType, failure, from_promise, success } from '@kustodian/core';
|
|
5
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
6
|
+
|
|
7
|
+
import { parse_yaml, stringify_yaml } from './yaml.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a file exists.
|
|
11
|
+
*/
|
|
12
|
+
export async function file_exists(file_path: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(file_path);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks if a path is a directory.
|
|
23
|
+
*/
|
|
24
|
+
export async function is_directory(dir_path: string): Promise<boolean> {
|
|
25
|
+
try {
|
|
26
|
+
const stats = await fs.stat(dir_path);
|
|
27
|
+
return stats.isDirectory();
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reads a file and returns its contents as a string.
|
|
35
|
+
*/
|
|
36
|
+
export async function read_file(
|
|
37
|
+
file_path: string,
|
|
38
|
+
): Promise<ResultType<string, KustodianErrorType>> {
|
|
39
|
+
const exists = await file_exists(file_path);
|
|
40
|
+
if (!exists) {
|
|
41
|
+
return failure(Errors.file_not_found(file_path));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return from_promise(fs.readFile(file_path, 'utf-8'), (error) =>
|
|
45
|
+
Errors.file_read_error(file_path, error),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Writes content to a file, creating directories as needed.
|
|
51
|
+
*/
|
|
52
|
+
export async function write_file(
|
|
53
|
+
file_path: string,
|
|
54
|
+
content: string,
|
|
55
|
+
): Promise<ResultType<void, KustodianErrorType>> {
|
|
56
|
+
try {
|
|
57
|
+
const dir = path.dirname(file_path);
|
|
58
|
+
await fs.mkdir(dir, { recursive: true });
|
|
59
|
+
await fs.writeFile(file_path, content, 'utf-8');
|
|
60
|
+
return success(undefined);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return failure(Errors.file_write_error(file_path, error));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reads a YAML file and parses it.
|
|
68
|
+
*/
|
|
69
|
+
export async function read_yaml_file<T>(
|
|
70
|
+
file_path: string,
|
|
71
|
+
): Promise<ResultType<T, KustodianErrorType>> {
|
|
72
|
+
const content_result = await read_file(file_path);
|
|
73
|
+
if (!content_result.success) {
|
|
74
|
+
return content_result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return parse_yaml<T>(content_result.value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Reads a multi-document YAML file and parses it.
|
|
82
|
+
*/
|
|
83
|
+
export async function read_multi_yaml_file<T>(
|
|
84
|
+
file_path: string,
|
|
85
|
+
): Promise<ResultType<T[], KustodianErrorType>> {
|
|
86
|
+
const content_result = await read_file(file_path);
|
|
87
|
+
if (!content_result.success) {
|
|
88
|
+
return content_result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { parse_multi_yaml } = await import('./yaml.js');
|
|
92
|
+
return parse_multi_yaml<T>(content_result.value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Writes an object to a YAML file.
|
|
97
|
+
*/
|
|
98
|
+
export async function write_yaml_file<T>(
|
|
99
|
+
file_path: string,
|
|
100
|
+
data: T,
|
|
101
|
+
): Promise<ResultType<void, KustodianErrorType>> {
|
|
102
|
+
const yaml_result = stringify_yaml(data);
|
|
103
|
+
if (!yaml_result.success) {
|
|
104
|
+
return yaml_result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return write_file(file_path, yaml_result.value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Lists all files in a directory matching a pattern.
|
|
112
|
+
*/
|
|
113
|
+
export async function list_files(
|
|
114
|
+
dir_path: string,
|
|
115
|
+
extension?: string,
|
|
116
|
+
): Promise<ResultType<string[], KustodianErrorType>> {
|
|
117
|
+
const exists = await is_directory(dir_path);
|
|
118
|
+
if (!exists) {
|
|
119
|
+
return failure(Errors.not_found('Directory', dir_path));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const entries = await fs.readdir(dir_path, { withFileTypes: true });
|
|
124
|
+
let files = entries.filter((e) => e.isFile()).map((e) => path.join(dir_path, e.name));
|
|
125
|
+
|
|
126
|
+
if (extension) {
|
|
127
|
+
files = files.filter((f) => f.endsWith(extension));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return success(files);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return failure(Errors.file_read_error(dir_path, error));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Lists all subdirectories in a directory.
|
|
138
|
+
*/
|
|
139
|
+
export async function list_directories(
|
|
140
|
+
dir_path: string,
|
|
141
|
+
): Promise<ResultType<string[], KustodianErrorType>> {
|
|
142
|
+
const exists = await is_directory(dir_path);
|
|
143
|
+
if (!exists) {
|
|
144
|
+
return failure(Errors.not_found('Directory', dir_path));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const entries = await fs.readdir(dir_path, { withFileTypes: true });
|
|
149
|
+
const dirs = entries.filter((e) => e.isDirectory()).map((e) => path.join(dir_path, e.name));
|
|
150
|
+
|
|
151
|
+
return success(dirs);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return failure(Errors.file_read_error(dir_path, error));
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/index.ts
ADDED
package/src/profile.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { Errors, type ResultType, failure, is_success, success } from '@kustodian/core';
|
|
4
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
5
|
+
import {
|
|
6
|
+
type NodeProfileType,
|
|
7
|
+
node_profile_resource_to_profile,
|
|
8
|
+
validate_node_profile_resource,
|
|
9
|
+
} from '@kustodian/schema';
|
|
10
|
+
|
|
11
|
+
import { file_exists, is_directory, list_files, read_multi_yaml_file } from './file.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Standard directory name for profiles.
|
|
15
|
+
*/
|
|
16
|
+
export const PROFILES_DIR = 'profiles';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loaded profile with its source path.
|
|
20
|
+
*/
|
|
21
|
+
export interface LoadedProfileType {
|
|
22
|
+
path: string;
|
|
23
|
+
profile: NodeProfileType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Loads profiles from a single YAML file (supports multi-document).
|
|
28
|
+
*/
|
|
29
|
+
async function load_profiles_from_file(
|
|
30
|
+
file_path: string,
|
|
31
|
+
): Promise<ResultType<NodeProfileType[], KustodianErrorType>> {
|
|
32
|
+
const docs_result = await read_multi_yaml_file<unknown>(file_path);
|
|
33
|
+
|
|
34
|
+
if (!is_success(docs_result)) {
|
|
35
|
+
return docs_result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const profiles: NodeProfileType[] = [];
|
|
39
|
+
const errors: string[] = [];
|
|
40
|
+
|
|
41
|
+
for (const doc of docs_result.value) {
|
|
42
|
+
const validation = validate_node_profile_resource(doc);
|
|
43
|
+
if (!validation.success) {
|
|
44
|
+
const validation_errors = validation.error.issues.map(
|
|
45
|
+
(issue) => `${issue.path.join('.')}: ${issue.message}`,
|
|
46
|
+
);
|
|
47
|
+
errors.push(`${path.basename(file_path)}:\n ${validation_errors.join('\n ')}`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
profiles.push(node_profile_resource_to_profile(validation.data));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (errors.length > 0) {
|
|
55
|
+
return failure(Errors.validation_error(`Failed to load profiles:\n${errors.join('\n')}`));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return success(profiles);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Loads all profiles from the profiles directory.
|
|
63
|
+
*/
|
|
64
|
+
export async function load_all_profiles(
|
|
65
|
+
project_root: string,
|
|
66
|
+
): Promise<ResultType<Map<string, NodeProfileType>, KustodianErrorType>> {
|
|
67
|
+
const profiles_dir = path.join(project_root, PROFILES_DIR);
|
|
68
|
+
const profiles_map = new Map<string, NodeProfileType>();
|
|
69
|
+
|
|
70
|
+
// Return empty map if profiles directory doesn't exist
|
|
71
|
+
if (!(await file_exists(profiles_dir))) {
|
|
72
|
+
return success(profiles_map);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!(await is_directory(profiles_dir))) {
|
|
76
|
+
return success(profiles_map);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const errors: string[] = [];
|
|
80
|
+
|
|
81
|
+
// Load all YAML files from directory
|
|
82
|
+
const yml_files = await list_files(profiles_dir, '.yml');
|
|
83
|
+
const yaml_files = await list_files(profiles_dir, '.yaml');
|
|
84
|
+
|
|
85
|
+
const all_files = [
|
|
86
|
+
...(is_success(yml_files) ? yml_files.value : []),
|
|
87
|
+
...(is_success(yaml_files) ? yaml_files.value : []),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const file_path of all_files) {
|
|
91
|
+
const result = await load_profiles_from_file(file_path);
|
|
92
|
+
if (is_success(result)) {
|
|
93
|
+
for (const profile of result.value) {
|
|
94
|
+
if (profiles_map.has(profile.name)) {
|
|
95
|
+
errors.push(`Duplicate profile name: ${profile.name}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
profiles_map.set(profile.name, profile);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
errors.push(result.error.message);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (errors.length > 0) {
|
|
106
|
+
return failure(Errors.validation_error(`Failed to load profiles:\n${errors.join('\n')}`));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return success(profiles_map);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Gets a profile by name from a profiles map.
|
|
114
|
+
*/
|
|
115
|
+
export function get_profile(
|
|
116
|
+
profiles: Map<string, NodeProfileType>,
|
|
117
|
+
name: string,
|
|
118
|
+
): ResultType<NodeProfileType, KustodianErrorType> {
|
|
119
|
+
const profile = profiles.get(name);
|
|
120
|
+
if (!profile) {
|
|
121
|
+
return failure(Errors.profile_not_found(name));
|
|
122
|
+
}
|
|
123
|
+
return success(profile);
|
|
124
|
+
}
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { Errors, type ResultType, failure, is_success, success } from '@kustodian/core';
|
|
4
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
5
|
+
import {
|
|
6
|
+
type ClusterType,
|
|
7
|
+
type NodeProfileType,
|
|
8
|
+
type NodeSchemaType,
|
|
9
|
+
type TemplateType,
|
|
10
|
+
node_resource_to_node,
|
|
11
|
+
validate_cluster,
|
|
12
|
+
validate_node_resource,
|
|
13
|
+
validate_template,
|
|
14
|
+
} from '@kustodian/schema';
|
|
15
|
+
|
|
16
|
+
import { file_exists, list_directories, list_files, read_yaml_file } from './file.js';
|
|
17
|
+
import { load_all_profiles } from './profile.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Standard file names used in Kustodian projects.
|
|
21
|
+
*/
|
|
22
|
+
export const StandardFiles = {
|
|
23
|
+
TEMPLATE: 'template.yaml',
|
|
24
|
+
CLUSTER: 'cluster.yaml',
|
|
25
|
+
NODES: 'nodes.yaml',
|
|
26
|
+
PROJECT: 'kustodian.yaml',
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Standard directory names used in Kustodian projects.
|
|
31
|
+
*/
|
|
32
|
+
export const StandardDirs = {
|
|
33
|
+
TEMPLATES: 'templates',
|
|
34
|
+
CLUSTERS: 'clusters',
|
|
35
|
+
NODES: 'nodes',
|
|
36
|
+
PROFILES: 'profiles',
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Loaded template with its source path.
|
|
41
|
+
*/
|
|
42
|
+
export interface LoadedTemplateType {
|
|
43
|
+
path: string;
|
|
44
|
+
template: TemplateType;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Loaded cluster with its source path.
|
|
49
|
+
*/
|
|
50
|
+
export interface LoadedClusterType {
|
|
51
|
+
path: string;
|
|
52
|
+
cluster: ClusterType;
|
|
53
|
+
nodes: NodeSchemaType[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A fully loaded Kustodian project.
|
|
58
|
+
*/
|
|
59
|
+
export interface ProjectType {
|
|
60
|
+
root: string;
|
|
61
|
+
templates: LoadedTemplateType[];
|
|
62
|
+
clusters: LoadedClusterType[];
|
|
63
|
+
profiles: Map<string, NodeProfileType>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Finds the project root by looking for kustodian.yaml.
|
|
68
|
+
*/
|
|
69
|
+
export async function find_project_root(
|
|
70
|
+
start_path: string,
|
|
71
|
+
): Promise<ResultType<string, KustodianErrorType>> {
|
|
72
|
+
let current = path.resolve(start_path);
|
|
73
|
+
const root = path.parse(current).root;
|
|
74
|
+
|
|
75
|
+
while (current !== root) {
|
|
76
|
+
const project_file = path.join(current, StandardFiles.PROJECT);
|
|
77
|
+
if (await file_exists(project_file)) {
|
|
78
|
+
return success(current);
|
|
79
|
+
}
|
|
80
|
+
current = path.dirname(current);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return failure(
|
|
84
|
+
Errors.config_not_found('Project', `${StandardFiles.PROJECT} not found in parent directories`),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Loads a single template from its directory.
|
|
90
|
+
*/
|
|
91
|
+
export async function load_template(
|
|
92
|
+
template_dir: string,
|
|
93
|
+
): Promise<ResultType<LoadedTemplateType, KustodianErrorType>> {
|
|
94
|
+
const template_path = path.join(template_dir, StandardFiles.TEMPLATE);
|
|
95
|
+
const yaml_result = await read_yaml_file<unknown>(template_path);
|
|
96
|
+
|
|
97
|
+
if (!is_success(yaml_result)) {
|
|
98
|
+
return yaml_result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const validation = validate_template(yaml_result.value);
|
|
102
|
+
if (!validation.success) {
|
|
103
|
+
const errors = validation.error.issues.map(
|
|
104
|
+
(issue) => `${issue.path.join('.')}: ${issue.message}`,
|
|
105
|
+
);
|
|
106
|
+
return failure(Errors.schema_validation_error(errors));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return success({
|
|
110
|
+
path: template_dir,
|
|
111
|
+
template: validation.data,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Loads nodes from a single YAML file (supports multi-document).
|
|
117
|
+
*/
|
|
118
|
+
async function load_nodes_from_file(
|
|
119
|
+
file_path: string,
|
|
120
|
+
): Promise<ResultType<NodeSchemaType[], KustodianErrorType>> {
|
|
121
|
+
const { read_multi_yaml_file } = await import('./file.js');
|
|
122
|
+
const docs_result = await read_multi_yaml_file<unknown>(file_path);
|
|
123
|
+
|
|
124
|
+
if (!is_success(docs_result)) {
|
|
125
|
+
return docs_result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const nodes: NodeSchemaType[] = [];
|
|
129
|
+
const errors: string[] = [];
|
|
130
|
+
|
|
131
|
+
for (const doc of docs_result.value) {
|
|
132
|
+
const validation = validate_node_resource(doc);
|
|
133
|
+
if (!validation.success) {
|
|
134
|
+
const validation_errors = validation.error.issues.map(
|
|
135
|
+
(issue) => `${issue.path.join('.')}: ${issue.message}`,
|
|
136
|
+
);
|
|
137
|
+
errors.push(`${path.basename(file_path)}:\n ${validation_errors.join('\n ')}`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
nodes.push(node_resource_to_node(validation.data));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (errors.length > 0) {
|
|
145
|
+
return failure(Errors.validation_error(`Failed to load nodes:\n${errors.join('\n')}`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return success(nodes);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Loads all node files from specified paths (files or directories).
|
|
153
|
+
*/
|
|
154
|
+
export async function load_cluster_nodes(
|
|
155
|
+
cluster_dir: string,
|
|
156
|
+
node_file_paths?: string[],
|
|
157
|
+
): Promise<ResultType<NodeSchemaType[], KustodianErrorType>> {
|
|
158
|
+
const nodes: NodeSchemaType[] = [];
|
|
159
|
+
const errors: string[] = [];
|
|
160
|
+
|
|
161
|
+
// If no paths specified, try default nodes/ directory
|
|
162
|
+
const paths_to_scan = node_file_paths || [StandardDirs.NODES];
|
|
163
|
+
|
|
164
|
+
for (const ref_path of paths_to_scan) {
|
|
165
|
+
const full_path = path.isAbsolute(ref_path) ? ref_path : path.join(cluster_dir, ref_path);
|
|
166
|
+
|
|
167
|
+
// Check if it's a directory
|
|
168
|
+
const { is_directory } = await import('./file.js');
|
|
169
|
+
if (await is_directory(full_path)) {
|
|
170
|
+
// Load all YAML files from directory
|
|
171
|
+
const yml_files = await list_files(full_path, '.yml');
|
|
172
|
+
const yaml_files = await list_files(full_path, '.yaml');
|
|
173
|
+
|
|
174
|
+
const all_files = [
|
|
175
|
+
...(is_success(yml_files) ? yml_files.value : []),
|
|
176
|
+
...(is_success(yaml_files) ? yaml_files.value : []),
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
for (const file_path of all_files) {
|
|
180
|
+
const result = await load_nodes_from_file(file_path);
|
|
181
|
+
if (is_success(result)) {
|
|
182
|
+
nodes.push(...result.value);
|
|
183
|
+
} else {
|
|
184
|
+
errors.push(result.error.message);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else if (await file_exists(full_path)) {
|
|
188
|
+
// Load single file (may contain multiple documents)
|
|
189
|
+
const result = await load_nodes_from_file(full_path);
|
|
190
|
+
if (is_success(result)) {
|
|
191
|
+
nodes.push(...result.value);
|
|
192
|
+
} else {
|
|
193
|
+
errors.push(result.error.message);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (errors.length > 0) {
|
|
199
|
+
return failure(Errors.validation_error(`Failed to load nodes:\n${errors.join('\n')}`));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return success(nodes);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Loads a single cluster from its directory.
|
|
207
|
+
*/
|
|
208
|
+
export async function load_cluster(
|
|
209
|
+
cluster_dir: string,
|
|
210
|
+
): Promise<ResultType<LoadedClusterType, KustodianErrorType>> {
|
|
211
|
+
const cluster_path = path.join(cluster_dir, StandardFiles.CLUSTER);
|
|
212
|
+
const yaml_result = await read_yaml_file<unknown>(cluster_path);
|
|
213
|
+
|
|
214
|
+
if (!is_success(yaml_result)) {
|
|
215
|
+
return yaml_result;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const validation = validate_cluster(yaml_result.value);
|
|
219
|
+
if (!validation.success) {
|
|
220
|
+
const errors = validation.error.issues.map(
|
|
221
|
+
(issue) => `${issue.path.join('.')}: ${issue.message}`,
|
|
222
|
+
);
|
|
223
|
+
return failure(Errors.schema_validation_error(errors));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Load nodes from specified paths or default nodes/ directory
|
|
227
|
+
const node_file_paths = validation.data.spec.nodes;
|
|
228
|
+
const nodes_result = await load_cluster_nodes(cluster_dir, node_file_paths);
|
|
229
|
+
if (!is_success(nodes_result)) {
|
|
230
|
+
return nodes_result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return success({
|
|
234
|
+
path: cluster_dir,
|
|
235
|
+
cluster: validation.data,
|
|
236
|
+
nodes: nodes_result.value,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Loads all templates from the templates directory.
|
|
242
|
+
*/
|
|
243
|
+
export async function load_all_templates(
|
|
244
|
+
project_root: string,
|
|
245
|
+
): Promise<ResultType<LoadedTemplateType[], KustodianErrorType>> {
|
|
246
|
+
const templates_dir = path.join(project_root, StandardDirs.TEMPLATES);
|
|
247
|
+
const dirs_result = await list_directories(templates_dir);
|
|
248
|
+
|
|
249
|
+
if (!is_success(dirs_result)) {
|
|
250
|
+
// Return empty array if templates directory doesn't exist
|
|
251
|
+
if (dirs_result.error.code === 'NOT_FOUND') {
|
|
252
|
+
return success([]);
|
|
253
|
+
}
|
|
254
|
+
return dirs_result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const templates: LoadedTemplateType[] = [];
|
|
258
|
+
const errors: string[] = [];
|
|
259
|
+
|
|
260
|
+
for (const dir of dirs_result.value) {
|
|
261
|
+
const result = await load_template(dir);
|
|
262
|
+
if (is_success(result)) {
|
|
263
|
+
templates.push(result.value);
|
|
264
|
+
} else {
|
|
265
|
+
errors.push(`${path.basename(dir)}: ${result.error.message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (errors.length > 0) {
|
|
270
|
+
return failure(Errors.validation_error(`Failed to load templates:\n${errors.join('\n')}`));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return success(templates);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Loads all clusters from the clusters directory.
|
|
278
|
+
*/
|
|
279
|
+
export async function load_all_clusters(
|
|
280
|
+
project_root: string,
|
|
281
|
+
): Promise<ResultType<LoadedClusterType[], KustodianErrorType>> {
|
|
282
|
+
const clusters_dir = path.join(project_root, StandardDirs.CLUSTERS);
|
|
283
|
+
const dirs_result = await list_directories(clusters_dir);
|
|
284
|
+
|
|
285
|
+
if (!is_success(dirs_result)) {
|
|
286
|
+
// Return empty array if clusters directory doesn't exist
|
|
287
|
+
if (dirs_result.error.code === 'NOT_FOUND') {
|
|
288
|
+
return success([]);
|
|
289
|
+
}
|
|
290
|
+
return dirs_result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const clusters: LoadedClusterType[] = [];
|
|
294
|
+
const errors: string[] = [];
|
|
295
|
+
|
|
296
|
+
for (const dir of dirs_result.value) {
|
|
297
|
+
const result = await load_cluster(dir);
|
|
298
|
+
if (is_success(result)) {
|
|
299
|
+
clusters.push(result.value);
|
|
300
|
+
} else {
|
|
301
|
+
errors.push(`${path.basename(dir)}: ${result.error.message}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (errors.length > 0) {
|
|
306
|
+
return failure(Errors.validation_error(`Failed to load clusters:\n${errors.join('\n')}`));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return success(clusters);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Loads a complete Kustodian project.
|
|
314
|
+
*/
|
|
315
|
+
export async function load_project(
|
|
316
|
+
project_root: string,
|
|
317
|
+
): Promise<ResultType<ProjectType, KustodianErrorType>> {
|
|
318
|
+
// Verify project exists
|
|
319
|
+
const project_file = path.join(project_root, StandardFiles.PROJECT);
|
|
320
|
+
if (!(await file_exists(project_file))) {
|
|
321
|
+
return failure(Errors.config_not_found('Project', project_file));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Load profiles first (they may be referenced by nodes)
|
|
325
|
+
const profiles_result = await load_all_profiles(project_root);
|
|
326
|
+
if (!is_success(profiles_result)) {
|
|
327
|
+
return profiles_result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Load templates
|
|
331
|
+
const templates_result = await load_all_templates(project_root);
|
|
332
|
+
if (!is_success(templates_result)) {
|
|
333
|
+
return templates_result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Load clusters
|
|
337
|
+
const clusters_result = await load_all_clusters(project_root);
|
|
338
|
+
if (!is_success(clusters_result)) {
|
|
339
|
+
return clusters_result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return success({
|
|
343
|
+
root: project_root,
|
|
344
|
+
templates: templates_result.value,
|
|
345
|
+
clusters: clusters_result.value,
|
|
346
|
+
profiles: profiles_result.value,
|
|
347
|
+
});
|
|
348
|
+
}
|
package/src/yaml.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Errors, type ResultType, failure, success } from '@kustodian/core';
|
|
2
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
3
|
+
import { parse, parseAllDocuments, stringify } from 'yaml';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parses a YAML string into an object.
|
|
7
|
+
*/
|
|
8
|
+
export function parse_yaml<T>(content: string): ResultType<T, KustodianErrorType> {
|
|
9
|
+
try {
|
|
10
|
+
const result = parse(content) as T;
|
|
11
|
+
return success(result);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
return failure(
|
|
14
|
+
Errors.yaml_parse_error(error instanceof Error ? error.message : String(error), error),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parses a multi-document YAML string (separated by ---) into an array of objects.
|
|
21
|
+
*/
|
|
22
|
+
export function parse_multi_yaml<T>(content: string): ResultType<T[], KustodianErrorType> {
|
|
23
|
+
try {
|
|
24
|
+
const docs = parseAllDocuments(content);
|
|
25
|
+
const results = docs.map((doc) => doc.toJSON() as T);
|
|
26
|
+
return success(results);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return failure(
|
|
29
|
+
Errors.yaml_parse_error(error instanceof Error ? error.message : String(error), error),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Converts an object to a YAML string.
|
|
36
|
+
*/
|
|
37
|
+
export function stringify_yaml<T>(data: T): ResultType<string, KustodianErrorType> {
|
|
38
|
+
try {
|
|
39
|
+
const result = stringify(data, {
|
|
40
|
+
indent: 2,
|
|
41
|
+
lineWidth: 0,
|
|
42
|
+
singleQuote: false,
|
|
43
|
+
});
|
|
44
|
+
return success(result);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return failure(
|
|
47
|
+
Errors.parse_error('YAML', error instanceof Error ? error.message : String(error), error),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|