@intentius/chant-lexicon-k8s 0.46.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.
- 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 +6 -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/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 +13 -5
- package/dist/kube/apply.d.ts.map +1 -1
- package/dist/lint/audit-catalog.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 +28 -1
- 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/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/plugin.d.ts.map +1 -1
- package/dist/rules/k8s-helpers.ts +55 -1
- package/dist/rules/sops-helpers.ts +76 -0
- package/dist/rules/wk8404.ts +55 -0
- package/dist/rules/wk8405.ts +88 -0
- package/dist/rules/wk8406.ts +60 -0
- package/dist/rules/wk8407.ts +71 -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 +273 -0
- package/src/composites/flux-app.ts +23 -0
- package/src/composites/index.ts +11 -0
- package/src/composites/inference-service.ts +140 -0
- package/src/composites/model.ts +150 -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 +6 -0
- package/src/kube/apply.ts +5 -0
- package/src/lint/audit-catalog.ts +7 -0
- package/src/lint/post-synth/index.ts +14 -0
- package/src/lint/post-synth/k8s-helpers.ts +55 -1
- 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/wk8404.ts +55 -0
- package/src/lint/post-synth/wk8405.ts +88 -0
- package/src/lint/post-synth/wk8406.ts +60 -0
- package/src/lint/post-synth/wk8407.ts +71 -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/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 +33 -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,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
|
|
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,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
|
+
}
|