@appthrust/kest 0.14.0 → 0.16.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 +48 -2
- package/example/dynamic-cluster.test.ts +157 -0
- package/package.json +1 -1
- package/ts/actions/create-namespace.ts +11 -6
- package/ts/apis/index.ts +111 -4
- package/ts/kubectl/index.ts +42 -1
- package/ts/scenario/index.ts +8 -28
- package/ts/scenario/use-cluster.ts +185 -0
- package/example/example-report-with-duration.md +0 -142
package/README.md
CHANGED
|
@@ -185,6 +185,52 @@ test("resources sync across clusters", async (s) => {
|
|
|
185
185
|
});
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
#### CAPI Dynamic Clusters
|
|
189
|
+
|
|
190
|
+
Kest can resolve [Cluster API](https://cluster-api.sigs.k8s.io/) (CAPI) provisioned clusters automatically. Pass a `ClusterResourceReference` to `useCluster` and Kest will:
|
|
191
|
+
|
|
192
|
+
1. Poll the CAPI `Cluster` resource until it reports ready
|
|
193
|
+
2. Fetch the kubeconfig from the `<name>-kubeconfig` Secret (data key `value`)
|
|
194
|
+
3. Write a temporary kubeconfig file and bind kubectl to it
|
|
195
|
+
4. Register cleanup to delete the temp file when the test ends
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
test("CAPI multi-hop: management → child → workload", async (s) => {
|
|
199
|
+
// 1. Connect to the permanent management cluster
|
|
200
|
+
const pmc = await s.useCluster({ context: "kind-pmc" });
|
|
201
|
+
|
|
202
|
+
// 2. Wait for the child cluster to become ready, then connect
|
|
203
|
+
const cc = await pmc.useCluster({
|
|
204
|
+
apiVersion: "cluster.x-k8s.io/v1beta1",
|
|
205
|
+
kind: "Cluster",
|
|
206
|
+
name: "child-cluster",
|
|
207
|
+
namespace: "capi-system",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// 3. Chain further — the child cluster manages a workload cluster
|
|
211
|
+
const wlc = await cc.useCluster({
|
|
212
|
+
apiVersion: "cluster.x-k8s.io/v1beta1",
|
|
213
|
+
kind: "Cluster",
|
|
214
|
+
name: "workload-cluster",
|
|
215
|
+
namespace: "default",
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const ns = await wlc.newNamespace();
|
|
219
|
+
await ns.apply(/* ... */);
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Readiness conditions:**
|
|
224
|
+
|
|
225
|
+
| API version | Condition type |
|
|
226
|
+
| ------------------------------- | -------------- |
|
|
227
|
+
| `cluster.x-k8s.io/v1beta1` | `Ready` |
|
|
228
|
+
| `cluster.x-k8s.io/v1beta2` | `Available` |
|
|
229
|
+
|
|
230
|
+
**Secret convention:** Kest reads the kubeconfig from a Secret named `${clusterName}-kubeconfig` with the data key `value`. This follows the default CAPI convention. Your RBAC must grant `get` on Secrets in the namespace where the Cluster resource lives.
|
|
231
|
+
|
|
232
|
+
**Temp kubeconfig cleanup:** The temporary kubeconfig file is deleted automatically during test cleanup. Set `KEST_PRESERVE_ON_FAILURE=1` to skip cleanup on failure, which keeps the temp file for debugging.
|
|
233
|
+
|
|
188
234
|
### Status Subresource Support
|
|
189
235
|
|
|
190
236
|
Simulate controller behavior by applying status subresources via server-side apply:
|
|
@@ -535,12 +581,12 @@ The top-level API surface available in every test callback.
|
|
|
535
581
|
| `newNamespace(name?, options?)` | Create an ephemeral namespace (supports `{ generateName }`) |
|
|
536
582
|
| `generateName(prefix)` | Generate a random-suffix name (statistical uniqueness) |
|
|
537
583
|
| `exec(input, options?)` | Execute shell commands with optional revert |
|
|
538
|
-
| `useCluster(ref)`
|
|
584
|
+
| `useCluster(ref, options?)` | Create a cluster-bound API surface |
|
|
539
585
|
| `given(desc)` / `when(desc)` / `then(desc)` / `and(desc)` / `but(desc)` | BDD annotations for reporting |
|
|
540
586
|
|
|
541
587
|
### Namespace / Cluster
|
|
542
588
|
|
|
543
|
-
Returned by `newNamespace()` and `useCluster()` respectively. They expose the same core methods (`apply`, `create`, `assertApplyError`, `assertCreateError`, `applyStatus`, `delete`, `label`, `get`, `assert`, `assertAbsence`, `assertList`, `assertOne`) scoped to their namespace or cluster context. `Cluster` additionally supports `newNamespace`.
|
|
589
|
+
Returned by `newNamespace()` and `useCluster()` respectively. They expose the same core methods (`apply`, `create`, `assertApplyError`, `assertCreateError`, `applyStatus`, `delete`, `label`, `get`, `assert`, `assertAbsence`, `assertList`, `assertOne`) scoped to their namespace or cluster context. `Cluster` additionally supports `newNamespace` and `useCluster`.
|
|
544
590
|
|
|
545
591
|
`Namespace` also exposes a `name` property:
|
|
546
592
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/** biome-ignore-all lint/style/noDoneCallback: that's not the done callback */
|
|
2
|
+
import { expect } from "bun:test";
|
|
3
|
+
import { type K8sResource, test } from "@appthrust/kest";
|
|
4
|
+
|
|
5
|
+
interface ConfigMap extends K8sResource {
|
|
6
|
+
apiVersion: "v1";
|
|
7
|
+
kind: "ConfigMap";
|
|
8
|
+
metadata: {
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
data: {
|
|
12
|
+
[key: string]: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test(
|
|
17
|
+
"Example: provision a CAPI child cluster, connect, and create resources",
|
|
18
|
+
async (s) => {
|
|
19
|
+
s.given("a management cluster with CAPI and k0smotron installed");
|
|
20
|
+
const mgmt = await s.useCluster({ context: "kind-kest-test-cluster-1" });
|
|
21
|
+
|
|
22
|
+
s.when("I provision a child cluster via CAPI");
|
|
23
|
+
await mgmt.apply({
|
|
24
|
+
apiVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
|
|
25
|
+
kind: "DockerMachineTemplate",
|
|
26
|
+
metadata: { name: "child-cluster-worker-mt", namespace: "default" },
|
|
27
|
+
spec: { template: { spec: {} } },
|
|
28
|
+
});
|
|
29
|
+
await mgmt.apply({
|
|
30
|
+
apiVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
|
|
31
|
+
kind: "K0sWorkerConfigTemplate",
|
|
32
|
+
metadata: { name: "child-cluster-worker-config", namespace: "default" },
|
|
33
|
+
spec: { template: { spec: { version: "v1.31.2+k0s.0" } } },
|
|
34
|
+
});
|
|
35
|
+
await mgmt.apply({
|
|
36
|
+
apiVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
|
|
37
|
+
kind: "DockerCluster",
|
|
38
|
+
metadata: {
|
|
39
|
+
name: "child-cluster",
|
|
40
|
+
namespace: "default",
|
|
41
|
+
annotations: { "cluster.x-k8s.io/managed-by": "k0smotron" },
|
|
42
|
+
},
|
|
43
|
+
spec: {},
|
|
44
|
+
});
|
|
45
|
+
await mgmt.apply({
|
|
46
|
+
apiVersion: "controlplane.cluster.x-k8s.io/v1beta1",
|
|
47
|
+
kind: "K0smotronControlPlane",
|
|
48
|
+
metadata: { name: "child-cluster-cp", namespace: "default" },
|
|
49
|
+
spec: {
|
|
50
|
+
version: "v1.31.2-k0s.0",
|
|
51
|
+
persistence: { type: "emptyDir" },
|
|
52
|
+
service: {
|
|
53
|
+
type: "LoadBalancer",
|
|
54
|
+
apiPort: 6443,
|
|
55
|
+
konnectivityPort: 8132,
|
|
56
|
+
},
|
|
57
|
+
k0sConfig: {
|
|
58
|
+
apiVersion: "k0s.k0sproject.io/v1beta1",
|
|
59
|
+
kind: "ClusterConfig",
|
|
60
|
+
spec: { telemetry: { enabled: false } },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
await mgmt.apply({
|
|
65
|
+
apiVersion: "cluster.x-k8s.io/v1beta1",
|
|
66
|
+
kind: "MachineDeployment",
|
|
67
|
+
metadata: { name: "child-cluster-workers", namespace: "default" },
|
|
68
|
+
spec: {
|
|
69
|
+
clusterName: "child-cluster",
|
|
70
|
+
replicas: 1,
|
|
71
|
+
selector: {
|
|
72
|
+
matchLabels: {
|
|
73
|
+
"cluster.x-k8s.io/cluster-name": "child-cluster",
|
|
74
|
+
pool: "worker-pool",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
template: {
|
|
78
|
+
metadata: {
|
|
79
|
+
labels: {
|
|
80
|
+
"cluster.x-k8s.io/cluster-name": "child-cluster",
|
|
81
|
+
pool: "worker-pool",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
spec: {
|
|
85
|
+
clusterName: "child-cluster",
|
|
86
|
+
version: "v1.31.2",
|
|
87
|
+
bootstrap: {
|
|
88
|
+
configRef: {
|
|
89
|
+
apiVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
|
|
90
|
+
kind: "K0sWorkerConfigTemplate",
|
|
91
|
+
name: "child-cluster-worker-config",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
infrastructureRef: {
|
|
95
|
+
apiVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
|
|
96
|
+
kind: "DockerMachineTemplate",
|
|
97
|
+
name: "child-cluster-worker-mt",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
await mgmt.apply({
|
|
104
|
+
apiVersion: "cluster.x-k8s.io/v1beta1",
|
|
105
|
+
kind: "Cluster",
|
|
106
|
+
metadata: { name: "child-cluster", namespace: "default" },
|
|
107
|
+
spec: {
|
|
108
|
+
clusterNetwork: {
|
|
109
|
+
pods: { cidrBlocks: ["192.168.0.0/16"] },
|
|
110
|
+
serviceDomain: "cluster.local",
|
|
111
|
+
services: { cidrBlocks: ["10.128.0.0/12"] },
|
|
112
|
+
},
|
|
113
|
+
controlPlaneRef: {
|
|
114
|
+
apiVersion: "controlplane.cluster.x-k8s.io/v1beta1",
|
|
115
|
+
kind: "K0smotronControlPlane",
|
|
116
|
+
name: "child-cluster-cp",
|
|
117
|
+
},
|
|
118
|
+
infrastructureRef: {
|
|
119
|
+
apiVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
|
|
120
|
+
kind: "DockerCluster",
|
|
121
|
+
name: "child-cluster",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
s.and("the child cluster becomes ready");
|
|
127
|
+
const child = await mgmt.useCluster(
|
|
128
|
+
{
|
|
129
|
+
apiVersion: "cluster.x-k8s.io/v1beta1",
|
|
130
|
+
kind: "Cluster",
|
|
131
|
+
name: "child-cluster",
|
|
132
|
+
namespace: "default",
|
|
133
|
+
},
|
|
134
|
+
{ timeout: "10m", interval: "5s" }
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
s.and("I create a namespace and apply a ConfigMap on the child cluster");
|
|
138
|
+
const ns = await child.newNamespace();
|
|
139
|
+
await ns.apply<ConfigMap>({
|
|
140
|
+
apiVersion: "v1",
|
|
141
|
+
kind: "ConfigMap",
|
|
142
|
+
metadata: { name: "hello-from-capi" },
|
|
143
|
+
data: { source: "management-cluster" },
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
s.then("the ConfigMap should exist on the child cluster");
|
|
147
|
+
await ns.assert<ConfigMap>({
|
|
148
|
+
apiVersion: "v1",
|
|
149
|
+
kind: "ConfigMap",
|
|
150
|
+
name: "hello-from-capi",
|
|
151
|
+
test() {
|
|
152
|
+
expect(this.data["source"]).toBe("management-cluster");
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
{ timeout: "15m" }
|
|
157
|
+
);
|
package/package.json
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { generateName } from "../naming";
|
|
2
|
-
import { create } from "./create";
|
|
3
2
|
import type { MutateDef } from "./types";
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -22,14 +21,20 @@ export const createNamespace = {
|
|
|
22
21
|
({ kubectl }) =>
|
|
23
22
|
async (input) => {
|
|
24
23
|
const name = resolveNamespaceName(input);
|
|
25
|
-
|
|
24
|
+
await kubectl.create({
|
|
26
25
|
apiVersion: "v1",
|
|
27
26
|
kind: "Namespace",
|
|
28
|
-
metadata: {
|
|
29
|
-
name,
|
|
30
|
-
},
|
|
27
|
+
metadata: { name },
|
|
31
28
|
});
|
|
32
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
async revert() {
|
|
31
|
+
await kubectl.delete("Namespace", name, {
|
|
32
|
+
ignoreNotFound: true,
|
|
33
|
+
wait: false,
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
output: name,
|
|
37
|
+
};
|
|
33
38
|
},
|
|
34
39
|
describe: (input) => {
|
|
35
40
|
if (input === undefined) {
|
package/ts/apis/index.ts
CHANGED
|
@@ -518,10 +518,12 @@ export interface Scenario {
|
|
|
518
518
|
* The returned {@link Cluster} uses the provided kubeconfig/context for all
|
|
519
519
|
* actions. It does not create any resources by itself.
|
|
520
520
|
*
|
|
521
|
-
* @param cluster - Target kubeconfig
|
|
521
|
+
* @param cluster - Target cluster reference (static kubeconfig/context or CAPI resource).
|
|
522
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
522
523
|
*
|
|
523
524
|
* @example
|
|
524
525
|
* ```ts
|
|
526
|
+
* // Static cluster reference
|
|
525
527
|
* const c = await s.useCluster({ context: "kind-kind" });
|
|
526
528
|
* const ns = await c.newNamespace("my-ns");
|
|
527
529
|
* await ns.apply({
|
|
@@ -531,8 +533,22 @@ export interface Scenario {
|
|
|
531
533
|
* data: { mode: "cluster" },
|
|
532
534
|
* });
|
|
533
535
|
* ```
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* ```ts
|
|
539
|
+
* // CAPI cluster resource reference
|
|
540
|
+
* const c = await s.useCluster({
|
|
541
|
+
* apiVersion: "cluster.x-k8s.io/v1beta1",
|
|
542
|
+
* kind: "Cluster",
|
|
543
|
+
* name: "workload-1",
|
|
544
|
+
* namespace: "default",
|
|
545
|
+
* });
|
|
546
|
+
* ```
|
|
534
547
|
*/
|
|
535
|
-
useCluster(
|
|
548
|
+
useCluster(
|
|
549
|
+
cluster: ClusterReference,
|
|
550
|
+
options?: undefined | ActionOptions
|
|
551
|
+
): Promise<Cluster>;
|
|
536
552
|
|
|
537
553
|
// BDD(behavior-driven development) actions
|
|
538
554
|
|
|
@@ -879,6 +895,35 @@ export interface Cluster {
|
|
|
879
895
|
name?: undefined | string | { readonly generateName: string },
|
|
880
896
|
options?: undefined | ActionOptions
|
|
881
897
|
): Promise<Namespace>;
|
|
898
|
+
|
|
899
|
+
// Multi-cluster actions
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Creates a cluster-bound API surface from this cluster.
|
|
903
|
+
*
|
|
904
|
+
* This enables multi-hop cluster access — for example, using a management
|
|
905
|
+
* cluster to reach a CAPI-provisioned workload cluster.
|
|
906
|
+
*
|
|
907
|
+
* @param cluster - Target cluster reference (static or CAPI resource).
|
|
908
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
909
|
+
*
|
|
910
|
+
* @example
|
|
911
|
+
* ```ts
|
|
912
|
+
* // From a management cluster, obtain a workload cluster handle
|
|
913
|
+
* const mgmt = await s.useCluster({ context: "kind-mgmt" });
|
|
914
|
+
* const workload = await mgmt.useCluster({
|
|
915
|
+
* apiVersion: "cluster.x-k8s.io/v1beta1",
|
|
916
|
+
* kind: "Cluster",
|
|
917
|
+
* name: "workload-1",
|
|
918
|
+
* namespace: "default",
|
|
919
|
+
* });
|
|
920
|
+
* const ns = await workload.newNamespace("test");
|
|
921
|
+
* ```
|
|
922
|
+
*/
|
|
923
|
+
useCluster(
|
|
924
|
+
cluster: ClusterReference,
|
|
925
|
+
options?: undefined | ActionOptions
|
|
926
|
+
): Promise<Cluster>;
|
|
882
927
|
}
|
|
883
928
|
|
|
884
929
|
/**
|
|
@@ -1483,9 +1528,14 @@ export interface AssertCreateErrorInput<T extends K8sResource = K8sResource> {
|
|
|
1483
1528
|
}
|
|
1484
1529
|
|
|
1485
1530
|
/**
|
|
1486
|
-
*
|
|
1531
|
+
* Static cluster selector that points to an existing kubeconfig/context.
|
|
1532
|
+
*
|
|
1533
|
+
* Use this when the cluster is already provisioned and you have direct access
|
|
1534
|
+
* to its kubeconfig file or context name.
|
|
1535
|
+
*
|
|
1536
|
+
* @see {@link ClusterReference} — the union accepted by {@link Scenario.useCluster}.
|
|
1487
1537
|
*/
|
|
1488
|
-
export interface
|
|
1538
|
+
export interface StaticClusterReference {
|
|
1489
1539
|
/**
|
|
1490
1540
|
* Path to a kubeconfig file to use for this cluster.
|
|
1491
1541
|
*/
|
|
@@ -1497,6 +1547,63 @@ export interface ClusterReference {
|
|
|
1497
1547
|
readonly context?: undefined | string;
|
|
1498
1548
|
}
|
|
1499
1549
|
|
|
1550
|
+
/**
|
|
1551
|
+
* Supported Cluster API versions for CAPI-provisioned clusters.
|
|
1552
|
+
*/
|
|
1553
|
+
export type CapiClusterApiVersion =
|
|
1554
|
+
| "cluster.x-k8s.io/v1beta1"
|
|
1555
|
+
| "cluster.x-k8s.io/v1beta2";
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Reference to a CAPI-provisioned cluster resource.
|
|
1559
|
+
*
|
|
1560
|
+
* Kest resolves the cluster's kubeconfig by reading the corresponding
|
|
1561
|
+
* `<name>-kubeconfig` Secret from the management cluster and passes it to
|
|
1562
|
+
* kubectl automatically.
|
|
1563
|
+
*
|
|
1564
|
+
* @example
|
|
1565
|
+
* ```ts
|
|
1566
|
+
* const c = await s.useCluster({
|
|
1567
|
+
* apiVersion: "cluster.x-k8s.io/v1beta1",
|
|
1568
|
+
* kind: "Cluster",
|
|
1569
|
+
* name: "workload-1",
|
|
1570
|
+
* namespace: "default",
|
|
1571
|
+
* });
|
|
1572
|
+
* ```
|
|
1573
|
+
*/
|
|
1574
|
+
export interface CapiClusterResourceReference {
|
|
1575
|
+
/** Cluster API group version. */
|
|
1576
|
+
readonly apiVersion: CapiClusterApiVersion;
|
|
1577
|
+
|
|
1578
|
+
/** Must be `"Cluster"`. */
|
|
1579
|
+
readonly kind: "Cluster";
|
|
1580
|
+
|
|
1581
|
+
/** Name of the CAPI Cluster resource. */
|
|
1582
|
+
readonly name: string;
|
|
1583
|
+
|
|
1584
|
+
/** Namespace where the CAPI Cluster resource lives. */
|
|
1585
|
+
readonly namespace: string;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* Union of cluster resource reference types.
|
|
1590
|
+
*
|
|
1591
|
+
* Currently only CAPI clusters are supported, but this alias leaves room for
|
|
1592
|
+
* additional providers in the future.
|
|
1593
|
+
*/
|
|
1594
|
+
export type ClusterResourceReference = CapiClusterResourceReference;
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Kubernetes cluster selector for {@link Scenario.useCluster} and
|
|
1598
|
+
* {@link Cluster.useCluster}.
|
|
1599
|
+
*
|
|
1600
|
+
* - {@link StaticClusterReference} — points to an existing kubeconfig/context.
|
|
1601
|
+
* - {@link ClusterResourceReference} — references a CAPI Cluster resource.
|
|
1602
|
+
*/
|
|
1603
|
+
export type ClusterReference =
|
|
1604
|
+
| StaticClusterReference
|
|
1605
|
+
| ClusterResourceReference;
|
|
1606
|
+
|
|
1500
1607
|
/**
|
|
1501
1608
|
* A Kubernetes manifest accepted by Kest actions.
|
|
1502
1609
|
*
|
package/ts/kubectl/index.ts
CHANGED
|
@@ -155,6 +155,26 @@ export interface Kubectl {
|
|
|
155
155
|
labels: Readonly<Record<string, string | null>>,
|
|
156
156
|
options?: KubectlLabelOptions
|
|
157
157
|
): Promise<string>;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Retrieves and base64-decodes a single key from a Kubernetes Secret.
|
|
161
|
+
*
|
|
162
|
+
* Runs: `kubectl get secret <name> -o jsonpath='{.data.<key>}'`
|
|
163
|
+
*
|
|
164
|
+
* The raw secret value is **not** recorded; the recorded stdout is replaced
|
|
165
|
+
* with `"<redacted>"` to avoid leaking sensitive data into reports/logs.
|
|
166
|
+
*
|
|
167
|
+
* @param name - The name of the Secret resource
|
|
168
|
+
* @param key - The data key to retrieve (e.g., "password", "kubeconfig")
|
|
169
|
+
* @param context - Optional context overrides for this call
|
|
170
|
+
* @returns The base64-decoded value of the requested key
|
|
171
|
+
* @throws Error if kubectl exits with non-zero code
|
|
172
|
+
*/
|
|
173
|
+
getSecretData(
|
|
174
|
+
name: string,
|
|
175
|
+
key: string,
|
|
176
|
+
context?: KubectlContext
|
|
177
|
+
): Promise<string>;
|
|
158
178
|
}
|
|
159
179
|
|
|
160
180
|
export interface KubectlDeleteOptions {
|
|
@@ -163,6 +183,8 @@ export interface KubectlDeleteOptions {
|
|
|
163
183
|
* that does not exist succeeds silently instead of failing.
|
|
164
184
|
*/
|
|
165
185
|
readonly ignoreNotFound?: undefined | boolean;
|
|
186
|
+
/** When false, adds --wait=false so delete returns immediately. */
|
|
187
|
+
readonly wait?: undefined | boolean;
|
|
166
188
|
readonly context?: undefined | KubectlContext;
|
|
167
189
|
}
|
|
168
190
|
|
|
@@ -304,6 +326,9 @@ export class RealKubectl implements Kubectl {
|
|
|
304
326
|
if (options?.ignoreNotFound) {
|
|
305
327
|
args.push("--ignore-not-found");
|
|
306
328
|
}
|
|
329
|
+
if (options?.wait === false) {
|
|
330
|
+
args.push("--wait=false");
|
|
331
|
+
}
|
|
307
332
|
return await this.runKubectl({
|
|
308
333
|
args,
|
|
309
334
|
stdoutLanguage: "text",
|
|
@@ -312,6 +337,21 @@ export class RealKubectl implements Kubectl {
|
|
|
312
337
|
});
|
|
313
338
|
}
|
|
314
339
|
|
|
340
|
+
async getSecretData(
|
|
341
|
+
name: string,
|
|
342
|
+
key: string,
|
|
343
|
+
context?: KubectlContext
|
|
344
|
+
): Promise<string> {
|
|
345
|
+
const stdout = await this.runKubectl({
|
|
346
|
+
args: ["get", "secret", name, "-o", `jsonpath={.data.${key}}`],
|
|
347
|
+
stdoutLanguage: "text",
|
|
348
|
+
stderrLanguage: "text",
|
|
349
|
+
overrideContext: context,
|
|
350
|
+
redactStdout: true,
|
|
351
|
+
});
|
|
352
|
+
return Buffer.from(stdout, "base64").toString("utf-8");
|
|
353
|
+
}
|
|
354
|
+
|
|
315
355
|
async label(
|
|
316
356
|
resource: string,
|
|
317
357
|
name: string,
|
|
@@ -372,7 +412,7 @@ export class RealKubectl implements Kubectl {
|
|
|
372
412
|
};
|
|
373
413
|
this.recorder.record("CommandResult", {
|
|
374
414
|
exitCode,
|
|
375
|
-
stdout,
|
|
415
|
+
stdout: params.redactStdout ? "<redacted>" : stdout,
|
|
376
416
|
stderr,
|
|
377
417
|
stdoutLanguage: params.stdoutLanguage,
|
|
378
418
|
stderrLanguage: params.stderrLanguage,
|
|
@@ -401,6 +441,7 @@ interface ExecParams {
|
|
|
401
441
|
readonly stdoutLanguage?: undefined | string;
|
|
402
442
|
readonly stderrLanguage?: undefined | string;
|
|
403
443
|
readonly overrideContext?: undefined | KubectlContext;
|
|
444
|
+
readonly redactStdout?: undefined | boolean;
|
|
404
445
|
}
|
|
405
446
|
|
|
406
447
|
function stringifyPatch(patch: KubectlPatch): string {
|
package/ts/scenario/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type { Recorder } from "../recording";
|
|
|
30
30
|
import type { Reporter } from "../reporter/interface";
|
|
31
31
|
import { retryUntil } from "../retry";
|
|
32
32
|
import type { Reverting } from "../reverting";
|
|
33
|
+
import { resolveCluster } from "./use-cluster";
|
|
33
34
|
|
|
34
35
|
export interface InternalScenario extends Scenario {
|
|
35
36
|
cleanup(options?: { skip?: boolean }): Promise<void>;
|
|
@@ -74,7 +75,7 @@ export function createScenario(deps: CreateScenarioOptions): InternalScenario {
|
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
interface CreateScenarioOptions {
|
|
78
|
+
export interface CreateScenarioOptions {
|
|
78
79
|
readonly name: string;
|
|
79
80
|
readonly recorder: Recorder;
|
|
80
81
|
readonly kubectl: Kubectl;
|
|
@@ -82,7 +83,7 @@ interface CreateScenarioOptions {
|
|
|
82
83
|
readonly reporter: Reporter;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
const createMutateFn =
|
|
86
|
+
export const createMutateFn =
|
|
86
87
|
<
|
|
87
88
|
const Action extends MutateDef<Input, Output>,
|
|
88
89
|
Input = Action extends MutateDef<infer I, infer _> ? I : never,
|
|
@@ -134,7 +135,7 @@ const createMutateFn =
|
|
|
134
135
|
}
|
|
135
136
|
};
|
|
136
137
|
|
|
137
|
-
const createOneWayMutateFn =
|
|
138
|
+
export const createOneWayMutateFn =
|
|
138
139
|
<
|
|
139
140
|
const Action extends OneWayMutateDef<Input, Output>,
|
|
140
141
|
Input = Action extends OneWayMutateDef<infer I, infer _> ? I : never,
|
|
@@ -165,7 +166,7 @@ const createOneWayMutateFn =
|
|
|
165
166
|
}
|
|
166
167
|
};
|
|
167
168
|
|
|
168
|
-
const createQueryFn =
|
|
169
|
+
export const createQueryFn =
|
|
169
170
|
<
|
|
170
171
|
const Action extends QueryDef<Input, Output>,
|
|
171
172
|
Input = Action extends QueryDef<infer I, infer _> ? I : never,
|
|
@@ -196,7 +197,7 @@ const createQueryFn =
|
|
|
196
197
|
}
|
|
197
198
|
};
|
|
198
199
|
|
|
199
|
-
const createNewNamespaceFn =
|
|
200
|
+
export const createNewNamespaceFn =
|
|
200
201
|
(scenarioDeps: CreateScenarioOptions) =>
|
|
201
202
|
async (
|
|
202
203
|
name?: CreateNamespaceInput,
|
|
@@ -228,27 +229,6 @@ const createNewNamespaceFn =
|
|
|
228
229
|
|
|
229
230
|
const createUseClusterFn =
|
|
230
231
|
(scenarioDeps: CreateScenarioOptions) =>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const { kubectl } = scenarioDeps;
|
|
234
|
-
const clusterKubectl = kubectl.extends({
|
|
235
|
-
context: cluster.context,
|
|
236
|
-
kubeconfig: cluster.kubeconfig,
|
|
237
|
-
});
|
|
238
|
-
const clusterDeps = { ...scenarioDeps, kubectl: clusterKubectl };
|
|
239
|
-
return {
|
|
240
|
-
apply: createMutateFn(clusterDeps, apply),
|
|
241
|
-
create: createMutateFn(clusterDeps, create),
|
|
242
|
-
assertApplyError: createMutateFn(clusterDeps, assertApplyError),
|
|
243
|
-
assertCreateError: createMutateFn(clusterDeps, assertCreateError),
|
|
244
|
-
applyStatus: createOneWayMutateFn(clusterDeps, applyStatus),
|
|
245
|
-
delete: createOneWayMutateFn(clusterDeps, deleteResource),
|
|
246
|
-
label: createOneWayMutateFn(clusterDeps, label),
|
|
247
|
-
get: createQueryFn(clusterDeps, get),
|
|
248
|
-
assert: createQueryFn(clusterDeps, assert),
|
|
249
|
-
assertAbsence: createQueryFn(clusterDeps, assertAbsence),
|
|
250
|
-
assertList: createQueryFn(clusterDeps, assertList),
|
|
251
|
-
assertOne: createQueryFn(clusterDeps, assertOne),
|
|
252
|
-
newNamespace: createNewNamespaceFn(clusterDeps),
|
|
253
|
-
};
|
|
232
|
+
(cluster: ClusterReference, options?: ActionOptions): Promise<Cluster> => {
|
|
233
|
+
return resolveCluster(scenarioDeps, cluster, options);
|
|
254
234
|
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { mkdir, unlink } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { apply } from "../actions/apply";
|
|
4
|
+
import { applyStatus } from "../actions/apply-status";
|
|
5
|
+
import { assert } from "../actions/assert";
|
|
6
|
+
import { assertAbsence } from "../actions/assert-absence";
|
|
7
|
+
import { assertApplyError } from "../actions/assert-apply-error";
|
|
8
|
+
import { assertCreateError } from "../actions/assert-create-error";
|
|
9
|
+
import { assertList } from "../actions/assert-list";
|
|
10
|
+
import { assertOne } from "../actions/assert-one";
|
|
11
|
+
import { create } from "../actions/create";
|
|
12
|
+
import { deleteResource } from "../actions/delete";
|
|
13
|
+
import { get } from "../actions/get";
|
|
14
|
+
import { label } from "../actions/label";
|
|
15
|
+
import type {
|
|
16
|
+
ActionOptions,
|
|
17
|
+
Cluster,
|
|
18
|
+
ClusterReference,
|
|
19
|
+
ClusterResourceReference,
|
|
20
|
+
} from "../apis";
|
|
21
|
+
import { retryUntil } from "../retry";
|
|
22
|
+
import { parseYaml } from "../yaml";
|
|
23
|
+
import {
|
|
24
|
+
type CreateScenarioOptions,
|
|
25
|
+
createMutateFn,
|
|
26
|
+
createNewNamespaceFn,
|
|
27
|
+
createOneWayMutateFn,
|
|
28
|
+
createQueryFn,
|
|
29
|
+
} from "./index";
|
|
30
|
+
|
|
31
|
+
function isClusterResourceReference(
|
|
32
|
+
ref: ClusterReference
|
|
33
|
+
): ref is ClusterResourceReference {
|
|
34
|
+
return (
|
|
35
|
+
"apiVersion" in ref &&
|
|
36
|
+
"kind" in ref &&
|
|
37
|
+
(ref as unknown as Record<string, unknown>)["kind"] === "Cluster"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Builds a {@link Cluster} object with all action methods wired to the given deps.
|
|
43
|
+
*/
|
|
44
|
+
export function buildClusterSurface(deps: CreateScenarioOptions): Cluster {
|
|
45
|
+
return {
|
|
46
|
+
apply: createMutateFn(deps, apply),
|
|
47
|
+
create: createMutateFn(deps, create),
|
|
48
|
+
assertApplyError: createMutateFn(deps, assertApplyError),
|
|
49
|
+
assertCreateError: createMutateFn(deps, assertCreateError),
|
|
50
|
+
applyStatus: createOneWayMutateFn(deps, applyStatus),
|
|
51
|
+
delete: createOneWayMutateFn(deps, deleteResource),
|
|
52
|
+
label: createOneWayMutateFn(deps, label),
|
|
53
|
+
get: createQueryFn(deps, get),
|
|
54
|
+
assert: createQueryFn(deps, assert),
|
|
55
|
+
assertAbsence: createQueryFn(deps, assertAbsence),
|
|
56
|
+
assertList: createQueryFn(deps, assertList),
|
|
57
|
+
assertOne: createQueryFn(deps, assertOne),
|
|
58
|
+
newNamespace: createNewNamespaceFn(deps),
|
|
59
|
+
useCluster: (
|
|
60
|
+
cluster: ClusterReference,
|
|
61
|
+
options?: ActionOptions
|
|
62
|
+
): Promise<Cluster> => {
|
|
63
|
+
return resolveCluster(deps, cluster, options);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolves a {@link ClusterReference} to a fully-wired {@link Cluster} surface.
|
|
70
|
+
*
|
|
71
|
+
* - For {@link StaticClusterReference}: extends kubectl with context/kubeconfig.
|
|
72
|
+
* - For {@link ClusterResourceReference}: polls CAPI until the cluster is ready,
|
|
73
|
+
* fetches the kubeconfig secret, writes a temp file, and returns a Cluster
|
|
74
|
+
* bound to that kubeconfig.
|
|
75
|
+
*/
|
|
76
|
+
export async function resolveCluster(
|
|
77
|
+
deps: CreateScenarioOptions,
|
|
78
|
+
cluster: ClusterReference,
|
|
79
|
+
options?: ActionOptions
|
|
80
|
+
): Promise<Cluster> {
|
|
81
|
+
if (!isClusterResourceReference(cluster)) {
|
|
82
|
+
// Static cluster reference — same as the original implementation.
|
|
83
|
+
const clusterKubectl = deps.kubectl.extends({
|
|
84
|
+
context: cluster.context,
|
|
85
|
+
kubeconfig: cluster.kubeconfig,
|
|
86
|
+
});
|
|
87
|
+
return buildClusterSurface({ ...deps, kubectl: clusterKubectl });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// CAPI cluster resource reference
|
|
91
|
+
const { apiVersion, name, namespace } = cluster;
|
|
92
|
+
const { recorder, kubectl, reverting } = deps;
|
|
93
|
+
|
|
94
|
+
const resourceType = `Cluster.${apiVersion.split("/")[1]}.${apiVersion.split("/")[0]}`;
|
|
95
|
+
const description = `useCluster(${apiVersion} Cluster "${name}" in "${namespace}")`;
|
|
96
|
+
|
|
97
|
+
recorder.record("ActionStart", { description });
|
|
98
|
+
|
|
99
|
+
let actionErr: undefined | Error;
|
|
100
|
+
try {
|
|
101
|
+
// Poll until the CAPI Cluster is ready.
|
|
102
|
+
await retryUntil(
|
|
103
|
+
async () => {
|
|
104
|
+
const yaml = await kubectl.get(resourceType, name, { namespace });
|
|
105
|
+
const resource = parseYaml(yaml) as Record<string, unknown>;
|
|
106
|
+
const status = resource["status"] as
|
|
107
|
+
| Record<string, unknown>
|
|
108
|
+
| undefined;
|
|
109
|
+
if (!status) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`Cluster "${name}" in "${namespace}" has no status yet`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const version = apiVersion.split("/")[1]; // "v1beta1" or "v1beta2"
|
|
116
|
+
let conditions: Array<Record<string, unknown>> | undefined;
|
|
117
|
+
let targetType: string;
|
|
118
|
+
|
|
119
|
+
if (version === "v1beta1") {
|
|
120
|
+
conditions = status["conditions"] as
|
|
121
|
+
| Array<Record<string, unknown>>
|
|
122
|
+
| undefined;
|
|
123
|
+
targetType = "Ready";
|
|
124
|
+
} else if (version === "v1beta2") {
|
|
125
|
+
const v1beta2 = status["v1beta2"] as
|
|
126
|
+
| Record<string, unknown>
|
|
127
|
+
| undefined;
|
|
128
|
+
conditions = v1beta2?.["conditions"] as
|
|
129
|
+
| Array<Record<string, unknown>>
|
|
130
|
+
| undefined;
|
|
131
|
+
targetType = "Available";
|
|
132
|
+
} else {
|
|
133
|
+
throw new Error(`Unsupported CAPI API version: ${apiVersion}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!conditions) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Cluster "${name}" in "${namespace}" has no conditions`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const condition = conditions.find((c) => c["type"] === targetType);
|
|
143
|
+
if (!condition || condition["status"] !== "True") {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Cluster "${name}" in "${namespace}" is not ready (${targetType} != True)`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{ ...options, recorder }
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Fetch kubeconfig from the cluster's secret.
|
|
153
|
+
const kubeconfigData = await retryUntil(
|
|
154
|
+
() => kubectl.getSecretData(`${name}-kubeconfig`, "value", { namespace }),
|
|
155
|
+
{ timeout: "30s", interval: "1s", recorder }
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Write kubeconfig to a temp file.
|
|
159
|
+
const dir = `${tmpdir()}/kest/kubeconfigs`;
|
|
160
|
+
await mkdir(dir, { recursive: true });
|
|
161
|
+
const tempPath = `${dir}/capi-${namespace}-${name}-${crypto.randomUUID()}.yaml`;
|
|
162
|
+
await Bun.write(tempPath, kubeconfigData);
|
|
163
|
+
|
|
164
|
+
// Register cleanup to delete the temp file.
|
|
165
|
+
reverting.add(async () => {
|
|
166
|
+
await unlink(tempPath);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Clear the parent's context so kubectl uses the current-context from the
|
|
170
|
+
// child kubeconfig file rather than the management cluster's context name.
|
|
171
|
+
const clusterKubectl = kubectl.extends({
|
|
172
|
+
kubeconfig: tempPath,
|
|
173
|
+
context: undefined,
|
|
174
|
+
});
|
|
175
|
+
return buildClusterSurface({ ...deps, kubectl: clusterKubectl });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
actionErr = error as Error;
|
|
178
|
+
throw error;
|
|
179
|
+
} finally {
|
|
180
|
+
recorder.record("ActionEnd", {
|
|
181
|
+
ok: actionErr === undefined,
|
|
182
|
+
error: actionErr,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
bun test v1.3.8 (b64edcb4)
|
|
2
|
-
|
|
3
|
-
example/example.test.ts:
|
|
4
|
-
# Example: applies ConfigMap using YAML, file import, and object literal
|
|
5
|
-
|
|
6
|
-
## Scenario Overview
|
|
7
|
-
|
|
8
|
-
| # | Action | Status | Duration |
|
|
9
|
-
|---|--------|--------|----------|
|
|
10
|
-
| 1 | Create `Namespace` with auto-generated name | ✅ | 186ms |
|
|
11
|
-
| 2 | Apply `ConfigMap` "my-config-1" | ✅ | 55ms |
|
|
12
|
-
| 3 | Apply a resource | ✅ | 117ms |
|
|
13
|
-
| 4 | Apply `ConfigMap` "my-config-3" | ✅ | 135ms |
|
|
14
|
-
| 5 | Assert `ConfigMap` "my-config-1" | ✅ | 63ms |
|
|
15
|
-
|
|
16
|
-
## Scenario Details
|
|
17
|
-
|
|
18
|
-
### Given: a new namespace exists
|
|
19
|
-
|
|
20
|
-
**✅ Create `Namespace` with auto-generated name**
|
|
21
|
-
|
|
22
|
-
```shell
|
|
23
|
-
kubectl create -f - <<EOF
|
|
24
|
-
apiVersion: v1
|
|
25
|
-
kind: Namespace
|
|
26
|
-
metadata:
|
|
27
|
-
name: kest-x173q
|
|
28
|
-
EOF
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
```text title="stdout"
|
|
32
|
-
namespace/kest-x173q created
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### When: I apply ConfigMaps using different formats
|
|
36
|
-
|
|
37
|
-
**✅ Apply `ConfigMap` "my-config-1"**
|
|
38
|
-
|
|
39
|
-
```shell
|
|
40
|
-
kubectl apply -f - -n kest-x173q <<EOF
|
|
41
|
-
apiVersion: v1
|
|
42
|
-
kind: ConfigMap
|
|
43
|
-
metadata:
|
|
44
|
-
name: my-config-1
|
|
45
|
-
data:
|
|
46
|
-
mode: demo-1
|
|
47
|
-
EOF
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
```text title="stdout"
|
|
51
|
-
configmap/my-config-1 created
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**✅ Apply a resource**
|
|
55
|
-
|
|
56
|
-
```shell
|
|
57
|
-
kubectl apply -f - -n kest-x173q <<EOF
|
|
58
|
-
apiVersion: v1
|
|
59
|
-
kind: ConfigMap
|
|
60
|
-
metadata:
|
|
61
|
-
name: my-config-2
|
|
62
|
-
data:
|
|
63
|
-
mode: demo-2
|
|
64
|
-
EOF
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
```text title="stdout"
|
|
68
|
-
configmap/my-config-2 created
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
**✅ Apply `ConfigMap` "my-config-3"**
|
|
72
|
-
|
|
73
|
-
```shell
|
|
74
|
-
kubectl apply -f - -n kest-x173q <<EOF
|
|
75
|
-
apiVersion: v1
|
|
76
|
-
kind: ConfigMap
|
|
77
|
-
metadata:
|
|
78
|
-
name: my-config-3
|
|
79
|
-
data:
|
|
80
|
-
mode: demo-3
|
|
81
|
-
EOF
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
```text title="stdout"
|
|
85
|
-
configmap/my-config-3 created
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Then: the ConfigMap should have the expected data
|
|
89
|
-
|
|
90
|
-
**✅ Assert `ConfigMap` "my-config-1"**
|
|
91
|
-
|
|
92
|
-
```shell
|
|
93
|
-
kubectl get ConfigMap my-config-1 -o yaml -n kest-x173q
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
```yaml title="stdout"
|
|
97
|
-
apiVersion: v1
|
|
98
|
-
data:
|
|
99
|
-
mode: demo-1
|
|
100
|
-
kind: ConfigMap
|
|
101
|
-
metadata:
|
|
102
|
-
annotations:
|
|
103
|
-
kubectl.kubernetes.io/last-applied-configuration: |
|
|
104
|
-
{"apiVersion":"v1","data":{"mode":"demo-1"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"my-config-1","namespace":"kest-x173q"}}
|
|
105
|
-
creationTimestamp: "2026-03-02T02:39:15Z"
|
|
106
|
-
name: my-config-1
|
|
107
|
-
namespace: kest-x173q
|
|
108
|
-
resourceVersion: "585"
|
|
109
|
-
uid: 25339d11-cf6b-44a5-a680-1548d1e80252
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### Cleanup
|
|
113
|
-
|
|
114
|
-
| # | Action | Status | Duration |
|
|
115
|
-
|---|--------|--------|----------|
|
|
116
|
-
| 1 | Apply `ConfigMap` "my-config-3" | ✅ | 75ms |
|
|
117
|
-
| 2 | Apply a resource | ✅ | 41ms |
|
|
118
|
-
| 3 | Apply `ConfigMap` "my-config-1" | ✅ | 36ms |
|
|
119
|
-
| 4 | Create `Namespace` with auto-generated name | ✅ | 5.103s |
|
|
120
|
-
|
|
121
|
-
```shellsession
|
|
122
|
-
$ kubectl delete ConfigMap/my-config-3 --ignore-not-found -n kest-x173q
|
|
123
|
-
configmap "my-config-3" deleted
|
|
124
|
-
|
|
125
|
-
$ kubectl delete ConfigMap/my-config-2 --ignore-not-found -n kest-x173q
|
|
126
|
-
configmap "my-config-2" deleted
|
|
127
|
-
|
|
128
|
-
$ kubectl delete ConfigMap/my-config-1 --ignore-not-found -n kest-x173q
|
|
129
|
-
configmap "my-config-1" deleted
|
|
130
|
-
|
|
131
|
-
$ kubectl delete Namespace/kest-x173q --ignore-not-found
|
|
132
|
-
namespace "kest-x173q" deleted
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
**Total: 5.815s** (Actions: 556ms, Cleanup: 5.255s)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
1 pass
|
|
139
|
-
7 filtered out
|
|
140
|
-
0 fail
|
|
141
|
-
1 expect() calls
|
|
142
|
-
Ran 1 test across 1 file. [6.04s]
|