Stand up a FOG server and actually call its API - #13
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stands up a throwaway FOG server and actually calls its REST API.
Why
The distro workflows prove
installfog.shexits 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.ymldoes — crun bootstrap and all, because that recipe is load-bearing and distrobox fails silently without it. Everything after the install is new:It takes
repoandrefas inputs rather than cloning a hardcodeddev-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.uAPITokenis never seeded.uAllowAPIdefaults to1and the column exists, but thefoguser has no personal token until the UI generates one — so a token-authenticated call cannot succeed on a fresh install without writing one first.globalSettingsholds the API token RAW. The web UI displaysbase64_encode()of it (fogconfigurationpage.page.php:1924) andRoute::_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_ENABLEDis upserted rather than assumed:working-1.6seeds it on,dev-branchseeds it off, and this should validate either.What it asserts
system/info→ 200hostwith tokens → 200hostwithout tokens → 403system/openapiandswagger.json→ 200, valid JSONjq -SThat 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
.fogsettingsis included with password lines stripped.The last step is a probe, not a check
The container is
--unshare-alland 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), andpull_requestscoped to this file — so it proves itself before merging, which it otherwise couldn't:workflow_dispatchonly 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 passesbash -nafter 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.