alchemy-docker-k3s 0.1.3 → 0.1.5

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/index.d.mts CHANGED
@@ -42,6 +42,29 @@ interface K3sDefinition {
42
42
  };
43
43
  /** K3s' built-in Flannel data plane. @default "vxlan" */
44
44
  flannelBackend?: "vxlan" | "wireguard-native";
45
+ /**
46
+ * Kubelet settings applied to every node. Changing them re-runs the K3s installer
47
+ * on each existing node in place, one node at a time; running containers keep running.
48
+ */
49
+ kubelet?: KubeletSettings;
50
+ }
51
+ interface KubeletSettings {
52
+ /**
53
+ * Image garbage collection. The kubelet starts deleting unused images when the
54
+ * image filesystem is `highThresholdPercent` full and stops at `lowThresholdPercent`.
55
+ * Unset keeps the kubelet defaults (85 % and 80 %).
56
+ */
57
+ imageGc?: {
58
+ /** Integer percentage, 1–100, above `lowThresholdPercent`. */
59
+ highThresholdPercent: number;
60
+ /** Integer percentage, 0–99, below `highThresholdPercent`. */
61
+ lowThresholdPercent: number;
62
+ };
63
+ /**
64
+ * Further kubelet flags as `name=value`, passed as `--kubelet-arg`. Flags the package
65
+ * manages itself (`cloud-provider`, `provider-id`, `image-gc-*`) are rejected.
66
+ */
67
+ extraArgs?: string[];
45
68
  }
46
69
  interface NormalizedK3sDefinition {
47
70
  channel: `v1.${number}`;
@@ -55,6 +78,8 @@ interface NormalizedK3sDefinition {
55
78
  traefikValues: Record<string, unknown>;
56
79
  };
57
80
  flannelBackend: "vxlan" | "wireguard-native";
81
+ /** Validated kubelet flags as `name=value`, without the `--kubelet-arg` prefix. */
82
+ kubeletArgs: string[];
58
83
  }
59
84
  interface ClusterVersion {
60
85
  node: string;
@@ -108,4 +133,4 @@ declare const Cluster: (id: string, props: ClusterProps) => Effect.Effect<Cluste
108
133
  //#region src/cluster-state.d.ts
109
134
  declare const ClusterState: import("alchemy").ResourceClass<ClusterResource>;
110
135
  //#endregion
111
- export { Cluster, type ClusterAttributes, type Cluster$1 as ClusterInstance, type ClusterProps, ClusterState, type K3sDefinition, type PortMapping, Providers, providers };
136
+ export { Cluster, type ClusterAttributes, type Cluster$1 as ClusterInstance, type ClusterProps, ClusterState, type K3sDefinition, type KubeletSettings, type PortMapping, Providers, providers };
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import * as Provider from "alchemy/Provider";
5
5
  import * as Redacted from "effect/Redacted";
6
6
  import { parse } from "yaml";
7
7
  import { chmod, mkdir, writeFile } from "node:fs/promises";
8
- import { dirname, join, resolve } from "node:path";
8
+ import { dirname, join } from "node:path";
9
9
  import { execFile } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
11
  import * as Layer from "effect/Layer";
@@ -51,6 +51,24 @@ const validateUpdateWindow = (window) => {
51
51
  }
52
52
  return window;
53
53
  };
54
+ const MANAGED_KUBELET_FLAGS = /^(cloud-provider|provider-id|image-gc-[a-z-]+)=/;
55
+ const KUBELET_FLAG = /^[a-z][a-z0-9-]*=\S*$/;
56
+ /** Validates kubelet settings and renders them as `name=value` kubelet flags. */
57
+ const kubeletArguments = (settings = {}) => {
58
+ const args = [];
59
+ if (settings.imageGc !== void 0) {
60
+ const { highThresholdPercent: high, lowThresholdPercent: low } = settings.imageGc;
61
+ const percent = (value) => Number.isInteger(value) && value >= 0 && value <= 100;
62
+ if (!percent(high) || !percent(low) || low >= high) throw new Error(`kubelet.imageGc thresholds must be integer percentages with lowThresholdPercent < highThresholdPercent; received ${JSON.stringify(settings.imageGc)}`);
63
+ args.push(`image-gc-high-threshold=${high}`, `image-gc-low-threshold=${low}`);
64
+ }
65
+ for (const arg of settings.extraArgs ?? []) {
66
+ if (!KUBELET_FLAG.test(arg)) throw new Error(`kubelet.extraArgs entries must be name=value without a leading --; received ${JSON.stringify(arg)}`);
67
+ if (MANAGED_KUBELET_FLAGS.test(arg)) throw new Error(`kubelet.extraArgs must not set ${JSON.stringify(arg.split("=")[0])}, which the package manages`);
68
+ args.push(arg);
69
+ }
70
+ return args;
71
+ };
54
72
  const normalizeK3sDefinition = (definition) => {
55
73
  const normalized = {
56
74
  channel: validateChannel(definition.channel),
@@ -63,7 +81,8 @@ const normalizeK3sDefinition = (definition) => {
63
81
  metricsServer: definition.addons?.metricsServer ?? true,
64
82
  traefikValues: definition.addons?.traefikValues ?? {}
65
83
  },
66
- flannelBackend: definition.flannelBackend ?? "vxlan"
84
+ flannelBackend: definition.flannelBackend ?? "vxlan",
85
+ kubeletArgs: kubeletArguments(definition.kubelet)
67
86
  };
68
87
  if (normalized.flannelBackend !== "vxlan" && normalized.flannelBackend !== "wireguard-native") throw new Error(`flannelBackend must be "vxlan" or "wireguard-native"; received ${JSON.stringify(normalized.flannelBackend)}`);
69
88
  const traefikValues = normalized.addons.traefikValues;
@@ -131,9 +150,16 @@ const resolveChannelVersion = async (channel, fetcher = fetch) => {
131
150
  };
132
151
  //#endregion
133
152
  //#region ../shared/src/kubeconfig.ts
153
+ /**
154
+ * Relative to the directory Alchemy runs from. Alchemy identifies a cluster by
155
+ * its connection's auth, which contains this path, so an absolute path would
156
+ * make every checkout or workspace look like a different cluster and replace
157
+ * everything deployed to it. Reading the cluster rewrites the file wherever
158
+ * Alchemy runs.
159
+ */
134
160
  const kubeconfigPath = (provider, fqn) => {
135
161
  const safe = fqn.replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-|-$/g, "");
136
- return resolve(join(".alchemy", "kubeconfigs", provider, `${safe}.yaml`));
162
+ return join(".alchemy", "kubeconfigs", provider, `${safe}.yaml`);
137
163
  };
138
164
  const writeKubeconfig = async (path, contents) => {
139
165
  await mkdir(dirname(path), {
@@ -234,7 +260,8 @@ const buildCreateArgs = (props, version, token) => {
234
260
  `--cluster-cidr=${props.k3s.clusterCidr}`,
235
261
  `--service-cidr=${props.k3s.serviceCidr}`,
236
262
  `--cluster-dns=${props.k3s.clusterDns}`,
237
- `--flannel-backend=${props.k3s.flannelBackend}`
263
+ `--flannel-backend=${props.k3s.flannelBackend}`,
264
+ ...props.k3s.kubeletArgs.map((arg) => `--kubelet-arg=${arg}`)
238
265
  ]) args.push("--k3s-arg", `${value}@server:0`);
239
266
  if (!props.k3s.addons.traefik) args.push("--k3s-arg", "--disable=traefik@server:0");
240
267
  if (!props.k3s.addons.metricsServer) args.push("--k3s-arg", "--disable=metrics-server@server:0");
@@ -418,7 +445,8 @@ const Cluster = (id, props) => Effect.gen(function* () {
418
445
  clusterCidr: k3s.clusterCidr,
419
446
  serviceCidr: k3s.serviceCidr,
420
447
  clusterDns: k3s.clusterDns,
421
- addons: k3s.addons
448
+ addons: k3s.addons,
449
+ kubeletArgs: k3s.kubeletArgs
422
450
  })
423
451
  });
424
452
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy-docker-k3s",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Persistent local k3d clusters for Alchemy",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",