alexandr 0.3.1 → 0.3.3
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/package.json +1 -1
- package/src/commands.js +25 -2
- package/src/instance.js +12 -0
- package/src/probe.js +11 -1
- package/templates/docker-compose.yml +8 -0
- package/templates/env.example +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alexandr",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Run the alexandr workspace runtime locally (npx alexandr up), and develop alexandr apps against a workspace (alexandr app link | dev | build | deploy).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/commands.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "./docker.js";
|
|
16
16
|
import {
|
|
17
17
|
resolveInstance, isMaterialized, materialize, readEnv, envHas, setEnv,
|
|
18
|
-
unsetEnv, kernelPort, pruneSnapshots,
|
|
18
|
+
unsetEnv, kernelPort, pruneSnapshots, dockerConfigDir,
|
|
19
19
|
} from "./instance.js";
|
|
20
20
|
export { updater } from "./updater.js";
|
|
21
21
|
import { kernelUrl, health, version as kVersion, waitHealthy, waitPosture } from "./probe.js";
|
|
@@ -111,7 +111,12 @@ function profileArgs(dir) {
|
|
|
111
111
|
function stampUpdaterEnv(inst) {
|
|
112
112
|
setEnv(inst.dir, "ALEXANDR_INSTANCE_DIR", hostPath(inst.dir));
|
|
113
113
|
setEnv(inst.dir, "ALEXANDR_COMPOSE_PROJECT", inst.projectName);
|
|
114
|
-
if (process.env.ALEXANDR_UPDATER_INSIDE !== "1")
|
|
114
|
+
if (process.env.ALEXANDR_UPDATER_INSIDE !== "1") {
|
|
115
|
+
setEnv(inst.dir, "ALEXANDR_UPDATER_IMAGE_TAG", pkgVersion());
|
|
116
|
+
// The host's docker config, so the sidecar's pulls carry this box's registry login
|
|
117
|
+
// (found on the first real press: an anonymous pull of the private image → `unauthorized`).
|
|
118
|
+
setEnv(inst.dir, "ALEXANDR_DOCKER_CONFIG", hostPath(dockerConfigDir()));
|
|
119
|
+
}
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
/**
|
|
@@ -611,6 +616,9 @@ export async function update(flags) {
|
|
|
611
616
|
stampUpdaterEnv(inst);
|
|
612
617
|
}
|
|
613
618
|
const prevImage = readEnv(inst.dir).ALEXANDR_IMAGE || `${IMAGE_BASE}:latest`;
|
|
619
|
+
// What `.previous-image` said BEFORE this run — restored if the pull fails (see below).
|
|
620
|
+
const pointerPath = path.join(inst.dir, ".previous-image");
|
|
621
|
+
const priorPointer = fs.existsSync(pointerPath) ? fs.readFileSync(pointerPath, "utf8") : null;
|
|
614
622
|
const base = kernelUrl(kernelPort(inst.dir));
|
|
615
623
|
const before = (await kVersion(base))?.version ?? "?";
|
|
616
624
|
|
|
@@ -657,6 +665,21 @@ export async function update(flags) {
|
|
|
657
665
|
pull = compose(inst.dir, inst.projectName, ["pull", "kernel"]);
|
|
658
666
|
}
|
|
659
667
|
if (pull.status !== 0) {
|
|
668
|
+
// ⚠ PUT THE PIN BACK. The new tag was written into .env before the pull so compose would
|
|
669
|
+
// pull it; a failed pull must not leave the box pointing at an image it does not have —
|
|
670
|
+
// the next `up` would then boot the new version behind the operator's back, which is
|
|
671
|
+
// exactly what the first sidecar press on aos did (2026-09-08). `.previous-image` is
|
|
672
|
+
// restored too, so a later `--rollback` still means what it says.
|
|
673
|
+
setEnv(inst.dir, "ALEXANDR_IMAGE", prevImage);
|
|
674
|
+
if (priorPointer === null) {
|
|
675
|
+
try {
|
|
676
|
+
fs.unlinkSync(pointerPath);
|
|
677
|
+
} catch {
|
|
678
|
+
/* nothing was recorded before this run either */
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
fs.writeFileSync(pointerPath, priorPointer);
|
|
682
|
+
}
|
|
660
683
|
fail("Pull failed — check your network / the image tag.", EXIT.RUNTIME);
|
|
661
684
|
}
|
|
662
685
|
}
|
package/src/instance.js
CHANGED
|
@@ -128,6 +128,18 @@ export function pruneSnapshots(dir, keep = 3) {
|
|
|
128
128
|
return removed;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Where THIS host's Docker CLI keeps its config — the registry login `alexandr up`/`login`
|
|
133
|
+
* wrote (`docker login` honours `DOCKER_CONFIG`, else `~/.docker`). The updater sidecar mounts
|
|
134
|
+
* it read-only: a pull's credential is sent by the client, not held by the daemon, so a docker
|
|
135
|
+
* CLI inside the sidecar with no config pulls anonymously and the private kernel image answers
|
|
136
|
+
* `unauthorized` — exactly what the first real press did (aos, 2026-09-08).
|
|
137
|
+
*/
|
|
138
|
+
export function dockerConfigDir(env = process.env, home = os.homedir()) {
|
|
139
|
+
const explicit = (env.DOCKER_CONFIG || "").trim();
|
|
140
|
+
return explicit || path.join(home, ".docker");
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
export function kernelPort(dir) {
|
|
132
144
|
const v = readEnv(dir).ALEXANDR_KERNEL_PORT;
|
|
133
145
|
const n = v ? Number(v) : 3030;
|
package/src/probe.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// Health/version probing of a running kernel over its loopback port. Uses the
|
|
2
2
|
// global fetch (Node 18+). All probes are best-effort and never throw.
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Where the kernel answers from HERE. On the host that is the loopback port compose mapped;
|
|
6
|
+
* inside the updater sidecar 127.0.0.1 is the sidecar itself, so the compose file sets
|
|
7
|
+
* `ALEXANDR_KERNEL_URL=http://kernel:3030` (the service name on the project network) — without
|
|
8
|
+
* it the recipe's health wait could not see the kernel from inside and reported `? → ?`
|
|
9
|
+
* (the first sidecar press on aos, 2026-09-08).
|
|
10
|
+
*/
|
|
11
|
+
export const kernelUrl = (port = 3030, env = process.env) => {
|
|
12
|
+
const explicit = (env.ALEXANDR_KERNEL_URL || "").trim();
|
|
13
|
+
return explicit ? explicit.replace(/\/+$/, "") : `http://127.0.0.1:${port}`;
|
|
14
|
+
};
|
|
5
15
|
|
|
6
16
|
async function getJson(url, timeoutMs = 2500) {
|
|
7
17
|
const ctrl = new AbortController();
|
|
@@ -48,12 +48,20 @@ services:
|
|
|
48
48
|
ALEXANDR_INSTANCE_DIR: ${ALEXANDR_INSTANCE_DIR:-/nonexistent}
|
|
49
49
|
ALEXANDR_COMPOSE_PROJECT: ${ALEXANDR_COMPOSE_PROJECT:-alexandr}
|
|
50
50
|
ALEXANDR_UPDATER_SOCKET: /run/alexandr/updater.sock
|
|
51
|
+
# The kernel, by its service name: from inside this container 127.0.0.1 is the sidecar
|
|
52
|
+
# itself, and the recipe's health wait must see the kernel come back.
|
|
53
|
+
ALEXANDR_KERNEL_URL: http://kernel:3030
|
|
51
54
|
env_file:
|
|
52
55
|
- path: ./.env
|
|
53
56
|
required: false # ALEXANDR_CP_URL / ALEXANDR_RELEASES_URL → the feed it reads
|
|
54
57
|
volumes:
|
|
55
58
|
- /var/run/docker.sock:/var/run/docker.sock # THE socket. Only here.
|
|
56
59
|
- ${ALEXANDR_INSTANCE_DIR:-/nonexistent}:${ALEXANDR_INSTANCE_DIR:-/nonexistent}
|
|
60
|
+
# The host's Docker CONFIG, read-only — the registry login `alexandr up`/`login` wrote
|
|
61
|
+
# lives there, and a pull's credential is sent by the CLIENT, not held by the daemon:
|
|
62
|
+
# without this the sidecar's own docker CLI pulls anonymously and the private kernel
|
|
63
|
+
# image answers `unauthorized` (found on the first real press, aos, 2026-09-08).
|
|
64
|
+
- ${ALEXANDR_DOCKER_CONFIG:-/nonexistent}:/root/.docker:ro
|
|
57
65
|
- updater_run:/run/alexandr
|
|
58
66
|
restart: unless-stopped
|
|
59
67
|
|
package/templates/env.example
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
# ALEXANDR_INSTANCE_DIR= # this directory, at its host path (mounted at the same path)
|
|
20
20
|
# ALEXANDR_COMPOSE_PROJECT= # the compose project the sidecar drives (the host's, verbatim)
|
|
21
21
|
# ALEXANDR_UPDATER_IMAGE_TAG= # the CLI version that installed it (= the sidecar's version)
|
|
22
|
+
# ALEXANDR_DOCKER_CONFIG= # this host's docker config dir (its registry login), mounted read-only
|
|
22
23
|
|
|
23
24
|
# ---- account link (REQUIRED — the runtime refuses to serve unlinked) --------
|
|
24
25
|
# Every runtime must be linked to an alexandr account before it serves anyone
|