@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.
Files changed (101) hide show
  1. package/README.md +277 -0
  2. package/dist/index.d.ts +314 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +544 -0
  5. package/dist/matchers.d.ts +113 -0
  6. package/dist/matchers.d.ts.map +1 -0
  7. package/dist/matchers.js +527 -0
  8. package/dist/plugin-kubectl/examples/00-simple-demo.d.ts +10 -0
  9. package/dist/plugin-kubectl/examples/00-simple-demo.d.ts.map +1 -0
  10. package/dist/plugin-kubectl/examples/00-simple-demo.js +51 -0
  11. package/dist/plugin-kubectl/examples/01-basic-kubectl.d.ts +13 -0
  12. package/dist/plugin-kubectl/examples/01-basic-kubectl.d.ts.map +1 -0
  13. package/dist/plugin-kubectl/examples/01-basic-kubectl.js +86 -0
  14. package/dist/plugin-kubectl/examples/02-debugging.d.ts +13 -0
  15. package/dist/plugin-kubectl/examples/02-debugging.d.ts.map +1 -0
  16. package/dist/plugin-kubectl/examples/02-debugging.js +80 -0
  17. package/dist/plugin-kubectl/examples/03-resource-management.d.ts +13 -0
  18. package/dist/plugin-kubectl/examples/03-resource-management.d.ts.map +1 -0
  19. package/dist/plugin-kubectl/examples/03-resource-management.js +134 -0
  20. package/dist/plugin-kubectl/examples/04-rollout.d.ts +13 -0
  21. package/dist/plugin-kubectl/examples/04-rollout.d.ts.map +1 -0
  22. package/dist/plugin-kubectl/examples/04-rollout.js +122 -0
  23. package/dist/plugin-kubectl/examples/05-matchers.d.ts +15 -0
  24. package/dist/plugin-kubectl/examples/05-matchers.d.ts.map +1 -0
  25. package/dist/plugin-kubectl/examples/05-matchers.js +138 -0
  26. package/dist/plugin-kubectl/examples/06-advanced.d.ts +14 -0
  27. package/dist/plugin-kubectl/examples/06-advanced.d.ts.map +1 -0
  28. package/dist/plugin-kubectl/examples/06-advanced.js +140 -0
  29. package/dist/plugin-kubectl/examples/tensor-fusion/00-prerequisites.d.ts +14 -0
  30. package/dist/plugin-kubectl/examples/tensor-fusion/00-prerequisites.d.ts.map +1 -0
  31. package/dist/plugin-kubectl/examples/tensor-fusion/00-prerequisites.js +66 -0
  32. package/dist/plugin-kubectl/examples/tensor-fusion/01-workload-allocation.d.ts +14 -0
  33. package/dist/plugin-kubectl/examples/tensor-fusion/01-workload-allocation.d.ts.map +1 -0
  34. package/dist/plugin-kubectl/examples/tensor-fusion/01-workload-allocation.js +145 -0
  35. package/dist/plugin-kubectl/examples/tensor-fusion/02-annotation-mode.d.ts +13 -0
  36. package/dist/plugin-kubectl/examples/tensor-fusion/02-annotation-mode.d.ts.map +1 -0
  37. package/dist/plugin-kubectl/examples/tensor-fusion/02-annotation-mode.js +123 -0
  38. package/dist/plugin-kubectl/examples/tensor-fusion/03-insufficient.d.ts +17 -0
  39. package/dist/plugin-kubectl/examples/tensor-fusion/03-insufficient.d.ts.map +1 -0
  40. package/dist/plugin-kubectl/examples/tensor-fusion/03-insufficient.js +96 -0
  41. package/dist/plugin-kubectl/examples/tensor-fusion/04-release.d.ts +13 -0
  42. package/dist/plugin-kubectl/examples/tensor-fusion/04-release.d.ts.map +1 -0
  43. package/dist/plugin-kubectl/examples/tensor-fusion/04-release.js +117 -0
  44. package/dist/plugin-kubectl/examples/tensor-fusion/05-multi-workload-shared-gpu.d.ts +14 -0
  45. package/dist/plugin-kubectl/examples/tensor-fusion/05-multi-workload-shared-gpu.d.ts.map +1 -0
  46. package/dist/plugin-kubectl/examples/tensor-fusion/05-multi-workload-shared-gpu.js +145 -0
  47. package/dist/plugin-kubectl/examples/tensor-fusion/06-workload-resource-resize.d.ts +14 -0
  48. package/dist/plugin-kubectl/examples/tensor-fusion/06-workload-resource-resize.d.ts.map +1 -0
  49. package/dist/plugin-kubectl/examples/tensor-fusion/06-workload-resource-resize.js +235 -0
  50. package/dist/plugin-kubectl/examples/tensor-fusion/07-workload-worker-pod-generation.d.ts +15 -0
  51. package/dist/plugin-kubectl/examples/tensor-fusion/07-workload-worker-pod-generation.d.ts.map +1 -0
  52. package/dist/plugin-kubectl/examples/tensor-fusion/07-workload-worker-pod-generation.js +146 -0
  53. package/dist/plugin-kubectl/examples/tensor-fusion/08-workload-replicas-scale.d.ts +13 -0
  54. package/dist/plugin-kubectl/examples/tensor-fusion/08-workload-replicas-scale.d.ts.map +1 -0
  55. package/dist/plugin-kubectl/examples/tensor-fusion/08-workload-replicas-scale.js +141 -0
  56. package/dist/plugin-kubectl/examples/tensor-fusion/09-gpu-remote-invocation.d.ts +15 -0
  57. package/dist/plugin-kubectl/examples/tensor-fusion/09-gpu-remote-invocation.d.ts.map +1 -0
  58. package/dist/plugin-kubectl/examples/tensor-fusion/09-gpu-remote-invocation.js +256 -0
  59. package/dist/plugin-kubectl/examples/tensor-fusion/_config.d.ts +71 -0
  60. package/dist/plugin-kubectl/examples/tensor-fusion/_config.d.ts.map +1 -0
  61. package/dist/plugin-kubectl/examples/tensor-fusion/_config.js +159 -0
  62. package/dist/plugin-kubectl/src/index.d.ts +314 -0
  63. package/dist/plugin-kubectl/src/index.d.ts.map +1 -0
  64. package/dist/plugin-kubectl/src/index.js +545 -0
  65. package/dist/plugin-kubectl/src/matchers.d.ts +113 -0
  66. package/dist/plugin-kubectl/src/matchers.d.ts.map +1 -0
  67. package/dist/plugin-kubectl/src/matchers.js +527 -0
  68. package/dist/plugin-kubectl/src/result.d.ts +80 -0
  69. package/dist/plugin-kubectl/src/result.d.ts.map +1 -0
  70. package/dist/plugin-kubectl/src/result.js +134 -0
  71. package/dist/repterm/src/api/describe.d.ts +18 -0
  72. package/dist/repterm/src/api/describe.d.ts.map +1 -0
  73. package/dist/repterm/src/api/describe.js +32 -0
  74. package/dist/repterm/src/api/expect.d.ts +43 -0
  75. package/dist/repterm/src/api/expect.d.ts.map +1 -0
  76. package/dist/repterm/src/api/expect.js +166 -0
  77. package/dist/repterm/src/api/hooks.d.ts +178 -0
  78. package/dist/repterm/src/api/hooks.d.ts.map +1 -0
  79. package/dist/repterm/src/api/hooks.js +230 -0
  80. package/dist/repterm/src/api/steps.d.ts +45 -0
  81. package/dist/repterm/src/api/steps.d.ts.map +1 -0
  82. package/dist/repterm/src/api/steps.js +105 -0
  83. package/dist/repterm/src/api/test.d.ts +101 -0
  84. package/dist/repterm/src/api/test.d.ts.map +1 -0
  85. package/dist/repterm/src/api/test.js +206 -0
  86. package/dist/repterm/src/index.d.ts +15 -0
  87. package/dist/repterm/src/index.d.ts.map +1 -0
  88. package/dist/repterm/src/index.js +23 -0
  89. package/dist/repterm/src/plugin/index.d.ts +47 -0
  90. package/dist/repterm/src/plugin/index.d.ts.map +1 -0
  91. package/dist/repterm/src/plugin/index.js +85 -0
  92. package/dist/repterm/src/plugin/withPlugins.d.ts +71 -0
  93. package/dist/repterm/src/plugin/withPlugins.d.ts.map +1 -0
  94. package/dist/repterm/src/plugin/withPlugins.js +100 -0
  95. package/dist/repterm/src/runner/models.d.ts +261 -0
  96. package/dist/repterm/src/runner/models.d.ts.map +1 -0
  97. package/dist/repterm/src/runner/models.js +4 -0
  98. package/dist/result.d.ts +80 -0
  99. package/dist/result.d.ts.map +1 -0
  100. package/dist/result.js +134 -0
  101. package/package.json +38 -0
@@ -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,10 @@
1
+ /**
2
+ * 示例 0: 简单演示(无需 K8s 集群)
3
+ *
4
+ * 验证 kubectl 插件基础功能,无需连接 Kubernetes 集群
5
+ *
6
+ * 运行方式:
7
+ * bun run repterm packages/plugin-kubectl/examples/00-simple-demo.ts
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=00-simple-demo.d.ts.map
@@ -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
+ });