@intentius/chant-lexicon-k8s 0.49.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/composites/index.d.ts +2 -0
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/operator-stack.d.ts +183 -0
- package/dist/composites/operator-stack.d.ts.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +35 -35
- 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/k8s-helpers.d.ts +28 -5
- package/dist/lint/post-synth/k8s-helpers.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/lint/post-synth/wk8405.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8406.d.ts.map +1 -1
- package/dist/lint/post-synth/wk8407.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/op/builders.d.ts +69 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/rules/argo-helpers.ts +2 -6
- package/dist/rules/crd-schema-helpers.ts +4 -6
- package/dist/rules/k8s-helpers.ts +32 -23
- 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 +15 -21
- package/dist/rules/wk8405.ts +15 -18
- package/dist/rules/wk8406.ts +29 -34
- package/dist/rules/wk8407.ts +17 -22
- package/dist/serializer.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/composites/composites.test.ts +283 -0
- package/src/composites/index.ts +4 -0
- package/src/composites/operator-stack.ts +385 -0
- package/src/index.ts +11 -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/k8s-helpers.test.ts +26 -9
- package/src/lint/post-synth/k8s-helpers.ts +32 -23
- 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 +15 -21
- package/src/lint/post-synth/wk8405.ts +15 -18
- package/src/lint/post-synth/wk8406.ts +29 -34
- package/src/lint/post-synth/wk8407.ts +17 -22
- package/src/op/builders.test.ts +82 -0
- package/src/op/builders.ts +95 -0
- package/src/serializer.ts +9 -1
|
@@ -11,6 +11,8 @@ import { StatefulApp } from "./stateful-app";
|
|
|
11
11
|
import { ArgoAppFor, ArgoAppSetForRegions, registerArgoCluster } from "./argo-app";
|
|
12
12
|
import { FluxGitSource, FluxAppFor } from "./flux-app";
|
|
13
13
|
import { Model, resolveModelStorageUri } from "./model";
|
|
14
|
+
import { OperatorStack, deriveHostVerbClass } from "./operator-stack";
|
|
15
|
+
import type { OpConfig } from "@intentius/chant/op";
|
|
14
16
|
import { CronWorkload } from "./cron-workload";
|
|
15
17
|
import { AutoscaledService } from "./autoscaled-service";
|
|
16
18
|
import { WorkerPool } from "./worker-pool";
|
|
@@ -3742,6 +3744,281 @@ describe("Model", () => {
|
|
|
3742
3744
|
});
|
|
3743
3745
|
});
|
|
3744
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
|
+
});
|
|
4020
|
+
});
|
|
4021
|
+
|
|
3745
4022
|
describe("package index re-exports (regression guard)", () => {
|
|
3746
4023
|
test("Argo composites are reachable from the package entry", async () => {
|
|
3747
4024
|
const pkg: any = await import("../index");
|
|
@@ -3761,4 +4038,10 @@ describe("package index re-exports (regression guard)", () => {
|
|
|
3761
4038
|
expect(typeof pkg.Model).toBe("function");
|
|
3762
4039
|
expect(typeof pkg.resolveModelStorageUri).toBe("function");
|
|
3763
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
|
+
});
|
|
3764
4047
|
});
|
package/src/composites/index.ts
CHANGED
|
@@ -113,3 +113,7 @@ export { VllmServingRuntime } from "./vllm-serving-runtime";
|
|
|
113
113
|
export type { VllmServingRuntimeProps, VllmServingRuntimeResult, VllmResourceSpec } from "./vllm-serving-runtime";
|
|
114
114
|
export { Model, resolveModelStorageUri } from "./model";
|
|
115
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";
|