@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
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EAId,MAAM,0BAA0B,CAAC;AAsClC,eAAO,MAAM,UAAU,EAAE,aA+IxB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How observed cpln resources reference each other, so `chant graph --live` can
|
|
3
|
+
* rebuild the topology from a bag of nodes.
|
|
4
|
+
*
|
|
5
|
+
* Control Plane addresses resources by **link string**
|
|
6
|
+
* (`//gvc/prod/identity/api`), and edge reconstruction matches identifiers
|
|
7
|
+
* exactly, so a raw link never equals the `name` it points at. Rather than add
|
|
8
|
+
* an enrichment pass, `describeResources` resolves each link down to the bare
|
|
9
|
+
* name it ends in and files it under a stable attribute — `refs.identity`,
|
|
10
|
+
* `refs.gvc`, `refs.pullSecrets` — and the rules below read those. See
|
|
11
|
+
* {@link referenceAttributes} in `describe-resources.ts`.
|
|
12
|
+
*
|
|
13
|
+
* Names are unique per kind within an org (per GVC for the GVC-scoped kinds),
|
|
14
|
+
* so `name` is the identity for every kind and `targetKind` disambiguates the
|
|
15
|
+
* cases where two kinds could hold the same name.
|
|
16
|
+
*
|
|
17
|
+
* Containment vs reference matters for the picture: "this workload is in this
|
|
18
|
+
* GVC" is a boundary the renderer draws as a box, not a line from every
|
|
19
|
+
* resource to its GVC. It still declares `viaAttr` so a traversal can walk it —
|
|
20
|
+
* "which GVCs have no workloads" is a containment question.
|
|
21
|
+
*/
|
|
22
|
+
import type { ReferenceCatalog } from "@intentius/chant/lexicon";
|
|
23
|
+
export declare const cplnReferenceCatalog: ReferenceCatalog;
|
|
24
|
+
//# sourceMappingURL=reference-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reference-catalog.d.ts","sourceRoot":"","sources":["../src/reference-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAKjE,eAAO,MAAM,oBAAoB,EAAE,gBAkFlC,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CPL001: no literal credential material in a cpln declaration.
|
|
6
|
+
*
|
|
7
|
+
* This is a source-level rule rather than a post-synth check on purpose. By
|
|
8
|
+
* the time the manifest exists, a literal is just a string in a `data` block
|
|
9
|
+
* and there is nothing left to point at; here there is a file and a line, which
|
|
10
|
+
* is what makes the finding actionable — and, more to the point, this is the
|
|
11
|
+
* form the credential is in when it enters git history.
|
|
12
|
+
*
|
|
13
|
+
* `Secret` is the obvious carrier, but a credential pasted into a workload's
|
|
14
|
+
* env or an identity's cloud config is the same leak, so the rule fires
|
|
15
|
+
* anywhere inside a `new <cpln kind>(…)`.
|
|
16
|
+
*
|
|
17
|
+
* Complementary to CPL012, which catches the shape this cannot see — a
|
|
18
|
+
* plausible-looking value in a credential-named env var — from the model.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const CPLN_KINDS = new Set(["Gvc", "Workload", "Identity", "VolumeSet", "Secret", "Policy", "Domain", "IpSet"]);
|
|
22
|
+
|
|
23
|
+
const CREDENTIAL_SHAPES: Array<{ pattern: RegExp; label: string }> = [
|
|
24
|
+
{ pattern: /^-----BEGIN [A-Z ]*PRIVATE KEY-----/, label: "private key material" },
|
|
25
|
+
{ pattern: /^AKIA[0-9A-Z]{16}$/, label: "AWS access key id" },
|
|
26
|
+
{ pattern: /^(ghp|gho|ghs|ghu)_[A-Za-z0-9]{20,}$/, label: "GitHub token" },
|
|
27
|
+
{ pattern: /^github_pat_[A-Za-z0-9_]{20,}$/, label: "GitHub fine-grained token" },
|
|
28
|
+
{ pattern: /^sk-[A-Za-z0-9_-]{20,}$/, label: "secret API key (sk-)" },
|
|
29
|
+
{ pattern: /^xox[baprs]-[A-Za-z0-9-]{10,}$/, label: "Slack token" },
|
|
30
|
+
{ pattern: /^ya29\.[A-Za-z0-9_-]{20,}$/, label: "Google OAuth access token" },
|
|
31
|
+
{ pattern: /^eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}$/, label: "JWT" },
|
|
32
|
+
{
|
|
33
|
+
pattern: /^postgres(ql)?:\/\/[^:@/\s]+:[^@/\s]+@/,
|
|
34
|
+
label: "database URL with an inline password",
|
|
35
|
+
},
|
|
36
|
+
{ pattern: /^mysql:\/\/[^:@/\s]+:[^@/\s]+@/, label: "database URL with an inline password" },
|
|
37
|
+
{ pattern: /^mongodb(\+srv)?:\/\/[^:@/\s]+:[^@/\s]+@/, label: "database URL with an inline password" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export const noSecretLiteralsRule: LintRule = {
|
|
41
|
+
id: "CPL001",
|
|
42
|
+
severity: "error",
|
|
43
|
+
category: "security",
|
|
44
|
+
description: "No literal credential material in cpln declarations — reference a secret instead",
|
|
45
|
+
|
|
46
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
47
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
48
|
+
const source = context.sourceFile;
|
|
49
|
+
|
|
50
|
+
const visit = (node: ts.Node, insideCplnNew: boolean): void => {
|
|
51
|
+
let inside = insideCplnNew;
|
|
52
|
+
|
|
53
|
+
if (ts.isNewExpression(node)) {
|
|
54
|
+
const name = node.expression.getText(source);
|
|
55
|
+
const short = name.split(".").pop() ?? name;
|
|
56
|
+
if (CPLN_KINDS.has(short)) inside = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (inside && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))) {
|
|
60
|
+
for (const { pattern, label } of CREDENTIAL_SHAPES) {
|
|
61
|
+
if (!pattern.test(node.text)) continue;
|
|
62
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
63
|
+
diagnostics.push({
|
|
64
|
+
ruleId: "CPL001",
|
|
65
|
+
severity: "error",
|
|
66
|
+
message:
|
|
67
|
+
`Literal ${label} in a cpln declaration. Store it as a Secret and reference it as ` +
|
|
68
|
+
`\`cpln://secret/NAME.FIELD\` — a literal here is a credential in git history.`,
|
|
69
|
+
file: context.filePath,
|
|
70
|
+
line: line + 1,
|
|
71
|
+
column: character + 1,
|
|
72
|
+
});
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
ts.forEachChild(node, (child) => visit(child, inside));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
visit(source, false);
|
|
81
|
+
return diagnostics;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CPL002: prefer a resource reference over a hand-written link.
|
|
6
|
+
*
|
|
7
|
+
* The cpln serializer resolves a declared resource passed where a link is
|
|
8
|
+
* expected into the correct link for its kind — including the GVC qualifier
|
|
9
|
+
* that the identity form needs and that is silently ignored when missing. A
|
|
10
|
+
* hand-spelled `"//gvc/prod/identity/api"` gets none of that: it does not
|
|
11
|
+
* follow a rename, it is not checked against anything at author time, and
|
|
12
|
+
* getting the shape subtly wrong fails at runtime rather than at apply.
|
|
13
|
+
*
|
|
14
|
+
* This is a style rule, not a correctness one — the literal form is valid, and
|
|
15
|
+
* is the only option for a resource this stack does not declare. It fires on
|
|
16
|
+
* the link syntax specifically, so ordinary strings are untouched.
|
|
17
|
+
*
|
|
18
|
+
* Not flagged: `cpln://secret/…` and `cpln://volumeset/…`. Those are runtime
|
|
19
|
+
* *resolution* URIs read by the container, not links between resources, and
|
|
20
|
+
* they have no reference form to prefer.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const CPLN_KINDS = new Set(["Gvc", "Workload", "Identity", "VolumeSet", "Secret", "Policy", "Domain", "IpSet"]);
|
|
24
|
+
|
|
25
|
+
/** `//gvc/prod/identity/api` or `//secret/db-password`. */
|
|
26
|
+
const LINK = /^\/\/(?:gvc\/[^/\s]+\/)?(gvc|workload|identity|volumeset|secret|policy|domain|ipset)\/[^/\s]+$/;
|
|
27
|
+
|
|
28
|
+
export const preferResourceReferenceRule: LintRule = {
|
|
29
|
+
id: "CPL002",
|
|
30
|
+
severity: "info",
|
|
31
|
+
category: "style",
|
|
32
|
+
description: "Prefer passing a declared resource over a hand-written Control Plane link",
|
|
33
|
+
|
|
34
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
35
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
36
|
+
const source = context.sourceFile;
|
|
37
|
+
|
|
38
|
+
const visit = (node: ts.Node, insideCplnNew: boolean): void => {
|
|
39
|
+
let inside = insideCplnNew;
|
|
40
|
+
|
|
41
|
+
if (ts.isNewExpression(node)) {
|
|
42
|
+
const name = node.expression.getText(source);
|
|
43
|
+
const short = name.split(".").pop() ?? name;
|
|
44
|
+
if (CPLN_KINDS.has(short)) inside = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (inside && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))) {
|
|
48
|
+
const match = LINK.exec(node.text);
|
|
49
|
+
if (match) {
|
|
50
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
51
|
+
diagnostics.push({
|
|
52
|
+
ruleId: "CPL002",
|
|
53
|
+
severity: "info",
|
|
54
|
+
message:
|
|
55
|
+
`Hand-written link "${node.text}". If the ${match[1]} is declared in this stack, pass the ` +
|
|
56
|
+
`resource itself — the serializer emits the correct link, including the GVC qualifier, and ` +
|
|
57
|
+
`the reference follows a rename.`,
|
|
58
|
+
file: context.filePath,
|
|
59
|
+
line: line + 1,
|
|
60
|
+
column: character + 1,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ts.forEachChild(node, (child) => visit(child, inside));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
visit(source, false);
|
|
69
|
+
return diagnostics;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, entitiesOfType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
const ANY_ADDRESS = "0.0.0.0/0";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CPL010: a workload that may call out to the entire internet.
|
|
9
|
+
*
|
|
10
|
+
* Inbound `0.0.0.0/0` is how a public service is *supposed* to be written, so
|
|
11
|
+
* it is not flagged. Outbound `0.0.0.0/0` is different: it is rarely required,
|
|
12
|
+
* it is the egress path for anything that gets a foothold in the container,
|
|
13
|
+
* and Control Plane supports hostname rules (with wildcards) precisely so it
|
|
14
|
+
* does not have to be all-or-nothing.
|
|
15
|
+
*/
|
|
16
|
+
export const unrestrictedOutboundCheck: PostSynthCheck = {
|
|
17
|
+
id: "CPL010",
|
|
18
|
+
description: "Workloads should not allow outbound traffic to every address",
|
|
19
|
+
|
|
20
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
21
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
22
|
+
|
|
23
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
24
|
+
const outbound = readArray(entity, "spec", "firewallConfig", "external", "outboundAllowCIDR");
|
|
25
|
+
if (!outbound.includes(ANY_ADDRESS)) continue;
|
|
26
|
+
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
checkId: "CPL010",
|
|
29
|
+
severity: "warning",
|
|
30
|
+
message:
|
|
31
|
+
`Workload "${name}" allows outbound traffic to ${ANY_ADDRESS}. Narrow it to the CIDRs it ` +
|
|
32
|
+
`actually calls, or use \`outboundAllowHostname\` (wildcards like "*.amazonaws.com" are ` +
|
|
33
|
+
`supported) so egress is bounded.`,
|
|
34
|
+
entity: name,
|
|
35
|
+
lexicon: "cpln",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return diagnostics;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readString } from "../../entity-props";
|
|
3
|
+
import { GVC, WORKLOAD, entitiesOfType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CPL011: internal firewall opened to the whole org.
|
|
7
|
+
*
|
|
8
|
+
* `same-org` lets every workload in every GVC reach this one, which crosses the
|
|
9
|
+
* boundary a GVC exists to draw. `same-gvc` and `workload-list` cover almost
|
|
10
|
+
* every real case, and `workload-list` works cross-GVC when that is genuinely
|
|
11
|
+
* needed.
|
|
12
|
+
*/
|
|
13
|
+
export const internalFirewallScopeCheck: PostSynthCheck = {
|
|
14
|
+
id: "CPL011",
|
|
15
|
+
description: "Internal firewall should not be opened to the entire org",
|
|
16
|
+
|
|
17
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
18
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
19
|
+
|
|
20
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
21
|
+
const allowType = readString(entity, "spec", "firewallConfig", "internal", "inboundAllowType");
|
|
22
|
+
if (allowType !== "same-org") continue;
|
|
23
|
+
|
|
24
|
+
diagnostics.push({
|
|
25
|
+
checkId: "CPL011",
|
|
26
|
+
severity: "warning",
|
|
27
|
+
message:
|
|
28
|
+
`Workload "${name}" sets internal inboundAllowType "same-org", so every workload in the org — ` +
|
|
29
|
+
`including other GVCs — can reach it. Prefer "same-gvc", or "workload-list" with the specific ` +
|
|
30
|
+
`workloads (which also works across GVCs).`,
|
|
31
|
+
entity: name,
|
|
32
|
+
lexicon: "cpln",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return diagnostics;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, entitiesOfType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/** `cpln://secret/NAME.FIELD` — the field qualifier is the part that matters. */
|
|
6
|
+
const SECRET_REF = /^cpln:\/\/secret\/([^/.\s]+)(?:\.([^/\s]+))?$/;
|
|
7
|
+
|
|
8
|
+
/** Env var names that read as credentials. */
|
|
9
|
+
const CREDENTIAL_NAME = /(password|passwd|secret|token|apikey|api_key|access_key|private_key|credential)/i;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Values that look like a deliberate non-secret rather than a leaked one: an
|
|
13
|
+
* empty string, an obvious placeholder, or another `cpln://` reference.
|
|
14
|
+
*/
|
|
15
|
+
const PLACEHOLDER = /^(|changeme|placeholder|todo|xxx+|\*+|<[^>]*>|\$\{[^}]*\})$/i;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* CPL012: a credential-shaped env var carrying a literal value.
|
|
19
|
+
*
|
|
20
|
+
* The alternative is one string change — `cpln://secret/db.password` — so a
|
|
21
|
+
* literal here is almost always an oversight rather than a decision. Env values
|
|
22
|
+
* end up in the workload spec, which is readable by anyone with `get` on the
|
|
23
|
+
* workload, and in every diff of the manifest.
|
|
24
|
+
*/
|
|
25
|
+
export const noLiteralCredentialEnvCheck: PostSynthCheck = {
|
|
26
|
+
id: "CPL012",
|
|
27
|
+
description: "Credential-shaped environment variables must reference a secret, not a literal",
|
|
28
|
+
|
|
29
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
30
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
31
|
+
|
|
32
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
33
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
34
|
+
const containerName = readString(container, "name") ?? "?";
|
|
35
|
+
for (const env of readArray(container, "env")) {
|
|
36
|
+
const envName = readString(env, "name");
|
|
37
|
+
const value = readString(env, "value");
|
|
38
|
+
if (!envName || value === undefined) continue;
|
|
39
|
+
if (!CREDENTIAL_NAME.test(envName)) continue;
|
|
40
|
+
if (value.startsWith("cpln://") || PLACEHOLDER.test(value)) continue;
|
|
41
|
+
|
|
42
|
+
diagnostics.push({
|
|
43
|
+
checkId: "CPL012",
|
|
44
|
+
severity: "error",
|
|
45
|
+
message:
|
|
46
|
+
`Workload "${name}" container "${containerName}" sets env "${envName}" to a literal value. ` +
|
|
47
|
+
`Reference a secret instead — \`cpln://secret/NAME.FIELD\` — so the value is not stored in ` +
|
|
48
|
+
`the workload spec or its diffs.`,
|
|
49
|
+
entity: name,
|
|
50
|
+
lexicon: "cpln",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return diagnostics;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { propsOf, readArray } from "../../entity-props";
|
|
3
|
+
import { GVC, POLICY, entitiesOfType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CPL013: an identity principal link that Control Plane will ignore.
|
|
7
|
+
*
|
|
8
|
+
* `//identity/NAME` is accepted by the policy API and then has no effect. Only
|
|
9
|
+
* the GVC-qualified `//gvc/GVC/identity/NAME` grants anything. Nothing reports
|
|
10
|
+
* this — the policy exists, the binding exists, and the permission is simply
|
|
11
|
+
* never granted.
|
|
12
|
+
*/
|
|
13
|
+
export const qualifiedIdentityPrincipalCheck: PostSynthCheck = {
|
|
14
|
+
id: "CPL013",
|
|
15
|
+
description: "Policy bindings must name identities by their GVC-qualified link",
|
|
16
|
+
|
|
17
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
18
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
19
|
+
|
|
20
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, POLICY)) {
|
|
21
|
+
for (const binding of readArray(entity, "bindings")) {
|
|
22
|
+
const links = propsOf(binding).principalLinks;
|
|
23
|
+
if (!Array.isArray(links)) continue;
|
|
24
|
+
|
|
25
|
+
for (const link of links) {
|
|
26
|
+
if (typeof link !== "string") continue;
|
|
27
|
+
if (!/^\/\/identity\/[^/]+$/.test(link)) continue;
|
|
28
|
+
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
checkId: "CPL013",
|
|
31
|
+
severity: "error",
|
|
32
|
+
message:
|
|
33
|
+
`Policy "${name}" binds the principal "${link}". Control Plane silently ignores the bare ` +
|
|
34
|
+
`//identity/NAME form — use //gvc/<gvc>/identity/${link.slice("//identity/".length)} instead. ` +
|
|
35
|
+
`The policy will apply cleanly and grant nothing as written.`,
|
|
36
|
+
entity: name,
|
|
37
|
+
lexicon: "cpln",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return diagnostics;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, entitiesOfType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/** `cpln://secret/NAME.FIELD` — the field qualifier is the part that matters. */
|
|
6
|
+
const SECRET_REF = /^cpln:\/\/secret\/([^/.\s]+)(?:\.([^/\s]+))?$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CPL014: a secret reference with no field qualifier.
|
|
10
|
+
*
|
|
11
|
+
* `cpln://secret/db` resolves to nothing for every secret type except `gcp`,
|
|
12
|
+
* which is conventionally mounted as a whole JSON file. The field name is not
|
|
13
|
+
* optional: `opaque` needs `.payload`, `userpass` needs `.username`/`.password`,
|
|
14
|
+
* `tls` needs `.cert`/`.key`, and a `dictionary` needs one of its keys.
|
|
15
|
+
*
|
|
16
|
+
* Volume mounts are exempt — mounting a secret as a directory is the documented
|
|
17
|
+
* way to consume a `dictionary` or a `gcp` credential file.
|
|
18
|
+
*/
|
|
19
|
+
export const qualifiedSecretRefCheck: PostSynthCheck = {
|
|
20
|
+
id: "CPL014",
|
|
21
|
+
description: "Secret references in environment variables must name a field",
|
|
22
|
+
|
|
23
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
24
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
25
|
+
|
|
26
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
27
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
28
|
+
const containerName = readString(container, "name") ?? "?";
|
|
29
|
+
for (const env of readArray(container, "env")) {
|
|
30
|
+
const envName = readString(env, "name") ?? "?";
|
|
31
|
+
const value = readString(env, "value");
|
|
32
|
+
if (!value) continue;
|
|
33
|
+
|
|
34
|
+
const match = SECRET_REF.exec(value);
|
|
35
|
+
if (!match || match[2]) continue;
|
|
36
|
+
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
checkId: "CPL014",
|
|
39
|
+
severity: "error",
|
|
40
|
+
message:
|
|
41
|
+
`Workload "${name}" container "${containerName}" sets env "${envName}" to "${value}", which ` +
|
|
42
|
+
`names no field. Use \`cpln://secret/${match[1]}.FIELD\` — \`.payload\` for opaque, ` +
|
|
43
|
+
`\`.username\`/\`.password\` for userpass, \`.cert\`/\`.key\` for tls, a key name for a ` +
|
|
44
|
+
`dictionary. An unqualified reference resolves to nothing at runtime.`,
|
|
45
|
+
entity: name,
|
|
46
|
+
lexicon: "cpln",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return diagnostics;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, entitiesOfType, exposedPorts, workloadType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CPL020: a serverless workload must expose exactly one HTTP port.
|
|
7
|
+
*
|
|
8
|
+
* Not "at least one" and not "any number" — exactly one, and it must be HTTP.
|
|
9
|
+
* Zero is the more common mistake and the more confusing one, because the
|
|
10
|
+
* workload deploys and reports healthy while serving nothing.
|
|
11
|
+
*/
|
|
12
|
+
export const serverlessSinglePortCheck: PostSynthCheck = {
|
|
13
|
+
id: "CPL020",
|
|
14
|
+
description: "Serverless workloads must expose exactly one HTTP port",
|
|
15
|
+
|
|
16
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
17
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
18
|
+
|
|
19
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
20
|
+
if (workloadType(entity) !== "serverless") continue;
|
|
21
|
+
|
|
22
|
+
const ports = exposedPorts(entity);
|
|
23
|
+
if (ports.length !== 1) {
|
|
24
|
+
diagnostics.push({
|
|
25
|
+
checkId: "CPL020",
|
|
26
|
+
severity: "error",
|
|
27
|
+
message:
|
|
28
|
+
`Serverless workload "${name}" exposes ${ports.length} ports; exactly one HTTP port is required. ` +
|
|
29
|
+
(ports.length === 0
|
|
30
|
+
? `It will deploy and serve nothing.`
|
|
31
|
+
: `Use a standard workload if it genuinely needs several.`),
|
|
32
|
+
entity: name,
|
|
33
|
+
lexicon: "cpln",
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The single port must be HTTP. `tcp` and `grpc` need a standard workload.
|
|
39
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
40
|
+
for (const port of readArray(container, "ports")) {
|
|
41
|
+
const protocol = readString(port, "protocol") ?? "http";
|
|
42
|
+
if (protocol === "http" || protocol === "http2") continue;
|
|
43
|
+
diagnostics.push({
|
|
44
|
+
checkId: "CPL020",
|
|
45
|
+
severity: "error",
|
|
46
|
+
message:
|
|
47
|
+
`Serverless workload "${name}" exposes a "${protocol}" port. Serverless supports only http ` +
|
|
48
|
+
`and http2 — use a standard workload for ${protocol}.`,
|
|
49
|
+
entity: name,
|
|
50
|
+
lexicon: "cpln",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return diagnostics;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readPath, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, entitiesOfType, exposedPorts, workloadType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CPL021: a cron workload needs a schedule and must expose no ports.
|
|
7
|
+
*
|
|
8
|
+
* Cron cannot serve traffic, so a port on one is always a misunderstanding —
|
|
9
|
+
* usually a workload converted from serverless with the ports left behind. A
|
|
10
|
+
* missing `spec.job.schedule` is the other half: `spec.job` is *required* for
|
|
11
|
+
* cron and forbidden for every other type.
|
|
12
|
+
*/
|
|
13
|
+
export const cronJobShapeCheck: PostSynthCheck = {
|
|
14
|
+
id: "CPL021",
|
|
15
|
+
description: "Cron workloads must declare a schedule and expose no ports",
|
|
16
|
+
|
|
17
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
18
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
19
|
+
|
|
20
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
21
|
+
const type = workloadType(entity);
|
|
22
|
+
const job = readPath(entity, "spec", "job");
|
|
23
|
+
const schedule = readString(entity, "spec", "job", "schedule");
|
|
24
|
+
|
|
25
|
+
if (type === "cron") {
|
|
26
|
+
if (!schedule) {
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
checkId: "CPL021",
|
|
29
|
+
severity: "error",
|
|
30
|
+
message: `Cron workload "${name}" has no spec.job.schedule. A cron workload requires one.`,
|
|
31
|
+
entity: name,
|
|
32
|
+
lexicon: "cpln",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ports = exposedPorts(entity);
|
|
37
|
+
if (ports.length > 0) {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
checkId: "CPL021",
|
|
40
|
+
severity: "error",
|
|
41
|
+
message:
|
|
42
|
+
`Cron workload "${name}" exposes ${ports.length} port(s) (${ports.join(", ")}). Cron workloads ` +
|
|
43
|
+
`cannot serve traffic — remove the ports, or use a standard workload.`,
|
|
44
|
+
entity: name,
|
|
45
|
+
lexicon: "cpln",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (job !== undefined) {
|
|
52
|
+
diagnostics.push({
|
|
53
|
+
checkId: "CPL021",
|
|
54
|
+
severity: "error",
|
|
55
|
+
message: `Workload "${name}" is type "${type}" but sets spec.job. Only cron workloads may declare one.`,
|
|
56
|
+
entity: name,
|
|
57
|
+
lexicon: "cpln",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return diagnostics;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
2
|
+
import { readArray, readNumber, readString } from "../../entity-props";
|
|
3
|
+
import { WORKLOAD, entitiesOfType } from "./helpers";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CPL022: `port` and `ports` are mutually exclusive, and port numbers must be
|
|
7
|
+
* unique across every container in a workload.
|
|
8
|
+
*
|
|
9
|
+
* The duplicate case is the one worth catching: two containers each binding
|
|
10
|
+
* 8080 is accepted at author time, rejected at apply, and reads as correct.
|
|
11
|
+
*/
|
|
12
|
+
export const containerPortsCheck: PostSynthCheck = {
|
|
13
|
+
id: "CPL022",
|
|
14
|
+
description: "Container ports must not mix `port` and `ports`, and must be unique across containers",
|
|
15
|
+
|
|
16
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
17
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
18
|
+
|
|
19
|
+
for (const [name, entity] of entitiesOfType(ctx.entities, WORKLOAD)) {
|
|
20
|
+
const seen = new Map<number, string>();
|
|
21
|
+
|
|
22
|
+
for (const container of readArray(entity, "spec", "containers")) {
|
|
23
|
+
const containerName = readString(container, "name") ?? "?";
|
|
24
|
+
const single = readNumber(container, "port");
|
|
25
|
+
const list = readArray(container, "ports");
|
|
26
|
+
|
|
27
|
+
if (single !== undefined && list.length > 0) {
|
|
28
|
+
diagnostics.push({
|
|
29
|
+
checkId: "CPL022",
|
|
30
|
+
severity: "error",
|
|
31
|
+
message:
|
|
32
|
+
`Workload "${name}" container "${containerName}" sets both \`port\` and \`ports\`. ` +
|
|
33
|
+
`They are mutually exclusive — keep \`ports\`.`,
|
|
34
|
+
entity: name,
|
|
35
|
+
lexicon: "cpln",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const numbers = [
|
|
40
|
+
...(single !== undefined ? [single] : []),
|
|
41
|
+
...list.map((port) => readNumber(port, "number")).filter((n): n is number => n !== undefined),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
for (const number of numbers) {
|
|
45
|
+
const owner = seen.get(number);
|
|
46
|
+
if (owner !== undefined && owner !== containerName) {
|
|
47
|
+
diagnostics.push({
|
|
48
|
+
checkId: "CPL022",
|
|
49
|
+
severity: "error",
|
|
50
|
+
message:
|
|
51
|
+
`Workload "${name}" exposes port ${number} from both "${owner}" and "${containerName}". ` +
|
|
52
|
+
`Port numbers must be unique across the containers of one workload.`,
|
|
53
|
+
entity: name,
|
|
54
|
+
lexicon: "cpln",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
seen.set(number, containerName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return diagnostics;
|
|
63
|
+
},
|
|
64
|
+
};
|