phase 0: project bootstrap

P0-01 Go module + cmd/server + cmd/agent skeletons + internal/ tree
P0-02 LICENSE (PolyForm NC 1.0.0), README, CONTRIBUTING
P0-03 golangci-lint, pre-commit, .editorconfig, .gitignore
P0-04 Gitea Actions CI: test (race+coverage), lint, cross-platform build matrix
P0-05 Dockerfile.server (multi-stage, distroless/static), docker-compose.yml
P0-06 Makefile with build/test/lint/fmt/run/release targets

build, vet, test, and cross-compile to linux/{amd64,arm64} + windows/amd64
all verified locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 00:03:59 +01:00
parent ab02869d82
commit 25aa001135
26 changed files with 641 additions and 6 deletions
+18
View File
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.go]
indent_style = tab
[Makefile]
indent_style = tab
[*.{md,yml,yaml,json}]
indent_size = 2
+80
View File
@@ -0,0 +1,80 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
GO_VERSION: "1.23"
jobs:
test:
name: Test (linux/amd64)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: go vet
run: go vet ./...
- name: go test
run: go test -race -coverprofile=coverage.out ./...
- name: coverage summary
run: go tool cover -func=coverage.out | tail -1
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- uses: golangci/golangci-lint-action@v6
with:
version: v1.61.0
args: --timeout=5m
build:
name: Build (${{ matrix.goos }}/${{ matrix.goarch }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: windows
goarch: amd64
ext: ".exe"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: build server + agent
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: "0"
run: |
mkdir -p bin
go build -trimpath -ldflags="-s -w" \
-o bin/restic-manager-server-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} \
./cmd/server
go build -trimpath -ldflags="-s -w" \
-o bin/restic-manager-agent-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.ext }} \
./cmd/agent
- uses: actions/upload-artifact@v3
with:
name: binaries-${{ matrix.goos }}-${{ matrix.goarch }}
path: bin/*
retention-days: 7
+27
View File
@@ -0,0 +1,27 @@
# Build output
/bin/
/dist/
# Local data / runtime state
/data/
/certs/
*.db
*.db-journal
*.db-wal
*.db-shm
# Editor / OS
.DS_Store
.idea/
.vscode/
*.swp
*.swo
# Coverage
coverage.out
coverage.html
# Local environment overrides
.env
.env.local
*.local
+42
View File
@@ -0,0 +1,42 @@
run:
timeout: 5m
tests: true
linters:
disable-all: true
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofumpt
- goimports
- misspell
- revive
- bodyclose
- errorlint
- nilerr
- prealloc
- unconvert
- unparam
linters-settings:
goimports:
local-prefixes: gitea.dcglab.co.uk/steve/restic-manager
revive:
rules:
- name: exported
arguments: ["disableStutteringCheck"]
misspell:
locale: US
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck
- unparam
max-issues-per-linter: 0
max-same-issues: 0
+25
View File
@@ -0,0 +1,25 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
args: ["--maxkb=512"]
- id: check-merge-conflict
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
hooks:
- id: go-fmt
- id: go-imports
- id: go-vet-mod
- id: go-mod-tidy
- repo: https://github.com/golangci/golangci-lint
rev: v1.61.0
hooks:
- id: golangci-lint
+30
View File
@@ -0,0 +1,30 @@
# Contributing
Thanks for your interest in contributing to restic-manager.
> This is a placeholder. The project is in pre-alpha (Phase 1 / MVP). A
> full contributor guide will land alongside the Phase 5 OSS-readiness
> work — see [`tasks.md`](./tasks.md) P5-02. Until then the notes below
> apply.
## Before opening a PR
1. Open an issue first for non-trivial changes — the design is still
moving (see [`spec.md`](./spec.md)) and unsolicited large PRs may
conflict with in-flight work.
2. `make lint test` should pass.
3. Match the existing code style — `gofumpt`, `goimports`, no comments
that just restate what the code does.
4. Keep commits focused; one logical change per commit.
## Reporting security issues
Please do **not** open a public issue for security problems. A
`SECURITY.md` with a private disclosure path will be added in Phase 5
(P5-05). Until then, contact the repository owner directly via the
contact details on their gitea profile.
## License
By contributing you agree that your contributions are licensed under
the [PolyForm Noncommercial 1.0.0](./LICENSE) license.
+120
View File
@@ -0,0 +1,120 @@
PolyForm Noncommercial License 1.0.0
<https://polyformproject.org/licenses/noncommercial/1.0.0>
## Acceptance
In order to get any license under these terms, you must agree to them
as both strict obligations and conditions to all your licenses.
## Copyright License
The licensor grants you a copyright license for the software to do
everything you might do with the software that would otherwise infringe
the licensor's copyright in it for any permitted purpose. However, you
may only distribute the software according to Distribution License and
make changes or new works based on the software according to Changes
and New Works License.
## Distribution License
The licensor grants you an additional copyright license to distribute
copies of the software. Your license to distribute covers distributing
the software with changes and new works permitted by Changes and New
Works License.
## Notices
You must ensure that anyone who gets a copy of any part of the software
from you also gets a copy of these terms or the URL for them above, as
well as copies of any plain-text lines beginning with "Required Notice:"
that the licensor provided with the software. For example:
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
## Changes and New Works License
The licensor grants you an additional copyright license to make changes
and new works based on the software for any permitted purpose.
## Patent License
The licensor grants you a patent license for the software that covers
patent claims the licensor can license, or becomes able to license,
that you would infringe by using the software.
## Noncommercial Purposes
Any noncommercial purpose is a permitted purpose.
## Personal Uses
Personal use for research, experiment, and testing for the benefit of
public knowledge, personal study, private entertainment, hobby projects,
amateur pursuits, or religious observance, without any anticipated
commercial application, is use for a permitted purpose.
## Noncommercial Organizations
Use by any charitable organization, educational institution, public
research organization, public safety or health organization,
environmental protection organization, or government institution is use
for a permitted purpose regardless of the source of funding or
obligations resulting from the funding.
## Fair Use
You may have "fair use" rights for the software under the law. These
terms do not limit them.
## No Other Rights
These terms do not allow you to sublicense or transfer any of your
licenses to anyone else, or prevent the licensor from granting licenses
to anyone else. These terms do not imply any other licenses.
## Patent Defense
If you make any written claim that the software infringes or contributes
to infringement of any patent, your patent license for the software
granted under these terms ends immediately. If your company makes such
a claim, your patent license ends immediately for work on behalf of
your company.
## Violations
The first time you are notified in writing that you have violated any
of these terms, or done anything with the software not covered by your
licenses, your licenses can nonetheless continue if you come into full
compliance with these terms, and take practical steps to correct past
violations, within 32 days of receiving notice. Otherwise, all your
licenses end immediately.
## No Liability
***As far as the law allows, the software comes as is, without any
warranty or condition, and the licensor will not be liable to you for
any damages arising out of these terms or the use or nature of the
software, under any kind of legal claim.***
## Definitions
The **licensor** is the individual or entity offering these terms, and
the **software** is the software the licensor makes available under
these terms.
**You** refers to the individual or entity agreeing to these terms.
**Your company** is any legal entity, sole proprietorship, or other
kind of organization that you work for, plus all organizations that
have control over, are under the control of, or are under common
control with that organization. *Control* means ownership of
substantially all the assets of an entity, or the power to direct its
management and policies by vote, contract, or otherwise. Control can
be direct or indirect.
**Your licenses** are all the licenses granted to you for the software
under these terms.
**Use** means anything you do with the software requiring one of your
licenses.
+68
View File
@@ -0,0 +1,68 @@
# restic-manager — common dev targets
SHELL := /bin/bash
BIN_DIR := bin
SERVER_BIN := $(BIN_DIR)/restic-manager-server
AGENT_BIN := $(BIN_DIR)/restic-manager-agent
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
LDFLAGS := -s -w -X main.version=$(VERSION)
GOFLAGS := -trimpath
DOCKER_IMAGE ?= ghcr.io/dcglab/restic-manager
DOCKER_TAG ?= dev
.PHONY: help build server agent test test-race lint fmt tidy clean run-server run-agent docker release
help:
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "};{printf " \033[36m%-14s\033[0m %s\n",$$1,$$2}'
build: server agent ## Build server + agent into ./bin
server: ## Build the server binary
@mkdir -p $(BIN_DIR)
CGO_ENABLED=0 go build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(SERVER_BIN) ./cmd/server
agent: ## Build the agent binary
@mkdir -p $(BIN_DIR)
CGO_ENABLED=0 go build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(AGENT_BIN) ./cmd/agent
test: ## Run tests
go test ./...
test-race: ## Run tests with the race detector
go test -race -coverprofile=coverage.out ./...
lint: ## Run golangci-lint
golangci-lint run ./...
fmt: ## Format with gofumpt + goimports
gofumpt -w .
goimports -local gitea.dcglab.co.uk/steve/restic-manager -w .
tidy: ## go mod tidy
go mod tidy
clean: ## Remove build artifacts
rm -rf $(BIN_DIR) coverage.out coverage.html
run-server: server ## Build and run the server
$(SERVER_BIN)
run-agent: agent ## Build and run the agent
$(AGENT_BIN)
docker: ## Build the server Docker image
docker build -f deploy/Dockerfile.server --build-arg VERSION=$(VERSION) -t $(DOCKER_IMAGE):$(DOCKER_TAG) .
release: ## Cross-compile for all supported platforms
@mkdir -p $(BIN_DIR)
@for target in linux/amd64 linux/arm64 windows/amd64; do \
goos=$${target%/*}; goarch=$${target#*/}; \
ext=""; if [ "$$goos" = "windows" ]; then ext=".exe"; fi; \
echo "==> $$goos/$$goarch"; \
GOOS=$$goos GOARCH=$$goarch CGO_ENABLED=0 \
go build $(GOFLAGS) -ldflags "$(LDFLAGS)" \
-o $(BIN_DIR)/restic-manager-server-$$goos-$$goarch$$ext ./cmd/server; \
GOOS=$$goos GOARCH=$$goarch CGO_ENABLED=0 \
go build $(GOFLAGS) -ldflags "$(LDFLAGS)" \
-o $(BIN_DIR)/restic-manager-agent-$$goos-$$goarch$$ext ./cmd/agent; \
done
+67
View File
@@ -0,0 +1,67 @@
# restic-manager
Self-hosted, browser-based, single-pane-of-glass for managing
[restic](https://restic.net) backups across a fleet of Linux and Windows
endpoints.
> Status: pre-alpha. Phase 0 (project bootstrap) complete; Phase 1 (MVP) in
> progress. See [`spec.md`](./spec.md) for the design and
> [`tasks.md`](./tasks.md) for the roadmap.
## What it does (target)
- Central visibility into backup state for every endpoint
- Trigger any restic operation remotely (`backup`, `forget`, `prune`,
`check`, `unlock`, `snapshots`, `stats`, `diff`, `restore`)
- Manage per-host backup schedules from the UI
- Live job progress streamed back to the UI
- Restore wizard (browse snapshots, pick paths, restore to original or
alternate host)
- Repo health surfacing (size, dedup ratio, last check, lock state)
- Alerting on failure or staleness
- Cross-platform agent (Linux + Windows)
- Ransomware-resistant repo access via append-only credentials
## Architecture (one-line summary)
A small Go control-plane on the Proxmox host, lightweight Go agents on each
endpoint that hold an outbound WebSocket to the control-plane, and a
`restic/rest-server` on Unraid that holds the actual backup data. The
control-plane never touches backup bytes.
Full architecture diagram and component breakdown:
[`spec.md` §3](./spec.md).
## Repository layout
```
cmd/server/ control-plane binary
cmd/agent/ endpoint agent binary
internal/api shared API types (REST + WS envelopes)
internal/server/ HTTP, WS, UI handlers
internal/agent/ service integration, restic runner, local scheduler
internal/restic restic CLI wrapper
internal/store SQLite persistence
internal/crypto secret encryption
internal/auth passwords, sessions, agent tokens
web/ server-rendered templates + static assets
deploy/ Dockerfile, docker-compose.yml, install scripts
design/ UI wireframes (Phase 0 design pass)
```
## Local development
Requires Go 1.23+ (built and tested on 1.26).
```sh
make build # builds cmd/server and cmd/agent into ./bin
make test # runs go test ./...
make lint # runs golangci-lint
make run-server # runs the server (dev defaults)
```
## License
PolyForm Noncommercial 1.0.0 — see [`LICENSE`](./LICENSE). Free for personal,
hobby, research, educational, governmental, and other noncommercial use.
Commercial use requires a separate license.
+33
View File
@@ -0,0 +1,33 @@
package main
import (
"context"
"flag"
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
)
var version = "dev"
func main() {
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *showVersion {
fmt.Println("restic-manager-agent", version)
return
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
slog.Info("restic-manager agent starting", "version", version)
<-ctx.Done()
slog.Info("shutting down")
}
+33
View File
@@ -0,0 +1,33 @@
package main
import (
"context"
"flag"
"fmt"
"log/slog"
"os"
"os/signal"
"syscall"
)
var version = "dev"
func main() {
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *showVersion {
fmt.Println("restic-manager-server", version)
return
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
slog.Info("restic-manager server starting", "version", version)
<-ctx.Done()
slog.Info("shutting down")
}
+37
View File
@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1.7
# ---- Build stage --------------------------------------------------------
FROM golang:1.23-alpine AS build
WORKDIR /src
# Pure-Go SQLite (modernc.org/sqlite) means we can keep CGO off and build a
# fully static binary that runs on distroless/static.
ENV CGO_ENABLED=0 \
GOOS=linux \
GOFLAGS="-trimpath"
# Cache module downloads in a separate layer.
COPY go.mod go.sum* ./
RUN go mod download
COPY . .
ARG VERSION=dev
RUN go build -ldflags="-s -w -X main.version=${VERSION}" \
-o /out/restic-manager-server \
./cmd/server
# ---- Runtime stage ------------------------------------------------------
FROM gcr.io/distroless/static-debian12:nonroot
LABEL org.opencontainers.image.source="https://gitea.dcglab.co.uk/steve/restic-manager"
LABEL org.opencontainers.image.licenses="PolyForm-Noncommercial-1.0.0"
USER nonroot:nonroot
WORKDIR /
COPY --from=build /out/restic-manager-server /usr/local/bin/restic-manager-server
EXPOSE 8443
ENTRYPOINT ["/usr/local/bin/restic-manager-server"]
+18
View File
@@ -0,0 +1,18 @@
# Reference deployment for the restic-manager control plane.
# Mirrors spec.md §10.1. Adjust image tag and RM_BASE_URL for your env.
services:
restic-manager:
image: ghcr.io/dcglab/restic-manager:latest
restart: unless-stopped
ports:
- "8443:8443"
volumes:
- ./data:/data
- ./certs:/certs:ro
environment:
- RM_DATA_DIR=/data
- RM_LISTEN=:8443
- RM_BASE_URL=https://restic.lab.example
- RM_TLS_CERT=/certs/fullchain.pem
- RM_TLS_KEY=/certs/privkey.pem
- RM_SECRET_KEY_FILE=/data/secret.key
+3
View File
@@ -0,0 +1,3 @@
module gitea.dcglab.co.uk/steve/restic-manager
go 1.23
+3
View File
@@ -0,0 +1,3 @@
// Package runner spawns restic processes, parses their --json output
// stream, and forwards events to the server over WebSocket.
package runner
+4
View File
@@ -0,0 +1,4 @@
// Package scheduler runs the agent's local cron loop. The server is the
// source of truth for schedules; this package reconciles to whatever
// the server most recently pushed.
package scheduler
+3
View File
@@ -0,0 +1,3 @@
// Package service wires the agent into the host's service manager
// (systemd on Linux, the Service Control Manager on Windows).
package service
+4
View File
@@ -0,0 +1,4 @@
// Package api defines shared types for the control-plane API:
// REST request/response shapes (server ↔ browser) and WebSocket
// message envelopes (server ↔ agent). See spec.md §6.
package api
+3
View File
@@ -0,0 +1,3 @@
// Package auth handles password hashing (argon2id), session cookies,
// CSRF tokens, and bearer-token verification for agents.
package auth
+3
View File
@@ -0,0 +1,3 @@
// Package crypto wraps AEAD encryption used to protect repo passwords,
// REST-server credentials, and pre/post hook bodies at rest.
package crypto
+3
View File
@@ -0,0 +1,3 @@
// Package restic wraps the restic CLI: locating the binary, invoking
// it with --json, and parsing the streamed event payloads.
package restic
+2
View File
@@ -0,0 +1,2 @@
// Package http hosts the chi-based REST handlers for the control plane.
package http
+3
View File
@@ -0,0 +1,3 @@
// Package ui renders the HTMX/Tailwind frontend from server-side
// html/templates.
package ui
+3
View File
@@ -0,0 +1,3 @@
// Package ws hosts the WebSocket transport for agent ↔ server and the
// browser-facing live job log stream.
package ws
+3
View File
@@ -0,0 +1,3 @@
// Package store is the SQLite persistence layer
// (modernc.org/sqlite, no CGo).
package store
+6 -6
View File
@@ -8,12 +8,12 @@ Sizes: **S** = under a day, **M** = 13 days, **L** = 37 days.
## Phase 0 — Project bootstrap
- [ ] **P0-01** (S) Initialize Go module, `cmd/server`, `cmd/agent`, baseline `internal/` packages
- [ ] **P0-02** (S) Add LICENSE (PolyForm Noncommercial 1.0.0), README stub, CONTRIBUTING placeholder
- [ ] **P0-03** (S) Set up `golangci-lint`, `gofumpt`, `goimports`; pre-commit config
- [ ] **P0-04** (S) GitHub Actions: build matrix (linux amd64/arm64, windows amd64), unit tests, lint
- [ ] **P0-05** (S) `Dockerfile.server` (multi-stage, distroless), `deploy/docker-compose.yml`
- [ ] **P0-06** (S) Makefile / `taskfile.yml` with common targets (`build`, `test`, `run`, `release`)
- [x] **P0-01** (S) Initialize Go module, `cmd/server`, `cmd/agent`, baseline `internal/` packages
- [x] **P0-02** (S) Add LICENSE (PolyForm Noncommercial 1.0.0), README stub, CONTRIBUTING placeholder
- [x] **P0-03** (S) Set up `golangci-lint`, `gofumpt`, `goimports`; pre-commit config
- [x] **P0-04** (S) ~~GitHub Actions~~ Gitea Actions: build matrix (linux amd64/arm64, windows amd64), unit tests, lint
- [x] **P0-05** (S) `Dockerfile.server` (multi-stage, distroless), `deploy/docker-compose.yml`
- [x] **P0-06** (S) Makefile / ~~`taskfile.yml`~~ with common targets (`build`, `test`, `run`, `release`)
---