@jr2/cli 0.2.1 → 0.4.1

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/src/nodes.ts CHANGED
@@ -7,13 +7,13 @@
7
7
  // at the moment of the read. The set moves (a pool autoscales, a node is cordoned), which is why an
8
8
  // empty set is reported and never refused.
9
9
 
10
- import type { SandboxPlacement, Toleration } from "@jr2/orchestrator";
10
+ import { cpuMillis, memoryBytes, type ResolvedSize, type SandboxPlacement, type Toleration } from "@jr2/orchestrator";
11
11
 
12
12
  /** A cluster node, read for the facts that decide placement and platform (ADR-0045/0052). */
13
13
  export type NodeObject = {
14
14
  metadata: { name: string; labels?: Record<string, string> };
15
15
  spec?: { unschedulable?: boolean; taints?: readonly Taint[] };
16
- status?: { nodeInfo?: { architecture?: string } };
16
+ status?: { nodeInfo?: { architecture?: string }; allocatable?: Record<string, string> };
17
17
  };
18
18
 
19
19
  export type Taint = { key: string; value?: string; effect: string };
@@ -72,3 +72,28 @@ export function tolerates(t: Toleration, taint: Taint): boolean {
72
72
  if (t.operator === "Exists") return true;
73
73
  return (t.value ?? "") === (taint.value ?? "");
74
74
  }
75
+
76
+ /**
77
+ * Whether any of `nodes` could hold `size` (ADR-0060): its allocatable memory and cpu both at least
78
+ * the whole Size — what the scheduler compares a Guaranteed pod against on an empty node. `undefined`
79
+ * when no node reports allocatable, which is an unknown, never a no. A WARNING at `jr2 up`, never a
80
+ * refusal: the node set moves (ADR-0052), and a pool may scale up a bigger node tomorrow.
81
+ */
82
+ export function anyNodeHolds(nodes: readonly NodeObject[], size: ResolvedSize): boolean | undefined {
83
+ let known = false;
84
+ for (const node of nodes) {
85
+ const alloc = node.status?.allocatable;
86
+ if (alloc?.memory === undefined || alloc.cpu === undefined) continue;
87
+ let memory: number;
88
+ let cpu: number;
89
+ try {
90
+ memory = memoryBytes(alloc.memory);
91
+ cpu = cpuMillis(alloc.cpu);
92
+ } catch {
93
+ continue;
94
+ }
95
+ known = true;
96
+ if (memory >= size.memory.bytes && cpu >= size.cpu.millis) return true;
97
+ }
98
+ return known ? false : undefined;
99
+ }
package/src/sweep.ts CHANGED
@@ -7,11 +7,12 @@
7
7
  // is not this instance's garbage):
8
8
  // 1. the `jr2-images` ConfigMap of every jr2 instance (namespaces labeled `jr2.dev/instance`) — what
9
9
  // FUTURE Sandboxes will be provisioned with;
10
- // 2. every Sandbox CR's `spec.image` in those namespaces — a parked Workspace must survive a pod
11
- // restart, and `imagePullPolicy: IfNotPresent` cannot re-pull a local tag;
12
- // 3. every pod's container images in those namespaces plus `jr2-system` — what is actually running
13
- // (orchestrator, Instance Harness, Adapter, Sandboxes, operator), mid-roll pods INCLUDED,
14
- // without naming Deployments one by one.
10
+ // 2. every Sandbox CR's `spec.image`, sidecar, init container and `image` volume in those
11
+ // namespaces — a parked Workspace must survive a pod restart, and `imagePullPolicy:
12
+ // IfNotPresent` cannot re-pull a local tag;
13
+ // 3. every pod's container and `image` volume refs in those namespaces plus `jr2-system` — what is
14
+ // actually running (orchestrator, Instance Harness, Custodians, Sandboxes, operator), mid-roll
15
+ // pods INCLUDED, without naming Deployments one by one.
15
16
  // The keep set is their union. "Kit images are never pruned" is not a rule here: a kit ref is kept
16
17
  // because some instance's map or pod names it, and collects like anything else when the last
17
18
  // instance leaves the cluster.
@@ -53,13 +54,18 @@ export type Roots = { keep: Set<string>; namespaces: string[] };
53
54
 
54
55
  type NamespacedObject = { metadata: { name: string; namespace?: string } };
55
56
  type ConfigMapObject = NamespacedObject & { data?: Record<string, string> };
56
- type SandboxObject = NamespacedObject & { spec?: { image?: string; sidecars?: ContainerSpec[] } };
57
+ type SandboxObject = NamespacedObject & {
58
+ spec?: { image?: string; sidecars?: ContainerSpec[]; initContainers?: ContainerSpec[]; volumes?: VolumeSpec[] };
59
+ };
57
60
  type ContainerSpec = { image?: string };
61
+ /** Only an `image` volume names a ref: every Sandbox's `/opt/jr2` is one, of the harness image (ADR-0037). */
62
+ type VolumeSpec = { image?: { reference?: string } };
58
63
  type PodObject = NamespacedObject & {
59
64
  spec?: {
60
65
  containers?: ContainerSpec[];
61
66
  initContainers?: ContainerSpec[];
62
67
  ephemeralContainers?: ContainerSpec[];
68
+ volumes?: VolumeSpec[];
63
69
  };
64
70
  };
65
71
 
@@ -93,18 +99,20 @@ export async function readRoots(kube: KubeAdmin, ctx: { context?: string } = {})
93
99
  }
94
100
 
95
101
  // 2. what a parked Workspace's pod will be recreated with — the Sandbox Image AND every sidecar's
96
- // ref. The CR always carries the Adapter as a sidecar (sandbox-kubectl.ts), and its ref is not
102
+ // ref. The CR always carries the Custodian as a sidecar (sandbox-kube.ts), and its ref is not
97
103
  // covered by the other roots: a running Sandbox is deliberately never re-imaged, so an `up` that
98
- // rebuilt the Adapter leaves the CR naming the OLD one while the map names the new. If that pod
104
+ // moved the Custodian's pin leaves the CR naming the OLD one while the map names the new. If that pod
99
105
  // is then lost (node restart, eviction, drain), the recreated one pulls the CR's sidecar ref —
100
- // and `IfNotPresent` cannot re-pull a local tag a sweep took.
106
+ // and `IfNotPresent` cannot re-pull a local tag a sweep took. The same holds for the harness ref: it
107
+ // reaches the pod only as the `runtime` image volume (ADR-0037), never as a container's image.
101
108
  const sandboxes = await listSandboxes(kube, ctx);
102
109
  for (const sandbox of sandboxes) {
103
110
  if (!instanceNs.has(sandbox.metadata.namespace ?? "")) continue;
104
111
  if (sandbox.spec?.image) keep.add(sandbox.spec.image);
105
- for (const sidecar of sandbox.spec?.sidecars ?? []) {
106
- if (sidecar.image) keep.add(sidecar.image);
112
+ for (const c of [...(sandbox.spec?.sidecars ?? []), ...(sandbox.spec?.initContainers ?? [])]) {
113
+ if (c.image) keep.add(c.image);
107
114
  }
115
+ for (const ref of volumeRefs(sandbox.spec?.volumes)) keep.add(ref);
108
116
  }
109
117
 
110
118
  // 3. what is running right now — plus the operator, which lives in the shared `jr2-system`.
@@ -116,11 +124,17 @@ export async function readRoots(kube: KubeAdmin, ctx: { context?: string } = {})
116
124
  for (const c of [...(spec.containers ?? []), ...(spec.initContainers ?? []), ...(spec.ephemeralContainers ?? [])]) {
117
125
  if (c.image) keep.add(c.image);
118
126
  }
127
+ for (const ref of volumeRefs(spec.volumes)) keep.add(ref);
119
128
  }
120
129
 
121
130
  return { keep, namespaces };
122
131
  }
123
132
 
133
+ /** The refs a volume list mounts as `image` volumes — invisible to a read of containers alone. */
134
+ function volumeRefs(volumes: VolumeSpec[] | undefined): string[] {
135
+ return (volumes ?? []).flatMap((v) => (v.image?.reference ? [v.image.reference] : []));
136
+ }
137
+
124
138
  /**
125
139
  * Root #2's read, with the ONE degradation the fail-closed rule allows: a cluster that has no
126
140
  * `sandboxes.core.jr2.dev` resource type can hold no Sandbox CRs, so "no Sandboxes" is the complete
@@ -141,8 +155,8 @@ async function listSandboxes(kube: KubeAdmin, ctx: { context?: string }): Promis
141
155
  }
142
156
  }
143
157
 
144
- /** Every ref one instance's image map names: the kit's own at the top level (`harness`, `adapter`,
145
- * and the `operator` that rides the same JSON), and each Sandbox Image under `sandbox`. An
158
+ /** Every ref one instance's image map names: the kit's own at the top level (`harness`,
159
+ * `custodian`, and the `operator` that rides the same JSON), and each Sandbox Image under `sandbox`. An
146
160
  * unreadable map is a FAILED root, not an empty one — a hand-edited ConfigMap must not be read as
147
161
  * "that instance needs nothing". */
148
162
  function imageMapRefs(cm: ConfigMapObject): string[] {