Skip to content

Fix/laptop install first boot - #4352

Closed
imnasnainaec wants to merge 16 commits into
masterfrom
fix/laptop-install-first-boot
Closed

Fix/laptop install first boot#4352
imnasnainaec wants to merge 16 commits into
masterfrom
fix/laptop-install-first-boot

Conversation

@imnasnainaec

@imnasnainaec imnasnainaec commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

imnasnainaec and others added 2 commits August 19, 2026 14:58
The database gained a postStart hook that initializes the rs0 replica set
and imports the semantic domains, so it is now the slowest deployment to
come up. Nothing in the install path waited for it, and the installer then
stopped k3s, which SIGKILLs pods because the k3s unit is patched to
KillMode=mixed.

wait-for-combine only ever read one deployment's READY count. Collapsing
the kubectl table with an unquoted expansion put every row on one line, so
grep matched the whole line for each name and the greedy sed captured the
last N/1 in it -- the alphabetically last deployment, maintenance. Replace
the parsing with kubectl wait, and check the database first.

Guard the semantic domain import with a completion record instead of a
document count. A count cannot tell a finished import from one killed part
way through, so an interrupted import was never redone and the domains
stayed incomplete. The installer now also waits for that record before
shutting the cluster down.

Add a readiness probe so the database stays out of its Service endpoints
until the replica set advertises the current pod IP, which changes on every
restart and which the backend needs since it connects with ?replicaSet=rs0.

Drain the deployments in combinectl stop, restoring their replica counts in
combinectl start, so pods terminate instead of being killed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nginx resolves the hostnames in its proxy_pass directives when it loads its
configuration and exits if any of them cannot be resolved:

  [emerg] host not found in upstream "backend" in
  /etc/nginx/conf.d/default.conf:48

On a cold start the frontend container can be running before the cluster DNS
can answer for the backend service, which leaves the pod crash looping until
the kubelet's restart backoff happens to line up with DNS being ready.

Wait for the name in a docker-entrypoint.d script, and start nginx anyway
after two minutes so that a real misconfiguration is still reported rather
than leaving a container that runs but never serves.

Add a readiness probe to the frontend, since with the wait in place a running
container is no longer necessarily a serving one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@imnasnainaec imnasnainaec self-assigned this Aug 19, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation deployment bash labels Aug 19, 2026
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 27b2df19-3a39-4986-b339-4f4dd20fda77

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.96%. Comparing base (b097bf0) to head (5597551).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #4352   +/-   ##
=======================================
  Coverage   75.96%   75.96%           
=======================================
  Files         305      305           
  Lines       11384    11384           
  Branches     1411     1411           
=======================================
  Hits         8648     8648           
  Misses       2332     2332           
  Partials      404      404           
Flag Coverage Δ
backend 87.22% <ø> (ø)
frontend 66.88% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

imnasnainaec and others added 14 commits August 19, 2026 15:22
Every other step that records a follow-on state checks SINGLE_STEP before
continuing, but Wait-for-combine fell straight through into
Shutdown-combine, so single-step could not be used to inspect a running
cluster before the installer stopped it.

Setting STATE directly rather than calling next-state leaves the recorded
state in place, so the next run resumes at Shutdown-combine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"The Combine is Running." was printed whenever the k3s unit was active,
which covers several states that are not running. Distinguish them:

  - the k3s unit is active but the Kubernetes API is not serving yet
  - the cluster is up but the namespace has no deployments
  - some of The Combine's deployments are missing
  - the deployments exist but are scaled down
  - the deployments are scaled up but not all available yet

The scaled down case is the one combinectl stop now creates, and it would
otherwise have been reported as running with every deployment at 0/0.

Restoring the replica counts also no longer depends on the saved counts
being present. k3s can be started without combinectl -- directly with
systemctl, or by the unit still being enabled on a server install -- which
would otherwise leave the deployments scaled to zero with nothing to bring
them back, so fall back to one replica each.

Guard the wait for the pods to exit with a check that there are pods to wait
for; a selector-based "kubectl wait" fails immediately when nothing matches
it, which reported a clean stop as a two-minute timeout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"kubectl wait" returns at once, rather than blocking for its timeout, when
the API server is unreachable or the deployment is gone, so its retry loop
had nothing but an echo in it and could spin, pegging a core and flooding
the terminal. Pace the retries.

Neither wait had an upper bound either, so a deployment that was never
going to come up produced no error, ever. Give each stage a deadline,
default one hour and overridable with WAIT_TIMEOUT_SECONDS, and print the
pods in the namespace before exiting so there is somewhere to start looking.
Rerunning the installer resumes the wait.

Check the semantic domain import every 30 seconds rather than every 10;
each check starts a mongosh inside the database container, competing with
the import it is waiting on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The completion record was written by the postStart hook, so the documented
manual rerun of update-semantic-domains.sh left it untouched and the next
pod start redid the whole import. Write it from the script instead, after
both imports succeed, so both paths agree.

Also trim /data/db/postStart.log to the most recent starts. It is appended
to on every container start and lives on the database's persistent volume,
with nothing rotating it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
start-combine-deployments replayed the saved counts as they were, so a
count for a deployment that no longer exists, for example one renamed by
a chart update, failed to scale on every start.  That failure is what
keeps the file, so the stale entry, and any deployment the file omits,
were retried forever.

Take the deployments that are scaled down from the cluster and the counts
from the file, falling back to one replica for a deployment the file does
not mention, and discard the file once nothing is scaled down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
combine-deployments discards kubectl's errors, so a cluster that is not
serving the API yet looks exactly like an empty namespace.
stop-combine-deployments read that as "nothing to stop" and returned
without saving any replica counts or scaling anything down, and
combine-stop went straight on to stop k3s, which with KillMode=mixed
SIGKILLs the containers.  A stop issued while The Combine was still
coming up therefore killed the database part way through its startup
work, which is what forces the semantic domain import to be redone.

Wait for the API in the stop path as the start path already does, and
leave k3s running when the deployments could not be stopped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A deployment scaled to zero has every replica it asked for, so it did not
land in the pending list, and it is in the namespace, so it was not
missing either.  With the other deployments still asking for replicas the
total was non-zero, and the status came out as "The Combine is Running"
even though one of its services was switched off.

Count the deployments that ask for no replicas separately and name them,
so that a service left behind by a partial start is visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One deadline covers the wait for all four deployments, so "Timed out
after 3600s waiting for deployment/maintenance" claimed a wait that had
mostly elapsed on the database.  Say what the wait was still waiting for
instead, and describe the timeout in the header comment as the budget for
the deployments and then for the import, rather than one per deployment.

The semantic domain import runs from the database pod's postStart hook,
so rerunning the installer does not start an import that never ran; give
that wait a hint that names the manual import instead of the default
advice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The readiness marker was written before the semantic domain import so
that the probe could pass while the import ran, but the kubelet does not
probe a container until its postStart hook returns, and the import runs
inside that hook.  The pod could not join the database Service any
sooner, so write the marker last, where it also means that everything
before it succeeded.

Report a failed import in the postStart log and fail the hook explicitly,
so the container restarts and retries: The Combine cannot be used without
the semantic domains, and a database that serves without them looks
healthy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
combinectl start returned the status of an echo when the cluster never
became ready, so it exited 0 after starting nothing, and a deployment
that could not be scaled up was reported the same way. Return non-zero
from both, as stop-combine-deployments already does, and make the other
exits explicit.

Correct what the comments and docs claim:

- The database is not available until its postStart hook has imported the
  semantic domains, so the import elapses inside the wait for the
  deployments, not inside the wait that follows it. That second wait is a
  guard for a database pod that an install left running, which therefore
  never ran the hook.
- The completion record outlives the pod that wrote it, so a release that
  ships updated semantic domain data needs the manual import too.
- The postStart log is trimmed to 200 lines rather than to whole entries,
  so its oldest entry can begin part way through.
- A new cluster-internal nginx upstream has to be added to the DNS wait,
  which only knows about "backend".

Comments now use one space after a period rather than two, and some
blocks are rewrapped to the width of the file around them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The postStart hook captured mongosh's output and imported only when it
was exactly "needed", so a stray line on stdout left the pod ready with
no semantic domains, and a failed query aborted the hook through set -e
with nothing in the log to say why.

Branch on mongosh's exit status instead, as the installer already does:
anything short of a completed import runs the import, which is a merge
and safe to repeat, and only stdout is discarded so an error still
reaches the postStart log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wait-for-semantic-domains had the same hour-long budget as the wait for
the deployments, but readiness is gated on the same record, so it either
passes on its first check or is in the case where no pod will ever write
the record. That spent an hour polling before printing the manual-import
hint that is the actual remedy; give it a short budget of its own.
check-wait-deadline now reports the budget of the wait that set it, so
the two cannot drift.

WAIT_TIMEOUT_SECONDS went into arithmetic unchecked, where bash reads a
non-number as 0 and a leading zero as octal: "abc" became a deadline that
had already passed, "007" became seven seconds, and "3600s" aborted with
a bare arithmetic error. Require a whole number greater than zero, as the
timeout option already does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
stop-combine-deployments claimed to return non-zero whenever the
deployments were not stopped, but a pod still terminating after two
minutes warns and returns 0 so that one stuck pod cannot leave The
Combine with no way to stop. Say what each path does.

The database is not ready until its postStart hook has imported the
semantic domains, which has two consequences worth writing down: a
timeout given to the installer makes helm wait for that import, so it
has to be long enough to cover it; and an installation that imported
before the completion record existed has none, so its first pod start
imports once more and stays out of the database Service until it is done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WAIT_TIMEOUT_SECONDS was checked at the top of the script, so a bad value
left in the environment stopped an uninstall, which never waits for
anything. Check it with the other install-only setup, where the state is
known, as the version number already is.

Also note that the support software the installer's update option skips
includes combinectl itself, so a change to that tool arrives with a full
installation and not with an update.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@imnasnainaec

Copy link
Copy Markdown
Collaborator Author

Splitting this PR: four stand-alone PRs, no conflicts

I built the split locally and tested it rather than eyeballing it: all 6 pairwise merges are clean, four-way
sequential merges are clean in every order I tried, and the union of the four reproduces this branch's tree exactly
(the only difference was a git apply-on-Windows file-mode artifact on the new script).

Everything is file-disjoint except two small overlaps, in deploy/scripts/install-combine.sh and
installer/README.md. Both were tested on their own: the regions are ~30 lines and one table row apart, and neither
conflicts.

1. Wait for cluster DNS before starting nginx

nginx/init/05-wait-for-backend-dns.sh (new), nginx/templates/default.conf.template,
deploy/helm/thecombine/charts/frontend/templates/deployment-frontend.yaml

From c67b43e3 plus the nginx-template hunk of fcb60fe4. Shares no file and no behavior with the rest, and
Dockerfile:66 already copies nginx/init/* into /docker-entrypoint.d/.

QA & prod: affected. Both changes live in the frontend image and the shared frontend chart, with no target
conditionals, so they ship to qa-kube.thecombine.app and thecombine.app as well. On a server whose cluster DNS is
already up, the script's first getent succeeds and it exits immediately, and the two-minute cap keeps it from ever
hanging a deploy. The new readiness probe changes when the frontend joins its Service endpoints on every target. All
three proxy_pass directives in the template currently point at backend, so the wait covers every cluster-internal
upstream as it stands, which is what the added comment is there to protect.

2. Database first boot: import record, postStart hook, readiness probe

database/init/update-semantic-domains.sh, deploy/helm/thecombine/charts/database/templates/database.yaml,
docs/deploy/README.md

From the database parts of 3360ecd4, plus e34efeb8, d5b578e7, 75ff4485, and parts of fcb60fe4 / e2fe421f.

QA & prod: affected, and this is the one to review with prod in mind. The chart template has no target
conditionals and updateStrategy is Recreate on every profile, so on the first upgrade to this chart the database
pod is replaced and then held out of the database Service by the new readiness probe until its postStart hook
finishes. An existing QA or prod installation has no completion record, so that hook imports the semantic domains once
more: several minutes with no database endpoint. The documented manual rerun,
kubectl -n thecombine exec deployment/database -- /opt/thecombine/update-semantic-domains.sh, now also writes the
completion record, and the notes being edited in docs/deploy/README.md are the QA/prod deployment docs.
setup_combine.py as documented for QA/prod is invoked without --wait, so helm itself does not block on the import
there.

3. combinectl start/stop/status

deploy/ansible/roles/support_tools/files/combinectl.sh, the Shutdown-combine hunk of
deploy/scripts/install-combine.sh, the update row of installer/README.md

From the combinectl parts of 3360ecd4, plus dbda435d, dc93c0a2, 64a94050, f6538023, and parts of
fcb60fe4 / e2fe421f / 55975511.

QA & prod: no effect on the server clusters. The support_tools role that installs combinectl is imported only
by playbook_desktop_setup.yml (hosts: localhost) and playbook_nuc_setup.yml (hosts: nuc), never for the
server or qa groups. It does reach the NUCs, including the QA NUC and nuc1-3, so it is not purely a laptop change.

4. Installer waits

deploy/scripts/install-combine.sh, the timeout row of installer/README.md

From the wait rewrite in 3360ecd4, plus 21e967ee, 368848b0, 5c53fc71, 29ccf687, 55975511, and parts of
fcb60fe4.

QA & prod: none. install-combine.sh ships in combine-installer.run; QA and prod are deployed with
setup_combine.py per docs/deploy/README.md. The installer's own server option is the only server-shaped path it
touches.

Ordering (functional, not textual)

  1. 4 needs 2 first. wait-for-semantic-domains polls CombineDatabase.SemanticDomainImportStatus, and nothing on
    master writes that record, so on its own 4 fails a first install after IMPORT_CHECK_TIMEOUT_SECONDS (120s) with
    the manual-import hint.
  2. 2 changes what the installer's helm --wait covers. The readiness probe keeps the database unready until the
    import finishes, so install-the-combine can exceed the default five-minute --timeout. The note documenting that
    is the installer/README.md timeout row, grouped above with 4; move it to 2 if 2 lands first. Tested: the two
    table rows split across separate PRs without conflict.
  3. The combinectl stop || error hunk has to travel with 3, not 4. On master, combine-stop ends on
    if systemctl is-active --quiet create_ap, so it exits 1 whenever the hotspot is not running, and that caller
    landing alone would abort the installer at Shutdown-combine.

So 1 and 3 can go in any order at any time; 2 before 4.

Cost of splitting

3360ecd4 and fcb60fe4 each straddle all four groups, so this cannot be done by cherry-picking commits. The four
branches have to be re-authored from the final state, which is how I built and tested them.

@imnasnainaec

Copy link
Copy Markdown
Collaborator Author

Superseded by four stand-alone PRs, each verified to merge without conflict in any order:

Split rationale and verification: #4352 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bash deployment documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant