@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,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,9 @@
|
|
|
1
|
+
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
2
|
+
import { noSecretLiteralsRule } from "./cpl001-no-secret-literals";
|
|
3
|
+
import { preferResourceReferenceRule } from "./cpl002-prefer-resource-reference";
|
|
4
|
+
|
|
5
|
+
export { noSecretLiteralsRule } from "./cpl001-no-secret-literals";
|
|
6
|
+
export { preferResourceReferenceRule } from "./cpl002-prefer-resource-reference";
|
|
7
|
+
|
|
8
|
+
/** All source-level lint rules provided by this lexicon (imported by plugin.ts). */
|
|
9
|
+
export const rules: LintRule[] = [noSecretLiteralsRule, preferResourceReferenceRule];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import type { LintContext } from "@intentius/chant/lint/rule";
|
|
4
|
+
import { rules, noSecretLiteralsRule, preferResourceReferenceRule } from "./index";
|
|
5
|
+
|
|
6
|
+
function lint(rule: (typeof rules)[number], source: string) {
|
|
7
|
+
const sourceFile = ts.createSourceFile("infra.ts", source, ts.ScriptTarget.Latest, true);
|
|
8
|
+
return rule.check({ sourceFile, entities: [], filePath: "infra.ts", lexicon: "cpln" } as LintContext);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("cpln lint rules", () => {
|
|
12
|
+
it("registers rules with unique ids under the CPL prefix", () => {
|
|
13
|
+
expect(rules.length).toBeGreaterThan(0);
|
|
14
|
+
const ids = rules.map((rule) => rule.id);
|
|
15
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
16
|
+
for (const id of ids) expect(id.startsWith("CPL")).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("CPL001 no secret literals", () => {
|
|
21
|
+
it("flags a private key in a Secret", () => {
|
|
22
|
+
const found = lint(
|
|
23
|
+
noSecretLiteralsRule,
|
|
24
|
+
`const s = new Secret({ name: "tls", type: "tls", data: { key: "-----BEGIN RSA PRIVATE KEY-----abc" } });`,
|
|
25
|
+
);
|
|
26
|
+
expect(found).toHaveLength(1);
|
|
27
|
+
expect(found[0].ruleId).toBe("CPL001");
|
|
28
|
+
expect(found[0].severity).toBe("error");
|
|
29
|
+
expect(found[0].line).toBe(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("flags a database URL with an inline password in a Workload", () => {
|
|
33
|
+
const found = lint(
|
|
34
|
+
noSecretLiteralsRule,
|
|
35
|
+
`const w = new Workload({ name: "api", gvc: "prod", spec: { containers: [{ env: [{ name: "URL", value: "postgres://u:p@host/db" }] }] } });`,
|
|
36
|
+
);
|
|
37
|
+
expect(found).toHaveLength(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("flags an AWS access key id", () => {
|
|
41
|
+
expect(
|
|
42
|
+
lint(noSecretLiteralsRule, `const s = new Secret({ name: "k", data: { accessKey: "AKIAIOSFODNN7EXAMPLE" } });`),
|
|
43
|
+
).toHaveLength(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("ignores credential shapes outside a cpln declaration", () => {
|
|
47
|
+
// The rule is scoped to cpln constructors on purpose — a fixture in a test
|
|
48
|
+
// file or an unrelated const is not this rule's business.
|
|
49
|
+
expect(lint(noSecretLiteralsRule, `const fixture = "AKIAIOSFODNN7EXAMPLE";`)).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("accepts a secret reference", () => {
|
|
53
|
+
expect(
|
|
54
|
+
lint(
|
|
55
|
+
noSecretLiteralsRule,
|
|
56
|
+
`const w = new Workload({ spec: { containers: [{ env: [{ value: "cpln://secret/db.payload" }] }] } });`,
|
|
57
|
+
),
|
|
58
|
+
).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("CPL002 prefer a resource reference", () => {
|
|
63
|
+
it("flags a hand-written GVC-qualified link", () => {
|
|
64
|
+
const found = lint(
|
|
65
|
+
preferResourceReferenceRule,
|
|
66
|
+
`const w = new Workload({ name: "api", spec: { identityLink: "//gvc/prod/identity/api" } });`,
|
|
67
|
+
);
|
|
68
|
+
expect(found).toHaveLength(1);
|
|
69
|
+
expect(found[0].severity).toBe("info");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("flags a hand-written org-scoped link", () => {
|
|
73
|
+
expect(
|
|
74
|
+
lint(preferResourceReferenceRule, `const p = new Policy({ targetLinks: ["//secret/db-password"] });`),
|
|
75
|
+
).toHaveLength(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("leaves runtime resolution URIs alone", () => {
|
|
79
|
+
// `cpln://secret/…` is read by the container at runtime, not a reference
|
|
80
|
+
// between resources, so there is no reference form to prefer.
|
|
81
|
+
expect(
|
|
82
|
+
lint(
|
|
83
|
+
preferResourceReferenceRule,
|
|
84
|
+
`const w = new Workload({ spec: { containers: [{ env: [{ value: "cpln://secret/db.payload" }] }] } });`,
|
|
85
|
+
),
|
|
86
|
+
).toEqual([]);
|
|
87
|
+
expect(
|
|
88
|
+
lint(preferResourceReferenceRule, `const w = new Workload({ spec: { containers: [{ volumes: [{ uri: "cpln://volumeset/data" }] }] } });`),
|
|
89
|
+
).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("leaves location links alone", () => {
|
|
93
|
+
// `/org/…/location/…` names a platform-provided resource this lexicon does
|
|
94
|
+
// not model, so there is nothing to pass instead.
|
|
95
|
+
expect(
|
|
96
|
+
lint(
|
|
97
|
+
preferResourceReferenceRule,
|
|
98
|
+
`const g = new Gvc({ spec: { staticPlacement: { locationLinks: ["/org/acme/location/aws-us-east-1"] } } });`,
|
|
99
|
+
),
|
|
100
|
+
).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("ignores ordinary strings", () => {
|
|
104
|
+
expect(lint(preferResourceReferenceRule, `const w = new Workload({ name: "api", description: "the api" });`)).toEqual(
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { CompletionContext } from "@intentius/chant/lsp/types";
|
|
3
|
+
import { completions } from "./completions";
|
|
4
|
+
|
|
5
|
+
function ctx(content: string, linePrefix = content.split("\n").pop() ?? ""): CompletionContext {
|
|
6
|
+
const lines = content.split("\n");
|
|
7
|
+
return {
|
|
8
|
+
uri: "file:///infra.ts",
|
|
9
|
+
content,
|
|
10
|
+
position: { line: lines.length - 1, character: lines[lines.length - 1].length },
|
|
11
|
+
wordAtCursor: "",
|
|
12
|
+
linePrefix,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("cpln LSP completions", () => {
|
|
17
|
+
it("returns an array", () => {
|
|
18
|
+
expect(Array.isArray(completions(ctx("")))).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("offers the resource classes after `new `", () => {
|
|
22
|
+
const labels = completions(ctx("const w = new ")).map((item) => item.label);
|
|
23
|
+
for (const kind of ["Gvc", "Workload", "Identity", "VolumeSet", "Secret", "Policy", "Domain", "IpSet"]) {
|
|
24
|
+
expect(labels, `${kind} missing from completions`).toContain(kind);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("offers a workload's property names inside its constructor", () => {
|
|
29
|
+
const labels = completions(ctx("const w = new Workload({\n ", " ")).map((item) => item.label);
|
|
30
|
+
expect(labels).toContain("name");
|
|
31
|
+
expect(labels).toContain("spec");
|
|
32
|
+
// The property GVC-scoped kinds carry, which the OpenAPI document does not.
|
|
33
|
+
expect(labels).toContain("gvc");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("does not offer read-only attributes as properties", () => {
|
|
37
|
+
// These are attributes, not authoring surface — and Control Plane rejects a
|
|
38
|
+
// manifest carrying them, which is why they must not be suggested.
|
|
39
|
+
const labels = completions(ctx("const w = new Workload({\n ", " ")).map((item) => item.label);
|
|
40
|
+
for (const attribute of ["id", "status", "created", "lastModified", "links"]) {
|
|
41
|
+
expect(labels, `${attribute} is an attribute, not authoring surface`).not.toContain(attribute);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
|
|
3
|
+
import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
|
|
7
|
+
let cachedIndex: LexiconIndex | null = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The generated registry, loaded once. It carries every cpln resource and
|
|
11
|
+
* property type with its constraints, which is exactly what the shared
|
|
12
|
+
* completion provider indexes — so completions cannot drift from the types.
|
|
13
|
+
*/
|
|
14
|
+
function getIndex(): LexiconIndex {
|
|
15
|
+
if (cachedIndex) return cachedIndex;
|
|
16
|
+
const data = require("../generated/lexicon-cpln.json") as Record<string, LexiconEntry>;
|
|
17
|
+
cachedIndex = new LexiconIndex(data);
|
|
18
|
+
return cachedIndex;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* LSP completions for cpln resources — class names after `new `, property
|
|
23
|
+
* names inside a constructor.
|
|
24
|
+
*/
|
|
25
|
+
export function completions(ctx: CompletionContext): CompletionItem[] {
|
|
26
|
+
return lexiconCompletions(ctx, getIndex(), "cpln resource");
|
|
27
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { HoverContext } from "@intentius/chant/lsp/types";
|
|
3
|
+
import { hover } from "./hover";
|
|
4
|
+
|
|
5
|
+
function ctx(word: string): HoverContext {
|
|
6
|
+
const lineText = `const x = new ${word}({});`;
|
|
7
|
+
return {
|
|
8
|
+
uri: "file:///infra.ts",
|
|
9
|
+
content: lineText,
|
|
10
|
+
position: { line: 0, character: lineText.indexOf(word) + 1 },
|
|
11
|
+
word,
|
|
12
|
+
lineText,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("cpln LSP hover", () => {
|
|
17
|
+
it("returns nothing for an unknown identifier", () => {
|
|
18
|
+
expect(hover(ctx("NotACplnThing"))).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("names the Control Plane type and manifest kind for a resource", () => {
|
|
22
|
+
const info = hover(ctx("Workload"));
|
|
23
|
+
expect(info?.contents).toContain("Cpln::Core::Workload");
|
|
24
|
+
expect(info?.contents).toContain("kind: `workload`".replace("kind: ", "Manifest kind: "));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("says a GVC-scoped kind needs a gvc", () => {
|
|
28
|
+
expect(hover(ctx("Workload"))?.contents).toContain("GVC-scoped");
|
|
29
|
+
expect(hover(ctx("VolumeSet"))?.contents).toContain("GVC-scoped");
|
|
30
|
+
expect(hover(ctx("Gvc"))?.contents).not.toContain("GVC-scoped —");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("carries the silent-failure notes the types cannot express", () => {
|
|
34
|
+
// These are the reason hover exists here: the property list is already in
|
|
35
|
+
// the types; what an author cannot see is which mistakes fail quietly.
|
|
36
|
+
expect(hover(ctx("Identity"))?.contents).toContain("silently ignored");
|
|
37
|
+
expect(hover(ctx("Secret"))?.contents).toContain("fails silently");
|
|
38
|
+
expect(hover(ctx("VolumeSet"))?.contents).toContain("immutable");
|
|
39
|
+
expect(hover(ctx("Workload"))?.contents).toContain("immutable");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("spells out enumerated properties", () => {
|
|
43
|
+
const info = hover(ctx("Secret"));
|
|
44
|
+
expect(info?.contents).toContain("Enumerated properties:");
|
|
45
|
+
expect(info?.contents).toContain("`opaque`");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("marks a property type as nested and names its owner", () => {
|
|
49
|
+
const info = hover(ctx("WorkloadSpecContainers"));
|
|
50
|
+
expect(info?.contents).toContain("Property type");
|
|
51
|
+
expect(info?.contents).toContain("Workload");
|
|
52
|
+
});
|
|
53
|
+
});
|
package/src/lsp/hover.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
3
|
+
import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
import { KINDS, kindByClassName } from "../kinds";
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
|
|
8
|
+
let cachedIndex: LexiconIndex | null = null;
|
|
9
|
+
|
|
10
|
+
function getIndex(): LexiconIndex {
|
|
11
|
+
if (cachedIndex) return cachedIndex;
|
|
12
|
+
const data = require("../generated/lexicon-cpln.json") as Record<string, LexiconEntry>;
|
|
13
|
+
cachedIndex = new LexiconIndex(data);
|
|
14
|
+
return cachedIndex;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Per-kind notes.
|
|
19
|
+
*
|
|
20
|
+
* These carry the rules an author cannot infer from the property names, and
|
|
21
|
+
* specifically the ones whose violation fails *silently* — an unqualified
|
|
22
|
+
* identity link, a scale-to-zero that never happens, an immutable field. The
|
|
23
|
+
* property list is already in the types; what hover adds is the part the types
|
|
24
|
+
* cannot say.
|
|
25
|
+
*/
|
|
26
|
+
const KIND_NOTES: Record<string, string> = {
|
|
27
|
+
Gvc:
|
|
28
|
+
"The placement and networking boundary. Workloads, identities and volume sets live inside one.\n\n" +
|
|
29
|
+
"Placement is set here, not per workload (`spec.staticPlacement.locationLinks`, e.g. " +
|
|
30
|
+
"`/org/acme/location/aws-us-east-1`). **Pull secrets are also GVC-level** (`spec.pullSecretLinks`) — " +
|
|
31
|
+
"only `docker`, `ecr` and `gcp` secret types are valid. KEDA must be enabled here before a standard or " +
|
|
32
|
+
"stateful workload in the GVC can use it.",
|
|
33
|
+
|
|
34
|
+
Workload:
|
|
35
|
+
"A running unit of work. **`type` and `name` are immutable** — changing either means delete and recreate.\n\n" +
|
|
36
|
+
"- `serverless` — exactly one HTTP port, required. Scales to zero with `rps` or `concurrency`.\n" +
|
|
37
|
+
"- `standard` — multiple ports, non-HTTP allowed. Scale to zero via KEDA only.\n" +
|
|
38
|
+
"- `stateful` — stable replica identity; the only type that mounts `ext4`/`xfs` volumes or uses " +
|
|
39
|
+
"`replicaDirect` load balancing.\n" +
|
|
40
|
+
"- `cron` — must set `spec.job.schedule` and expose no ports. Probes, autoscaling and `timeoutSeconds` " +
|
|
41
|
+
"are accepted and ignored.\n\n" +
|
|
42
|
+
"Capacity AI is **on by default** for serverless, standard and cron, and conflicts with CPU-utilization " +
|
|
43
|
+
"autoscaling, multi-metric autoscaling, and GPUs. Memory(MiB)/CPU(millicores) must be ≤ 8.",
|
|
44
|
+
|
|
45
|
+
Identity:
|
|
46
|
+
"A workload's identity: cloud-provider access, network resources, and the principal a policy grants to.\n\n" +
|
|
47
|
+
"**GVC-scoped and not shareable** — an identity cannot be used from another GVC; declare one per GVC with " +
|
|
48
|
+
"the same spec. A workload has at most one. Each provider section allows one account " +
|
|
49
|
+
"(AWS `roleName` ⊻ `policyRefs`, GCP `serviceAccount` ⊻ `bindings`).\n\n" +
|
|
50
|
+
"In a policy binding the principal must be `//gvc/GVC/identity/NAME`. The bare `//identity/NAME` is " +
|
|
51
|
+
"**silently ignored** (CPL013).",
|
|
52
|
+
|
|
53
|
+
VolumeSet:
|
|
54
|
+
"Persistent storage inside one GVC.\n\n" +
|
|
55
|
+
"**`fileSystemType` and `performanceClass` are both immutable** — changing either means delete, recreate, " +
|
|
56
|
+
"and data loss. `ext4`/`xfs` are RWO and bind to exactly one *stateful* workload; `shared` is RWX, works " +
|
|
57
|
+
"with any type, and supports no snapshots. `high-throughput-ssd` has a 200 GB floor where the others have " +
|
|
58
|
+
"10 GB. Mount with `cpln://volumeset/NAME`.",
|
|
59
|
+
|
|
60
|
+
Secret:
|
|
61
|
+
"An org-scoped secret, in one of 12 types.\n\n" +
|
|
62
|
+
"Reading one at runtime takes three steps, and **missing any one fails silently**: the workload has an " +
|
|
63
|
+
"identity, a policy grants that identity `reveal` on the secret, and the reference is field-qualified — " +
|
|
64
|
+
"`cpln://secret/NAME.payload`, not `cpln://secret/NAME` (CPL014).",
|
|
65
|
+
|
|
66
|
+
Policy:
|
|
67
|
+
"Binds permissions to principals over a target.\n\n" +
|
|
68
|
+
"`targetKind` is singular and lowercase. Pick exactly one scope: `target: \"all\"`, `targetLinks`, or " +
|
|
69
|
+
"`targetQuery`. `ipset` and `mk8s` are **not** valid targets — they are governed through their parent. " +
|
|
70
|
+
"Permissions in a binding must be sorted and unique. Never set `origin` by hand.",
|
|
71
|
+
|
|
72
|
+
Domain:
|
|
73
|
+
"A custom domain with its TLS, CORS and routing.\n\n" +
|
|
74
|
+
"**Apex domains must use `dnsMode: cname`** — NS does not support apex. NS mode requires " +
|
|
75
|
+
"`certChallengeType: dns01`. `gvcLink`, `workloadLink` and `ports[].routes` are mutually exclusive, and " +
|
|
76
|
+
"`workloadLink` (replica-direct) is stateful-only. Every route must target workloads in one GVC.",
|
|
77
|
+
|
|
78
|
+
IpSet:
|
|
79
|
+
"Dedicated IP addresses reserved in named locations and bound to a workload.\n\n" +
|
|
80
|
+
"Not a valid policy target — access is governed through the workload it fronts.",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Enum-valued props are worth spelling out inline — they are the typo surface. */
|
|
84
|
+
function enumLines(entry: LexiconEntry): string[] {
|
|
85
|
+
const constraints = entry.propertyConstraints as Record<string, { enum?: unknown[] }> | undefined;
|
|
86
|
+
if (!constraints) return [];
|
|
87
|
+
|
|
88
|
+
const lines: string[] = [];
|
|
89
|
+
for (const [prop, constraint] of Object.entries(constraints)) {
|
|
90
|
+
if (!Array.isArray(constraint?.enum) || constraint.enum.length === 0) continue;
|
|
91
|
+
lines.push(`- \`${prop}\`: ${constraint.enum.map((v) => `\`${String(v)}\``).join(" | ")}`);
|
|
92
|
+
}
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function cplnHover(className: string, entry: LexiconEntry): HoverInfo | undefined {
|
|
97
|
+
const lines: string[] = [`**${className}**`, "", `Control Plane type: \`${entry.resourceType}\``];
|
|
98
|
+
|
|
99
|
+
const kind = kindByClassName(className);
|
|
100
|
+
if (!kind) {
|
|
101
|
+
// A property type — a nested shape rather than a standalone declarable.
|
|
102
|
+
// Naming its owner is the useful part, since the names are path-derived.
|
|
103
|
+
const owner = KINDS.find((k) => className.startsWith(k.className));
|
|
104
|
+
lines.push(
|
|
105
|
+
"",
|
|
106
|
+
owner
|
|
107
|
+
? `*Property type — a nested shape within \`${owner.className}\`, not a standalone declarable.*`
|
|
108
|
+
: "*Property type — a nested shape, not a standalone declarable.*",
|
|
109
|
+
);
|
|
110
|
+
const propertyEnums = enumLines(entry);
|
|
111
|
+
if (propertyEnums.length > 0) lines.push("", "Enumerated properties:", ...propertyEnums);
|
|
112
|
+
return { contents: lines.join("\n") };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
lines.push("", `Manifest kind: \`${kind.kind}\``);
|
|
116
|
+
if (kind.gvcScoped) lines.push("", "GVC-scoped — requires a `gvc`.");
|
|
117
|
+
|
|
118
|
+
const note = KIND_NOTES[className];
|
|
119
|
+
if (note) lines.push("", note);
|
|
120
|
+
|
|
121
|
+
const enums = enumLines(entry);
|
|
122
|
+
if (enums.length > 0) lines.push("", "Enumerated properties:", ...enums);
|
|
123
|
+
|
|
124
|
+
return { contents: lines.join("\n") };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Provide LSP hover information for cpln resources. */
|
|
128
|
+
export function hover(ctx: HoverContext): HoverInfo | undefined {
|
|
129
|
+
return lexiconHover(ctx, getIndex(), cplnHover);
|
|
130
|
+
}
|
package/src/ownership.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cpln lexicon's ownership convention.
|
|
3
|
+
*
|
|
4
|
+
* Declared here rather than in core — the same seam aws (`chant:managed-by`),
|
|
5
|
+
* azure and fly sit on. Core owns the generic stamp/detect logic; each lexicon
|
|
6
|
+
* owns key names its provider will actually accept.
|
|
7
|
+
*
|
|
8
|
+
* Control Plane gives every kind a free-form `tags` map, which makes this the
|
|
9
|
+
* cleanest ownership channel of any target chant supports: one uniform place on
|
|
10
|
+
* all eight kinds, readable straight off a plain `GET`, with no per-kind
|
|
11
|
+
* special case of the sort fly needs for volumes and certificates.
|
|
12
|
+
*
|
|
13
|
+
* Key naming. Tag keys accept a `/`, which Control Plane uses itself for its
|
|
14
|
+
* reserved `cpln/…` keys (`cpln/relaxMemoryToCpuRatio`). A prefixed key is
|
|
15
|
+
* therefore both idiomatic and safely namespaced against a future reserved key,
|
|
16
|
+
* so these follow the same `chant.intentius.io/…` convention as the shared
|
|
17
|
+
* label channel rather than inventing a flat spelling. The `cpln/` prefix is
|
|
18
|
+
* Control Plane's own and is never written here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ChannelKeys } from "@intentius/chant/ownership";
|
|
22
|
+
|
|
23
|
+
/** Tag keys carrying chant's ownership marker on every cpln resource. */
|
|
24
|
+
export const CPLN_TAG_OWNERSHIP_KEYS: ChannelKeys = {
|
|
25
|
+
managedBy: "chant.intentius.io/managed-by",
|
|
26
|
+
stack: "chant.intentius.io/stack",
|
|
27
|
+
env: "chant.intentius.io/env",
|
|
28
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Entry point for `npm run bundle` — generates `src/generated/` and writes the
|
|
4
|
+
* `dist/` bundle.
|
|
5
|
+
*
|
|
6
|
+
* Top-level await rather than an async wrapper, matching the other lexicons:
|
|
7
|
+
* an async main() leaves event-loop references that keep the process alive
|
|
8
|
+
* after the bundle is written.
|
|
9
|
+
*/
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { writeBundleSpec } from "@intentius/chant/codegen/package";
|
|
13
|
+
import { packageLexicon } from "./codegen/package";
|
|
14
|
+
|
|
15
|
+
const pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const distDir = join(pkgDir, "..", "dist");
|
|
17
|
+
|
|
18
|
+
const verbose = process.argv.includes("--verbose") || !process.argv.includes("--quiet");
|
|
19
|
+
const force = process.argv.includes("--force");
|
|
20
|
+
|
|
21
|
+
const { spec, stats } = await packageLexicon({ verbose, force });
|
|
22
|
+
writeBundleSpec(spec, distDir);
|
|
23
|
+
|
|
24
|
+
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
25
|
+
console.error(`dist/ written to ${distDir}`);
|