Skip to content

Stand up a FOG server and actually call its API - #13

Merged
darksidemilk merged 5 commits into
mainfrom
api-validation-workflow
Aug 16, 2026
Merged

Stand up a FOG server and actually call its API#13
darksidemilk merged 5 commits into
mainfrom
api-validation-workflow

Conversation

@darksidemilk

Copy link
Copy Markdown
Member

Stands up a throwaway FOG server and actually calls its REST API.

Why

The distro workflows prove installfog.sh exits 0, which is worth more than it sounds — the installer probes its own web tier (checkWebTier()) and verifies the schema deploy before returning, so a clean exit already means apache, php-fpm and a populated database are up.

What none of them do is call the API afterwards. The surface clients actually use has never been touched by CI.

What it does

Creates the container exactly the way reusable_distro_workflow.yml does — crun bootstrap and all, because that recipe is load-bearing and distrobox fails silently without it. Everything after the install is new:

  1. Seed API credentials into the fresh install
  2. Call the API with them and assert on the results
  3. Collect logs, and probe whether the runner can reach the container

It takes repo and ref as inputs rather than cloning a hardcoded dev-branch, so it can validate a pull request before it merges. That's the immediate reason it exists — FOGProject/fogproject#1042 adds a generated OpenAPI document and there's no way to check it against a running server otherwise.

Two things the seeding has to get right

users.uAPIToken is never seeded. uAllowAPI defaults to 1 and the column exists, but the fog user has no personal token until the UI generates one — so a token-authenticated call cannot succeed on a fresh install without writing one first.

globalSettings holds the API token RAW. The web UI displays base64_encode() of it (fogconfigurationpage.page.php:1924) and Route::_testToken() base64_decodes the header. So a token copied from the UI is sent verbatim, while anything reading the database has to encode it. Getting this backwards produces a 403 that looks like a permissions problem.

FOG_API_ENABLED is upserted rather than assumed: working-1.6 seeds it on, dev-branch seeds it off, and this should validate either.

What it asserts

Check Why
system/info → 200 web tier and routing are live
host with tokens → 200 token auth works end to end
host without tokens → 403 and that it is actually enforced
system/openapi and swagger.json → 200, valid JSON the document is served under both names
paths > 300, schemas > 40 the one that matters
both paths byte-identical after jq -S two names can't drift apart

That floor on document size is the point. A generated spec that resolved almost no model classes still returns 200 and still parses as JSON — a near-empty document is the realistic failure mode, not a malformed one. Counting paths is what actually proves getClass() resolved the class list on a running system.

Also here, both absent from the distro workflows

  • A timeout (60 min), so a hung install stops rather than sitting on the 360-minute default.
  • An install log artifact, so a failure leaves something to read. .fogsettings is included with password lines stripped.

The last step is a probe, not a check

The container is --unshare-all and so has its own network namespace. Whether the runner can reach it decides whether a future job can run a client from the runner or has to install one inside the container — which matters for wiring FogApi's real-server suite into this later. It reports and moves on rather than failing.

Triggers

workflow_call (future release gating), workflow_dispatch (manual runs now), and pull_request scoped to this file — so it proves itself before merging, which it otherwise couldn't: workflow_dispatch only appears once a file reaches the default branch.

Not yet verified

I have no way to execute Actions from here, so this has been checked statically only: the YAML parses, every run: block passes bash -n after de-indentation, and both heredoc'd container-side scripts parse too. The install path itself is inherited unchanged from the existing distro workflow, but the seed and validate steps have never run. The PR trigger is there precisely so its first real execution happens here, in the open, rather than after merge.

Expect roughly 10–20 minutes per run for the install.

darksidemilk and others added 5 commits August 15, 2026 18:51
The distro workflows prove installfog.sh exits 0, which is worth more than it
sounds - the installer probes its own web tier and verifies the schema deploy
before returning, so a clean exit means apache, php-fpm and a populated
database are all up. What none of them do is call the API afterwards. The
surface clients actually use has never been tested by CI.

This creates the container exactly the way reusable_distro_workflow.yml does,
crun bootstrap and all, because that recipe is load-bearing and distrobox
fails silently without it. Everything after the install is new: seed API
credentials, then use them.

It takes repo and ref as inputs rather than cloning a hardcoded dev-branch, so
it can validate a pull request before it merges. That is the immediate reason
it exists - FOGProject/fogproject#1042 adds a generated OpenAPI document and
there is no way to check it against a running server otherwise.

Two things the seeding has to get right:

  - users.uAPIToken is never seeded by the schema. uAllowAPI defaults to 1 and
    the column exists, but the fog user has no personal token until the UI
    generates one, so a token-authenticated call cannot succeed on a fresh
    install without writing one first.
  - globalSettings holds the API token RAW. The web UI displays
    base64_encode() of it and Route::_testToken() base64_decodes the header,
    so a token copied from the UI is sent verbatim while anything reading the
    database has to encode it. Getting this backwards produces a 403 that
    looks like a permissions problem.

FOG_API_ENABLED is upserted rather than assumed: working-1.6 seeds it on,
dev-branch seeds it off, and this should validate either.

The assertions worth having are the ones about document size. A generated spec
that resolved almost no model classes still returns 200 and still parses as
JSON, so path and schema counts are checked against a floor - a near-empty
document is the realistic failure mode, not a malformed one. Both
/system/openapi and /swagger.json are fetched and compared, since serving the
same handler under two names is only useful if they cannot drift.

Also here, both absent from the distro workflows: a timeout, so a hung install
stops at 60 minutes rather than the 360 minute default, and an install log
artifact, so a failure leaves something to read.

The last step is a probe rather than a check. The container is --unshare-all
and therefore has its own network namespace; whether the runner can reach it
decides if a future job can run a client from the runner or has to install one
inside the container. Worth knowing before wiring FogApi's real-server suite
into this, so it reports and moves on.

Triggers on pull_request against itself so it proves itself before merging,
which it otherwise could not - workflow_dispatch only appears once a file
reaches the default branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUJ7GnCu7w3cZMDQSWmjMu
First run got all the way through the install -- FOG was up in under five
minutes, "Setup complete" and all -- then died seeding credentials:

    bash: line 1: /tmp/seed.sh: Permission denied

distrobox shares the host /tmp, so the script staged on the runner and the
path the container writes to are the same file, and the container could not
overwrite it. Nothing is staged now; the script is piped in, and the values
come back on stdout behind a FOGENV marker, masked before they reach
GITHUB_ENV.

The probe answered its question at the same time: podman gives the container
a routable address despite --unshare-all, and the runner reached it at
10.88.0.2. So the address lookup is now a real step rather than a curiosity,
the seeding stays inside the container where the database is, and every test
runs on the runner in ordinary curl. That drops a whole layer of smuggling
scripts through distrobox.

It also makes the FogApi suite nearly free, so this adds it behind a
fogapi_ref input. GitHub's ubuntu images already ship PowerShell and the
server is reachable, so the module runs on the runner and talks to the
container -- nothing needs installing in the container at all. Two details
that matter: the settings file is written directly rather than through
Set-FogServerSettings, which refuses non-interactively when a value still
looks like placeholder text; and the installer's self-signed CA is added to
the runner's trust store, because curl can pass -k but FogApi calls
Invoke-RestMethod with no certificate override, so the trust has to be real.

Also fixes the artifact upload, which was the other failure. Globbing the
container path produced a literal '*' in a filename and upload-artifact
rejects those, so the logs are copied by explicit name. Empty files are
dropped, and the generated openapi document is saved alongside them, since
that is the artifact actually worth reading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUJ7GnCu7w3cZMDQSWmjMu
Second run got further: install, seed and address lookup all green, token auth
working end to end. That last part is worth stating plainly, because it
confirms the encoding question this workflow exists to settle - a token read
raw from globalSettings and base64 encoded into the header authenticates, and
the same request without tokens is refused 403.

The two remaining failures were the workflow's fault, not the server's.

Stock working-1.6 has no /system/openapi, so the router never allowlists the
path, asks for tokens, and answers 403 rather than 404. Asserting those
endpoints unconditionally meant this could only ever pass against a ref that
already carries the generator, which is backwards for a general validation
workflow. They are now soft by default and required only when the caller
passes expect_openapi, so validating FOGProject/fogproject#1042 is a matter of
pointing ref at that branch and asking for them.

The same-document check also passed spuriously: with both endpoints answering
403 it was comparing two identical error bodies and calling that agreement.
It now only runs when both actually returned a document, which is the only
case where it means anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUJ7GnCu7w3cZMDQSWmjMu
…1042

Reverted in the next commit. workflow_dispatch is not available until this
file reaches main, so the only way to exercise the openapi assertions before
merging either PR is to make the pull_request self-test do it.
The pin did its job. Pointed at FOGProject/fogproject#1042 with
expect_openapi on, a real install produced:

  393 paths, 53 schemas, openapi 3.1.0
  system/openapi and swagger.json both 200, both valid, byte-identical
  14 passed, 0 failed

53 schemas is the number that mattered. It means getClass() resolved every
model class on a running server, which was the one thing the generator could
not be checked for without one - a document that resolved almost nothing
would still have returned 200 and still parsed.

The live run also caught something no fixture could: the sensitive-field
tiering picked up ADPassLegacy alongside ADPass, productKey, pub_key and
sec_tok, because it reads Route::sensitiveFieldMap() at request time rather
than a list written into the generator.

Defaults are back to working-1.6 with the openapi assertions soft. Once this
is on main, the same run is a workflow_dispatch with ref and expect_openapi
set, no edit required.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUJ7GnCu7w3cZMDQSWmjMu
@darksidemilk
darksidemilk merged commit 349f106 into main Aug 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant