Skip to content

fix(doctor): stop creating files and database during diagnostics - #3646

Merged
kolaente merged 11 commits into
mainfrom
doctor-pwd-assumption
Aug 29, 2026
Merged

fix(doctor): stop creating files and database during diagnostics#3646
kolaente merged 11 commits into
mainfrom
doctor-pwd-assumption

Conversation

@tink-bot

@tink-bot tink-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

vikunja doctor created a files directory and a SQLite database in the working directory, then reported them as existing, writable, and connected — passing checks against storage it had just made itself.

Reported at https://community.vikunja.io/t/vikunja-doctor-assumes-pwd-is-vikunja-files-basepath/4778

Root cause: doctor called files.InitFileHandler(), which creates the base path, and db.CreateDBEngine(), which creates the SQLite file plus WAL and SHM sidecars. Both are split into create vs verify, with doctor using verify-only paths. Server and restore behavior unchanged, verified against a binary built from the merge base.

Also covers the same class of bug elsewhere in doctor: database.path: memory created and leaked a temp WAL database per run, ResolvedDatabasePath created the user data directory, and a basepath pointing at a regular file passed four checks before failing.

service.rootpath now appears in the diagnostic output, since every relative config path resolves against it and it defaults to the working directory.

The --config flag that was originally part of this PR now lives in #3652.

How to verify

  1. Create an empty directory outside your Vikunja install and change into it: mkdir /tmp/somewhere && cd /tmp/somewhere
  2. Run VIKUNJA_SERVICE_PUBLICURL=http://x.test/ vikunja doctor
  3. Expected: both the Files and Database groups report failures, ✗ Directory exists: ... no such file or directory and ✗ Database file: ... no such file or directory. The directory is still empty afterwards; check with ls -A, which must print nothing.
  4. Before this PR: the same run printed "All checks passed" and left behind files/, vikunja.db, vikunja.db-shm and vikunja.db-wal.

Then confirm the ephemeral database is no longer materialised by a diagnostic:

  1. Point a config at an in-memory database with database: {type: sqlite, path: memory} and run vikunja doctor.
  2. Expected: the Database group shows ✓ Database file: memory (ephemeral, nothing to verify) and no Connection row, and no new /tmp/vikunja-* directory appears.
  3. Run vikunja migrate with that same config. Expected: it still logs Using ephemeral SQLite database at: /tmp/vikunja-.../vikunja.db and migrates successfully, because only doctor abstains.

Finally, confirm the real server and restore paths still create storage:

  1. Delete the files directory from a test install, then start vikunja web.
  2. Expected: files/ is recreated and the API serves /api/v1/info.
  3. Repeat with vikunja restore against a dump. Expected: files/ is recreated there too.

@github-actions github-actions Bot added area/config config.yml, env vars, runtime flags, deployment config area/database Database engine behavior, schema issues, cross-engine DB bugs labels Aug 28, 2026
@tink-bot tink-bot added the pr-swarm/started PR Swarm run is currently active on this PR label Aug 28, 2026
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

Preview Deployment

Preview deployments for this PR are available at:

URL Tag Commit
https://pr-3646.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:pr-3646 latest
https://sha-ff88e098470ab95c15bbad81a268e0417546bf47.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-ff88e098470ab95c15bbad81a268e0417546bf47 ff88e09
https://sha-23954c7b251eddedc66673f65f76b7c40cd2819b.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-23954c7b251eddedc66673f65f76b7c40cd2819b 23954c7
https://sha-cff515a3d9d4a6dbbbd6ca637f10d07d4bc32ef3.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-cff515a3d9d4a6dbbbd6ca637f10d07d4bc32ef3 cff515a
https://sha-0064730c76522616cf17efb34f1fb468a71cfdf4.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-0064730c76522616cf17efb34f1fb468a71cfdf4 0064730
https://sha-887f9b1031041286d933d4fd12fd2bf36fa9736a.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-887f9b1031041286d933d4fd12fd2bf36fa9736a 887f9b1
https://sha-33a8939a715bb6d4d03674f3acaf0ed776c91740.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-33a8939a715bb6d4d03674f3acaf0ed776c91740 33a8939
https://sha-1b60e9a2b9721175ef5d976b4c7bbcc65383885e.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-1b60e9a2b9721175ef5d976b4c7bbcc65383885e 1b60e9a
https://sha-999a9fbbb2e45e6e709237e929c7582cc5ff10c6.preview.vikunja.dev ghcr.io/go-vikunja/vikunja:sha-999a9fbbb2e45e6e709237e929c7582cc5ff10c6 999a9fb

The preview environment will start automatically on first visit. Subsequent pushes to this PR will update the pr-3646 image — the preview picks up the new version on restart. The per-commit URLs point to a specific version and will not change.

Run locally with Docker
docker pull ghcr.io/go-vikunja/vikunja:pr-3646
docker run -p 3456:3456 ghcr.io/go-vikunja/vikunja:pr-3646

Last updated for commit ff88e09

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 PR Swarm — automated review, round 1. Panel: bughunter · security · conventions · design · verifier. See inline comments.

Comment thread pkg/cmd/cmd.go
Comment thread pkg/cmd/cmd.go
Comment thread pkg/db/db.go
Comment thread pkg/doctor/database.go
Comment thread pkg/config/config.go Outdated
Comment thread pkg/doctor/files.go
Comment thread pkg/files/filehandling_init_test.go
Comment thread pkg/files/filehandling.go
@tink-bot

tink-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 PR Swarm — automated review. Panel: bughunter · security · conventions · design · verifier — 3 rounds, final @ 23954c7 (PR +597/−114)

Important

This summary was rewritten after the PR was restructured. The --config flag was split out into #3652 (now merged), and the branch was rebased twice, orphaning every commit link the earlier rounds posted. Inline comments have been relinked; six of them describe work that now lives in #3652 and are marked as such.

Verdict: ✅ APPROVE — 17 fixed across 3 rounds, 5 deferred (all now have follow-ups)

Scope of this PR after the split

Doctor no longer creates the storage it reports on. pkg/files, pkg/doctor, pkg/db, plus a test-isolation fix in pkg/config.

✅ Fixed here (11)

  • pkg/db/db.goResolvedDatabasePath created the user data dir via getUserDataDir41bae1d
  • pkg/doctor/database.gopath: memory leaked a temp WAL database per run and reported Connection: OK against it — 37b51e0
  • pkg/files/filehandling.goInitFileHandlerReadOnlyInitStorageBackend; it mutates globals, nothing read-only — 041645d
  • pkg/doctor/files.go — a non-directory basepath passed four checks before failing — 1b075ef
  • tests — four assertions that passed with the code under test deleted — d8fbef2, 6505f4e
  • pkg/db/db.goround-1 regression: doctor and the server disagreed on the path, failing a working install — 0130977
  • pkg/db/db.go — deleted the now-callerless getUserDataDir2dcbe41
  • pkg/doctor/database.godatabase.path at a directory passed the file check — b058805
  • pkg/doctor/files_test.go — a flake fix swallowed genuinely failing ownership checks — cb668a0
  • pkg/doctor/database_test.go — first tests for the database half; mutation-verified — a4c953b
  • pkg/db/db.goround-2 regressions: blanket MkdirAll silently created operator-configured trees at 0700, and killed the rootpath fallback for an unwritable HOMEc71ee82
  • pkg/doctor/config.go — relative config path printed beside absolute ones — e25cdcc

🔴 CI failure introduced and fixed after the review

The review rounds missed this; CI caught it. TestRestore failed on every test-api job with open .../pkg/db: is a directory.

Capturing a config value before init returns "", and config.Key.Set writes at viper's override level, which outranks defaults — so restoring pinned database.path to "" process-wide. Invisible locally because CreateTestEngine only reads the config when VIKUNJA_TESTS_USE_CONFIG=1, which CI sets for every database except sqlite-in-memory — the one job that passed. Local -shuffle runs took the branch that cannot see it.

  • config.ResetForTests()viper.Reset() + InitDefaultConfig()0ee8002
  • applied across pkg/db, pkg/doctor, pkg/filese74b33b
  • pkg/files/s3_test.gopre-existing order-dependence: TestInitFileHandler_* replaced the storage global and never restored it — 23954c7

❓ Deferred — all now have follow-ups

Item Outcome
--config scope creep #3652, merged
rootpath-relative defaults baked pre-config-read #3656 (draft, breaking)
doctor hangs on unreachable S3 #3654 (stacked on this PR)
docs for --config go-vikunja/website#393 (draft) + config-raw.json in #3652
FileStorage.Ensure() refactor scoped, not opened — awaiting a call

🔎 Runtime verification

Every round compared against a binary built from the merge base, so pre-existing behavior was never misreported as a regression.

  • ✅ original bug: base printed "All checks passed" and left 4 files behind; now the directory stays byte-for-byte empty
  • ✅ composes with feat(cmd): add --config flag to pin the config file #3652: --config anchors to the install, doctor reports those paths and creates nothing, cwd stays empty
  • ✅ server + restore still recreate files/; migrate still creates the data dir and the ephemeral database
  • ✅ absolute/relative database.path with a missing parent fails loudly and creates nothing, as base
  • ✅ unwritable HOME warns and falls back to rootpath, as base
  • ✅ exit codes, idempotency, no probe residue, -count=2 -shuffle=on clean with and without VIKUNJA_TESTS_USE_CONFIG=1
Round history

round 1 — 8 fixed, 2 nit-resolved, 5 deferred. Included two verified regressions in the --config flag (now in #3652): a -c shorthand collision that panicked user delete, and the flag being silently ignored across the migrate subtree.

round 2 — all 8 round-1 fixes held under re-verification; 6 more fixed, including a regression round 1 itself introduced.

round 3 — audited round 2's fixes; found two regressions it introduced plus one gap, all fixed.

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 PR Swarm — automated review, round 2. Re-reviewed round 1's own fixes. All 8 held under re-verification; 6 new items found and fixed, including one regression round 1 introduced. See inline comments.

Comment thread pkg/db/db.go Outdated
Comment thread pkg/config/config.go
Comment thread pkg/doctor/database.go
Comment thread pkg/doctor/files_test.go
Comment thread pkg/doctor/database_test.go
Comment thread pkg/db/db.go
@tink-bot
tink-bot force-pushed the doctor-pwd-assumption branch from 33a8939 to 887f9b1 Compare August 28, 2026 22:22

@tink-bot tink-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 PR Swarm — automated review, round 3 (final). Audited round 2's fixes and found two regressions it introduced, plus one gap in the new flag. All fixed. Round cap reached.

Comment thread pkg/db/db.go
Comment thread pkg/config/config.go
Comment thread pkg/doctor/config.go
@tink-bot tink-bot added pr-swarm/needs-input PR Swarm finished, deferred items are waiting on maintainer input and removed pr-swarm/started PR Swarm run is currently active on this PR labels Aug 28, 2026
@tink-bot
tink-bot force-pushed the doctor-pwd-assumption branch from 887f9b1 to 0064730 Compare August 29, 2026 14:44
@tink-bot

Copy link
Copy Markdown
Collaborator Author

🤖 PR Swarm — follow-up: the --config flag has been split out into #3652 per review. This PR is now the doctor fix only: 18 commits, pkg/files, pkg/doctor and pkg/db. Both branches build, lint clean, and pass the full suite independently.

@tink-bot

Copy link
Copy Markdown
Collaborator Author

🤖 PR Swarm — all five deferred items now have follow-ups:

Deferred item Outcome
--config scope creep split out into #3652
rootpath-relative defaults baked pre-config-read #3656 (draft, breaking)
FileStorage.Ensure() refactor scoped, not opened — awaiting your call
docs for --config go-vikunja/website#393 (draft) + config-raw.json in #3652
doctor hangs on unreachable S3 #3654 (stacked on this PR)

#3656 also closes a hazard found while building it: resolveDatabasePath's RootPath == ExecutablePath test meant "rootpath was never configured", which stopped being true when rootpath began defaulting to the working directory. An explicit service.rootpath equal to the install directory would have silently relocated the database to the user data directory.

ValidateFileStorage created the local base directory as a side effect of
checking it. Move that into ensureLocalBasePath so callers that only want to
inspect storage can do so without creating it.

InitFileHandler now calls both, so server and restore behavior is unchanged.
InitFileHandlerReadOnly sets up the storage backend and nothing else.
Running `vikunja doctor` outside the install directory created a `files`
directory in the current working directory and then reported it as existing and
writable, which reads as a passing check against storage doctor made itself.

Use the read-only file handler init. A missing base directory is now reported
as a failed check instead of being silently created.
ResolvedDatabasePath exists so callers can report the path a diagnostic would
use. Resolving it must therefore create nothing, which took three things:

getUserDataDir ended in os.MkdirAll, so merely reporting the path created the
user data directory. resolveUserDataDir computes it; getUserDataDir creates it.

resolveDatabasePath then chose its rootpath fallback from whether the injected
getter errored, which only the creating variant could do — so the reporting and
engine paths disagreed, and doctor failed a working install by naming a path the
server never opened. Creation now lives inside the injected getter: resolution
passes existingUserDataDir, the engine passes createdUserDataDir, and both fall
back to rootpath on failure.

Creation stays scoped to the user data directory. An operator-configured
database.path never gets its parents created, and an unwritable data directory
still degrades to rootpath rather than refusing to start.

getUserDataDir, the resolve-and-create variant, has no caller left. Keeping one
around invites reintroducing the bug where a reporting path creates the
directory it reports on.
Connecting to SQLite creates the database file plus its WAL and SHM sidecars, so
running `vikunja doctor` in the wrong directory left three files behind and
reported "Connection: OK" against an empty database it had just created.

Stat the resolved path first and report it as a check of its own. If the file is
missing, skip connecting entirely.
database.path: memory made doctor call CreateDBEngine, which creates a real WAL
database under os.MkdirTemp and leaks it on every invocation, then reported
"Connection: OK" against it. Report the ephemeral database and verify nothing.

Also drops checkSqliteFile's redundant bool return, which duplicated
CheckResult.Passed on every path.
checkSqliteFile only checked the stat error, so a database.path pointing at a
directory passed "Database file" and surfaced one line later at Connection.
Mirrors the files-side check, with the polarity inverted.
pkg/doctor had no test for the database half — the part that created vikunja.db
and its WAL sidecars. Covers the missing-file, ephemeral and path-is-a-directory
cases, each asserting nothing lands on disk.

Verified by mutation: removing the early return, the memory branch or the IsDir
guard fails these tests.
Every relative config path resolves against it and it defaults to the working
directory, so a reported path is untraceable without it. It sits with the config
file it explains rather than in system info.
A relative --config rendered as-is next to absolute paths in the same report,
which tells the reader nothing without knowing the cwd it came from.
Capturing a config value before init returns "", and Key.Set writes at viper's
override level, which outranks defaults. Restoring a captured value therefore
pinned database.path to "" for the rest of the process, and TestRestore resolved
it to the bare rootpath and failed with "is a directory".

Only visible when VIKUNJA_TESTS_USE_CONFIG=1, which CI sets for every database
except sqlite-in-memory, which is why every test-api job failed while local runs
passed. ResetForTests restores genuine fresh-process state instead.
TestInitFileHandler_S3Configuration and TestInitFileHandler_LocalFilesystem
replaced the package-level mem storage and never put it back, so any test
ordered after them failed. Pre-existing; surfaced by running with -shuffle=on.
@tink-bot
tink-bot force-pushed the doctor-pwd-assumption branch from 23954c7 to ff88e09 Compare August 29, 2026 17:11
@kolaente
kolaente merged commit 21c0939 into main Aug 29, 2026
41 checks passed
@kolaente
kolaente deleted the doctor-pwd-assumption branch August 29, 2026 18:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config config.yml, env vars, runtime flags, deployment config area/database Database engine behavior, schema issues, cross-engine DB bugs pr-swarm/needs-input PR Swarm finished, deferred items are waiting on maintainer input

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants