@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,527 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubernetes Matchers for Repterm
|
|
3
|
+
*
|
|
4
|
+
* Provides expect() matchers for Kubernetes resources.
|
|
5
|
+
* All operations are executed through kubectl API (PTY visible).
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
import { expect } from 'bun:test';
|
|
10
|
+
import { KubectlResult } from './result.js';
|
|
11
|
+
// ===== K8sResource Wrapper =====
|
|
12
|
+
/**
|
|
13
|
+
* Wrapper class for Kubernetes resources
|
|
14
|
+
* Holds reference to kubectl methods for matcher operations
|
|
15
|
+
*/
|
|
16
|
+
export class K8sResource {
|
|
17
|
+
kubectl;
|
|
18
|
+
kind;
|
|
19
|
+
name;
|
|
20
|
+
constructor(kubectl, kind, name) {
|
|
21
|
+
this.kubectl = kubectl;
|
|
22
|
+
this.kind = kind;
|
|
23
|
+
this.name = name;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// ===== Helper Functions =====
|
|
27
|
+
/**
|
|
28
|
+
* Create a Pod resource wrapper
|
|
29
|
+
*/
|
|
30
|
+
export function pod(kubectl, name) {
|
|
31
|
+
return new K8sResource(kubectl, 'pod', name);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create a Deployment resource wrapper
|
|
35
|
+
*/
|
|
36
|
+
export function deployment(kubectl, name) {
|
|
37
|
+
return new K8sResource(kubectl, 'deployment', name);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a Service resource wrapper
|
|
41
|
+
*/
|
|
42
|
+
export function service(kubectl, name) {
|
|
43
|
+
return new K8sResource(kubectl, 'service', name);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create a StatefulSet resource wrapper
|
|
47
|
+
*/
|
|
48
|
+
export function statefulset(kubectl, name) {
|
|
49
|
+
return new K8sResource(kubectl, 'statefulset', name);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create a Job resource wrapper
|
|
53
|
+
*/
|
|
54
|
+
export function job(kubectl, name) {
|
|
55
|
+
return new K8sResource(kubectl, 'job', name);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Create a ConfigMap resource wrapper
|
|
59
|
+
*/
|
|
60
|
+
export function configmap(kubectl, name) {
|
|
61
|
+
return new K8sResource(kubectl, 'configmap', name);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create a Secret resource wrapper
|
|
65
|
+
*/
|
|
66
|
+
export function secret(kubectl, name) {
|
|
67
|
+
return new K8sResource(kubectl, 'secret', name);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a generic resource wrapper
|
|
71
|
+
*/
|
|
72
|
+
export function resource(kubectl, kind, name) {
|
|
73
|
+
return new K8sResource(kubectl, kind, name);
|
|
74
|
+
}
|
|
75
|
+
// ===== Tensor Fusion CRD Wrappers =====
|
|
76
|
+
/**
|
|
77
|
+
* Create a GPUPool resource wrapper (Tensor Fusion CRD)
|
|
78
|
+
*/
|
|
79
|
+
export function gpupool(kubectl, name) {
|
|
80
|
+
return new K8sResource(kubectl, 'gpupool', name);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create a GPU resource wrapper (Tensor Fusion CRD)
|
|
84
|
+
*/
|
|
85
|
+
export function gpu(kubectl, name) {
|
|
86
|
+
return new K8sResource(kubectl, 'gpu', name);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a TensorFusionWorkload resource wrapper (Tensor Fusion CRD)
|
|
90
|
+
*/
|
|
91
|
+
export function tensorfusionworkload(kubectl, name) {
|
|
92
|
+
return new K8sResource(kubectl, 'tensorfusionworkload', name);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create a TensorFusionConnection resource wrapper (Tensor Fusion CRD)
|
|
96
|
+
*/
|
|
97
|
+
export function tensorfusionconnection(kubectl, name) {
|
|
98
|
+
return new K8sResource(kubectl, 'tensorfusionconnection', name);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create a custom CRD resource wrapper with explicit API group
|
|
102
|
+
* @param kubectl - kubectl methods instance
|
|
103
|
+
* @param kind - Resource kind (e.g., 'gpupool.tensor-fusion.ai')
|
|
104
|
+
* @param name - Resource name
|
|
105
|
+
*/
|
|
106
|
+
export function crd(kubectl, kind, name) {
|
|
107
|
+
return new K8sResource(kubectl, kind, name);
|
|
108
|
+
}
|
|
109
|
+
// ===== Type Guard =====
|
|
110
|
+
function isK8sResource(value) {
|
|
111
|
+
return value instanceof K8sResource;
|
|
112
|
+
}
|
|
113
|
+
function isKubectlResult(value) {
|
|
114
|
+
return value instanceof KubectlResult;
|
|
115
|
+
}
|
|
116
|
+
// ===== Register Matchers =====
|
|
117
|
+
/**
|
|
118
|
+
* Register K8s matchers with expect
|
|
119
|
+
*/
|
|
120
|
+
export function registerK8sMatchers() {
|
|
121
|
+
expect.extend({
|
|
122
|
+
/**
|
|
123
|
+
* Assert that a kubectl operation succeeded
|
|
124
|
+
*/
|
|
125
|
+
async toBeSuccessful(received) {
|
|
126
|
+
if (!isKubectlResult(received)) {
|
|
127
|
+
return {
|
|
128
|
+
pass: false,
|
|
129
|
+
message: () => 'Expected value to be a KubectlResult (from kubectl.apply/delete/patch/scale/label)',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const pass = received.successful;
|
|
133
|
+
return {
|
|
134
|
+
pass,
|
|
135
|
+
message: () => pass
|
|
136
|
+
? 'Expected kubectl command to fail, but it succeeded'
|
|
137
|
+
: `Expected kubectl command to succeed, but failed:\n${received.output.slice(0, 500)}`,
|
|
138
|
+
actual: pass ? 'succeeded' : 'failed',
|
|
139
|
+
expected: 'succeeded',
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
/**
|
|
143
|
+
* Assert that a Pod is in Running state
|
|
144
|
+
*/
|
|
145
|
+
async toBeRunning(received, ...args) {
|
|
146
|
+
const timeout = args[0] ?? 60000;
|
|
147
|
+
if (!isK8sResource(received)) {
|
|
148
|
+
return {
|
|
149
|
+
pass: false,
|
|
150
|
+
message: () => 'Expected value to be a K8sResource',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const { kubectl, kind, name } = received;
|
|
154
|
+
if (kind !== 'pod') {
|
|
155
|
+
return {
|
|
156
|
+
pass: false,
|
|
157
|
+
message: () => `toBeRunning() is only valid for pods, got ${kind}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await kubectl.waitForPod(name, 'Running', timeout);
|
|
162
|
+
return {
|
|
163
|
+
pass: true,
|
|
164
|
+
message: () => `Expected pod/${name} not to be Running`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return {
|
|
169
|
+
pass: false,
|
|
170
|
+
message: () => `Expected pod/${name} to be Running within ${timeout}ms`,
|
|
171
|
+
actual: 'Not Running',
|
|
172
|
+
expected: 'Running',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
/**
|
|
177
|
+
* Assert that a Pod has a specific phase
|
|
178
|
+
*/
|
|
179
|
+
async toHavePhase(received, ...args) {
|
|
180
|
+
const phase = args[0];
|
|
181
|
+
if (!isK8sResource(received)) {
|
|
182
|
+
return {
|
|
183
|
+
pass: false,
|
|
184
|
+
message: () => 'Expected value to be a K8sResource',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const { kubectl, kind, name } = received;
|
|
188
|
+
try {
|
|
189
|
+
const actualPhase = await kubectl.getJsonPath(kind, name, '.status.phase');
|
|
190
|
+
const pass = actualPhase === phase;
|
|
191
|
+
return {
|
|
192
|
+
pass,
|
|
193
|
+
message: () => pass
|
|
194
|
+
? `Expected ${kind}/${name} not to have phase ${phase}`
|
|
195
|
+
: `Expected ${kind}/${name} to have phase ${phase}, got ${actualPhase}`,
|
|
196
|
+
actual: actualPhase,
|
|
197
|
+
expected: phase,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
return {
|
|
202
|
+
pass: false,
|
|
203
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
/**
|
|
208
|
+
* Assert that a resource has a specific number of replicas
|
|
209
|
+
*/
|
|
210
|
+
async toHaveReplicas(received, ...args) {
|
|
211
|
+
const count = args[0];
|
|
212
|
+
if (!isK8sResource(received)) {
|
|
213
|
+
return {
|
|
214
|
+
pass: false,
|
|
215
|
+
message: () => 'Expected value to be a K8sResource',
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const { kubectl, kind, name } = received;
|
|
219
|
+
try {
|
|
220
|
+
const actual = await kubectl.getJsonPath(kind, name, '.status.replicas') ?? 0;
|
|
221
|
+
const pass = actual === count;
|
|
222
|
+
return {
|
|
223
|
+
pass,
|
|
224
|
+
message: () => pass
|
|
225
|
+
? `Expected ${kind}/${name} not to have ${count} replicas`
|
|
226
|
+
: `Expected ${kind}/${name} to have ${count} replicas, got ${actual}`,
|
|
227
|
+
actual,
|
|
228
|
+
expected: count,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
return {
|
|
233
|
+
pass: false,
|
|
234
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
/**
|
|
239
|
+
* Assert that a resource has a specific number of available replicas
|
|
240
|
+
*/
|
|
241
|
+
async toHaveAvailableReplicas(received, ...args) {
|
|
242
|
+
const count = args[0];
|
|
243
|
+
if (!isK8sResource(received)) {
|
|
244
|
+
return {
|
|
245
|
+
pass: false,
|
|
246
|
+
message: () => 'Expected value to be a K8sResource',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const { kubectl, kind, name } = received;
|
|
250
|
+
try {
|
|
251
|
+
const actual = await kubectl.getJsonPath(kind, name, '.status.availableReplicas') ?? 0;
|
|
252
|
+
const pass = actual === count;
|
|
253
|
+
return {
|
|
254
|
+
pass,
|
|
255
|
+
message: () => pass
|
|
256
|
+
? `Expected ${kind}/${name} not to have ${count} available replicas`
|
|
257
|
+
: `Expected ${kind}/${name} to have ${count} available replicas, got ${actual}`,
|
|
258
|
+
actual,
|
|
259
|
+
expected: count,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
return {
|
|
264
|
+
pass: false,
|
|
265
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
/**
|
|
270
|
+
* Assert that a Deployment is available
|
|
271
|
+
*/
|
|
272
|
+
async toBeAvailable(received) {
|
|
273
|
+
if (!isK8sResource(received)) {
|
|
274
|
+
return {
|
|
275
|
+
pass: false,
|
|
276
|
+
message: () => 'Expected value to be a K8sResource',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const { kubectl, kind, name } = received;
|
|
280
|
+
try {
|
|
281
|
+
const jsonPath = '.status.conditions[?(@.type=="Available")].status';
|
|
282
|
+
const statusValue = await kubectl.getJsonPath(kind, name, jsonPath);
|
|
283
|
+
// JSONPath may return multiple values separated by space, take first
|
|
284
|
+
const actualStatus = statusValue?.split(' ')?.[0];
|
|
285
|
+
const pass = actualStatus === 'True';
|
|
286
|
+
return {
|
|
287
|
+
pass,
|
|
288
|
+
message: () => pass
|
|
289
|
+
? `Expected ${kind}/${name} not to be available`
|
|
290
|
+
: `Expected ${kind}/${name} to be available`,
|
|
291
|
+
actual: actualStatus,
|
|
292
|
+
expected: 'True',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
catch (e) {
|
|
296
|
+
return {
|
|
297
|
+
pass: false,
|
|
298
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
/**
|
|
303
|
+
* Assert that a resource exists in the cluster
|
|
304
|
+
*/
|
|
305
|
+
async toExistInCluster(received) {
|
|
306
|
+
if (!isK8sResource(received)) {
|
|
307
|
+
return {
|
|
308
|
+
pass: false,
|
|
309
|
+
message: () => 'Expected value to be a K8sResource',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const { kubectl, kind, name } = received;
|
|
313
|
+
const exists = await kubectl.exists(kind, name);
|
|
314
|
+
return {
|
|
315
|
+
pass: exists,
|
|
316
|
+
message: () => exists
|
|
317
|
+
? `Expected ${kind}/${name} not to exist in cluster`
|
|
318
|
+
: `Expected ${kind}/${name} to exist in cluster`,
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
/**
|
|
322
|
+
* Assert that a resource has a specific label
|
|
323
|
+
*/
|
|
324
|
+
async toHaveLabel(received, ...args) {
|
|
325
|
+
const key = args[0];
|
|
326
|
+
const value = args[1];
|
|
327
|
+
if (!isK8sResource(received)) {
|
|
328
|
+
return {
|
|
329
|
+
pass: false,
|
|
330
|
+
message: () => 'Expected value to be a K8sResource',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const { kubectl, kind, name } = received;
|
|
334
|
+
try {
|
|
335
|
+
// Escape dots in label key for JSONPath
|
|
336
|
+
const escapedKey = key.replace(/\./g, '\\.');
|
|
337
|
+
const jsonPath = `.metadata.labels.${escapedKey}`;
|
|
338
|
+
const actualValue = await kubectl.getJsonPath(kind, name, jsonPath);
|
|
339
|
+
const pass = value !== undefined ? actualValue === value : actualValue !== undefined;
|
|
340
|
+
return {
|
|
341
|
+
pass,
|
|
342
|
+
message: () => {
|
|
343
|
+
if (value !== undefined) {
|
|
344
|
+
return pass
|
|
345
|
+
? `Expected ${kind}/${name} not to have label ${key}=${value}`
|
|
346
|
+
: `Expected ${kind}/${name} to have label ${key}=${value}, got ${actualValue}`;
|
|
347
|
+
}
|
|
348
|
+
return pass
|
|
349
|
+
? `Expected ${kind}/${name} not to have label ${key}`
|
|
350
|
+
: `Expected ${kind}/${name} to have label ${key}`;
|
|
351
|
+
},
|
|
352
|
+
actual: actualValue,
|
|
353
|
+
expected: value ?? `label "${key}" exists`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
return {
|
|
358
|
+
pass: false,
|
|
359
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
/**
|
|
364
|
+
* Assert that a resource has a specific annotation
|
|
365
|
+
*/
|
|
366
|
+
async toHaveAnnotation(received, ...args) {
|
|
367
|
+
const key = args[0];
|
|
368
|
+
const value = args[1];
|
|
369
|
+
if (!isK8sResource(received)) {
|
|
370
|
+
return {
|
|
371
|
+
pass: false,
|
|
372
|
+
message: () => 'Expected value to be a K8sResource',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const { kubectl, kind, name } = received;
|
|
376
|
+
try {
|
|
377
|
+
// Escape dots in annotation key for JSONPath
|
|
378
|
+
const escapedKey = key.replace(/\./g, '\\.');
|
|
379
|
+
const jsonPath = `.metadata.annotations.${escapedKey}`;
|
|
380
|
+
const actualValue = await kubectl.getJsonPath(kind, name, jsonPath);
|
|
381
|
+
const pass = value !== undefined ? actualValue === value : actualValue !== undefined;
|
|
382
|
+
return {
|
|
383
|
+
pass,
|
|
384
|
+
message: () => {
|
|
385
|
+
if (value !== undefined) {
|
|
386
|
+
return pass
|
|
387
|
+
? `Expected ${kind}/${name} not to have annotation ${key}=${value}`
|
|
388
|
+
: `Expected ${kind}/${name} to have annotation ${key}=${value}, got ${actualValue}`;
|
|
389
|
+
}
|
|
390
|
+
return pass
|
|
391
|
+
? `Expected ${kind}/${name} not to have annotation ${key}`
|
|
392
|
+
: `Expected ${kind}/${name} to have annotation ${key}`;
|
|
393
|
+
},
|
|
394
|
+
actual: actualValue,
|
|
395
|
+
expected: value ?? `annotation "${key}" exists`,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
return {
|
|
400
|
+
pass: false,
|
|
401
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
/**
|
|
406
|
+
* Assert that a resource has a specific condition
|
|
407
|
+
*/
|
|
408
|
+
async toHaveCondition(received, ...args) {
|
|
409
|
+
const type = args[0];
|
|
410
|
+
const status = args[1];
|
|
411
|
+
if (!isK8sResource(received)) {
|
|
412
|
+
return {
|
|
413
|
+
pass: false,
|
|
414
|
+
message: () => 'Expected value to be a K8sResource',
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const { kubectl, kind, name } = received;
|
|
418
|
+
try {
|
|
419
|
+
const jsonPath = `.status.conditions[?(@.type=="${type}")].status`;
|
|
420
|
+
const statusValue = await kubectl.getJsonPath(kind, name, jsonPath);
|
|
421
|
+
// JSONPath may return multiple values separated by space, take first
|
|
422
|
+
const actualStatus = statusValue?.split(' ')?.[0];
|
|
423
|
+
const pass = actualStatus === status;
|
|
424
|
+
return {
|
|
425
|
+
pass,
|
|
426
|
+
message: () => pass
|
|
427
|
+
? `Expected ${kind}/${name} not to have condition ${type}=${status}`
|
|
428
|
+
: `Expected ${kind}/${name} to have condition ${type}=${status}, got ${actualStatus ?? 'not found'}`,
|
|
429
|
+
actual: actualStatus,
|
|
430
|
+
expected: status,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
return {
|
|
435
|
+
pass: false,
|
|
436
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
/**
|
|
441
|
+
* Assert that a resource does not exist in the cluster
|
|
442
|
+
*/
|
|
443
|
+
async toNotExistInCluster(received) {
|
|
444
|
+
if (!isK8sResource(received)) {
|
|
445
|
+
return {
|
|
446
|
+
pass: false,
|
|
447
|
+
message: () => 'Expected value to be a K8sResource',
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const { kubectl, kind, name } = received;
|
|
451
|
+
const exists = await kubectl.exists(kind, name);
|
|
452
|
+
return {
|
|
453
|
+
pass: !exists,
|
|
454
|
+
message: () => !exists
|
|
455
|
+
? `Expected ${kind}/${name} to exist in cluster`
|
|
456
|
+
: `Expected ${kind}/${name} not to exist in cluster`,
|
|
457
|
+
};
|
|
458
|
+
},
|
|
459
|
+
/**
|
|
460
|
+
* Assert that a resource has a specific number of ready replicas
|
|
461
|
+
*/
|
|
462
|
+
async toHaveReadyReplicas(received, ...args) {
|
|
463
|
+
const count = args[0];
|
|
464
|
+
if (!isK8sResource(received)) {
|
|
465
|
+
return {
|
|
466
|
+
pass: false,
|
|
467
|
+
message: () => 'Expected value to be a K8sResource',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const { kubectl, kind, name } = received;
|
|
471
|
+
try {
|
|
472
|
+
const actual = await kubectl.getJsonPath(kind, name, '.status.readyReplicas') ?? 0;
|
|
473
|
+
const pass = actual === count;
|
|
474
|
+
return {
|
|
475
|
+
pass,
|
|
476
|
+
message: () => pass
|
|
477
|
+
? `Expected ${kind}/${name} not to have ${count} ready replicas`
|
|
478
|
+
: `Expected ${kind}/${name} to have ${count} ready replicas, got ${actual}`,
|
|
479
|
+
actual,
|
|
480
|
+
expected: count,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
catch (e) {
|
|
484
|
+
return {
|
|
485
|
+
pass: false,
|
|
486
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
/**
|
|
491
|
+
* Assert that a resource has a specific status field value
|
|
492
|
+
* Supports dot notation for nested paths (e.g., 'phase', 'available.tflops')
|
|
493
|
+
*/
|
|
494
|
+
async toHaveStatusField(received, ...args) {
|
|
495
|
+
const path = args[0];
|
|
496
|
+
const expectedValue = args[1];
|
|
497
|
+
if (!isK8sResource(received)) {
|
|
498
|
+
return {
|
|
499
|
+
pass: false,
|
|
500
|
+
message: () => 'Expected value to be a K8sResource',
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const { kubectl, kind, name } = received;
|
|
504
|
+
try {
|
|
505
|
+
const jsonPath = `.status.${path}`;
|
|
506
|
+
const actual = await kubectl.getJsonPath(kind, name, jsonPath);
|
|
507
|
+
const pass = actual === expectedValue;
|
|
508
|
+
return {
|
|
509
|
+
pass,
|
|
510
|
+
message: () => pass
|
|
511
|
+
? `Expected ${kind}/${name} not to have status.${path} = ${JSON.stringify(expectedValue)}`
|
|
512
|
+
: `Expected ${kind}/${name} to have status.${path} = ${JSON.stringify(expectedValue)}, got ${JSON.stringify(actual)}`,
|
|
513
|
+
actual,
|
|
514
|
+
expected: expectedValue,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
catch (e) {
|
|
518
|
+
return {
|
|
519
|
+
pass: false,
|
|
520
|
+
message: () => `Failed to get ${kind}/${name}: ${e}`,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// Auto-register matchers when this module is imported
|
|
527
|
+
registerK8sMatchers();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kubectl Result Types
|
|
3
|
+
*
|
|
4
|
+
* Provides result classes for kubectl commands with intelligent success detection.
|
|
5
|
+
* Handles both normal mode (exitCode-based) and PTY/recording mode (output-based).
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Base class for kubectl command execution results.
|
|
11
|
+
* Provides unified success detection logic for both PTY and non-PTY modes.
|
|
12
|
+
*/
|
|
13
|
+
export declare abstract class KubectlResult {
|
|
14
|
+
/** Command output (stdout + stderr in PTY mode) */
|
|
15
|
+
readonly output: string;
|
|
16
|
+
/** The executed command */
|
|
17
|
+
readonly command: string;
|
|
18
|
+
/** Exit code (-1 in PTY mode where exit code is unavailable) */
|
|
19
|
+
readonly exitCode: number;
|
|
20
|
+
constructor(
|
|
21
|
+
/** Command output (stdout + stderr in PTY mode) */
|
|
22
|
+
output: string,
|
|
23
|
+
/** The executed command */
|
|
24
|
+
command: string,
|
|
25
|
+
/** Exit code (-1 in PTY mode where exit code is unavailable) */
|
|
26
|
+
exitCode: number);
|
|
27
|
+
/**
|
|
28
|
+
* Check if the command succeeded.
|
|
29
|
+
* - Non-PTY mode: Uses exitCode === 0
|
|
30
|
+
* - PTY mode (exitCode === -1): Delegates to subclass isOutputSuccessful()
|
|
31
|
+
*/
|
|
32
|
+
get successful(): boolean;
|
|
33
|
+
/** Subclass implementation: determine success based on output content */
|
|
34
|
+
protected abstract isOutputSuccessful(): boolean;
|
|
35
|
+
/** Common error detection helper */
|
|
36
|
+
protected hasError(): boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Result for kubectl apply command.
|
|
40
|
+
* Success indicators: "created", "configured", "unchanged"
|
|
41
|
+
*/
|
|
42
|
+
export declare class ApplyResult extends KubectlResult {
|
|
43
|
+
protected isOutputSuccessful(): boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Result for kubectl delete command.
|
|
47
|
+
* Success indicators: "deleted" or "not found" (with --ignore-not-found)
|
|
48
|
+
*/
|
|
49
|
+
export declare class DeleteResult extends KubectlResult {
|
|
50
|
+
protected isOutputSuccessful(): boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Result for kubectl patch command.
|
|
54
|
+
* Success indicators: "patched" or "(no change)"
|
|
55
|
+
*/
|
|
56
|
+
export declare class PatchResult extends KubectlResult {
|
|
57
|
+
protected isOutputSuccessful(): boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Result for kubectl scale command.
|
|
61
|
+
* Success indicators: "scaled"
|
|
62
|
+
*/
|
|
63
|
+
export declare class ScaleResult extends KubectlResult {
|
|
64
|
+
protected isOutputSuccessful(): boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Result for kubectl label/annotate commands.
|
|
68
|
+
* Success indicators: "labeled" or "annotated"
|
|
69
|
+
*/
|
|
70
|
+
export declare class LabelResult extends KubectlResult {
|
|
71
|
+
protected isOutputSuccessful(): boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Result for kubectl wait command.
|
|
75
|
+
* Success indicators: "condition met" or specific condition messages
|
|
76
|
+
*/
|
|
77
|
+
export declare class WaitResult extends KubectlResult {
|
|
78
|
+
protected isOutputSuccessful(): boolean;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=result.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../../src/result.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;GAGG;AACH,8BAAsB,aAAa;IAE3B,mDAAmD;aACnC,MAAM,EAAE,MAAM;IAC9B,2BAA2B;aACX,OAAO,EAAE,MAAM;IAC/B,gEAAgE;aAChD,QAAQ,EAAE,MAAM;;IALhC,mDAAmD;IACnC,MAAM,EAAE,MAAM;IAC9B,2BAA2B;IACX,OAAO,EAAE,MAAM;IAC/B,gEAAgE;IAChD,QAAQ,EAAE,MAAM;IAGpC;;;;OAIG;IACH,IAAI,UAAU,IAAI,OAAO,CAOxB;IAED,yEAAyE;IACzE,SAAS,CAAC,QAAQ,CAAC,kBAAkB,IAAI,OAAO;IAEhD,oCAAoC;IACpC,SAAS,CAAC,QAAQ,IAAI,OAAO;CAUhC;AAED;;;GAGG;AACH,qBAAa,WAAY,SAAQ,aAAa;IAC1C,SAAS,CAAC,kBAAkB,IAAI,OAAO;CAM1C;AAED;;;GAGG;AACH,qBAAa,YAAa,SAAQ,aAAa;IAC3C,SAAS,CAAC,kBAAkB,IAAI,OAAO;CAU1C;AAED;;;GAGG;AACH,qBAAa,WAAY,SAAQ,aAAa;IAC1C,SAAS,CAAC,kBAAkB,IAAI,OAAO;CAM1C;AAED;;;GAGG;AACH,qBAAa,WAAY,SAAQ,aAAa;IAC1C,SAAS,CAAC,kBAAkB,IAAI,OAAO;CAM1C;AAED;;;GAGG;AACH,qBAAa,WAAY,SAAQ,aAAa;IAC1C,SAAS,CAAC,kBAAkB,IAAI,OAAO;CAM1C;AAED;;;GAGG;AACH,qBAAa,UAAW,SAAQ,aAAa;IACzC,SAAS,CAAC,kBAAkB,IAAI,OAAO;CAS1C"}
|