@appthrust/kest 0.2.0 → 0.3.1

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/ts/apis/index.ts CHANGED
@@ -54,6 +54,39 @@ export interface Scenario {
54
54
  options?: undefined | ActionOptions
55
55
  ): Promise<void>;
56
56
 
57
+ /**
58
+ * Creates a Kubernetes resource with `kubectl create`.
59
+ *
60
+ * The manifest is validated and then created. When the action succeeds, Kest
61
+ * registers a cleanup handler that deletes the resource using
62
+ * `kubectl delete <kind>/<metadata.name>` during scenario cleanup.
63
+ *
64
+ * Unlike {@link Scenario.apply}, this action uses `kubectl create` which
65
+ * fails if the resource already exists. Use this when you need to ensure the
66
+ * resource is freshly created (e.g. for resources that use `generateName` or
67
+ * when you want to guarantee no prior state).
68
+ *
69
+ * This action is retried when it throws.
70
+ *
71
+ * @template T - The expected Kubernetes resource shape.
72
+ * @param manifest - YAML string, resource object, or imported YAML module.
73
+ * @param options - Retry options such as timeout and polling interval.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * await s.create({
78
+ * apiVersion: "v1",
79
+ * kind: "ConfigMap",
80
+ * metadata: { name: "my-config" },
81
+ * data: { mode: "demo" },
82
+ * });
83
+ * ```
84
+ */
85
+ create<T extends K8sResource>(
86
+ manifest: ApplyingManifest<T>,
87
+ options?: undefined | ActionOptions
88
+ ): Promise<void>;
89
+
57
90
  /**
58
91
  * Applies the `status` subresource using server-side apply.
59
92
  *
@@ -114,6 +147,38 @@ export interface Scenario {
114
147
  options?: undefined | ActionOptions
115
148
  ): Promise<void>;
116
149
 
150
+ /**
151
+ * Adds, updates, or removes labels on a Kubernetes resource using
152
+ * `kubectl label`.
153
+ *
154
+ * Set a label value to a string to add/update it, or to `null` to remove it.
155
+ *
156
+ * This action is retried when it throws.
157
+ *
158
+ * Note: this is a one-way mutation and does not register a cleanup handler.
159
+ *
160
+ * @template T - The expected Kubernetes resource shape.
161
+ * @param input - Resource reference and label changes.
162
+ * @param options - Retry options such as timeout and polling interval.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * await s.label({
167
+ * apiVersion: "v1",
168
+ * kind: "ConfigMap",
169
+ * name: "my-config",
170
+ * labels: {
171
+ * env: "production", // add or update
172
+ * deprecated: null, // remove
173
+ * },
174
+ * });
175
+ * ```
176
+ */
177
+ label<T extends K8sResource>(
178
+ input: LabelInput<T>,
179
+ options?: undefined | ActionOptions
180
+ ): Promise<void>;
181
+
117
182
  /**
118
183
  * Fetches a Kubernetes resource and returns it as a typed object.
119
184
  *
@@ -176,6 +241,32 @@ export interface Scenario {
176
241
  options?: undefined | ActionOptions
177
242
  ): Promise<T>;
178
243
 
244
+ /**
245
+ * Asserts that a Kubernetes resource does not exist.
246
+ *
247
+ * Internally, this uses `kubectl get` and expects it to fail with a
248
+ * "not found" error. If the resource exists, the assertion fails.
249
+ *
250
+ * This action is retried until the resource is absent or a timeout expires.
251
+ *
252
+ * @template T - The expected Kubernetes resource shape.
253
+ * @param resource - Group/version/kind and name of the resource.
254
+ * @param options - Retry options such as timeout and polling interval.
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * await s.assertAbsence({
259
+ * apiVersion: "v1",
260
+ * kind: "ConfigMap",
261
+ * name: "deleted-config",
262
+ * });
263
+ * ```
264
+ */
265
+ assertAbsence<T extends K8sResource>(
266
+ resource: K8sResourceReference<T>,
267
+ options?: undefined | ActionOptions
268
+ ): Promise<void>;
269
+
179
270
  /**
180
271
  * Lists Kubernetes resources of a given type and runs a test function.
181
272
  *
@@ -212,7 +303,11 @@ export interface Scenario {
212
303
  * `kest-abc12`). The namespace creation is a mutating action that registers a
213
304
  * cleanup handler; the namespace is deleted during scenario cleanup.
214
305
  *
215
- * @param name - Optional namespace name to create.
306
+ * You can also pass `{ generateName: "prefix-" }` to generate a name with a
307
+ * custom prefix (e.g. `"prefix-d7kpn"`).
308
+ *
309
+ * @param name - Optional namespace name, or `{ generateName }` for prefixed
310
+ * generation.
216
311
  * @param options - Retry options such as timeout and polling interval.
217
312
  *
218
313
  * @example
@@ -225,9 +320,15 @@ export interface Scenario {
225
320
  * data: { mode: "namespaced" },
226
321
  * });
227
322
  * ```
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * // Generate a namespace with a custom prefix (e.g. "foo-d7kpn")
327
+ * const ns = await s.newNamespace({ generateName: "foo-" });
328
+ * ```
228
329
  */
229
330
  newNamespace(
230
- name?: undefined | string,
331
+ name?: undefined | string | { readonly generateName: string },
231
332
  options?: undefined | ActionOptions
232
333
  ): Promise<Namespace>;
233
334
 
@@ -374,6 +475,30 @@ export interface Cluster {
374
475
  options?: undefined | ActionOptions
375
476
  ): Promise<void>;
376
477
 
478
+ /**
479
+ * Creates a Kubernetes resource with `kubectl create` and registers cleanup.
480
+ *
481
+ * Unlike {@link Cluster.apply}, this uses `kubectl create` which fails if the
482
+ * resource already exists.
483
+ *
484
+ * @template T - The expected Kubernetes resource shape.
485
+ * @param manifest - YAML string, resource object, or imported YAML module.
486
+ * @param options - Retry options such as timeout and polling interval.
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * await cluster.create({
491
+ * apiVersion: "v1",
492
+ * kind: "Namespace",
493
+ * metadata: { name: "my-team" },
494
+ * });
495
+ * ```
496
+ */
497
+ create<T extends K8sResource>(
498
+ manifest: ApplyingManifest<T>,
499
+ options?: undefined | ActionOptions
500
+ ): Promise<void>;
501
+
377
502
  /**
378
503
  * Applies the `status` subresource using server-side apply.
379
504
  *
@@ -412,6 +537,38 @@ export interface Cluster {
412
537
  options?: undefined | ActionOptions
413
538
  ): Promise<void>;
414
539
 
540
+ /**
541
+ * Adds, updates, or removes labels on a Kubernetes resource in this cluster
542
+ * using `kubectl label`.
543
+ *
544
+ * Set a label value to a string to add/update it, or to `null` to remove it.
545
+ *
546
+ * This action is retried when it throws.
547
+ *
548
+ * Note: this is a one-way mutation and does not register a cleanup handler.
549
+ *
550
+ * @template T - The expected Kubernetes resource shape.
551
+ * @param input - Resource reference and label changes.
552
+ * @param options - Retry options such as timeout and polling interval.
553
+ *
554
+ * @example
555
+ * ```ts
556
+ * await cluster.label({
557
+ * apiVersion: "v1",
558
+ * kind: "Namespace",
559
+ * name: "my-team",
560
+ * labels: {
561
+ * team: "backend",
562
+ * deprecated: null,
563
+ * },
564
+ * });
565
+ * ```
566
+ */
567
+ label<T extends K8sResource>(
568
+ input: LabelInput<T>,
569
+ options?: undefined | ActionOptions
570
+ ): Promise<void>;
571
+
415
572
  /**
416
573
  * Fetches a Kubernetes resource by GVK and name.
417
574
  *
@@ -457,6 +614,27 @@ export interface Cluster {
457
614
  options?: undefined | ActionOptions
458
615
  ): Promise<T>;
459
616
 
617
+ /**
618
+ * Asserts that a Kubernetes resource does not exist in this cluster.
619
+ *
620
+ * @template T - The expected Kubernetes resource shape.
621
+ * @param resource - Group/version/kind and name of the resource.
622
+ * @param options - Retry options such as timeout and polling interval.
623
+ *
624
+ * @example
625
+ * ```ts
626
+ * await cluster.assertAbsence({
627
+ * apiVersion: "v1",
628
+ * kind: "Namespace",
629
+ * name: "deleted-ns",
630
+ * });
631
+ * ```
632
+ */
633
+ assertAbsence<T extends K8sResource>(
634
+ resource: K8sResourceReference<T>,
635
+ options?: undefined | ActionOptions
636
+ ): Promise<void>;
637
+
460
638
  /**
461
639
  * Lists Kubernetes resources of a given type and runs a test function.
462
640
  *
@@ -483,7 +661,8 @@ export interface Cluster {
483
661
  /**
484
662
  * Creates a new namespace in this cluster and returns a namespaced API.
485
663
  *
486
- * @param name - Optional namespace name to create.
664
+ * @param name - Optional namespace name, or `{ generateName }` for prefixed
665
+ * generation.
487
666
  * @param options - Retry options such as timeout and polling interval.
488
667
  *
489
668
  * @example
@@ -496,9 +675,15 @@ export interface Cluster {
496
675
  * data: { mode: "from-cluster" },
497
676
  * });
498
677
  * ```
678
+ *
679
+ * @example
680
+ * ```ts
681
+ * // Generate a namespace with a custom prefix
682
+ * const ns = await cluster.newNamespace({ generateName: "foo-" });
683
+ * ```
499
684
  */
500
685
  newNamespace(
501
- name?: undefined | string,
686
+ name?: undefined | string | { readonly generateName: string },
502
687
  options?: undefined | ActionOptions
503
688
  ): Promise<Namespace>;
504
689
  }
@@ -522,6 +707,11 @@ export interface Cluster {
522
707
  * causes `kubectl` to fail.
523
708
  */
524
709
  export interface Namespace {
710
+ /**
711
+ * The name of this namespace (e.g. `"kest-abc12"`).
712
+ */
713
+ readonly name: string;
714
+
525
715
  /**
526
716
  * Applies a Kubernetes manifest in this namespace and registers cleanup.
527
717
  *
@@ -550,6 +740,37 @@ export interface Namespace {
550
740
  options?: undefined | ActionOptions
551
741
  ): Promise<void>;
552
742
 
743
+ /**
744
+ * Creates a Kubernetes resource in this namespace with `kubectl create` and
745
+ * registers cleanup.
746
+ *
747
+ * The target namespace is controlled by this {@link Namespace} instance.
748
+ * Prefer omitting `manifest.metadata.namespace`; if it is set, it must match
749
+ * this namespace (otherwise `kubectl` fails).
750
+ *
751
+ * Unlike {@link Namespace.apply}, this uses `kubectl create` which fails if
752
+ * the resource already exists.
753
+ *
754
+ * @template T - The expected Kubernetes resource shape.
755
+ * @param manifest - YAML string, resource object, or imported YAML module.
756
+ * @param options - Retry options such as timeout and polling interval.
757
+ *
758
+ * @example
759
+ * ```ts
760
+ * const ns = await s.newNamespace("my-ns");
761
+ * await ns.create({
762
+ * apiVersion: "v1",
763
+ * kind: "ConfigMap",
764
+ * metadata: { name: "my-config" },
765
+ * data: { mode: "demo" },
766
+ * });
767
+ * ```
768
+ */
769
+ create<T extends K8sResource>(
770
+ manifest: ApplyingManifest<T>,
771
+ options?: undefined | ActionOptions
772
+ ): Promise<void>;
773
+
553
774
  /**
554
775
  * Applies the `status` subresource in this namespace using server-side apply.
555
776
  *
@@ -592,6 +813,40 @@ export interface Namespace {
592
813
  options?: undefined | ActionOptions
593
814
  ): Promise<void>;
594
815
 
816
+ /**
817
+ * Adds, updates, or removes labels on a namespaced Kubernetes resource using
818
+ * `kubectl label`.
819
+ *
820
+ * The target namespace is controlled by this {@link Namespace} instance.
821
+ *
822
+ * Set a label value to a string to add/update it, or to `null` to remove it.
823
+ *
824
+ * This action is retried when it throws.
825
+ *
826
+ * Note: this is a one-way mutation and does not register a cleanup handler.
827
+ *
828
+ * @template T - The expected Kubernetes resource shape.
829
+ * @param input - Resource reference and label changes.
830
+ * @param options - Retry options such as timeout and polling interval.
831
+ *
832
+ * @example
833
+ * ```ts
834
+ * await ns.label({
835
+ * apiVersion: "v1",
836
+ * kind: "ConfigMap",
837
+ * name: "my-config",
838
+ * labels: {
839
+ * env: "production",
840
+ * deprecated: null,
841
+ * },
842
+ * });
843
+ * ```
844
+ */
845
+ label<T extends K8sResource>(
846
+ input: LabelInput<T>,
847
+ options?: undefined | ActionOptions
848
+ ): Promise<void>;
849
+
595
850
  /**
596
851
  * Fetches a namespaced Kubernetes resource by GVK and name.
597
852
  *
@@ -637,6 +892,27 @@ export interface Namespace {
637
892
  options?: undefined | ActionOptions
638
893
  ): Promise<T>;
639
894
 
895
+ /**
896
+ * Asserts that a namespaced Kubernetes resource does not exist.
897
+ *
898
+ * @template T - The expected Kubernetes resource shape.
899
+ * @param resource - Group/version/kind and name of the resource.
900
+ * @param options - Retry options such as timeout and polling interval.
901
+ *
902
+ * @example
903
+ * ```ts
904
+ * await ns.assertAbsence({
905
+ * apiVersion: "v1",
906
+ * kind: "ConfigMap",
907
+ * name: "deleted-config",
908
+ * });
909
+ * ```
910
+ */
911
+ assertAbsence<T extends K8sResource>(
912
+ resource: K8sResourceReference<T>,
913
+ options?: undefined | ActionOptions
914
+ ): Promise<void>;
915
+
640
916
  /**
641
917
  * Lists namespaced Kubernetes resources of a given type and runs a test.
642
918
  *
@@ -737,6 +1013,57 @@ export interface K8sResourceReference<T extends K8sResource = K8sResource> {
737
1013
  readonly name: string;
738
1014
  }
739
1015
 
1016
+ /**
1017
+ * Input for {@link Scenario.label}, {@link Cluster.label}, and
1018
+ * {@link Namespace.label}.
1019
+ *
1020
+ * Identifies a Kubernetes resource and the label changes to apply.
1021
+ *
1022
+ * - A label value of `string` adds or updates the label.
1023
+ * - A label value of `null` removes the label.
1024
+ */
1025
+ export interface LabelInput<T extends K8sResource = K8sResource> {
1026
+ /**
1027
+ * Kubernetes API version (e.g. `"v1"`, `"apps/v1"`).
1028
+ */
1029
+ readonly apiVersion: T["apiVersion"];
1030
+
1031
+ /**
1032
+ * Kubernetes kind (e.g. `"ConfigMap"`, `"Deployment"`).
1033
+ */
1034
+ readonly kind: T["kind"];
1035
+
1036
+ /**
1037
+ * `metadata.name` of the target resource.
1038
+ */
1039
+ readonly name: string;
1040
+
1041
+ /**
1042
+ * Optional namespace override.
1043
+ *
1044
+ * When used on a {@link Namespace}-scoped API surface the namespace is
1045
+ * already set; this field is mainly useful at the {@link Scenario} or
1046
+ * {@link Cluster} level for namespaced resources.
1047
+ */
1048
+ readonly namespace?: undefined | string;
1049
+
1050
+ /**
1051
+ * Label mutations to apply.
1052
+ *
1053
+ * - `"value"` -- add or update the label to the given value.
1054
+ * - `null` -- remove the label.
1055
+ */
1056
+ readonly labels: Readonly<Record<string, string | null>>;
1057
+
1058
+ /**
1059
+ * When `true`, passes `--overwrite` to allow updating labels that already
1060
+ * exist on the resource.
1061
+ *
1062
+ * @default false
1063
+ */
1064
+ readonly overwrite?: undefined | boolean;
1065
+ }
1066
+
740
1067
  /**
741
1068
  * A test definition for {@link Scenario.assert}.
742
1069
  */
@@ -118,3 +118,25 @@ export interface ESM {
118
118
  function isESM(value: unknown): value is ESM {
119
119
  return typeof value === "object" && value !== null && "default" in value;
120
120
  }
121
+
122
+ export function getResourceMeta(
123
+ value: unknown
124
+ ): undefined | { kind: string; name: string } {
125
+ if (typeof value === "string") {
126
+ const result = parseK8sResourceYaml(value);
127
+ if (result.ok) {
128
+ return { kind: result.value.kind, name: result.value.metadata.name };
129
+ }
130
+ }
131
+ if (isESM(value)) {
132
+ const result = parseK8sResourceFromESM(value);
133
+ if (result.ok) {
134
+ return { kind: result.value.kind, name: result.value.metadata.name };
135
+ }
136
+ }
137
+ const result = parseK8sResource(value);
138
+ if (result.ok) {
139
+ return { kind: result.value.kind, name: result.value.metadata.name };
140
+ }
141
+ return undefined;
142
+ }
@@ -135,6 +135,26 @@ export interface Kubectl {
135
135
  name: string,
136
136
  options?: KubectlDeleteOptions
137
137
  ): Promise<string>;
138
+
139
+ /**
140
+ * Adds, updates, or removes labels on a Kubernetes resource using
141
+ * `kubectl label <resource>/<name> key=value ... [--overwrite]`.
142
+ *
143
+ * Labels with a `null` value are removed (emitted as `key-`).
144
+ *
145
+ * @param resource - The resource type (e.g., "configmap", "deployment.v1.apps")
146
+ * @param name - The name of the resource to label
147
+ * @param labels - Label mutations (string to set, null to remove)
148
+ * @param options - Optional label options (overwrite, context)
149
+ * @returns The trimmed stdout from kubectl
150
+ * @throws Error if kubectl exits with non-zero code
151
+ */
152
+ label(
153
+ resource: string,
154
+ name: string,
155
+ labels: Readonly<Record<string, string | null>>,
156
+ options?: KubectlLabelOptions
157
+ ): Promise<string>;
138
158
  }
139
159
 
140
160
  export interface KubectlDeleteOptions {
@@ -146,6 +166,15 @@ export interface KubectlDeleteOptions {
146
166
  readonly context?: undefined | KubectlContext;
147
167
  }
148
168
 
169
+ export interface KubectlLabelOptions {
170
+ /**
171
+ * If true, adds `--overwrite` to allow updating labels that already
172
+ * exist on the resource.
173
+ */
174
+ readonly overwrite?: undefined | boolean;
175
+ readonly context?: undefined | KubectlContext;
176
+ }
177
+
149
178
  export class RealKubectl implements Kubectl {
150
179
  private readonly recorder: Recorder;
151
180
  private readonly cwd: undefined | string;
@@ -283,6 +312,31 @@ export class RealKubectl implements Kubectl {
283
312
  });
284
313
  }
285
314
 
315
+ async label(
316
+ resource: string,
317
+ name: string,
318
+ labels: Readonly<Record<string, string | null>>,
319
+ options?: KubectlLabelOptions
320
+ ): Promise<string> {
321
+ const args: [string, ...Array<string>] = ["label", `${resource}/${name}`];
322
+ for (const [key, value] of Object.entries(labels)) {
323
+ if (value === null) {
324
+ args.push(`${key}-`);
325
+ } else {
326
+ args.push(`${key}=${value}`);
327
+ }
328
+ }
329
+ if (options?.overwrite) {
330
+ args.push("--overwrite");
331
+ }
332
+ return await this.runKubectl({
333
+ args,
334
+ stdoutLanguage: "text",
335
+ stderrLanguage: "text",
336
+ overrideContext: options?.context,
337
+ });
338
+ }
339
+
286
340
  private async runKubectl(params: ExecParams): Promise<string> {
287
341
  const cmd = "kubectl";
288
342
  const ctx = { ...this.defaultContext, ...params.overrideContext };