@yadurajfleetos/cli 0.16.0 → 0.17.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/dist/commands/doctor.js +33 -2
- package/dist/commands/services.js +22 -2
- package/dist/commands/up.js +21 -0
- package/dist/detect.js +50 -15
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -3,6 +3,37 @@ import { loadProfile } from '../config.js';
|
|
|
3
3
|
import { c, relativeTime } from '../render.js';
|
|
4
4
|
import { glyph, rule, task } from '../ui.js';
|
|
5
5
|
const icon = (state) => state === 'ok' ? glyph.ok : state === 'warn' ? glyph.warn : glyph.fail;
|
|
6
|
+
/**
|
|
7
|
+
* How full a node's disk is, from the right two numbers.
|
|
8
|
+
*
|
|
9
|
+
* It reported 615% used, which is arithmetic that cannot be right and quietly
|
|
10
|
+
* undermines every other line of the report. The denominator was `node.diskMb`
|
|
11
|
+
* — and the control plane says, in a comment directly above the field it sends
|
|
12
|
+
* instead:
|
|
13
|
+
*
|
|
14
|
+
* Capacity. node.diskMb is FREE space and is what the scheduler places
|
|
15
|
+
* against, so it is not the denominator for a "used of total" reading.
|
|
16
|
+
*
|
|
17
|
+
* Somebody wrote that warning and this divided by the wrong one anyway. Used
|
|
18
|
+
* over free exceeds 100% the moment a disk is more than half full, which is
|
|
19
|
+
* why the number looked wild rather than merely wrong.
|
|
20
|
+
*
|
|
21
|
+
* An agent too old to report a capacity gets no percentage at all. A missing
|
|
22
|
+
* figure is a gap somebody can fix; an invented one is a number people act on.
|
|
23
|
+
*/
|
|
24
|
+
export function diskUse(usedMb, totalMb) {
|
|
25
|
+
if (!totalMb || usedMb === undefined) {
|
|
26
|
+
return { state: 'ok', detail: 'capacity not reported by this agent' };
|
|
27
|
+
}
|
|
28
|
+
const percent = Math.round((usedMb / totalMb) * 100);
|
|
29
|
+
return {
|
|
30
|
+
state: percent >= 90 ? 'fail' : percent >= 80 ? 'warn' : 'ok',
|
|
31
|
+
detail: `${percent}% used · ${Math.round(usedMb / 1024)}GB of ${Math.round(totalMb / 1024)}GB`,
|
|
32
|
+
remedy: percent >= 80
|
|
33
|
+
? 'Free space from Docker images/volumes before the node becomes unschedulable.'
|
|
34
|
+
: undefined,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
6
37
|
/**
|
|
7
38
|
* Services that answer on a health path but do not declare one.
|
|
8
39
|
*
|
|
@@ -191,7 +222,7 @@ export const doctorCommand = {
|
|
|
191
222
|
});
|
|
192
223
|
for (const node of result.nodes) {
|
|
193
224
|
const runtime = node.telemetry?.runtime;
|
|
194
|
-
const
|
|
225
|
+
const disk = diskUse(node.telemetry?.diskUsedMb, node.telemetry?.diskTotalMb);
|
|
195
226
|
// Redis intentionally retains the last heartbeat briefly, but a node
|
|
196
227
|
// that has stopped reporting must not have old host facts rendered as
|
|
197
228
|
// current failures. The heartbeat check above is the only actionable
|
|
@@ -207,7 +238,7 @@ export const doctorCommand = {
|
|
|
207
238
|
}
|
|
208
239
|
checks.push({ state: runtime?.dockerAvailable ? 'ok' : 'fail', label: `Docker ${node.name}`, detail: runtime?.dockerAvailable ? `available${runtime.dockerVersion ? ` · ${runtime.dockerVersion}` : ''}` : runtime?.dockerError ?? 'No Docker runtime reported', remedy: runtime?.dockerAvailable ? undefined : 'Start Docker, then inspect the local fleet-agent log.' });
|
|
209
240
|
checks.push({ state: runtime?.registryStatus === 'ok' ? 'ok' : runtime?.registryStatus === 'failed' ? 'fail' : 'warn', label: `registry ${node.name}`, detail: runtime?.registryStatus === 'ok' ? 'latest real image pull succeeded' : runtime?.registryError ?? 'not tested by a real image pull yet', remedy: runtime?.registryStatus === 'ok' ? undefined : 'Use a LAN-reachable REGISTRY_URL, then restart a service to run an authenticated pull.' });
|
|
210
|
-
checks.push({
|
|
241
|
+
checks.push({ ...disk, label: `disk ${node.name}` });
|
|
211
242
|
if (runtime?.lastReconcileError)
|
|
212
243
|
checks.push({ state: 'fail', label: `reconcile ${node.name}`, detail: runtime.lastReconcileError, remedy: 'Run `fleet logs <service> --follow` and inspect the deployment history.' });
|
|
213
244
|
}
|
|
@@ -332,6 +332,24 @@ export const rescheduleCommand = {
|
|
|
332
332
|
console.log(`${c.green('moved')} ${service.name} → ${c.bold(body.movedTo.name)}`);
|
|
333
333
|
},
|
|
334
334
|
};
|
|
335
|
+
/**
|
|
336
|
+
* A building row, with what the builder is actually doing.
|
|
337
|
+
*
|
|
338
|
+
* Every field here has been travelling from buildx through Redis to the API for
|
|
339
|
+
* a while and stopping there, so this shows what already exists rather than
|
|
340
|
+
* measuring anything new. Emulation is called out because it is the usual
|
|
341
|
+
* answer to "why is this taking twenty minutes".
|
|
342
|
+
*/
|
|
343
|
+
function buildStatus(status, progress) {
|
|
344
|
+
const parts = [statusColour(status)];
|
|
345
|
+
if (progress.step && progress.ofSteps)
|
|
346
|
+
parts.push(c.dim(`${progress.step}/${progress.ofSteps}`));
|
|
347
|
+
if (progress.platform)
|
|
348
|
+
parts.push(c.dim(progress.platform));
|
|
349
|
+
if (progress.emulated)
|
|
350
|
+
parts.push(c.yellow('emulated'));
|
|
351
|
+
return parts.join(' ');
|
|
352
|
+
}
|
|
335
353
|
export const deploymentsCommand = {
|
|
336
354
|
async run(args, flags) {
|
|
337
355
|
const fleetId = await requireFleet(typeof flags.fleet === 'string' ? flags.fleet : undefined);
|
|
@@ -346,8 +364,10 @@ export const deploymentsCommand = {
|
|
|
346
364
|
relativeTime(d.startedAt),
|
|
347
365
|
d.gitSha?.slice(0, 7) ?? c.dim('—'),
|
|
348
366
|
d.nodeName ?? c.dim('—'),
|
|
349
|
-
|
|
350
|
-
|
|
367
|
+
// "building" alone reads as stuck. The step counter is what tells a
|
|
368
|
+
// reader the difference between a slow build and a hung one.
|
|
369
|
+
d.progress ? buildStatus(d.status, d.progress) : statusColour(d.status),
|
|
370
|
+
d.failureReason ?? (d.progress?.detail ? c.dim(d.progress.detail) : ''),
|
|
351
371
|
])));
|
|
352
372
|
},
|
|
353
373
|
};
|
package/dist/commands/up.js
CHANGED
|
@@ -188,6 +188,27 @@ async function deployOne(service, opts) {
|
|
|
188
188
|
// is measured in tens of minutes, not minutes.
|
|
189
189
|
const deadline = Date.now() + 45 * 60_000;
|
|
190
190
|
while (Date.now() < deadline) {
|
|
191
|
+
// What the builder is doing, rather than a hint about what it might
|
|
192
|
+
// be doing. Every field here has been reaching /progress since the
|
|
193
|
+
// phase writer was added and nothing asked for it, so this loop sat
|
|
194
|
+
// cycling generic advice past a reader who could have been told the
|
|
195
|
+
// step number. Failures are swallowed: this is a label, and losing it
|
|
196
|
+
// must not end a deploy that is going fine.
|
|
197
|
+
const line = await request('GET', `/services/${service.id}/progress`)
|
|
198
|
+
.then((r) => r.body)
|
|
199
|
+
.catch(() => null);
|
|
200
|
+
if (line && ['queued', 'building', 'pushing'].includes(line.status)) {
|
|
201
|
+
const parts = [line.status];
|
|
202
|
+
if (line.step && line.ofSteps)
|
|
203
|
+
parts.push(`${line.step}/${line.ofSteps}`);
|
|
204
|
+
if (line.platform)
|
|
205
|
+
parts.push(line.platform);
|
|
206
|
+
if (line.emulated)
|
|
207
|
+
parts.push('emulated');
|
|
208
|
+
s.update(`${c.bold(service.name)} · ${parts.join(' · ')}`);
|
|
209
|
+
if (line.detail)
|
|
210
|
+
s.hints([line.detail]);
|
|
211
|
+
}
|
|
191
212
|
const { body } = await request('GET', `/fleets/${opts.fleetId}/services`);
|
|
192
213
|
const current = body.services.find((s) => s.id === service.id)?.current;
|
|
193
214
|
if (current?.status === 'running')
|
package/dist/detect.js
CHANGED
|
@@ -45,11 +45,12 @@ const hasDep = (pkg, name) => {
|
|
|
45
45
|
return Boolean(deps?.[name] || devDeps?.[name]);
|
|
46
46
|
};
|
|
47
47
|
// ── Dockerfile templates ────────────────────────────────────────────────
|
|
48
|
-
const NEXTJS_DOCKERFILE = `#
|
|
48
|
+
const NEXTJS_DOCKERFILE = `# syntax=docker/dockerfile:1
|
|
49
|
+
# --- Build ---
|
|
49
50
|
FROM node:22-alpine AS builder
|
|
50
51
|
WORKDIR /app
|
|
51
52
|
COPY package*.json ./
|
|
52
|
-
RUN npm ci
|
|
53
|
+
RUN --mount=type=cache,target=/root/.npm npm ci
|
|
53
54
|
COPY . .
|
|
54
55
|
RUN npm run build
|
|
55
56
|
|
|
@@ -63,11 +64,12 @@ COPY --from=builder /app/public ./public
|
|
|
63
64
|
EXPOSE 3000
|
|
64
65
|
CMD ["node", "server.js"]
|
|
65
66
|
`;
|
|
66
|
-
const VITE_DOCKERFILE = `#
|
|
67
|
+
const VITE_DOCKERFILE = `# syntax=docker/dockerfile:1
|
|
68
|
+
# --- Build ---
|
|
67
69
|
FROM node:22-alpine AS builder
|
|
68
70
|
WORKDIR /app
|
|
69
71
|
COPY package*.json ./
|
|
70
|
-
RUN npm ci
|
|
72
|
+
RUN --mount=type=cache,target=/root/.npm npm ci
|
|
71
73
|
COPY . .
|
|
72
74
|
RUN npm run build
|
|
73
75
|
|
|
@@ -77,32 +79,57 @@ COPY --from=builder /app/dist /usr/share/nginx/html
|
|
|
77
79
|
EXPOSE 80
|
|
78
80
|
CMD ["nginx", "-g", "daemon off;"]
|
|
79
81
|
`;
|
|
80
|
-
const NODE_DOCKERFILE =
|
|
82
|
+
const NODE_DOCKERFILE = `# syntax=docker/dockerfile:1
|
|
83
|
+
FROM node:22-alpine
|
|
81
84
|
WORKDIR /app
|
|
82
85
|
COPY package*.json ./
|
|
83
|
-
RUN npm ci --omit=dev
|
|
86
|
+
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
|
|
84
87
|
COPY . .
|
|
85
88
|
EXPOSE 3000
|
|
86
89
|
CMD ["node", "src/index.js"]
|
|
87
90
|
`;
|
|
88
|
-
|
|
91
|
+
/**
|
|
92
|
+
* `--mount=type=cache` rather than `--no-cache-dir`.
|
|
93
|
+
*
|
|
94
|
+
* The two look interchangeable and are opposites. `--no-cache-dir` tells pip to
|
|
95
|
+
* keep no wheel cache *inside the layer*, which keeps the image small and makes
|
|
96
|
+
* every rebuild download and recompile from nothing. A BuildKit cache mount
|
|
97
|
+
* lives outside the image entirely — it is not a layer, so it adds nothing to
|
|
98
|
+
* the final size — and survives between builds, so a rebuild that changes only
|
|
99
|
+
* application source reuses every wheel it already has.
|
|
100
|
+
*
|
|
101
|
+
* That is the difference between a two-minute rebuild and a twenty-minute one
|
|
102
|
+
* for anything with `cryptography` or `psycopg2` in it, and more again when the
|
|
103
|
+
* build is emulated.
|
|
104
|
+
*
|
|
105
|
+
* The syntax line is required: `RUN --mount` is a Dockerfile frontend feature,
|
|
106
|
+
* and without it an older builder fails on the flag rather than ignoring it.
|
|
107
|
+
*/
|
|
108
|
+
const PYTHON_DOCKERFILE = (entry, usesPoetry) => `# syntax=docker/dockerfile:1
|
|
109
|
+
FROM python:3.12-slim
|
|
89
110
|
WORKDIR /app
|
|
90
111
|
${usesPoetry
|
|
91
112
|
? `COPY pyproject.toml poetry.lock* ./
|
|
92
|
-
RUN
|
|
113
|
+
RUN --mount=type=cache,target=/root/.cache/pip \\
|
|
114
|
+
--mount=type=cache,target=/root/.cache/pypoetry \\
|
|
115
|
+
pip install poetry && poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi --no-dev`
|
|
93
116
|
: `COPY requirements*.txt ./
|
|
94
|
-
RUN
|
|
117
|
+
RUN --mount=type=cache,target=/root/.cache/pip \\
|
|
118
|
+
pip install -r requirements.txt`}
|
|
95
119
|
COPY . .
|
|
96
120
|
EXPOSE 8000
|
|
97
121
|
CMD ["python", "-m", "${entry}"]
|
|
98
122
|
`;
|
|
99
|
-
const GO_DOCKERFILE = (module) => `#
|
|
123
|
+
const GO_DOCKERFILE = (module) => `# syntax=docker/dockerfile:1
|
|
124
|
+
# --- Build ---
|
|
100
125
|
FROM golang:1.24-alpine AS builder
|
|
101
126
|
WORKDIR /app
|
|
102
127
|
COPY go.mod go.sum* ./
|
|
103
|
-
RUN go mod download
|
|
128
|
+
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
|
104
129
|
COPY . .
|
|
105
|
-
RUN
|
|
130
|
+
RUN --mount=type=cache,target=/go/pkg/mod \\
|
|
131
|
+
--mount=type=cache,target=/root/.cache/go-build \\
|
|
132
|
+
CGO_ENABLED=0 go build -o /server .
|
|
106
133
|
|
|
107
134
|
# --- Run ---
|
|
108
135
|
FROM alpine:3.21
|
|
@@ -110,13 +137,21 @@ COPY --from=builder /server /server
|
|
|
110
137
|
EXPOSE 8080
|
|
111
138
|
CMD ["/server"]
|
|
112
139
|
`;
|
|
113
|
-
const RUST_DOCKERFILE = `#
|
|
140
|
+
const RUST_DOCKERFILE = `# syntax=docker/dockerfile:1
|
|
141
|
+
# --- Build ---
|
|
114
142
|
FROM rust:1.87-slim AS builder
|
|
115
143
|
WORKDIR /app
|
|
116
144
|
COPY Cargo.toml Cargo.lock* ./
|
|
117
|
-
RUN
|
|
145
|
+
RUN --mount=type=cache,target=/usr/local/cargo/registry \\
|
|
146
|
+
mkdir src && echo 'fn main(){}' > src/main.rs && cargo build --release && rm -rf src
|
|
118
147
|
COPY . .
|
|
119
|
-
|
|
148
|
+
# Only the registry is cached, deliberately. A cache mount on /app/target would
|
|
149
|
+
# speed the compile up and put the binary somewhere the runtime stage cannot
|
|
150
|
+
# copy from: a mount is not part of the image, so "COPY --from=builder
|
|
151
|
+
# /app/target/release/*" would find an empty directory and the image would
|
|
152
|
+
# build cleanly and contain nothing.
|
|
153
|
+
RUN --mount=type=cache,target=/usr/local/cargo/registry \\
|
|
154
|
+
cargo build --release
|
|
120
155
|
|
|
121
156
|
# --- Run ---
|
|
122
157
|
FROM debian:bookworm-slim
|