@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,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live observation for declared k3s Server/Agent entities (#1603).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the #1412 k3d answer for the k3s shape, and reuses the same
|
|
5
|
+
* `k8s.profiles.<env>.context` binding helm and the k8s lexicon read
|
|
6
|
+
* (`@intentius/chant/kubectl-context`, #1488): the kubectl context is a
|
|
7
|
+
* property of the cluster chant talks to, not of any one lexicon, so k3s
|
|
8
|
+
* does not invent a second binding namespace for the same thing.
|
|
9
|
+
*
|
|
10
|
+
* ## What this can honestly say
|
|
11
|
+
*
|
|
12
|
+
* A declared `K3s::Server` / `K3s::Agent` is chant's description of one node
|
|
13
|
+
* joining a cluster. From where chant runs — never the host itself — the
|
|
14
|
+
* only observables are ones the apiserver answers:
|
|
15
|
+
*
|
|
16
|
+
* - the declared kubectl context exists and answers at all (`kubectl
|
|
17
|
+
* version`), which also reports the live k3s build so a caller can see
|
|
18
|
+
* it against the pin (`K3S_VERSION`);
|
|
19
|
+
* - the node the entity names is registered and its `Ready` condition.
|
|
20
|
+
*
|
|
21
|
+
* Everything that would require reaching the host directly — whether the
|
|
22
|
+
* `k3s` systemd unit is up, what `/etc/rancher/k3s/config.yaml` currently
|
|
23
|
+
* holds on disk — is out of scope. That is the provisioning boundary #1598
|
|
24
|
+
* draws, and this reader never crosses it.
|
|
25
|
+
*
|
|
26
|
+
* `K3s::Registries` has no live counterpart at all: `registries.yaml` is
|
|
27
|
+
* consumed once at containerd startup and leaves no object an apiserver
|
|
28
|
+
* serves, so it reads `unsupported-kind` — honest, not a gap to close here.
|
|
29
|
+
*
|
|
30
|
+
* ## Node identity
|
|
31
|
+
*
|
|
32
|
+
* The only address chant can back up is a declared `node-name`. Unset, k3s
|
|
33
|
+
* registers the node under the host's own hostname — which chant, running
|
|
34
|
+
* off the host, cannot know or guess. An entity with no `node-name` is
|
|
35
|
+
* `read-failed`, never a guessed address and never `absent`.
|
|
36
|
+
*
|
|
37
|
+
* ## Ownership rides node-label (#1603)
|
|
38
|
+
*
|
|
39
|
+
* The serializer stamps chant's marker into `node-label` when a build carries
|
|
40
|
+
* ownership (`./serializer.ts`), which lands on the registered Node as
|
|
41
|
+
* ordinary Kubernetes labels — the durable channel a host config file has no
|
|
42
|
+
* other way to carry. Read back here the same way every label-based lexicon
|
|
43
|
+
* does (`LABEL_OWNERSHIP_KEYS`).
|
|
44
|
+
*
|
|
45
|
+
* ## Tri-state (#1089, and the #1488 lesson)
|
|
46
|
+
*
|
|
47
|
+
* An unreachable apiserver — context missing, connection refused, no
|
|
48
|
+
* credentials — is `read-failed`/`no-credentials`/`no-binding` for every
|
|
49
|
+
* declared entity, naming the context that was actually read, never
|
|
50
|
+
* `absent`. Only a genuine per-node `NotFound` is absence.
|
|
51
|
+
*/
|
|
52
|
+
import { exec } from "node:child_process";
|
|
53
|
+
import { promisify } from "node:util";
|
|
54
|
+
import { observeEntities, } from "@intentius/chant/observation";
|
|
55
|
+
import { classifyOwnership, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
56
|
+
import { loadChantConfigUpward } from "@intentius/chant/config";
|
|
57
|
+
import { resolveClusterTarget, classifyKubectlFailure } from "@intentius/chant/kubectl-context";
|
|
58
|
+
import { SERVER_TYPE, AGENT_TYPE } from "./serializer.js";
|
|
59
|
+
import { K3S_VERSION } from "./spec/fetch.js";
|
|
60
|
+
const execAsync = promisify(exec);
|
|
61
|
+
/** Shell-quote one argv element (same convention as helm's release-observe). */
|
|
62
|
+
function q(arg) {
|
|
63
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The environment's kube context (#1488): the declared `k8s.profiles.<env>`
|
|
67
|
+
* binding when present, ambient otherwise. This is deliberately the SAME
|
|
68
|
+
* binding the k8s and helm lexicons read — a cluster is one target however
|
|
69
|
+
* many lexicons observe it — not a `k3s.profiles` of its own.
|
|
70
|
+
*/
|
|
71
|
+
export async function resolveK3sContext(environment) {
|
|
72
|
+
try {
|
|
73
|
+
const { config } = await loadChantConfigUpward(process.cwd());
|
|
74
|
+
return (await resolveClusterTarget(config, environment, "k3s")).context;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** The declared node identity: the `node-name` flag, chant's only honest
|
|
81
|
+
* source. Absent means k3s will name the node after the host's own hostname
|
|
82
|
+
* — unknowable from here, so there is nothing to query by. */
|
|
83
|
+
export function declaredNodeName(entity) {
|
|
84
|
+
const name = entity.props?.["node-name"];
|
|
85
|
+
return typeof name === "string" && name.length > 0 ? name : undefined;
|
|
86
|
+
}
|
|
87
|
+
/** The node's `Ready` condition as a status word: no condition at all reads
|
|
88
|
+
* `unknown` (asked, and the object said nothing) rather than a guess. */
|
|
89
|
+
function nodeReadyStatus(node) {
|
|
90
|
+
const ready = node.status?.conditions?.find((c) => c.type === "Ready");
|
|
91
|
+
if (!ready)
|
|
92
|
+
return "unknown";
|
|
93
|
+
if (ready.status === "True")
|
|
94
|
+
return "Ready";
|
|
95
|
+
return ready.reason && ready.reason.length > 0 ? ready.reason : "NotReady";
|
|
96
|
+
}
|
|
97
|
+
function pruneUndefined(obj) {
|
|
98
|
+
const out = {};
|
|
99
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
100
|
+
if (v !== undefined)
|
|
101
|
+
out[k] = v;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function adapter(execFn, environment) {
|
|
106
|
+
// Captured so a bind() failure can name the exact context it tried,
|
|
107
|
+
// per the #1488 lesson — a bare kubectl error rarely does.
|
|
108
|
+
let contextTried;
|
|
109
|
+
return {
|
|
110
|
+
async bind() {
|
|
111
|
+
const context = await resolveK3sContext(environment);
|
|
112
|
+
contextTried = context;
|
|
113
|
+
const ctxFlag = context ? ` --context ${q(context)}` : "";
|
|
114
|
+
const { stdout } = await execFn(`kubectl version -o json${ctxFlag}`);
|
|
115
|
+
const parsed = JSON.parse(stdout);
|
|
116
|
+
return { exec: execFn, ctxFlag, k3sVersion: parsed.serverVersion?.gitVersion };
|
|
117
|
+
},
|
|
118
|
+
classifyBindFailure(err) {
|
|
119
|
+
const outcome = classifyKubectlFailure(err);
|
|
120
|
+
const named = contextTried
|
|
121
|
+
? `context "${contextTried}" (from k8s.profiles.${environment}.context)`
|
|
122
|
+
: "the ambient kubectl context (no k8s.profiles binding declared)";
|
|
123
|
+
const raw = outcome.kind === "unobserved" ? outcome.detail : err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
124
|
+
const reason = outcome.kind === "unobserved" ? outcome.reason : "read-failed";
|
|
125
|
+
return { reason, detail: `${named}: ${raw}` };
|
|
126
|
+
},
|
|
127
|
+
async read(client, entity) {
|
|
128
|
+
if (entity.type !== SERVER_TYPE && entity.type !== AGENT_TYPE) {
|
|
129
|
+
return { unobserved: { reason: "unsupported-kind", detail: entity.type } };
|
|
130
|
+
}
|
|
131
|
+
const name = declaredNodeName(entity);
|
|
132
|
+
if (!name) {
|
|
133
|
+
return {
|
|
134
|
+
unobserved: {
|
|
135
|
+
reason: "read-failed",
|
|
136
|
+
detail: "no `node-name` declared — unset, k3s names the node after the host's own hostname, which chant cannot know from here",
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const command = `kubectl get node ${q(name)} -o json${client.ctxFlag}`;
|
|
141
|
+
try {
|
|
142
|
+
const { stdout } = await client.exec(command);
|
|
143
|
+
const node = JSON.parse(stdout);
|
|
144
|
+
const ownership = classifyOwnership(node.metadata?.labels, LABEL_OWNERSHIP_KEYS);
|
|
145
|
+
return {
|
|
146
|
+
present: {
|
|
147
|
+
type: entity.type,
|
|
148
|
+
physicalId: node.metadata?.uid ?? name,
|
|
149
|
+
status: nodeReadyStatus(node),
|
|
150
|
+
lastUpdated: node.metadata?.creationTimestamp,
|
|
151
|
+
ownership,
|
|
152
|
+
attributes: pruneUndefined({
|
|
153
|
+
nodeName: name,
|
|
154
|
+
kubeletVersion: node.status?.nodeInfo?.kubeletVersion,
|
|
155
|
+
k3sVersion: client.k3sVersion,
|
|
156
|
+
versionMatchesPin: client.k3sVersion ? client.k3sVersion === K3S_VERSION : undefined,
|
|
157
|
+
labels: node.metadata?.labels,
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
queried: command,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const outcome = classifyKubectlFailure(err);
|
|
165
|
+
if (outcome.kind === "absent")
|
|
166
|
+
return { absent: true, queried: command };
|
|
167
|
+
return { unobserved: { reason: outcome.reason, detail: outcome.detail }, queried: command };
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export async function describeResources(options, execFn = execAsync) {
|
|
173
|
+
const declared = options.entityNames.map((name) => {
|
|
174
|
+
const entity = options.entities.get(name);
|
|
175
|
+
return {
|
|
176
|
+
name,
|
|
177
|
+
type: entity?.entityType ?? "",
|
|
178
|
+
props: entity?.props ?? {},
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
const result = await observeEntities(declared, adapter(execFn, options.environment));
|
|
182
|
+
if (!options.owned)
|
|
183
|
+
return result;
|
|
184
|
+
// `--owned`: withhold a node that is present but carries no chant marker.
|
|
185
|
+
// Withheld is `filtered`, never absent — a foreign node still exists.
|
|
186
|
+
const resources = { ...result.resources };
|
|
187
|
+
const unobserved = { ...result.unobserved };
|
|
188
|
+
for (const [name, meta] of Object.entries(result.resources)) {
|
|
189
|
+
if (meta.ownership === "owned")
|
|
190
|
+
continue;
|
|
191
|
+
delete resources[name];
|
|
192
|
+
unobserved[name] = {
|
|
193
|
+
type: meta.type,
|
|
194
|
+
reason: "filtered",
|
|
195
|
+
detail: "live node carries no chant ownership marker and --owned was requested",
|
|
196
|
+
...(result.queried?.[name] ? { queried: result.queried[name] } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return { ...result, resources, unobserved };
|
|
200
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Code generated by chant generate. DO NOT EDIT.
|
|
2
|
+
import { createResource, createProperty } from "./runtime.js";
|
|
3
|
+
export const Agent = /* @__PURE__ */ createResource("K3s::Agent", "k3s", {});
|
|
4
|
+
export const Registries = /* @__PURE__ */ createResource("K3s::Registries", "k3s", {});
|
|
5
|
+
export const Server = /* @__PURE__ */ createResource("K3s::Server", "k3s", {});
|
|
6
|
+
export const Mirror = /* @__PURE__ */ createProperty("K3s::Mirror", "k3s");
|
|
7
|
+
export const RegistryAuth = /* @__PURE__ */ createProperty("K3s::RegistryAuth", "k3s");
|
|
8
|
+
export const RegistryConfig = /* @__PURE__ */ createProperty("K3s::RegistryConfig", "k3s");
|
|
9
|
+
export const RegistryTLS = /* @__PURE__ */ createProperty("K3s::RegistryTLS", "k3s");
|
|
10
|
+
// Re-exports for convenience
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Typed Op step-builder wrappers (chant #1288 Stage 2) — k3sInstall/
|
|
2
|
+
// k3sUninstall with authoring-time types derived from this lexicon's own
|
|
3
|
+
// *Args interfaces (see ./op/builders.ts's module doc). Opt-in:
|
|
4
|
+
// `@intentius/chant/op`'s same-named exports are core's original untyped
|
|
5
|
+
// builders, unchanged.
|
|
6
|
+
export { k3sInstall, k3sUninstall } from "./op/builders.js";
|
|
7
|
+
// Plugin
|
|
8
|
+
export { k3sPlugin } from "./plugin.js";
|
|
9
|
+
// Serializer
|
|
10
|
+
export { k3sSerializer } from "./serializer.js";
|
|
11
|
+
// Generated resources — export everything from generated index
|
|
12
|
+
export * from "./generated/index.js";
|
|
13
|
+
// The pinned upstream release and its Docker image tag form
|
|
14
|
+
export { K3S_VERSION, K3S_IMAGE_TAG } from "./spec/fetch.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { k3sAuditLineage } from "./audit-lineage.js";
|
|
2
|
+
import { applyLineage } from "@intentius/chant/audit/catalog";
|
|
3
|
+
function entityRule(id, category, title, remediation) {
|
|
4
|
+
return { id, tier: "merge-worthy", fixKind: "guidance", category, title, remediation, yamlBased: false };
|
|
5
|
+
}
|
|
6
|
+
export const k3sAuditCatalog = {
|
|
7
|
+
K3S101: entityRule("K3S101", "security", "Literal join token in a k3s config", "Remove the `token` value; point `token-file` at a path on the host instead."),
|
|
8
|
+
K3S102: entityRule("K3S102", "security", "Literal registry credential in registries.yaml", "Remove the credential from the declaration; configure registry auth on the host."),
|
|
9
|
+
K3S103: entityRule("K3S103", "correctness", "Agent config with no server to join", "Set `server: https://<server-host>:6443` on the agent declaration."),
|
|
10
|
+
K3S104: entityRule("K3S104", "security", "Kubeconfig written wider than 0644", "Drop `write-kubeconfig-mode`, or keep it at 0600/0644."),
|
|
11
|
+
K3S105: entityRule("K3S105", "security", "Registry TLS verification disabled", "Remove `insecure_skip_verify`; pin the registry CA via `ca_file`."),
|
|
12
|
+
K3S106: entityRule("K3S106", "correctness", "tls-san missing for a declared bind/advertise address", "Add the bind-address / advertise-address value to `tls-san`."),
|
|
13
|
+
K3S107: entityRule("K3S107", "correctness", "Disabled component still configured", "Remove the component's config key, or drop it from `disable`."),
|
|
14
|
+
};
|
|
15
|
+
// Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
|
|
16
|
+
applyLineage(k3sAuditCatalog, k3sAuditLineage);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { k3s101 } from "./k3s101.js";
|
|
2
|
+
import { k3s102 } from "./k3s102.js";
|
|
3
|
+
import { k3s103 } from "./k3s103.js";
|
|
4
|
+
import { k3s104 } from "./k3s104.js";
|
|
5
|
+
import { k3s105 } from "./k3s105.js";
|
|
6
|
+
import { k3s106 } from "./k3s106.js";
|
|
7
|
+
import { k3s107 } from "./k3s107.js";
|
|
8
|
+
export const postSynthChecks = [
|
|
9
|
+
k3s101,
|
|
10
|
+
k3s102,
|
|
11
|
+
k3s103,
|
|
12
|
+
k3s104,
|
|
13
|
+
k3s105,
|
|
14
|
+
k3s106,
|
|
15
|
+
k3s107,
|
|
16
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the k3s post-synth checks.
|
|
3
|
+
*
|
|
4
|
+
* These checks read the chant model (`ctx.entities`), not the emitted
|
|
5
|
+
* YAML: an emitted config.yaml carries no marker naming its own role, so
|
|
6
|
+
* the entity type is the only honest way to know a document is a server
|
|
7
|
+
* config, an agent config, or a registries file. The audit catalog marks
|
|
8
|
+
* every entity-based check `yamlBased: false` accordingly.
|
|
9
|
+
*/
|
|
10
|
+
/** Every declared k3s entity of one type, with its props. */
|
|
11
|
+
export function entitiesOfType(ctx, entityType) {
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const [name, entity] of ctx.entities) {
|
|
14
|
+
const e = entity;
|
|
15
|
+
if (e.entityType !== entityType)
|
|
16
|
+
continue;
|
|
17
|
+
const props = typeof e.props === "object" && e.props !== null ? e.props : {};
|
|
18
|
+
out.push({ name, entityType, props });
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
export function configEntities(ctx) {
|
|
23
|
+
return [...entitiesOfType(ctx, "K3s::Server"), ...entitiesOfType(ctx, "K3s::Agent")];
|
|
24
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K3S101: a literal join token reached the build.
|
|
3
|
+
*
|
|
4
|
+
* The typed surface omits `token` / `agent-token` (#1601) and K3S001
|
|
5
|
+
* catches the literal at source. This is the backstop for the path both
|
|
6
|
+
* miss: a raw prop object built at runtime, spread in, or emitted by a
|
|
7
|
+
* composite. If the key is present with a non-empty string value in the
|
|
8
|
+
* entity the build serialized, the emitted config.yaml carries the
|
|
9
|
+
* cluster's join secret, and the build fails.
|
|
10
|
+
*/
|
|
11
|
+
import { configEntities } from "./k3s-helpers.js";
|
|
12
|
+
/**
|
|
13
|
+
* `token`/`agent-token`/`cluster-secret` are the join secret. The etcd S3
|
|
14
|
+
* pair are snapshot-store credentials the flag surface types as plain
|
|
15
|
+
* strings — k3s's own docs point at `etcd-s3-config-secret` (a Secret on
|
|
16
|
+
* the cluster) for exactly this reason.
|
|
17
|
+
*/
|
|
18
|
+
const SECRET_KEYS = [
|
|
19
|
+
"token",
|
|
20
|
+
"agent-token",
|
|
21
|
+
"cluster-secret",
|
|
22
|
+
"etcd-s3-secret-key",
|
|
23
|
+
"etcd-s3-session-token",
|
|
24
|
+
];
|
|
25
|
+
export const k3s101 = {
|
|
26
|
+
id: "K3S101",
|
|
27
|
+
description: "A k3s config entity carries a literal join token",
|
|
28
|
+
check(ctx) {
|
|
29
|
+
const diagnostics = [];
|
|
30
|
+
for (const entity of configEntities(ctx)) {
|
|
31
|
+
for (const key of SECRET_KEYS) {
|
|
32
|
+
const value = entity.props[key];
|
|
33
|
+
if (typeof value === "string" && value.length > 0) {
|
|
34
|
+
diagnostics.push({
|
|
35
|
+
checkId: "K3S101",
|
|
36
|
+
severity: "error",
|
|
37
|
+
message: `"${entity.name}" carries a literal \`${key}\` — the emitted config.yaml would ` +
|
|
38
|
+
"contain the secret. Use the file/Secret reference form instead " +
|
|
39
|
+
"(`token-file`, `etcd-s3-config-secret`).",
|
|
40
|
+
entity: entity.name,
|
|
41
|
+
lexicon: "k3s",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return diagnostics;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K3S102: registry credentials as literals.
|
|
3
|
+
*
|
|
4
|
+
* registries.yaml supports inline `auth.password` / `auth.token`, and a
|
|
5
|
+
* committed estate that uses them has published its registry credentials.
|
|
6
|
+
* The TLS file paths beside them are host paths — those are fine; it is
|
|
7
|
+
* the credential values that must come from somewhere else.
|
|
8
|
+
*/
|
|
9
|
+
import { entitiesOfType } from "./k3s-helpers.js";
|
|
10
|
+
export const k3s102 = {
|
|
11
|
+
id: "K3S102",
|
|
12
|
+
description: "registries.yaml carries a literal registry credential",
|
|
13
|
+
check(ctx) {
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
for (const entity of entitiesOfType(ctx, "K3s::Registries")) {
|
|
16
|
+
const configs = entity.props.configs;
|
|
17
|
+
if (typeof configs !== "object" || configs === null)
|
|
18
|
+
continue;
|
|
19
|
+
for (const [registry, config] of Object.entries(configs)) {
|
|
20
|
+
const auth = resolveProps(config)?.auth;
|
|
21
|
+
const authProps = resolveProps(auth);
|
|
22
|
+
if (!authProps)
|
|
23
|
+
continue;
|
|
24
|
+
for (const key of ["password", "token"]) {
|
|
25
|
+
const value = authProps[key];
|
|
26
|
+
if (typeof value === "string" && value.length > 0) {
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
checkId: "K3S102",
|
|
29
|
+
severity: "error",
|
|
30
|
+
message: `"${entity.name}" carries a literal \`${key}\` for registry "${registry}" — ` +
|
|
31
|
+
"a committed registries.yaml is a leaked credential. Configure registry auth " +
|
|
32
|
+
"on the host outside the declaration.",
|
|
33
|
+
entity: entity.name,
|
|
34
|
+
lexicon: "k3s",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return diagnostics;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
/** A nested value may be a plain object or a property declarable carrying props. */
|
|
44
|
+
function resolveProps(value) {
|
|
45
|
+
if (typeof value !== "object" || value === null)
|
|
46
|
+
return undefined;
|
|
47
|
+
const obj = value;
|
|
48
|
+
if (typeof obj.props === "object" && obj.props !== null && "entityType" in obj) {
|
|
49
|
+
return obj.props;
|
|
50
|
+
}
|
|
51
|
+
return obj;
|
|
52
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
import { entitiesOfType } from "./k3s-helpers.js";
|
|
10
|
+
export const k3s103 = {
|
|
11
|
+
id: "K3S103",
|
|
12
|
+
description: "An agent config declares no server to join",
|
|
13
|
+
check(ctx) {
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
for (const entity of entitiesOfType(ctx, "K3s::Agent")) {
|
|
16
|
+
const server = entity.props.server;
|
|
17
|
+
if (typeof server === "string" && server.length > 0)
|
|
18
|
+
continue;
|
|
19
|
+
diagnostics.push({
|
|
20
|
+
checkId: "K3S103",
|
|
21
|
+
severity: "error",
|
|
22
|
+
message: `"${entity.name}" declares no \`server\` — the agent has no cluster to join. ` +
|
|
23
|
+
"Set `server: https://<server-host>:6443` in the declaration.",
|
|
24
|
+
entity: entity.name,
|
|
25
|
+
lexicon: "k3s",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return diagnostics;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
import { entitiesOfType } from "./k3s-helpers.js";
|
|
11
|
+
export const k3s104 = {
|
|
12
|
+
id: "K3S104",
|
|
13
|
+
description: "write-kubeconfig-mode is wider than 0644",
|
|
14
|
+
check(ctx) {
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
for (const entity of entitiesOfType(ctx, "K3s::Server")) {
|
|
17
|
+
const raw = entity.props["write-kubeconfig-mode"];
|
|
18
|
+
if (raw === undefined)
|
|
19
|
+
continue;
|
|
20
|
+
const mode = parseInt(String(raw), 8);
|
|
21
|
+
if (Number.isNaN(mode))
|
|
22
|
+
continue;
|
|
23
|
+
// Bits outside rw-r--r-- mean group/other write, or execute anywhere.
|
|
24
|
+
if ((mode & ~0o644) !== 0) {
|
|
25
|
+
diagnostics.push({
|
|
26
|
+
checkId: "K3S104",
|
|
27
|
+
severity: "warning",
|
|
28
|
+
message: `"${entity.name}" sets write-kubeconfig-mode ${String(raw)} — wider than 0644. ` +
|
|
29
|
+
"The kubeconfig carries the cluster admin credential; keep it 0600, or 0644 at most.",
|
|
30
|
+
entity: entity.name,
|
|
31
|
+
lexicon: "k3s",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return diagnostics;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
import { entitiesOfType } from "./k3s-helpers.js";
|
|
10
|
+
export const k3s105 = {
|
|
11
|
+
id: "K3S105",
|
|
12
|
+
description: "registries.yaml disables TLS verification for a registry",
|
|
13
|
+
check(ctx) {
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
for (const entity of entitiesOfType(ctx, "K3s::Registries")) {
|
|
16
|
+
const configs = entity.props.configs;
|
|
17
|
+
if (typeof configs !== "object" || configs === null)
|
|
18
|
+
continue;
|
|
19
|
+
for (const [registry, config] of Object.entries(configs)) {
|
|
20
|
+
const tls = resolveProps(resolveProps(config)?.tls);
|
|
21
|
+
if (tls?.insecure_skip_verify === true) {
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
checkId: "K3S105",
|
|
24
|
+
severity: "warning",
|
|
25
|
+
message: `"${entity.name}" disables TLS verification for registry "${registry}" — ` +
|
|
26
|
+
"every pull through it is unauthenticated. Pin the registry CA via `ca_file` instead.",
|
|
27
|
+
entity: entity.name,
|
|
28
|
+
lexicon: "k3s",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return diagnostics;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
function resolveProps(value) {
|
|
37
|
+
if (typeof value !== "object" || value === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
const obj = value;
|
|
40
|
+
if (typeof obj.props === "object" && obj.props !== null && "entityType" in obj) {
|
|
41
|
+
return obj.props;
|
|
42
|
+
}
|
|
43
|
+
return obj;
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K3S106: tls-san missing when the server is reachable at a declared
|
|
3
|
+
* address beyond the implicit default.
|
|
4
|
+
*
|
|
5
|
+
* The kube-apiserver certificate is only valid for the addresses baked
|
|
6
|
+
* into it. `bind-address` and `advertise-address` both say "clients reach
|
|
7
|
+
* this server somewhere other than its bare node-ip" — a fixed bind
|
|
8
|
+
* address, or a front-door address for an HA load balancer / VIP. Either
|
|
9
|
+
* one without a matching `tls-san` entry means the first connection
|
|
10
|
+
* through that address hits a certificate mismatch.
|
|
11
|
+
*/
|
|
12
|
+
import { entitiesOfType } from "./k3s-helpers.js";
|
|
13
|
+
function isNonEmptyString(value) {
|
|
14
|
+
return typeof value === "string" && value.length > 0;
|
|
15
|
+
}
|
|
16
|
+
export const k3s106 = {
|
|
17
|
+
id: "K3S106",
|
|
18
|
+
description: "tls-san is missing while bind-address or advertise-address is declared",
|
|
19
|
+
check(ctx) {
|
|
20
|
+
const diagnostics = [];
|
|
21
|
+
for (const entity of entitiesOfType(ctx, "K3s::Server")) {
|
|
22
|
+
const tlsSan = entity.props["tls-san"];
|
|
23
|
+
const hasTlsSan = isNonEmptyString(tlsSan) || (Array.isArray(tlsSan) && tlsSan.length > 0);
|
|
24
|
+
if (hasTlsSan)
|
|
25
|
+
continue;
|
|
26
|
+
const bindAddress = entity.props["bind-address"];
|
|
27
|
+
const advertiseAddress = entity.props["advertise-address"];
|
|
28
|
+
const declaredField = isNonEmptyString(bindAddress)
|
|
29
|
+
? "bind-address"
|
|
30
|
+
: isNonEmptyString(advertiseAddress)
|
|
31
|
+
? "advertise-address"
|
|
32
|
+
: undefined;
|
|
33
|
+
if (!declaredField)
|
|
34
|
+
continue;
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
checkId: "K3S106",
|
|
37
|
+
severity: "warning",
|
|
38
|
+
message: `"${entity.name}" sets \`${declaredField}\` but declares no \`tls-san\` — ` +
|
|
39
|
+
"the apiserver certificate won't cover that address, and the first client to reach " +
|
|
40
|
+
"it there hits a TLS mismatch. Add it to `tls-san`.",
|
|
41
|
+
entity: entity.name,
|
|
42
|
+
lexicon: "k3s",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return diagnostics;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K3S107: a packaged component is disabled but also configured.
|
|
3
|
+
*
|
|
4
|
+
* `disable` removes a packaged component entirely — the flags that
|
|
5
|
+
* configure it (`cluster-dns` for coredns, `servicelb-namespace` for
|
|
6
|
+
* servicelb, `default-local-storage-path` for local-storage) then land in
|
|
7
|
+
* config.yaml for a component that never starts. Dead config, and a
|
|
8
|
+
* signal the `disable` and the config drifted out of sync rather than
|
|
9
|
+
* being written together.
|
|
10
|
+
*/
|
|
11
|
+
import { entitiesOfType } from "./k3s-helpers.js";
|
|
12
|
+
/**
|
|
13
|
+
* Packaged component name (as it appears in `disable`) to the config keys
|
|
14
|
+
* that only make sense while that component runs. Limited to the
|
|
15
|
+
* components the k3s server flag surface actually has dedicated
|
|
16
|
+
* configuration for — traefik and metrics-server are configured via Helm
|
|
17
|
+
* chart manifests, not server flags, so there is nothing here to conflict.
|
|
18
|
+
*/
|
|
19
|
+
const COMPONENT_CONFIG_KEYS = {
|
|
20
|
+
coredns: ["cluster-dns"],
|
|
21
|
+
servicelb: ["servicelb-namespace"],
|
|
22
|
+
"local-storage": ["default-local-storage-path"],
|
|
23
|
+
};
|
|
24
|
+
export const k3s107 = {
|
|
25
|
+
id: "K3S107",
|
|
26
|
+
description: "disable names a component the config also configures",
|
|
27
|
+
check(ctx) {
|
|
28
|
+
const diagnostics = [];
|
|
29
|
+
for (const entity of entitiesOfType(ctx, "K3s::Server")) {
|
|
30
|
+
const raw = entity.props.disable;
|
|
31
|
+
const disabled = typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : [];
|
|
32
|
+
for (const component of disabled) {
|
|
33
|
+
if (typeof component !== "string")
|
|
34
|
+
continue;
|
|
35
|
+
const configKeys = COMPONENT_CONFIG_KEYS[component];
|
|
36
|
+
if (!configKeys)
|
|
37
|
+
continue;
|
|
38
|
+
for (const key of configKeys) {
|
|
39
|
+
const value = entity.props[key];
|
|
40
|
+
const isSet = (typeof value === "string" && value.length > 0) ||
|
|
41
|
+
(Array.isArray(value) && value.length > 0);
|
|
42
|
+
if (!isSet)
|
|
43
|
+
continue;
|
|
44
|
+
diagnostics.push({
|
|
45
|
+
checkId: "K3S107",
|
|
46
|
+
severity: "warning",
|
|
47
|
+
message: `"${entity.name}" disables \`${component}\` but also sets \`${key}\` — ` +
|
|
48
|
+
`that config has no effect on a component that never starts. Remove \`${key}\`, ` +
|
|
49
|
+
`or drop \`${component}\` from \`disable\`.`,
|
|
50
|
+
entity: entity.name,
|
|
51
|
+
lexicon: "k3s",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return diagnostics;
|
|
57
|
+
},
|
|
58
|
+
};
|