@intentius/chant-lexicon-k3s 0.46.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/describe-resources.d.ts +82 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.d.ts +1 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/k3s106.d.ts +14 -0
- package/dist/lint/post-synth/k3s106.d.ts.map +1 -0
- package/dist/lint/post-synth/k3s107.d.ts +13 -0
- package/dist/lint/post-synth/k3s107.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +22 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/activities/k3s.d.ts +98 -0
- package/dist/op/activities/k3s.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/skills/chant-k3s.md +45 -0
- package/package.json +8 -3
- package/src/describe-resources.test.ts +209 -0
- package/src/describe-resources.ts +258 -0
- package/src/lint/audit-catalog.ts +13 -1
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/k3s106.ts +57 -0
- package/src/lint/post-synth/k3s107.ts +66 -0
- package/src/lint/post-synth/post-synth.test.ts +86 -0
- package/src/op/activities/index.ts +30 -0
- package/src/op/activities/k3s.test.ts +182 -0
- package/src/op/activities/k3s.ts +170 -0
- package/src/plugin.ts +17 -0
- package/src/skills/chant-k3s.md +45 -0
package/README.md
CHANGED
|
@@ -31,6 +31,6 @@ npx vitest run lexicons/k3s
|
|
|
31
31
|
- `src/serializer.ts` — config.yaml / registries.yaml emitter
|
|
32
32
|
- `src/spec/fetch.ts` — pinned upstream sources (the CLI flag definitions)
|
|
33
33
|
- `src/codegen/parse.ts` — Go flag-definition parser
|
|
34
|
-
- `src/lint/` — K3S001 pre-synth rule, K3S101–
|
|
34
|
+
- `src/lint/` — K3S001 pre-synth rule, K3S101–K3S107 post-synth checks
|
|
35
35
|
- `src/lsp/` — completions and hover from the generated registry
|
|
36
36
|
- `src/generated/` — generated artifacts (do not edit)
|
|
@@ -0,0 +1,82 @@
|
|
|
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 type { DescribeResourcesResult } from "@intentius/chant/lexicon";
|
|
53
|
+
import { type DeclaredEntity } from "@intentius/chant/observation";
|
|
54
|
+
/** Injectable command runner, so tests drive every branch without kubectl or a cluster. */
|
|
55
|
+
export type ExecFn = (command: string) => Promise<{
|
|
56
|
+
stdout: string;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* The environment's kube context (#1488): the declared `k8s.profiles.<env>`
|
|
60
|
+
* binding when present, ambient otherwise. This is deliberately the SAME
|
|
61
|
+
* binding the k8s and helm lexicons read — a cluster is one target however
|
|
62
|
+
* many lexicons observe it — not a `k3s.profiles` of its own.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveK3sContext(environment: string): Promise<string | undefined>;
|
|
65
|
+
/** The declared node identity: the `node-name` flag, chant's only honest
|
|
66
|
+
* source. Absent means k3s will name the node after the host's own hostname
|
|
67
|
+
* — unknowable from here, so there is nothing to query by. */
|
|
68
|
+
export declare function declaredNodeName(entity: DeclaredEntity): string | undefined;
|
|
69
|
+
export interface DescribeResourcesOptions {
|
|
70
|
+
environment: string;
|
|
71
|
+
buildOutput: string;
|
|
72
|
+
entityNames: string[];
|
|
73
|
+
entities: Map<string, {
|
|
74
|
+
entityType: string;
|
|
75
|
+
props: Record<string, unknown>;
|
|
76
|
+
}>;
|
|
77
|
+
/** Restrict to chant-owned nodes (#1348). Withheld foreign nodes are
|
|
78
|
+
* `filtered`, never a silent drop into `absent`. */
|
|
79
|
+
owned?: boolean;
|
|
80
|
+
}
|
|
81
|
+
export declare function describeResources(options: DescribeResourcesOptions, execFn?: ExecFn): Promise<DescribeResourcesResult>;
|
|
82
|
+
//# sourceMappingURL=describe-resources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"describe-resources.d.ts","sourceRoot":"","sources":["../src/describe-resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAIH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAEL,KAAK,cAAc,EAGpB,MAAM,8BAA8B,CAAC;AAOtC,2FAA2F;AAC3F,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAStE;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAOxF;AAED;;8DAE8D;AAC9D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS,CAG3E;AAsHD,MAAM,WAAW,wBAAwB;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9E;wDACoD;IACpD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,EACjC,MAAM,GAAE,MAAkB,GACzB,OAAO,CAAC,uBAAuB,CAAC,CA4BlC"}
|
package/dist/integrity.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"manifest.json": "5208bf3cb7308fcf74bcfae5cd70e1e4c73b0b567e71ac0f593246f1d72eaef2",
|
|
5
5
|
"meta.json": "5988c37e9e93d52828d34fa043b0804fec7eb833153d387a9711333bcdff4bff",
|
|
6
6
|
"types/index.d.ts": "98f3f3175d38aa2979c57577512c554eafed49b9125967c8617c137c01358e26",
|
|
7
|
-
"skills/chant-k3s.md": "
|
|
7
|
+
"skills/chant-k3s.md": "647bd3e1c6f75b1695150b895273edff6e25fe5851ce85dd3a213280fb6a0e75"
|
|
8
8
|
},
|
|
9
|
-
"composite": "
|
|
9
|
+
"composite": "c5aa14712fce0c3baf881168a643b0af27bdd787ad431c959e07ada098c73bbd"
|
|
10
10
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* post-synth check has an entry, or the check contributes nothing to
|
|
5
5
|
* `chant audit`, silently.
|
|
6
6
|
*
|
|
7
|
-
* All
|
|
7
|
+
* All checks read the chant model (`ctx.entities`) rather than the
|
|
8
8
|
* emitted YAML — an emitted config.yaml carries no marker naming its own
|
|
9
9
|
* role — so every entry is constructed directly with `yamlBased: false`.
|
|
10
10
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-catalog.d.ts","sourceRoot":"","sources":["../../src/lint/audit-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAW/D,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"audit-catalog.d.ts","sourceRoot":"","sources":["../../src/lint/audit-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAW/D,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CA2CpD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AASvE,eAAO,MAAM,eAAe,EAAE,cAAc,EAQ3C,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
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 type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
13
|
+
export declare const k3s106: PostSynthCheck;
|
|
14
|
+
//# sourceMappingURL=k3s106.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"k3s106.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/k3s106.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,kCAAkC,CAAC;AAO1C,eAAO,MAAM,MAAM,EAAE,cAiCpB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
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 type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
12
|
+
export declare const k3s107: PostSynthCheck;
|
|
13
|
+
//# sourceMappingURL=k3s107.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"k3s107.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/k3s107.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,cAAc,EAGf,MAAM,kCAAkC,CAAC;AAgB1C,eAAO,MAAM,MAAM,EAAE,cAkCpB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
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, re-exported from
|
|
15
|
+
* the temporal Op-authoring barrel 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 a
|
|
18
|
+
* Temporal worker loads them cheaply.
|
|
19
|
+
*/
|
|
20
|
+
export { k3sInstall, k3sUninstall, k3sInstallCommand, k3sInstallEnv, k3sUninstallCommand, k3sUninstallScript, k3sVersionCommand, parseK3sVersion, } from "./k3s.js";
|
|
21
|
+
export type { K3sRole, K3sInstallArgs, K3sUninstallArgs, K3sInstallResult } from "./k3s.js";
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/op/activities/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EACL,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,GAChB,MAAM,OAAO,CAAC;AACf,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** `k3s server` runs the control plane; `k3s agent` joins one as a worker. */
|
|
2
|
+
export type K3sRole = "server" | "agent";
|
|
3
|
+
export interface K3sInstallArgs {
|
|
4
|
+
/** Which k3s subcommand the installer configures (`k3s server` / `k3s agent`). */
|
|
5
|
+
role: K3sRole;
|
|
6
|
+
/**
|
|
7
|
+
* Path to the chant-emitted `config.yaml` on the reachable host, passed as
|
|
8
|
+
* `--config` to the installed binary. chant does not write this file itself
|
|
9
|
+
* — a build/apply step upstream of this activity is responsible for getting
|
|
10
|
+
* it onto the host (host provisioning is out of scope, chant#1598).
|
|
11
|
+
*/
|
|
12
|
+
configFile: string;
|
|
13
|
+
/**
|
|
14
|
+
* Installer version pin (`INSTALL_K3S_VERSION`). Default: the lexicon's
|
|
15
|
+
* {@link K3S_VERSION} pin — bump that constant, not this arg, to track a
|
|
16
|
+
* new upstream release across the whole lexicon.
|
|
17
|
+
*/
|
|
18
|
+
version?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Path to a file on the host holding the cluster join token, passed to the
|
|
21
|
+
* installer as `K3S_TOKEN_FILE`. This is the only token-shaped input this
|
|
22
|
+
* activity accepts — see the "token boundary" note below.
|
|
23
|
+
*/
|
|
24
|
+
tokenFile?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface K3sUninstallArgs {
|
|
27
|
+
/** Which uninstall script to run — k3s ships a separate one per role. */
|
|
28
|
+
role: K3sRole;
|
|
29
|
+
}
|
|
30
|
+
/** What {@link k3sInstall} resolved. */
|
|
31
|
+
export interface K3sInstallResult {
|
|
32
|
+
/** The k3s version now installed (the target version, whether freshly installed or already present). */
|
|
33
|
+
version: string;
|
|
34
|
+
/** `false` when an already-installed matching version made the install a no-op. */
|
|
35
|
+
installed: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `k3s --version` output, first line: `k3s version v1.36.3+k3s1 (hash)`.
|
|
39
|
+
* Returns `undefined` when k3s is not installed or the output doesn't match.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseK3sVersion(stdout: string): string | undefined;
|
|
42
|
+
/** Check-install-version command — errors (non-zero exit) when k3s is absent. */
|
|
43
|
+
export declare function k3sVersionCommand(): string;
|
|
44
|
+
/**
|
|
45
|
+
* Build the get.k3s.io installer command. The version pin and the join-token
|
|
46
|
+
* reference (if any) travel as environment variables via {@link k3sInstallEnv},
|
|
47
|
+
* not interpolated into this string — see the token-boundary note on
|
|
48
|
+
* {@link k3sInstall}.
|
|
49
|
+
*/
|
|
50
|
+
export declare function k3sInstallCommand(args: K3sInstallArgs): string;
|
|
51
|
+
/**
|
|
52
|
+
* Environment for {@link k3sInstallCommand}: `INSTALL_K3S_VERSION` (the pin)
|
|
53
|
+
* and, only when a `tokenFile` path is given, `K3S_TOKEN_FILE`.
|
|
54
|
+
*
|
|
55
|
+
* ## The token boundary (#1601)
|
|
56
|
+
*
|
|
57
|
+
* This is the only token-shaped input the install activity accepts, and it
|
|
58
|
+
* is a path, not a secret — the join token itself never passes through
|
|
59
|
+
* activity args, never gets logged, and never lands in an emitted
|
|
60
|
+
* config.yaml (K3S001 rejects a literal `token`/`agent-token` at lint). A
|
|
61
|
+
* `K3S_TOKEN` (the literal, env-var form k3s itself supports for agents) is
|
|
62
|
+
* deliberately not modeled here: the reference form the lexicon carries
|
|
63
|
+
* end-to-end is a file path, matching `token-file`/`agent-token-file` on the
|
|
64
|
+
* declared config surface (chant#1365's provenance stance applied at this
|
|
65
|
+
* surface, not a new mechanism).
|
|
66
|
+
*/
|
|
67
|
+
export declare function k3sInstallEnv(args: K3sInstallArgs): Record<string, string>;
|
|
68
|
+
/** Path k3s installs its uninstall script at — one per role. */
|
|
69
|
+
export declare function k3sUninstallScript(role: K3sRole): string;
|
|
70
|
+
/**
|
|
71
|
+
* Build the uninstall command. Gated the way `k3dDown` is (chant#1410): no
|
|
72
|
+
* pre-check activity, no SSH orchestration — just the native idempotent
|
|
73
|
+
* behavior. Unlike `k3d cluster delete` (idempotent on its own), the k3s
|
|
74
|
+
* uninstall script does not exist at all on a host where k3s was never
|
|
75
|
+
* installed, so the command guards on the script's presence itself rather
|
|
76
|
+
* than assume it errors safely.
|
|
77
|
+
*/
|
|
78
|
+
export declare function k3sUninstallCommand(args: K3sUninstallArgs): string;
|
|
79
|
+
/**
|
|
80
|
+
* Run the pinned k3s installer against a reachable host. Idempotent on an
|
|
81
|
+
* already-installed matching version: if `k3s --version` already reports the
|
|
82
|
+
* target version, the install is skipped. Uses longInfra profile — 20m
|
|
83
|
+
* timeout, heartbeat every 15s (the installer downloads and starts the
|
|
84
|
+
* k3s binary).
|
|
85
|
+
*
|
|
86
|
+
* Bounded exactly as `k3dUp`/`k3dDown` were (chant#1410, epic #1598): this
|
|
87
|
+
* drives the case where the host is reachable from where the Op runs. It
|
|
88
|
+
* does not provision the host, and it is not an SSH orchestrator.
|
|
89
|
+
*/
|
|
90
|
+
export declare function k3sInstall(args: K3sInstallArgs, signal?: AbortSignal): Promise<K3sInstallResult>;
|
|
91
|
+
/**
|
|
92
|
+
* Uninstall k3s from a reachable host. Uses fastIdempotent profile — 5m
|
|
93
|
+
* timeout. A host where k3s was never installed (no uninstall script present)
|
|
94
|
+
* is a no-op success, the same idempotent shape as `k3dDown` against an
|
|
95
|
+
* already-gone cluster.
|
|
96
|
+
*/
|
|
97
|
+
export declare function k3sUninstall(args: K3sUninstallArgs, signal?: AbortSignal): Promise<void>;
|
|
98
|
+
//# sourceMappingURL=k3s.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"k3s.d.ts","sourceRoot":"","sources":["../../../src/op/activities/k3s.ts"],"names":[],"mappings":"AAOA,8EAA8E;AAC9E,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEzC,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,IAAI,EAAE,OAAO,CAAC;IACd;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wCAAwC;AACxC,MAAM,WAAW,gBAAgB;IAC/B,wGAAwG;IACxG,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAElE;AAED,iFAAiF;AACjF,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAE9D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI1E;AAED,gEAAgE;AAChE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAExD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAGlE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAiC3B;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAI9F"}
|
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;AAgB7F;;;;GAIG;AACH,eAAO,MAAM,SAAS,EAAE,aA4HvB,CAAC"}
|
package/dist/skills/chant-k3s.md
CHANGED
|
@@ -70,6 +70,49 @@ export const registries = new Registries({
|
|
|
70
70
|
Literal `auth.password` / `auth.token` fail K3S102; `insecure_skip_verify`
|
|
71
71
|
warns via K3S105 — pin the CA instead.
|
|
72
72
|
|
|
73
|
+
## Op lifecycle (reachable-host case)
|
|
74
|
+
|
|
75
|
+
`k3sInstall` / `k3sUninstall` (chant#1601) drive a k3s installer against a
|
|
76
|
+
host the Op runs on or can reach directly — the same boundary `k3dUp`/
|
|
77
|
+
`k3dDown` draw (chant#1410): no host provisioning, no SSH orchestration.
|
|
78
|
+
Requires `"k3s"` in the project's `chant.config.ts` `lexicons`.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { Op, phase, k3sInstall, k3sUninstall } from "@intentius/chant-lexicon-temporal";
|
|
82
|
+
|
|
83
|
+
export default Op({
|
|
84
|
+
name: "k3s-controlplane",
|
|
85
|
+
phases: [
|
|
86
|
+
phase("Install", [
|
|
87
|
+
k3sInstall("server", { configFile: "/etc/rancher/k3s/config.yaml" }),
|
|
88
|
+
]),
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`k3sInstall` runs the pinned `get.k3s.io` installer (`INSTALL_K3S_VERSION`
|
|
94
|
+
from the lexicon's pin, or an explicit `version` override) with `--config`
|
|
95
|
+
pointing at a config.yaml already on the host — a build/apply step upstream
|
|
96
|
+
puts it there. It is idempotent on an already-installed matching version:
|
|
97
|
+
`k3s --version` is checked first, and a match skips the install entirely.
|
|
98
|
+
`k3sUninstall` runs the matching uninstall script; a host where k3s was
|
|
99
|
+
never installed is a no-op success.
|
|
100
|
+
|
|
101
|
+
**The token boundary carries through to the Op surface.** There is no
|
|
102
|
+
`token` option on `k3sInstall` — only `tokenFile`, a path passed to the
|
|
103
|
+
installer as `K3S_TOKEN_FILE`. The join secret's value never appears in
|
|
104
|
+
Op-authored TypeScript (which is committed source, same as a config.yaml
|
|
105
|
+
declaration), is never logged, and is never interpolated into the installer
|
|
106
|
+
command string — it travels only as an environment variable set on the
|
|
107
|
+
child process at install time.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
k3sInstall("agent", {
|
|
111
|
+
configFile: "/etc/rancher/k3s/config.yaml",
|
|
112
|
+
tokenFile: "/etc/rancher/k3s/agent-token", // a path, not the secret
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
73
116
|
## Output shape
|
|
74
117
|
|
|
75
118
|
One file per declared entity. The first Server/Agent config is the primary
|
|
@@ -88,3 +131,5 @@ with any kubectl.
|
|
|
88
131
|
| K3S103 | error | an Agent with no `server` to join |
|
|
89
132
|
| K3S104 | warning | kubeconfig written wider than 0644 |
|
|
90
133
|
| K3S105 | warning | registry TLS verification disabled |
|
|
134
|
+
| K3S106 | warning | tls-san missing for a declared bind/advertise address |
|
|
135
|
+
| K3S107 | warning | disable names a component the config also configures |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-k3s",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/",
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"types": "./dist/*.d.ts",
|
|
18
18
|
"default": "./src/*.ts"
|
|
19
19
|
},
|
|
20
|
+
"./op/activities": {
|
|
21
|
+
"development": "./src/op/activities/index.ts",
|
|
22
|
+
"types": "./dist/op/activities/index.d.ts",
|
|
23
|
+
"default": "./src/op/activities/index.ts"
|
|
24
|
+
},
|
|
20
25
|
"./lint/post-synth": {
|
|
21
26
|
"development": "./src/lint/post-synth/index.ts",
|
|
22
27
|
"types": "./dist/lint/post-synth/index.d.ts",
|
|
@@ -38,14 +43,14 @@
|
|
|
38
43
|
"@intentius/chant": "*",
|
|
39
44
|
"tsc-alias": "^1.8.17",
|
|
40
45
|
"typescript": "^5.9.3",
|
|
41
|
-
"js-yaml": "^4.1
|
|
46
|
+
"js-yaml": "^4.3.1",
|
|
42
47
|
"@types/js-yaml": "^4.0.9"
|
|
43
48
|
},
|
|
44
49
|
"publishConfig": {
|
|
45
50
|
"access": "public"
|
|
46
51
|
},
|
|
47
52
|
"peerDependencies": {
|
|
48
|
-
"@intentius/chant": "^0.
|
|
53
|
+
"@intentius/chant": "^0.49.0",
|
|
49
54
|
"typescript": "^5.9.3"
|
|
50
55
|
},
|
|
51
56
|
"description": "k3s lexicon for chant — the k3s distro's host configuration as declarable data",
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { describeObservationConformance } from "@intentius/chant-test-utils";
|
|
3
|
+
import { normalizeObservation } from "@intentius/chant/observation";
|
|
4
|
+
import {
|
|
5
|
+
describeResources,
|
|
6
|
+
declaredNodeName,
|
|
7
|
+
type ExecFn,
|
|
8
|
+
} from "./describe-resources";
|
|
9
|
+
import { k3sPlugin } from "./plugin";
|
|
10
|
+
import { SERVER_TYPE, AGENT_TYPE, REGISTRIES_TYPE } from "./serializer";
|
|
11
|
+
import { K3S_VERSION } from "./spec/fetch";
|
|
12
|
+
|
|
13
|
+
const VERSION_CMD = "kubectl version -o json";
|
|
14
|
+
|
|
15
|
+
function fakeExec(handlers: Record<string, string | Error>): ExecFn {
|
|
16
|
+
return async (command: string) => {
|
|
17
|
+
for (const [prefix, result] of Object.entries(handlers)) {
|
|
18
|
+
if (command.startsWith(prefix)) {
|
|
19
|
+
if (result instanceof Error) throw result;
|
|
20
|
+
return { stdout: result };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`unexpected command: ${command}`);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const VERSION_OK = JSON.stringify({ serverVersion: { gitVersion: K3S_VERSION } });
|
|
28
|
+
const VERSION_OLD = JSON.stringify({ serverVersion: { gitVersion: "v1.30.0+k3s1" } });
|
|
29
|
+
|
|
30
|
+
function readyNode(labels: Record<string, string> = {}) {
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
metadata: { uid: "node-uid-1", labels, creationTimestamp: "2026-01-01T00:00:00Z" },
|
|
33
|
+
status: {
|
|
34
|
+
nodeInfo: { kubeletVersion: K3S_VERSION },
|
|
35
|
+
conditions: [{ type: "Ready", status: "True" }],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const OWNED_LABELS = {
|
|
41
|
+
"app.kubernetes.io/managed-by": "chant",
|
|
42
|
+
"chant.intentius.io/stack": "probe",
|
|
43
|
+
"chant.intentius.io/env": "local",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function options(
|
|
47
|
+
entities: Array<[string, string, Record<string, unknown>]> = [["cp", SERVER_TYPE, { "node-name": "cp-1" }]],
|
|
48
|
+
extra: { owned?: boolean } = {},
|
|
49
|
+
) {
|
|
50
|
+
return {
|
|
51
|
+
environment: "probe-env-unbound",
|
|
52
|
+
buildOutput: "",
|
|
53
|
+
entityNames: entities.map(([name]) => name),
|
|
54
|
+
entities: new Map(entities.map(([name, entityType, props]) => [name, { entityType, props }])),
|
|
55
|
+
...extra,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("k3s describeResources", () => {
|
|
60
|
+
it("reports a Ready, chant-labelled server node as present and owned", async () => {
|
|
61
|
+
const result = await describeResources(
|
|
62
|
+
options(),
|
|
63
|
+
fakeExec({ [VERSION_CMD]: VERSION_OK, "kubectl get node": readyNode(OWNED_LABELS) }),
|
|
64
|
+
);
|
|
65
|
+
const { resources } = normalizeObservation(result);
|
|
66
|
+
expect(resources.cp.status).toBe("Ready");
|
|
67
|
+
expect(resources.cp.ownership).toBe("owned");
|
|
68
|
+
expect(resources.cp.physicalId).toBe("node-uid-1");
|
|
69
|
+
expect((resources.cp.attributes as Record<string, unknown>).versionMatchesPin).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("reports a node without the marker as foreign", async () => {
|
|
73
|
+
const result = await describeResources(
|
|
74
|
+
options(),
|
|
75
|
+
fakeExec({ [VERSION_CMD]: VERSION_OK, "kubectl get node": readyNode() }),
|
|
76
|
+
);
|
|
77
|
+
const { resources } = normalizeObservation(result);
|
|
78
|
+
expect(resources.cp.ownership).toBe("foreign");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("reports the live k3s build against the pin — a mismatch is informational, not a failure", async () => {
|
|
82
|
+
const result = await describeResources(
|
|
83
|
+
options(),
|
|
84
|
+
fakeExec({ [VERSION_CMD]: VERSION_OLD, "kubectl get node": readyNode(OWNED_LABELS) }),
|
|
85
|
+
);
|
|
86
|
+
const { resources } = normalizeObservation(result);
|
|
87
|
+
expect((resources.cp.attributes as Record<string, unknown>).k3sVersion).toBe("v1.30.0+k3s1");
|
|
88
|
+
expect((resources.cp.attributes as Record<string, unknown>).versionMatchesPin).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("reports a NotReady node's reason as the status", async () => {
|
|
92
|
+
const notReady = JSON.stringify({
|
|
93
|
+
metadata: { uid: "node-uid-2", labels: OWNED_LABELS },
|
|
94
|
+
status: { conditions: [{ type: "Ready", status: "False", reason: "KubeletNotReady" }] },
|
|
95
|
+
});
|
|
96
|
+
const result = await describeResources(
|
|
97
|
+
options(),
|
|
98
|
+
fakeExec({ [VERSION_CMD]: VERSION_OK, "kubectl get node": notReady }),
|
|
99
|
+
);
|
|
100
|
+
const { resources } = normalizeObservation(result);
|
|
101
|
+
expect(resources.cp.status).toBe("KubeletNotReady");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("reports a missing node as absent — in neither map", async () => {
|
|
105
|
+
const result = await describeResources(
|
|
106
|
+
options(),
|
|
107
|
+
fakeExec({
|
|
108
|
+
[VERSION_CMD]: VERSION_OK,
|
|
109
|
+
"kubectl get node": new Error(`Error from server (NotFound): nodes "cp-1" not found`),
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
const { resources, unobserved } = normalizeObservation(result);
|
|
113
|
+
expect(resources.cp).toBeUndefined();
|
|
114
|
+
expect(unobserved.cp).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("reports not-observed, naming the context, when the apiserver is unreachable — never absence", async () => {
|
|
118
|
+
const result = await describeResources(
|
|
119
|
+
options([
|
|
120
|
+
["cp", SERVER_TYPE, { "node-name": "cp-1" }],
|
|
121
|
+
["worker", AGENT_TYPE, { "node-name": "agent-1" }],
|
|
122
|
+
]),
|
|
123
|
+
fakeExec({ [VERSION_CMD]: new Error("some unexpected apiserver fault") }),
|
|
124
|
+
);
|
|
125
|
+
const { resources, unobserved } = normalizeObservation(result);
|
|
126
|
+
expect(Object.keys(resources)).toHaveLength(0);
|
|
127
|
+
expect(unobserved.cp.reason).toBe("read-failed");
|
|
128
|
+
expect(unobserved.worker.reason).toBe("read-failed");
|
|
129
|
+
expect(unobserved.cp.detail).toContain("kubectl context");
|
|
130
|
+
expect(unobserved.cp.detail).toContain("apiserver fault");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("reports read-failed for an entity with no declared node-name — never a guessed address", async () => {
|
|
134
|
+
const result = await describeResources(
|
|
135
|
+
options([["cp", SERVER_TYPE, {}]]),
|
|
136
|
+
fakeExec({ [VERSION_CMD]: VERSION_OK }),
|
|
137
|
+
);
|
|
138
|
+
const { resources, unobserved } = normalizeObservation(result);
|
|
139
|
+
expect(resources.cp).toBeUndefined();
|
|
140
|
+
expect(unobserved.cp.reason).toBe("read-failed");
|
|
141
|
+
expect(unobserved.cp.detail).toContain("node-name");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("reports K3s::Registries as unsupported-kind — no live object exists to read", async () => {
|
|
145
|
+
const result = await describeResources(
|
|
146
|
+
options([["reg", REGISTRIES_TYPE, {}]]),
|
|
147
|
+
fakeExec({ [VERSION_CMD]: VERSION_OK }),
|
|
148
|
+
);
|
|
149
|
+
const { unobserved } = normalizeObservation(result);
|
|
150
|
+
expect(unobserved.reg.reason).toBe("unsupported-kind");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("filters a foreign node out under --owned, without reporting it absent", async () => {
|
|
154
|
+
const result = await describeResources(
|
|
155
|
+
options(undefined, { owned: true }),
|
|
156
|
+
fakeExec({ [VERSION_CMD]: VERSION_OK, "kubectl get node": readyNode() }),
|
|
157
|
+
);
|
|
158
|
+
const { resources, unobserved } = normalizeObservation(result);
|
|
159
|
+
expect(resources.cp).toBeUndefined();
|
|
160
|
+
expect(unobserved.cp.reason).toBe("filtered");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("keeps an owned node present under --owned", async () => {
|
|
164
|
+
const result = await describeResources(
|
|
165
|
+
options(undefined, { owned: true }),
|
|
166
|
+
fakeExec({ [VERSION_CMD]: VERSION_OK, "kubectl get node": readyNode(OWNED_LABELS) }),
|
|
167
|
+
);
|
|
168
|
+
const { resources } = normalizeObservation(result);
|
|
169
|
+
expect(resources.cp.ownership).toBe("owned");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("resolves node identity from the declared node-name only — never a guess", () => {
|
|
173
|
+
expect(declaredNodeName({ name: "entity", type: SERVER_TYPE, props: { "node-name": "real" } })).toBe(
|
|
174
|
+
"real",
|
|
175
|
+
);
|
|
176
|
+
expect(declaredNodeName({ name: "entity", type: SERVER_TYPE, props: {} })).toBeUndefined();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describeObservationConformance({
|
|
181
|
+
lexicon: "k3s",
|
|
182
|
+
ownershipChannel: k3sPlugin.ownershipChannel,
|
|
183
|
+
scenarios: [
|
|
184
|
+
{
|
|
185
|
+
name: "Ready owned server node",
|
|
186
|
+
declared: ["cp"],
|
|
187
|
+
owned: true,
|
|
188
|
+
expectPresent: ["cp"],
|
|
189
|
+
run: () =>
|
|
190
|
+
describeResources(options(), fakeExec({ [VERSION_CMD]: VERSION_OK, "kubectl get node": readyNode(OWNED_LABELS) })),
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "apiserver unreachable",
|
|
194
|
+
declared: ["cp"],
|
|
195
|
+
expectUnobserved: ["cp"],
|
|
196
|
+
run: () => describeResources(options(), fakeExec({ [VERSION_CMD]: new Error("connection refused") })),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "node absent",
|
|
200
|
+
declared: ["cp"],
|
|
201
|
+
expectAbsent: ["cp"],
|
|
202
|
+
run: () =>
|
|
203
|
+
describeResources(
|
|
204
|
+
options(),
|
|
205
|
+
fakeExec({ [VERSION_CMD]: VERSION_OK, "kubectl get node": new Error("NotFound") }),
|
|
206
|
+
),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|