Skip to content

feat(backends): filesystem + DSM install backends - #2

Closed
albedosehen wants to merge 1 commit into
feat/namecheap-backendfrom
feat/install-backends
Closed

feat(backends): filesystem + DSM install backends#2
albedosehen wants to merge 1 commit into
feat/namecheap-backendfrom
feat/install-backends

Conversation

@albedosehen

@albedosehen albedosehen commented May 7, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1 (namecheap CA + registrar). Marked draft until the parent merges; will retarget to main afterward.

Summary

Lands the two install backends rota's first operator needs: plain filesystem (any nginx/Caddy/HAProxy/rustls consumer) and Synology DSM (example.com's actual host).

FilesystemInstall

Writes four files in the configured directory using the certbot naming convention so existing reload scripts work unchanged:

  • <id>.crt — leaf cert
  • <id>.chain.crt — intermediate chain only
  • <id>.fullchain.crt — leaf + chain concatenated
  • <id>.key — private key (mode 0600); cert files are 0644

Each artifact goes through write to .tmp.<n>fsyncrename so a crash mid-install can never leave a half-written file visible to the consuming service. Parent directory is created if missing for ergonomics; existing files overwrite idempotently.

DsmInstall

Shells out to synowebapi --exec api=SYNO.Core.Certificate:

  1. method=list — find the existing entry whose desc matches the configured description.
  2. Stage cert/chain/key into a per-install tempdir.
  3. method=import with the staged paths and optional id=<existing> so re-imports update in place rather than create duplicates. DSM reloads its own nginx as part of import.

synowebapi only exists on a running DSM box, so the binary call itself is exercised on the host; CI tests cover the JSON parsing (list/import response shapes) and the staging step (file presence + modes).

Dispatch

backends::build_from_config now matches both InstallSpec variants and returns concrete trait objects. The CertBackends.install field is no longer always None.

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo test --workspace — 18 passing (10 new)
    • filesystem: file presence + 0644/0600 modes, fullchain ordering, missing-directory creation, overwrite idempotency, concat_pem newline edge cases
    • dsm: stage() writes three files with correct modes, list response parses (with + without data), import-error envelope surfaces
    • config: filesystem InstallSpec variant round-trips
  • End-to-end exercise on the host: full DSM import with example.com cert (after PR-3 lands the scheduler)

What's not in this PR

  • Daemon scheduler loop, UNIX socket, SQLite audit DB (PR-3)
  • Dashboard (PR-4)
  • ACME / Let's Encrypt CA backend

Wires the InstallBackend trait surface for both targets rota's first
operator (kushtaka.ai on a Synology DSM) needs.

FilesystemInstall — writes four files in the configured directory
under the certbot naming convention so existing reload scripts work
unchanged: <id>.crt (leaf), <id>.chain.crt (intermediate chain),
<id>.fullchain.crt (leaf + chain concatenated), <id>.key (private
key, mode 0600). Cert files are 0644. Each artifact is written via
write-temp + fsync + rename so a crash mid-install can never expose a
half-written cert to the consuming service. Parent directory is
created if missing for ergonomics; existing files are overwritten
idempotently.

DsmInstall — shells out to `synowebapi --exec api=SYNO.Core.Certificate`
to import certs into DSM's certificate panel. Two-step flow: list
existing certs and find the one whose `desc` matches our configured
description (so re-imports update in place rather than create
duplicates), then call import with the optional `id=<existing>`. DSM
reloads its own nginx as part of import. Cert/chain/key are staged
into a per-install tempdir; tests inject a fixed staging directory
and a stub binary path to assert command construction without a real
DSM.

Dispatch in `backends::build_from_config` now matches both InstallSpec
variants and returns concrete trait objects; the bundle's `install`
field is no longer always None.

Tests (10 new, 18 total in workspace):
- filesystem: file presence + modes (0644/0600), fullchain ordering,
  missing-directory creation, overwrite idempotency, concat_pem
  newline edge cases
- dsm: stage produces three files with correct modes, list response
  parses (with + without `data` block), import error envelope
- config: filesystem InstallSpec variant round-trips
@albedosehen
albedosehen deleted the branch feat/namecheap-backend May 7, 2026 19:09
@albedosehen albedosehen closed this May 7, 2026
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