@intentius/chant-lexicon-k8s 0.1.0 → 0.1.5
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/dist/integrity.json +42 -39
- package/dist/manifest.json +1 -1
- package/dist/meta.json +121 -0
- package/dist/rules/k8s-helpers.ts +39 -0
- package/dist/rules/wk8401.ts +98 -0
- package/dist/rules/wk8402.ts +43 -0
- package/dist/rules/wk8403.ts +60 -0
- package/dist/types/index.d.ts +30 -0
- package/package.json +11 -7
- package/src/actions/actions.test.ts +1 -1
- package/src/codegen/generate-cli.ts +1 -1
- package/src/codegen/generate.ts +22 -0
- package/src/codegen/naming.test.ts +1 -1
- package/src/codegen/package.ts +2 -5
- package/src/codegen/snapshot.test.ts +1 -1
- package/src/codegen/typecheck.test.ts +1 -1
- package/src/composites/cockroachdb-region-stack.ts +553 -0
- package/src/composites/composites.test.ts +4 -4
- package/src/composites/index.ts +8 -0
- package/src/composites/ray-cluster.ts +590 -0
- package/src/composites/ray-job.ts +235 -0
- package/src/composites/ray-service.ts +271 -0
- package/src/coverage.test.ts +1 -1
- package/src/crd/crd-sources.ts +29 -0
- package/src/crd/loader.ts +13 -21
- package/src/crd/parser.test.ts +1 -1
- package/src/crd/parser.ts +17 -12
- package/src/default-labels.test.ts +1 -1
- package/src/generated/index.d.ts +30 -0
- package/src/generated/index.ts +13 -0
- package/src/generated/lexicon-k8s.json +121 -0
- package/src/import/generator.test.ts +1 -1
- package/src/import/parser.test.ts +1 -1
- package/src/import/roundtrip.test.ts +1 -1
- package/src/index.ts +4 -0
- package/src/lint/post-synth/k8s-helpers.test.ts +1 -1
- package/src/lint/post-synth/k8s-helpers.ts +39 -0
- package/src/lint/post-synth/post-synth.test.ts +149 -1
- package/src/lint/post-synth/wk8401.ts +98 -0
- package/src/lint/post-synth/wk8402.ts +43 -0
- package/src/lint/post-synth/wk8403.ts +60 -0
- package/src/lint/rules/rules.test.ts +1 -1
- package/src/lsp/completions.test.ts +1 -1
- package/src/lsp/hover.test.ts +1 -1
- package/src/package-cli.ts +1 -1
- package/src/plugin.test.ts +3 -3
- package/src/plugin.ts +7 -9
- package/src/serializer.test.ts +1 -1
- package/src/serializer.ts +2 -0
- package/src/skills/chant-k8s-ray.md +252 -0
- package/src/spec/fetch.test.ts +1 -1
- package/src/spec/parse.test.ts +1 -1
- package/src/validate-cli.ts +1 -1
- package/src/validate.test.ts +1 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RayJob composite — KubeRay RayJob CR + surrounding K8s infra.
|
|
3
|
+
*
|
|
4
|
+
* RayJob spins up an ephemeral Ray cluster, runs a single entrypoint command,
|
|
5
|
+
* then tears the cluster down. Ideal for batch training pipelines.
|
|
6
|
+
*
|
|
7
|
+
* Encodes the same production defaults as RayCluster (NetworkPolicy,
|
|
8
|
+
* preStop hooks, optional shared PVC, optional autoscaler RBAC).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
12
|
+
import {
|
|
13
|
+
ServiceAccount,
|
|
14
|
+
ClusterRole,
|
|
15
|
+
ClusterRoleBinding,
|
|
16
|
+
NetworkPolicy,
|
|
17
|
+
PersistentVolumeClaim,
|
|
18
|
+
RayJob as RayJobResource,
|
|
19
|
+
} from "../generated";
|
|
20
|
+
import {
|
|
21
|
+
type RayClusterSpec,
|
|
22
|
+
buildRayClusterParts,
|
|
23
|
+
buildRayNetworkPolicy,
|
|
24
|
+
} from "./ray-cluster";
|
|
25
|
+
|
|
26
|
+
export type { RayClusterSpec };
|
|
27
|
+
export type { ResourceSpec, HeadGroupSpec, WorkerGroupSpec } from "./ray-cluster";
|
|
28
|
+
|
|
29
|
+
export interface RayJobProps {
|
|
30
|
+
name: string;
|
|
31
|
+
namespace: string;
|
|
32
|
+
/**
|
|
33
|
+
* Entrypoint command to run on the Ray cluster.
|
|
34
|
+
* E.g. "python train.py --epochs 10"
|
|
35
|
+
*/
|
|
36
|
+
entrypoint: string;
|
|
37
|
+
cluster: RayClusterSpec;
|
|
38
|
+
/**
|
|
39
|
+
* Ray runtime environment YAML string.
|
|
40
|
+
* Prefer pre-built images over pip installs here — each pip install adds
|
|
41
|
+
* startup latency per worker on cold start.
|
|
42
|
+
*/
|
|
43
|
+
runtimeEnvYAML?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Tear down the cluster after the job finishes (default: true).
|
|
46
|
+
* Set false to keep the cluster alive for debugging.
|
|
47
|
+
*/
|
|
48
|
+
shutdownAfterJobFinishes?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* TTL in seconds before a finished RayJob is garbage-collected (default: 300).
|
|
51
|
+
*/
|
|
52
|
+
ttlSecondsAfterFinished?: number;
|
|
53
|
+
sharedStorage?: {
|
|
54
|
+
storageClass: string;
|
|
55
|
+
size: string;
|
|
56
|
+
mountPath?: string;
|
|
57
|
+
};
|
|
58
|
+
enableAutoscaler?: boolean;
|
|
59
|
+
spilloverBucket?: string;
|
|
60
|
+
labels?: Record<string, string>;
|
|
61
|
+
defaults?: {
|
|
62
|
+
serviceAccount?: Partial<Record<string, unknown>>;
|
|
63
|
+
clusterRole?: Partial<Record<string, unknown>>;
|
|
64
|
+
clusterRoleBinding?: Partial<Record<string, unknown>>;
|
|
65
|
+
networkPolicy?: Partial<Record<string, unknown>>;
|
|
66
|
+
pvc?: Partial<Record<string, unknown>>;
|
|
67
|
+
rayJob?: Partial<Record<string, unknown>>;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface RayJobResult {
|
|
72
|
+
serviceAccount: InstanceType<typeof ServiceAccount>;
|
|
73
|
+
clusterRole?: InstanceType<typeof ClusterRole>;
|
|
74
|
+
clusterRoleBinding?: InstanceType<typeof ClusterRoleBinding>;
|
|
75
|
+
networkPolicy: InstanceType<typeof NetworkPolicy>;
|
|
76
|
+
pvc?: InstanceType<typeof PersistentVolumeClaim>;
|
|
77
|
+
rayJob: InstanceType<typeof RayJobResource>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a RayJob composite — ephemeral Ray cluster for a single batch job.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* import { RayJob } from "@intentius/chant-lexicon-k8s";
|
|
86
|
+
*
|
|
87
|
+
* const job = RayJob({
|
|
88
|
+
* name: "train-resnet",
|
|
89
|
+
* namespace: "ray-system",
|
|
90
|
+
* entrypoint: "python train.py --epochs 50",
|
|
91
|
+
* cluster: {
|
|
92
|
+
* image: "us-docker.pkg.dev/my-project/ray-images/ray:2.40.0",
|
|
93
|
+
* head: { resources: { cpu: "2", memory: "8Gi" } },
|
|
94
|
+
* workerGroups: [
|
|
95
|
+
* { groupName: "gpu", replicas: 4,
|
|
96
|
+
* resources: { cpu: "8", memory: "32Gi", gpu: 1 },
|
|
97
|
+
* gpuTolerations: true },
|
|
98
|
+
* ],
|
|
99
|
+
* },
|
|
100
|
+
* spilloverBucket: "my-ray-spill",
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export const RayJob = Composite<RayJobProps>((props) => {
|
|
105
|
+
const {
|
|
106
|
+
name,
|
|
107
|
+
namespace,
|
|
108
|
+
entrypoint,
|
|
109
|
+
cluster,
|
|
110
|
+
runtimeEnvYAML,
|
|
111
|
+
shutdownAfterJobFinishes = true,
|
|
112
|
+
ttlSecondsAfterFinished = 300,
|
|
113
|
+
sharedStorage,
|
|
114
|
+
enableAutoscaler = false,
|
|
115
|
+
spilloverBucket,
|
|
116
|
+
labels: extraLabels = {},
|
|
117
|
+
defaults: defs,
|
|
118
|
+
} = props;
|
|
119
|
+
|
|
120
|
+
const saName = `${name}-head`;
|
|
121
|
+
const pvcName = sharedStorage ? `${name}-shared` : undefined;
|
|
122
|
+
const mountPath = sharedStorage?.mountPath;
|
|
123
|
+
|
|
124
|
+
const commonLabels: Record<string, string> = {
|
|
125
|
+
"app.kubernetes.io/name": name,
|
|
126
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
127
|
+
"app.kubernetes.io/component": "ray",
|
|
128
|
+
...extraLabels,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// -- ServiceAccount --
|
|
132
|
+
|
|
133
|
+
const serviceAccount = new ServiceAccount(mergeDefaults({
|
|
134
|
+
metadata: {
|
|
135
|
+
name: saName,
|
|
136
|
+
namespace,
|
|
137
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "service-account" },
|
|
138
|
+
},
|
|
139
|
+
}, defs?.serviceAccount));
|
|
140
|
+
|
|
141
|
+
// -- Autoscaler RBAC (optional) --
|
|
142
|
+
|
|
143
|
+
const clusterRole = enableAutoscaler ? new ClusterRole(mergeDefaults({
|
|
144
|
+
metadata: {
|
|
145
|
+
name: `${name}-autoscaler`,
|
|
146
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
147
|
+
},
|
|
148
|
+
rules: [
|
|
149
|
+
{
|
|
150
|
+
apiGroups: [""],
|
|
151
|
+
resources: ["pods"],
|
|
152
|
+
verbs: ["get", "list", "watch", "create", "delete", "patch"],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
apiGroups: [""],
|
|
156
|
+
resources: ["pods/status"],
|
|
157
|
+
verbs: ["get"],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
apiGroups: ["ray.io"],
|
|
161
|
+
resources: ["rayjobs"],
|
|
162
|
+
verbs: ["get", "list", "patch"],
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
}, defs?.clusterRole)) : undefined;
|
|
166
|
+
|
|
167
|
+
const clusterRoleBinding = enableAutoscaler ? new ClusterRoleBinding(mergeDefaults({
|
|
168
|
+
metadata: {
|
|
169
|
+
name: `${name}-autoscaler`,
|
|
170
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
171
|
+
},
|
|
172
|
+
roleRef: {
|
|
173
|
+
apiGroup: "rbac.authorization.k8s.io",
|
|
174
|
+
kind: "ClusterRole",
|
|
175
|
+
name: `${name}-autoscaler`,
|
|
176
|
+
},
|
|
177
|
+
subjects: [{ kind: "ServiceAccount", name: saName, namespace }],
|
|
178
|
+
}, defs?.clusterRoleBinding)) : undefined;
|
|
179
|
+
|
|
180
|
+
// -- NetworkPolicy --
|
|
181
|
+
|
|
182
|
+
const networkPolicy = buildRayNetworkPolicy(
|
|
183
|
+
name, namespace, commonLabels, false, defs?.networkPolicy,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// -- Shared PVC (optional) --
|
|
187
|
+
|
|
188
|
+
const pvc = sharedStorage ? new PersistentVolumeClaim(mergeDefaults({
|
|
189
|
+
metadata: {
|
|
190
|
+
name: pvcName!,
|
|
191
|
+
namespace,
|
|
192
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "storage" },
|
|
193
|
+
},
|
|
194
|
+
spec: {
|
|
195
|
+
accessModes: ["ReadWriteMany"],
|
|
196
|
+
storageClassName: sharedStorage.storageClass,
|
|
197
|
+
resources: { requests: { storage: sharedStorage.size } },
|
|
198
|
+
},
|
|
199
|
+
}, defs?.pvc)) : undefined;
|
|
200
|
+
|
|
201
|
+
// -- RayJob CR --
|
|
202
|
+
|
|
203
|
+
const { headGroupSpec, workerGroupSpecs, rayVersion } = buildRayClusterParts(
|
|
204
|
+
cluster, saName, spilloverBucket, pvcName, mountPath,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const rayJob = new RayJobResource(mergeDefaults({
|
|
208
|
+
metadata: {
|
|
209
|
+
name,
|
|
210
|
+
namespace,
|
|
211
|
+
labels: commonLabels,
|
|
212
|
+
},
|
|
213
|
+
spec: {
|
|
214
|
+
entrypoint,
|
|
215
|
+
...(runtimeEnvYAML && { runtimeEnvYAML }),
|
|
216
|
+
shutdownAfterJobFinishes,
|
|
217
|
+
ttlSecondsAfterFinished,
|
|
218
|
+
rayClusterSpec: {
|
|
219
|
+
...(rayVersion && { rayVersion }),
|
|
220
|
+
...(enableAutoscaler && { enableInTreeAutoscaling: true }),
|
|
221
|
+
headGroupSpec,
|
|
222
|
+
workerGroupSpecs,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
}, defs?.rayJob));
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
serviceAccount,
|
|
229
|
+
...(clusterRole && { clusterRole }),
|
|
230
|
+
...(clusterRoleBinding && { clusterRoleBinding }),
|
|
231
|
+
networkPolicy,
|
|
232
|
+
...(pvc && { pvc }),
|
|
233
|
+
rayJob,
|
|
234
|
+
};
|
|
235
|
+
}, "RayJob");
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RayService composite — KubeRay RayService CR + surrounding K8s infra.
|
|
3
|
+
*
|
|
4
|
+
* RayService manages a persistent Ray Serve HTTP endpoint with zero-downtime
|
|
5
|
+
* blue-green upgrades. Ideal for online inference and serving workloads.
|
|
6
|
+
*
|
|
7
|
+
* Encodes the same production defaults as RayCluster (NetworkPolicy, preStop
|
|
8
|
+
* hooks, PDB, optional shared PVC, optional autoscaler RBAC) plus a
|
|
9
|
+
* LoadBalancer Service exposing the Serve HTTP endpoint on port 8000.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
13
|
+
import {
|
|
14
|
+
ServiceAccount,
|
|
15
|
+
ClusterRole,
|
|
16
|
+
ClusterRoleBinding,
|
|
17
|
+
NetworkPolicy,
|
|
18
|
+
PodDisruptionBudget,
|
|
19
|
+
PersistentVolumeClaim,
|
|
20
|
+
Service,
|
|
21
|
+
RayService as RayServiceResource,
|
|
22
|
+
} from "../generated";
|
|
23
|
+
import {
|
|
24
|
+
type RayClusterSpec,
|
|
25
|
+
buildRayClusterParts,
|
|
26
|
+
buildRayNetworkPolicy,
|
|
27
|
+
} from "./ray-cluster";
|
|
28
|
+
|
|
29
|
+
export type { RayClusterSpec };
|
|
30
|
+
export type { ResourceSpec, HeadGroupSpec, WorkerGroupSpec } from "./ray-cluster";
|
|
31
|
+
|
|
32
|
+
export interface RayServiceProps {
|
|
33
|
+
name: string;
|
|
34
|
+
namespace: string;
|
|
35
|
+
/**
|
|
36
|
+
* Ray Serve application config in YAML format.
|
|
37
|
+
* Passed verbatim to spec.serveConfigV2.
|
|
38
|
+
* See https://docs.ray.io/en/latest/serve/production-guide/config.html
|
|
39
|
+
*/
|
|
40
|
+
serveConfigV2: string;
|
|
41
|
+
cluster: RayClusterSpec;
|
|
42
|
+
sharedStorage?: {
|
|
43
|
+
storageClass: string;
|
|
44
|
+
size: string;
|
|
45
|
+
mountPath?: string;
|
|
46
|
+
};
|
|
47
|
+
enableAutoscaler?: boolean;
|
|
48
|
+
spilloverBucket?: string;
|
|
49
|
+
labels?: Record<string, string>;
|
|
50
|
+
defaults?: {
|
|
51
|
+
serviceAccount?: Partial<Record<string, unknown>>;
|
|
52
|
+
clusterRole?: Partial<Record<string, unknown>>;
|
|
53
|
+
clusterRoleBinding?: Partial<Record<string, unknown>>;
|
|
54
|
+
networkPolicy?: Partial<Record<string, unknown>>;
|
|
55
|
+
pdb?: Partial<Record<string, unknown>>;
|
|
56
|
+
pvc?: Partial<Record<string, unknown>>;
|
|
57
|
+
serveService?: Partial<Record<string, unknown>>;
|
|
58
|
+
rayService?: Partial<Record<string, unknown>>;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RayServiceResult {
|
|
63
|
+
serviceAccount: InstanceType<typeof ServiceAccount>;
|
|
64
|
+
clusterRole?: InstanceType<typeof ClusterRole>;
|
|
65
|
+
clusterRoleBinding?: InstanceType<typeof ClusterRoleBinding>;
|
|
66
|
+
networkPolicy: InstanceType<typeof NetworkPolicy>;
|
|
67
|
+
pdb: InstanceType<typeof PodDisruptionBudget>;
|
|
68
|
+
pvc?: InstanceType<typeof PersistentVolumeClaim>;
|
|
69
|
+
/** LoadBalancer Service exposing Ray Serve on port 8000. */
|
|
70
|
+
serveService: InstanceType<typeof Service>;
|
|
71
|
+
rayService: InstanceType<typeof RayServiceResource>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a RayService composite — persistent Ray Serve endpoint with
|
|
76
|
+
* zero-downtime blue-green upgrades.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* import { RayService } from "@intentius/chant-lexicon-k8s";
|
|
81
|
+
*
|
|
82
|
+
* const svc = RayService({
|
|
83
|
+
* name: "inference",
|
|
84
|
+
* namespace: "ray-system",
|
|
85
|
+
* serveConfigV2: `
|
|
86
|
+
* applications:
|
|
87
|
+
* - name: classifier
|
|
88
|
+
* import_path: app:deployment
|
|
89
|
+
* deployments:
|
|
90
|
+
* - name: Classifier
|
|
91
|
+
* num_replicas: 2
|
|
92
|
+
* `,
|
|
93
|
+
* cluster: {
|
|
94
|
+
* image: "us-docker.pkg.dev/my-project/ray-images/ray:2.40.0",
|
|
95
|
+
* head: { resources: { cpu: "2", memory: "8Gi" } },
|
|
96
|
+
* workerGroups: [
|
|
97
|
+
* { groupName: "gpu", replicas: 2,
|
|
98
|
+
* resources: { cpu: "4", memory: "16Gi", gpu: 1 },
|
|
99
|
+
* gpuTolerations: true },
|
|
100
|
+
* ],
|
|
101
|
+
* },
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export const RayService = Composite<RayServiceProps>((props) => {
|
|
106
|
+
const {
|
|
107
|
+
name,
|
|
108
|
+
namespace,
|
|
109
|
+
serveConfigV2,
|
|
110
|
+
cluster,
|
|
111
|
+
sharedStorage,
|
|
112
|
+
enableAutoscaler = false,
|
|
113
|
+
spilloverBucket,
|
|
114
|
+
labels: extraLabels = {},
|
|
115
|
+
defaults: defs,
|
|
116
|
+
} = props;
|
|
117
|
+
|
|
118
|
+
const saName = `${name}-head`;
|
|
119
|
+
const pvcName = sharedStorage ? `${name}-shared` : undefined;
|
|
120
|
+
const mountPath = sharedStorage?.mountPath;
|
|
121
|
+
|
|
122
|
+
const commonLabels: Record<string, string> = {
|
|
123
|
+
"app.kubernetes.io/name": name,
|
|
124
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
125
|
+
"app.kubernetes.io/component": "ray",
|
|
126
|
+
...extraLabels,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// -- ServiceAccount --
|
|
130
|
+
|
|
131
|
+
const serviceAccount = new ServiceAccount(mergeDefaults({
|
|
132
|
+
metadata: {
|
|
133
|
+
name: saName,
|
|
134
|
+
namespace,
|
|
135
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "service-account" },
|
|
136
|
+
},
|
|
137
|
+
}, defs?.serviceAccount));
|
|
138
|
+
|
|
139
|
+
// -- Autoscaler RBAC (optional) --
|
|
140
|
+
|
|
141
|
+
const clusterRole = enableAutoscaler ? new ClusterRole(mergeDefaults({
|
|
142
|
+
metadata: {
|
|
143
|
+
name: `${name}-autoscaler`,
|
|
144
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
145
|
+
},
|
|
146
|
+
rules: [
|
|
147
|
+
{
|
|
148
|
+
apiGroups: [""],
|
|
149
|
+
resources: ["pods"],
|
|
150
|
+
verbs: ["get", "list", "watch", "create", "delete", "patch"],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
apiGroups: [""],
|
|
154
|
+
resources: ["pods/status"],
|
|
155
|
+
verbs: ["get"],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
apiGroups: ["ray.io"],
|
|
159
|
+
resources: ["rayservices"],
|
|
160
|
+
verbs: ["get", "list", "patch"],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
}, defs?.clusterRole)) : undefined;
|
|
164
|
+
|
|
165
|
+
const clusterRoleBinding = enableAutoscaler ? new ClusterRoleBinding(mergeDefaults({
|
|
166
|
+
metadata: {
|
|
167
|
+
name: `${name}-autoscaler`,
|
|
168
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "rbac" },
|
|
169
|
+
},
|
|
170
|
+
roleRef: {
|
|
171
|
+
apiGroup: "rbac.authorization.k8s.io",
|
|
172
|
+
kind: "ClusterRole",
|
|
173
|
+
name: `${name}-autoscaler`,
|
|
174
|
+
},
|
|
175
|
+
subjects: [{ kind: "ServiceAccount", name: saName, namespace }],
|
|
176
|
+
}, defs?.clusterRoleBinding)) : undefined;
|
|
177
|
+
|
|
178
|
+
// -- NetworkPolicy --
|
|
179
|
+
// RayService exposes port 8000 (Serve HTTP) externally via LoadBalancer,
|
|
180
|
+
// so we allow ingress from outside the cluster on that port.
|
|
181
|
+
|
|
182
|
+
const networkPolicy = buildRayNetworkPolicy(
|
|
183
|
+
name, namespace, commonLabels, false, defs?.networkPolicy,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// -- PodDisruptionBudget (head) --
|
|
187
|
+
|
|
188
|
+
const pdb = new PodDisruptionBudget(mergeDefaults({
|
|
189
|
+
metadata: {
|
|
190
|
+
name: `${name}-head`,
|
|
191
|
+
namespace,
|
|
192
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "disruption-budget" },
|
|
193
|
+
},
|
|
194
|
+
spec: {
|
|
195
|
+
minAvailable: 1,
|
|
196
|
+
selector: {
|
|
197
|
+
matchLabels: {
|
|
198
|
+
"ray.io/cluster-name": name,
|
|
199
|
+
"ray.io/node-type": "head",
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
}, defs?.pdb));
|
|
204
|
+
|
|
205
|
+
// -- Shared PVC (optional) --
|
|
206
|
+
|
|
207
|
+
const pvc = sharedStorage ? new PersistentVolumeClaim(mergeDefaults({
|
|
208
|
+
metadata: {
|
|
209
|
+
name: pvcName!,
|
|
210
|
+
namespace,
|
|
211
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "storage" },
|
|
212
|
+
},
|
|
213
|
+
spec: {
|
|
214
|
+
accessModes: ["ReadWriteMany"],
|
|
215
|
+
storageClassName: sharedStorage.storageClass,
|
|
216
|
+
resources: { requests: { storage: sharedStorage.size } },
|
|
217
|
+
},
|
|
218
|
+
}, defs?.pvc)) : undefined;
|
|
219
|
+
|
|
220
|
+
// -- Ray Serve LoadBalancer Service --
|
|
221
|
+
|
|
222
|
+
const serveService = new Service(mergeDefaults({
|
|
223
|
+
metadata: {
|
|
224
|
+
name: `${name}-serve`,
|
|
225
|
+
namespace,
|
|
226
|
+
labels: { ...commonLabels, "app.kubernetes.io/component": "serve" },
|
|
227
|
+
},
|
|
228
|
+
spec: {
|
|
229
|
+
type: "LoadBalancer",
|
|
230
|
+
selector: {
|
|
231
|
+
"ray.io/cluster-name": name,
|
|
232
|
+
"ray.io/node-type": "head",
|
|
233
|
+
},
|
|
234
|
+
ports: [{ port: 8000, targetPort: 8000, protocol: "TCP", name: "serve" }],
|
|
235
|
+
},
|
|
236
|
+
}, defs?.serveService));
|
|
237
|
+
|
|
238
|
+
// -- RayService CR --
|
|
239
|
+
|
|
240
|
+
const { headGroupSpec, workerGroupSpecs, rayVersion } = buildRayClusterParts(
|
|
241
|
+
cluster, saName, spilloverBucket, pvcName, mountPath,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const rayService = new RayServiceResource(mergeDefaults({
|
|
245
|
+
metadata: {
|
|
246
|
+
name,
|
|
247
|
+
namespace,
|
|
248
|
+
labels: commonLabels,
|
|
249
|
+
},
|
|
250
|
+
spec: {
|
|
251
|
+
serveConfigV2,
|
|
252
|
+
rayClusterConfig: {
|
|
253
|
+
...(rayVersion && { rayVersion }),
|
|
254
|
+
...(enableAutoscaler && { enableInTreeAutoscaling: true }),
|
|
255
|
+
headGroupSpec,
|
|
256
|
+
workerGroupSpecs,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
}, defs?.rayService));
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
serviceAccount,
|
|
263
|
+
...(clusterRole && { clusterRole }),
|
|
264
|
+
...(clusterRoleBinding && { clusterRoleBinding }),
|
|
265
|
+
networkPolicy,
|
|
266
|
+
pdb,
|
|
267
|
+
...(pvc && { pvc }),
|
|
268
|
+
serveService,
|
|
269
|
+
rayService,
|
|
270
|
+
};
|
|
271
|
+
}, "RayService");
|
package/src/coverage.test.ts
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Third-party CRD sources included in k8s lexicon generation.
|
|
3
|
+
*
|
|
4
|
+
* Add entries here to have CRDs fetched and code-generated alongside
|
|
5
|
+
* the core Kubernetes OpenAPI types. The CRD YAML is fetched at
|
|
6
|
+
* generation time (npm run generate) and baked into the output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CRDSource } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* KubeRay operator CRDs — ray.io/v1
|
|
13
|
+
*
|
|
14
|
+
* Produces:
|
|
15
|
+
* K8s::Ray::RayCluster → apiVersion: ray.io/v1, kind: RayCluster
|
|
16
|
+
* K8s::Ray::RayJob → apiVersion: ray.io/v1, kind: RayJob
|
|
17
|
+
* K8s::Ray::RayService → apiVersion: ray.io/v1, kind: RayService
|
|
18
|
+
*
|
|
19
|
+
* Operator install: kubectl apply -f
|
|
20
|
+
* https://github.com/ray-project/kuberay/releases/download/v1.3.0/kuberay-operator.yaml
|
|
21
|
+
*/
|
|
22
|
+
const KUBERAY_VERSION = "v1.3.0";
|
|
23
|
+
const KUBERAY_CRD_BASE = `https://raw.githubusercontent.com/ray-project/kuberay/${KUBERAY_VERSION}/helm-chart/kuberay-operator/crds`;
|
|
24
|
+
|
|
25
|
+
export const CRD_SOURCES: CRDSource[] = [
|
|
26
|
+
{ type: "url", url: `${KUBERAY_CRD_BASE}/ray.io_rayclusters.yaml` },
|
|
27
|
+
{ type: "url", url: `${KUBERAY_CRD_BASE}/ray.io_rayjobs.yaml` },
|
|
28
|
+
{ type: "url", url: `${KUBERAY_CRD_BASE}/ray.io_rayservices.yaml` },
|
|
29
|
+
];
|
package/src/crd/loader.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* live cluster introspection via kubectl.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { existsSync, readFileSync } from "fs";
|
|
8
9
|
import type { CRDSource, CRDSpec } from "./types";
|
|
9
10
|
import type { K8sParseResult } from "../spec/parse";
|
|
10
11
|
import { parseCRD } from "./parser";
|
|
@@ -53,13 +54,11 @@ async function loadFromFile(source: CRDSource): Promise<string> {
|
|
|
53
54
|
throw new Error("CRD source type 'file' requires a 'path' property");
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
const exists = await file.exists();
|
|
58
|
-
if (!exists) {
|
|
57
|
+
if (!existsSync(source.path)) {
|
|
59
58
|
throw new Error(`CRD file not found: ${source.path}`);
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
return
|
|
61
|
+
return readFileSync(source.path, "utf8");
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
/**
|
|
@@ -85,28 +84,21 @@ async function loadFromURL(source: CRDSource): Promise<string> {
|
|
|
85
84
|
* requires kubectl access and proper authentication.
|
|
86
85
|
*/
|
|
87
86
|
async function loadFromCluster(source: CRDSource): Promise<string> {
|
|
88
|
-
const
|
|
89
|
-
const
|
|
87
|
+
const { execFile } = await import("child_process");
|
|
88
|
+
const { promisify } = await import("util");
|
|
89
|
+
const execFileAsync = promisify(execFile);
|
|
90
90
|
|
|
91
|
-
const args = ["
|
|
92
|
-
if (
|
|
93
|
-
if (
|
|
91
|
+
const args = ["get", "crds", "-o", "yaml"];
|
|
92
|
+
if (source.context) args.push(`--context=${source.context}`);
|
|
93
|
+
if (source.namespace) args.push(`--namespace=${source.namespace}`);
|
|
94
94
|
|
|
95
|
-
const
|
|
96
|
-
stdout: "pipe",
|
|
97
|
-
stderr: "pipe",
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const stdout = await new Response(proc.stdout).text();
|
|
101
|
-
const stderr = await new Response(proc.stderr).text();
|
|
102
|
-
const exitCode = await proc.exited;
|
|
103
|
-
|
|
104
|
-
if (exitCode !== 0) {
|
|
95
|
+
const { stdout, stderr } = await execFileAsync("kubectl", args).catch((err: NodeJS.ErrnoException & { stdout?: string; stderr?: string }) => {
|
|
105
96
|
throw new Error(
|
|
106
|
-
`kubectl failed
|
|
97
|
+
`kubectl failed: ${err.stderr?.trim() || err.message}. ` +
|
|
107
98
|
"Ensure kubectl is installed and configured with access to the target cluster.",
|
|
108
99
|
);
|
|
109
|
-
}
|
|
100
|
+
});
|
|
110
101
|
|
|
102
|
+
void stderr;
|
|
111
103
|
return stdout;
|
|
112
104
|
}
|
package/src/crd/parser.test.ts
CHANGED
package/src/crd/parser.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import type { K8sParseResult, ParsedProperty, ParsedPropertyType, GroupVersionKind } from "../spec/parse";
|
|
11
11
|
import type { CRDSpec } from "./types";
|
|
12
12
|
import type { PropertyConstraints } from "@intentius/chant/codegen/json-schema";
|
|
13
|
-
import {
|
|
13
|
+
import { loadAll } from "js-yaml";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Normalize a CRD group to a PascalCase namespace segment.
|
|
@@ -34,17 +34,17 @@ function normalizeGroupName(group: string): string {
|
|
|
34
34
|
export function parseCRD(content: string): K8sParseResult[] {
|
|
35
35
|
const results: K8sParseResult[] = [];
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.filter((d) => d.length > 0);
|
|
37
|
+
// Use js-yaml to handle full YAML spec (CRD YAMLs use same-indent arrays,
|
|
38
|
+
// nested block scalars, and deep nesting not supported by the lightweight parser).
|
|
39
|
+
const documents: unknown[] = [];
|
|
40
|
+
loadAll(content, (doc) => documents.push(doc));
|
|
42
41
|
|
|
43
|
-
for (const
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
for (const doc of documents) {
|
|
43
|
+
if (!doc || typeof doc !== "object") continue;
|
|
44
|
+
const docObj = doc as Record<string, unknown>;
|
|
45
|
+
if (docObj.kind !== "CustomResourceDefinition") continue;
|
|
46
46
|
|
|
47
|
-
const spec =
|
|
47
|
+
const spec = docObj.spec as CRDSpec | undefined;
|
|
48
48
|
if (!spec?.group || !spec?.names?.kind || !spec?.versions) continue;
|
|
49
49
|
|
|
50
50
|
const crdResults = parseCRDSpec(spec);
|
|
@@ -160,10 +160,15 @@ function extractPropertyTypes(schema: OpenAPISchema, parentTypeName: string): Pa
|
|
|
160
160
|
const specSchema = schema.properties?.spec;
|
|
161
161
|
if (!specSchema?.properties) return results;
|
|
162
162
|
|
|
163
|
+
// Use the short name (last :: segment) as prefix so the naming pipeline
|
|
164
|
+
// produces valid TS identifiers: "RayCluster_AutoscalerOptions" not
|
|
165
|
+
// "K8s::Ray::RayCluster::AutoscalerOptions".
|
|
166
|
+
const shortName = parentTypeName.split("::").pop()!;
|
|
167
|
+
|
|
163
168
|
for (const [name, prop] of Object.entries(specSchema.properties)) {
|
|
164
169
|
// Extract inline object definitions as property types
|
|
165
170
|
if (prop.type === "object" && prop.properties) {
|
|
166
|
-
const ptName = `${
|
|
171
|
+
const ptName = `${shortName}_${pascalCase(name)}`;
|
|
167
172
|
const requiredSet = new Set<string>(prop.required ?? []);
|
|
168
173
|
|
|
169
174
|
results.push({
|
|
@@ -184,7 +189,7 @@ function extractPropertyTypes(schema: OpenAPISchema, parentTypeName: string): Pa
|
|
|
184
189
|
if (prop.type === "array" && prop.items?.type === "object" && prop.items.properties) {
|
|
185
190
|
const itemSchema = prop.items;
|
|
186
191
|
const itemProps = itemSchema.properties!;
|
|
187
|
-
const ptName = `${
|
|
192
|
+
const ptName = `${shortName}_${pascalCase(singularize(name))}`;
|
|
188
193
|
const requiredSet = new Set<string>(itemSchema.required ?? []);
|
|
189
194
|
|
|
190
195
|
results.push({
|