@intentius/chant-lexicon-cpln 0.53.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/README.md +85 -0
- package/dist/api.d.ts +77 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +17 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +6 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate-lexicon.d.ts +36 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -0
- package/dist/codegen/generate-typescript.d.ts +11 -0
- package/dist/codegen/generate-typescript.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +15 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/naming.d.ts +30 -0
- package/dist/codegen/naming.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +17 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/composites/cron-job.d.ts +60 -0
- package/dist/composites/cron-job.d.ts.map +1 -0
- package/dist/composites/gvc-environment.d.ts +47 -0
- package/dist/composites/gvc-environment.d.ts.map +1 -0
- package/dist/composites/index.d.ts +13 -0
- package/dist/composites/index.d.ts.map +1 -0
- package/dist/composites/public-domain.d.ts +68 -0
- package/dist/composites/public-domain.d.ts.map +1 -0
- package/dist/composites/secret-access.d.ts +78 -0
- package/dist/composites/secret-access.d.ts.map +1 -0
- package/dist/composites/serverless-service.d.ts +78 -0
- package/dist/composites/serverless-service.d.ts.map +1 -0
- package/dist/composites/stateful-service.d.ts +79 -0
- package/dist/composites/stateful-service.d.ts.map +1 -0
- package/dist/coverage.d.ts +59 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +71 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/detect.d.ts +14 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/entity-props.d.ts +34 -0
- package/dist/entity-props.d.ts.map +1 -0
- package/dist/generated/index.d.ts +151 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/runtime.d.ts +5 -0
- package/dist/generated/runtime.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/init-templates.d.ts +16 -0
- package/dist/init-templates.d.ts.map +1 -0
- package/dist/integrity.json +35 -0
- package/dist/kinds.d.ts +70 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/lint/audit-catalog.d.ts +19 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts +12 -0
- package/dist/lint/post-synth/cpl010-unrestricted-outbound.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts +11 -0
- package/dist/lint/post-synth/cpl011-internal-firewall-scope.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts +11 -0
- package/dist/lint/post-synth/cpl012-no-literal-credential-env.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts +11 -0
- package/dist/lint/post-synth/cpl013-qualified-identity-principal.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts +14 -0
- package/dist/lint/post-synth/cpl014-qualified-secret-ref.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts +10 -0
- package/dist/lint/post-synth/cpl020-serverless-single-port.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts +11 -0
- package/dist/lint/post-synth/cpl021-cron-job-shape.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl022-container-ports.d.ts +10 -0
- package/dist/lint/post-synth/cpl022-container-ports.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl023-container-resources.d.ts +12 -0
- package/dist/lint/post-synth/cpl023-container-resources.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl024-probe-handler.d.ts +10 -0
- package/dist/lint/post-synth/cpl024-probe-handler.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts +11 -0
- package/dist/lint/post-synth/cpl025-autoscaling-shape.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts +11 -0
- package/dist/lint/post-synth/cpl026-scale-to-zero.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts +13 -0
- package/dist/lint/post-synth/cpl027-capacity-ai-conflict.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl028-volume-set.d.ts +3 -0
- package/dist/lint/post-synth/cpl028-volume-set.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl029-link-targets.d.ts +3 -0
- package/dist/lint/post-synth/cpl029-link-targets.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl030-domain-routing.d.ts +3 -0
- package/dist/lint/post-synth/cpl030-domain-routing.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl040-pinned-image.d.ts +11 -0
- package/dist/lint/post-synth/cpl040-pinned-image.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl041-image-reference-form.d.ts +12 -0
- package/dist/lint/post-synth/cpl041-image-reference-form.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl042-gvc-placement.d.ts +10 -0
- package/dist/lint/post-synth/cpl042-gvc-placement.d.ts.map +1 -0
- package/dist/lint/post-synth/cpl043-policy-scope.d.ts +16 -0
- package/dist/lint/post-synth/cpl043-policy-scope.d.ts.map +1 -0
- package/dist/lint/post-synth/helpers.d.ts +45 -0
- package/dist/lint/post-synth/helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/rules/cpl001-no-secret-literals.d.ts +3 -0
- package/dist/lint/rules/cpl001-no-secret-literals.d.ts.map +1 -0
- package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts +3 -0
- package/dist/lint/rules/cpl002-prefer-resource-reference.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +6 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +7 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +4 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +6 -0
- package/dist/meta.json +2110 -0
- package/dist/okf/index.md +39 -0
- package/dist/okf/rules/CPL001.md +22 -0
- package/dist/okf/rules/CPL002.md +22 -0
- package/dist/okf/rules/CPL010.md +15 -0
- package/dist/okf/rules/CPL011.md +15 -0
- package/dist/okf/rules/CPL012.md +15 -0
- package/dist/okf/rules/CPL013.md +15 -0
- package/dist/okf/rules/CPL014.md +16 -0
- package/dist/okf/rules/CPL020.md +11 -0
- package/dist/okf/rules/CPL021.md +15 -0
- package/dist/okf/rules/CPL022.md +15 -0
- package/dist/okf/rules/CPL023.md +15 -0
- package/dist/okf/rules/CPL024.md +15 -0
- package/dist/okf/rules/CPL025.md +15 -0
- package/dist/okf/rules/CPL026.md +15 -0
- package/dist/okf/rules/CPL027.md +15 -0
- package/dist/okf/rules/CPL028.md +15 -0
- package/dist/okf/rules/CPL029.md +15 -0
- package/dist/okf/rules/CPL030.md +16 -0
- package/dist/okf/rules/CPL040.md +15 -0
- package/dist/okf/rules/CPL041.md +15 -0
- package/dist/okf/rules/CPL042.md +11 -0
- package/dist/okf/rules/CPL043.md +15 -0
- package/dist/okf/types/Domain.md +22 -0
- package/dist/okf/types/Gvc.md +21 -0
- package/dist/okf/types/Identity.md +29 -0
- package/dist/okf/types/IpSet.md +21 -0
- package/dist/okf/types/Policy.md +28 -0
- package/dist/okf/types/Secret.md +23 -0
- package/dist/okf/types/VolumeSet.md +22 -0
- package/dist/okf/types/Workload.md +38 -0
- package/dist/ownership.d.ts +23 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/reference-catalog.d.ts +24 -0
- package/dist/reference-catalog.d.ts.map +1 -0
- package/dist/rules/cpl001-no-secret-literals.ts +83 -0
- package/dist/rules/cpl002-prefer-resource-reference.ts +71 -0
- package/dist/rules/cpl010-unrestricted-outbound.ts +41 -0
- package/dist/rules/cpl011-internal-firewall-scope.ts +38 -0
- package/dist/rules/cpl012-no-literal-credential-env.ts +58 -0
- package/dist/rules/cpl013-qualified-identity-principal.ts +45 -0
- package/dist/rules/cpl014-qualified-secret-ref.ts +54 -0
- package/dist/rules/cpl020-serverless-single-port.ts +58 -0
- package/dist/rules/cpl021-cron-job-shape.ts +64 -0
- package/dist/rules/cpl022-container-ports.ts +64 -0
- package/dist/rules/cpl023-container-resources.ts +87 -0
- package/dist/rules/cpl024-probe-handler.ts +51 -0
- package/dist/rules/cpl025-autoscaling-shape.ts +100 -0
- package/dist/rules/cpl026-scale-to-zero.ts +67 -0
- package/dist/rules/cpl027-capacity-ai-conflict.ts +78 -0
- package/dist/rules/cpl028-volume-set.ts +183 -0
- package/dist/rules/cpl029-link-targets.ts +155 -0
- package/dist/rules/cpl030-domain-routing.ts +137 -0
- package/dist/rules/cpl040-pinned-image.ts +48 -0
- package/dist/rules/cpl041-image-reference-form.ts +60 -0
- package/dist/rules/cpl042-gvc-placement.ts +57 -0
- package/dist/rules/cpl043-policy-scope.ts +0 -0
- package/dist/rules/helpers.ts +114 -0
- package/dist/serializer.d.ts +69 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skills/chant-cpln-secrets.md +110 -0
- package/dist/skills/chant-cpln-workloads.md +78 -0
- package/dist/skills/chant-cpln.md +103 -0
- package/dist/spec/fetch.d.ts +43 -0
- package/dist/spec/fetch.d.ts.map +1 -0
- package/dist/spec/parse.d.ts +79 -0
- package/dist/spec/parse.d.ts.map +1 -0
- package/dist/spec/snapshot-cli.d.ts +18 -0
- package/dist/spec/snapshot-cli.d.ts.map +1 -0
- package/dist/types/index.d.ts +1649 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate.d.ts +26 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +74 -0
- package/src/api.ts +134 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +94 -0
- package/src/codegen/generate-cli.ts +34 -0
- package/src/codegen/generate-lexicon.ts +88 -0
- package/src/codegen/generate-typescript.ts +71 -0
- package/src/codegen/generate.ts +110 -0
- package/src/codegen/naming.ts +61 -0
- package/src/codegen/package.ts +35 -0
- package/src/composites/composites.test.ts +252 -0
- package/src/composites/cron-job.ts +124 -0
- package/src/composites/gvc-environment.ts +104 -0
- package/src/composites/index.ts +17 -0
- package/src/composites/public-domain.ts +144 -0
- package/src/composites/secret-access.ts +136 -0
- package/src/composites/serverless-service.ts +143 -0
- package/src/composites/stateful-service.ts +179 -0
- package/src/coverage.ts +149 -0
- package/src/describe-resources.test.ts +252 -0
- package/src/describe-resources.ts +310 -0
- package/src/detect.ts +58 -0
- package/src/entity-props.ts +98 -0
- package/src/generated/index.d.ts +1649 -0
- package/src/generated/index.ts +156 -0
- package/src/generated/lexicon-cpln.json +2110 -0
- package/src/generated/runtime.ts +4 -0
- package/src/index.ts +31 -0
- package/src/init-templates.ts +131 -0
- package/src/kinds.ts +173 -0
- package/src/lint/audit-catalog.ts +230 -0
- package/src/lint/post-synth/cpl010-unrestricted-outbound.ts +41 -0
- package/src/lint/post-synth/cpl011-internal-firewall-scope.ts +38 -0
- package/src/lint/post-synth/cpl012-no-literal-credential-env.ts +58 -0
- package/src/lint/post-synth/cpl013-qualified-identity-principal.ts +45 -0
- package/src/lint/post-synth/cpl014-qualified-secret-ref.ts +54 -0
- package/src/lint/post-synth/cpl020-serverless-single-port.ts +58 -0
- package/src/lint/post-synth/cpl021-cron-job-shape.ts +64 -0
- package/src/lint/post-synth/cpl022-container-ports.ts +64 -0
- package/src/lint/post-synth/cpl023-container-resources.ts +87 -0
- package/src/lint/post-synth/cpl024-probe-handler.ts +51 -0
- package/src/lint/post-synth/cpl025-autoscaling-shape.ts +100 -0
- package/src/lint/post-synth/cpl026-scale-to-zero.ts +67 -0
- package/src/lint/post-synth/cpl027-capacity-ai-conflict.ts +78 -0
- package/src/lint/post-synth/cpl028-volume-set.ts +183 -0
- package/src/lint/post-synth/cpl029-link-targets.ts +155 -0
- package/src/lint/post-synth/cpl030-domain-routing.ts +137 -0
- package/src/lint/post-synth/cpl040-pinned-image.ts +48 -0
- package/src/lint/post-synth/cpl041-image-reference-form.ts +60 -0
- package/src/lint/post-synth/cpl042-gvc-placement.ts +57 -0
- package/src/lint/post-synth/cpl043-policy-scope.ts +0 -0
- package/src/lint/post-synth/helpers.ts +114 -0
- package/src/lint/post-synth/index.ts +45 -0
- package/src/lint/post-synth/post-synth.test.ts +737 -0
- package/src/lint/rules/cpl001-no-secret-literals.ts +83 -0
- package/src/lint/rules/cpl002-prefer-resource-reference.ts +71 -0
- package/src/lint/rules/index.ts +9 -0
- package/src/lint/rules/rules.test.ts +108 -0
- package/src/lsp/completions.test.ts +44 -0
- package/src/lsp/completions.ts +27 -0
- package/src/lsp/hover.test.ts +53 -0
- package/src/lsp/hover.ts +130 -0
- package/src/ownership.ts +28 -0
- package/src/package-cli.ts +25 -0
- package/src/plugin.test.ts +131 -0
- package/src/plugin.ts +187 -0
- package/src/reference-catalog.ts +110 -0
- package/src/serializer.test.ts +233 -0
- package/src/serializer.ts +253 -0
- package/src/skills/chant-cpln-secrets.md +110 -0
- package/src/skills/chant-cpln-workloads.md +78 -0
- package/src/skills/chant-cpln.md +103 -0
- package/src/spec/cpln-openapi.snapshot.json +10651 -0
- package/src/spec/fetch.ts +77 -0
- package/src/spec/parse.ts +451 -0
- package/src/spec/snapshot-cli.ts +71 -0
- package/src/validate-cli.ts +8 -0
- package/src/validate.ts +102 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
3
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
4
|
+
import { postSynthChecks } from "./index";
|
|
5
|
+
import { parseCpuMillicores, parseMemoryMib, imageTag, parseLink } from "./helpers";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a context from plain objects.
|
|
9
|
+
*
|
|
10
|
+
* The checks read `ctx.entities` through `propsOf`, which handles both a real
|
|
11
|
+
* runtime instance and a plain object — so fixtures stay readable and still
|
|
12
|
+
* exercise the same path a build does.
|
|
13
|
+
*/
|
|
14
|
+
function ctx(entities: Record<string, { entityType: string; props: Record<string, unknown> }>): PostSynthContext {
|
|
15
|
+
const map = new Map<string, Declarable>(Object.entries(entities) as unknown as Array<[string, Declarable]>);
|
|
16
|
+
return {
|
|
17
|
+
outputs: new Map(),
|
|
18
|
+
entities: map,
|
|
19
|
+
buildResult: { outputs: new Map(), entities: map, warnings: [], errors: [], sourceFileCount: 1 },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const workload = (props: Record<string, unknown>) => ({ entityType: "Cpln::Core::Workload", props });
|
|
24
|
+
const gvc = (props: Record<string, unknown>) => ({ entityType: "Cpln::Core::Gvc", props });
|
|
25
|
+
const policy = (props: Record<string, unknown>) => ({ entityType: "Cpln::Core::Policy", props });
|
|
26
|
+
const domain = (props: Record<string, unknown>) => ({ entityType: "Cpln::Core::Domain", props });
|
|
27
|
+
const volumeSet = (props: Record<string, unknown>) => ({ entityType: "Cpln::Core::VolumeSet", props });
|
|
28
|
+
|
|
29
|
+
/** Run one check by id and return its diagnostics. */
|
|
30
|
+
function run(id: string, context: PostSynthContext) {
|
|
31
|
+
const check = postSynthChecks.find((c) => c.id === id);
|
|
32
|
+
if (!check) throw new Error(`no check ${id}`);
|
|
33
|
+
return check.check(context);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Run every check — used to assert a fixture is clean overall. */
|
|
37
|
+
function runAll(context: PostSynthContext) {
|
|
38
|
+
return postSynthChecks.flatMap((check) => check.check(context));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("post-synth check registry", () => {
|
|
42
|
+
it("registers at least 15 checks with unique ids", () => {
|
|
43
|
+
expect(postSynthChecks.length).toBeGreaterThanOrEqual(15);
|
|
44
|
+
const ids = postSynthChecks.map((c) => c.id);
|
|
45
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("gives every check a description", () => {
|
|
49
|
+
for (const check of postSynthChecks) expect(check.description.length).toBeGreaterThan(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("reports nothing for an empty build", () => {
|
|
53
|
+
expect(runAll(ctx({}))).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("CPL010 unrestricted outbound", () => {
|
|
58
|
+
it("flags outbound 0.0.0.0/0", () => {
|
|
59
|
+
const found = run(
|
|
60
|
+
"CPL010",
|
|
61
|
+
ctx({ web: workload({ spec: { firewallConfig: { external: { outboundAllowCIDR: ["0.0.0.0/0"] } } } }) }),
|
|
62
|
+
);
|
|
63
|
+
expect(found).toHaveLength(1);
|
|
64
|
+
expect(found[0].severity).toBe("warning");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("leaves inbound 0.0.0.0/0 alone — that is how a public service is written", () => {
|
|
68
|
+
const found = run(
|
|
69
|
+
"CPL010",
|
|
70
|
+
ctx({ web: workload({ spec: { firewallConfig: { external: { inboundAllowCIDR: ["0.0.0.0/0"] } } } }) }),
|
|
71
|
+
);
|
|
72
|
+
expect(found).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("CPL011 internal firewall scope", () => {
|
|
77
|
+
it("flags same-org", () => {
|
|
78
|
+
const found = run(
|
|
79
|
+
"CPL011",
|
|
80
|
+
ctx({ web: workload({ spec: { firewallConfig: { internal: { inboundAllowType: "same-org" } } } }) }),
|
|
81
|
+
);
|
|
82
|
+
expect(found).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("accepts same-gvc", () => {
|
|
86
|
+
expect(
|
|
87
|
+
run("CPL011", ctx({ web: workload({ spec: { firewallConfig: { internal: { inboundAllowType: "same-gvc" } } } }) })),
|
|
88
|
+
).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("CPL012 literal credentials in env", () => {
|
|
93
|
+
const withEnv = (env: Array<{ name: string; value: string }>) =>
|
|
94
|
+
ctx({ web: workload({ spec: { containers: [{ name: "main", env }] } }) });
|
|
95
|
+
|
|
96
|
+
it("flags a credential-named var set to a literal", () => {
|
|
97
|
+
const found = run("CPL012", withEnv([{ name: "DB_PASSWORD", value: "hunter2" }]));
|
|
98
|
+
expect(found).toHaveLength(1);
|
|
99
|
+
expect(found[0].severity).toBe("error");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("accepts a secret reference", () => {
|
|
103
|
+
expect(run("CPL012", withEnv([{ name: "DB_PASSWORD", value: "cpln://secret/db.payload" }]))).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("accepts an obvious placeholder", () => {
|
|
107
|
+
expect(run("CPL012", withEnv([{ name: "API_TOKEN", value: "changeme" }]))).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("ignores a var that is not credential-shaped", () => {
|
|
111
|
+
expect(run("CPL012", withEnv([{ name: "LOG_LEVEL", value: "debug" }]))).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("CPL013 identity principal qualification", () => {
|
|
116
|
+
it("flags the bare //identity/NAME form", () => {
|
|
117
|
+
const found = run(
|
|
118
|
+
"CPL013",
|
|
119
|
+
ctx({ p: policy({ bindings: [{ permissions: ["reveal"], principalLinks: ["//identity/api"] }] }) }),
|
|
120
|
+
);
|
|
121
|
+
expect(found).toHaveLength(1);
|
|
122
|
+
expect(found[0].severity).toBe("error");
|
|
123
|
+
expect(found[0].message).toContain("//gvc/<gvc>/identity/api");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("accepts the GVC-qualified form", () => {
|
|
127
|
+
expect(
|
|
128
|
+
run(
|
|
129
|
+
"CPL013",
|
|
130
|
+
ctx({ p: policy({ bindings: [{ permissions: ["reveal"], principalLinks: ["//gvc/prod/identity/api"] }] }) }),
|
|
131
|
+
),
|
|
132
|
+
).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("leaves other principal types alone", () => {
|
|
136
|
+
expect(
|
|
137
|
+
run("CPL013", ctx({ p: policy({ bindings: [{ principalLinks: ["//user/a@b.com", "//group/devs"] }] }) })),
|
|
138
|
+
).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("CPL014 secret reference field qualifier", () => {
|
|
143
|
+
const withEnv = (value: string) =>
|
|
144
|
+
ctx({ web: workload({ spec: { containers: [{ name: "main", env: [{ name: "X", value }] }] } }) });
|
|
145
|
+
|
|
146
|
+
it("flags an unqualified reference", () => {
|
|
147
|
+
const found = run("CPL014", withEnv("cpln://secret/db"));
|
|
148
|
+
expect(found).toHaveLength(1);
|
|
149
|
+
expect(found[0].message).toContain("cpln://secret/db.FIELD");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("accepts a qualified reference", () => {
|
|
153
|
+
expect(run("CPL014", withEnv("cpln://secret/db.payload"))).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("CPL020 serverless port count", () => {
|
|
158
|
+
it("flags zero ports", () => {
|
|
159
|
+
const found = run("CPL020", ctx({ web: workload({ spec: { type: "serverless", containers: [{ name: "m" }] } }) }));
|
|
160
|
+
expect(found).toHaveLength(1);
|
|
161
|
+
expect(found[0].message).toContain("serve nothing");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("flags two ports", () => {
|
|
165
|
+
const found = run(
|
|
166
|
+
"CPL020",
|
|
167
|
+
ctx({
|
|
168
|
+
web: workload({
|
|
169
|
+
spec: {
|
|
170
|
+
type: "serverless",
|
|
171
|
+
containers: [{ name: "m", ports: [{ number: 8080 }, { number: 9090 }] }],
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
expect(found).toHaveLength(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("flags a non-HTTP port", () => {
|
|
180
|
+
const found = run(
|
|
181
|
+
"CPL020",
|
|
182
|
+
ctx({
|
|
183
|
+
web: workload({
|
|
184
|
+
spec: { type: "serverless", containers: [{ name: "m", ports: [{ number: 5432, protocol: "tcp" }] }] },
|
|
185
|
+
}),
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
expect(found).toHaveLength(1);
|
|
189
|
+
expect(found[0].message).toContain("only http");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("accepts exactly one HTTP port", () => {
|
|
193
|
+
expect(
|
|
194
|
+
run(
|
|
195
|
+
"CPL020",
|
|
196
|
+
ctx({
|
|
197
|
+
web: workload({
|
|
198
|
+
spec: { type: "serverless", containers: [{ name: "m", ports: [{ number: 8080, protocol: "http" }] }] },
|
|
199
|
+
}),
|
|
200
|
+
}),
|
|
201
|
+
),
|
|
202
|
+
).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("defaults an unset type to serverless", () => {
|
|
206
|
+
// Control Plane's own default, so a workload with no `type` is checked as
|
|
207
|
+
// what it will actually become.
|
|
208
|
+
expect(run("CPL020", ctx({ web: workload({ spec: { containers: [{ name: "m" }] } }) }))).toHaveLength(1);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("CPL021 cron shape", () => {
|
|
213
|
+
it("flags a cron workload with no schedule", () => {
|
|
214
|
+
const found = run("CPL021", ctx({ job: workload({ spec: { type: "cron" } }) }));
|
|
215
|
+
expect(found.some((d) => d.message.includes("no spec.job.schedule"))).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("flags a cron workload exposing ports", () => {
|
|
219
|
+
const found = run(
|
|
220
|
+
"CPL021",
|
|
221
|
+
ctx({
|
|
222
|
+
job: workload({
|
|
223
|
+
spec: { type: "cron", job: { schedule: "0 * * * *" }, containers: [{ name: "m", ports: [{ number: 80 }] }] },
|
|
224
|
+
}),
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
expect(found.some((d) => d.message.includes("cannot serve traffic"))).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("flags spec.job on a non-cron workload", () => {
|
|
231
|
+
const found = run("CPL021", ctx({ web: workload({ spec: { type: "standard", job: { schedule: "* * * * *" } } }) }));
|
|
232
|
+
expect(found).toHaveLength(1);
|
|
233
|
+
expect(found[0].message).toContain("Only cron");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("accepts a well-formed cron workload", () => {
|
|
237
|
+
expect(
|
|
238
|
+
run("CPL021", ctx({ job: workload({ spec: { type: "cron", job: { schedule: "0 * * * *" } } }) })),
|
|
239
|
+
).toEqual([]);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("CPL022 container ports", () => {
|
|
244
|
+
it("flags port and ports together", () => {
|
|
245
|
+
const found = run(
|
|
246
|
+
"CPL022",
|
|
247
|
+
ctx({ web: workload({ spec: { containers: [{ name: "m", port: 80, ports: [{ number: 80 }] }] } }) }),
|
|
248
|
+
);
|
|
249
|
+
expect(found.some((d) => d.message.includes("mutually exclusive"))).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("flags a port number reused across containers", () => {
|
|
253
|
+
const found = run(
|
|
254
|
+
"CPL022",
|
|
255
|
+
ctx({
|
|
256
|
+
web: workload({
|
|
257
|
+
spec: {
|
|
258
|
+
containers: [
|
|
259
|
+
{ name: "a", ports: [{ number: 8080 }] },
|
|
260
|
+
{ name: "b", ports: [{ number: 8080 }] },
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
}),
|
|
265
|
+
);
|
|
266
|
+
expect(found.some((d) => d.message.includes("both"))).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("CPL023 container resources", () => {
|
|
271
|
+
const container = (props: Record<string, unknown>) =>
|
|
272
|
+
ctx({ web: workload({ spec: { containers: [{ name: "m", ...props }] } }) });
|
|
273
|
+
|
|
274
|
+
it("flags CPU below the floor", () => {
|
|
275
|
+
expect(run("CPL023", container({ cpu: "10m", memory: "32Mi" })).some((d) => d.message.includes("minimum is 25m"))).toBe(
|
|
276
|
+
true,
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("flags a memory-to-CPU ratio above 8", () => {
|
|
281
|
+
const found = run("CPL023", container({ cpu: "50m", memory: "2Gi" }));
|
|
282
|
+
expect(found.some((d) => d.message.includes("memory-to-CPU ratio"))).toBe(true);
|
|
283
|
+
expect(found.find((d) => d.message.includes("ratio"))!.message).toContain("256m");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("honours the relax tag", () => {
|
|
287
|
+
const relaxed = ctx({
|
|
288
|
+
web: workload({
|
|
289
|
+
tags: { "cpln/relaxMemoryToCpuRatio": "true" },
|
|
290
|
+
spec: { containers: [{ name: "m", cpu: "100m", memory: "2Gi" }] },
|
|
291
|
+
}),
|
|
292
|
+
});
|
|
293
|
+
expect(run("CPL023", relaxed)).toEqual([]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("accepts the platform defaults", () => {
|
|
297
|
+
// 128Mi / 50m = 2.56, comfortably under the ceiling.
|
|
298
|
+
expect(run("CPL023", container({}))).toEqual([]);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe("CPL024 probe handlers", () => {
|
|
303
|
+
const probe = (readinessProbe: Record<string, unknown>) =>
|
|
304
|
+
ctx({ web: workload({ spec: { containers: [{ name: "m", readinessProbe }] } }) });
|
|
305
|
+
|
|
306
|
+
it("flags a probe with no handler", () => {
|
|
307
|
+
const found = run("CPL024", probe({ initialDelaySeconds: 5 }));
|
|
308
|
+
expect(found).toHaveLength(1);
|
|
309
|
+
expect(found[0].message).toContain("checks nothing");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("flags two handlers", () => {
|
|
313
|
+
const found = run("CPL024", probe({ httpGet: { path: "/" }, tcpSocket: { port: 80 } }));
|
|
314
|
+
expect(found).toHaveLength(1);
|
|
315
|
+
expect(found[0].message).toContain("mutually exclusive");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("accepts exactly one", () => {
|
|
319
|
+
expect(run("CPL024", probe({ httpGet: { path: "/healthz" } }))).toEqual([]);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe("CPL025 autoscaling shape", () => {
|
|
324
|
+
const scaling = (autoscaling: Record<string, unknown>, type = "standard") =>
|
|
325
|
+
ctx({ web: workload({ spec: { type, defaultOptions: { autoscaling } } }) });
|
|
326
|
+
|
|
327
|
+
it("flags metric with multi", () => {
|
|
328
|
+
expect(
|
|
329
|
+
run("CPL025", scaling({ metric: "cpu", multi: [{ metric: "cpu" }] })).some((d) =>
|
|
330
|
+
d.message.includes("mutually exclusive"),
|
|
331
|
+
),
|
|
332
|
+
).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("flags target above 100 with cpu", () => {
|
|
336
|
+
expect(run("CPL025", scaling({ metric: "cpu", target: 150 })).some((d) => d.message.includes("capped at 100"))).toBe(
|
|
337
|
+
true,
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("flags a metric the workload type does not support", () => {
|
|
342
|
+
expect(
|
|
343
|
+
run("CPL025", scaling({ metric: "concurrency" }, "standard")).some((d) => d.message.includes("does not support")),
|
|
344
|
+
).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("flags multi-metric on serverless", () => {
|
|
348
|
+
expect(
|
|
349
|
+
run("CPL025", scaling({ multi: [{ metric: "cpu" }] }, "serverless")).some((d) =>
|
|
350
|
+
d.message.includes("standard and stateful"),
|
|
351
|
+
),
|
|
352
|
+
).toBe(true);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("CPL026 scale to zero", () => {
|
|
357
|
+
it("flags minScale 0 on serverless with cpu", () => {
|
|
358
|
+
const found = run(
|
|
359
|
+
"CPL026",
|
|
360
|
+
ctx({ web: workload({ spec: { type: "serverless", defaultOptions: { autoscaling: { minScale: 0, metric: "cpu" } } } }) }),
|
|
361
|
+
);
|
|
362
|
+
expect(found).toHaveLength(1);
|
|
363
|
+
expect(found[0].message).toContain("holds at one replica");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("accepts minScale 0 on serverless with concurrency", () => {
|
|
367
|
+
expect(
|
|
368
|
+
run(
|
|
369
|
+
"CPL026",
|
|
370
|
+
ctx({
|
|
371
|
+
web: workload({
|
|
372
|
+
spec: { type: "serverless", defaultOptions: { autoscaling: { minScale: 0, metric: "concurrency" } } },
|
|
373
|
+
}),
|
|
374
|
+
}),
|
|
375
|
+
),
|
|
376
|
+
).toEqual([]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("accepts minScale 0 on standard under KEDA", () => {
|
|
380
|
+
expect(
|
|
381
|
+
run(
|
|
382
|
+
"CPL026",
|
|
383
|
+
ctx({
|
|
384
|
+
web: workload({
|
|
385
|
+
spec: { type: "standard", defaultOptions: { autoscaling: { minScale: 0, metric: "keda" } } },
|
|
386
|
+
}),
|
|
387
|
+
}),
|
|
388
|
+
),
|
|
389
|
+
).toEqual([]);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("flags minScale 0 on cron outright", () => {
|
|
393
|
+
const found = run(
|
|
394
|
+
"CPL026",
|
|
395
|
+
ctx({ job: workload({ spec: { type: "cron", defaultOptions: { autoscaling: { minScale: 0 } } } }) }),
|
|
396
|
+
);
|
|
397
|
+
expect(found[0].severity).toBe("error");
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("CPL027 Capacity AI conflicts", () => {
|
|
402
|
+
it("flags CPU autoscaling under the default-on Capacity AI", () => {
|
|
403
|
+
const found = run(
|
|
404
|
+
"CPL027",
|
|
405
|
+
ctx({ web: workload({ spec: { type: "standard", defaultOptions: { autoscaling: { metric: "cpu" } } } }) }),
|
|
406
|
+
);
|
|
407
|
+
expect(found).toHaveLength(1);
|
|
408
|
+
expect(found[0].message).toContain("on by default");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("says so differently when Capacity AI is declared", () => {
|
|
412
|
+
const found = run(
|
|
413
|
+
"CPL027",
|
|
414
|
+
ctx({
|
|
415
|
+
web: workload({
|
|
416
|
+
spec: { type: "standard", defaultOptions: { capacityAI: true, autoscaling: { metric: "cpu" } } },
|
|
417
|
+
}),
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
420
|
+
expect(found[0].message).not.toContain("on by default");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("flags a GPU", () => {
|
|
424
|
+
const found = run(
|
|
425
|
+
"CPL027",
|
|
426
|
+
ctx({ web: workload({ spec: { type: "standard", containers: [{ name: "m", gpu: { nvidia: {} } }] } }) }),
|
|
427
|
+
);
|
|
428
|
+
expect(found.some((d) => d.message.includes("GPU"))).toBe(true);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("accepts the conflict once Capacity AI is off", () => {
|
|
432
|
+
expect(
|
|
433
|
+
run(
|
|
434
|
+
"CPL027",
|
|
435
|
+
ctx({
|
|
436
|
+
web: workload({
|
|
437
|
+
spec: { type: "standard", defaultOptions: { capacityAI: false, autoscaling: { metric: "cpu" } } },
|
|
438
|
+
}),
|
|
439
|
+
}),
|
|
440
|
+
),
|
|
441
|
+
).toEqual([]);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe("CPL028 storage", () => {
|
|
446
|
+
it("flags a capacity below the performance class floor", () => {
|
|
447
|
+
const found = run(
|
|
448
|
+
"CPL028",
|
|
449
|
+
ctx({ data: volumeSet({ spec: { initialCapacity: 50, performanceClass: "high-throughput-ssd" } }) }),
|
|
450
|
+
);
|
|
451
|
+
expect(found.some((d) => d.message.includes("200 GB minimum"))).toBe(true);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("flags an ext4 volume mounted by a non-stateful workload", () => {
|
|
455
|
+
const found = run(
|
|
456
|
+
"CPL028",
|
|
457
|
+
ctx({
|
|
458
|
+
data: volumeSet({ name: "data", gvc: "prod", spec: { initialCapacity: 20, fileSystemType: "ext4" } }),
|
|
459
|
+
web: workload({
|
|
460
|
+
name: "web",
|
|
461
|
+
gvc: "prod",
|
|
462
|
+
spec: {
|
|
463
|
+
type: "serverless",
|
|
464
|
+
containers: [{ name: "m", volumes: [{ uri: "cpln://volumeset/data", path: "/data" }] }],
|
|
465
|
+
},
|
|
466
|
+
}),
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
expect(found.some((d) => d.message.includes("stateful"))).toBe(true);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("flags a cross-GVC mount", () => {
|
|
473
|
+
const found = run(
|
|
474
|
+
"CPL028",
|
|
475
|
+
ctx({
|
|
476
|
+
data: volumeSet({ name: "data", gvc: "staging", spec: { fileSystemType: "shared" } }),
|
|
477
|
+
db: workload({
|
|
478
|
+
name: "db",
|
|
479
|
+
gvc: "prod",
|
|
480
|
+
spec: {
|
|
481
|
+
type: "stateful",
|
|
482
|
+
containers: [{ name: "m", volumes: [{ uri: "cpln://volumeset/data", path: "/data" }] }],
|
|
483
|
+
},
|
|
484
|
+
}),
|
|
485
|
+
}),
|
|
486
|
+
);
|
|
487
|
+
expect(found.some((d) => d.message.includes("own GVC"))).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("flags nested mount paths", () => {
|
|
491
|
+
const found = run(
|
|
492
|
+
"CPL028",
|
|
493
|
+
ctx({
|
|
494
|
+
db: workload({
|
|
495
|
+
spec: {
|
|
496
|
+
type: "stateful",
|
|
497
|
+
containers: [
|
|
498
|
+
{
|
|
499
|
+
name: "m",
|
|
500
|
+
volumes: [
|
|
501
|
+
{ uri: "cpln://volumeset/a", path: "/data" },
|
|
502
|
+
{ uri: "cpln://volumeset/b", path: "/data/inner" },
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
},
|
|
507
|
+
}),
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
510
|
+
expect(found.some((d) => d.message.includes("parent of another"))).toBe(true);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
describe("CPL029 link targets", () => {
|
|
515
|
+
it("flags a link to an undeclared resource of a declared kind", () => {
|
|
516
|
+
const found = run(
|
|
517
|
+
"CPL029",
|
|
518
|
+
ctx({
|
|
519
|
+
prod: gvc({ name: "prod" }),
|
|
520
|
+
web: workload({ name: "web", gvc: "prod", spec: { identityLink: "//gvc/prod/identity/missing" } }),
|
|
521
|
+
api: { entityType: "Cpln::Core::Identity", props: { name: "api", gvc: "prod" } },
|
|
522
|
+
}),
|
|
523
|
+
);
|
|
524
|
+
expect(found.some((d) => d.message.includes("no identity named \"missing\""))).toBe(true);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("says nothing about a kind this stack does not declare", () => {
|
|
528
|
+
// Referencing a secret another team manages is ordinary, not a defect.
|
|
529
|
+
const found = run(
|
|
530
|
+
"CPL029",
|
|
531
|
+
ctx({
|
|
532
|
+
prod: gvc({ name: "prod" }),
|
|
533
|
+
web: workload({ name: "web", gvc: "prod", spec: { identityLink: "//gvc/prod/identity/elsewhere" } }),
|
|
534
|
+
}),
|
|
535
|
+
);
|
|
536
|
+
expect(found).toEqual([]);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("flags a workload whose identity is in another GVC", () => {
|
|
540
|
+
const found = run(
|
|
541
|
+
"CPL029",
|
|
542
|
+
ctx({
|
|
543
|
+
prod: gvc({ name: "prod" }),
|
|
544
|
+
staging: gvc({ name: "staging" }),
|
|
545
|
+
api: { entityType: "Cpln::Core::Identity", props: { name: "api", gvc: "staging" } },
|
|
546
|
+
web: workload({ name: "web", gvc: "prod", spec: { identityLink: "//gvc/staging/identity/api" } }),
|
|
547
|
+
}),
|
|
548
|
+
);
|
|
549
|
+
expect(found.some((d) => d.message.includes("cannot be shared across GVCs"))).toBe(true);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("flags a resource placed in an undeclared GVC", () => {
|
|
553
|
+
const found = run(
|
|
554
|
+
"CPL029",
|
|
555
|
+
ctx({
|
|
556
|
+
prod: gvc({ name: "prod" }),
|
|
557
|
+
web: workload({ name: "web", gvc: "typo" }),
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
expect(found.some((d) => d.message.includes('GVC "typo"'))).toBe(true);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe("CPL030 domain routing", () => {
|
|
565
|
+
it("flags an apex domain in ns mode", () => {
|
|
566
|
+
const found = run("CPL030", ctx({ d: domain({ name: "example.com", spec: { dnsMode: "ns", gvcLink: "//gvc/prod" } }) }));
|
|
567
|
+
expect(found.some((d) => d.message.includes("apex"))).toBe(true);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("flags ns mode with http01", () => {
|
|
571
|
+
const found = run(
|
|
572
|
+
"CPL030",
|
|
573
|
+
ctx({ d: domain({ name: "api.example.com", spec: { dnsMode: "ns", certChallengeType: "http01", gvcLink: "//gvc/prod" } }) }),
|
|
574
|
+
);
|
|
575
|
+
expect(found.some((d) => d.message.includes("dns01"))).toBe(true);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("flags two routing targets at once", () => {
|
|
579
|
+
const found = run(
|
|
580
|
+
"CPL030",
|
|
581
|
+
ctx({ d: domain({ name: "api.example.com", spec: { gvcLink: "//gvc/prod", workloadLink: "//gvc/prod/workload/w" } }) }),
|
|
582
|
+
);
|
|
583
|
+
expect(found.some((d) => d.message.includes("mutually exclusive"))).toBe(true);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("flags workloadLink to a non-stateful workload", () => {
|
|
587
|
+
const found = run(
|
|
588
|
+
"CPL030",
|
|
589
|
+
ctx({
|
|
590
|
+
w: workload({ name: "w", gvc: "prod", spec: { type: "serverless" } }),
|
|
591
|
+
d: domain({ name: "api.example.com", spec: { workloadLink: "//gvc/prod/workload/w" } }),
|
|
592
|
+
}),
|
|
593
|
+
);
|
|
594
|
+
expect(found.some((d) => d.message.includes("stateful workloads only"))).toBe(true);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("flags routes spanning two GVCs", () => {
|
|
598
|
+
const found = run(
|
|
599
|
+
"CPL030",
|
|
600
|
+
ctx({
|
|
601
|
+
d: domain({
|
|
602
|
+
name: "api.example.com",
|
|
603
|
+
spec: {
|
|
604
|
+
ports: [
|
|
605
|
+
{
|
|
606
|
+
number: 443,
|
|
607
|
+
routes: [
|
|
608
|
+
{ prefix: "/a", workloadLink: "//gvc/prod/workload/a" },
|
|
609
|
+
{ prefix: "/b", workloadLink: "//gvc/staging/workload/b" },
|
|
610
|
+
],
|
|
611
|
+
},
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
}),
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
617
|
+
expect(found.some((d) => d.message.includes("same GVC"))).toBe(true);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
describe("CPL040 / CPL041 images", () => {
|
|
622
|
+
const image = (value: string) => ctx({ web: workload({ spec: { containers: [{ name: "m", image: value }] } }) });
|
|
623
|
+
|
|
624
|
+
it("flags :latest", () => {
|
|
625
|
+
expect(run("CPL040", image("nginx:latest"))).toHaveLength(1);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("flags an untagged image", () => {
|
|
629
|
+
expect(run("CPL040", image("nginx"))[0].message).toContain("untagged");
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it("accepts a pinned tag and a digest", () => {
|
|
633
|
+
expect(run("CPL040", image("nginx:1.27"))).toEqual([]);
|
|
634
|
+
expect(run("CPL040", image("nginx@sha256:abc123"))).toEqual([]);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("flags a docker.io/ prefix and suggests the bare form", () => {
|
|
638
|
+
const found = run("CPL041", image("docker.io/library/nginx:1.27"));
|
|
639
|
+
expect(found).toHaveLength(1);
|
|
640
|
+
expect(found[0].message).toContain('use "nginx:1.27"');
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("flags the registry hostname form", () => {
|
|
644
|
+
expect(run("CPL041", image("acme.registry.cpln.io/app:1.0"))[0].message).toContain("//image/app:1.0");
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
describe("CPL042 GVC placement", () => {
|
|
649
|
+
it("flags a GVC with no placement", () => {
|
|
650
|
+
const found = run("CPL042", ctx({ g: gvc({ name: "prod" }) }));
|
|
651
|
+
expect(found).toHaveLength(1);
|
|
652
|
+
expect(found[0].message).toContain("scheduled nowhere");
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("accepts location links", () => {
|
|
656
|
+
expect(
|
|
657
|
+
run(
|
|
658
|
+
"CPL042",
|
|
659
|
+
ctx({ g: gvc({ name: "prod", spec: { staticPlacement: { locationLinks: ["/org/acme/location/aws-us-east-1"] } } }) }),
|
|
660
|
+
),
|
|
661
|
+
).toEqual([]);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("flags a link that is not a location", () => {
|
|
665
|
+
const found = run(
|
|
666
|
+
"CPL042",
|
|
667
|
+
ctx({ g: gvc({ name: "prod", spec: { staticPlacement: { locationLinks: ["aws-us-east-1"] } } }) }),
|
|
668
|
+
);
|
|
669
|
+
expect(found.some((d) => d.message.includes("does not look like a location"))).toBe(true);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
describe("CPL043 policy scope", () => {
|
|
674
|
+
it("flags a non-targetable kind", () => {
|
|
675
|
+
const found = run("CPL043", ctx({ p: policy({ name: "p", targetKind: "ipset", target: "all" }) }));
|
|
676
|
+
expect(found.some((d) => d.message.includes("not a valid policy target"))).toBe(true);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("flags two target scopes", () => {
|
|
680
|
+
const found = run(
|
|
681
|
+
"CPL043",
|
|
682
|
+
ctx({ p: policy({ name: "p", targetKind: "secret", target: "all", targetLinks: ["//secret/a"] }) }),
|
|
683
|
+
);
|
|
684
|
+
expect(found.some((d) => d.message.includes("exactly one target scope"))).toBe(true);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it("flags a hand-set origin", () => {
|
|
688
|
+
const found = run("CPL043", ctx({ p: policy({ name: "p", targetKind: "secret", target: "all", origin: "default" }) }));
|
|
689
|
+
expect(found.some((d) => d.message.includes("origin"))).toBe(true);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it("flags unsorted permissions", () => {
|
|
693
|
+
const found = run(
|
|
694
|
+
"CPL043",
|
|
695
|
+
ctx({
|
|
696
|
+
p: policy({ name: "p", targetKind: "secret", target: "all", bindings: [{ permissions: ["reveal", "edit"] }] }),
|
|
697
|
+
}),
|
|
698
|
+
);
|
|
699
|
+
expect(found.some((d) => d.message.includes("sorted"))).toBe(true);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("flags duplicate permissions", () => {
|
|
703
|
+
const found = run(
|
|
704
|
+
"CPL043",
|
|
705
|
+
ctx({
|
|
706
|
+
p: policy({ name: "p", targetKind: "secret", target: "all", bindings: [{ permissions: ["edit", "edit"] }] }),
|
|
707
|
+
}),
|
|
708
|
+
);
|
|
709
|
+
expect(found.some((d) => d.message.includes("duplicate"))).toBe(true);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
describe("quantity and link parsing", () => {
|
|
714
|
+
it("parses CPU quantities", () => {
|
|
715
|
+
expect(parseCpuMillicores("50m")).toBe(50);
|
|
716
|
+
expect(parseCpuMillicores("1")).toBe(1000);
|
|
717
|
+
expect(parseCpuMillicores("1.5")).toBe(1500);
|
|
718
|
+
expect(parseCpuMillicores("bogus")).toBeUndefined();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("parses memory quantities", () => {
|
|
722
|
+
expect(parseMemoryMib("128Mi")).toBe(128);
|
|
723
|
+
expect(parseMemoryMib("1Gi")).toBe(1024);
|
|
724
|
+
expect(parseMemoryMib("bogus")).toBeUndefined();
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("does not mistake a registry port for a tag", () => {
|
|
728
|
+
expect(imageTag("registry:5000/app")).toBeUndefined();
|
|
729
|
+
expect(imageTag("registry:5000/app:1.0")).toBe("1.0");
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it("parses both link forms", () => {
|
|
733
|
+
expect(parseLink("//secret/db")).toEqual({ kind: "secret", name: "db" });
|
|
734
|
+
expect(parseLink("//gvc/prod/identity/api")).toEqual({ gvc: "prod", kind: "identity", name: "api" });
|
|
735
|
+
expect(parseLink("not-a-link")).toBeUndefined();
|
|
736
|
+
});
|
|
737
|
+
});
|