Skip to content

Development

Markus Langer edited this page Mar 12, 2026 · 1 revision

Development

Build

go build ./...   # must pass before committing
go vet ./...     # must pass

No external Go dependencies — standard library only.

Project layout

.
├── main.go                          # HTTP server, flag parsing, startup checks
├── go.mod
├── internal/
│   ├── zfs/zfs.go                   # Direct zpool/zfs command execution (reads)
│   ├── zfs/acl.go                   # GetDatasetACL — getfacl/nfs4_getfacl parsing
│   ├── ansible/runner.go            # Ansible playbook execution + ndjson parsing
│   ├── api/handlers.go              # REST API handlers + input validation
│   ├── broker/broker.go             # Thread-safe pub/sub broker
│   ├── broker/poller.go             # Background ZFS + user/group poller
│   ├── system/system.go             # Host info, ListUsers, ListGroups
│   └── smart/smart.go               # S.M.A.R.T. data via smartctl
├── playbooks/
│   ├── inventory/localhost          # Local connection inventory
│   ├── zfs_dataset_create.yml
│   ├── zfs_dataset_set.yml
│   ├── zfs_dataset_destroy.yml
│   ├── zfs_snapshot_create.yml
│   ├── zfs_snapshot_destroy.yml
│   ├── user_create.yml / user_modify.yml / user_delete.yml
│   ├── group_create.yml / group_modify.yml / group_delete.yml
│   ├── acl_set_posix.yml / acl_remove_posix.yml
│   ├── acl_set_nfs4.yml / acl_remove_nfs4.yml
│   ├── smb_setup.yml
│   ├── smb_usershare_set.yml / smb_usershare_unset.yml
│   └── smb_user_add.yml / smb_user_remove.yml
├── static/
│   ├── index.html                   # SPA shell + dialogs
│   ├── app.js                       # Vanilla JS frontend
│   └── style.css                    # Dark monospace theme
├── contrib/
│   ├── dumpstore.service            # systemd unit (Linux)
│   └── dumpstore.rc                 # rc.d script (FreeBSD)
├── install.sh
└── Makefile

Adding a new feature

Read operation (e.g. list something)

  1. Add a function to internal/zfs/zfs.go (or internal/system/system.go)
  2. Add a handler in internal/api/handlers.go
  3. Register the route in handlers.go:RegisterRoutes
  4. Add the UI in static/app.js (render function + fetch call) and static/index.html

Write operation (e.g. create/modify/destroy something)

  1. Add a playbook in playbooks/ (assert first, then mutate)
  2. Add a handler in internal/api/handlers.go using h.runner.Run(...)
  3. Register the route in RegisterRoutes
  4. Add the UI in static/app.js and static/index.html
  5. Call showOpLog(title, tasks, errorMsg) on the result — never skip the op-log for Ansible-backed actions

Architecture rules

  • Reads → internal/zfs package (direct exec). No Ansible for reads.
  • Writes → Ansible playbooks via internal/ansible/runner.go.
  • Do not change this split without a good reason — it exists to avoid Python startup overhead on every API call.

Playbook conventions

  • Target localhost with gather_facts: false
  • Always include an assert task before any mutating command
  • Document required extra vars in a header comment
  • Task names must be stable — RunAndGetStdout looks them up by name
  • Task names must be capitalised (ansible-lint requirement)

Frontend conventions

  • Vanilla JS, no framework, no build step
  • All data lives in the state object
  • Render functions are pure: read from state, write innerHTML
  • Always escape user-controlled strings through esc() before inserting into HTML
  • Use api(method, path, body) for all fetch calls — throws on non-2xx with err.tasks populated
  • Use showOpLog(title, tasks, errorMsg) after every write operation (success or failure)
  • Use toast(msg, type) only for non-Ansible notifications

Input validation

  • All handler inputs are validated with whitelist regexes before reaching the playbook
  • Shell-special characters (`@;|&$``) are rejected on all string inputs
  • System accounts (UID/GID < UID_MIN) are blocked from deletion at both API and playbook level

Clone this wiki locally