@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/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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"}
|