@nexusgpu/repterm-plugin-kubectl 0.1.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 +277 -0
- package/dist/index.d.ts +314 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +544 -0
- package/dist/matchers.d.ts +113 -0
- package/dist/matchers.d.ts.map +1 -0
- package/dist/matchers.js +527 -0
- package/dist/plugin-kubectl/examples/00-simple-demo.d.ts +10 -0
- package/dist/plugin-kubectl/examples/00-simple-demo.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/00-simple-demo.js +51 -0
- package/dist/plugin-kubectl/examples/01-basic-kubectl.d.ts +13 -0
- package/dist/plugin-kubectl/examples/01-basic-kubectl.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/01-basic-kubectl.js +86 -0
- package/dist/plugin-kubectl/examples/02-debugging.d.ts +13 -0
- package/dist/plugin-kubectl/examples/02-debugging.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/02-debugging.js +80 -0
- package/dist/plugin-kubectl/examples/03-resource-management.d.ts +13 -0
- package/dist/plugin-kubectl/examples/03-resource-management.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/03-resource-management.js +134 -0
- package/dist/plugin-kubectl/examples/04-rollout.d.ts +13 -0
- package/dist/plugin-kubectl/examples/04-rollout.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/04-rollout.js +122 -0
- package/dist/plugin-kubectl/examples/05-matchers.d.ts +15 -0
- package/dist/plugin-kubectl/examples/05-matchers.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/05-matchers.js +138 -0
- package/dist/plugin-kubectl/examples/06-advanced.d.ts +14 -0
- package/dist/plugin-kubectl/examples/06-advanced.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/06-advanced.js +140 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/00-prerequisites.d.ts +14 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/00-prerequisites.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/00-prerequisites.js +66 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/01-workload-allocation.d.ts +14 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/01-workload-allocation.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/01-workload-allocation.js +145 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/02-annotation-mode.d.ts +13 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/02-annotation-mode.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/02-annotation-mode.js +123 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/03-insufficient.d.ts +17 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/03-insufficient.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/03-insufficient.js +96 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/04-release.d.ts +13 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/04-release.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/04-release.js +117 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/05-multi-workload-shared-gpu.d.ts +14 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/05-multi-workload-shared-gpu.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/05-multi-workload-shared-gpu.js +145 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/06-workload-resource-resize.d.ts +14 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/06-workload-resource-resize.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/06-workload-resource-resize.js +235 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/07-workload-worker-pod-generation.d.ts +15 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/07-workload-worker-pod-generation.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/07-workload-worker-pod-generation.js +146 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/08-workload-replicas-scale.d.ts +13 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/08-workload-replicas-scale.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/08-workload-replicas-scale.js +141 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/09-gpu-remote-invocation.d.ts +15 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/09-gpu-remote-invocation.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/09-gpu-remote-invocation.js +256 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/_config.d.ts +71 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/_config.d.ts.map +1 -0
- package/dist/plugin-kubectl/examples/tensor-fusion/_config.js +159 -0
- package/dist/plugin-kubectl/src/index.d.ts +314 -0
- package/dist/plugin-kubectl/src/index.d.ts.map +1 -0
- package/dist/plugin-kubectl/src/index.js +545 -0
- package/dist/plugin-kubectl/src/matchers.d.ts +113 -0
- package/dist/plugin-kubectl/src/matchers.d.ts.map +1 -0
- package/dist/plugin-kubectl/src/matchers.js +527 -0
- package/dist/plugin-kubectl/src/result.d.ts +80 -0
- package/dist/plugin-kubectl/src/result.d.ts.map +1 -0
- package/dist/plugin-kubectl/src/result.js +134 -0
- package/dist/repterm/src/api/describe.d.ts +18 -0
- package/dist/repterm/src/api/describe.d.ts.map +1 -0
- package/dist/repterm/src/api/describe.js +32 -0
- package/dist/repterm/src/api/expect.d.ts +43 -0
- package/dist/repterm/src/api/expect.d.ts.map +1 -0
- package/dist/repterm/src/api/expect.js +166 -0
- package/dist/repterm/src/api/hooks.d.ts +178 -0
- package/dist/repterm/src/api/hooks.d.ts.map +1 -0
- package/dist/repterm/src/api/hooks.js +230 -0
- package/dist/repterm/src/api/steps.d.ts +45 -0
- package/dist/repterm/src/api/steps.d.ts.map +1 -0
- package/dist/repterm/src/api/steps.js +105 -0
- package/dist/repterm/src/api/test.d.ts +101 -0
- package/dist/repterm/src/api/test.d.ts.map +1 -0
- package/dist/repterm/src/api/test.js +206 -0
- package/dist/repterm/src/index.d.ts +15 -0
- package/dist/repterm/src/index.d.ts.map +1 -0
- package/dist/repterm/src/index.js +23 -0
- package/dist/repterm/src/plugin/index.d.ts +47 -0
- package/dist/repterm/src/plugin/index.d.ts.map +1 -0
- package/dist/repterm/src/plugin/index.js +85 -0
- package/dist/repterm/src/plugin/withPlugins.d.ts +71 -0
- package/dist/repterm/src/plugin/withPlugins.d.ts.map +1 -0
- package/dist/repterm/src/plugin/withPlugins.js +100 -0
- package/dist/repterm/src/runner/models.d.ts +261 -0
- package/dist/repterm/src/runner/models.d.ts.map +1 -0
- package/dist/repterm/src/runner/models.js +4 -0
- package/dist/result.d.ts +80 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +134 -0
- package/package.json +38 -0
package/dist/matchers.js
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubernetes Matchers for Repterm
|
|
3
|
+
*
|
|
4
|
+
* Provides expect() matchers for Kubernetes resources.
|
|
5
|
+
* All operations are executed through kubectl API (PTY visible).
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import { expect } from 'bun:test';
|
|
10
|
+
import { KubectlResult } from './result.js';
|
|
11
|
+
// ===== K8sResource Wrapper =====
|
|
12
|
+
/**
|
|
13
|
+
* Wrapper class for Kubernetes resources
|
|
14
|
+
* Holds reference to kubectl methods for matcher operations
|
|
15
|
+
*/
|
|
16
|
+
export class K8sResource {
|
|
17
|
+
kubectl;
|
|
18
|
+
kind;
|
|
19
|
+
name;
|
|
20
|
+
constructor(kubectl, kind, name) {
|
|
21
|
+
this.kubectl = kubectl;
|
|
22
|
+
this.kind = kind;
|
|
23
|
+
this.name = name;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// ===== Helper Functions =====
|
|
27
|
+
/**
|
|
28
|
+
* Create a Pod resource wrapper
|
|
29
|
+
*/
|
|
30
|
+
export function pod(kubectl, name) {
|
|
31
|
+
return new K8sResource(kubectl, 'pod', name);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create a Deployment resource wrapper
|
|
35
|
+
*/
|
|
36
|
+
export function deployment(kubectl, name) {
|
|
37
|
+
return new K8sResource(kubectl, 'deployment', name);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a Service resource wrapper
|
|
41
|
+
*/
|
|
42
|
+
export function service(kubectl, name) {
|
|
43
|
+
return new K8sResource(kubectl, 'service', name);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create a StatefulSet resource wrapper
|
|
47
|
+
*/
|
|
48
|
+
export function statefulset(kubectl, name) {
|
|
49
|
+
return new K8sResource(kubectl, 'statefulset', name);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create a Job resource wrapper
|
|
53
|
+
*/
|
|
54
|
+
export function job(kubectl, name) {
|
|
55
|
+
return new K8sResource(kubectl, 'job', name);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Create a ConfigMap resource wrapper
|
|
59
|
+
*/
|
|
60
|
+
export function configmap(kubectl, name) {
|
|
61
|
+
return new K8sResource(kubectl, 'configmap', name);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a Secret resource wrapper
|
|
65
|
+
*/
|
|
66
|
+
export function secret(kubectl, name) {
|
|
67
|
+
return new K8sResource(kubectl, 'secret', name);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a generic resource wrapper
|
|
71
|
+
*/
|
|
72
|
+
export function resource(kubectl, kind, name) {
|
|
73
|
+
return new K8sResource(kubectl, kind, name);
|
|
74
|
+
}
|
|
75
|
+
// ===== Tensor Fusion CRD Wrappers =====
|
|
76
|
+
/**
|
|
77
|
+
* Create a GPUPool resource wrapper (Tensor Fusion CRD)
|
|
78
|
+
*/
|
|
79
|
+
export function gpupool(kubectl, name) {
|
|
80
|
+
return new K8sResource(kubectl, 'gpupool', name);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create a GPU resource wrapper (Tensor Fusion CRD)
|
|
84
|
+
*/
|
|
85
|
+
export function gpu(kubectl, name) {
|
|
86
|
+
return new K8sResource(kubectl, 'gpu', name);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a TensorFusionWorkload resource wrapper (Tensor Fusion CRD)
|
|
90
|
+
*/
|
|
91
|
+
export function tensorfusionworkload(kubectl, name) {
|
|
92
|
+
return new K8sResource(kubectl, 'tensorfusionworkload', name);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create a TensorFusionConnection resource wrapper (Tensor Fusion CRD)
|
|
96
|
+
*/
|
|
97
|
+
export function tensorfusionconnection(kubectl, name) {
|
|
98
|
+
return new K8sResource(kubectl, 'tensorfusionconnection', name);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create a custom CRD resource wrapper with explicit API group
|
|
102
|
+
* @param kubectl - kubectl methods instance
|
|
103
|
+
* @param kind - Resource kind (e.g., 'gpupool.tensor-fusion.ai')
|
|
104
|
+
* @param name - Resource name
|
|
105
|
+
*/
|
|
106
|
+
export function crd(kubectl, kind, name) {
|
|
107
|
+
return new K8sResource(kubectl, kind, name);
|
|
108
|
+
}
|
|
109
|
+
// ===== Type Guard =====
|
|
110
|
+
function isK8sResource(value) {
|
|
111
|
+
return value instanceof K8sResource;
|
|
112
|
+
}
|
|
113
|
+
function isKubectlResult(value) {
|
|
114
|
+
return value instanceof KubectlResult;
|
|
115
|
+
}
|
|
116
|
+
// ===== Register Matchers =====
|
|
117
|
+
/**
|
|
118
|
+
* Register K8s matchers with expect
|
|
119
|
+
*/
|
|
120
|
+
export function registerK8sMatchers() {
|
|
121
|
+
expect.extend({
|
|
122
|
+
/**
|
|
123
|
+
* Assert that a kubectl operation succeeded
|
|
124
|
+
*/
|
|
125
|
+
async toBeSuccessful(received) {
|
|
126
|
+
if (!isKubectlResult(received)) {
|
|
127
|
+
return {
|
|
128
|
+
pass: false,
|
|
129
|
+
message: () => 'Expected value to be a KubectlResult (from kubectl.apply/delete/patch/scale/label)',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const pass = received.successful;
|
|
133
|
+
return {
|
|
134
|
+
pass,
|
|
135
|
+
message: () => pass
|
|
136
|
+
? 'Expected kubectl command to fail, but it succeeded'
|
|
137
|
+
: `Expected kubectl command to succeed, but failed:\n${received.output.slice(0, 500)}`,
|
|
138
|
+
actual: pass ? 'succeeded' : 'failed',
|
|
139
|
+
expected: 'succeeded',
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* Assert that a Pod is in Running state
|
|
144
|
+
*/
|
|
145
|
+
async toBeRunning(received, ...args) {
|
|
146
|
+
const timeout = args[0] ?? 60000;
|
|
147
|
+
if (!isK8sResource(received)) {
|
|
148
|
+
return {
|
|
149
|
+
pass: false,
|
|
150
|
+
message: () => 'Expected value to be a K8sResource',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const { kubectl, kind, name } = received;
|
|
154
|
+
if (kind !== 'pod') {
|
|
155
|
+
return {
|
|
156
|
+
pass: false,
|
|
157
|
+
message: () => `toBeRunning() is only valid for pods, got ${kind}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await kubectl.waitForPod(name, 'Running', timeout);
|
|
162
|
+
return {
|
|
163
|
+
pass: true,
|
|
164
|
+
message: () => `Expected pod/${name} not to be Running`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return {
|
|
169
|
+
pass: false,
|
|
170
|
+
message: () => `Expected pod/${name} to be Running within ${timeout}ms`,
|
|
171
|
+
actual: 'Not Running',
|
|
172
|
+
expected: 'Running',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
/**
|
|
177
|
+
* Assert that a Pod has a specific phase
|
|
178
|
+
*/
|
|
179
|
+
async toHavePhase(received, ...args) {
|
|
180
|
+
const phase = args[0];
|
|
181
|
+
if (!isK8sResource(received)) {
|
|
182
|
+
return {
|
|
183
|
+
pass: false,
|
|
184
|
+
message: () => 'Expected value to be a K8sResource',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const { kubectl, kind, name } = received;
|
|
188
|
+
try {
|
|
189
|
+
const actualPhase = await kubectl.getJsonPath(kind, name, '.status.phase');
|
|
190
|
+
const pass = actualPhase === phase;
|
|
191
|
+
return {
|
|
192
|
+
pass,
|
|
193
|
+
message: () => pass
|
|
194
|
+
? `Expected ${kind}/${name} not to have phase ${phase}`
|
|
195
|
+
: `Expected ${kind}/${name} to have phase ${phase}, got ${actualPhase}`,
|
|
196
|
+
actual: actualPhase,
|
|
197
|
+
expected: phase,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
return {
|
|
202
|
+
pass: false,
|
|
203
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
/**
|
|
208
|
+
* Assert that a resource has a specific number of replicas
|
|
209
|
+
*/
|
|
210
|
+
async toHaveReplicas(received, ...args) {
|
|
211
|
+
const count = args[0];
|
|
212
|
+
if (!isK8sResource(received)) {
|
|
213
|
+
return {
|
|
214
|
+
pass: false,
|
|
215
|
+
message: () => 'Expected value to be a K8sResource',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const { kubectl, kind, name } = received;
|
|
219
|
+
try {
|
|
220
|
+
const actual = await kubectl.getJsonPath(kind, name, '.status.replicas') ?? 0;
|
|
221
|
+
const pass = actual === count;
|
|
222
|
+
return {
|
|
223
|
+
pass,
|
|
224
|
+
message: () => pass
|
|
225
|
+
? `Expected ${kind}/${name} not to have ${count} replicas`
|
|
226
|
+
: `Expected ${kind}/${name} to have ${count} replicas, got ${actual}`,
|
|
227
|
+
actual,
|
|
228
|
+
expected: count,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
return {
|
|
233
|
+
pass: false,
|
|
234
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
/**
|
|
239
|
+
* Assert that a resource has a specific number of available replicas
|
|
240
|
+
*/
|
|
241
|
+
async toHaveAvailableReplicas(received, ...args) {
|
|
242
|
+
const count = args[0];
|
|
243
|
+
if (!isK8sResource(received)) {
|
|
244
|
+
return {
|
|
245
|
+
pass: false,
|
|
246
|
+
message: () => 'Expected value to be a K8sResource',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const { kubectl, kind, name } = received;
|
|
250
|
+
try {
|
|
251
|
+
const actual = await kubectl.getJsonPath(kind, name, '.status.availableReplicas') ?? 0;
|
|
252
|
+
const pass = actual === count;
|
|
253
|
+
return {
|
|
254
|
+
pass,
|
|
255
|
+
message: () => pass
|
|
256
|
+
? `Expected ${kind}/${name} not to have ${count} available replicas`
|
|
257
|
+
: `Expected ${kind}/${name} to have ${count} available replicas, got ${actual}`,
|
|
258
|
+
actual,
|
|
259
|
+
expected: count,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
return {
|
|
264
|
+
pass: false,
|
|
265
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
/**
|
|
270
|
+
* Assert that a Deployment is available
|
|
271
|
+
*/
|
|
272
|
+
async toBeAvailable(received) {
|
|
273
|
+
if (!isK8sResource(received)) {
|
|
274
|
+
return {
|
|
275
|
+
pass: false,
|
|
276
|
+
message: () => 'Expected value to be a K8sResource',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const { kubectl, kind, name } = received;
|
|
280
|
+
try {
|
|
281
|
+
const jsonPath = '.status.conditions[?(@.type=="Available")].status';
|
|
282
|
+
const statusValue = await kubectl.getJsonPath(kind, name, jsonPath);
|
|
283
|
+
// JSONPath may return multiple values separated by space, take first
|
|
284
|
+
const actualStatus = statusValue?.split(' ')?.[0];
|
|
285
|
+
const pass = actualStatus === 'True';
|
|
286
|
+
return {
|
|
287
|
+
pass,
|
|
288
|
+
message: () => pass
|
|
289
|
+
? `Expected ${kind}/${name} not to be available`
|
|
290
|
+
: `Expected ${kind}/${name} to be available`,
|
|
291
|
+
actual: actualStatus,
|
|
292
|
+
expected: 'True',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
return {
|
|
297
|
+
pass: false,
|
|
298
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
/**
|
|
303
|
+
* Assert that a resource exists in the cluster
|
|
304
|
+
*/
|
|
305
|
+
async toExistInCluster(received) {
|
|
306
|
+
if (!isK8sResource(received)) {
|
|
307
|
+
return {
|
|
308
|
+
pass: false,
|
|
309
|
+
message: () => 'Expected value to be a K8sResource',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const { kubectl, kind, name } = received;
|
|
313
|
+
const exists = await kubectl.exists(kind, name);
|
|
314
|
+
return {
|
|
315
|
+
pass: exists,
|
|
316
|
+
message: () => exists
|
|
317
|
+
? `Expected ${kind}/${name} not to exist in cluster`
|
|
318
|
+
: `Expected ${kind}/${name} to exist in cluster`,
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
/**
|
|
322
|
+
* Assert that a resource has a specific label
|
|
323
|
+
*/
|
|
324
|
+
async toHaveLabel(received, ...args) {
|
|
325
|
+
const key = args[0];
|
|
326
|
+
const value = args[1];
|
|
327
|
+
if (!isK8sResource(received)) {
|
|
328
|
+
return {
|
|
329
|
+
pass: false,
|
|
330
|
+
message: () => 'Expected value to be a K8sResource',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const { kubectl, kind, name } = received;
|
|
334
|
+
try {
|
|
335
|
+
// Escape dots in label key for JSONPath
|
|
336
|
+
const escapedKey = key.replace(/\./g, '\\.');
|
|
337
|
+
const jsonPath = `.metadata.labels.${escapedKey}`;
|
|
338
|
+
const actualValue = await kubectl.getJsonPath(kind, name, jsonPath);
|
|
339
|
+
const pass = value !== undefined ? actualValue === value : actualValue !== undefined;
|
|
340
|
+
return {
|
|
341
|
+
pass,
|
|
342
|
+
message: () => {
|
|
343
|
+
if (value !== undefined) {
|
|
344
|
+
return pass
|
|
345
|
+
? `Expected ${kind}/${name} not to have label ${key}=${value}`
|
|
346
|
+
: `Expected ${kind}/${name} to have label ${key}=${value}, got ${actualValue}`;
|
|
347
|
+
}
|
|
348
|
+
return pass
|
|
349
|
+
? `Expected ${kind}/${name} not to have label ${key}`
|
|
350
|
+
: `Expected ${kind}/${name} to have label ${key}`;
|
|
351
|
+
},
|
|
352
|
+
actual: actualValue,
|
|
353
|
+
expected: value ?? `label "${key}" exists`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
return {
|
|
358
|
+
pass: false,
|
|
359
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
/**
|
|
364
|
+
* Assert that a resource has a specific annotation
|
|
365
|
+
*/
|
|
366
|
+
async toHaveAnnotation(received, ...args) {
|
|
367
|
+
const key = args[0];
|
|
368
|
+
const value = args[1];
|
|
369
|
+
if (!isK8sResource(received)) {
|
|
370
|
+
return {
|
|
371
|
+
pass: false,
|
|
372
|
+
message: () => 'Expected value to be a K8sResource',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const { kubectl, kind, name } = received;
|
|
376
|
+
try {
|
|
377
|
+
// Escape dots in annotation key for JSONPath
|
|
378
|
+
const escapedKey = key.replace(/\./g, '\\.');
|
|
379
|
+
const jsonPath = `.metadata.annotations.${escapedKey}`;
|
|
380
|
+
const actualValue = await kubectl.getJsonPath(kind, name, jsonPath);
|
|
381
|
+
const pass = value !== undefined ? actualValue === value : actualValue !== undefined;
|
|
382
|
+
return {
|
|
383
|
+
pass,
|
|
384
|
+
message: () => {
|
|
385
|
+
if (value !== undefined) {
|
|
386
|
+
return pass
|
|
387
|
+
? `Expected ${kind}/${name} not to have annotation ${key}=${value}`
|
|
388
|
+
: `Expected ${kind}/${name} to have annotation ${key}=${value}, got ${actualValue}`;
|
|
389
|
+
}
|
|
390
|
+
return pass
|
|
391
|
+
? `Expected ${kind}/${name} not to have annotation ${key}`
|
|
392
|
+
: `Expected ${kind}/${name} to have annotation ${key}`;
|
|
393
|
+
},
|
|
394
|
+
actual: actualValue,
|
|
395
|
+
expected: value ?? `annotation "${key}" exists`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
return {
|
|
400
|
+
pass: false,
|
|
401
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
/**
|
|
406
|
+
* Assert that a resource has a specific condition
|
|
407
|
+
*/
|
|
408
|
+
async toHaveCondition(received, ...args) {
|
|
409
|
+
const type = args[0];
|
|
410
|
+
const status = args[1];
|
|
411
|
+
if (!isK8sResource(received)) {
|
|
412
|
+
return {
|
|
413
|
+
pass: false,
|
|
414
|
+
message: () => 'Expected value to be a K8sResource',
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const { kubectl, kind, name } = received;
|
|
418
|
+
try {
|
|
419
|
+
const jsonPath = `.status.conditions[?(@.type=="${type}")].status`;
|
|
420
|
+
const statusValue = await kubectl.getJsonPath(kind, name, jsonPath);
|
|
421
|
+
// JSONPath may return multiple values separated by space, take first
|
|
422
|
+
const actualStatus = statusValue?.split(' ')?.[0];
|
|
423
|
+
const pass = actualStatus === status;
|
|
424
|
+
return {
|
|
425
|
+
pass,
|
|
426
|
+
message: () => pass
|
|
427
|
+
? `Expected ${kind}/${name} not to have condition ${type}=${status}`
|
|
428
|
+
: `Expected ${kind}/${name} to have condition ${type}=${status}, got ${actualStatus ?? 'not found'}`,
|
|
429
|
+
actual: actualStatus,
|
|
430
|
+
expected: status,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
return {
|
|
435
|
+
pass: false,
|
|
436
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
/**
|
|
441
|
+
* Assert that a resource does not exist in the cluster
|
|
442
|
+
*/
|
|
443
|
+
async toNotExistInCluster(received) {
|
|
444
|
+
if (!isK8sResource(received)) {
|
|
445
|
+
return {
|
|
446
|
+
pass: false,
|
|
447
|
+
message: () => 'Expected value to be a K8sResource',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const { kubectl, kind, name } = received;
|
|
451
|
+
const exists = await kubectl.exists(kind, name);
|
|
452
|
+
return {
|
|
453
|
+
pass: !exists,
|
|
454
|
+
message: () => !exists
|
|
455
|
+
? `Expected ${kind}/${name} to exist in cluster`
|
|
456
|
+
: `Expected ${kind}/${name} not to exist in cluster`,
|
|
457
|
+
};
|
|
458
|
+
},
|
|
459
|
+
/**
|
|
460
|
+
* Assert that a resource has a specific number of ready replicas
|
|
461
|
+
*/
|
|
462
|
+
async toHaveReadyReplicas(received, ...args) {
|
|
463
|
+
const count = args[0];
|
|
464
|
+
if (!isK8sResource(received)) {
|
|
465
|
+
return {
|
|
466
|
+
pass: false,
|
|
467
|
+
message: () => 'Expected value to be a K8sResource',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const { kubectl, kind, name } = received;
|
|
471
|
+
try {
|
|
472
|
+
const actual = await kubectl.getJsonPath(kind, name, '.status.readyReplicas') ?? 0;
|
|
473
|
+
const pass = actual === count;
|
|
474
|
+
return {
|
|
475
|
+
pass,
|
|
476
|
+
message: () => pass
|
|
477
|
+
? `Expected ${kind}/${name} not to have ${count} ready replicas`
|
|
478
|
+
: `Expected ${kind}/${name} to have ${count} ready replicas, got ${actual}`,
|
|
479
|
+
actual,
|
|
480
|
+
expected: count,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
return {
|
|
485
|
+
pass: false,
|
|
486
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
/**
|
|
491
|
+
* Assert that a resource has a specific status field value
|
|
492
|
+
* Supports dot notation for nested paths (e.g., 'phase', 'available.tflops')
|
|
493
|
+
*/
|
|
494
|
+
async toHaveStatusField(received, ...args) {
|
|
495
|
+
const path = args[0];
|
|
496
|
+
const expectedValue = args[1];
|
|
497
|
+
if (!isK8sResource(received)) {
|
|
498
|
+
return {
|
|
499
|
+
pass: false,
|
|
500
|
+
message: () => 'Expected value to be a K8sResource',
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const { kubectl, kind, name } = received;
|
|
504
|
+
try {
|
|
505
|
+
const jsonPath = `.status.${path}`;
|
|
506
|
+
const actual = await kubectl.getJsonPath(kind, name, jsonPath);
|
|
507
|
+
const pass = actual === expectedValue;
|
|
508
|
+
return {
|
|
509
|
+
pass,
|
|
510
|
+
message: () => pass
|
|
511
|
+
? `Expected ${kind}/${name} not to have status.${path} = ${JSON.stringify(expectedValue)}`
|
|
512
|
+
: `Expected ${kind}/${name} to have status.${path} = ${JSON.stringify(expectedValue)}, got ${JSON.stringify(actual)}`,
|
|
513
|
+
actual,
|
|
514
|
+
expected: expectedValue,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
catch (e) {
|
|
518
|
+
return {
|
|
519
|
+
pass: false,
|
|
520
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// Auto-register matchers when this module is imported
|
|
527
|
+
registerK8sMatchers();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"00-simple-demo.d.ts","sourceRoot":"","sources":["../../../examples/00-simple-demo.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 示例 0: 简单演示(无需 K8s 集群)
|
|
3
|
+
*
|
|
4
|
+
* 验证 kubectl 插件基础功能,无需连接 Kubernetes 集群
|
|
5
|
+
*
|
|
6
|
+
* 运行方式:
|
|
7
|
+
* bun run repterm packages/plugin-kubectl/examples/00-simple-demo.ts
|
|
8
|
+
*/
|
|
9
|
+
import { describe, defineConfig, createTestWithPlugins, } from '../../repterm/src/index.js';
|
|
10
|
+
import { kubectlPlugin } from '../src/index.js';
|
|
11
|
+
// 配置插件
|
|
12
|
+
const config = defineConfig({
|
|
13
|
+
plugins: [kubectlPlugin({ namespace: 'default' })],
|
|
14
|
+
});
|
|
15
|
+
const test = createTestWithPlugins(config);
|
|
16
|
+
describe('Kubectl 插件基础功能', () => {
|
|
17
|
+
test('验证命名空间配置', async (ctx) => {
|
|
18
|
+
const { kubectl } = ctx.plugins;
|
|
19
|
+
// 验证初始命名空间
|
|
20
|
+
const ns = kubectl.getNamespace();
|
|
21
|
+
if (ns !== 'default') {
|
|
22
|
+
throw new Error(`Expected namespace 'default', got '${ns}'`);
|
|
23
|
+
}
|
|
24
|
+
// 切换命名空间
|
|
25
|
+
kubectl.setNamespace('kube-system');
|
|
26
|
+
if (kubectl.getNamespace() !== 'kube-system') {
|
|
27
|
+
throw new Error('Namespace switch failed');
|
|
28
|
+
}
|
|
29
|
+
// 恢复
|
|
30
|
+
kubectl.setNamespace('default');
|
|
31
|
+
});
|
|
32
|
+
test('验证命令构建', async (ctx) => {
|
|
33
|
+
const { kubectl } = ctx.plugins;
|
|
34
|
+
// 验证 command 方法生成正确的命令
|
|
35
|
+
const cmd = kubectl.command('get pods');
|
|
36
|
+
if (!cmd.includes('kubectl')) {
|
|
37
|
+
throw new Error('Command should contain kubectl');
|
|
38
|
+
}
|
|
39
|
+
if (!cmd.includes('-n default')) {
|
|
40
|
+
throw new Error('Command should contain namespace');
|
|
41
|
+
}
|
|
42
|
+
if (!cmd.includes('get pods')) {
|
|
43
|
+
throw new Error('Command should contain get pods');
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
test('执行 kubectl version (run API)', async (ctx) => {
|
|
47
|
+
const { kubectl } = ctx.plugins;
|
|
48
|
+
// 使用插件的 run 方法执行命令
|
|
49
|
+
await kubectl.run('version --client');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 示例 1: 基础 Kubectl 操作
|
|
3
|
+
*
|
|
4
|
+
* 演示 kubectl 插件的基础 API:apply, delete, get, exists, waitForPod
|
|
5
|
+
*
|
|
6
|
+
* 运行方式:
|
|
7
|
+
* bun run repterm packages/plugin-kubectl/examples/01-basic-kubectl.ts
|
|
8
|
+
*
|
|
9
|
+
* 前置条件:
|
|
10
|
+
* - 已配置 kubectl 并连接到 Kubernetes 集群
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=01-basic-kubectl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"01-basic-kubectl.d.ts","sourceRoot":"","sources":["../../../examples/01-basic-kubectl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 示例 1: 基础 Kubectl 操作
|
|
3
|
+
*
|
|
4
|
+
* 演示 kubectl 插件的基础 API:apply, delete, get, exists, waitForPod
|
|
5
|
+
*
|
|
6
|
+
* 运行方式:
|
|
7
|
+
* bun run repterm packages/plugin-kubectl/examples/01-basic-kubectl.ts
|
|
8
|
+
*
|
|
9
|
+
* 前置条件:
|
|
10
|
+
* - 已配置 kubectl 并连接到 Kubernetes 集群
|
|
11
|
+
*/
|
|
12
|
+
import { describe, defineConfig, createTestWithPlugins, } from '../../repterm/src/index.js';
|
|
13
|
+
import { kubectlPlugin } from '../src/index.js';
|
|
14
|
+
// 配置插件
|
|
15
|
+
const config = defineConfig({
|
|
16
|
+
plugins: [kubectlPlugin({ namespace: 'default' })],
|
|
17
|
+
});
|
|
18
|
+
const test = createTestWithPlugins(config);
|
|
19
|
+
// 测试用 Pod YAML
|
|
20
|
+
const nginxPodYaml = `
|
|
21
|
+
apiVersion: v1
|
|
22
|
+
kind: Pod
|
|
23
|
+
metadata:
|
|
24
|
+
name: nginx-test
|
|
25
|
+
labels:
|
|
26
|
+
app: nginx
|
|
27
|
+
env: test
|
|
28
|
+
spec:
|
|
29
|
+
containers:
|
|
30
|
+
- name: nginx
|
|
31
|
+
image: nginx:alpine
|
|
32
|
+
ports:
|
|
33
|
+
- containerPort: 80
|
|
34
|
+
`;
|
|
35
|
+
describe('基础 Kubectl API', { record: true }, () => {
|
|
36
|
+
// ===== apply - 创建资源 =====
|
|
37
|
+
test('apply - 创建 Pod', async (ctx) => {
|
|
38
|
+
const { kubectl } = ctx.plugins;
|
|
39
|
+
// 使用 apply API 创建 Pod
|
|
40
|
+
await kubectl.apply(nginxPodYaml);
|
|
41
|
+
});
|
|
42
|
+
// ===== waitForPod - 等待 Pod 就绪 =====
|
|
43
|
+
test('waitForPod - 等待 Pod Running', async (ctx) => {
|
|
44
|
+
const { kubectl } = ctx.plugins;
|
|
45
|
+
// 使用 waitForPod API 等待 Pod 进入 Running 状态
|
|
46
|
+
await kubectl.waitForPod('nginx-test', 'Running', 60000);
|
|
47
|
+
});
|
|
48
|
+
// ===== exists - 检查资源是否存在 =====
|
|
49
|
+
test('exists - 检查 Pod 存在', async (ctx) => {
|
|
50
|
+
const { kubectl } = ctx.plugins;
|
|
51
|
+
// 使用 exists API 检查资源
|
|
52
|
+
const podExists = await kubectl.exists('pod', 'nginx-test');
|
|
53
|
+
if (!podExists) {
|
|
54
|
+
throw new Error('Pod should exist');
|
|
55
|
+
}
|
|
56
|
+
// 检查不存在的资源
|
|
57
|
+
const notExists = await kubectl.exists('pod', 'non-existent-pod');
|
|
58
|
+
if (notExists) {
|
|
59
|
+
throw new Error('Non-existent pod should not exist');
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
// ===== get - 获取资源信息 =====
|
|
63
|
+
test('get - 获取 Pod 信息', async (ctx) => {
|
|
64
|
+
const { kubectl } = ctx.plugins;
|
|
65
|
+
// 使用 get API 获取资源 JSON
|
|
66
|
+
const pod = await kubectl.get('pod', 'nginx-test');
|
|
67
|
+
if (pod.metadata.name !== 'nginx-test') {
|
|
68
|
+
throw new Error(`Expected pod name 'nginx-test', got '${pod.metadata.name}'`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// ===== run - 执行原始命令 =====
|
|
72
|
+
test('run - 执行原始 kubectl 命令', async (ctx) => {
|
|
73
|
+
const { kubectl } = ctx.plugins;
|
|
74
|
+
// 使用 run API 执行任意 kubectl 命令
|
|
75
|
+
await kubectl.run('get pod nginx-test -o wide');
|
|
76
|
+
});
|
|
77
|
+
// ===== delete - 删除资源 =====
|
|
78
|
+
test('delete - 删除 Pod', async (ctx) => {
|
|
79
|
+
const { kubectl } = ctx.plugins;
|
|
80
|
+
// 使用 delete API 删除资源
|
|
81
|
+
await kubectl.delete('pod', 'nginx-test', { force: true });
|
|
82
|
+
// 验证已删除
|
|
83
|
+
// 注意:删除可能需要一些时间
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
85
|
+
});
|
|
86
|
+
});
|