@intentius/chant-lexicon-k3s 0.124.31 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +83 -0
- package/dist/codegen/generate-cli.js +7 -0
- package/dist/codegen/generate-lexicon.js +39 -0
- package/dist/codegen/generate-typescript.js +38 -0
- package/dist/codegen/generate.js +93 -0
- package/dist/codegen/naming.js +34 -0
- package/dist/codegen/package.js +23 -0
- package/dist/codegen/parse.js +313 -0
- package/dist/describe-resources.js +200 -0
- package/dist/generated/index.js +10 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/index.js +14 -0
- package/dist/lint/audit-catalog.js +16 -0
- package/dist/lint/audit-lineage.js +5 -0
- package/dist/lint/post-synth/index.js +16 -0
- package/dist/lint/post-synth/k3s-helpers.js +24 -0
- package/dist/lint/post-synth/k3s101.js +48 -0
- package/dist/lint/post-synth/k3s102.js +52 -0
- package/dist/lint/post-synth/k3s103.js +30 -0
- package/dist/lint/post-synth/k3s104.js +37 -0
- package/dist/lint/post-synth/k3s105.js +44 -0
- package/dist/lint/post-synth/k3s106.js +47 -0
- package/dist/lint/post-synth/k3s107.js +58 -0
- package/dist/lint/rules/index.js +4 -0
- package/dist/lint/rules/token-literal.js +78 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +19 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +33 -0
- package/dist/op/activities/index.js +21 -0
- package/dist/op/activities/k3s.js +112 -0
- package/dist/op/builders.js +31 -0
- package/dist/package-cli.js +19 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +119 -0
- package/dist/serializer.js +169 -0
- package/dist/spec/fetch.js +72 -0
- package/dist/validate-cli.js +4 -0
- package/dist/validate.js +35 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/plugin.ts +2 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
/**
|
|
3
|
+
* K3S001: literal join token in source.
|
|
4
|
+
*
|
|
5
|
+
* `token` / `agent-token` are the cluster's shared join secret, and a
|
|
6
|
+
* committed config declaration is exactly the file that leaks it. The
|
|
7
|
+
* typed surface deliberately carries only the reference forms
|
|
8
|
+
* (`token-file`, `agent-token-file` — #1601), but nothing stops a raw
|
|
9
|
+
* object literal from writing the key anyway; this rule does.
|
|
10
|
+
*
|
|
11
|
+
* Flags a non-empty string-literal `token` / `agent-token` (identifier or
|
|
12
|
+
* quoted) inside a `Server(...)` / `Agent(...)` construction, with or
|
|
13
|
+
* without `new`. A bare `token:` elsewhere in the file is someone else's
|
|
14
|
+
* business.
|
|
15
|
+
*/
|
|
16
|
+
export const tokenLiteralRule = {
|
|
17
|
+
id: "K3S001",
|
|
18
|
+
severity: "error",
|
|
19
|
+
category: "security",
|
|
20
|
+
description: "k3s join token declared as a string literal in source",
|
|
21
|
+
check(context) {
|
|
22
|
+
const diagnostics = [];
|
|
23
|
+
const source = context.sourceFile;
|
|
24
|
+
const flag = (node, key) => {
|
|
25
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
26
|
+
diagnostics.push({
|
|
27
|
+
ruleId: "K3S001",
|
|
28
|
+
severity: "error",
|
|
29
|
+
message: `\`${key}\` is a literal in source — a committed k3s config is a leaked cluster join ` +
|
|
30
|
+
"secret. Point `token-file` at a path on the host, or supply K3S_TOKEN_FILE at " +
|
|
31
|
+
"install time; the value itself never belongs in the declaration.",
|
|
32
|
+
file: context.filePath,
|
|
33
|
+
line: line + 1,
|
|
34
|
+
column: character + 1,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const tokenLiteral = (obj) => {
|
|
38
|
+
for (const prop of obj.properties) {
|
|
39
|
+
if (!ts.isPropertyAssignment(prop))
|
|
40
|
+
continue;
|
|
41
|
+
const name = ts.isIdentifier(prop.name)
|
|
42
|
+
? prop.name.text
|
|
43
|
+
: ts.isStringLiteral(prop.name)
|
|
44
|
+
? prop.name.text
|
|
45
|
+
: undefined;
|
|
46
|
+
if (name !== "token" && name !== "agent-token")
|
|
47
|
+
continue;
|
|
48
|
+
if ((ts.isStringLiteral(prop.initializer) ||
|
|
49
|
+
ts.isNoSubstitutionTemplateLiteral(prop.initializer)) &&
|
|
50
|
+
prop.initializer.text.length > 0) {
|
|
51
|
+
return { node: prop.initializer, key: name };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
};
|
|
56
|
+
const visit = (node) => {
|
|
57
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
58
|
+
const callee = node.expression;
|
|
59
|
+
const name = ts.isIdentifier(callee)
|
|
60
|
+
? callee.text
|
|
61
|
+
: ts.isPropertyAccessExpression(callee)
|
|
62
|
+
? callee.name.text
|
|
63
|
+
: undefined;
|
|
64
|
+
if (name === "Server" || name === "Agent") {
|
|
65
|
+
const arg = node.arguments?.[0];
|
|
66
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
67
|
+
const literal = tokenLiteral(arg);
|
|
68
|
+
if (literal)
|
|
69
|
+
flag(literal.node, literal.key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
ts.forEachChild(node, visit);
|
|
74
|
+
};
|
|
75
|
+
visit(source);
|
|
76
|
+
return diagnostics;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"completions.d.ts","sourceRoot":"","sources":["../../src/lsp/completions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAiBpF;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAEpE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
3
|
+
import { LexiconIndex, lexiconCompletions, } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
|
+
let cachedIndex = null;
|
|
6
|
+
function getIndex() {
|
|
7
|
+
if (cachedIndex)
|
|
8
|
+
return cachedIndex;
|
|
9
|
+
const data = require("../generated/lexicon-k3s.json");
|
|
10
|
+
cachedIndex = new LexiconIndex(data);
|
|
11
|
+
return cachedIndex;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Provide k3s completions based on context — Server, Agent, Registries and
|
|
15
|
+
* the registry property entities, from the generated registry.
|
|
16
|
+
*/
|
|
17
|
+
export function completions(ctx) {
|
|
18
|
+
return lexiconCompletions(ctx, getIndex(), "k3s entity");
|
|
19
|
+
}
|
package/dist/lsp/hover.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hover.d.ts","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAiB1E;;GAEG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAE9D"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { moduleSourceUrl } from "@intentius/chant/module-source";
|
|
3
|
+
import { LexiconIndex, lexiconHover, } from "@intentius/chant/lsp/lexicon-providers";
|
|
4
|
+
const require = createRequire(moduleSourceUrl(import.meta.url));
|
|
5
|
+
let cachedIndex = null;
|
|
6
|
+
function getIndex() {
|
|
7
|
+
if (cachedIndex)
|
|
8
|
+
return cachedIndex;
|
|
9
|
+
const data = require("../generated/lexicon-k3s.json");
|
|
10
|
+
cachedIndex = new LexiconIndex(data);
|
|
11
|
+
return cachedIndex;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Provide hover information for k3s entity types.
|
|
15
|
+
*/
|
|
16
|
+
export function hover(ctx) {
|
|
17
|
+
return lexiconHover(ctx, getIndex(), resourceHover);
|
|
18
|
+
}
|
|
19
|
+
function resourceHover(className, entry) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
lines.push(`**${className}**`);
|
|
22
|
+
lines.push("");
|
|
23
|
+
lines.push(`k3s type: \`${entry.resourceType}\``);
|
|
24
|
+
lines.push("");
|
|
25
|
+
if (entry.kind === "resource") {
|
|
26
|
+
lines.push("*Resource entity — serialized as a file k3s consumes verbatim: " +
|
|
27
|
+
"config.yaml for Server/Agent, registries.yaml for Registries*");
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
lines.push("*Property entity — a nested value in a Registries declaration*");
|
|
31
|
+
}
|
|
32
|
+
return { contents: lines.join("\n") };
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* k3s Op activities — resolved by the core activity registry when a project's
|
|
3
|
+
* `chant.config.ts` lists the `k3s` lexicon. Lifecycle activities for the
|
|
4
|
+
* reachable-host case (chant#1601, epic #1598):
|
|
5
|
+
* - k3sInstall — run the pinned installer (`INSTALL_K3S_VERSION`), idempotent
|
|
6
|
+
* on an already-installed matching version.
|
|
7
|
+
* - k3sUninstall — the uninstall script, gated the way k3dDown is.
|
|
8
|
+
*
|
|
9
|
+
* Bounded exactly as k3dUp/k3dDown were (chant#1410): no SSH orchestration,
|
|
10
|
+
* no host provisioning. The join token never travels through these activities
|
|
11
|
+
* as a value — only `tokenFile`, a path — see the token-boundary note on
|
|
12
|
+
* {@link k3sInstall} in ./k3s.
|
|
13
|
+
*
|
|
14
|
+
* The step builders (k3sInstall, k3sUninstall) live in core and reach authors
|
|
15
|
+
* through `@intentius/chant/op` like k3dUp/k3dDown. The activities here are
|
|
16
|
+
* dependency-light — they shell out to the k3s installer/uninstall scripts and
|
|
17
|
+
* only pull in the lexicon's version pin, not its declarable surface — so
|
|
18
|
+
* `loadActivities` (`packages/core/src/op/activity-registry.ts`), which imports
|
|
19
|
+
* this module at run time, pulls in nothing expensive.
|
|
20
|
+
*/
|
|
21
|
+
export { k3sInstall, k3sUninstall, k3sInstallCommand, k3sInstallEnv, k3sUninstallCommand, k3sUninstallScript, k3sVersionCommand, parseK3sVersion, } from "./k3s.js";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { K3S_VERSION } from "../../spec/fetch.js";
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
/**
|
|
6
|
+
* `k3s --version` output, first line: `k3s version v1.36.3+k3s1 (hash)`.
|
|
7
|
+
* Returns `undefined` when k3s is not installed or the output doesn't match.
|
|
8
|
+
*/
|
|
9
|
+
export function parseK3sVersion(stdout) {
|
|
10
|
+
return stdout.match(/k3s version (\S+)/)?.[1];
|
|
11
|
+
}
|
|
12
|
+
/** Check-install-version command — errors (non-zero exit) when k3s is absent. */
|
|
13
|
+
export function k3sVersionCommand() {
|
|
14
|
+
return "k3s --version";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build the get.k3s.io installer command. The version pin and the join-token
|
|
18
|
+
* reference (if any) travel as environment variables via {@link k3sInstallEnv},
|
|
19
|
+
* not interpolated into this string — see the token-boundary note on
|
|
20
|
+
* {@link k3sInstall}.
|
|
21
|
+
*/
|
|
22
|
+
export function k3sInstallCommand(args) {
|
|
23
|
+
return `curl -sfL https://get.k3s.io | sh -s - ${args.role} --config ${args.configFile}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Environment for {@link k3sInstallCommand}: `INSTALL_K3S_VERSION` (the pin)
|
|
27
|
+
* and, only when a `tokenFile` path is given, `K3S_TOKEN_FILE`.
|
|
28
|
+
*
|
|
29
|
+
* ## The token boundary (#1601)
|
|
30
|
+
*
|
|
31
|
+
* This is the only token-shaped input the install activity accepts, and it
|
|
32
|
+
* is a path, not a secret — the join token itself never passes through
|
|
33
|
+
* activity args, never gets logged, and never lands in an emitted
|
|
34
|
+
* config.yaml (K3S001 rejects a literal `token`/`agent-token` at lint). A
|
|
35
|
+
* `K3S_TOKEN` (the literal, env-var form k3s itself supports for agents) is
|
|
36
|
+
* deliberately not modeled here: the reference form the lexicon carries
|
|
37
|
+
* end-to-end is a file path, matching `token-file`/`agent-token-file` on the
|
|
38
|
+
* declared config surface (chant#1365's provenance stance applied at this
|
|
39
|
+
* surface, not a new mechanism).
|
|
40
|
+
*/
|
|
41
|
+
export function k3sInstallEnv(args) {
|
|
42
|
+
const env = { INSTALL_K3S_VERSION: args.version ?? K3S_VERSION };
|
|
43
|
+
if (args.tokenFile)
|
|
44
|
+
env.K3S_TOKEN_FILE = args.tokenFile;
|
|
45
|
+
return env;
|
|
46
|
+
}
|
|
47
|
+
/** Path k3s installs its uninstall script at — one per role. */
|
|
48
|
+
export function k3sUninstallScript(role) {
|
|
49
|
+
return role === "agent" ? "/usr/local/bin/k3s-agent-uninstall.sh" : "/usr/local/bin/k3s-uninstall.sh";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the uninstall command. Gated the way `k3dDown` is (chant#1410): no
|
|
53
|
+
* pre-check activity, no SSH orchestration — just the native idempotent
|
|
54
|
+
* behavior. Unlike `k3d cluster delete` (idempotent on its own), the k3s
|
|
55
|
+
* uninstall script does not exist at all on a host where k3s was never
|
|
56
|
+
* installed, so the command guards on the script's presence itself rather
|
|
57
|
+
* than assume it errors safely.
|
|
58
|
+
*/
|
|
59
|
+
export function k3sUninstallCommand(args) {
|
|
60
|
+
const script = k3sUninstallScript(args.role);
|
|
61
|
+
return `test -x ${script} && ${script} || echo "k3s ${args.role} already uninstalled"`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Run the pinned k3s installer against a reachable host. Idempotent on an
|
|
65
|
+
* already-installed matching version: if `k3s --version` already reports the
|
|
66
|
+
* target version, the install is skipped. Uses the longInfra profile: a 20m
|
|
67
|
+
* timeout, wide enough for the installer to download and start the k3s binary,
|
|
68
|
+
* and three attempts backing off from 30s.
|
|
69
|
+
*
|
|
70
|
+
* Bounded exactly as `k3dUp`/`k3dDown` were (chant#1410, epic #1598): this
|
|
71
|
+
* drives the case where the host is reachable from where the Op runs. It
|
|
72
|
+
* does not provision the host, and it is not an SSH orchestrator.
|
|
73
|
+
*/
|
|
74
|
+
export async function k3sInstall(args, signal) {
|
|
75
|
+
const target = args.version ?? K3S_VERSION;
|
|
76
|
+
try {
|
|
77
|
+
const { stdout } = await execAsync(k3sVersionCommand(), { signal });
|
|
78
|
+
const installed = parseK3sVersion(stdout);
|
|
79
|
+
if (installed === target) {
|
|
80
|
+
console.log(`k3s ${target} already installed (${args.role}) — skipping install`);
|
|
81
|
+
return { version: target, installed: false };
|
|
82
|
+
}
|
|
83
|
+
if (installed) {
|
|
84
|
+
console.log(`k3s ${installed} installed, target is ${target} — reinstalling`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// `k3s --version` errors when k3s is absent — fall through to install.
|
|
89
|
+
}
|
|
90
|
+
const { stdout, stderr } = await execAsync(k3sInstallCommand(args), {
|
|
91
|
+
signal,
|
|
92
|
+
env: { ...process.env, ...k3sInstallEnv(args) },
|
|
93
|
+
});
|
|
94
|
+
if (stdout)
|
|
95
|
+
console.log(stdout);
|
|
96
|
+
if (stderr)
|
|
97
|
+
console.error(stderr);
|
|
98
|
+
return { version: target, installed: true };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Uninstall k3s from a reachable host. Uses fastIdempotent profile — 5m
|
|
102
|
+
* timeout. A host where k3s was never installed (no uninstall script present)
|
|
103
|
+
* is a no-op success, the same idempotent shape as `k3dDown` against an
|
|
104
|
+
* already-gone cluster.
|
|
105
|
+
*/
|
|
106
|
+
export async function k3sUninstall(args, signal) {
|
|
107
|
+
const { stdout, stderr } = await execAsync(k3sUninstallCommand(args), { signal });
|
|
108
|
+
if (stdout)
|
|
109
|
+
console.log(stdout);
|
|
110
|
+
if (stderr)
|
|
111
|
+
console.error(stderr);
|
|
112
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers for this lexicon's activities (chant #1288
|
|
3
|
+
* Stage 2). See `lexicons/k8s/src/op/builders.ts`'s module doc for why these
|
|
4
|
+
* live beside their `*Args` interfaces rather than in core: core cannot
|
|
5
|
+
* import a lexicon's types, so a wrapper whose `opts` IS the activity's own
|
|
6
|
+
* `*Args` interface can only sit in the lexicon that declares that
|
|
7
|
+
* interface. `opts`'s type in each wrapper below IS the activity's own
|
|
8
|
+
* `*Args` interface (via `Omit`/`WithStepRefs`) — never restated. `core`'s own
|
|
9
|
+
* `k3sInstall`/`k3sUninstall` are unchanged and produce byte-identical
|
|
10
|
+
* `ActivityStep` output; these are purely additive.
|
|
11
|
+
*/
|
|
12
|
+
import { activity, takeProfileAndId, } from "@intentius/chant/op";
|
|
13
|
+
/**
|
|
14
|
+
* Run the pinned k3s installer against a reachable host — the fully typed
|
|
15
|
+
* twin of core's `k3sInstall`. `opts` is {@link K3sInstallArgs} itself,
|
|
16
|
+
* minus the positional `role` (`configFile` remains required). Defaults to
|
|
17
|
+
* the `longInfra` profile.
|
|
18
|
+
*/
|
|
19
|
+
export const k3sInstall = (role, opts) => {
|
|
20
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
21
|
+
return activity("k3sInstall", { role, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Uninstall k3s from a reachable host — the fully typed twin of core's
|
|
25
|
+
* `k3sUninstall`. `opts` is {@link K3sUninstallArgs} itself, minus the
|
|
26
|
+
* positional `role`. Defaults to the `fastIdempotent` profile.
|
|
27
|
+
*/
|
|
28
|
+
export const k3sUninstall = (role, opts) => {
|
|
29
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
30
|
+
return activity("k3sUninstall", { role, ...args }, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
31
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
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.js";
|
|
6
|
+
import { packageLexicon } from "./codegen/package.js";
|
|
7
|
+
import { writeBundleSpec } from "@intentius/chant/codegen/package";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
const pkgDir = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
// 1. Generate src/generated/ files (writeGeneratedFiles resolves its own target)
|
|
12
|
+
const genResult = await generate({ verbose: true });
|
|
13
|
+
writeGeneratedFiles(genResult);
|
|
14
|
+
// 2. Run package pipeline and write dist/
|
|
15
|
+
const { spec, stats } = await packageLexicon({ verbose: true });
|
|
16
|
+
const distDir = join(dirname(pkgDir), "dist");
|
|
17
|
+
writeBundleSpec(spec, distDir);
|
|
18
|
+
console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
19
|
+
console.error(`dist/ written to ${distDir}`);
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AAiB7F;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,aA4HvB,CAAC"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
4
|
+
import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
5
|
+
import { k3sSerializer } from "./serializer.js";
|
|
6
|
+
import { rules } from "./lint/rules/index.js";
|
|
7
|
+
import { postSynthChecks } from "./lint/post-synth/index.js";
|
|
8
|
+
import { k3sAuditCatalog } from "./lint/audit-catalog.js";
|
|
9
|
+
import { completions } from "./lsp/completions.js";
|
|
10
|
+
import { hover } from "./lsp/hover.js";
|
|
11
|
+
/**
|
|
12
|
+
* k3s lexicon plugin.
|
|
13
|
+
*
|
|
14
|
+
* Implements all required LexiconPlugin lifecycle methods.
|
|
15
|
+
*/
|
|
16
|
+
export const k3sPlugin = {
|
|
17
|
+
name: "k3s",
|
|
18
|
+
serializer: k3sSerializer,
|
|
19
|
+
/**
|
|
20
|
+
* Where the upstream pin lives and where to look for a newer one, for
|
|
21
|
+
* the self-upgrade tooling (#523). k3s releases carry a `+k3s1` build
|
|
22
|
+
* suffix; the release tag is the version constant verbatim.
|
|
23
|
+
*/
|
|
24
|
+
upstreamPin: {
|
|
25
|
+
file: "src/spec/fetch.ts",
|
|
26
|
+
pattern: /export const K3S_VERSION\s*=\s*"([^"]+)"/,
|
|
27
|
+
replace: (v, line) => line.replace(/= "[^"]+"/, `= "${v}"`),
|
|
28
|
+
upstream: { owner: "k3s-io", repo: "k3s", kind: "releases" },
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* The marker rides `node-label` (the serializer stamps it there when a
|
|
32
|
+
* build carries ownership) and lands on the registered Node as ordinary
|
|
33
|
+
* Kubernetes labels — read back the same way every label-based lexicon
|
|
34
|
+
* does. See `./describe-resources.ts` (#1603).
|
|
35
|
+
*/
|
|
36
|
+
ownershipChannel: {
|
|
37
|
+
keys: LABEL_OWNERSHIP_KEYS,
|
|
38
|
+
reads: ["describeResources"],
|
|
39
|
+
},
|
|
40
|
+
async describeResources(options) {
|
|
41
|
+
const { describeResources } = await import("./describe-resources.js");
|
|
42
|
+
return describeResources(options);
|
|
43
|
+
},
|
|
44
|
+
// ── Required lifecycle methods ────────────────────────────────
|
|
45
|
+
async generate(options) {
|
|
46
|
+
const { generate } = await import("./codegen/generate.js");
|
|
47
|
+
await generate(options);
|
|
48
|
+
},
|
|
49
|
+
async validate(options) {
|
|
50
|
+
const { validate } = await import("./validate.js");
|
|
51
|
+
const { printValidationResult } = await import("@intentius/chant/codegen/validate");
|
|
52
|
+
const result = await validate();
|
|
53
|
+
printValidationResult(result);
|
|
54
|
+
},
|
|
55
|
+
async coverage(options) {
|
|
56
|
+
// TODO: Implement coverage analysis
|
|
57
|
+
console.error("Coverage analysis not yet implemented");
|
|
58
|
+
},
|
|
59
|
+
async package(options) {
|
|
60
|
+
const { packageLexicon } = await import("./codegen/package.js");
|
|
61
|
+
const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
|
|
62
|
+
const { join, dirname } = await import("path");
|
|
63
|
+
const { fileURLToPath } = await import("url");
|
|
64
|
+
const { spec, stats } = await packageLexicon(options);
|
|
65
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
66
|
+
writeBundleSpec(spec, join(pkgDir, "dist"));
|
|
67
|
+
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
68
|
+
},
|
|
69
|
+
// ── Optional extensions ────────────────────────────────────
|
|
70
|
+
lintRules() {
|
|
71
|
+
return rules;
|
|
72
|
+
},
|
|
73
|
+
postSynthChecks() {
|
|
74
|
+
return postSynthChecks;
|
|
75
|
+
},
|
|
76
|
+
auditCatalog() {
|
|
77
|
+
return k3sAuditCatalog;
|
|
78
|
+
},
|
|
79
|
+
skills() {
|
|
80
|
+
const skills = [];
|
|
81
|
+
const dir = moduleSourceDir(import.meta.url);
|
|
82
|
+
const entries = [
|
|
83
|
+
{
|
|
84
|
+
file: "chant-k3s.md",
|
|
85
|
+
name: "chant-k3s",
|
|
86
|
+
description: "Declare k3s host configuration — server/agent config.yaml and registries.yaml — as typed chant source",
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
try {
|
|
91
|
+
const content = readFileSync(join(dir, "skills", entry.file), "utf-8");
|
|
92
|
+
skills.push({ name: entry.name, description: entry.description, content });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// skill file missing from a partial checkout — skip rather than throw
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return skills;
|
|
99
|
+
},
|
|
100
|
+
mcpTools() {
|
|
101
|
+
return []; // TODO: Implement MCP tools
|
|
102
|
+
},
|
|
103
|
+
mcpResources() {
|
|
104
|
+
return []; // TODO: Implement MCP resources
|
|
105
|
+
},
|
|
106
|
+
detectTemplate(data) {
|
|
107
|
+
return false; // TODO: Detect if a template belongs to this lexicon
|
|
108
|
+
},
|
|
109
|
+
completionProvider(ctx) {
|
|
110
|
+
return completions(ctx);
|
|
111
|
+
},
|
|
112
|
+
hoverProvider(ctx) {
|
|
113
|
+
return hover(ctx);
|
|
114
|
+
},
|
|
115
|
+
async docs(options) {
|
|
116
|
+
const { generateDocs } = await import("./codegen/docs.js");
|
|
117
|
+
return generateDocs(options);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* k3s config YAML serializer.
|
|
3
|
+
*
|
|
4
|
+
* Emits the files k3s consumes verbatim — the walk-away artifacts that
|
|
5
|
+
* justify a lexicon here at all (#1600):
|
|
6
|
+
*
|
|
7
|
+
* K3s::Server / K3s::Agent → config.yaml (`k3s server --config <file>`)
|
|
8
|
+
* K3s::Registries → registries.yaml
|
|
9
|
+
*
|
|
10
|
+
* One document per declared entity: the first config is the primary
|
|
11
|
+
* output, every further entity lands in files[] keyed `<name>.config.yaml`
|
|
12
|
+
* / `<name>.registries.yaml`. A config.yaml's keys are exactly the CLI
|
|
13
|
+
* flag names, so the emitted file diffs cleanly against any hand-written
|
|
14
|
+
* one and `k3s check-config` style tooling sees nothing unusual.
|
|
15
|
+
*
|
|
16
|
+
* ## Ownership rides node-label (#1603)
|
|
17
|
+
*
|
|
18
|
+
* config.yaml has no free-form metadata channel, but `node-label` is a
|
|
19
|
+
* first-class key on both server and agent and lands on the registered
|
|
20
|
+
* Node as ordinary Kubernetes labels. When a build carries ownership,
|
|
21
|
+
* the serializer appends the chant label pair to `node-label` — readable
|
|
22
|
+
* back through any kubectl against the cluster, and inert to k3s itself.
|
|
23
|
+
* An author's own label with the same key wins.
|
|
24
|
+
*
|
|
25
|
+
* ## What never appears
|
|
26
|
+
*
|
|
27
|
+
* `token` / `agent-token` literals are not part of the typed surface
|
|
28
|
+
* (#1601), and K3SC001 fails the build if one arrives through untyped
|
|
29
|
+
* props anyway. The file k3s reads secrets from is the file's business
|
|
30
|
+
* (`token-file`), not the artifact's.
|
|
31
|
+
*/
|
|
32
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
33
|
+
import { walkValue } from "@intentius/chant/serializer-walker";
|
|
34
|
+
import { emitYAMLEntry } from "@intentius/chant/yaml";
|
|
35
|
+
import { ownershipEntries, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
36
|
+
export const SERVER_TYPE = "K3s::Server";
|
|
37
|
+
export const AGENT_TYPE = "K3s::Agent";
|
|
38
|
+
export const REGISTRIES_TYPE = "K3s::Registries";
|
|
39
|
+
/**
|
|
40
|
+
* Keys that lead the document; everything else follows alphabetically.
|
|
41
|
+
* The identity-and-join block first is how the documented examples read,
|
|
42
|
+
* so a chant emission diffs cleanly against them.
|
|
43
|
+
*/
|
|
44
|
+
const KEY_PRIORITY = [
|
|
45
|
+
"cluster-init",
|
|
46
|
+
"server",
|
|
47
|
+
"token-file",
|
|
48
|
+
"agent-token-file",
|
|
49
|
+
"data-dir",
|
|
50
|
+
"node-name",
|
|
51
|
+
];
|
|
52
|
+
const visitor = {
|
|
53
|
+
attrRef: (name) => name,
|
|
54
|
+
resourceRef: (name) => name,
|
|
55
|
+
propertyDeclarable: (entity, walk) => {
|
|
56
|
+
if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const props = entity.props;
|
|
60
|
+
const result = {};
|
|
61
|
+
for (const [key, value] of Object.entries(props)) {
|
|
62
|
+
if (value !== undefined)
|
|
63
|
+
result[key] = walk(value);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
function orderedKeys(props) {
|
|
69
|
+
const rest = Object.keys(props)
|
|
70
|
+
.filter((k) => !KEY_PRIORITY.includes(k))
|
|
71
|
+
.sort();
|
|
72
|
+
return [...KEY_PRIORITY.filter((k) => props[k] !== undefined), ...rest];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Append the ownership marker to `node-label`. The key survives into the
|
|
76
|
+
* registered Node's labels, which is the only durable channel a host
|
|
77
|
+
* config file has. An author-set key of the same name wins.
|
|
78
|
+
*/
|
|
79
|
+
function stampOwnership(props, ownership) {
|
|
80
|
+
const existing = props["node-label"];
|
|
81
|
+
const labels = existing === undefined ? [] : Array.isArray(existing) ? [...existing] : [String(existing)];
|
|
82
|
+
const present = new Set(labels.map((l) => String(l).split("=")[0]));
|
|
83
|
+
for (const [key, value] of Object.entries(ownershipEntries(LABEL_OWNERSHIP_KEYS, ownership))) {
|
|
84
|
+
if (present.has(key))
|
|
85
|
+
continue;
|
|
86
|
+
labels.push(`${key}=${value}`);
|
|
87
|
+
}
|
|
88
|
+
props["node-label"] = labels;
|
|
89
|
+
}
|
|
90
|
+
function configDocument(entity, entityNames, context) {
|
|
91
|
+
const raw = (isResourceDeclarable(entity) ? entity.props : {});
|
|
92
|
+
const props = (walkValue(raw, entityNames, visitor) ?? {});
|
|
93
|
+
if (context?.ownership)
|
|
94
|
+
stampOwnership(props, context.ownership);
|
|
95
|
+
const doc = {};
|
|
96
|
+
for (const key of orderedKeys(props)) {
|
|
97
|
+
if (props[key] !== undefined)
|
|
98
|
+
doc[key] = props[key];
|
|
99
|
+
}
|
|
100
|
+
return doc;
|
|
101
|
+
}
|
|
102
|
+
function registriesDocument(entity, entityNames) {
|
|
103
|
+
const raw = (isResourceDeclarable(entity) ? entity.props : {});
|
|
104
|
+
const props = (walkValue(raw, entityNames, visitor) ?? {});
|
|
105
|
+
const doc = {};
|
|
106
|
+
// mirrors before configs, matching the documented examples.
|
|
107
|
+
if (props.mirrors !== undefined)
|
|
108
|
+
doc.mirrors = props.mirrors;
|
|
109
|
+
if (props.configs !== undefined)
|
|
110
|
+
doc.configs = props.configs;
|
|
111
|
+
for (const [key, value] of Object.entries(props)) {
|
|
112
|
+
if (key === "mirrors" || key === "configs" || value === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
doc[key] = value;
|
|
115
|
+
}
|
|
116
|
+
return doc;
|
|
117
|
+
}
|
|
118
|
+
function emitDocument(doc) {
|
|
119
|
+
const sections = [];
|
|
120
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
continue;
|
|
123
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
124
|
+
sections.push(`${key}: ${scalar(value)}`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
sections.push(emitYAMLEntry(key, value));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return sections.join("\n") + "\n";
|
|
131
|
+
}
|
|
132
|
+
function scalar(value) {
|
|
133
|
+
if (typeof value === "boolean")
|
|
134
|
+
return value ? "true" : "false";
|
|
135
|
+
if (typeof value === "number")
|
|
136
|
+
return String(value);
|
|
137
|
+
if (/[:#\[\]{}|>&*!%@`'"]|^[\s\d]|\s$|^$|^(true|false|null|yes|no|on|off)$/i.test(value)) {
|
|
138
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
export const k3sSerializer = {
|
|
143
|
+
name: "k3s",
|
|
144
|
+
rulePrefix: "K3S",
|
|
145
|
+
serialize(entities, _outputs, context) {
|
|
146
|
+
const entityNames = new Map();
|
|
147
|
+
for (const [name, entity] of entities)
|
|
148
|
+
entityNames.set(entity, name);
|
|
149
|
+
const configs = [...entities].filter(([, e]) => e.entityType === SERVER_TYPE || e.entityType === AGENT_TYPE);
|
|
150
|
+
const registries = [...entities].filter(([, e]) => e.entityType === REGISTRIES_TYPE);
|
|
151
|
+
let primary = "";
|
|
152
|
+
const files = {};
|
|
153
|
+
configs.forEach(([name, entity], i) => {
|
|
154
|
+
const content = emitDocument(configDocument(entity, entityNames, context));
|
|
155
|
+
if (i === 0)
|
|
156
|
+
primary = content;
|
|
157
|
+
else
|
|
158
|
+
files[`${name}.config.yaml`] = content;
|
|
159
|
+
});
|
|
160
|
+
registries.forEach(([name, entity], i) => {
|
|
161
|
+
const content = emitDocument(registriesDocument(entity, entityNames));
|
|
162
|
+
if (primary === "" && i === 0 && configs.length === 0)
|
|
163
|
+
primary = content;
|
|
164
|
+
else
|
|
165
|
+
files[i === 0 && configs.length > 0 ? "registries.yaml" : `${name}.registries.yaml`] = content;
|
|
166
|
+
});
|
|
167
|
+
return Object.keys(files).length > 0 ? { primary, files } : primary;
|
|
168
|
+
},
|
|
169
|
+
};
|