/
load.ts
151 lines (131 loc) · 3.79 KB
/
load.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import { loadAll } from "js-yaml";
import { readFile } from "node:fs/promises";
import { getResourceModule, ResourceKind } from "./module";
import logger, { LogLevel } from "@kosko/log";
import stringify from "fast-safe-stringify";
import { isRecord } from "@kosko/common-utils";
import { importModule } from "./import";
import defaultFetch from "./fetch";
/**
* Describes an object which seems to be a Kubernetes manifest.
*
* @public
*/
export interface Manifest extends ResourceKind {
[key: string]: unknown;
}
type ManifestConstructor = new (data: Manifest) => Manifest;
/**
* @public
*/
export interface LoadOptions {
transform?(this: void, manifest: Manifest): Manifest | null | undefined;
}
function isManifest(value: Record<string, unknown>): value is Manifest {
return (
typeof value.apiVersion === "string" &&
!!value.apiVersion &&
typeof value.kind === "string" &&
!!value.kind
);
}
async function getConstructor(
res: ResourceKind
): Promise<ManifestConstructor | undefined> {
const mod = await getResourceModule(res);
if (!mod) {
logger.log(LogLevel.Debug, "No resource modules", {
data: res
});
return;
}
try {
const result = await importModule(mod.path);
return result[mod.export];
} catch {
logger.log(LogLevel.Debug, "Failed to import the resource module", {
data: mod
});
return;
}
}
/**
* Loads a Kubernetes YAML file from a string.
*
* @public
*/
export async function loadString(
content: string,
options: LoadOptions = {}
): Promise<Manifest[]> {
const { transform = (x) => x } = options;
const input = loadAll(content).filter((x) => x != null);
const manifests: Manifest[] = [];
for (const entry of input) {
if (!isRecord(entry)) {
throw new Error(`The value must be an object: ${stringify(entry)}`);
}
if (!isManifest(entry)) {
throw new Error(`apiVersion and kind are required: ${stringify(entry)}`);
}
const Constructor = await getConstructor(entry);
const manifest = transform(Constructor ? new Constructor(entry) : entry);
if (manifest) {
manifests.push(manifest);
}
}
return manifests;
}
/**
* Loads a Kubernetes YAML file from `path`.
*
* @param path - Path to a Kubernetes YAML file.
* @public
*/
export function loadFile(path: string, options?: LoadOptions) {
return async (): Promise<Manifest[]> => {
const content = await readFile(path, "utf-8");
logger.log(LogLevel.Debug, `File loaded from: ${path}`);
return loadString(content, options);
};
}
/**
* {@link LoadOptions} and properties defined in {@link https://developer.mozilla.org/en-US/docs/Web/API/Request | Request} class.
*
* @privateRemarks
* This type exists because sometimes `RequestInit` is `any` when `DOM` type
* is not loaded. Using an interface instead of `LoadOptions & RequestInit`
* allows us to ignoring `RequestInit` when it is `any`.
*
* @public
*/
export interface LoadUrlOptions extends LoadOptions, RequestInit {
fetch?: typeof fetch;
}
/**
* Loads a Kubernetes YAML file from `url`.
*
* @remarks
* By default, this function uses `fetch` API defined in the global scope.
* On Node.js, if `global.fetch` is undefined, {@link https://github.com/node-fetch/node-fetch | node-fetch}
* will be used instead.
*
* @param url - URL to a Kubernetes YAML file.
* @public
*/
export function loadUrl(
url: RequestInfo,
options: LoadUrlOptions = {}
): () => Promise<Manifest[]> {
const { transform, fetch = defaultFetch, ...init } = options;
return async () => {
const res = await fetch(url, init);
logger.log(LogLevel.Debug, `Fetched YAML`, {
data: { url, status: res.status }
});
if (!res.ok) {
throw new Error(`Failed to fetch YAML file from: ${url}`);
}
return loadString(await res.text(), { transform });
};
}