@intentius/chant-lexicon-k8s 0.45.0 → 0.49.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.
Files changed (160) hide show
  1. package/dist/components/kubectl-apply.d.ts +5 -0
  2. package/dist/components/kubectl-apply.d.ts.map +1 -1
  3. package/dist/composites/flux-app.d.ts +12 -0
  4. package/dist/composites/flux-app.d.ts.map +1 -1
  5. package/dist/composites/index.d.ts +6 -0
  6. package/dist/composites/index.d.ts.map +1 -1
  7. package/dist/composites/inference-service.d.ts +86 -0
  8. package/dist/composites/inference-service.d.ts.map +1 -0
  9. package/dist/composites/model.d.ts +80 -0
  10. package/dist/composites/model.d.ts.map +1 -0
  11. package/dist/composites/vllm-serving-runtime.d.ts +100 -0
  12. package/dist/composites/vllm-serving-runtime.d.ts.map +1 -0
  13. package/dist/crd/crd-sources.d.ts.map +1 -1
  14. package/dist/deep-observe-hooks.d.ts.map +1 -1
  15. package/dist/describe-resources.d.ts.map +1 -1
  16. package/dist/generated/index.d.ts +167 -40
  17. package/dist/generated/index.d.ts.map +1 -1
  18. package/dist/group-namespace.d.ts.map +1 -1
  19. package/dist/index.d.ts +3 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/integrity.json +13 -5
  22. package/dist/kube/apply.d.ts.map +1 -1
  23. package/dist/lint/audit-catalog.d.ts.map +1 -1
  24. package/dist/lint/post-synth/index.d.ts.map +1 -1
  25. package/dist/lint/post-synth/k8s-helpers.d.ts +28 -1
  26. package/dist/lint/post-synth/k8s-helpers.d.ts.map +1 -1
  27. package/dist/lint/post-synth/sops-helpers.d.ts +38 -0
  28. package/dist/lint/post-synth/sops-helpers.d.ts.map +1 -0
  29. package/dist/lint/post-synth/wk8404.d.ts +12 -0
  30. package/dist/lint/post-synth/wk8404.d.ts.map +1 -0
  31. package/dist/lint/post-synth/wk8405.d.ts +13 -0
  32. package/dist/lint/post-synth/wk8405.d.ts.map +1 -0
  33. package/dist/lint/post-synth/wk8406.d.ts +12 -0
  34. package/dist/lint/post-synth/wk8406.d.ts.map +1 -0
  35. package/dist/lint/post-synth/wk8407.d.ts +21 -0
  36. package/dist/lint/post-synth/wk8407.d.ts.map +1 -0
  37. package/dist/lint/post-synth/wk8503.d.ts +39 -0
  38. package/dist/lint/post-synth/wk8503.d.ts.map +1 -0
  39. package/dist/lint/post-synth/wk8504.d.ts +28 -0
  40. package/dist/lint/post-synth/wk8504.d.ts.map +1 -0
  41. package/dist/lint/post-synth/wk8505.d.ts +28 -0
  42. package/dist/lint/post-synth/wk8505.d.ts.map +1 -0
  43. package/dist/manifest.json +1 -1
  44. package/dist/meta.json +1039 -68
  45. package/dist/okf/index.md +26 -2
  46. package/dist/okf/rules/FLUX002.md +0 -1
  47. package/dist/okf/rules/WK8404.md +11 -0
  48. package/dist/okf/rules/WK8405.md +17 -0
  49. package/dist/okf/rules/WK8406.md +11 -0
  50. package/dist/okf/rules/WK8407.md +15 -0
  51. package/dist/okf/rules/WK8503.md +17 -0
  52. package/dist/okf/rules/WK8504.md +11 -0
  53. package/dist/okf/rules/WK8505.md +15 -0
  54. package/dist/okf/types/AWSClusterControllerIdentity.md +14 -0
  55. package/dist/okf/types/AWSManagedCluster.md +14 -0
  56. package/dist/okf/types/AWSManagedControlPlane.md +14 -0
  57. package/dist/okf/types/AWSManagedMachinePool.md +14 -0
  58. package/dist/okf/types/CAPICluster.md +14 -0
  59. package/dist/okf/types/Certificate.md +1 -0
  60. package/dist/okf/types/ClusterResourceSet.md +14 -0
  61. package/dist/okf/types/ClusterServingRuntime.md +14 -0
  62. package/dist/okf/types/{Cluster.md → CnpgCluster.md} +2 -2
  63. package/dist/okf/types/DBInstance.md +14 -0
  64. package/dist/okf/types/{Bucket.md → FluxBucket.md} +2 -6
  65. package/dist/okf/types/HelmChartProxy.md +14 -0
  66. package/dist/okf/types/IamRole.md +14 -0
  67. package/dist/okf/types/InferenceService.md +19 -0
  68. package/dist/okf/types/InfisicalDynamicSecret.md +4 -0
  69. package/dist/okf/types/InfisicalSecret.md +4 -0
  70. package/dist/okf/types/Kustomization.md +1 -0
  71. package/dist/okf/types/MachinePool.md +14 -0
  72. package/dist/okf/types/PDB.md +1 -0
  73. package/dist/okf/types/PodIdentityAssociation.md +14 -0
  74. package/dist/okf/types/Policy.md +14 -0
  75. package/dist/okf/types/S3Bucket.md +14 -0
  76. package/dist/okf/types/ServingRuntime.md +14 -0
  77. package/dist/okf/types/StatefulSet.md +1 -0
  78. package/dist/okf/types/User.md +14 -0
  79. package/dist/op/activities/ensure-secret.d.ts +41 -0
  80. package/dist/op/activities/ensure-secret.d.ts.map +1 -0
  81. package/dist/op/activities/index.d.ts +2 -0
  82. package/dist/op/activities/index.d.ts.map +1 -1
  83. package/dist/op/activities/kubectl.d.ts +9 -0
  84. package/dist/op/activities/kubectl.d.ts.map +1 -1
  85. package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
  86. package/dist/plugin.d.ts.map +1 -1
  87. package/dist/rules/k8s-helpers.ts +55 -1
  88. package/dist/rules/sops-helpers.ts +76 -0
  89. package/dist/rules/wk8404.ts +55 -0
  90. package/dist/rules/wk8405.ts +88 -0
  91. package/dist/rules/wk8406.ts +60 -0
  92. package/dist/rules/wk8407.ts +71 -0
  93. package/dist/rules/wk8503.ts +220 -0
  94. package/dist/rules/wk8504.ts +51 -0
  95. package/dist/rules/wk8505.ts +63 -0
  96. package/dist/secret-labels.d.ts +31 -0
  97. package/dist/secret-labels.d.ts.map +1 -0
  98. package/dist/secret-store.d.ts +73 -0
  99. package/dist/secret-store.d.ts.map +1 -0
  100. package/dist/serializer.d.ts.map +1 -1
  101. package/dist/sops/encrypted-secret-file.d.ts +52 -0
  102. package/dist/sops/encrypted-secret-file.d.ts.map +1 -0
  103. package/dist/sops/entity.d.ts +31 -0
  104. package/dist/sops/entity.d.ts.map +1 -0
  105. package/dist/teardown.d.ts +46 -0
  106. package/dist/teardown.d.ts.map +1 -0
  107. package/dist/types/index.d.ts +261 -9
  108. package/package.json +4 -4
  109. package/src/components/kubectl-apply.ts +5 -0
  110. package/src/composites/composites.test.ts +273 -0
  111. package/src/composites/flux-app.ts +23 -0
  112. package/src/composites/index.ts +11 -0
  113. package/src/composites/inference-service.ts +140 -0
  114. package/src/composites/model.ts +150 -0
  115. package/src/composites/vllm-serving-runtime.test.ts +167 -0
  116. package/src/composites/vllm-serving-runtime.ts +196 -0
  117. package/src/crd/cnpg.test.ts +8 -3
  118. package/src/crd/crd-sources.ts +183 -0
  119. package/src/crd/parser.test.ts +14 -0
  120. package/src/deep-observe-hooks.ts +24 -0
  121. package/src/deep-observe.test.ts +89 -0
  122. package/src/describe-resources.ts +6 -1
  123. package/src/generated/index.d.ts +261 -9
  124. package/src/generated/index.ts +167 -40
  125. package/src/generated/lexicon-k8s.json +1039 -68
  126. package/src/generated/operations.json +272 -0
  127. package/src/group-namespace.test.ts +31 -0
  128. package/src/group-namespace.ts +13 -0
  129. package/src/index.ts +6 -0
  130. package/src/kube/apply.ts +5 -0
  131. package/src/lifecycle-integration.test.ts +42 -0
  132. package/src/lint/audit-catalog.ts +7 -0
  133. package/src/lint/post-synth/index.ts +14 -0
  134. package/src/lint/post-synth/k8s-helpers.ts +55 -1
  135. package/src/lint/post-synth/post-synth.test.ts +826 -0
  136. package/src/lint/post-synth/sops-helpers.ts +76 -0
  137. package/src/lint/post-synth/wk8404.ts +55 -0
  138. package/src/lint/post-synth/wk8405.ts +88 -0
  139. package/src/lint/post-synth/wk8406.ts +60 -0
  140. package/src/lint/post-synth/wk8407.ts +71 -0
  141. package/src/lint/post-synth/wk8503.ts +220 -0
  142. package/src/lint/post-synth/wk8504.ts +51 -0
  143. package/src/lint/post-synth/wk8505.ts +63 -0
  144. package/src/op/activities/ensure-secret.ts +71 -0
  145. package/src/op/activities/index.ts +7 -0
  146. package/src/op/activities/kubectl.test.ts +32 -0
  147. package/src/op/activities/kubectl.ts +28 -5
  148. package/src/op/activities/wait-for-ready.test.ts +34 -0
  149. package/src/op/activities/wait-for-ready.ts +26 -0
  150. package/src/plugin.ts +34 -2
  151. package/src/secret-labels.ts +36 -0
  152. package/src/secret-store.test.ts +184 -0
  153. package/src/secret-store.ts +165 -0
  154. package/src/serializer.ts +33 -2
  155. package/src/sops/encrypted-secret-file.test.ts +333 -0
  156. package/src/sops/encrypted-secret-file.ts +264 -0
  157. package/src/sops/entity.ts +53 -0
  158. package/src/teardown.test.ts +240 -0
  159. package/src/teardown.ts +239 -0
  160. package/src/testdata/sops/db-credentials.sops.yaml +23 -0
@@ -10,6 +10,7 @@ function p(member: unknown): Record<string, unknown> {
10
10
  import { StatefulApp } from "./stateful-app";
11
11
  import { ArgoAppFor, ArgoAppSetForRegions, registerArgoCluster } from "./argo-app";
12
12
  import { FluxGitSource, FluxAppFor } from "./flux-app";
13
+ import { Model, resolveModelStorageUri } from "./model";
13
14
  import { CronWorkload } from "./cron-workload";
14
15
  import { AutoscaledService } from "./autoscaled-service";
15
16
  import { WorkerPool } from "./worker-pool";
@@ -35,6 +36,7 @@ import { GcePdStorageClass } from "./gce-pd-storage-class";
35
36
  import { FilestoreStorageClass } from "./filestore-storage-class";
36
37
  import { GkeGateway } from "./gke-gateway";
37
38
  import { ConfigConnectorContext } from "./config-connector-context";
39
+ import { InferenceService } from "./inference-service";
38
40
 
39
41
  // ── WebApp ──────────────────────────────────────────────────────────
40
42
 
@@ -3473,6 +3475,271 @@ describe("FluxAppFor", () => {
3473
3475
  expect(spec.timeout).toBe("3m");
3474
3476
  expect(spec.serviceAccountName).toBe("flux-applier");
3475
3477
  });
3478
+
3479
+ test("omits decryption when the option is not set", () => {
3480
+ const spec = p(FluxAppFor("hello", { source: "flux-system", path: "./k8s" }).kustomization).spec as any;
3481
+ expect(spec.decryption).toBeUndefined();
3482
+ });
3483
+
3484
+ test('decryption: "sops" defaults the secretRef to sops-age', () => {
3485
+ const spec = p(
3486
+ FluxAppFor("hello", { source: "flux-system", path: "./k8s", decryption: "sops" }).kustomization,
3487
+ ).spec as any;
3488
+ expect(spec.decryption).toEqual({ provider: "sops", secretRef: { name: "sops-age" } });
3489
+ });
3490
+
3491
+ test("decryption object form names a custom secretRef", () => {
3492
+ const spec = p(
3493
+ FluxAppFor("hello", {
3494
+ source: "flux-system",
3495
+ path: "./k8s",
3496
+ decryption: { provider: "sops", secretRef: "my-age-identity" },
3497
+ }).kustomization,
3498
+ ).spec as any;
3499
+ expect(spec.decryption).toEqual({ provider: "sops", secretRef: { name: "my-age-identity" } });
3500
+ });
3501
+
3502
+ test("decryption object form with no secretRef still defaults to sops-age", () => {
3503
+ const spec = p(
3504
+ FluxAppFor("hello", { source: "flux-system", path: "./k8s", decryption: { provider: "sops" } }).kustomization,
3505
+ ).spec as any;
3506
+ expect(spec.decryption).toEqual({ provider: "sops", secretRef: { name: "sops-age" } });
3507
+ });
3508
+ });
3509
+
3510
+ // ── InferenceService ─────────────────────────────────────────────────
3511
+
3512
+ describe("InferenceService", () => {
3513
+ test("emits a valid InferenceService referencing the runtime and storageUri", () => {
3514
+ const result = InferenceService({
3515
+ name: "llama-3-8b",
3516
+ namespace: "serving",
3517
+ model: "gs://my-models/llama-3-8b/v1",
3518
+ runtime: "vllm-runtime",
3519
+ });
3520
+ expect(result.inferenceService).toBeDefined();
3521
+ const spec = p(result.inferenceService).spec as any;
3522
+ expect(spec.predictor.model.runtime).toBe("vllm-runtime");
3523
+ expect(spec.predictor.model.storageUri).toBe("gs://my-models/llama-3-8b/v1");
3524
+ });
3525
+
3526
+ test("accepts a resolved model ref ({ storageUri }) in place of a raw string", () => {
3527
+ const result = InferenceService({
3528
+ name: "svc",
3529
+ namespace: "ns",
3530
+ model: { storageUri: "s3://bucket/model/v2" },
3531
+ runtime: "vllm-runtime",
3532
+ });
3533
+ const spec = p(result.inferenceService).spec as any;
3534
+ expect(spec.predictor.model.storageUri).toBe("s3://bucket/model/v2");
3535
+ });
3536
+
3537
+ test("modelFormat is omitted unless explicitly set", () => {
3538
+ const result = InferenceService({
3539
+ name: "svc",
3540
+ namespace: "ns",
3541
+ model: "gs://bucket/model",
3542
+ runtime: "vllm-runtime",
3543
+ });
3544
+ const spec = p(result.inferenceService).spec as any;
3545
+ expect(spec.predictor.model.modelFormat).toBeUndefined();
3546
+ });
3547
+
3548
+ test("modelFormat.name flows through when set", () => {
3549
+ const result = InferenceService({
3550
+ name: "svc",
3551
+ namespace: "ns",
3552
+ model: "gs://bucket/model",
3553
+ runtime: "vllm-runtime",
3554
+ modelFormat: "vLLM",
3555
+ });
3556
+ const spec = p(result.inferenceService).spec as any;
3557
+ expect(spec.predictor.model.modelFormat).toEqual({ name: "vLLM" });
3558
+ });
3559
+
3560
+ test("autoscaling bounds (minReplicas/maxReplicas/scaleTarget/scaleMetric) are expressible and present", () => {
3561
+ const result = InferenceService({
3562
+ name: "svc",
3563
+ namespace: "ns",
3564
+ model: "gs://bucket/model",
3565
+ runtime: "vllm-runtime",
3566
+ minReplicas: 1,
3567
+ maxReplicas: 4,
3568
+ scaleTarget: 8,
3569
+ scaleMetric: "concurrency",
3570
+ });
3571
+ const spec = p(result.inferenceService).spec as any;
3572
+ expect(spec.predictor.minReplicas).toBe(1);
3573
+ expect(spec.predictor.maxReplicas).toBe(4);
3574
+ expect(spec.predictor.scaleTarget).toBe(8);
3575
+ expect(spec.predictor.scaleMetric).toBe("concurrency");
3576
+ });
3577
+
3578
+ test("raw-HPA scaleMetric values (cpu/memory/rps) pass through", () => {
3579
+ const result = InferenceService({
3580
+ name: "svc",
3581
+ namespace: "ns",
3582
+ model: "gs://bucket/model",
3583
+ runtime: "vllm-runtime",
3584
+ scaleMetric: "cpu",
3585
+ });
3586
+ const spec = p(result.inferenceService).spec as any;
3587
+ expect(spec.predictor.scaleMetric).toBe("cpu");
3588
+ });
3589
+
3590
+ test("autoscaling fields are absent when not set", () => {
3591
+ const result = InferenceService({
3592
+ name: "svc",
3593
+ namespace: "ns",
3594
+ model: "gs://bucket/model",
3595
+ runtime: "vllm-runtime",
3596
+ });
3597
+ const spec = p(result.inferenceService).spec as any;
3598
+ expect(spec.predictor.minReplicas).toBeUndefined();
3599
+ expect(spec.predictor.maxReplicas).toBeUndefined();
3600
+ expect(spec.predictor.scaleTarget).toBeUndefined();
3601
+ expect(spec.predictor.scaleMetric).toBeUndefined();
3602
+ });
3603
+
3604
+ test("canary traffic split is expressible and present", () => {
3605
+ const result = InferenceService({
3606
+ name: "svc",
3607
+ namespace: "ns",
3608
+ model: "gs://bucket/model",
3609
+ runtime: "vllm-runtime",
3610
+ canaryTrafficPercent: 10,
3611
+ });
3612
+ const spec = p(result.inferenceService).spec as any;
3613
+ expect(spec.predictor.canaryTrafficPercent).toBe(10);
3614
+ });
3615
+
3616
+ test("canary traffic split is absent when not set", () => {
3617
+ const result = InferenceService({
3618
+ name: "svc",
3619
+ namespace: "ns",
3620
+ model: "gs://bucket/model",
3621
+ runtime: "vllm-runtime",
3622
+ });
3623
+ const spec = p(result.inferenceService).spec as any;
3624
+ expect(spec.predictor.canaryTrafficPercent).toBeUndefined();
3625
+ });
3626
+
3627
+ test("includes common labels and metadata", () => {
3628
+ const result = InferenceService({
3629
+ name: "svc",
3630
+ namespace: "ns",
3631
+ model: "gs://bucket/model",
3632
+ runtime: "vllm-runtime",
3633
+ labels: { team: "ml-platform" },
3634
+ });
3635
+ const meta = p(result.inferenceService).metadata as any;
3636
+ expect(meta.name).toBe("svc");
3637
+ expect(meta.namespace).toBe("ns");
3638
+ expect(meta.labels["app.kubernetes.io/name"]).toBe("svc");
3639
+ expect(meta.labels["app.kubernetes.io/managed-by"]).toBe("chant");
3640
+ expect(meta.labels.team).toBe("ml-platform");
3641
+ });
3642
+
3643
+ test("golden: full spec shape with predictor, model, autoscaling, and canary", () => {
3644
+ const result = InferenceService({
3645
+ name: "llama-3-8b",
3646
+ namespace: "serving",
3647
+ model: "gs://my-models/llama-3-8b/v1",
3648
+ runtime: "vllm-runtime",
3649
+ modelFormat: "vLLM",
3650
+ minReplicas: 1,
3651
+ maxReplicas: 4,
3652
+ scaleTarget: 8,
3653
+ scaleMetric: "concurrency",
3654
+ canaryTrafficPercent: 10,
3655
+ });
3656
+ const spec = p(result.inferenceService).spec as any;
3657
+ expect(spec).toEqual({
3658
+ predictor: {
3659
+ model: {
3660
+ runtime: "vllm-runtime",
3661
+ storageUri: "gs://my-models/llama-3-8b/v1",
3662
+ modelFormat: { name: "vLLM" },
3663
+ },
3664
+ minReplicas: 1,
3665
+ maxReplicas: 4,
3666
+ scaleTarget: 8,
3667
+ scaleMetric: "concurrency",
3668
+ canaryTrafficPercent: 10,
3669
+ },
3670
+ });
3671
+ });
3672
+
3673
+ test("defaults passthrough merges into the InferenceService", () => {
3674
+ const result = InferenceService({
3675
+ name: "svc",
3676
+ namespace: "ns",
3677
+ model: "gs://bucket/model",
3678
+ runtime: "vllm-runtime",
3679
+ defaults: {
3680
+ inferenceService: { spec: { predictor: { model: { protocolVersion: "v2" } } } },
3681
+ },
3682
+ });
3683
+ const spec = p(result.inferenceService).spec as any;
3684
+ expect(spec.predictor.model.protocolVersion).toBe("v2");
3685
+ expect(spec.predictor.model.runtime).toBe("vllm-runtime");
3686
+ });
3687
+ });
3688
+
3689
+ // ── Model ─────────────────────────────────────────────────────────
3690
+
3691
+ describe("Model", () => {
3692
+ test("resolves a gcs source to a gs:// storageUri pinned to version", () => {
3693
+ const result = Model({ id: "llama-3-8b", version: "2024-07-01", source: "gcs" });
3694
+ expect(result.storageUri).toBe("gs://llama-3-8b/2024-07-01");
3695
+ });
3696
+
3697
+ test("resolves an s3 source to an s3:// storageUri pinned to version", () => {
3698
+ const result = Model({ id: "llama-3-8b", version: "2024-07-01", source: "s3" });
3699
+ expect(result.storageUri).toBe("s3://llama-3-8b/2024-07-01");
3700
+ });
3701
+
3702
+ test("resolves a pvc source to a pvc:// storageUri pinned to version", () => {
3703
+ const result = Model({ id: "llama-3-8b", version: "2024-07-01", source: "pvc" });
3704
+ expect(result.storageUri).toBe("pvc://llama-3-8b/2024-07-01");
3705
+ });
3706
+
3707
+ test("resolves an hf source to an hf:// storageUri pinned to version", () => {
3708
+ const result = Model({ id: "llama-3-8b", version: "2024-07-01", source: "hf" });
3709
+ expect(result.storageUri).toBe("hf://llama-3-8b/2024-07-01");
3710
+ });
3711
+
3712
+ test("an explicit uri overrides the id-derived path but version is still appended", () => {
3713
+ const result = Model({
3714
+ id: "llama-3-8b",
3715
+ version: "2024-07-01",
3716
+ source: "gcs",
3717
+ uri: "my-models-bucket/llama-3-8b-instruct",
3718
+ });
3719
+ expect(result.storageUri).toBe("gs://my-models-bucket/llama-3-8b-instruct/2024-07-01");
3720
+ });
3721
+
3722
+ test("no cache PVC by default", () => {
3723
+ const result = Model({ id: "llama-3-8b", version: "2024-07-01", source: "gcs" });
3724
+ expect(result.cache).toBeUndefined();
3725
+ });
3726
+
3727
+ test("emits a cache PVC named `${id}-${version}` when cache is set", () => {
3728
+ const result = Model({
3729
+ id: "llama-3-8b",
3730
+ version: "2024-07-01",
3731
+ source: "gcs",
3732
+ cache: { storageClass: "premium-rwo", size: "200Gi" },
3733
+ });
3734
+ expect(result.cache).toBeDefined();
3735
+ expect((p(result.cache) as any).metadata.name).toBe("llama-3-8b-2024-07-01");
3736
+ expect((p(result.cache) as any).spec.storageClassName).toBe("premium-rwo");
3737
+ expect((p(result.cache) as any).spec.resources.requests.storage).toBe("200Gi");
3738
+ });
3739
+
3740
+ test("resolveModelStorageUri is the same resolution the composite uses", () => {
3741
+ expect(resolveModelStorageUri({ id: "m", version: "v1", source: "s3" })).toBe("s3://m/v1");
3742
+ });
3476
3743
  });
3477
3744
 
3478
3745
  describe("package index re-exports (regression guard)", () => {
@@ -3488,4 +3755,10 @@ describe("package index re-exports (regression guard)", () => {
3488
3755
  expect(typeof pkg.FluxGitSource).toBe("function");
3489
3756
  expect(typeof pkg.FluxAppFor).toBe("function");
3490
3757
  });
3758
+
3759
+ test("Model is reachable from the package entry", async () => {
3760
+ const pkg: any = await import("../index");
3761
+ expect(typeof pkg.Model).toBe("function");
3762
+ expect(typeof pkg.resolveModelStorageUri).toBe("function");
3763
+ });
3491
3764
  });
@@ -29,6 +29,10 @@ import {
29
29
  /** The namespace the Flux controllers run in and watch by default. */
30
30
  const FLUX_NAMESPACE = "flux-system";
31
31
 
32
+ /** knr-ops's name for the bootstrap-injected age identity Secret, and the
33
+ * name `flux bootstrap` documentation uses. */
34
+ const SOPS_AGE_SECRET_NAME = "sops-age";
35
+
32
36
  /** Source kinds a Kustomization can reconcile from. */
33
37
  export type FluxSourceKind = "GitRepository" | "OCIRepository" | "Bucket";
34
38
 
@@ -154,6 +158,15 @@ export interface FluxAppForOptions {
154
158
  suspend?: boolean;
155
159
  /** ServiceAccount the kustomize-controller impersonates for this app. */
156
160
  serviceAccountName?: string;
161
+ /**
162
+ * SOPS decryption for the reconciled path. `"sops"` is shorthand for the
163
+ * default age Secret name (`sops-age`); pass an object to name a different
164
+ * one. Never inferred — the path this Kustomization reconciles is usually
165
+ * the output of a *different* `chant build`, so the composite cannot know
166
+ * whether it contains SOPS ciphertext. See WK8505 for the build-time check
167
+ * that catches a committed-encrypted secret with no wiring here.
168
+ */
169
+ decryption?: "sops" | { provider: "sops"; secretRef?: string };
157
170
  /** Namespace the Kustomization object itself lives in (default "flux-system"). */
158
171
  fluxNamespace?: string;
159
172
  /** Extra labels applied to the Kustomization. */
@@ -210,12 +223,21 @@ const FluxKustomization = Composite<{ target: string } & FluxAppForOptions, Flux
210
223
  timeout,
211
224
  suspend,
212
225
  serviceAccountName,
226
+ decryption,
213
227
  fluxNamespace = FLUX_NAMESPACE,
214
228
  labels = {},
215
229
  defaults,
216
230
  } = props;
217
231
 
218
232
  const sourceRef = resolveSourceRef(target, source);
233
+ const decryptionSpec = decryption === undefined
234
+ ? undefined
235
+ : {
236
+ provider: "sops",
237
+ secretRef: {
238
+ name: typeof decryption === "string" ? SOPS_AGE_SECRET_NAME : decryption.secretRef ?? SOPS_AGE_SECRET_NAME,
239
+ },
240
+ };
219
241
 
220
242
  // The Kustomization's namespace also scopes the sourceRef: an in-spec
221
243
  // sourceRef without a namespace resolves in the Kustomization's own.
@@ -240,6 +262,7 @@ const FluxKustomization = Composite<{ target: string } & FluxAppForOptions, Flux
240
262
  ...(timeout !== undefined && { timeout }),
241
263
  ...(suspend !== undefined && { suspend }),
242
264
  ...(serviceAccountName !== undefined && { serviceAccountName }),
265
+ ...(decryptionSpec !== undefined && { decryption: decryptionSpec }),
243
266
  },
244
267
  }, defaults?.kustomization));
245
268
 
@@ -81,6 +81,13 @@ export { RayJob } from "./ray-job";
81
81
  export type { RayJobProps, RayJobResult } from "./ray-job";
82
82
  export { RayService } from "./ray-service";
83
83
  export type { RayServiceProps, RayServiceResult } from "./ray-service";
84
+ export { InferenceService } from "./inference-service";
85
+ export type {
86
+ InferenceServiceProps,
87
+ InferenceServiceResult,
88
+ ModelReference,
89
+ ScaleMetric,
90
+ } from "./inference-service";
84
91
  export { ArgoAppFor, ArgoAppSetForRegions, registerArgoCluster } from "./argo-app";
85
92
  export type {
86
93
  ArgoDestination,
@@ -102,3 +109,7 @@ export type {
102
109
  FluxAppForOptions,
103
110
  FluxAppForResult,
104
111
  } from "./flux-app";
112
+ export { VllmServingRuntime } from "./vllm-serving-runtime";
113
+ export type { VllmServingRuntimeProps, VllmServingRuntimeResult, VllmResourceSpec } from "./vllm-serving-runtime";
114
+ export { Model, resolveModelStorageUri } from "./model";
115
+ export type { ModelSource, ModelProps, ModelResult } from "./model";
@@ -0,0 +1,140 @@
1
+ /**
2
+ * InferenceService composite — KServe `InferenceService` (v1beta1).
3
+ *
4
+ * Spec-true to KServe: the predictor references a `ServingRuntime` by name
5
+ * (the plug seam KServe itself defines — see #984's VllmServingRuntime) and
6
+ * a model `storageUri`, plus the real `ComponentExtensionSpec` autoscaling
7
+ * knobs (`minReplicas`/`maxReplicas`/`scaleTarget`/`scaleMetric`) and
8
+ * `canaryTrafficPercent`. No chant-flavored runtime abstraction is invented;
9
+ * `runtime` is just the `ServingRuntime`/`ClusterServingRuntime` name string
10
+ * KServe's own webhook resolves against `supportedModelFormats`.
11
+ *
12
+ * Coupling note (#984/#986 land in parallel in this same lexicon): this
13
+ * composite does not import either sibling. `runtime` is a plain name
14
+ * string (whatever `VllmServingRuntime` from #984 names its `ServingRuntime`
15
+ * object). `model` accepts either a raw `storageUri` string or a
16
+ * `{ storageUri }`-shaped object — the resolved-value shape `Model` from
17
+ * #986 is expected to return — matched structurally, not by import.
18
+ */
19
+
20
+ import { Composite, mergeDefaults } from "@intentius/chant";
21
+ import { InferenceService as InferenceServiceResource } from "../generated";
22
+
23
+ /** Knative concurrency, or a raw HPA metric (cpu/memory/rps). */
24
+ export type ScaleMetric = "concurrency" | "cpu" | "memory" | "rps";
25
+
26
+ /**
27
+ * A resolved model reference. Matches the resolved-value shape the `Model`
28
+ * composite (#986) is expected to return — structurally, not by import.
29
+ */
30
+ export interface ModelReference {
31
+ storageUri: string;
32
+ }
33
+
34
+ export interface InferenceServiceProps {
35
+ name: string;
36
+ namespace: string;
37
+ /** A raw `storageUri` (e.g. `"gs://bucket/model"`) or a resolved model ref. */
38
+ model: string | ModelReference;
39
+ /** `ServingRuntime`/`ClusterServingRuntime` name the predictor pins to. */
40
+ runtime: string;
41
+ /**
42
+ * Model format name for KServe's runtime auto-matching (e.g. `"vLLM"`).
43
+ * Optional since `runtime` already pins the ServingRuntime explicitly;
44
+ * set it if the ServingRuntime's `supportedModelFormats` expects it.
45
+ */
46
+ modelFormat?: string;
47
+ /** Minimum predictor replicas. */
48
+ minReplicas?: number;
49
+ /** Maximum predictor replicas. */
50
+ maxReplicas?: number;
51
+ /** Autoscaling target value (e.g. concurrency-per-replica, or CPU/memory/RPS target). */
52
+ scaleTarget?: number;
53
+ /** Which metric `scaleTarget` is measured against. */
54
+ scaleMetric?: ScaleMetric;
55
+ /** Percentage of traffic (0-100) routed to this revision as a canary. */
56
+ canaryTrafficPercent?: number;
57
+ labels?: Record<string, string>;
58
+ defaults?: {
59
+ inferenceService?: Partial<Record<string, unknown>>;
60
+ };
61
+ }
62
+
63
+ export type InferenceServiceResult = {
64
+ inferenceService: InstanceType<typeof InferenceServiceResource>;
65
+ };
66
+
67
+ function resolveStorageUri(model: string | ModelReference): string {
68
+ return typeof model === "string" ? model : model.storageUri;
69
+ }
70
+
71
+ /**
72
+ * Create an InferenceService composite — a KServe `InferenceService`
73
+ * predictor referencing a `ServingRuntime` and a model `storageUri`, with
74
+ * autoscaling bounds and an optional canary traffic split.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * import { InferenceService } from "@intentius/chant-lexicon-k8s";
79
+ *
80
+ * const svc = InferenceService({
81
+ * name: "llama-3-8b",
82
+ * namespace: "serving",
83
+ * model: "gs://my-models/llama-3-8b/v1",
84
+ * runtime: "vllm-runtime",
85
+ * modelFormat: "vLLM",
86
+ * minReplicas: 1,
87
+ * maxReplicas: 4,
88
+ * scaleTarget: 8,
89
+ * scaleMetric: "concurrency",
90
+ * canaryTrafficPercent: 10,
91
+ * });
92
+ * ```
93
+ */
94
+ export const InferenceService = Composite((props: InferenceServiceProps) => {
95
+ const {
96
+ name,
97
+ namespace,
98
+ model,
99
+ runtime,
100
+ modelFormat,
101
+ minReplicas,
102
+ maxReplicas,
103
+ scaleTarget,
104
+ scaleMetric,
105
+ canaryTrafficPercent,
106
+ labels: extraLabels = {},
107
+ defaults: defs,
108
+ } = props;
109
+
110
+ const commonLabels: Record<string, string> = {
111
+ "app.kubernetes.io/name": name,
112
+ "app.kubernetes.io/managed-by": "chant",
113
+ "app.kubernetes.io/component": "inference-service",
114
+ ...extraLabels,
115
+ };
116
+
117
+ const predictor: Record<string, unknown> = {
118
+ model: {
119
+ runtime,
120
+ storageUri: resolveStorageUri(model),
121
+ ...(modelFormat !== undefined && { modelFormat: { name: modelFormat } }),
122
+ },
123
+ ...(minReplicas !== undefined && { minReplicas }),
124
+ ...(maxReplicas !== undefined && { maxReplicas }),
125
+ ...(scaleTarget !== undefined && { scaleTarget }),
126
+ ...(scaleMetric !== undefined && { scaleMetric }),
127
+ ...(canaryTrafficPercent !== undefined && { canaryTrafficPercent }),
128
+ };
129
+
130
+ const inferenceService = new InferenceServiceResource(mergeDefaults({
131
+ metadata: {
132
+ name,
133
+ namespace,
134
+ labels: commonLabels,
135
+ },
136
+ spec: { predictor },
137
+ }, defs?.inferenceService));
138
+
139
+ return { inferenceService };
140
+ }, "InferenceService");
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Model composite — resolves a model artifact reference to a KServe
3
+ * `storageUri` (+ optional warm-cache PVC), keyed by `(id, version)`.
4
+ *
5
+ * This is the FSx-warm-cache equivalent from the Netflix serving yardstick
6
+ * (see epic #982): a small, spec-true resolver that InferenceService (#985)
7
+ * consumes for its predictor's model reference. It does not itself emit an
8
+ * InferenceService or ServingRuntime — those are #984/#985's job.
9
+ *
10
+ * `version` is required so every resolved `storageUri` is pinned: the
11
+ * version is always appended as the trailing path segment, whether the
12
+ * default `id`-derived path is used or `uri` overrides it. This keeps the
13
+ * output stable for the unpinned-model lint rule (#988).
14
+ *
15
+ * `Model` itself is a plain resolver — a resolved value object, per the
16
+ * composite conventions — rather than a `Composite`, because its primary
17
+ * output (`storageUri`) is a string, not a Declarable; a `Composite`
18
+ * factory's members must all be Declarables. The optional cache PVC is
19
+ * built through an internal `Composite` so it gets the same bookkeeping
20
+ * (provenance, defaults-merging) as every other emitted resource.
21
+ */
22
+
23
+ import { Composite, mergeDefaults } from "@intentius/chant";
24
+ import { PersistentVolumeClaim } from "../generated";
25
+
26
+ /** Where the model's weights live. Maps to the matching KServe storage-initializer scheme. */
27
+ export type ModelSource = "hf" | "gcs" | "s3" | "pvc";
28
+
29
+ const STORAGE_SCHEME: Record<ModelSource, string> = {
30
+ hf: "hf",
31
+ gcs: "gs",
32
+ s3: "s3",
33
+ pvc: "pvc",
34
+ };
35
+
36
+ export interface ModelProps {
37
+ /** Model identifier, e.g. "llama-3-8b-instruct". Used to key the resolved storageUri and cache PVC. */
38
+ id: string;
39
+ /**
40
+ * Model version/revision. REQUIRED — feeds the unpinned-model lint rule
41
+ * (#988). Always appears as the trailing segment of the resolved
42
+ * `storageUri` so the reference is pinned.
43
+ */
44
+ version: string;
45
+ /** Where the model artifact is hosted; selects the storageUri scheme. */
46
+ source: ModelSource;
47
+ /**
48
+ * Explicit path override (e.g. a bucket/prefix, HF repo id, or PVC name)
49
+ * in place of the `id`-derived default. `version` is still appended.
50
+ */
51
+ uri?: string;
52
+ /** When set, emits a warm-cache PVC named `${id}-${version}` for the InferenceService (#985) to mount. */
53
+ cache?: {
54
+ storageClass: string;
55
+ size: string;
56
+ };
57
+ /** Per-member defaults for fine-grained overrides. */
58
+ defaults?: {
59
+ cachePvc?: Partial<Record<string, unknown>>;
60
+ };
61
+ }
62
+
63
+ export interface ModelResult {
64
+ /** Resolved KServe storageUri, pinned to `version`. */
65
+ storageUri: string;
66
+ /** Warm-cache PVC, present only when `cache` is set. Named `${id}-${version}`. */
67
+ cache?: InstanceType<typeof PersistentVolumeClaim>;
68
+ }
69
+
70
+ /**
71
+ * Resolve a model artifact's `{ id, version, source, uri }` to a pinned
72
+ * KServe `storageUri` string.
73
+ */
74
+ export function resolveModelStorageUri(props: Pick<ModelProps, "id" | "version" | "source" | "uri">): string {
75
+ const { id, version, source, uri } = props;
76
+ const scheme = STORAGE_SCHEME[source];
77
+ const path = uri ?? id;
78
+ return `${scheme}://${path}/${version}`;
79
+ }
80
+
81
+ interface ModelCachePvcProps {
82
+ id: string;
83
+ version: string;
84
+ storageClass: string;
85
+ size: string;
86
+ defaults?: Partial<Record<string, unknown>>;
87
+ }
88
+
89
+ const ModelCachePvc = Composite<ModelCachePvcProps, { pvc: InstanceType<typeof PersistentVolumeClaim> }>(
90
+ (props) => {
91
+ const { id, version, storageClass, size, defaults } = props;
92
+
93
+ const pvc = new PersistentVolumeClaim(mergeDefaults({
94
+ metadata: {
95
+ name: `${id}-${version}`,
96
+ labels: {
97
+ "app.kubernetes.io/name": id,
98
+ "app.kubernetes.io/managed-by": "chant",
99
+ "app.kubernetes.io/component": "model-cache",
100
+ },
101
+ },
102
+ spec: {
103
+ accessModes: ["ReadWriteMany"],
104
+ storageClassName: storageClass,
105
+ resources: { requests: { storage: size } },
106
+ },
107
+ }, defaults));
108
+
109
+ return { pvc };
110
+ },
111
+ "ModelCachePvc",
112
+ );
113
+
114
+ /**
115
+ * Resolve a model artifact `{ id, version, source, uri, cache }` to a pinned
116
+ * `storageUri`, with an optional warm-cache PVC.
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * import { Model } from "@intentius/chant-lexicon-k8s";
121
+ *
122
+ * const { storageUri, cache } = Model({
123
+ * id: "llama-3-8b-instruct",
124
+ * version: "2024-07-01",
125
+ * source: "gcs",
126
+ * uri: "my-models-bucket/llama-3-8b-instruct",
127
+ * cache: { storageClass: "premium-rwo", size: "200Gi" },
128
+ * });
129
+ * ```
130
+ */
131
+ export function Model(props: ModelProps): ModelResult {
132
+ const { id, version, cache, defaults } = props;
133
+
134
+ const storageUri = resolveModelStorageUri(props);
135
+
136
+ const cachePvc = cache
137
+ ? ModelCachePvc({
138
+ id,
139
+ version,
140
+ storageClass: cache.storageClass,
141
+ size: cache.size,
142
+ defaults: defaults?.cachePvc,
143
+ }).pvc
144
+ : undefined;
145
+
146
+ return {
147
+ storageUri,
148
+ ...(cachePvc && { cache: cachePvc }),
149
+ };
150
+ }