@appthrust/kest 0.16.0 → 0.17.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 +28 -1
- package/package.json +1 -1
- package/ts/apis/index.ts +65 -2
- package/ts/scenario/index.ts +54 -18
- package/ts/scenario/use-cluster.ts +2 -0
package/README.md
CHANGED
|
@@ -185,6 +185,31 @@ test("resources sync across clusters", async (s) => {
|
|
|
185
185
|
});
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
### Existing Namespaces
|
|
189
|
+
|
|
190
|
+
When testing resources in namespaces that kest did not create (e.g. system namespaces or namespaces installed by Helm charts), use `useNamespace` to obtain the same namespace-scoped DSL without creating or cleaning up the namespace:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
test("istio root cert exists", async (s) => {
|
|
194
|
+
const istio = await s.useNamespace("istio-system");
|
|
195
|
+
await istio.assert({
|
|
196
|
+
apiVersion: "v1",
|
|
197
|
+
kind: "ConfigMap",
|
|
198
|
+
name: "istio-ca-root-cert",
|
|
199
|
+
test() {
|
|
200
|
+
expect(this.data["root-cert.pem"]).toBeDefined();
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`useNamespace` is also available on `Cluster`:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const cluster = await s.useCluster({ context: "kind-kind" });
|
|
210
|
+
const kubeSystem = await cluster.useNamespace("kube-system");
|
|
211
|
+
```
|
|
212
|
+
|
|
188
213
|
#### CAPI Dynamic Clusters
|
|
189
214
|
|
|
190
215
|
Kest can resolve [Cluster API](https://cluster-api.sigs.k8s.io/) (CAPI) provisioned clusters automatically. Pass a `ClusterResourceReference` to `useCluster` and Kest will:
|
|
@@ -579,6 +604,7 @@ The top-level API surface available in every test callback.
|
|
|
579
604
|
| `assertList(resource, options?)` | Fetch a list of resources and run assertions |
|
|
580
605
|
| `assertOne(resource, options?)` | Assert exactly one resource matches, then run assertions |
|
|
581
606
|
| `newNamespace(name?, options?)` | Create an ephemeral namespace (supports `{ generateName }`) |
|
|
607
|
+
| `useNamespace(name, options?)` | Obtain a `Namespace` for an existing namespace (no cleanup) |
|
|
582
608
|
| `generateName(prefix)` | Generate a random-suffix name (statistical uniqueness) |
|
|
583
609
|
| `exec(input, options?)` | Execute shell commands with optional revert |
|
|
584
610
|
| `useCluster(ref, options?)` | Create a cluster-bound API surface |
|
|
@@ -586,7 +612,7 @@ The top-level API surface available in every test callback.
|
|
|
586
612
|
|
|
587
613
|
### Namespace / Cluster
|
|
588
614
|
|
|
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`.
|
|
615
|
+
Returned by `newNamespace()`, `useNamespace()`, 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`, `useNamespace`, and `useCluster`.
|
|
590
616
|
|
|
591
617
|
`Namespace` also exposes a `name` property:
|
|
592
618
|
|
|
@@ -1039,6 +1065,7 @@ await ns.apply({
|
|
|
1039
1065
|
| Situation | Approach |
|
|
1040
1066
|
| ------------------------------------------------------ | ----------------------------------------- |
|
|
1041
1067
|
| Namespaced resource inside `newNamespace()` | Use a fixed name (default) |
|
|
1068
|
+
| Resource in pre-existing namespace (`useNamespace()`) | Use a fixed name (default) |
|
|
1042
1069
|
| Same-kind resources created multiple times in one test | `s.generateName` or numbered fixed names |
|
|
1043
1070
|
| Cluster-scoped resource (e.g. `ClusterRole`, `CRD`) | `s.generateName` (no namespace isolation) |
|
|
1044
1071
|
| Fixed name causes unintended upsert / side effects | `s.generateName` |
|
package/package.json
CHANGED
package/ts/apis/index.ts
CHANGED
|
@@ -550,6 +550,38 @@ export interface Scenario {
|
|
|
550
550
|
options?: undefined | ActionOptions
|
|
551
551
|
): Promise<Cluster>;
|
|
552
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Obtains a {@link Namespace} interface for an existing namespace.
|
|
555
|
+
*
|
|
556
|
+
* Unlike {@link Scenario.newNamespace}, this does **not** create the namespace
|
|
557
|
+
* and does **not** register a cleanup handler. It verifies the namespace exists
|
|
558
|
+
* (via `kubectl get namespace <name>`) and returns the same namespace-scoped
|
|
559
|
+
* DSL surface.
|
|
560
|
+
*
|
|
561
|
+
* Use this when testing resources in namespaces that kest did not create, such
|
|
562
|
+
* as system namespaces or namespaces provisioned by controllers or Helm charts.
|
|
563
|
+
*
|
|
564
|
+
* @param name - The name of the existing namespace.
|
|
565
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```ts
|
|
569
|
+
* const istio = await s.useNamespace("istio-system");
|
|
570
|
+
* await istio.assert({
|
|
571
|
+
* apiVersion: "v1",
|
|
572
|
+
* kind: "ConfigMap",
|
|
573
|
+
* name: "istio-ca-root-cert",
|
|
574
|
+
* test() {
|
|
575
|
+
* expect(this.data["root-cert.pem"]).toBeDefined();
|
|
576
|
+
* },
|
|
577
|
+
* });
|
|
578
|
+
* ```
|
|
579
|
+
*/
|
|
580
|
+
useNamespace(
|
|
581
|
+
name: string,
|
|
582
|
+
options?: undefined | ActionOptions
|
|
583
|
+
): Promise<Namespace>;
|
|
584
|
+
|
|
553
585
|
// BDD(behavior-driven development) actions
|
|
554
586
|
|
|
555
587
|
/**
|
|
@@ -924,13 +956,44 @@ export interface Cluster {
|
|
|
924
956
|
cluster: ClusterReference,
|
|
925
957
|
options?: undefined | ActionOptions
|
|
926
958
|
): Promise<Cluster>;
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Obtains a {@link Namespace} interface for an existing namespace in this cluster.
|
|
962
|
+
*
|
|
963
|
+
* Unlike {@link Cluster.newNamespace}, this does **not** create the namespace
|
|
964
|
+
* and does **not** register a cleanup handler. It verifies the namespace exists
|
|
965
|
+
* (via `kubectl get namespace <name>`) and returns the same namespace-scoped
|
|
966
|
+
* DSL surface.
|
|
967
|
+
*
|
|
968
|
+
* @param name - The name of the existing namespace.
|
|
969
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
970
|
+
*
|
|
971
|
+
* @example
|
|
972
|
+
* ```ts
|
|
973
|
+
* const cluster = await s.useCluster({ context: "kind-kind" });
|
|
974
|
+
* const kubeSystem = await cluster.useNamespace("kube-system");
|
|
975
|
+
* await kubeSystem.assert({
|
|
976
|
+
* apiVersion: "v1",
|
|
977
|
+
* kind: "ConfigMap",
|
|
978
|
+
* name: "kube-root-ca.crt",
|
|
979
|
+
* test() {
|
|
980
|
+
* expect(this.data["ca.crt"]).toBeDefined();
|
|
981
|
+
* },
|
|
982
|
+
* });
|
|
983
|
+
* ```
|
|
984
|
+
*/
|
|
985
|
+
useNamespace(
|
|
986
|
+
name: string,
|
|
987
|
+
options?: undefined | ActionOptions
|
|
988
|
+
): Promise<Namespace>;
|
|
927
989
|
}
|
|
928
990
|
|
|
929
991
|
/**
|
|
930
992
|
* Namespace-bound API surface.
|
|
931
993
|
*
|
|
932
|
-
* A {@link Namespace} is typically obtained via {@link Scenario.newNamespace}
|
|
933
|
-
* {@link Cluster.newNamespace}
|
|
994
|
+
* A {@link Namespace} is typically obtained via {@link Scenario.newNamespace},
|
|
995
|
+
* {@link Scenario.useNamespace}, {@link Cluster.newNamespace}, or
|
|
996
|
+
* {@link Cluster.useNamespace}.
|
|
934
997
|
*
|
|
935
998
|
* Operations are scoped by setting the kubectl namespace context (equivalent to
|
|
936
999
|
* passing `kubectl -n <namespace>`).
|
package/ts/scenario/index.ts
CHANGED
|
@@ -61,6 +61,7 @@ export function createScenario(deps: CreateScenarioOptions): InternalScenario {
|
|
|
61
61
|
but: bdd.but(deps),
|
|
62
62
|
generateName: (prefix: string) => generateRandomName(prefix),
|
|
63
63
|
newNamespace: createNewNamespaceFn(deps),
|
|
64
|
+
useNamespace: createUseNamespaceFn(deps),
|
|
64
65
|
useCluster: createUseClusterFn(deps),
|
|
65
66
|
async cleanup(options?: { skip?: boolean }) {
|
|
66
67
|
if (options?.skip) {
|
|
@@ -197,6 +198,31 @@ export const createQueryFn =
|
|
|
197
198
|
}
|
|
198
199
|
};
|
|
199
200
|
|
|
201
|
+
export function buildNamespaceSurface(
|
|
202
|
+
scenarioDeps: CreateScenarioOptions,
|
|
203
|
+
namespaceName: string
|
|
204
|
+
): Namespace {
|
|
205
|
+
const namespacedKubectl = scenarioDeps.kubectl.extends({
|
|
206
|
+
namespace: namespaceName,
|
|
207
|
+
});
|
|
208
|
+
const namespacedDeps = { ...scenarioDeps, kubectl: namespacedKubectl };
|
|
209
|
+
return {
|
|
210
|
+
name: namespaceName,
|
|
211
|
+
apply: createMutateFn(namespacedDeps, apply),
|
|
212
|
+
create: createMutateFn(namespacedDeps, create),
|
|
213
|
+
assertApplyError: createMutateFn(namespacedDeps, assertApplyError),
|
|
214
|
+
assertCreateError: createMutateFn(namespacedDeps, assertCreateError),
|
|
215
|
+
applyStatus: createOneWayMutateFn(namespacedDeps, applyStatus),
|
|
216
|
+
delete: createOneWayMutateFn(namespacedDeps, deleteResource),
|
|
217
|
+
label: createOneWayMutateFn(namespacedDeps, label),
|
|
218
|
+
get: createQueryFn(namespacedDeps, get),
|
|
219
|
+
assert: createQueryFn(namespacedDeps, assert),
|
|
220
|
+
assertAbsence: createQueryFn(namespacedDeps, assertAbsence),
|
|
221
|
+
assertList: createQueryFn(namespacedDeps, assertList),
|
|
222
|
+
assertOne: createQueryFn(namespacedDeps, assertOne),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
200
226
|
export const createNewNamespaceFn =
|
|
201
227
|
(scenarioDeps: CreateScenarioOptions) =>
|
|
202
228
|
async (
|
|
@@ -207,24 +233,34 @@ export const createNewNamespaceFn =
|
|
|
207
233
|
name,
|
|
208
234
|
options
|
|
209
235
|
);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
236
|
+
return buildNamespaceSurface(scenarioDeps, namespaceName);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export const createUseNamespaceFn =
|
|
240
|
+
(scenarioDeps: CreateScenarioOptions) =>
|
|
241
|
+
async (
|
|
242
|
+
name: string,
|
|
243
|
+
options?: undefined | ActionOptions
|
|
244
|
+
): Promise<Namespace> => {
|
|
245
|
+
const { kubectl, recorder } = scenarioDeps;
|
|
246
|
+
const description = `useNamespace("${name}")`;
|
|
247
|
+
recorder.record("ActionStart", { description });
|
|
248
|
+
let actionErr: undefined | Error;
|
|
249
|
+
try {
|
|
250
|
+
await retryUntil(() => kubectl.get("Namespace", name), {
|
|
251
|
+
...options,
|
|
252
|
+
recorder,
|
|
253
|
+
});
|
|
254
|
+
return buildNamespaceSurface(scenarioDeps, name);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
actionErr = error as Error;
|
|
257
|
+
throw error;
|
|
258
|
+
} finally {
|
|
259
|
+
recorder.record("ActionEnd", {
|
|
260
|
+
ok: actionErr === undefined,
|
|
261
|
+
error: actionErr,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
228
264
|
};
|
|
229
265
|
|
|
230
266
|
const createUseClusterFn =
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
createNewNamespaceFn,
|
|
27
27
|
createOneWayMutateFn,
|
|
28
28
|
createQueryFn,
|
|
29
|
+
createUseNamespaceFn,
|
|
29
30
|
} from "./index";
|
|
30
31
|
|
|
31
32
|
function isClusterResourceReference(
|
|
@@ -56,6 +57,7 @@ export function buildClusterSurface(deps: CreateScenarioOptions): Cluster {
|
|
|
56
57
|
assertList: createQueryFn(deps, assertList),
|
|
57
58
|
assertOne: createQueryFn(deps, assertOne),
|
|
58
59
|
newNamespace: createNewNamespaceFn(deps),
|
|
60
|
+
useNamespace: createUseNamespaceFn(deps),
|
|
59
61
|
useCluster: (
|
|
60
62
|
cluster: ClusterReference,
|
|
61
63
|
options?: ActionOptions
|