@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
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubectl Plugin for Repterm
|
|
3
|
+
*
|
|
4
|
+
* Provides Kubernetes-specific testing utilities and commands.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import { definePlugin } from '@repterm/plugin-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 '@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
|
+
// 使用 kubectl wait 替代轮询,录制效果更好
|
|
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
|
+
// 使用 heredoc 语法避免录制时出现 quote> 提示符
|
|
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 模式:启动持续 watch,返回 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
|
+
// ★ 修复:返回 Promise,等待命令输入完成后再返回
|
|
96
|
+
// 调用方必须 await,确保 stepTitle 和命令输入完成
|
|
97
|
+
return (async () => {
|
|
98
|
+
await proc.start(); // 等待输入完成
|
|
99
|
+
return {
|
|
100
|
+
interrupt: async () => {
|
|
101
|
+
await proc.interrupt();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
})();
|
|
105
|
+
}
|
|
106
|
+
// 普通模式:获取 JSON 数据
|
|
107
|
+
return (async () => {
|
|
108
|
+
let args = name ? `get ${resource} ${name}` : `get ${resource}`;
|
|
109
|
+
if (options?.selector) {
|
|
110
|
+
args += ` -l ${options.selector}`;
|
|
111
|
+
}
|
|
112
|
+
if (options?.fieldSelector) {
|
|
113
|
+
args += ` --field-selector=${options.fieldSelector}`;
|
|
114
|
+
}
|
|
115
|
+
if (options?.allNamespaces) {
|
|
116
|
+
args += ' -A';
|
|
117
|
+
}
|
|
118
|
+
args += ' -o json';
|
|
119
|
+
// Build full command with optional jq filter
|
|
120
|
+
let fullCmd = buildCommand(args);
|
|
121
|
+
if (options?.jqFilter) {
|
|
122
|
+
fullCmd += ` | jq '${options.jqFilter}'`;
|
|
123
|
+
}
|
|
124
|
+
const result = await testContext.terminal.run(fullCmd);
|
|
125
|
+
let stdout;
|
|
126
|
+
if (testContext.terminal.isPtyMode?.()) {
|
|
127
|
+
const silentResult = await testContext.terminal.run(fullCmd, { silent: true });
|
|
128
|
+
stdout = silentResult.stdout;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
stdout = result.stdout;
|
|
132
|
+
}
|
|
133
|
+
// Check for kubectl errors before parsing JSON
|
|
134
|
+
const trimmed = stdout.trim();
|
|
135
|
+
if (trimmed.startsWith('error:') || trimmed.startsWith('Error') ||
|
|
136
|
+
trimmed.includes('Error from server') || trimmed.includes('not found')) {
|
|
137
|
+
throw new Error(`kubectl get failed: ${trimmed}`);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(trimmed);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return trimmed;
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
}),
|
|
147
|
+
getJsonPath: async (resource, name, jsonPath, options) => {
|
|
148
|
+
let args = `get ${resource} ${name}`;
|
|
149
|
+
if (options?.selector) {
|
|
150
|
+
args += ` -l ${options.selector}`;
|
|
151
|
+
}
|
|
152
|
+
if (options?.fieldSelector) {
|
|
153
|
+
args += ` --field-selector=${options.fieldSelector}`;
|
|
154
|
+
}
|
|
155
|
+
if (options?.allNamespaces) {
|
|
156
|
+
args += ' -A';
|
|
157
|
+
}
|
|
158
|
+
// Ensure jsonPath is properly quoted
|
|
159
|
+
const escapedPath = jsonPath.startsWith('{') ? jsonPath : `{${jsonPath}}`;
|
|
160
|
+
args += ` -o jsonpath='${escapedPath}'`;
|
|
161
|
+
const cmd = buildCommand(args);
|
|
162
|
+
const result = await testContext.terminal.run(cmd);
|
|
163
|
+
const output = testContext.terminal.isPtyMode?.()
|
|
164
|
+
? (await testContext.terminal.run(cmd, { silent: true })).stdout
|
|
165
|
+
: result.stdout;
|
|
166
|
+
const trimmed = output.trim();
|
|
167
|
+
// Handle empty/null values
|
|
168
|
+
if (trimmed === '' || trimmed === '<none>' || trimmed === '<nil>') {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
// Try to parse as JSON (numbers, booleans, etc.), otherwise return as string
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(trimmed);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return trimmed;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
exists: async (resource, name) => {
|
|
180
|
+
try {
|
|
181
|
+
const cmd = buildCommand(`get ${resource} ${name} -o name`);
|
|
182
|
+
const result = await testContext.terminal.run(`${cmd}`);
|
|
183
|
+
// 在 PTY 模式下,使用 silent 执行获取干净输出
|
|
184
|
+
let stdout = result.stdout;
|
|
185
|
+
if (testContext.terminal.isPtyMode?.()) {
|
|
186
|
+
const silentResult = await testContext.terminal.run(cmd, { silent: true });
|
|
187
|
+
stdout = silentResult.stdout;
|
|
188
|
+
}
|
|
189
|
+
// 兼容两种格式:deployment/name 和 deployment.apps/name
|
|
190
|
+
return stdout.includes(`/${name}`);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
setNamespace: (namespace) => {
|
|
197
|
+
currentNamespace = namespace;
|
|
198
|
+
},
|
|
199
|
+
getNamespace: () => currentNamespace,
|
|
200
|
+
getKubeconfig: () => kubeconfig,
|
|
201
|
+
setCluster: (kubeConfigPath) => {
|
|
202
|
+
const quoted = kubeConfigPath.replace(/'/g, "'\\''");
|
|
203
|
+
return `export KUBECONFIG='${quoted}'`;
|
|
204
|
+
},
|
|
205
|
+
clusterInfo: async () => {
|
|
206
|
+
try {
|
|
207
|
+
// Run cluster-info command
|
|
208
|
+
let kubectlCmd = 'kubectl';
|
|
209
|
+
if (kubeconfig) {
|
|
210
|
+
kubectlCmd += ` --kubeconfig=${kubeconfig}`;
|
|
211
|
+
}
|
|
212
|
+
const result = await testContext.terminal.run(`${kubectlCmd} cluster-info`);
|
|
213
|
+
const output = result.output;
|
|
214
|
+
// Parse control plane URL
|
|
215
|
+
const controlPlaneMatch = output.match(/Kubernetes (?:control plane|master) is running at (https?:\/\/[^\s]+)/);
|
|
216
|
+
const coreDNSMatch = output.match(/CoreDNS is running at (https?:\/\/[^\s]+)/);
|
|
217
|
+
// Get server version
|
|
218
|
+
let serverVersion;
|
|
219
|
+
try {
|
|
220
|
+
const versionResult = await testContext.terminal.run(`${kubectlCmd} version --short 2>/dev/null || ${kubectlCmd} version -o json`, { silent: true });
|
|
221
|
+
const versionOutput = versionResult.stdout;
|
|
222
|
+
// Try to parse JSON format
|
|
223
|
+
try {
|
|
224
|
+
const versionJson = JSON.parse(versionOutput);
|
|
225
|
+
serverVersion = versionJson.serverVersion?.gitVersion;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Try short format: Server Version: v1.28.0
|
|
229
|
+
const versionMatch = versionOutput.match(/Server Version:\s*(v[\d.]+)/);
|
|
230
|
+
serverVersion = versionMatch?.[1];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Ignore version fetch errors
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
reachable: true,
|
|
238
|
+
controlPlane: controlPlaneMatch?.[1],
|
|
239
|
+
coreDNS: coreDNSMatch?.[1],
|
|
240
|
+
serverVersion,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
return {
|
|
245
|
+
reachable: false,
|
|
246
|
+
error: e instanceof Error ? e.message : String(e),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
// ===== New Core APIs =====
|
|
251
|
+
logs: async (podName, options) => {
|
|
252
|
+
let args = `logs ${podName}`;
|
|
253
|
+
if (options?.container)
|
|
254
|
+
args += ` -c ${options.container}`;
|
|
255
|
+
if (options?.tail !== undefined)
|
|
256
|
+
args += ` --tail=${options.tail}`;
|
|
257
|
+
if (options?.since)
|
|
258
|
+
args += ` --since=${options.since}`;
|
|
259
|
+
if (options?.previous)
|
|
260
|
+
args += ` --previous`;
|
|
261
|
+
if (options?.follow)
|
|
262
|
+
args += ` -f`;
|
|
263
|
+
return executeCommand(args);
|
|
264
|
+
},
|
|
265
|
+
exec: async (podName, command, options) => {
|
|
266
|
+
const cmd = Array.isArray(command) ? command.join(' ') : command;
|
|
267
|
+
let args = `exec ${podName}`;
|
|
268
|
+
if (options?.container)
|
|
269
|
+
args += ` -c ${options.container}`;
|
|
270
|
+
args += ` -- ${cmd}`;
|
|
271
|
+
return executeCommand(args);
|
|
272
|
+
},
|
|
273
|
+
describe: async (resource, name) => {
|
|
274
|
+
const args = name ? `describe ${resource} ${name}` : `describe ${resource}`;
|
|
275
|
+
return executeCommand(args);
|
|
276
|
+
},
|
|
277
|
+
wait: async (resource, name, condition, options) => {
|
|
278
|
+
const timeoutMs = options?.timeout ?? 20000;
|
|
279
|
+
const timeoutSec = Math.ceil(timeoutMs / 1000);
|
|
280
|
+
let cmd;
|
|
281
|
+
if (options?.forDelete) {
|
|
282
|
+
cmd = buildCommand(`wait ${resource}/${name} --for=delete --timeout=${timeoutSec}s`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
cmd = buildCommand(`wait ${resource}/${name} --for=condition=${condition} --timeout=${timeoutSec}s`);
|
|
286
|
+
}
|
|
287
|
+
const result = await testContext.terminal.run(cmd);
|
|
288
|
+
return new WaitResult(result.output, cmd, result.code);
|
|
289
|
+
},
|
|
290
|
+
waitForJsonPath: async (resource, name, jsonPath, value, timeout = 60000) => {
|
|
291
|
+
const timeoutSec = Math.ceil(timeout / 1000);
|
|
292
|
+
// Ensure jsonPath is properly formatted for kubectl wait
|
|
293
|
+
const formattedPath = jsonPath.startsWith('{') ? jsonPath : `{${jsonPath}}`;
|
|
294
|
+
await executeCommand(`wait ${resource}/${name} --for=jsonpath='${formattedPath}'=${value} --timeout=${timeoutSec}s`);
|
|
295
|
+
},
|
|
296
|
+
waitForReplicas: async (resource, name, count, timeout = 60000) => {
|
|
297
|
+
const startTime = Date.now();
|
|
298
|
+
const checkInterval = 2000;
|
|
299
|
+
while (Date.now() - startTime < timeout) {
|
|
300
|
+
try {
|
|
301
|
+
const res = await methods.get(resource, name);
|
|
302
|
+
const readyReplicas = res?.status?.readyReplicas ?? 0;
|
|
303
|
+
if (readyReplicas >= count) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// Resource might not exist yet, continue waiting
|
|
309
|
+
}
|
|
310
|
+
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
311
|
+
}
|
|
312
|
+
throw new Error(`Timeout waiting for ${resource}/${name} to have ${count} ready replicas`);
|
|
313
|
+
},
|
|
314
|
+
// ===== Resource Management APIs =====
|
|
315
|
+
scale: async (resource, name, replicas) => {
|
|
316
|
+
const cmd = buildCommand(`scale ${resource}/${name} --replicas=${replicas}`);
|
|
317
|
+
const result = await testContext.terminal.run(cmd);
|
|
318
|
+
return new ScaleResult(result.output, cmd, result.code);
|
|
319
|
+
},
|
|
320
|
+
patch: async (resource, name, patch, type = 'strategic') => {
|
|
321
|
+
const patchStr = typeof patch === 'string' ? patch : JSON.stringify(patch);
|
|
322
|
+
const escapedPatch = patchStr.replace(/'/g, "'\\''");
|
|
323
|
+
const cmd = buildCommand(`patch ${resource} ${name} --type=${type} -p '${escapedPatch}'`);
|
|
324
|
+
const result = await testContext.terminal.run(cmd);
|
|
325
|
+
return new PatchResult(result.output, cmd, result.code);
|
|
326
|
+
},
|
|
327
|
+
label: async (resource, name, labels) => {
|
|
328
|
+
const labelArgs = Object.entries(labels)
|
|
329
|
+
.map(([key, value]) => (value === null ? `${key}-` : `${key}=${value}`))
|
|
330
|
+
.join(' ');
|
|
331
|
+
const cmd = buildCommand(`label ${resource} ${name} ${labelArgs} --overwrite`);
|
|
332
|
+
const result = await testContext.terminal.run(cmd);
|
|
333
|
+
return new LabelResult(result.output, cmd, result.code);
|
|
334
|
+
},
|
|
335
|
+
annotate: async (resource, name, annotations) => {
|
|
336
|
+
const annotationArgs = Object.entries(annotations)
|
|
337
|
+
.map(([key, value]) => (value === null ? `${key}-` : `${key}=${value}`))
|
|
338
|
+
.join(' ');
|
|
339
|
+
const cmd = buildCommand(`annotate ${resource} ${name} ${annotationArgs} --overwrite`);
|
|
340
|
+
const result = await testContext.terminal.run(cmd);
|
|
341
|
+
return new LabelResult(result.output, cmd, result.code);
|
|
342
|
+
},
|
|
343
|
+
// ===== Rollout Management =====
|
|
344
|
+
rollout: {
|
|
345
|
+
status: async (resource, name) => {
|
|
346
|
+
// Get resource status via JSON
|
|
347
|
+
const res = await methods.get(resource, name);
|
|
348
|
+
const status = res?.status ?? {};
|
|
349
|
+
const conditions = status.conditions ?? [];
|
|
350
|
+
const availableCondition = conditions.find((c) => c.type === 'Available');
|
|
351
|
+
const progressingCondition = conditions.find((c) => c.type === 'Progressing');
|
|
352
|
+
const ready = availableCondition?.status === 'True' &&
|
|
353
|
+
(progressingCondition?.status === 'True' || status.updatedReplicas === status.replicas);
|
|
354
|
+
return {
|
|
355
|
+
ready,
|
|
356
|
+
replicas: status.replicas ?? 0,
|
|
357
|
+
updatedReplicas: status.updatedReplicas ?? 0,
|
|
358
|
+
availableReplicas: status.availableReplicas ?? 0,
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
history: async (resource, name) => {
|
|
362
|
+
const output = await executeCommand(`rollout history ${resource}/${name}`);
|
|
363
|
+
const lines = output.split('\n').filter((line) => line.trim());
|
|
364
|
+
// Parse history output
|
|
365
|
+
const entries = [];
|
|
366
|
+
for (const line of lines) {
|
|
367
|
+
const match = line.match(/^\s*(\d+)\s+(.*)$/);
|
|
368
|
+
if (match) {
|
|
369
|
+
entries.push({
|
|
370
|
+
revision: parseInt(match[1], 10),
|
|
371
|
+
changeCause: match[2].trim() || undefined,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return entries;
|
|
376
|
+
},
|
|
377
|
+
undo: async (resource, name, revision) => {
|
|
378
|
+
const args = revision !== undefined ? `--to-revision=${revision}` : '';
|
|
379
|
+
await executeCommand(`rollout undo ${resource}/${name} ${args}`.trim());
|
|
380
|
+
},
|
|
381
|
+
restart: async (resource, name) => {
|
|
382
|
+
await executeCommand(`rollout restart ${resource}/${name}`);
|
|
383
|
+
},
|
|
384
|
+
pause: async (resource, name) => {
|
|
385
|
+
await executeCommand(`rollout pause ${resource}/${name}`);
|
|
386
|
+
},
|
|
387
|
+
resume: async (resource, name) => {
|
|
388
|
+
await executeCommand(`rollout resume ${resource}/${name}`);
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
// ===== Advanced Features =====
|
|
392
|
+
portForward: async (resource, ports, options) => {
|
|
393
|
+
const address = options?.address ?? '127.0.0.1';
|
|
394
|
+
const [localPortStr] = ports.split(':');
|
|
395
|
+
const localPort = parseInt(localPortStr, 10);
|
|
396
|
+
// Build the kubectl command
|
|
397
|
+
const args = ['kubectl'];
|
|
398
|
+
if (currentNamespace) {
|
|
399
|
+
args.push('-n', currentNamespace);
|
|
400
|
+
}
|
|
401
|
+
args.push('port-forward', resource, ports, `--address=${address}`);
|
|
402
|
+
// Start port-forward using Bun.spawn (silent, not in terminal)
|
|
403
|
+
const proc = Bun.spawn(args, {
|
|
404
|
+
stdout: 'pipe',
|
|
405
|
+
stderr: 'pipe',
|
|
406
|
+
});
|
|
407
|
+
// Wait for port-forward to establish
|
|
408
|
+
const delay = options?.delay ?? 2000;
|
|
409
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
410
|
+
return {
|
|
411
|
+
localPort,
|
|
412
|
+
stop: async () => {
|
|
413
|
+
// Kill the process directly
|
|
414
|
+
proc.kill();
|
|
415
|
+
await proc.exited;
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
waitForService: async (name, timeout = 60000) => {
|
|
420
|
+
const timeoutSec = Math.ceil(timeout / 1000);
|
|
421
|
+
// 使用 kubectl wait 等待 Endpoints 有地址,替代轮询
|
|
422
|
+
const waitCmd = buildCommand(`wait --for=jsonpath='{.subsets[0].addresses}' endpoints/${name} --timeout=${timeoutSec}s`);
|
|
423
|
+
await testContext.terminal.run(waitCmd);
|
|
424
|
+
// 获取 Service 和 Endpoints 详细信息
|
|
425
|
+
const svc = await methods.get('service', name);
|
|
426
|
+
const endpoints = await methods.get('endpoints', name);
|
|
427
|
+
const clusterIP = svc?.spec?.clusterIP;
|
|
428
|
+
const port = svc?.spec?.ports?.[0]?.port;
|
|
429
|
+
const addresses = endpoints?.subsets?.[0]?.addresses ?? [];
|
|
430
|
+
if (!clusterIP || !port) {
|
|
431
|
+
throw new Error(`Service ${name} missing clusterIP or port`);
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
clusterIP,
|
|
435
|
+
port,
|
|
436
|
+
endpoints: addresses.map((a) => a.ip),
|
|
437
|
+
};
|
|
438
|
+
},
|
|
439
|
+
getEvents: async (options) => {
|
|
440
|
+
let args = 'get events -o json';
|
|
441
|
+
if (options?.fieldSelector) {
|
|
442
|
+
args += ` --field-selector=${options.fieldSelector}`;
|
|
443
|
+
}
|
|
444
|
+
const cmd = buildCommand(args);
|
|
445
|
+
try {
|
|
446
|
+
const result = await testContext.terminal.run(cmd);
|
|
447
|
+
let stdout = result.stdout;
|
|
448
|
+
if (testContext.terminal.isPtyMode?.()) {
|
|
449
|
+
const silentResult = await testContext.terminal.run(cmd, { silent: true });
|
|
450
|
+
stdout = silentResult.stdout;
|
|
451
|
+
}
|
|
452
|
+
const data = JSON.parse(stdout);
|
|
453
|
+
const items = data.items ?? [];
|
|
454
|
+
return items.map((item) => ({
|
|
455
|
+
type: (item.type ?? 'Normal'),
|
|
456
|
+
reason: item.reason ?? '',
|
|
457
|
+
message: item.message ?? '',
|
|
458
|
+
involvedObject: {
|
|
459
|
+
kind: item.involvedObject?.kind ?? '',
|
|
460
|
+
name: item.involvedObject?.name ?? '',
|
|
461
|
+
},
|
|
462
|
+
lastTimestamp: item.lastTimestamp ?? '',
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
getNodes: async (options) => {
|
|
470
|
+
let args = 'get nodes -o json';
|
|
471
|
+
if (options?.selector) {
|
|
472
|
+
args += ` -l ${options.selector}`;
|
|
473
|
+
}
|
|
474
|
+
const cmd = buildCommand(args);
|
|
475
|
+
try {
|
|
476
|
+
const result = await testContext.terminal.run(cmd);
|
|
477
|
+
let stdout = result.stdout;
|
|
478
|
+
if (testContext.terminal.isPtyMode?.()) {
|
|
479
|
+
const silentResult = await testContext.terminal.run(cmd, { silent: true });
|
|
480
|
+
stdout = silentResult.stdout;
|
|
481
|
+
}
|
|
482
|
+
const data = JSON.parse(stdout);
|
|
483
|
+
const items = data.items ?? [];
|
|
484
|
+
return items.map((item) => {
|
|
485
|
+
const conditions = item.status?.conditions ?? [];
|
|
486
|
+
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
|
487
|
+
const labels = item.metadata?.labels ?? {};
|
|
488
|
+
// Extract roles from labels
|
|
489
|
+
const roles = Object.keys(labels)
|
|
490
|
+
.filter((k) => k.startsWith('node-role.kubernetes.io/'))
|
|
491
|
+
.map((k) => k.replace('node-role.kubernetes.io/', ''));
|
|
492
|
+
const addresses = item.status?.addresses ?? [];
|
|
493
|
+
const internalIP = addresses.find((a) => a.type === 'InternalIP')?.address ?? '';
|
|
494
|
+
return {
|
|
495
|
+
name: item.metadata?.name ?? '',
|
|
496
|
+
status: (readyCondition?.status === 'True' ? 'Ready' : 'NotReady'),
|
|
497
|
+
roles: roles.length > 0 ? roles : ['<none>'],
|
|
498
|
+
version: item.status?.nodeInfo?.kubeletVersion ?? '',
|
|
499
|
+
internalIP,
|
|
500
|
+
};
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
cp: async (source, dest, options) => {
|
|
508
|
+
let args = `cp ${source} ${dest}`;
|
|
509
|
+
if (options?.container) {
|
|
510
|
+
args += ` -c ${options.container}`;
|
|
511
|
+
}
|
|
512
|
+
await executeCommand(args);
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
const hooks = {
|
|
516
|
+
beforeTest: async () => {
|
|
517
|
+
if (ctx.debug) {
|
|
518
|
+
console.log(`[kubectl] Using namespace: ${currentNamespace}`);
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
afterTest: async (_, error) => {
|
|
522
|
+
if (error && ctx.debug) {
|
|
523
|
+
console.log(`[kubectl] Test failed, consider cleaning up resources`);
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
return {
|
|
528
|
+
methods,
|
|
529
|
+
hooks,
|
|
530
|
+
context: {
|
|
531
|
+
kubectl: {
|
|
532
|
+
namespace: currentNamespace,
|
|
533
|
+
kubeconfig,
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
export const defaultKubectlPlugin = kubectlPlugin();
|
|
540
|
+
// Re-export matchers
|
|
541
|
+
export { K8sResource, pod, deployment, service, statefulset, job, configmap, secret, resource,
|
|
542
|
+
// Tensor Fusion CRD wrappers
|
|
543
|
+
gpupool, gpu, tensorfusionworkload, tensorfusionconnection, crd, registerK8sMatchers, } from './matchers.js';
|
|
544
|
+
// Re-export result types
|
|
545
|
+
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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"}
|