@intentius/chant-lexicon-k3s 0.44.3
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 +36 -0
- package/dist/codegen/docs.d.ts +7 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate-lexicon.d.ts +19 -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 +13 -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/codegen/parse.d.ts +93 -0
- package/dist/codegen/parse.d.ts.map +1 -0
- package/dist/generated/index.d.ts +8 -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 +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/integrity.json +10 -0
- package/dist/lint/audit-catalog.d.ts +13 -0
- package/dist/lint/audit-catalog.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/post-synth/k3s-helpers.d.ts +19 -0
- package/dist/lint/post-synth/k3s-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/k3s101.d.ts +13 -0
- package/dist/lint/post-synth/k3s101.d.ts.map +1 -0
- package/dist/lint/post-synth/k3s102.d.ts +11 -0
- package/dist/lint/post-synth/k3s102.d.ts.map +1 -0
- package/dist/lint/post-synth/k3s103.d.ts +11 -0
- package/dist/lint/post-synth/k3s103.d.ts.map +1 -0
- package/dist/lint/post-synth/k3s104.d.ts +12 -0
- package/dist/lint/post-synth/k3s104.d.ts.map +1 -0
- package/dist/lint/post-synth/k3s105.d.ts +11 -0
- package/dist/lint/post-synth/k3s105.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +5 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/token-literal.d.ts +17 -0
- package/dist/lint/rules/token-literal.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 +6 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +6 -0
- package/dist/meta.json +37 -0
- package/dist/okf/index.md +9 -0
- package/dist/okf/types/Agent.md +55 -0
- package/dist/okf/types/Registries.md +14 -0
- package/dist/okf/types/Server.md +122 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/serializer.d.ts +37 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skills/chant-k3s.md +90 -0
- package/dist/spec/fetch.d.ts +39 -0
- package/dist/spec/fetch.d.ts.map +1 -0
- package/dist/types/index.d.ts +370 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate.d.ts +15 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +68 -0
- package/src/codegen/docs.ts +25 -0
- package/src/codegen/generate-cli.ts +8 -0
- package/src/codegen/generate-lexicon.ts +58 -0
- package/src/codegen/generate-typescript.ts +63 -0
- package/src/codegen/generate.test.ts +157 -0
- package/src/codegen/generate.ts +116 -0
- package/src/codegen/naming.ts +42 -0
- package/src/codegen/package.ts +28 -0
- package/src/codegen/parse.ts +386 -0
- package/src/generated/.gitkeep +0 -0
- package/src/generated/index.d.ts +370 -0
- package/src/generated/index.ts +13 -0
- package/src/generated/lexicon-k3s.json +37 -0
- package/src/generated/runtime.ts +4 -0
- package/src/index.ts +11 -0
- package/src/lint/audit-catalog.ts +53 -0
- package/src/lint/post-synth/index.ts +15 -0
- package/src/lint/post-synth/k3s-helpers.ts +34 -0
- package/src/lint/post-synth/k3s101.ts +58 -0
- package/src/lint/post-synth/k3s102.ts +59 -0
- package/src/lint/post-synth/k3s103.ts +38 -0
- package/src/lint/post-synth/k3s104.ts +44 -0
- package/src/lint/post-synth/k3s105.ts +52 -0
- package/src/lint/post-synth/post-synth.test.ts +137 -0
- package/src/lint/rules/index.ts +7 -0
- package/src/lint/rules/token-literal.test.ts +53 -0
- package/src/lint/rules/token-literal.ts +87 -0
- package/src/lsp/completions.test.ts +10 -0
- package/src/lsp/completions.ts +25 -0
- package/src/lsp/hover.test.ts +10 -0
- package/src/lsp/hover.ts +43 -0
- package/src/package-cli.ts +24 -0
- package/src/plugin.test.ts +17 -0
- package/src/plugin.ts +129 -0
- package/src/serializer.acceptance.test.ts +137 -0
- package/src/serializer.test.ts +172 -0
- package/src/serializer.ts +191 -0
- package/src/skills/chant-k3s.md +90 -0
- package/src/spec/fetch.ts +80 -0
- package/src/validate-cli.ts +5 -0
- package/src/validate.ts +41 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K3S103: an agent that joins nothing.
|
|
3
|
+
*
|
|
4
|
+
* An agent config without `server` is a node with no cluster to join —
|
|
5
|
+
* `k3s agent` refuses to start without it (or K3S_URL in the service
|
|
6
|
+
* environment). Declaring the agent while leaving the join out of band
|
|
7
|
+
* defeats the declaration, so this asks for it in the config.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
PostSynthCheck,
|
|
12
|
+
PostSynthContext,
|
|
13
|
+
PostSynthDiagnostic,
|
|
14
|
+
} from "@intentius/chant/lint/post-synth";
|
|
15
|
+
import { entitiesOfType } from "./k3s-helpers";
|
|
16
|
+
|
|
17
|
+
export const k3s103: PostSynthCheck = {
|
|
18
|
+
id: "K3S103",
|
|
19
|
+
description: "An agent config declares no server to join",
|
|
20
|
+
|
|
21
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
22
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
23
|
+
for (const entity of entitiesOfType(ctx, "K3s::Agent")) {
|
|
24
|
+
const server = entity.props.server;
|
|
25
|
+
if (typeof server === "string" && server.length > 0) continue;
|
|
26
|
+
diagnostics.push({
|
|
27
|
+
checkId: "K3S103",
|
|
28
|
+
severity: "error",
|
|
29
|
+
message:
|
|
30
|
+
`"${entity.name}" declares no \`server\` — the agent has no cluster to join. ` +
|
|
31
|
+
"Set `server: https://<server-host>:6443` in the declaration.",
|
|
32
|
+
entity: entity.name,
|
|
33
|
+
lexicon: "k3s",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return diagnostics;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K3S104: kubeconfig written wider than 0644.
|
|
3
|
+
*
|
|
4
|
+
* `write-kubeconfig-mode` widens /etc/rancher/k3s/k3s.yaml — the file
|
|
5
|
+
* carrying the cluster's admin credential. k3s's own default is 0600;
|
|
6
|
+
* 0644 is the documented convenience for a single-user host. Anything
|
|
7
|
+
* granting write to group/other, or beyond read to other, hands the
|
|
8
|
+
* admin kubeconfig to every local account.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
PostSynthCheck,
|
|
13
|
+
PostSynthContext,
|
|
14
|
+
PostSynthDiagnostic,
|
|
15
|
+
} from "@intentius/chant/lint/post-synth";
|
|
16
|
+
import { entitiesOfType } from "./k3s-helpers";
|
|
17
|
+
|
|
18
|
+
export const k3s104: PostSynthCheck = {
|
|
19
|
+
id: "K3S104",
|
|
20
|
+
description: "write-kubeconfig-mode is wider than 0644",
|
|
21
|
+
|
|
22
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
23
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
24
|
+
for (const entity of entitiesOfType(ctx, "K3s::Server")) {
|
|
25
|
+
const raw = entity.props["write-kubeconfig-mode"];
|
|
26
|
+
if (raw === undefined) continue;
|
|
27
|
+
const mode = parseInt(String(raw), 8);
|
|
28
|
+
if (Number.isNaN(mode)) continue;
|
|
29
|
+
// Bits outside rw-r--r-- mean group/other write, or execute anywhere.
|
|
30
|
+
if ((mode & ~0o644) !== 0) {
|
|
31
|
+
diagnostics.push({
|
|
32
|
+
checkId: "K3S104",
|
|
33
|
+
severity: "warning",
|
|
34
|
+
message:
|
|
35
|
+
`"${entity.name}" sets write-kubeconfig-mode ${String(raw)} — wider than 0644. ` +
|
|
36
|
+
"The kubeconfig carries the cluster admin credential; keep it 0600, or 0644 at most.",
|
|
37
|
+
entity: entity.name,
|
|
38
|
+
lexicon: "k3s",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return diagnostics;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K3S105: a registry mirror that skips TLS verification.
|
|
3
|
+
*
|
|
4
|
+
* `insecure_skip_verify: true` in registries.yaml turns every image pull
|
|
5
|
+
* through that registry into an unauthenticated trust decision. Legitimate
|
|
6
|
+
* for a lab pull-through cache; worth a flag anywhere, because the estate
|
|
7
|
+
* that sets it for the lab keeps it when the registry moves.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
PostSynthCheck,
|
|
12
|
+
PostSynthContext,
|
|
13
|
+
PostSynthDiagnostic,
|
|
14
|
+
} from "@intentius/chant/lint/post-synth";
|
|
15
|
+
import { entitiesOfType } from "./k3s-helpers";
|
|
16
|
+
|
|
17
|
+
export const k3s105: PostSynthCheck = {
|
|
18
|
+
id: "K3S105",
|
|
19
|
+
description: "registries.yaml disables TLS verification for a registry",
|
|
20
|
+
|
|
21
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
22
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
23
|
+
for (const entity of entitiesOfType(ctx, "K3s::Registries")) {
|
|
24
|
+
const configs = entity.props.configs;
|
|
25
|
+
if (typeof configs !== "object" || configs === null) continue;
|
|
26
|
+
for (const [registry, config] of Object.entries(configs as Record<string, unknown>)) {
|
|
27
|
+
const tls = resolveProps(resolveProps(config)?.tls);
|
|
28
|
+
if (tls?.insecure_skip_verify === true) {
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
checkId: "K3S105",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
message:
|
|
33
|
+
`"${entity.name}" disables TLS verification for registry "${registry}" — ` +
|
|
34
|
+
"every pull through it is unauthenticated. Pin the registry CA via `ca_file` instead.",
|
|
35
|
+
entity: entity.name,
|
|
36
|
+
lexicon: "k3s",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return diagnostics;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function resolveProps(value: unknown): Record<string, unknown> | undefined {
|
|
46
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
47
|
+
const obj = value as Record<string, unknown>;
|
|
48
|
+
if (typeof obj.props === "object" && obj.props !== null && "entityType" in obj) {
|
|
49
|
+
return obj.props as Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
return obj;
|
|
52
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { k3s101 } from "./k3s101";
|
|
4
|
+
import { k3s102 } from "./k3s102";
|
|
5
|
+
import { k3s103 } from "./k3s103";
|
|
6
|
+
import { k3s104 } from "./k3s104";
|
|
7
|
+
import { k3s105 } from "./k3s105";
|
|
8
|
+
|
|
9
|
+
function makeCtx(entities: Record<string, { entityType: string; props: Record<string, unknown> }>): PostSynthContext {
|
|
10
|
+
return {
|
|
11
|
+
outputs: new Map(),
|
|
12
|
+
entities: new Map(Object.entries(entities)),
|
|
13
|
+
} as unknown as PostSynthContext;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("K3S101: literal secret in a config entity", () => {
|
|
17
|
+
test("flags a literal token on a server", () => {
|
|
18
|
+
const diags = k3s101.check(
|
|
19
|
+
makeCtx({ cp: { entityType: "K3s::Server", props: { token: "hunter2" } } }),
|
|
20
|
+
);
|
|
21
|
+
expect(diags).toHaveLength(1);
|
|
22
|
+
expect(diags[0].checkId).toBe("K3S101");
|
|
23
|
+
expect(diags[0].severity).toBe("error");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("flags an etcd S3 secret key", () => {
|
|
27
|
+
const diags = k3s101.check(
|
|
28
|
+
makeCtx({ cp: { entityType: "K3s::Server", props: { "etcd-s3-secret-key": "shh" } } }),
|
|
29
|
+
);
|
|
30
|
+
expect(diags).toHaveLength(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("passes token-file and an agent token on the agent side", () => {
|
|
34
|
+
const diags = k3s101.check(
|
|
35
|
+
makeCtx({
|
|
36
|
+
cp: { entityType: "K3s::Server", props: { "token-file": "/etc/rancher/k3s/token" } },
|
|
37
|
+
worker: { entityType: "K3s::Agent", props: { server: "https://cp:6443", "token-file": "/t" } },
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
expect(diags).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("K3S102: literal registry credential", () => {
|
|
45
|
+
test("flags a literal password", () => {
|
|
46
|
+
const diags = k3s102.check(
|
|
47
|
+
makeCtx({
|
|
48
|
+
registries: {
|
|
49
|
+
entityType: "K3s::Registries",
|
|
50
|
+
props: { configs: { "registry.example.com": { auth: { username: "u", password: "p" } } } },
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
expect(diags).toHaveLength(1);
|
|
55
|
+
expect(diags[0].checkId).toBe("K3S102");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("passes username-only auth and TLS file paths", () => {
|
|
59
|
+
const diags = k3s102.check(
|
|
60
|
+
makeCtx({
|
|
61
|
+
registries: {
|
|
62
|
+
entityType: "K3s::Registries",
|
|
63
|
+
props: {
|
|
64
|
+
configs: { "registry.example.com": { auth: { username: "u" }, tls: { ca_file: "/ca" } } },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
expect(diags).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("K3S103: agent with no server", () => {
|
|
74
|
+
test("flags an agent that joins nothing", () => {
|
|
75
|
+
const diags = k3s103.check(makeCtx({ worker: { entityType: "K3s::Agent", props: {} } }));
|
|
76
|
+
expect(diags).toHaveLength(1);
|
|
77
|
+
expect(diags[0].checkId).toBe("K3S103");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("passes an agent with a server, and ignores servers entirely", () => {
|
|
81
|
+
const diags = k3s103.check(
|
|
82
|
+
makeCtx({
|
|
83
|
+
worker: { entityType: "K3s::Agent", props: { server: "https://cp:6443" } },
|
|
84
|
+
cp: { entityType: "K3s::Server", props: {} },
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
expect(diags).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("K3S104: kubeconfig mode wider than 0644", () => {
|
|
92
|
+
test("flags 0666", () => {
|
|
93
|
+
const diags = k3s104.check(
|
|
94
|
+
makeCtx({ cp: { entityType: "K3s::Server", props: { "write-kubeconfig-mode": "0666" } } }),
|
|
95
|
+
);
|
|
96
|
+
expect(diags).toHaveLength(1);
|
|
97
|
+
expect(diags[0].severity).toBe("warning");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("passes 0600, 0644, and an unset mode", () => {
|
|
101
|
+
const diags = k3s104.check(
|
|
102
|
+
makeCtx({
|
|
103
|
+
a: { entityType: "K3s::Server", props: { "write-kubeconfig-mode": "0600" } },
|
|
104
|
+
b: { entityType: "K3s::Server", props: { "write-kubeconfig-mode": "0644" } },
|
|
105
|
+
c: { entityType: "K3s::Server", props: {} },
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
expect(diags).toHaveLength(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("K3S105: TLS verification disabled for a registry", () => {
|
|
113
|
+
test("flags insecure_skip_verify", () => {
|
|
114
|
+
const diags = k3s105.check(
|
|
115
|
+
makeCtx({
|
|
116
|
+
registries: {
|
|
117
|
+
entityType: "K3s::Registries",
|
|
118
|
+
props: { configs: { "registry.example.com": { tls: { insecure_skip_verify: true } } } },
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
expect(diags).toHaveLength(1);
|
|
123
|
+
expect(diags[0].checkId).toBe("K3S105");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("passes a pinned CA", () => {
|
|
127
|
+
const diags = k3s105.check(
|
|
128
|
+
makeCtx({
|
|
129
|
+
registries: {
|
|
130
|
+
entityType: "K3s::Registries",
|
|
131
|
+
props: { configs: { "registry.example.com": { tls: { ca_file: "/ca" } } } },
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
expect(diags).toHaveLength(0);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
2
|
+
import { tokenLiteralRule } from "./token-literal";
|
|
3
|
+
|
|
4
|
+
export { tokenLiteralRule } from "./token-literal";
|
|
5
|
+
|
|
6
|
+
/** All lint rules provided by this lexicon (imported by plugin.ts's lintRules()). */
|
|
7
|
+
export const rules: LintRule[] = [tokenLiteralRule];
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
import type { LintContext } from "@intentius/chant/lint/rule";
|
|
4
|
+
import { tokenLiteralRule } from "./token-literal";
|
|
5
|
+
|
|
6
|
+
function createContext(code: string, fileName = "cluster.ts"): LintContext {
|
|
7
|
+
const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
|
|
8
|
+
return { sourceFile, entities: [], filePath: fileName };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("K3S001: literal join token", () => {
|
|
12
|
+
test("flags a literal token inside new Server(...)", () => {
|
|
13
|
+
const diags = tokenLiteralRule.check(
|
|
14
|
+
createContext(`const s = new Server({ "cluster-init": true, token: "K10abc::server:secret" });`),
|
|
15
|
+
);
|
|
16
|
+
expect(diags).toHaveLength(1);
|
|
17
|
+
expect(diags[0].ruleId).toBe("K3S001");
|
|
18
|
+
expect(diags[0].severity).toBe("error");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("flags a quoted agent-token key inside Agent(...)", () => {
|
|
22
|
+
const diags = tokenLiteralRule.check(
|
|
23
|
+
createContext(`const a = Agent({ server: "https://cp:6443", "agent-token": \`secret\` });`),
|
|
24
|
+
);
|
|
25
|
+
expect(diags).toHaveLength(1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("does not flag token-file", () => {
|
|
29
|
+
const diags = tokenLiteralRule.check(
|
|
30
|
+
createContext(`const s = new Server({ "token-file": "/etc/rancher/k3s/token" });`),
|
|
31
|
+
);
|
|
32
|
+
expect(diags).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("does not flag a non-literal token", () => {
|
|
36
|
+
const diags = tokenLiteralRule.check(
|
|
37
|
+
createContext(`const s = new Server({ token: readTokenSomehow() });`),
|
|
38
|
+
);
|
|
39
|
+
expect(diags).toHaveLength(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("does not flag tokens outside Server/Agent constructions", () => {
|
|
43
|
+
const diags = tokenLiteralRule.check(
|
|
44
|
+
createContext(`const config = { token: "not-k3s's-business" };`),
|
|
45
|
+
);
|
|
46
|
+
expect(diags).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("does not flag an empty literal", () => {
|
|
50
|
+
const diags = tokenLiteralRule.check(createContext(`const s = new Server({ token: "" });`));
|
|
51
|
+
expect(diags).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* K3S001: literal join token in source.
|
|
6
|
+
*
|
|
7
|
+
* `token` / `agent-token` are the cluster's shared join secret, and a
|
|
8
|
+
* committed config declaration is exactly the file that leaks it. The
|
|
9
|
+
* typed surface deliberately carries only the reference forms
|
|
10
|
+
* (`token-file`, `agent-token-file` — #1601), but nothing stops a raw
|
|
11
|
+
* object literal from writing the key anyway; this rule does.
|
|
12
|
+
*
|
|
13
|
+
* Flags a non-empty string-literal `token` / `agent-token` (identifier or
|
|
14
|
+
* quoted) inside a `Server(...)` / `Agent(...)` construction, with or
|
|
15
|
+
* without `new`. A bare `token:` elsewhere in the file is someone else's
|
|
16
|
+
* business.
|
|
17
|
+
*/
|
|
18
|
+
export const tokenLiteralRule: LintRule = {
|
|
19
|
+
id: "K3S001",
|
|
20
|
+
severity: "error",
|
|
21
|
+
category: "security",
|
|
22
|
+
description: "k3s join token declared as a string literal in source",
|
|
23
|
+
|
|
24
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
25
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
26
|
+
const source = context.sourceFile;
|
|
27
|
+
|
|
28
|
+
const flag = (node: ts.Node, key: string) => {
|
|
29
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
30
|
+
diagnostics.push({
|
|
31
|
+
ruleId: "K3S001",
|
|
32
|
+
severity: "error",
|
|
33
|
+
message:
|
|
34
|
+
`\`${key}\` is a literal in source — a committed k3s config is a leaked cluster join ` +
|
|
35
|
+
"secret. Point `token-file` at a path on the host, or supply K3S_TOKEN_FILE at " +
|
|
36
|
+
"install time; the value itself never belongs in the declaration.",
|
|
37
|
+
file: context.filePath,
|
|
38
|
+
line: line + 1,
|
|
39
|
+
column: character + 1,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const tokenLiteral = (
|
|
44
|
+
obj: ts.ObjectLiteralExpression,
|
|
45
|
+
): { node: ts.Node; key: string } | undefined => {
|
|
46
|
+
for (const prop of obj.properties) {
|
|
47
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
48
|
+
const name = ts.isIdentifier(prop.name)
|
|
49
|
+
? prop.name.text
|
|
50
|
+
: ts.isStringLiteral(prop.name)
|
|
51
|
+
? prop.name.text
|
|
52
|
+
: undefined;
|
|
53
|
+
if (name !== "token" && name !== "agent-token") continue;
|
|
54
|
+
if (
|
|
55
|
+
(ts.isStringLiteral(prop.initializer) ||
|
|
56
|
+
ts.isNoSubstitutionTemplateLiteral(prop.initializer)) &&
|
|
57
|
+
prop.initializer.text.length > 0
|
|
58
|
+
) {
|
|
59
|
+
return { node: prop.initializer, key: name };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const visit = (node: ts.Node) => {
|
|
66
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
67
|
+
const callee = node.expression;
|
|
68
|
+
const name = ts.isIdentifier(callee)
|
|
69
|
+
? callee.text
|
|
70
|
+
: ts.isPropertyAccessExpression(callee)
|
|
71
|
+
? callee.name.text
|
|
72
|
+
: undefined;
|
|
73
|
+
if (name === "Server" || name === "Agent") {
|
|
74
|
+
const arg = node.arguments?.[0];
|
|
75
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
76
|
+
const literal = tokenLiteral(arg);
|
|
77
|
+
if (literal) flag(literal.node, literal.key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
ts.forEachChild(node, visit);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
visit(source);
|
|
85
|
+
return diagnostics;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { completions } from "./completions";
|
|
3
|
+
|
|
4
|
+
describe("LSP completions", () => {
|
|
5
|
+
it("returns an array", () => {
|
|
6
|
+
// TODO: Replace with a real CompletionContext
|
|
7
|
+
const result = completions({} as any);
|
|
8
|
+
expect(Array.isArray(result)).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
|
|
3
|
+
import {
|
|
4
|
+
LexiconIndex,
|
|
5
|
+
lexiconCompletions,
|
|
6
|
+
type LexiconEntry,
|
|
7
|
+
} from "@intentius/chant/lsp/lexicon-providers";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
let cachedIndex: LexiconIndex | null = null;
|
|
11
|
+
|
|
12
|
+
function getIndex(): LexiconIndex {
|
|
13
|
+
if (cachedIndex) return cachedIndex;
|
|
14
|
+
const data = require("../generated/lexicon-k3s.json") as Record<string, LexiconEntry>;
|
|
15
|
+
cachedIndex = new LexiconIndex(data);
|
|
16
|
+
return cachedIndex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Provide k3s completions based on context — Server, Agent, Registries and
|
|
21
|
+
* the registry property entities, from the generated registry.
|
|
22
|
+
*/
|
|
23
|
+
export function completions(ctx: CompletionContext): CompletionItem[] {
|
|
24
|
+
return lexiconCompletions(ctx, getIndex(), "k3s entity");
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { hover } from "./hover";
|
|
3
|
+
|
|
4
|
+
describe("LSP hover", () => {
|
|
5
|
+
it("returns undefined for unknown context", () => {
|
|
6
|
+
// TODO: Replace with a real HoverContext
|
|
7
|
+
const result = hover({} as any);
|
|
8
|
+
expect(result).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
});
|
package/src/lsp/hover.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
3
|
+
import {
|
|
4
|
+
LexiconIndex,
|
|
5
|
+
lexiconHover,
|
|
6
|
+
type LexiconEntry,
|
|
7
|
+
} from "@intentius/chant/lsp/lexicon-providers";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
let cachedIndex: LexiconIndex | null = null;
|
|
11
|
+
|
|
12
|
+
function getIndex(): LexiconIndex {
|
|
13
|
+
if (cachedIndex) return cachedIndex;
|
|
14
|
+
const data = require("../generated/lexicon-k3s.json") as Record<string, LexiconEntry>;
|
|
15
|
+
cachedIndex = new LexiconIndex(data);
|
|
16
|
+
return cachedIndex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Provide hover information for k3s entity types.
|
|
21
|
+
*/
|
|
22
|
+
export function hover(ctx: HoverContext): HoverInfo | undefined {
|
|
23
|
+
return lexiconHover(ctx, getIndex(), resourceHover);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resourceHover(className: string, entry: LexiconEntry): HoverInfo | undefined {
|
|
27
|
+
const lines: string[] = [];
|
|
28
|
+
|
|
29
|
+
lines.push(`**${className}**`);
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push(`k3s type: \`${entry.resourceType}\``);
|
|
32
|
+
lines.push("");
|
|
33
|
+
if (entry.kind === "resource") {
|
|
34
|
+
lines.push(
|
|
35
|
+
"*Resource entity — serialized as a file k3s consumes verbatim: " +
|
|
36
|
+
"config.yaml for Server/Agent, registries.yaml for Registries*",
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
lines.push("*Property entity — a nested value in a Registries declaration*");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { contents: lines.join("\n") };
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Thin entry point for `npm run bundle` in lexicon-k3s.
|
|
4
|
+
*/
|
|
5
|
+
import { generate, writeGeneratedFiles } from "./codegen/generate";
|
|
6
|
+
import { packageLexicon } from "./codegen/package";
|
|
7
|
+
import { writeBundleSpec } from "@intentius/chant/codegen/package";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
const pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
// 1. Generate src/generated/ files (writeGeneratedFiles resolves its own target)
|
|
14
|
+
const genResult = await generate({ verbose: true });
|
|
15
|
+
writeGeneratedFiles(genResult);
|
|
16
|
+
|
|
17
|
+
// 2. Run package pipeline and write dist/
|
|
18
|
+
const { spec, stats } = await packageLexicon({ verbose: true });
|
|
19
|
+
|
|
20
|
+
const distDir = join(dirname(pkgDir), "dist");
|
|
21
|
+
writeBundleSpec(spec, distDir);
|
|
22
|
+
|
|
23
|
+
console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
24
|
+
console.error(`dist/ written to ${distDir}`);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { k3sPlugin } from "./plugin";
|
|
3
|
+
import { isLexiconPlugin } from "@intentius/chant/lexicon";
|
|
4
|
+
|
|
5
|
+
describe("k3s plugin", () => {
|
|
6
|
+
it("is a valid LexiconPlugin", () => {
|
|
7
|
+
expect(isLexiconPlugin(k3sPlugin)).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("has the correct name", () => {
|
|
11
|
+
expect(k3sPlugin.name).toBe("k3s");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("has a serializer", () => {
|
|
15
|
+
expect(k3sPlugin.serializer).toBeDefined();
|
|
16
|
+
});
|
|
17
|
+
});
|