Feature Specification: Component windmill
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/components/hosting-provider/windmill/default.nix, core/components/hosting-provider/windmill/windmill.nix, core/components/hosting-provider/windmill/package.nix, core/components/hosting-provider/windmill/workspace-imports-test.nix, core/components/hosting-provider/windmill/f/fediversity/, core/components/hosting-provider/windmill-otel.nix, core/components/hosting-provider/otel-wiring-test.nix, core/resources/oidc/forward-auth-domain.nix, core/checks/nix-unit-tests.nix, core/checks/default.nix), pull requests #1622 and #1691.
User Scenarios & Testing (mandatory)
User Story 1 - Run operator deploys as jobs (Priority: P1)
A front-end submits an operator deployment as a job. The windmill
component runs a Windmill server and its workers on a node of the
hosting-provider group. A bootstrap oneshot uploads the scripts
f/fediversity/deploy and f/fediversity/destroy. A caller names one of
these scripts. It does not supply code. The script runs the deploy program
of 004 with
nix-shell -A devShells.<system>.<attribute> --run <executable>. The
component sets nixStore = "host-daemon", so the builds land in the store
of the hypervisor.
Why this priority: operator deploys from a front-end run only through this node.
Independent Test: nix-unit-components-windmill-workspace-imports
holds that the bootstrap uploads each workspace import, that the runnable
scripts define only main, and that deploy and destroy share one job
environment. nix-unit-setups-windmill-config checks the realised node.
Acceptance Scenarios:
- Given a fresh node, When
fediversity-windmill-bootstrap.serviceruns, Then it logs in asadmin@windmill.dev, mints an API token labeledfediversity, creates the workspacefediversityand the folderfediversity, uploads the scripts and setsinstance_python_versionto3.12. - Given a script that the instance already holds with the same content, When the bootstrap runs again, Then it skips the upload. A changed script gets a new version with
parent_hash. - Given a deploy job, When
deploy.pybuilds the environment, Then the keys from/etc/fediversity-windmill/deploy-env.jsontake priority over the keys of the payload, and the payload keys take priority overos.environ. - Given the API name
cfg.domain, When a client requests/, Then nginx returns404. Only/api/reaches127.0.0.1:8001.
User Story 2 - Give the deploy its credentials (Priority: P1)
A deploy job needs credentials for OpenBao, NetBox, DNS, the registrar and the Incus store. The front-end does not send them. The node reads them for itself.
Why this priority: without the credentials, a deploy cannot allocate addresses, publish records or reach the hypervisor.
Independent Test: nix-unit-setups-windmill-config holds that the
node reads each credential from its own OpenBao render files.
Acceptance Scenarios:
- Given the node has
services.vars-openbao, When it renders, Then each entry ofdeploySecretsbecomes one file under/run/fediversity-windmill/deploy-secretswith ownerroot:windmilland mode0440. - Given the node, When
openbao-agentrenders, Then/run/incus-deploy/tf-token.envholdsTF_VAR_tokenfromincus-deploy/<hostName>/tokenHosts, with mode0440. - Given the node, When
openbao-agentrenders, Then/run/incus-deploy/incus-store-ssh.keyand/run/incus-deploy/incus-store-signing.keyhold the Incus store keys. - Given a minted API token, When the bootstrap finishes, Then it runs
bao kv puton<prefix>/secret/windmill/apiToken.
User Story 3 - Read job logs in the browser (Priority: P2)
An administrator wants to follow a deploy. Windmill CE cannot do SSO. The
component puts an Authelia forward-auth vhost on uiDomain in front of
127.0.0.1:8001. The vhost presents the API token of Windmill on behalf of
the user.
Why this priority: the deploys run without the UI. The UI helps an administrator to find failures.
Independent Test: nix-unit-setups-windmill-config holds that the
vhost sends a browser without a session to the portal and passes the
job-log websocket.
Acceptance Scenarios:
- Given the policy sets
deployAdminUIs, When theoidcresource applieswindmill-ui, Then the vhost asks forone_factorand admits only the groupfediversity-admins. - Given a minted token, When the bootstrap writes
/run/windmill-ui-auth/token.conf, Then nginx includes it through the glob/run/windmill-ui-auth/*.conf.
User Story 4 - Point Windmill at the collector (Priority: P3)
The component requests otel with windmill-otel.nix. This sets the OTLP
variables on windmill-server, windmill-worker and
windmill-worker-native.
Why this priority: no signal leaves the process. The OSS build of Windmill has stub exporters.
Independent Test: test-otel-wiring boots a node with the collector,
windmill.nix and windmill-otel.nix, and checks the OTel variables on
the units.
Acceptance Scenarios:
- Given an
otelendpoint, When the node renders, Then each unit getsOTEL_EXPORTER_OTLP_ENDPOINT,OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf,OTEL_SERVICE_NAME=windmill-<role>andOTEL_RESOURCE_ATTRIBUTES=service.namespace=fediversity.
Edge Cases
POST /api/users/setpasswordof Windmill CE returns500. The bootstrap writes the argon2 hash of the superadmin password into thepasswordtable over the database DSN withSET ROLE windmill_admin.- The bootstrap has
Restart=on-failure,RestartSec=30sandStartLimitIntervalSec=0. It retries the login 12 times. - A new render of
windmill-superadmin/adminPasswordrestarts the bootstrap throughreloadUnits. - The database request uses rotating credentials, not dynamic ones. Dynamic credentials restart the units at each SVID rotation.
- The server and both workers have
RestartSec=5sand start afteropenbao-agent-default. - On a standalone eval without
services.vars-openbao,deploySecretsis empty.
Requirements (mandatory)
Functional Requirements
- FR-001: The component MUST declare
components.windmillwithnixStore = "host-daemon",certAudience = "bootstrap"and the default subdomainwindmill. - FR-002: The component MUST declare the options
operatorDns(serverUrl,providerwith defaultpowerdns),operatorNetbox.rangeSelector(kind,value),operatorRegistrar(maxCostin EUR cents,ownerHandle,baseUrl,nameservers) anduiDomain(defaultjobs.<apex>). - FR-003: The implementation MUST request
spiffe,sslforcfg.domain,netboxanddnswithconsumerName = "windmill", andotelfromwindmill-otel.nix. - FR-004: The
ipamrequest MUST write the operator curation toservices.fediversity-windmill.deployEnv.HOSTING_CONFIG, and setdeploySecretsfromdeploy-credentials.nixonly whenoptions.services ? vars-openbao. - FR-005: The
windmill-dbrequest MUST askpostgresqlfor database and userwindmillwith rotating credentials, and declare ingroupRolesthe group roleswindmill_user, with all privileges on the database, andwindmill_admin, withBYPASSRLS, a member ofwindmill_userand granted to the user. - FR-006: The
windmill-dbrequest MUST contain only data. Thecodemodule MUST render its client (database.nix) fromcontracts.postgresql.results.windmill.default, MUST render nothing when the result is absent, and when the result is present MUST setservices.windmill.database.createLocally = falseandservices.windmill.database.urlPathto the resulturlFile. - FR-007: The
windmill-uirequest MUST setdomain = cfg.uiDomain,upstream = "127.0.0.1:8001",authorizationPolicy = "one_factor",allowedGroups = [ "fediversity-admins" ],proxyWebsockets = trueandupstreamAuthorizationInclude = "/run/windmill-ui-auth/*.conf". - FR-008: The component MUST generate
windmill-superadmin/adminPasswordwithopenssl rand -hex 32and declarewindmillas a written instance ofapi-token. - FR-009: The
windmill-smtprequest MUST setSMTP_HOST,SMTP_PORT,SMTP_USERNAME,SMTP_FROM=noreply@<domain>andSMTP_TLS_IMPLICIT=falseonwindmill-server, and passSMTP_PASSWORDthroughcontracts.envSecrets. - FR-010: The
coderequest MUST importwindmill.nix, setservices.windmill.baseUrl = "https://<uiDomain>", and setdeployEnv.DEPLOYMENT_ROOTto the git-tracked repository tree anddeployEnv.DEPLOYMENT_NIXPKGStopkgs.path. - FR-011:
windmill.nixMUST usepackage.nix, which keeps only the Python and Bash toolchains of Windmill. - FR-012:
windmill.nixMUST make the userwindmilla trusted Nix user, pin the registry entrynixpkgstoDEPLOYMENT_NIXPKGS, turn off the global registry and enable flakes. - FR-013:
windmill.nixMUST givewindmill-workernix,openssh,gitandbashon itsPATH, andKillMode=mixed. - FR-014: The deploy script MUST read
DEPLOYMENT_ROOT,ATTRIBUTE,EXECUTABLE,SYSTEM,NIXPKGS,SSH_PRIVATE_KEY_FILE,INTERNAL_HOSTS,PUBLISHED_HOSTS,ENVIRONMENTSandTF_HTTP_ADDRESSfrom its job environment. - FR-015: The implementation MUST request
garagethroughmkBackupsGarageRequestwith the bucketwindmill-backups, and thecodemodule MUST add astreamingBackupof thewindmill-dbdatabase to that bucket with the restic provider, as the userroot.
Key Entities
- Workspace
fediversity: holds the folderf/fediversitywith_runner,_jobenv,deployanddestroy.coreholds_runner.pyand gives it toapiascore.lib.components.hosting-provider.windmill-runner. - API token: minted on each boot, stored in OpenBao KV and in the UI auth snippet.
- Deploy environment:
/etc/fediversity-windmill/deploy-env.json, fromdeployEnv. - Deploy secrets: one file per variable under
/run/fediversity-windmill/deploy-secrets.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
nix-unit-components-windmill-workspace-importsandnix-unit-setups-windmill-configpass. - SC-002:
nix-unit-components-incus-store-identitypasses, so_jobenv.pyagrees withincus-store-identity.nix. - SC-003:
test-otel-wiringpasses. - SC-004: On a live node,
fediversity-windmill-bootstrap.serviceisactive, and<prefix>/secret/windmill/apiTokenholds the current token.
Assumptions
- The
postgresql,smtp,netboxanddnsresources resolve on the node (seepostgresql). core/render.nixsets theoperator*options.- OpenBao and
openbao-agentrun on the node.
Known Deviations
windmill-otel.nixsets OTLP variables, but the OSS exporters of Windmill are stubs. No signal reaches the collector.