@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appthrust/kest",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Kubernetes E2E testing framework designed for humans and AI alike",
5
5
  "type": "module",
6
6
  "module": "ts/index.ts",
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} or
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>`).
@@ -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
- const { kubectl } = scenarioDeps;
211
- const namespacedKubectl = kubectl.extends({ namespace: namespaceName });
212
- const namespacedDeps = { ...scenarioDeps, kubectl: namespacedKubectl };
213
- return {
214
- name: namespaceName,
215
- apply: createMutateFn(namespacedDeps, apply),
216
- create: createMutateFn(namespacedDeps, create),
217
- assertApplyError: createMutateFn(namespacedDeps, assertApplyError),
218
- assertCreateError: createMutateFn(namespacedDeps, assertCreateError),
219
- applyStatus: createOneWayMutateFn(namespacedDeps, applyStatus),
220
- delete: createOneWayMutateFn(namespacedDeps, deleteResource),
221
- label: createOneWayMutateFn(namespacedDeps, label),
222
- get: createQueryFn(namespacedDeps, get),
223
- assert: createQueryFn(namespacedDeps, assert),
224
- assertAbsence: createQueryFn(namespacedDeps, assertAbsence),
225
- assertList: createQueryFn(namespacedDeps, assertList),
226
- assertOne: createQueryFn(namespacedDeps, assertOne),
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