@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
package/dist/index.js ADDED
@@ -0,0 +1,544 @@
1
+ /**
2
+ * Kubectl Plugin for Repterm
3
+ *
4
+ * Provides Kubernetes-specific testing utilities and commands.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ import { definePlugin } from 'repterm-api';
9
+ import { ApplyResult, DeleteResult, PatchResult, ScaleResult, LabelResult, WaitResult, } from './result.js';
10
+ /**
11
+ * Create the kubectl plugin
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { defineConfig, createTestWithPlugins } from 'repterm';
16
+ * import { kubectlPlugin } from '@nexusgpu/repterm-plugin-kubectl';
17
+ *
18
+ * const config = defineConfig({
19
+ * plugins: [kubectlPlugin({ namespace: 'test' })] as const,
20
+ * });
21
+ *
22
+ * const test = createTestWithPlugins(config);
23
+ *
24
+ * test('deploy nginx', async (ctx) => {
25
+ * await ctx.plugins.kubectl.apply(`...`);
26
+ * await ctx.plugins.kubectl.waitForPod('nginx', 'Running');
27
+ * });
28
+ * ```
29
+ */
30
+ export function kubectlPlugin(options = {}) {
31
+ let currentNamespace = options.namespace || 'default';
32
+ const kubeconfig = options.kubeconfig;
33
+ return definePlugin('kubectl', (ctx) => {
34
+ const testContext = ctx.testContext;
35
+ const buildCommand = (args) => {
36
+ let cmd = 'kubectl';
37
+ if (kubeconfig) {
38
+ cmd += ` --kubeconfig=${kubeconfig}`;
39
+ }
40
+ cmd += ` -n ${currentNamespace} ${args}`;
41
+ return cmd;
42
+ };
43
+ const executeCommand = async (args) => {
44
+ const cmd = buildCommand(args);
45
+ if (ctx.debug) {
46
+ console.log(`[kubectl] Executing: ${cmd}`);
47
+ }
48
+ const result = await testContext.terminal.run(`${cmd}`);
49
+ return result.output;
50
+ };
51
+ const methods = {
52
+ run: executeCommand,
53
+ command: (args) => buildCommand(args),
54
+ waitForPod: async (name, status = 'Running', timeout = 60000) => {
55
+ const timeoutSec = Math.ceil(timeout / 1000);
56
+ // Use kubectl wait instead of polling for cleaner recording
57
+ const cmd = buildCommand(`wait --for=jsonpath='{.status.phase}'=${status} pod/${name} --timeout=${timeoutSec}s`);
58
+ await testContext.terminal.run(cmd);
59
+ },
60
+ apply: async (yaml) => {
61
+ // Use heredoc to avoid quote> prompt in recording
62
+ const cmd = `cat <<'EOF' | kubectl -n ${currentNamespace} apply -f -
63
+ ${yaml.trim()}
64
+ EOF`;
65
+ const result = await testContext.terminal.run(`${cmd}`);
66
+ return new ApplyResult(result.output, cmd, result.code);
67
+ },
68
+ delete: async (resource, name, options) => {
69
+ let args = `delete ${resource} ${name} --ignore-not-found`;
70
+ if (options?.force) {
71
+ args += ' --grace-period=0 --force';
72
+ }
73
+ const cmd = buildCommand(args);
74
+ const result = await testContext.terminal.run(cmd, { timeout: options?.force ? 10000 : 60000 });
75
+ return new DeleteResult(result.output, cmd, result.code);
76
+ },
77
+ get: ((resource, name, options) => {
78
+ // Watch mode: start continuous watch, return Promise<WatchProcess>
79
+ if (options?.watch) {
80
+ let args = name ? `get ${resource} ${name} -w` : `get ${resource} -w`;
81
+ if (options.selector) {
82
+ args += ` -l ${options.selector}`;
83
+ }
84
+ if (options.fieldSelector) {
85
+ args += ` --field-selector=${options.fieldSelector}`;
86
+ }
87
+ if (options.allNamespaces) {
88
+ args += ' -A';
89
+ }
90
+ if (options.output) {
91
+ args += ` -o ${options.output}`;
92
+ }
93
+ const cmd = buildCommand(args);
94
+ const proc = testContext.terminal.run(cmd);
95
+ // Return Promise so caller must await; ensures stepTitle and command input complete
96
+ return (async () => {
97
+ await proc.start?.(); // Wait for input to finish
98
+ return {
99
+ interrupt: async () => {
100
+ await proc.interrupt?.();
101
+ }
102
+ };
103
+ })();
104
+ }
105
+ // Normal mode: fetch JSON data
106
+ return (async () => {
107
+ let args = name ? `get ${resource} ${name}` : `get ${resource}`;
108
+ if (options?.selector) {
109
+ args += ` -l ${options.selector}`;
110
+ }
111
+ if (options?.fieldSelector) {
112
+ args += ` --field-selector=${options.fieldSelector}`;
113
+ }
114
+ if (options?.allNamespaces) {
115
+ args += ' -A';
116
+ }
117
+ args += ' -o json';
118
+ // Build full command with optional jq filter
119
+ let fullCmd = buildCommand(args);
120
+ if (options?.jqFilter) {
121
+ fullCmd += ` | jq '${options.jqFilter}'`;
122
+ }
123
+ const result = await testContext.terminal.run(fullCmd);
124
+ let stdout;
125
+ if (testContext.terminal.isPtyMode?.()) {
126
+ const silentResult = await testContext.terminal.run(fullCmd, { silent: true });
127
+ stdout = silentResult.stdout;
128
+ }
129
+ else {
130
+ stdout = result.stdout;
131
+ }
132
+ // Check for kubectl errors before parsing JSON
133
+ const trimmed = stdout.trim();
134
+ if (trimmed.startsWith('error:') || trimmed.startsWith('Error') ||
135
+ trimmed.includes('Error from server') || trimmed.includes('not found')) {
136
+ throw new Error(`kubectl get failed: ${trimmed}`);
137
+ }
138
+ try {
139
+ return JSON.parse(trimmed);
140
+ }
141
+ catch {
142
+ return trimmed;
143
+ }
144
+ })();
145
+ }),
146
+ getJsonPath: async (resource, name, jsonPath, options) => {
147
+ let args = `get ${resource} ${name}`;
148
+ if (options?.selector) {
149
+ args += ` -l ${options.selector}`;
150
+ }
151
+ if (options?.fieldSelector) {
152
+ args += ` --field-selector=${options.fieldSelector}`;
153
+ }
154
+ if (options?.allNamespaces) {
155
+ args += ' -A';
156
+ }
157
+ // Ensure jsonPath is properly quoted
158
+ const escapedPath = jsonPath.startsWith('{') ? jsonPath : `{${jsonPath}}`;
159
+ args += ` -o jsonpath='${escapedPath}'`;
160
+ const cmd = buildCommand(args);
161
+ const result = await testContext.terminal.run(cmd);
162
+ const output = testContext.terminal.isPtyMode?.()
163
+ ? (await testContext.terminal.run(cmd, { silent: true })).stdout
164
+ : result.stdout;
165
+ const trimmed = output.trim();
166
+ // Handle empty/null values
167
+ if (trimmed === '' || trimmed === '<none>' || trimmed === '<nil>') {
168
+ return undefined;
169
+ }
170
+ // Try to parse as JSON (numbers, booleans, etc.), otherwise return as string
171
+ try {
172
+ return JSON.parse(trimmed);
173
+ }
174
+ catch {
175
+ return trimmed;
176
+ }
177
+ },
178
+ exists: async (resource, name) => {
179
+ try {
180
+ const cmd = buildCommand(`get ${resource} ${name} -o name`);
181
+ const result = await testContext.terminal.run(`${cmd}`);
182
+ // In PTY mode, use silent run for clean output
183
+ let stdout = result.stdout;
184
+ if (testContext.terminal.isPtyMode?.()) {
185
+ const silentResult = await testContext.terminal.run(cmd, { silent: true });
186
+ stdout = silentResult.stdout;
187
+ }
188
+ // Support both formats: deployment/name and deployment.apps/name
189
+ return stdout.includes(`/${name}`);
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ },
195
+ setNamespace: (namespace) => {
196
+ currentNamespace = namespace;
197
+ },
198
+ getNamespace: () => currentNamespace,
199
+ getKubeconfig: () => kubeconfig,
200
+ setCluster: (kubeConfigPath) => {
201
+ const quoted = kubeConfigPath.replace(/'/g, "'\\''");
202
+ return `export KUBECONFIG='${quoted}'`;
203
+ },
204
+ clusterInfo: async () => {
205
+ try {
206
+ // Run cluster-info command
207
+ let kubectlCmd = 'kubectl';
208
+ if (kubeconfig) {
209
+ kubectlCmd += ` --kubeconfig=${kubeconfig}`;
210
+ }
211
+ const result = await testContext.terminal.run(`${kubectlCmd} cluster-info`);
212
+ const output = result.output;
213
+ // Parse control plane URL
214
+ const controlPlaneMatch = output.match(/Kubernetes (?:control plane|master) is running at (https?:\/\/[^\s]+)/);
215
+ const coreDNSMatch = output.match(/CoreDNS is running at (https?:\/\/[^\s]+)/);
216
+ // Get server version
217
+ let serverVersion;
218
+ try {
219
+ const versionResult = await testContext.terminal.run(`${kubectlCmd} version --short 2>/dev/null || ${kubectlCmd} version -o json`, { silent: true });
220
+ const versionOutput = versionResult.stdout;
221
+ // Try to parse JSON format
222
+ try {
223
+ const versionJson = JSON.parse(versionOutput);
224
+ serverVersion = versionJson.serverVersion?.gitVersion;
225
+ }
226
+ catch {
227
+ // Try short format: Server Version: v1.28.0
228
+ const versionMatch = versionOutput.match(/Server Version:\s*(v[\d.]+)/);
229
+ serverVersion = versionMatch?.[1];
230
+ }
231
+ }
232
+ catch {
233
+ // Ignore version fetch errors
234
+ }
235
+ return {
236
+ reachable: true,
237
+ controlPlane: controlPlaneMatch?.[1],
238
+ coreDNS: coreDNSMatch?.[1],
239
+ serverVersion,
240
+ };
241
+ }
242
+ catch (e) {
243
+ return {
244
+ reachable: false,
245
+ error: e instanceof Error ? e.message : String(e),
246
+ };
247
+ }
248
+ },
249
+ // ===== New Core APIs =====
250
+ logs: async (podName, options) => {
251
+ let args = `logs ${podName}`;
252
+ if (options?.container)
253
+ args += ` -c ${options.container}`;
254
+ if (options?.tail !== undefined)
255
+ args += ` --tail=${options.tail}`;
256
+ if (options?.since)
257
+ args += ` --since=${options.since}`;
258
+ if (options?.previous)
259
+ args += ` --previous`;
260
+ if (options?.follow)
261
+ args += ` -f`;
262
+ return executeCommand(args);
263
+ },
264
+ exec: async (podName, command, options) => {
265
+ const cmd = Array.isArray(command) ? command.join(' ') : command;
266
+ let args = `exec ${podName}`;
267
+ if (options?.container)
268
+ args += ` -c ${options.container}`;
269
+ args += ` -- ${cmd}`;
270
+ return executeCommand(args);
271
+ },
272
+ describe: async (resource, name) => {
273
+ const args = name ? `describe ${resource} ${name}` : `describe ${resource}`;
274
+ return executeCommand(args);
275
+ },
276
+ wait: async (resource, name, condition, options) => {
277
+ const timeoutMs = options?.timeout ?? 20000;
278
+ const timeoutSec = Math.ceil(timeoutMs / 1000);
279
+ let cmd;
280
+ if (options?.forDelete) {
281
+ cmd = buildCommand(`wait ${resource}/${name} --for=delete --timeout=${timeoutSec}s`);
282
+ }
283
+ else {
284
+ cmd = buildCommand(`wait ${resource}/${name} --for=condition=${condition} --timeout=${timeoutSec}s`);
285
+ }
286
+ const result = await testContext.terminal.run(cmd);
287
+ return new WaitResult(result.output, cmd, result.code);
288
+ },
289
+ waitForJsonPath: async (resource, name, jsonPath, value, timeout = 60000) => {
290
+ const timeoutSec = Math.ceil(timeout / 1000);
291
+ // Ensure jsonPath is properly formatted for kubectl wait
292
+ const formattedPath = jsonPath.startsWith('{') ? jsonPath : `{${jsonPath}}`;
293
+ await executeCommand(`wait ${resource}/${name} --for=jsonpath='${formattedPath}'=${value} --timeout=${timeoutSec}s`);
294
+ },
295
+ waitForReplicas: async (resource, name, count, timeout = 60000) => {
296
+ const startTime = Date.now();
297
+ const checkInterval = 2000;
298
+ while (Date.now() - startTime < timeout) {
299
+ try {
300
+ const res = await methods.get(resource, name);
301
+ const readyReplicas = res?.status?.readyReplicas ?? 0;
302
+ if (readyReplicas >= count) {
303
+ return;
304
+ }
305
+ }
306
+ catch {
307
+ // Resource might not exist yet, continue waiting
308
+ }
309
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
310
+ }
311
+ throw new Error(`Timeout waiting for ${resource}/${name} to have ${count} ready replicas`);
312
+ },
313
+ // ===== Resource Management APIs =====
314
+ scale: async (resource, name, replicas) => {
315
+ const cmd = buildCommand(`scale ${resource}/${name} --replicas=${replicas}`);
316
+ const result = await testContext.terminal.run(cmd);
317
+ return new ScaleResult(result.output, cmd, result.code);
318
+ },
319
+ patch: async (resource, name, patch, type = 'strategic') => {
320
+ const patchStr = typeof patch === 'string' ? patch : JSON.stringify(patch);
321
+ const escapedPatch = patchStr.replace(/'/g, "'\\''");
322
+ const cmd = buildCommand(`patch ${resource} ${name} --type=${type} -p '${escapedPatch}'`);
323
+ const result = await testContext.terminal.run(cmd);
324
+ return new PatchResult(result.output, cmd, result.code);
325
+ },
326
+ label: async (resource, name, labels) => {
327
+ const labelArgs = Object.entries(labels)
328
+ .map(([key, value]) => (value === null ? `${key}-` : `${key}=${value}`))
329
+ .join(' ');
330
+ const cmd = buildCommand(`label ${resource} ${name} ${labelArgs} --overwrite`);
331
+ const result = await testContext.terminal.run(cmd);
332
+ return new LabelResult(result.output, cmd, result.code);
333
+ },
334
+ annotate: async (resource, name, annotations) => {
335
+ const annotationArgs = Object.entries(annotations)
336
+ .map(([key, value]) => (value === null ? `${key}-` : `${key}=${value}`))
337
+ .join(' ');
338
+ const cmd = buildCommand(`annotate ${resource} ${name} ${annotationArgs} --overwrite`);
339
+ const result = await testContext.terminal.run(cmd);
340
+ return new LabelResult(result.output, cmd, result.code);
341
+ },
342
+ // ===== Rollout Management =====
343
+ rollout: {
344
+ status: async (resource, name) => {
345
+ // Get resource status via JSON
346
+ const res = await methods.get(resource, name);
347
+ const status = res?.status ?? {};
348
+ const conditions = status.conditions ?? [];
349
+ const availableCondition = conditions.find((c) => c.type === 'Available');
350
+ const progressingCondition = conditions.find((c) => c.type === 'Progressing');
351
+ const ready = availableCondition?.status === 'True' &&
352
+ (progressingCondition?.status === 'True' || status.updatedReplicas === status.replicas);
353
+ return {
354
+ ready,
355
+ replicas: status.replicas ?? 0,
356
+ updatedReplicas: status.updatedReplicas ?? 0,
357
+ availableReplicas: status.availableReplicas ?? 0,
358
+ };
359
+ },
360
+ history: async (resource, name) => {
361
+ const output = await executeCommand(`rollout history ${resource}/${name}`);
362
+ const lines = output.split('\n').filter((line) => line.trim());
363
+ // Parse history output
364
+ const entries = [];
365
+ for (const line of lines) {
366
+ const match = line.match(/^\s*(\d+)\s+(.*)$/);
367
+ if (match) {
368
+ entries.push({
369
+ revision: parseInt(match[1], 10),
370
+ changeCause: match[2].trim() || undefined,
371
+ });
372
+ }
373
+ }
374
+ return entries;
375
+ },
376
+ undo: async (resource, name, revision) => {
377
+ const args = revision !== undefined ? `--to-revision=${revision}` : '';
378
+ await executeCommand(`rollout undo ${resource}/${name} ${args}`.trim());
379
+ },
380
+ restart: async (resource, name) => {
381
+ await executeCommand(`rollout restart ${resource}/${name}`);
382
+ },
383
+ pause: async (resource, name) => {
384
+ await executeCommand(`rollout pause ${resource}/${name}`);
385
+ },
386
+ resume: async (resource, name) => {
387
+ await executeCommand(`rollout resume ${resource}/${name}`);
388
+ },
389
+ },
390
+ // ===== Advanced Features =====
391
+ portForward: async (resource, ports, options) => {
392
+ const address = options?.address ?? '127.0.0.1';
393
+ const [localPortStr] = ports.split(':');
394
+ const localPort = parseInt(localPortStr, 10);
395
+ // Build the kubectl command
396
+ const args = ['kubectl'];
397
+ if (currentNamespace) {
398
+ args.push('-n', currentNamespace);
399
+ }
400
+ args.push('port-forward', resource, ports, `--address=${address}`);
401
+ // Start port-forward using Bun.spawn (silent, not in terminal)
402
+ const proc = Bun.spawn(args, {
403
+ stdout: 'pipe',
404
+ stderr: 'pipe',
405
+ });
406
+ // Wait for port-forward to establish
407
+ const delay = options?.delay ?? 2000;
408
+ await new Promise((resolve) => setTimeout(resolve, delay));
409
+ return {
410
+ localPort,
411
+ stop: async () => {
412
+ // Kill the process directly
413
+ proc.kill();
414
+ await proc.exited;
415
+ },
416
+ };
417
+ },
418
+ waitForService: async (name, timeout = 60000) => {
419
+ const timeoutSec = Math.ceil(timeout / 1000);
420
+ // Use kubectl wait for Endpoints to have addresses instead of polling
421
+ const waitCmd = buildCommand(`wait --for=jsonpath='{.subsets[0].addresses}' endpoints/${name} --timeout=${timeoutSec}s`);
422
+ await testContext.terminal.run(waitCmd);
423
+ // Fetch Service and Endpoints details
424
+ const svc = await methods.get('service', name);
425
+ const endpoints = await methods.get('endpoints', name);
426
+ const clusterIP = svc?.spec?.clusterIP;
427
+ const port = svc?.spec?.ports?.[0]?.port;
428
+ const addresses = endpoints?.subsets?.[0]?.addresses ?? [];
429
+ if (!clusterIP || !port) {
430
+ throw new Error(`Service ${name} missing clusterIP or port`);
431
+ }
432
+ return {
433
+ clusterIP,
434
+ port,
435
+ endpoints: addresses.map((a) => a.ip),
436
+ };
437
+ },
438
+ getEvents: async (options) => {
439
+ let args = 'get events -o json';
440
+ if (options?.fieldSelector) {
441
+ args += ` --field-selector=${options.fieldSelector}`;
442
+ }
443
+ const cmd = buildCommand(args);
444
+ try {
445
+ const result = await testContext.terminal.run(cmd);
446
+ let stdout = result.stdout;
447
+ if (testContext.terminal.isPtyMode?.()) {
448
+ const silentResult = await testContext.terminal.run(cmd, { silent: true });
449
+ stdout = silentResult.stdout;
450
+ }
451
+ const data = JSON.parse(stdout);
452
+ const items = data.items ?? [];
453
+ return items.map((item) => ({
454
+ type: (item.type ?? 'Normal'),
455
+ reason: item.reason ?? '',
456
+ message: item.message ?? '',
457
+ involvedObject: {
458
+ kind: item.involvedObject?.kind ?? '',
459
+ name: item.involvedObject?.name ?? '',
460
+ },
461
+ lastTimestamp: item.lastTimestamp ?? '',
462
+ }));
463
+ }
464
+ catch {
465
+ return [];
466
+ }
467
+ },
468
+ getNodes: async (options) => {
469
+ let args = 'get nodes -o json';
470
+ if (options?.selector) {
471
+ args += ` -l ${options.selector}`;
472
+ }
473
+ const cmd = buildCommand(args);
474
+ try {
475
+ const result = await testContext.terminal.run(cmd);
476
+ let stdout = result.stdout;
477
+ if (testContext.terminal.isPtyMode?.()) {
478
+ const silentResult = await testContext.terminal.run(cmd, { silent: true });
479
+ stdout = silentResult.stdout;
480
+ }
481
+ const data = JSON.parse(stdout);
482
+ const items = data.items ?? [];
483
+ return items.map((item) => {
484
+ const conditions = item.status?.conditions ?? [];
485
+ const readyCondition = conditions.find((c) => c.type === 'Ready');
486
+ const labels = item.metadata?.labels ?? {};
487
+ // Extract roles from labels
488
+ const roles = Object.keys(labels)
489
+ .filter((k) => k.startsWith('node-role.kubernetes.io/'))
490
+ .map((k) => k.replace('node-role.kubernetes.io/', ''));
491
+ const addresses = item.status?.addresses ?? [];
492
+ const internalIP = addresses.find((a) => a.type === 'InternalIP')?.address ?? '';
493
+ return {
494
+ name: item.metadata?.name ?? '',
495
+ status: (readyCondition?.status === 'True' ? 'Ready' : 'NotReady'),
496
+ roles: roles.length > 0 ? roles : ['<none>'],
497
+ version: item.status?.nodeInfo?.kubeletVersion ?? '',
498
+ internalIP,
499
+ };
500
+ });
501
+ }
502
+ catch {
503
+ return [];
504
+ }
505
+ },
506
+ cp: async (source, dest, options) => {
507
+ let args = `cp ${source} ${dest}`;
508
+ if (options?.container) {
509
+ args += ` -c ${options.container}`;
510
+ }
511
+ await executeCommand(args);
512
+ },
513
+ };
514
+ const hooks = {
515
+ beforeTest: async () => {
516
+ if (ctx.debug) {
517
+ console.log(`[kubectl] Using namespace: ${currentNamespace}`);
518
+ }
519
+ },
520
+ afterTest: async (_, error) => {
521
+ if (error && ctx.debug) {
522
+ console.log(`[kubectl] Test failed, consider cleaning up resources`);
523
+ }
524
+ },
525
+ };
526
+ return {
527
+ methods,
528
+ hooks,
529
+ context: {
530
+ kubectl: {
531
+ namespace: currentNamespace,
532
+ kubeconfig,
533
+ },
534
+ },
535
+ };
536
+ });
537
+ }
538
+ export const defaultKubectlPlugin = kubectlPlugin();
539
+ // Re-export matchers
540
+ export { K8sResource, pod, deployment, service, statefulset, job, configmap, secret, resource,
541
+ // Tensor Fusion CRD wrappers
542
+ gpupool, gpu, tensorfusionworkload, tensorfusionconnection, crd, registerK8sMatchers, } from './matchers.js';
543
+ // Re-export result types
544
+ export { KubectlResult, ApplyResult, DeleteResult, PatchResult, ScaleResult, LabelResult, } from './result.js';
@@ -0,0 +1,113 @@
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 type { KubectlMethods } from './index.js';
10
+ /**
11
+ * Augment bun:test Matchers to include K8s matcher methods.
12
+ * This provides TypeScript type information for dynamically added matchers.
13
+ */
14
+ declare module 'bun:test' {
15
+ interface Matchers<T> {
16
+ /** Assert that a kubectl operation succeeded */
17
+ toBeSuccessful(): Promise<void>;
18
+ /** Assert that a resource exists in the cluster */
19
+ toExistInCluster(): Promise<void>;
20
+ /** Assert that a resource does not exist in the cluster */
21
+ toNotExistInCluster(): Promise<void>;
22
+ /** Assert that a Pod is in Running state */
23
+ toBeRunning(timeout?: number): Promise<void>;
24
+ /** Assert that a Pod has a specific phase */
25
+ toHavePhase(phase: string): Promise<void>;
26
+ /** Assert that a resource has a specific number of replicas */
27
+ toHaveReplicas(count: number): Promise<void>;
28
+ /** Assert that a resource has a specific number of ready replicas */
29
+ toHaveReadyReplicas(count: number): Promise<void>;
30
+ /** Assert that a resource has a specific number of available replicas */
31
+ toHaveAvailableReplicas(count: number): Promise<void>;
32
+ /** Assert that a Deployment is available */
33
+ toBeAvailable(): Promise<void>;
34
+ /** Assert that a resource has a specific label */
35
+ toHaveLabel(key: string, value?: string): Promise<void>;
36
+ /** Assert that a resource has a specific annotation */
37
+ toHaveAnnotation(key: string, value?: string): Promise<void>;
38
+ /** Assert that a resource has a specific condition */
39
+ toHaveCondition(type: string, status: 'True' | 'False' | 'Unknown'): Promise<void>;
40
+ /** Assert that a resource has a specific status field value (supports dot notation for nested paths) */
41
+ toHaveStatusField(path: string, value: unknown): Promise<void>;
42
+ }
43
+ }
44
+ /**
45
+ * Wrapper class for Kubernetes resources
46
+ * Holds reference to kubectl methods for matcher operations
47
+ */
48
+ export declare class K8sResource {
49
+ readonly kubectl: KubectlMethods;
50
+ readonly kind: string;
51
+ readonly name: string;
52
+ constructor(kubectl: KubectlMethods, kind: string, name: string);
53
+ }
54
+ /**
55
+ * Create a Pod resource wrapper
56
+ */
57
+ export declare function pod(kubectl: KubectlMethods, name: string): K8sResource;
58
+ /**
59
+ * Create a Deployment resource wrapper
60
+ */
61
+ export declare function deployment(kubectl: KubectlMethods, name: string): K8sResource;
62
+ /**
63
+ * Create a Service resource wrapper
64
+ */
65
+ export declare function service(kubectl: KubectlMethods, name: string): K8sResource;
66
+ /**
67
+ * Create a StatefulSet resource wrapper
68
+ */
69
+ export declare function statefulset(kubectl: KubectlMethods, name: string): K8sResource;
70
+ /**
71
+ * Create a Job resource wrapper
72
+ */
73
+ export declare function job(kubectl: KubectlMethods, name: string): K8sResource;
74
+ /**
75
+ * Create a ConfigMap resource wrapper
76
+ */
77
+ export declare function configmap(kubectl: KubectlMethods, name: string): K8sResource;
78
+ /**
79
+ * Create a Secret resource wrapper
80
+ */
81
+ export declare function secret(kubectl: KubectlMethods, name: string): K8sResource;
82
+ /**
83
+ * Create a generic resource wrapper
84
+ */
85
+ export declare function resource(kubectl: KubectlMethods, kind: string, name: string): K8sResource;
86
+ /**
87
+ * Create a GPUPool resource wrapper (Tensor Fusion CRD)
88
+ */
89
+ export declare function gpupool(kubectl: KubectlMethods, name: string): K8sResource;
90
+ /**
91
+ * Create a GPU resource wrapper (Tensor Fusion CRD)
92
+ */
93
+ export declare function gpu(kubectl: KubectlMethods, name: string): K8sResource;
94
+ /**
95
+ * Create a TensorFusionWorkload resource wrapper (Tensor Fusion CRD)
96
+ */
97
+ export declare function tensorfusionworkload(kubectl: KubectlMethods, name: string): K8sResource;
98
+ /**
99
+ * Create a TensorFusionConnection resource wrapper (Tensor Fusion CRD)
100
+ */
101
+ export declare function tensorfusionconnection(kubectl: KubectlMethods, name: string): K8sResource;
102
+ /**
103
+ * Create a custom CRD resource wrapper with explicit API group
104
+ * @param kubectl - kubectl methods instance
105
+ * @param kind - Resource kind (e.g., 'gpupool.tensor-fusion.ai')
106
+ * @param name - Resource name
107
+ */
108
+ export declare function crd(kubectl: KubectlMethods, kind: string, name: string): K8sResource;
109
+ /**
110
+ * Register K8s matchers with expect
111
+ */
112
+ export declare function registerK8sMatchers(): void;
113
+ //# sourceMappingURL=matchers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matchers.d.ts","sourceRoot":"","sources":["../src/matchers.ts"],"names":[],"mappings":"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"}