update: roll back what the restart actually deploys
On the deployment deploy/README.md documents, source_dir and install_dir are
the same tree and the restart command rebuilds the image from it. The
Dockerfile builds from cmd/ and internal/ and .dockerignore keeps the host
binaries out, so restoring the snapshotted binaries restored bytes nothing
reads. A bad commit therefore cost two health timeouts and two image builds
and ended in ErrRollbackFailed with an instruction to copy files back by hand,
which would not have helped either.
A deployment that rebuilds from source now has to say how the source is put
back. source_rollback "git" records the commit before the update and checks it
back out before the rollback restart. It refuses a dirty tree, because the
recorded commit does not describe one and a forced checkout would delete his
work. A build-from-source config that says nothing is refused by Validate, at
startup, rather than at the one rollback that mattered.
Also in this change, all from the same review:
- MethodPing, the one method a locked daemon answers. Preflight passed on an
unlocked daemon and the post-restart Presence read failed on a locked one,
so a good update read as SHE IS PROBABLY DOWN once the env key is gone.
- A dial failure is reported apart from a read failure. The documented
socket is under /var/lib/docker, which a non-root operator cannot
traverse, and "she is not answering" was the wrong diagnosis.
- Verify refuses to run as root over a tree owned by someone else. It runs
make build and make test in place, and root-owned artifacts break his next
ordinary make.
- A rollback no longer reverts config_files. That undid every config edit
since the last apply, phraser.model_path among them.
- The verify-failure path no longer reports rolled_back for a compile error.
- waitHealthy caps each attempt at the remaining budget, so a 90s timeout
cannot run to 99s.
- tail cuts on a rune boundary. Russian test names showed the seam.
- The claim that mavend does not import internal/update is replaced with
what is enforced: mavend constructs no Updater and nothing can call Apply.
- snapshot_dir inside source_dir is refused. It landed in the build context.
Found in review of #69.
This commit is contained in:
@@ -2,8 +2,12 @@ package update
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Verification is "does this tree build and does it pass its own tests", run
|
||||
@@ -34,6 +38,9 @@ type Step struct {
|
||||
|
||||
// Verify runs the build and the test suite in SourceDir.
|
||||
func (u *Updater) Verify(ctx context.Context) ([]Step, error) {
|
||||
if err := u.refuseRootBuild(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, u.cfg.verifyTimeout())
|
||||
defer cancel()
|
||||
var steps []Step
|
||||
@@ -55,11 +62,43 @@ func (u *Updater) Verify(ctx context.Context) ([]Step, error) {
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
// refuseRootBuild stops a sudo'd apply from building in a tree it does not own.
|
||||
//
|
||||
// The seam is injected so the tests can drive both sides without a second uid.
|
||||
func (u *Updater) refuseRootBuild() error {
|
||||
uid, owner, err := u.ids(u.cfg.SourceDir)
|
||||
if err != nil || uid != 0 || owner == 0 {
|
||||
return nil // not root, or root's own tree, or we cannot tell
|
||||
}
|
||||
return fmt.Errorf("%w: %s is owned by uid %d", ErrRootOnHisTree, u.cfg.SourceDir, owner)
|
||||
}
|
||||
|
||||
// realIDs — the running uid and the owner of dir. Split out for the tests.
|
||||
func realIDs(dir string) (uid int, owner uint32, err error) {
|
||||
fi, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
st, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return 0, 0, errors.New("update: cannot read directory ownership")
|
||||
}
|
||||
return os.Geteuid(), st.Uid, nil
|
||||
}
|
||||
|
||||
// tail keeps the last n bytes — a failing `make test` prints far more than is
|
||||
// useful, and the failure is always at the end.
|
||||
//
|
||||
// The cut is nudged forward to a rune boundary. Russian test names and fixture
|
||||
// strings are the common case in this tree, and a slice landing mid-rune starts
|
||||
// the log with a replacement character.
|
||||
func tail(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return "…" + s[len(s)-n:]
|
||||
cut := len(s) - n
|
||||
for cut < len(s) && !utf8.RuneStart(s[cut]) {
|
||||
cut++
|
||||
}
|
||||
return "…" + s[cut:]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user