Wise Foundry Alloy is the central manager of Wise Foundry: one place to enroll and operate many Wise Foundry Iron hosts, with its own services running as containers on Wise Foundry Steel.
  • TypeScript 91.4%
  • Shell 6.7%
  • CSS 0.7%
  • PLpgSQL 0.4%
  • JavaScript 0.3%
  • Other 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Michael J. Manley 611822a978 Datacenter pages, clock skew, plugin features, shared storage, versioning
- Datacenters have their own page; the Navigator selects exactly the node clicked.
- Host clock offsets measured from heartbeats and replies; clock_skew alert,
  health check, and refusal errors that name the offset and the likely culprit
  (schema 54).
- Iron plugin features in Alloy: plugin pages load over the relay with Alloy's
  permissions; Import from Proxmox from the VMs toolbar, admission-checked.
- Shared storage: add an NFS, OCFS2 or shareable plugin datastore to other
  hosts with a per-host plan, CHAP re-entered never stored, proven by the probe;
  a cluster Shared storage view.
- scripts/version.sh: release/YY.MM branches set the version.
- Sweep fixes: test-steel bundles its driver for musl node_modules, the fake
  enroll host signs with its host id, a clock test no longer reads build time
  as skew.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvgVC2H8Wa7QukpNzTJia9
2026-10-01 16:43:54 -07:00
api Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00
db Host management, inventory, tasks and events, secrets and upgrades for CTP 2026-09-26 20:45:48 -07:00
deploy Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00
docs Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00
locales/en Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00
scripts Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00
test Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00
web Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00
.containerignore Updates by build, a deployed signing key, Iron-style builds 2026-09-15 23:19:52 -07:00
.env.example Installer ISO with Install, Upgrade and Restore; images carried once, in the pack; port 443 2026-09-27 18:04:09 -07:00
.gitignore Every string a person reads comes from a translation catalog 2026-09-29 11:46:26 -07:00
alloy-icon.png Version by build date, use Alloy's icon, show Iron's VLAN trunks 2026-09-24 08:23:46 -07:00
alloy-product-logo-white.png Version by build date, use Alloy's icon, show Iron's VLAN trunks 2026-09-24 08:23:46 -07:00
alloy-product-logo.png Version by build date, use Alloy's icon, show Iron's VLAN trunks 2026-09-24 08:23:46 -07:00
CHANGELOG.md Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00
CLAUDE.md Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00
compose.yaml Wise Foundry Alloy 0.1.0 2026-09-15 22:32:11 -07:00
LICENSE.md Wise Foundry Alloy 0.1.0 2026-09-15 22:32:11 -07:00
Makefile Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00
NOTICE.md Identity: TOTP two-factor sign-in, OpenID Connect, LDAP, break-glass accounts 2026-09-27 21:23:57 -07:00
README.md Datacenter pages, clock skew, plugin features, shared storage, versioning 2026-10-01 16:43:54 -07:00

Wise Foundry Alloy

Wise Foundry Alloy is the central manager of Wise Foundry: one place to enroll and operate many Wise Foundry Iron hosts, with its own services running as containers on Wise Foundry Steel.

This repository is currently a proof of concept that sets up the product's shape for the main project:

  • Three container images, each with its own Compose file: PostgreSQL (db/), the Node.js API and manager (api/), and the web console behind nginx (web/). The root compose.yaml includes all three as one project.

  • The same web console as Iron and Steel: login, header and Navigator, Recent tasks, idle logout, and change password, with pages for the overview, users and roles, tasks and events, clusters and hosts.

  • Sessions like Steel's: an HttpOnly session cookie, a CSRF header, login back-off, three roles and an audit log, stored in PostgreSQL.

  • An installer for Iron: an Electron wizard (deploy/) on one ISO — a Linux Electron in linux/, a Windows one in windows/, and the Steel OVA and update bundle and Alloy's images and pack in payload/ — that installs Alloy (imports Steel on a Wise Foundry Iron host, finds its address, deploys Alloy on it as a Compose stack), upgrades a running Alloy through Alloy's own upgrade (and its Steel appliance, when the ISO's Steel is newer), or restores one from a Steel backup onto a new appliance. See docs/deploy.md.

  • Host enrollment as specified by Iron (iron/docs/alloy.md):

    • single-use enrollment codes;
    • enrollment, signed confirmation and heartbeats, and leaving;
    • Alloy's signed, certificate-pinned calls to hosts: a live query and removal.

    See docs/enrollment.md.

  • Counts in the Navigator: every Navigator entry that stands for a collection ends with how many things are in it — clusters, hosts, datastores per host, port groups, oSwitches, virtual routers, and the items under each host and folder in the Virtual Machines tree. An entry that is a single page (Manage, Monitor) says nothing, because there is nothing to count. The numbers come from what the console has already read, so a count never costs a request of its own. Wise Foundry Iron and the Alloy Steel Appliance do the same in their own consoles.

  • Home and a section switcher beside the logo: a login lands on Home, which gathers every part of the console into groups — Settings (the pages above), Iron Control (Virtual Machines, Storage and Networking) and Updates (Update Management) — as a list in the Navigator and as tiles on the page. Each section has its own Navigator, and the switcher beside the logo moves between them and back to Home.

  • Virtual Machines: a tree of every enrolled host's virtual machines, LXC and Docker containers, read from the hosts with Alloy's signed calls, viewed by folder, by host or by kind. Operators sort them into global folders and subfolders — one folder holds workloads of any host — by drag and drop or Move to; folders and placement are kept in PostgreSQL. From the same pages they create, start, stop, snapshot, clone, edit and delete those workloads: Alloy asks the host's own API and follows the job it starts. Consoles open here too: graphical (noVNC) or serial for a virtual machine, Console, TTY or Shell for an LXC container, and an exec session for a Docker container. Alloy signs the WebSocket upgrade and passes the frames to the host untouched. See docs/vms.md.

  • Storage: every enrolled host's datastores in one table, and the datastore browser Iron has — folders and files, upload, download, new folder, move and delete — with the host in the address. New datastore puts one datastore on the hosts an operator chooses, which is what an NFS export is for; mounting and removing offer every host that has one of that name, and removing never erases data. Uploads stream from the browser through Alloy to the host and are never written to Alloy's disk. See docs/datastores.md.

  • Networking: every enrolled host's networking, gathered around the one part of it that is a cluster matter — the port group name. A machine's spec names its port group, so it only migrates to a host that has that name: the first tab is one row per name across the cluster, saying which hosts have it, which do not, and where their VLANs or subnets disagree, which is a machine that would come up on the wrong network after a move. Port groups and oSwitches are created, edited and removed on the hosts an operator chooses, as datastores are. Virtual routers are managed per host, with their uplink, the port groups they serve, DHCP with reservations, NAT, port forwards, floating addresses, static routes and DNS, plus their leases and Restart. kSwitches, kernel adapters and physical NICs are shown read-only: changing one can move the address Alloy reaches the host on, and that keep-or-revert countdown belongs to the host's own console. There is no per-host page — every tab is the whole cluster, and the hosts appear where they decide something: the dialog that changes a port group lists the hosts that have it, what each one has it as, and the hosts that do not have it, which it can create it on. See docs/networking.md.

  • Update Management: upload a signed image pack (alloy-<VERSION>.alloyupd, what make image writes) and apply it. Alloy verifies its signature and checksums as the hosts verify theirs, hands the images to the Alloy Steel Appliance it runs on, and Steel updates Alloy's stack. Iron hosts take an .ironupd bundle: Alloy serves it on its own plain-HTTP port, each host downloads it and checks the Wise Global Solutions signature itself, and Alloy has the host verify and apply it, and restart when asked. The Alloy Steel Appliance takes a .steelupd bundle over the appliance's API token. See docs/updates.md.

  • Support bundles. Monitor › Support collects one gzip tar of Alloy's settings, its database and the tail of its own log, for Wise Global Solutions support, the way iron-support and steel-support collect theirs. Passwords, session and enrollment tokens and Alloy's signing private key are never in one. See docs/support-bundles.md.

What is still open is under "Roadmap" in docs/architecture.md. The API is described in docs/api.md, and served as OpenAPI 3.1 at /api/v1/openapi.json with Swagger UI at /api/docs. docs/README.md indexes every document.

Components

Part Directory Image Role
db db/ postgres:17.11-alpine3.24 PostgreSQL 17; the schema belongs to the API (api/migrations)
api api/ wisegs/alloy-api Node.js 22 (Fastify) API and manager on port 3000, on the Compose network only
web web/ wisegs/alloy-web nginx: the web console over HTTPS (host port 443; 9443 for the development stack), proxies /api/ to the API
deploy deploy/ - Electron wizard that imports Steel on Iron and deploys the three above onto it

Running

Requirements: Podman with podman compose (or Docker with docker compose), make and openssl. Node.js is not needed on the host.

make env        # .env from .env.example, with random database and admin passwords
make up         # build the images and start the stack; waits until it is healthy

Open https://localhost:9443/ (the certificate is self-signed; the development .env publishes 9443, because rootless Podman cannot bind 443) and log in as admin with the password in ALLOY_ADMIN_PASSWORD in .env. That password is used only on the first start, while the database has no users. Change it in the user menu afterwards.

Adding hosts

  1. Set Alloy's address. Hosts reach Alloy at ALLOY_URL, which defaults to https://ALLOY_HOSTNAME:ALLOY_HTTPS_PORT. Before enrolling real hosts, set ALLOY_HOSTNAME in .env to a name or address the hosts can reach, and run make reset up. The certificate is created for that name on the first start, and enrolled hosts pin it.
  2. Create a code. In Hosts › Add host, choose or create a cluster and create an enrollment code.
  3. Enroll the host. On the Iron host, open Host › Manage › Alloy › Enroll in the Foundry Console (or run iron-agent alloy enroll CODE from a shell), paste the code, and confirm.
  4. Check the host. It appears in Hosts when it has confirmed its enrollment. Open it to see its heartbeats, query it, or remove it.

Make targets

<VERSION> below is YY.MM.PATCH (scripts/version.sh): YY.MM is this repository's branch when it is release/YY.MM, else (dev, any other branch, a detached HEAD, no git) the year and month of the build (UTC); PATCH is 0 unless given (make image PATCH=1); VERSION=YY.MM.PATCH overrides both. It reaches compose and the images as ALLOY_VERSION. The installer's package metadata spells it as semver, without the month's leading zero; the wizard still shows YY.MM.PATCH.

A bare make builds nothing: it prints make help, every target grouped by cost with the variables that matter. The build variants are dev (the debug build, whose depots go to PREREL) and release (PRODUCTION); VARIANT defaults to dev.

Target Does
make / make help Lists the targets and variables; builds nothing
make env Creates .env (never overwrites it)
make keys Creates the development signing key set in keys/ (docs/release-keys.md)
make image Builds the api and web images, exports all three to out/images/alloy-images-<VERSION>.tar.gz for Steel, and signs out/images/alloy-<VERSION>.alloyupd. make build is the same target
make release / make debug The images and then the installer, for the release and dev (debug) variants
make depot out/depot/alloy (or DEPOT_DIR) from this build's pack, with a signed depot.xml that also lists the installer ISO when built (docs/depots.md)
make keyguard Checks that the signing key set fits VARIANT, without building
make export-images Exports that archive and pack again without building
make payload Puts the Steel OVA and the same build's update bundle, the signed pack (which holds the image archive) and the update public key in out/payload
make installer Builds, then the Linux and Windows wizards and the ISO with them and the payload: out/alloy-installer-<VERSION>.iso
make up / make down Starts / stops the stack; data is kept
make logs / make ps Follows the logs / lists the containers
make restore FILE=… Restores an Alloy backup into the stack: stops web and api, restores the database, puts the certificate and keys back, starts the stack (FORCE=1 over a database that is not empty; docs/backup.md)
make reset Stops the stack and deletes its data directory: database, certificate, image packs and scheduled backups
make test API, web console and wizard typecheck, lint and unit tests, in a Node.js container
make deploy-wizard Opens the wizard from a container, with out/payload
make test-enroll Host enrollment end to end against the running stack, with a fake host built from ../iron
make test-e2e A stack of its own against two real Iron test VMs from ../iron/out/test in one cluster: enrollment, placement, bulk power, VM (cold and live), LXC and Docker moves, evacuation and maintenance mode, templates, sealed secrets, backup and restore, and a baseline update; about nine minutes, E2E_ONLY= for some steps (docs/architecture.md "Tests")
make clean Removes node_modules and build outputs

Without make: copy .env.example to .env, set both passwords and ALLOY_DATA_DIR (an absolute path), then podman compose up -d --build (or docker compose).

Deploying to Iron

The development stack above builds its images locally and runs wherever you are. Alloy belongs on a Wise Foundry Alloy Steel Appliance, which runs as a VM on a Wise Foundry Iron host. The installer does both:

make installer    # builds the images, takes ../steel/out/steel-<version>.ova (STEEL_OVA)
                  # and the .steelupd beside it (STEEL_BUNDLE),
                  # and writes out/alloy-installer-<VERSION>.iso

Mount or burn the ISO and run linux/alloy-installer or windows/alloy-installer.exe on a desktop that can reach Iron; its first page offers Install, Upgrade and Restore. For Install it connects to Iron, asks where Steel goes (VM name, datastore, port group, OS disk of at least 64 GB, memory) and how Steel gets its address (DHCP or static), checks Iron, then uploads and imports Steel, starts it, reads its DHCP address from Iron when there is one, and deploys Alloy on it from the images it carries. No registry is needed.

Without packaging, make payload deploy-wizard runs the same wizard from a container (Podman and a desktop session; no Node.js on the host). See deploy/README.md and docs/deploy.md.

Updating Alloy

Build the new version with make image. Then, in Alloy's console, open Update Management › Alloy:

  1. Connect Alloy Steel Appliance once: its address, its certificate (compare the fingerprint with Steel's console) and a Steel API token.
  2. Upload image pack: out/images/alloy-<VERSION>.alloyupd, the signed pack.
  3. Apply update: Alloy runs its checks, then hands the pack to Steel, which recreates Alloy's containers. The console reconnects when the new version answers.

See docs/updates.md.

Developing

  • API: api/src (TypeScript, compiled to dist/). Schema changes are new files in api/migrations/ (NNN_name.sql); never edit an applied migration.
  • Deployment wizard: deploy/, an Electron app sharing the console's theme; deploy/src/main holds everything that touches the network or a secret. It runs from its own container image (deploy/Containerfile), so a host needs only Podman; with Node.js 22.12 or later, npm ci && npm start in deploy/ runs it directly.
  • Web console: web/src, the Steel console's code base. With Node.js 22.12 or later, npm install && npm run dev in web/ serves it with hot reload and proxies /api to the running stack (ALLOY_URL, default https://127.0.0.1:9443). The production CSP forbids inline styles; use CSS modules.

Layout

compose.yaml         the alloy project: includes the three parts below
.env.example         settings; `make env` creates .env
data/                the stack's data: db/, tls/, alloy/ and keys/ (not committed)
keys/                the development signing key set, `make keys` (never committed)
db/compose.yaml      PostgreSQL
api/                 alloy-api: Dockerfile, compose.yaml, src/, migrations/
web/                 alloy-web: Dockerfile, compose.yaml, nginx/, src/
deploy/              the Electron installer that deploys Steel on Iron and Alloy on Steel,
                     with the Containerfile and launcher that run it from Podman
scripts/             keys.sh and keyguard.sh (make keys), export-images.sh (make image),
                     make-payload.sh (make payload) and make-iso.sh (make installer)
out/images/          the image archive, the signed pack and their .sha256 (not committed)
out/payload/         what the installer carries: steel/, images/ and keys/ (not committed)
out/*.iso            the installer ISO and its .sha256 (not committed)
test/enroll/         enrollment end to end with Iron's Alloy client code
test/e2e/            make test-e2e: a stack of its own against two real Iron test VMs
docs/                the design documents, indexed in docs/README.md

License

Apache License 2.0 (LICENSE.md); third-party components are listed in NOTICE.md.