Docker Build / docker (push) Successful in 42s
## Summary
- Drift detection: `model.Service.Drifted()` compares desired and running version by digest (tag when the digest is unknown). `services.awaiting_inventory` hides drift between a job result and the next inventory write.
- Poll and webhook jobs are refused for a drifted service; `POST /services/{id}/restore` (manual job to `desired_ref`) and `POST /services/{id}/accept` (running version becomes desired, guarded by `actual` = `Service.RunningRef`).
- Move suggestions (`store.MoveCandidates`, Compose only) and `POST /services/{id}/merge`: the old row keeps its id, mode, `desired_ref` and history and takes the new location; the duplicate is deleted.
- Backend only. The buttons and badges come with #28, on top of the Ledger UI (#18).
Closes #10
## Test plan
- [x] `gofmt -l .`, `go vet ./...`, `go test ./...`
- [x] `go test -race ./internal/agent/... ./internal/master/... ./internal/e2e/`
- [x] `go test -tags integration ./internal/master/store/` against postgres:17-alpine
- [ ] On the server: migration 0005 applies; no drift shown after a normal update
- [ ] On the server: manual `docker compose up` with another tag gives drift; webhook for an auto service is refused
- [ ] On the server: restore and accept via POST; merge after moving a stack
Reviewed-on: #30
52 lines
1.2 KiB
Go
52 lines
1.2 KiB
Go
// Package imageref splits container image references into repository, tag
|
|
// and digest.
|
|
package imageref
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Ref is an image reference split into its parts. Tag and Digest are empty
|
|
// when the reference has none.
|
|
type Ref struct {
|
|
Repo string
|
|
Tag string
|
|
Digest string
|
|
}
|
|
|
|
// Parse splits ref into repository, tag and digest. A colon before the last
|
|
// "/" belongs to a registry host and port ("localhost:5000/foo"), not a tag.
|
|
func Parse(ref string) Ref {
|
|
var r Ref
|
|
if at := strings.Index(ref, "@"); at >= 0 {
|
|
r.Digest = ref[at+1:]
|
|
ref = ref[:at]
|
|
}
|
|
slash := strings.LastIndex(ref, "/")
|
|
last := ref[slash+1:]
|
|
if colon := strings.Index(last, ":"); colon >= 0 {
|
|
r.Tag = last[colon+1:]
|
|
ref = ref[:slash+1+colon]
|
|
}
|
|
r.Repo = ref
|
|
return r
|
|
}
|
|
|
|
// String reassembles the reference as repo[:tag][@digest].
|
|
func (r Ref) String() string {
|
|
s := r.Repo
|
|
if r.Tag != "" {
|
|
s += ":" + r.Tag
|
|
}
|
|
if r.Digest != "" {
|
|
s += "@" + r.Digest
|
|
}
|
|
return s
|
|
}
|
|
|
|
var digestPattern = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
|
|
|
// IsDigest reports whether d is a well-formed sha256 digest.
|
|
func IsDigest(d string) bool { return digestPattern.MatchString(d) }
|