e871b05b38
CI / Test (linux/amd64) (pull_request) Successful in 34s
CI / Lint (pull_request) Failing after 16s
CI / Build (windows/amd64) (pull_request) Successful in 22s
CI / Build (linux/amd64) (pull_request) Successful in 20s
CI / Build (linux/arm64) (pull_request) Successful in 21s
Cleanup pass over the repo so CI can enforce lint going forward
without the only-new-issues escape hatch:
* gofumpt -w across the tree (31 hits, all formatting)
* misspell --fix (25 hits, US-locale spelling) — but reverted on
api.JobCancelled = "cancelled" since that literal is the wire +
DB CHECK constraint value, plus matched the case in store/fleet.go
back to "cancelled" and added //nolint:misspell on both for the
next time someone reaches for the auto-fix
* Wrap every `defer rows.Close()` / `defer stmt.Close()` /
`defer res.Body.Close()` in `defer func() { _ = .Close() }()`
to satisfy errcheck without losing the close itself
* websocket.Dial callers (1 prod, 4 tests) now capture + close the
upgrade response Body — coder/websocket can return res with a nil
Body on success, so the test deferred-closes guard against that
* Annotate the two genuine-by-design nilerr cases with //nolint
comments explaining why nil-on-error is the contract (cookie
missing = no session; ctx cancelled mid-backoff = clean shutdown)
* Add brief godoc on the 10 exported const groups + types that
revive flagged (api.HostOS/HostArch/JobKind/JobStatus/LogStream/
ErrorCode, restic.EventKind, store.Role, web.FS)
* Drop the unused (*Server).userByID method
* Inline the unparam baseView(active) — every UI page is under
the dashboard primary nav today
Result: `golangci-lint run ./...` reports 0 issues. CI lint job
no longer needs only-new-issues: true; X-06 follow-up entry in
tasks.md removed.
90 lines
2.8 KiB
Go
90 lines
2.8 KiB
Go
package http
|
||
|
||
import (
|
||
"fmt"
|
||
stdhttp "net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
|
||
)
|
||
|
||
// agent_assets.go serves the agent binary (one per OS/arch) and the
|
||
// install scripts. The binaries live under <DataDir>/agent-binaries/,
|
||
// laid down by the release pipeline (or copied by hand for now).
|
||
// The install scripts live in <DataDir>/install/ alongside the
|
||
// systemd unit.
|
||
//
|
||
// Both endpoints are intentionally unauthenticated: the install
|
||
// payload is unprivileged on its own — it's the one-time enrollment
|
||
// token that grants access. Anyone can pull the binary; only
|
||
// someone with a valid token can use it productively.
|
||
//
|
||
// P1-31: signed-binary verification is deferred. Today we serve
|
||
// whatever the operator dropped on disk. Future work bumps this to
|
||
// minisign/cosign signed bundles.
|
||
|
||
// installAssetsRoutes adds /agent/binary and /install/* to r.
|
||
func (s *Server) handleAgentBinary(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||
osTag := r.URL.Query().Get("os")
|
||
archTag := r.URL.Query().Get("arch")
|
||
if osTag == "" || archTag == "" {
|
||
writeJSONError(w, stdhttp.StatusBadRequest, "missing_os_or_arch",
|
||
"query params os= and arch= are required")
|
||
return
|
||
}
|
||
if !validOS(osTag) || !validArch(archTag) {
|
||
writeJSONError(w, stdhttp.StatusBadRequest, "unsupported_target",
|
||
fmt.Sprintf("os=%q arch=%q not in {linux,windows} × {amd64,arm64}", osTag, archTag))
|
||
return
|
||
}
|
||
|
||
ext := ""
|
||
if osTag == "windows" {
|
||
ext = ".exe"
|
||
}
|
||
name := fmt.Sprintf("restic-manager-agent-%s-%s%s", osTag, archTag, ext)
|
||
path := filepath.Join(s.deps.Cfg.DataDir, "agent-binaries", name)
|
||
if _, err := os.Stat(path); err != nil {
|
||
writeJSONError(w, stdhttp.StatusNotFound, "binary_not_published",
|
||
fmt.Sprintf("agent binary for %s/%s not published on this server", osTag, archTag))
|
||
return
|
||
}
|
||
w.Header().Set("Content-Type", "application/octet-stream")
|
||
w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
|
||
stdhttp.ServeFile(w, r, path)
|
||
}
|
||
|
||
func (s *Server) handleInstallAsset(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||
// chi's TrimPrefix-like behavior: r.URL.Path is "/install/<file>".
|
||
rel := strings.TrimPrefix(r.URL.Path, "/install/")
|
||
// Reject any path traversal — must be a flat filename.
|
||
if rel == "" || strings.ContainsAny(rel, "/\\") {
|
||
writeJSONError(w, stdhttp.StatusBadRequest, "bad_path", "")
|
||
return
|
||
}
|
||
path := filepath.Join(s.deps.Cfg.DataDir, "install", rel)
|
||
if _, err := os.Stat(path); err != nil {
|
||
writeJSONError(w, stdhttp.StatusNotFound, "not_found", "")
|
||
return
|
||
}
|
||
stdhttp.ServeFile(w, r, path)
|
||
}
|
||
|
||
func validOS(s string) bool {
|
||
switch api.HostOS(s) {
|
||
case api.OSLinux, api.OSWindows:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func validArch(s string) bool {
|
||
switch api.HostArch(s) {
|
||
case api.ArchAmd64, api.ArchArm64:
|
||
return true
|
||
}
|
||
return false
|
||
}
|