@intentius/chant-lexicon-k8s 0.46.0 → 0.50.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/dist/components/kubectl-apply.d.ts +5 -0
- package/dist/components/kubectl-apply.d.ts.map +1 -1
- package/dist/composites/flux-app.d.ts +12 -0
- package/dist/composites/flux-app.d.ts.map +1 -1
- package/dist/composites/index.d.ts +8 -0
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/inference-service.d.ts +86 -0
- package/dist/composites/inference-service.d.ts.map +1 -0
- package/dist/composites/model.d.ts +80 -0
- package/dist/composites/model.d.ts.map +1 -0
- package/dist/composites/operator-stack.d.ts +183 -0
- package/dist/composites/operator-stack.d.ts.map +1 -0
- package/dist/composites/vllm-serving-runtime.d.ts +100 -0
- package/dist/composites/vllm-serving-runtime.d.ts.map +1 -0
- package/dist/crd/crd-sources.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts.map +1 -1
- package/dist/generated/index.d.ts +167 -40
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/group-namespace.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +41 -33
- package/dist/kube/apply.d.ts.map +1 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/argo-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/k8s-helpers.d.ts +56 -6
- package/dist/lint/post-synth/k8s-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/sops-helpers.d.ts +38 -0
- package/dist/lint/post-synth/sops-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8005.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8006.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8041.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8042.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8101.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8102.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8103.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8104.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8105.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8201.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8202.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8203.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8204.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8205.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8207.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8208.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8209.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8301.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8302.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8303.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8304.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8305.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8306.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8401.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8402.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8403.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8404.d.ts +12 -0
- package/dist/lint/post-synth/wk8404.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8405.d.ts +13 -0
- package/dist/lint/post-synth/wk8405.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8406.d.ts +12 -0
- package/dist/lint/post-synth/wk8406.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8407.d.ts +21 -0
- package/dist/lint/post-synth/wk8407.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8503.d.ts +39 -0
- package/dist/lint/post-synth/wk8503.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8504.d.ts +28 -0
- package/dist/lint/post-synth/wk8504.d.ts.map +1 -0
- package/dist/lint/post-synth/wk8505.d.ts +28 -0
- package/dist/lint/post-synth/wk8505.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +1039 -68
- package/dist/okf/index.md +26 -2
- package/dist/okf/rules/FLUX002.md +0 -1
- package/dist/okf/rules/WK8404.md +11 -0
- package/dist/okf/rules/WK8405.md +17 -0
- package/dist/okf/rules/WK8406.md +11 -0
- package/dist/okf/rules/WK8407.md +15 -0
- package/dist/okf/rules/WK8503.md +17 -0
- package/dist/okf/rules/WK8504.md +11 -0
- package/dist/okf/rules/WK8505.md +15 -0
- package/dist/okf/types/AWSClusterControllerIdentity.md +14 -0
- package/dist/okf/types/AWSManagedCluster.md +14 -0
- package/dist/okf/types/AWSManagedControlPlane.md +14 -0
- package/dist/okf/types/AWSManagedMachinePool.md +14 -0
- package/dist/okf/types/CAPICluster.md +14 -0
- package/dist/okf/types/Certificate.md +1 -0
- package/dist/okf/types/ClusterResourceSet.md +14 -0
- package/dist/okf/types/ClusterServingRuntime.md +14 -0
- package/dist/okf/types/{Cluster.md → CnpgCluster.md} +2 -2
- package/dist/okf/types/DBInstance.md +14 -0
- package/dist/okf/types/{Bucket.md → FluxBucket.md} +2 -6
- package/dist/okf/types/HelmChartProxy.md +14 -0
- package/dist/okf/types/IamRole.md +14 -0
- package/dist/okf/types/InferenceService.md +19 -0
- package/dist/okf/types/InfisicalDynamicSecret.md +4 -0
- package/dist/okf/types/InfisicalSecret.md +4 -0
- package/dist/okf/types/Kustomization.md +1 -0
- package/dist/okf/types/MachinePool.md +14 -0
- package/dist/okf/types/PDB.md +1 -0
- package/dist/okf/types/PodIdentityAssociation.md +14 -0
- package/dist/okf/types/Policy.md +14 -0
- package/dist/okf/types/S3Bucket.md +14 -0
- package/dist/okf/types/ServingRuntime.md +14 -0
- package/dist/okf/types/StatefulSet.md +1 -0
- package/dist/okf/types/User.md +14 -0
- package/dist/op/activities/ensure-secret.d.ts +41 -0
- package/dist/op/activities/ensure-secret.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/kubectl.d.ts +9 -0
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
- package/dist/op/builders.d.ts +69 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/argo-helpers.ts +2 -6
- package/dist/rules/crd-schema-helpers.ts +4 -6
- package/dist/rules/k8s-helpers.ts +87 -24
- package/dist/rules/sops-helpers.ts +76 -0
- package/dist/rules/wk8005.ts +27 -32
- package/dist/rules/wk8006.ts +33 -38
- package/dist/rules/wk8041.ts +19 -24
- package/dist/rules/wk8042.ts +17 -22
- package/dist/rules/wk8101.ts +29 -34
- package/dist/rules/wk8102.ts +13 -18
- package/dist/rules/wk8103.ts +17 -22
- package/dist/rules/wk8104.ts +30 -35
- package/dist/rules/wk8105.ts +16 -21
- package/dist/rules/wk8201.ts +26 -31
- package/dist/rules/wk8202.ts +17 -22
- package/dist/rules/wk8203.ts +17 -22
- package/dist/rules/wk8204.ts +46 -51
- package/dist/rules/wk8205.ts +27 -32
- package/dist/rules/wk8207.ts +14 -19
- package/dist/rules/wk8208.ts +14 -19
- package/dist/rules/wk8209.ts +14 -19
- package/dist/rules/wk8301.ts +25 -30
- package/dist/rules/wk8302.ts +19 -24
- package/dist/rules/wk8303.ts +23 -26
- package/dist/rules/wk8304.ts +35 -40
- package/dist/rules/wk8305.ts +39 -42
- package/dist/rules/wk8306.ts +20 -25
- package/dist/rules/wk8401.ts +59 -64
- package/dist/rules/wk8402.ts +13 -18
- package/dist/rules/wk8403.ts +32 -37
- package/dist/rules/wk8404.ts +49 -0
- package/dist/rules/wk8405.ts +85 -0
- package/dist/rules/wk8406.ts +55 -0
- package/dist/rules/wk8407.ts +66 -0
- package/dist/rules/wk8503.ts +220 -0
- package/dist/rules/wk8504.ts +51 -0
- package/dist/rules/wk8505.ts +63 -0
- package/dist/secret-labels.d.ts +31 -0
- package/dist/secret-labels.d.ts.map +1 -0
- package/dist/secret-store.d.ts +73 -0
- package/dist/secret-store.d.ts.map +1 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/sops/encrypted-secret-file.d.ts +52 -0
- package/dist/sops/encrypted-secret-file.d.ts.map +1 -0
- package/dist/sops/entity.d.ts +31 -0
- package/dist/sops/entity.d.ts.map +1 -0
- package/dist/teardown.d.ts.map +1 -1
- package/dist/types/index.d.ts +261 -9
- package/package.json +4 -4
- package/src/components/kubectl-apply.ts +5 -0
- package/src/composites/composites.test.ts +556 -0
- package/src/composites/flux-app.ts +23 -0
- package/src/composites/index.ts +15 -0
- package/src/composites/inference-service.ts +140 -0
- package/src/composites/model.ts +150 -0
- package/src/composites/operator-stack.ts +385 -0
- package/src/composites/vllm-serving-runtime.test.ts +167 -0
- package/src/composites/vllm-serving-runtime.ts +196 -0
- package/src/crd/cnpg.test.ts +8 -3
- package/src/crd/crd-sources.ts +183 -0
- package/src/crd/parser.test.ts +14 -0
- package/src/deep-observe-hooks.ts +24 -0
- package/src/deep-observe.test.ts +89 -0
- package/src/generated/index.d.ts +261 -9
- package/src/generated/index.ts +167 -40
- package/src/generated/lexicon-k8s.json +1039 -68
- package/src/generated/operations.json +272 -0
- package/src/group-namespace.test.ts +31 -0
- package/src/group-namespace.ts +13 -0
- package/src/index.ts +17 -0
- package/src/kube/apply.ts +5 -0
- package/src/lint/audit-catalog.ts +7 -0
- package/src/lint/post-synth/argo-helpers.ts +2 -6
- package/src/lint/post-synth/crd-schema-helpers.ts +4 -6
- package/src/lint/post-synth/index.ts +14 -0
- package/src/lint/post-synth/k8s-helpers.test.ts +26 -9
- package/src/lint/post-synth/k8s-helpers.ts +87 -24
- package/src/lint/post-synth/post-synth.test.ts +826 -0
- package/src/lint/post-synth/sops-helpers.ts +76 -0
- package/src/lint/post-synth/wk8005.ts +27 -32
- package/src/lint/post-synth/wk8006.ts +33 -38
- package/src/lint/post-synth/wk8041.ts +19 -24
- package/src/lint/post-synth/wk8042.ts +17 -22
- package/src/lint/post-synth/wk8101.ts +29 -34
- package/src/lint/post-synth/wk8102.ts +13 -18
- package/src/lint/post-synth/wk8103.ts +17 -22
- package/src/lint/post-synth/wk8104.ts +30 -35
- package/src/lint/post-synth/wk8105.ts +16 -21
- package/src/lint/post-synth/wk8201.ts +26 -31
- package/src/lint/post-synth/wk8202.ts +17 -22
- package/src/lint/post-synth/wk8203.ts +17 -22
- package/src/lint/post-synth/wk8204.ts +46 -51
- package/src/lint/post-synth/wk8205.ts +27 -32
- package/src/lint/post-synth/wk8207.ts +14 -19
- package/src/lint/post-synth/wk8208.ts +14 -19
- package/src/lint/post-synth/wk8209.ts +14 -19
- package/src/lint/post-synth/wk8301.ts +25 -30
- package/src/lint/post-synth/wk8302.ts +19 -24
- package/src/lint/post-synth/wk8303.ts +23 -26
- package/src/lint/post-synth/wk8304.ts +35 -40
- package/src/lint/post-synth/wk8305.ts +39 -42
- package/src/lint/post-synth/wk8306.ts +20 -25
- package/src/lint/post-synth/wk8401.ts +59 -64
- package/src/lint/post-synth/wk8402.ts +13 -18
- package/src/lint/post-synth/wk8403.ts +32 -37
- package/src/lint/post-synth/wk8404.ts +49 -0
- package/src/lint/post-synth/wk8405.ts +85 -0
- package/src/lint/post-synth/wk8406.ts +55 -0
- package/src/lint/post-synth/wk8407.ts +66 -0
- package/src/lint/post-synth/wk8503.ts +220 -0
- package/src/lint/post-synth/wk8504.ts +51 -0
- package/src/lint/post-synth/wk8505.ts +63 -0
- package/src/op/activities/ensure-secret.ts +71 -0
- package/src/op/activities/index.ts +7 -0
- package/src/op/activities/kubectl.test.ts +32 -0
- package/src/op/activities/kubectl.ts +28 -5
- package/src/op/activities/wait-for-ready.test.ts +34 -0
- package/src/op/activities/wait-for-ready.ts +26 -0
- package/src/op/builders.test.ts +82 -0
- package/src/op/builders.ts +95 -0
- package/src/plugin.ts +21 -2
- package/src/secret-labels.ts +36 -0
- package/src/secret-store.test.ts +184 -0
- package/src/secret-store.ts +165 -0
- package/src/serializer.ts +41 -2
- package/src/sops/encrypted-secret-file.test.ts +333 -0
- package/src/sops/encrypted-secret-file.ts +264 -0
- package/src/sops/entity.ts +53 -0
- package/src/teardown.test.ts +36 -0
- package/src/teardown.ts +14 -0
- package/src/testdata/sops/db-credentials.sops.yaml +23 -0
|
@@ -10,6 +10,9 @@ 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";
|
|
14
|
+
import { OperatorStack, deriveHostVerbClass } from "./operator-stack";
|
|
15
|
+
import type { OpConfig } from "@intentius/chant/op";
|
|
13
16
|
import { CronWorkload } from "./cron-workload";
|
|
14
17
|
import { AutoscaledService } from "./autoscaled-service";
|
|
15
18
|
import { WorkerPool } from "./worker-pool";
|
|
@@ -35,6 +38,7 @@ import { GcePdStorageClass } from "./gce-pd-storage-class";
|
|
|
35
38
|
import { FilestoreStorageClass } from "./filestore-storage-class";
|
|
36
39
|
import { GkeGateway } from "./gke-gateway";
|
|
37
40
|
import { ConfigConnectorContext } from "./config-connector-context";
|
|
41
|
+
import { InferenceService } from "./inference-service";
|
|
38
42
|
|
|
39
43
|
// ── WebApp ──────────────────────────────────────────────────────────
|
|
40
44
|
|
|
@@ -3473,6 +3477,546 @@ describe("FluxAppFor", () => {
|
|
|
3473
3477
|
expect(spec.timeout).toBe("3m");
|
|
3474
3478
|
expect(spec.serviceAccountName).toBe("flux-applier");
|
|
3475
3479
|
});
|
|
3480
|
+
|
|
3481
|
+
test("omits decryption when the option is not set", () => {
|
|
3482
|
+
const spec = p(FluxAppFor("hello", { source: "flux-system", path: "./k8s" }).kustomization).spec as any;
|
|
3483
|
+
expect(spec.decryption).toBeUndefined();
|
|
3484
|
+
});
|
|
3485
|
+
|
|
3486
|
+
test('decryption: "sops" defaults the secretRef to sops-age', () => {
|
|
3487
|
+
const spec = p(
|
|
3488
|
+
FluxAppFor("hello", { source: "flux-system", path: "./k8s", decryption: "sops" }).kustomization,
|
|
3489
|
+
).spec as any;
|
|
3490
|
+
expect(spec.decryption).toEqual({ provider: "sops", secretRef: { name: "sops-age" } });
|
|
3491
|
+
});
|
|
3492
|
+
|
|
3493
|
+
test("decryption object form names a custom secretRef", () => {
|
|
3494
|
+
const spec = p(
|
|
3495
|
+
FluxAppFor("hello", {
|
|
3496
|
+
source: "flux-system",
|
|
3497
|
+
path: "./k8s",
|
|
3498
|
+
decryption: { provider: "sops", secretRef: "my-age-identity" },
|
|
3499
|
+
}).kustomization,
|
|
3500
|
+
).spec as any;
|
|
3501
|
+
expect(spec.decryption).toEqual({ provider: "sops", secretRef: { name: "my-age-identity" } });
|
|
3502
|
+
});
|
|
3503
|
+
|
|
3504
|
+
test("decryption object form with no secretRef still defaults to sops-age", () => {
|
|
3505
|
+
const spec = p(
|
|
3506
|
+
FluxAppFor("hello", { source: "flux-system", path: "./k8s", decryption: { provider: "sops" } }).kustomization,
|
|
3507
|
+
).spec as any;
|
|
3508
|
+
expect(spec.decryption).toEqual({ provider: "sops", secretRef: { name: "sops-age" } });
|
|
3509
|
+
});
|
|
3510
|
+
});
|
|
3511
|
+
|
|
3512
|
+
// ── InferenceService ─────────────────────────────────────────────────
|
|
3513
|
+
|
|
3514
|
+
describe("InferenceService", () => {
|
|
3515
|
+
test("emits a valid InferenceService referencing the runtime and storageUri", () => {
|
|
3516
|
+
const result = InferenceService({
|
|
3517
|
+
name: "llama-3-8b",
|
|
3518
|
+
namespace: "serving",
|
|
3519
|
+
model: "gs://my-models/llama-3-8b/v1",
|
|
3520
|
+
runtime: "vllm-runtime",
|
|
3521
|
+
});
|
|
3522
|
+
expect(result.inferenceService).toBeDefined();
|
|
3523
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3524
|
+
expect(spec.predictor.model.runtime).toBe("vllm-runtime");
|
|
3525
|
+
expect(spec.predictor.model.storageUri).toBe("gs://my-models/llama-3-8b/v1");
|
|
3526
|
+
});
|
|
3527
|
+
|
|
3528
|
+
test("accepts a resolved model ref ({ storageUri }) in place of a raw string", () => {
|
|
3529
|
+
const result = InferenceService({
|
|
3530
|
+
name: "svc",
|
|
3531
|
+
namespace: "ns",
|
|
3532
|
+
model: { storageUri: "s3://bucket/model/v2" },
|
|
3533
|
+
runtime: "vllm-runtime",
|
|
3534
|
+
});
|
|
3535
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3536
|
+
expect(spec.predictor.model.storageUri).toBe("s3://bucket/model/v2");
|
|
3537
|
+
});
|
|
3538
|
+
|
|
3539
|
+
test("modelFormat is omitted unless explicitly set", () => {
|
|
3540
|
+
const result = InferenceService({
|
|
3541
|
+
name: "svc",
|
|
3542
|
+
namespace: "ns",
|
|
3543
|
+
model: "gs://bucket/model",
|
|
3544
|
+
runtime: "vllm-runtime",
|
|
3545
|
+
});
|
|
3546
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3547
|
+
expect(spec.predictor.model.modelFormat).toBeUndefined();
|
|
3548
|
+
});
|
|
3549
|
+
|
|
3550
|
+
test("modelFormat.name flows through when set", () => {
|
|
3551
|
+
const result = InferenceService({
|
|
3552
|
+
name: "svc",
|
|
3553
|
+
namespace: "ns",
|
|
3554
|
+
model: "gs://bucket/model",
|
|
3555
|
+
runtime: "vllm-runtime",
|
|
3556
|
+
modelFormat: "vLLM",
|
|
3557
|
+
});
|
|
3558
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3559
|
+
expect(spec.predictor.model.modelFormat).toEqual({ name: "vLLM" });
|
|
3560
|
+
});
|
|
3561
|
+
|
|
3562
|
+
test("autoscaling bounds (minReplicas/maxReplicas/scaleTarget/scaleMetric) are expressible and present", () => {
|
|
3563
|
+
const result = InferenceService({
|
|
3564
|
+
name: "svc",
|
|
3565
|
+
namespace: "ns",
|
|
3566
|
+
model: "gs://bucket/model",
|
|
3567
|
+
runtime: "vllm-runtime",
|
|
3568
|
+
minReplicas: 1,
|
|
3569
|
+
maxReplicas: 4,
|
|
3570
|
+
scaleTarget: 8,
|
|
3571
|
+
scaleMetric: "concurrency",
|
|
3572
|
+
});
|
|
3573
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3574
|
+
expect(spec.predictor.minReplicas).toBe(1);
|
|
3575
|
+
expect(spec.predictor.maxReplicas).toBe(4);
|
|
3576
|
+
expect(spec.predictor.scaleTarget).toBe(8);
|
|
3577
|
+
expect(spec.predictor.scaleMetric).toBe("concurrency");
|
|
3578
|
+
});
|
|
3579
|
+
|
|
3580
|
+
test("raw-HPA scaleMetric values (cpu/memory/rps) pass through", () => {
|
|
3581
|
+
const result = InferenceService({
|
|
3582
|
+
name: "svc",
|
|
3583
|
+
namespace: "ns",
|
|
3584
|
+
model: "gs://bucket/model",
|
|
3585
|
+
runtime: "vllm-runtime",
|
|
3586
|
+
scaleMetric: "cpu",
|
|
3587
|
+
});
|
|
3588
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3589
|
+
expect(spec.predictor.scaleMetric).toBe("cpu");
|
|
3590
|
+
});
|
|
3591
|
+
|
|
3592
|
+
test("autoscaling fields are absent when not set", () => {
|
|
3593
|
+
const result = InferenceService({
|
|
3594
|
+
name: "svc",
|
|
3595
|
+
namespace: "ns",
|
|
3596
|
+
model: "gs://bucket/model",
|
|
3597
|
+
runtime: "vllm-runtime",
|
|
3598
|
+
});
|
|
3599
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3600
|
+
expect(spec.predictor.minReplicas).toBeUndefined();
|
|
3601
|
+
expect(spec.predictor.maxReplicas).toBeUndefined();
|
|
3602
|
+
expect(spec.predictor.scaleTarget).toBeUndefined();
|
|
3603
|
+
expect(spec.predictor.scaleMetric).toBeUndefined();
|
|
3604
|
+
});
|
|
3605
|
+
|
|
3606
|
+
test("canary traffic split is expressible and present", () => {
|
|
3607
|
+
const result = InferenceService({
|
|
3608
|
+
name: "svc",
|
|
3609
|
+
namespace: "ns",
|
|
3610
|
+
model: "gs://bucket/model",
|
|
3611
|
+
runtime: "vllm-runtime",
|
|
3612
|
+
canaryTrafficPercent: 10,
|
|
3613
|
+
});
|
|
3614
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3615
|
+
expect(spec.predictor.canaryTrafficPercent).toBe(10);
|
|
3616
|
+
});
|
|
3617
|
+
|
|
3618
|
+
test("canary traffic split is absent when not set", () => {
|
|
3619
|
+
const result = InferenceService({
|
|
3620
|
+
name: "svc",
|
|
3621
|
+
namespace: "ns",
|
|
3622
|
+
model: "gs://bucket/model",
|
|
3623
|
+
runtime: "vllm-runtime",
|
|
3624
|
+
});
|
|
3625
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3626
|
+
expect(spec.predictor.canaryTrafficPercent).toBeUndefined();
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
test("includes common labels and metadata", () => {
|
|
3630
|
+
const result = InferenceService({
|
|
3631
|
+
name: "svc",
|
|
3632
|
+
namespace: "ns",
|
|
3633
|
+
model: "gs://bucket/model",
|
|
3634
|
+
runtime: "vllm-runtime",
|
|
3635
|
+
labels: { team: "ml-platform" },
|
|
3636
|
+
});
|
|
3637
|
+
const meta = p(result.inferenceService).metadata as any;
|
|
3638
|
+
expect(meta.name).toBe("svc");
|
|
3639
|
+
expect(meta.namespace).toBe("ns");
|
|
3640
|
+
expect(meta.labels["app.kubernetes.io/name"]).toBe("svc");
|
|
3641
|
+
expect(meta.labels["app.kubernetes.io/managed-by"]).toBe("chant");
|
|
3642
|
+
expect(meta.labels.team).toBe("ml-platform");
|
|
3643
|
+
});
|
|
3644
|
+
|
|
3645
|
+
test("golden: full spec shape with predictor, model, autoscaling, and canary", () => {
|
|
3646
|
+
const result = InferenceService({
|
|
3647
|
+
name: "llama-3-8b",
|
|
3648
|
+
namespace: "serving",
|
|
3649
|
+
model: "gs://my-models/llama-3-8b/v1",
|
|
3650
|
+
runtime: "vllm-runtime",
|
|
3651
|
+
modelFormat: "vLLM",
|
|
3652
|
+
minReplicas: 1,
|
|
3653
|
+
maxReplicas: 4,
|
|
3654
|
+
scaleTarget: 8,
|
|
3655
|
+
scaleMetric: "concurrency",
|
|
3656
|
+
canaryTrafficPercent: 10,
|
|
3657
|
+
});
|
|
3658
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3659
|
+
expect(spec).toEqual({
|
|
3660
|
+
predictor: {
|
|
3661
|
+
model: {
|
|
3662
|
+
runtime: "vllm-runtime",
|
|
3663
|
+
storageUri: "gs://my-models/llama-3-8b/v1",
|
|
3664
|
+
modelFormat: { name: "vLLM" },
|
|
3665
|
+
},
|
|
3666
|
+
minReplicas: 1,
|
|
3667
|
+
maxReplicas: 4,
|
|
3668
|
+
scaleTarget: 8,
|
|
3669
|
+
scaleMetric: "concurrency",
|
|
3670
|
+
canaryTrafficPercent: 10,
|
|
3671
|
+
},
|
|
3672
|
+
});
|
|
3673
|
+
});
|
|
3674
|
+
|
|
3675
|
+
test("defaults passthrough merges into the InferenceService", () => {
|
|
3676
|
+
const result = InferenceService({
|
|
3677
|
+
name: "svc",
|
|
3678
|
+
namespace: "ns",
|
|
3679
|
+
model: "gs://bucket/model",
|
|
3680
|
+
runtime: "vllm-runtime",
|
|
3681
|
+
defaults: {
|
|
3682
|
+
inferenceService: { spec: { predictor: { model: { protocolVersion: "v2" } } } },
|
|
3683
|
+
},
|
|
3684
|
+
});
|
|
3685
|
+
const spec = p(result.inferenceService).spec as any;
|
|
3686
|
+
expect(spec.predictor.model.protocolVersion).toBe("v2");
|
|
3687
|
+
expect(spec.predictor.model.runtime).toBe("vllm-runtime");
|
|
3688
|
+
});
|
|
3689
|
+
});
|
|
3690
|
+
|
|
3691
|
+
// ── Model ─────────────────────────────────────────────────────────
|
|
3692
|
+
|
|
3693
|
+
describe("Model", () => {
|
|
3694
|
+
test("resolves a gcs source to a gs:// storageUri pinned to version", () => {
|
|
3695
|
+
const result = Model({ id: "llama-3-8b", version: "2024-07-01", source: "gcs" });
|
|
3696
|
+
expect(result.storageUri).toBe("gs://llama-3-8b/2024-07-01");
|
|
3697
|
+
});
|
|
3698
|
+
|
|
3699
|
+
test("resolves an s3 source to an s3:// storageUri pinned to version", () => {
|
|
3700
|
+
const result = Model({ id: "llama-3-8b", version: "2024-07-01", source: "s3" });
|
|
3701
|
+
expect(result.storageUri).toBe("s3://llama-3-8b/2024-07-01");
|
|
3702
|
+
});
|
|
3703
|
+
|
|
3704
|
+
test("resolves a pvc source to a pvc:// storageUri pinned to version", () => {
|
|
3705
|
+
const result = Model({ id: "llama-3-8b", version: "2024-07-01", source: "pvc" });
|
|
3706
|
+
expect(result.storageUri).toBe("pvc://llama-3-8b/2024-07-01");
|
|
3707
|
+
});
|
|
3708
|
+
|
|
3709
|
+
test("resolves an hf source to an hf:// storageUri pinned to version", () => {
|
|
3710
|
+
const result = Model({ id: "llama-3-8b", version: "2024-07-01", source: "hf" });
|
|
3711
|
+
expect(result.storageUri).toBe("hf://llama-3-8b/2024-07-01");
|
|
3712
|
+
});
|
|
3713
|
+
|
|
3714
|
+
test("an explicit uri overrides the id-derived path but version is still appended", () => {
|
|
3715
|
+
const result = Model({
|
|
3716
|
+
id: "llama-3-8b",
|
|
3717
|
+
version: "2024-07-01",
|
|
3718
|
+
source: "gcs",
|
|
3719
|
+
uri: "my-models-bucket/llama-3-8b-instruct",
|
|
3720
|
+
});
|
|
3721
|
+
expect(result.storageUri).toBe("gs://my-models-bucket/llama-3-8b-instruct/2024-07-01");
|
|
3722
|
+
});
|
|
3723
|
+
|
|
3724
|
+
test("no cache PVC by default", () => {
|
|
3725
|
+
const result = Model({ id: "llama-3-8b", version: "2024-07-01", source: "gcs" });
|
|
3726
|
+
expect(result.cache).toBeUndefined();
|
|
3727
|
+
});
|
|
3728
|
+
|
|
3729
|
+
test("emits a cache PVC named `${id}-${version}` when cache is set", () => {
|
|
3730
|
+
const result = Model({
|
|
3731
|
+
id: "llama-3-8b",
|
|
3732
|
+
version: "2024-07-01",
|
|
3733
|
+
source: "gcs",
|
|
3734
|
+
cache: { storageClass: "premium-rwo", size: "200Gi" },
|
|
3735
|
+
});
|
|
3736
|
+
expect(result.cache).toBeDefined();
|
|
3737
|
+
expect((p(result.cache) as any).metadata.name).toBe("llama-3-8b-2024-07-01");
|
|
3738
|
+
expect((p(result.cache) as any).spec.storageClassName).toBe("premium-rwo");
|
|
3739
|
+
expect((p(result.cache) as any).spec.resources.requests.storage).toBe("200Gi");
|
|
3740
|
+
});
|
|
3741
|
+
|
|
3742
|
+
test("resolveModelStorageUri is the same resolution the composite uses", () => {
|
|
3743
|
+
expect(resolveModelStorageUri({ id: "m", version: "v1", source: "s3" })).toBe("s3://m/v1");
|
|
3744
|
+
});
|
|
3745
|
+
});
|
|
3746
|
+
|
|
3747
|
+
// ── OperatorStack ────────────────────────────────────────────────────────
|
|
3748
|
+
|
|
3749
|
+
const READ_ONLY_OP: Pick<OpConfig, "phases" | "onFailure"> = {
|
|
3750
|
+
phases: [{ name: "check", steps: [{ kind: "activity", fn: "lifecycleDiff" }] }],
|
|
3751
|
+
};
|
|
3752
|
+
const MUTATING_OP: Pick<OpConfig, "phases" | "onFailure"> = {
|
|
3753
|
+
phases: [{ name: "apply", steps: [{ kind: "activity", fn: "kubectlApply" }] }],
|
|
3754
|
+
};
|
|
3755
|
+
const DESTRUCTIVE_OP: Pick<OpConfig, "phases" | "onFailure"> = {
|
|
3756
|
+
phases: [{ name: "teardown", steps: [{ kind: "activity", fn: "envTeardown" }] }],
|
|
3757
|
+
};
|
|
3758
|
+
|
|
3759
|
+
describe("OperatorStack", () => {
|
|
3760
|
+
test("returns a namespace and one CronJob/ServiceAccount/Role/RoleBinding per hosted ConvergeOp", () => {
|
|
3761
|
+
const result: any = OperatorStack({
|
|
3762
|
+
name: "chant-operator",
|
|
3763
|
+
image: "chant:latest",
|
|
3764
|
+
converge: [
|
|
3765
|
+
{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging" },
|
|
3766
|
+
{ name: "fountain-apply", schedule: "*/10 * * * *", env: "staging", dial: "apply", dispatchTargets: [MUTATING_OP] },
|
|
3767
|
+
],
|
|
3768
|
+
});
|
|
3769
|
+
expect(result.namespace).toBeDefined();
|
|
3770
|
+
expect(result["cronJob_fountain-observe"]).toBeDefined();
|
|
3771
|
+
expect(result["cronJob_fountain-apply"]).toBeDefined();
|
|
3772
|
+
expect(result["serviceAccount_fountain-observe"]).toBeDefined();
|
|
3773
|
+
expect(result["serviceAccount_fountain-apply"]).toBeDefined();
|
|
3774
|
+
expect(result["role_fountain-observe"]).toBeDefined();
|
|
3775
|
+
expect(result["role_fountain-apply"]).toBeDefined();
|
|
3776
|
+
expect(result["roleBinding_fountain-observe"]).toBeDefined();
|
|
3777
|
+
expect(result["roleBinding_fountain-apply"]).toBeDefined();
|
|
3778
|
+
});
|
|
3779
|
+
|
|
3780
|
+
test("namespace name defaults to the stack name", () => {
|
|
3781
|
+
const result: any = OperatorStack({
|
|
3782
|
+
name: "chant-operator",
|
|
3783
|
+
image: "chant:latest",
|
|
3784
|
+
converge: [{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging" }],
|
|
3785
|
+
});
|
|
3786
|
+
expect((p(result.namespace).metadata as any).name).toBe("chant-operator");
|
|
3787
|
+
});
|
|
3788
|
+
|
|
3789
|
+
test("namespace name is overridable", () => {
|
|
3790
|
+
const result: any = OperatorStack({
|
|
3791
|
+
name: "chant-operator",
|
|
3792
|
+
namespace: "chant-ops",
|
|
3793
|
+
image: "chant:latest",
|
|
3794
|
+
converge: [{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging" }],
|
|
3795
|
+
});
|
|
3796
|
+
expect((p(result.namespace).metadata as any).name).toBe("chant-ops");
|
|
3797
|
+
expect((p(result["cronJob_fountain-observe"]).metadata as any).namespace).toBe("chant-ops");
|
|
3798
|
+
});
|
|
3799
|
+
|
|
3800
|
+
test("CronJob schedule passes through from the hosted ConvergeOp", () => {
|
|
3801
|
+
const result: any = OperatorStack({
|
|
3802
|
+
name: "chant-operator",
|
|
3803
|
+
image: "chant:latest",
|
|
3804
|
+
converge: [{ name: "fountain-observe", schedule: "17 * * * *", env: "staging" }],
|
|
3805
|
+
});
|
|
3806
|
+
expect((p(result["cronJob_fountain-observe"]).spec as any).schedule).toBe("17 * * * *");
|
|
3807
|
+
});
|
|
3808
|
+
|
|
3809
|
+
test("CronJob uses concurrencyPolicy Forbid — never queue a tick behind one in flight", () => {
|
|
3810
|
+
const result: any = OperatorStack({
|
|
3811
|
+
name: "chant-operator",
|
|
3812
|
+
image: "chant:latest",
|
|
3813
|
+
converge: [{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging" }],
|
|
3814
|
+
});
|
|
3815
|
+
expect((p(result["cronJob_fountain-observe"]).spec as any).concurrencyPolicy).toBe("Forbid");
|
|
3816
|
+
});
|
|
3817
|
+
|
|
3818
|
+
test("CronJob container command defaults to `chant run <name>`", () => {
|
|
3819
|
+
const result: any = OperatorStack({
|
|
3820
|
+
name: "chant-operator",
|
|
3821
|
+
image: "chant:latest",
|
|
3822
|
+
converge: [{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging" }],
|
|
3823
|
+
});
|
|
3824
|
+
const container = (p(result["cronJob_fountain-observe"]).spec as any).jobTemplate.spec.template.spec.containers[0];
|
|
3825
|
+
expect(container.image).toBe("chant:latest");
|
|
3826
|
+
expect(container.command).toEqual(["chant", "run", "fountain-observe"]);
|
|
3827
|
+
expect(container.env).toEqual([
|
|
3828
|
+
{ name: "CHANT_CONVERGE_ENV", value: "staging" },
|
|
3829
|
+
{ name: "CHANT_CONVERGE_DIAL", value: "observe" },
|
|
3830
|
+
]);
|
|
3831
|
+
});
|
|
3832
|
+
|
|
3833
|
+
test("CronJob container command is overridable per host", () => {
|
|
3834
|
+
const result: any = OperatorStack({
|
|
3835
|
+
name: "chant-operator",
|
|
3836
|
+
image: "chant:latest",
|
|
3837
|
+
converge: [{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging", command: ["chant", "operator", "tick", "fountain-observe"] }],
|
|
3838
|
+
});
|
|
3839
|
+
const container = (p(result["cronJob_fountain-observe"]).spec as any).jobTemplate.spec.template.spec.containers[0];
|
|
3840
|
+
expect(container.command).toEqual(["chant", "operator", "tick", "fountain-observe"]);
|
|
3841
|
+
});
|
|
3842
|
+
|
|
3843
|
+
test("ServiceAccount is bound to the CronJob's pod spec", () => {
|
|
3844
|
+
const result: any = OperatorStack({
|
|
3845
|
+
name: "chant-operator",
|
|
3846
|
+
image: "chant:latest",
|
|
3847
|
+
converge: [{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging" }],
|
|
3848
|
+
});
|
|
3849
|
+
const podSpec = (p(result["cronJob_fountain-observe"]).spec as any).jobTemplate.spec.template.spec;
|
|
3850
|
+
expect(podSpec.serviceAccountName).toBe("fountain-observe-sa");
|
|
3851
|
+
expect((p(result["serviceAccount_fountain-observe"]).metadata as any).name).toBe("fountain-observe-sa");
|
|
3852
|
+
const binding = p(result["roleBinding_fountain-observe"]) as any;
|
|
3853
|
+
expect(binding.subjects[0]).toEqual({ kind: "ServiceAccount", name: "fountain-observe-sa", namespace: "chant-operator" });
|
|
3854
|
+
expect(binding.roleRef).toEqual({ apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "fountain-observe-role" });
|
|
3855
|
+
});
|
|
3856
|
+
|
|
3857
|
+
// ── RBAC derivation ──────────────────────────────────────────────────
|
|
3858
|
+
|
|
3859
|
+
test("a ConvergeOp with no dispatch targets gets read-only RBAC", () => {
|
|
3860
|
+
const result: any = OperatorStack({
|
|
3861
|
+
name: "chant-operator",
|
|
3862
|
+
image: "chant:latest",
|
|
3863
|
+
converge: [{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging" }],
|
|
3864
|
+
});
|
|
3865
|
+
const role = p(result["role_fountain-observe"]) as any;
|
|
3866
|
+
for (const rule of role.rules) {
|
|
3867
|
+
expect(rule.verbs).toEqual(["get", "list", "watch"]);
|
|
3868
|
+
}
|
|
3869
|
+
});
|
|
3870
|
+
|
|
3871
|
+
test("observe-dial ConvergeOp with a mutating dispatch target still gets read-only RBAC — observe never dispatches, per TMP014/convergeTick", () => {
|
|
3872
|
+
const result: any = OperatorStack({
|
|
3873
|
+
name: "chant-operator",
|
|
3874
|
+
image: "chant:latest",
|
|
3875
|
+
converge: [{ name: "fountain-watch", schedule: "*/10 * * * *", env: "staging", dial: "observe", dispatchTargets: [MUTATING_OP] }],
|
|
3876
|
+
});
|
|
3877
|
+
const role = p(result["role_fountain-watch"]) as any;
|
|
3878
|
+
for (const rule of role.rules) {
|
|
3879
|
+
expect(rule.verbs).toEqual(["get", "list", "watch"]);
|
|
3880
|
+
}
|
|
3881
|
+
});
|
|
3882
|
+
|
|
3883
|
+
test("reconcile-dial ConvergeOp with a mutating dispatch target still gets read-only RBAC — TMP014 only lets apply free-run a mutating dispatch", () => {
|
|
3884
|
+
const result: any = OperatorStack({
|
|
3885
|
+
name: "chant-operator",
|
|
3886
|
+
image: "chant:latest",
|
|
3887
|
+
converge: [{ name: "fountain-reconcile", schedule: "*/10 * * * *", env: "staging", dial: "reconcile", dispatchTargets: [MUTATING_OP] }],
|
|
3888
|
+
});
|
|
3889
|
+
const role = p(result["role_fountain-reconcile"]) as any;
|
|
3890
|
+
for (const rule of role.rules) {
|
|
3891
|
+
expect(rule.verbs).toEqual(["get", "list", "watch"]);
|
|
3892
|
+
}
|
|
3893
|
+
});
|
|
3894
|
+
|
|
3895
|
+
test("apply-dial ConvergeOp with a mutating dispatch target gets create/update/patch, never delete or wildcard", () => {
|
|
3896
|
+
const result: any = OperatorStack({
|
|
3897
|
+
name: "chant-operator",
|
|
3898
|
+
image: "chant:latest",
|
|
3899
|
+
converge: [{ name: "fountain-apply", schedule: "*/10 * * * *", env: "staging", dial: "apply", dispatchTargets: [MUTATING_OP] }],
|
|
3900
|
+
});
|
|
3901
|
+
const role = p(result["role_fountain-apply"]) as any;
|
|
3902
|
+
expect(role.rules.length).toBeGreaterThan(0);
|
|
3903
|
+
for (const rule of role.rules) {
|
|
3904
|
+
expect(rule.verbs).toEqual(["get", "list", "watch", "create", "update", "patch"]);
|
|
3905
|
+
expect(rule.verbs).not.toContain("delete");
|
|
3906
|
+
expect(rule.verbs).not.toContain("deletecollection");
|
|
3907
|
+
expect(rule.verbs).not.toContain("*");
|
|
3908
|
+
expect(rule.resources).not.toContain("*");
|
|
3909
|
+
expect(rule.apiGroups).not.toContain("*");
|
|
3910
|
+
}
|
|
3911
|
+
});
|
|
3912
|
+
|
|
3913
|
+
test("apply-dial ConvergeOp with only a read-only dispatch target stays read-only — least privilege, not dial-implied ceiling", () => {
|
|
3914
|
+
const result: any = OperatorStack({
|
|
3915
|
+
name: "chant-operator",
|
|
3916
|
+
image: "chant:latest",
|
|
3917
|
+
converge: [{ name: "fountain-verify", schedule: "*/10 * * * *", env: "staging", dial: "apply", dispatchTargets: [READ_ONLY_OP] }],
|
|
3918
|
+
});
|
|
3919
|
+
const role = p(result["role_fountain-verify"]) as any;
|
|
3920
|
+
for (const rule of role.rules) {
|
|
3921
|
+
expect(rule.verbs).toEqual(["get", "list", "watch"]);
|
|
3922
|
+
}
|
|
3923
|
+
});
|
|
3924
|
+
|
|
3925
|
+
test("a destructive dispatch target is refused at construction, even under dial apply", () => {
|
|
3926
|
+
expect(() =>
|
|
3927
|
+
OperatorStack({
|
|
3928
|
+
name: "chant-operator",
|
|
3929
|
+
image: "chant:latest",
|
|
3930
|
+
converge: [{ name: "fountain-teardown", schedule: "*/10 * * * *", env: "staging", dial: "apply", dispatchTargets: [DESTRUCTIVE_OP] }],
|
|
3931
|
+
}),
|
|
3932
|
+
).toThrow(/destructive/);
|
|
3933
|
+
});
|
|
3934
|
+
|
|
3935
|
+
test("two hosts in the same stack get independently scoped RBAC — never the union", () => {
|
|
3936
|
+
const result: any = OperatorStack({
|
|
3937
|
+
name: "chant-operator",
|
|
3938
|
+
image: "chant:latest",
|
|
3939
|
+
converge: [
|
|
3940
|
+
{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging", dial: "observe" },
|
|
3941
|
+
{ name: "fountain-apply", schedule: "*/10 * * * *", env: "staging", dial: "apply", dispatchTargets: [MUTATING_OP] },
|
|
3942
|
+
],
|
|
3943
|
+
});
|
|
3944
|
+
expect((p(result["role_fountain-observe"]) as any).rules[0].verbs).toEqual(["get", "list", "watch"]);
|
|
3945
|
+
expect((p(result["role_fountain-apply"]) as any).rules[0].verbs).toEqual(["get", "list", "watch", "create", "update", "patch"]);
|
|
3946
|
+
});
|
|
3947
|
+
|
|
3948
|
+
test("custom resource rules are used instead of the default set", () => {
|
|
3949
|
+
const result: any = OperatorStack({
|
|
3950
|
+
name: "chant-operator",
|
|
3951
|
+
image: "chant:latest",
|
|
3952
|
+
converge: [{
|
|
3953
|
+
name: "fountain-secrets",
|
|
3954
|
+
schedule: "*/10 * * * *",
|
|
3955
|
+
env: "staging",
|
|
3956
|
+
resources: [{ apiGroups: [""], resources: ["secrets"] }],
|
|
3957
|
+
}],
|
|
3958
|
+
});
|
|
3959
|
+
const role = p(result["role_fountain-secrets"]) as any;
|
|
3960
|
+
expect(role.rules).toEqual([{ apiGroups: [""], resources: ["secrets"], verbs: ["get", "list", "watch"] }]);
|
|
3961
|
+
});
|
|
3962
|
+
|
|
3963
|
+
// ── deriveHostVerbClass (unit) ───────────────────────────────────────
|
|
3964
|
+
|
|
3965
|
+
test("deriveHostVerbClass: reconcile dial with a mutating target stays read-only", () => {
|
|
3966
|
+
expect(deriveHostVerbClass("x", "reconcile", [MUTATING_OP])).toBe("read-only");
|
|
3967
|
+
});
|
|
3968
|
+
|
|
3969
|
+
test("deriveHostVerbClass: apply dial with a mutating target elevates to mutating", () => {
|
|
3970
|
+
expect(deriveHostVerbClass("x", "apply", [MUTATING_OP])).toBe("mutating");
|
|
3971
|
+
});
|
|
3972
|
+
|
|
3973
|
+
test("deriveHostVerbClass: apply dial with only read-only targets stays read-only", () => {
|
|
3974
|
+
expect(deriveHostVerbClass("x", "apply", [READ_ONLY_OP])).toBe("read-only");
|
|
3975
|
+
});
|
|
3976
|
+
|
|
3977
|
+
test("deriveHostVerbClass: a destructive target throws under any dial", () => {
|
|
3978
|
+
expect(() => deriveHostVerbClass("x", "apply", [DESTRUCTIVE_OP])).toThrow(/destructive/);
|
|
3979
|
+
expect(() => deriveHostVerbClass("x", "observe", [DESTRUCTIVE_OP])).toThrow(/destructive/);
|
|
3980
|
+
});
|
|
3981
|
+
|
|
3982
|
+
// ── Build-time refusals ──────────────────────────────────────────────
|
|
3983
|
+
|
|
3984
|
+
test("refuses an empty converge array", () => {
|
|
3985
|
+
expect(() => OperatorStack({ name: "chant-operator", image: "chant:latest", converge: [] })).toThrow(/at least one/);
|
|
3986
|
+
});
|
|
3987
|
+
|
|
3988
|
+
test("refuses duplicate hosted ConvergeOp names", () => {
|
|
3989
|
+
expect(() =>
|
|
3990
|
+
OperatorStack({
|
|
3991
|
+
name: "chant-operator",
|
|
3992
|
+
image: "chant:latest",
|
|
3993
|
+
converge: [
|
|
3994
|
+
{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging" },
|
|
3995
|
+
{ name: "fountain-observe", schedule: "*/5 * * * *", env: "staging" },
|
|
3996
|
+
],
|
|
3997
|
+
}),
|
|
3998
|
+
).toThrow(/duplicate/);
|
|
3999
|
+
});
|
|
4000
|
+
|
|
4001
|
+
test("refuses a host with a blank schedule", () => {
|
|
4002
|
+
expect(() =>
|
|
4003
|
+
OperatorStack({
|
|
4004
|
+
name: "chant-operator",
|
|
4005
|
+
image: "chant:latest",
|
|
4006
|
+
converge: [{ name: "fountain-observe", schedule: "", env: "staging" }],
|
|
4007
|
+
}),
|
|
4008
|
+
).toThrow(/schedule/);
|
|
4009
|
+
});
|
|
4010
|
+
|
|
4011
|
+
test("refuses a blank image", () => {
|
|
4012
|
+
expect(() =>
|
|
4013
|
+
OperatorStack({
|
|
4014
|
+
name: "chant-operator",
|
|
4015
|
+
image: "",
|
|
4016
|
+
converge: [{ name: "fountain-observe", schedule: "*/10 * * * *", env: "staging" }],
|
|
4017
|
+
}),
|
|
4018
|
+
).toThrow(/image/);
|
|
4019
|
+
});
|
|
3476
4020
|
});
|
|
3477
4021
|
|
|
3478
4022
|
describe("package index re-exports (regression guard)", () => {
|
|
@@ -3488,4 +4032,16 @@ describe("package index re-exports (regression guard)", () => {
|
|
|
3488
4032
|
expect(typeof pkg.FluxGitSource).toBe("function");
|
|
3489
4033
|
expect(typeof pkg.FluxAppFor).toBe("function");
|
|
3490
4034
|
});
|
|
4035
|
+
|
|
4036
|
+
test("Model is reachable from the package entry", async () => {
|
|
4037
|
+
const pkg: any = await import("../index");
|
|
4038
|
+
expect(typeof pkg.Model).toBe("function");
|
|
4039
|
+
expect(typeof pkg.resolveModelStorageUri).toBe("function");
|
|
4040
|
+
});
|
|
4041
|
+
|
|
4042
|
+
test("OperatorStack is reachable from the package entry", async () => {
|
|
4043
|
+
const pkg: any = await import("../index");
|
|
4044
|
+
expect(typeof pkg.OperatorStack).toBe("function");
|
|
4045
|
+
expect(typeof pkg.deriveHostVerbClass).toBe("function");
|
|
4046
|
+
});
|
|
3491
4047
|
});
|
|
@@ -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
|
|
package/src/composites/index.ts
CHANGED
|
@@ -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,11 @@ 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";
|
|
116
|
+
export { OperatorStack, deriveHostVerbClass, DEFAULT_RESOURCE_RULES } from "./operator-stack";
|
|
117
|
+
export type {
|
|
118
|
+
OperatorDial, OperatorRbacResourceRule, OperatorStackConvergeHost, OperatorStackConfig, OperatorStackResult,
|
|
119
|
+
} from "./operator-stack";
|