feat: restart to apply update, privileged helper, rollback
This commit is contained in:
1 parent
70460e4170
commit
54e8fd5fed
63 files changed
+8635
-164
No files matched your search
@@ -0,0 +1,592 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/update"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newUpdateStatusUseCase(store update.StatusStore, jobs update.JobStore, helper update.HelperDetector, running string) *useCases.GetUpdateStatusUseCase {
|
||||
return useCases.NewGetUpdateStatusUseCase(store, jobs, helper, &fakeResultStore{}, &fakeRestartRequestStore{}, running, time.Now)
|
||||
}
|
||||
|
||||
type fakeRestartRequestStore struct {
|
||||
mu sync.Mutex
|
||||
request update.RestartRequest
|
||||
present bool
|
||||
corrupt bool
|
||||
writeErr error
|
||||
writes int
|
||||
deletes int
|
||||
}
|
||||
|
||||
var _ update.RestartRequestStore = &fakeRestartRequestStore{}
|
||||
|
||||
func (f *fakeRestartRequestStore) Write(request update.RestartRequest) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.writeErr != nil {
|
||||
return f.writeErr
|
||||
}
|
||||
f.request = request
|
||||
f.present = true
|
||||
f.writes++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRestartRequestStore) Read() (update.RestartRequest, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.corrupt {
|
||||
return update.RestartRequest{}, errors.New("corrupt marker")
|
||||
}
|
||||
if !f.present {
|
||||
return update.RestartRequest{}, update.ErrNoRestartRequest
|
||||
}
|
||||
return f.request, nil
|
||||
}
|
||||
|
||||
func (f *fakeRestartRequestStore) Delete() error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.present = false
|
||||
f.deletes++
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeResultStore struct {
|
||||
mu sync.Mutex
|
||||
result update.Result
|
||||
found bool
|
||||
writeErr error
|
||||
writes []update.Result
|
||||
deletes int
|
||||
}
|
||||
|
||||
var _ update.ResultStore = &fakeResultStore{}
|
||||
|
||||
func (f *fakeResultStore) Read() (update.Result, bool) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.result, f.found
|
||||
}
|
||||
|
||||
func (f *fakeResultStore) Write(result update.Result) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.writeErr != nil {
|
||||
return f.writeErr
|
||||
}
|
||||
f.result = result
|
||||
f.found = true
|
||||
f.writes = append(f.writes, result)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeResultStore) Delete() error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.found = false
|
||||
f.result = update.Result{}
|
||||
f.deletes++
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeRunningInfoStore struct {
|
||||
mu sync.Mutex
|
||||
info update.RunningInfo
|
||||
found bool
|
||||
writeErr error
|
||||
writes []update.RunningInfo
|
||||
}
|
||||
|
||||
var _ update.RunningInfoStore = &fakeRunningInfoStore{}
|
||||
|
||||
func (f *fakeRunningInfoStore) Write(info update.RunningInfo) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.writeErr != nil {
|
||||
return f.writeErr
|
||||
}
|
||||
f.info = info
|
||||
f.found = true
|
||||
f.writes = append(f.writes, info)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeRunningInfoStore) Read() (update.RunningInfo, bool) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.info, f.found
|
||||
}
|
||||
|
||||
func (f *fakeRunningInfoStore) set(info update.RunningInfo) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.info = info
|
||||
f.found = true
|
||||
}
|
||||
|
||||
func (f *fakeRunningInfoStore) unreadable() {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.info = update.RunningInfo{}
|
||||
f.found = false
|
||||
}
|
||||
|
||||
type fakeClock struct {
|
||||
mu sync.Mutex
|
||||
now time.Time
|
||||
sleeps int
|
||||
onSleep func(sleeps int)
|
||||
}
|
||||
|
||||
var _ update.Clock = &fakeClock{}
|
||||
|
||||
func (f *fakeClock) Now() time.Time {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.now
|
||||
}
|
||||
|
||||
func (f *fakeClock) Sleep(duration time.Duration) {
|
||||
f.mu.Lock()
|
||||
f.now = f.now.Add(duration)
|
||||
f.sleeps++
|
||||
sleeps, hook := f.sleeps, f.onSleep
|
||||
f.mu.Unlock()
|
||||
if hook != nil {
|
||||
hook(sleeps)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeClock) advance(duration time.Duration) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.now = f.now.Add(duration)
|
||||
}
|
||||
|
||||
type fakeServiceControl struct {
|
||||
mu sync.Mutex
|
||||
restarts []time.Time
|
||||
clock *fakeClock
|
||||
failOn map[int]error
|
||||
panicOn int
|
||||
onRestart func(call int)
|
||||
events *[]string
|
||||
}
|
||||
|
||||
var _ update.ServiceControl = &fakeServiceControl{}
|
||||
|
||||
func (f *fakeServiceControl) Restart() error {
|
||||
f.mu.Lock()
|
||||
f.restarts = append(f.restarts, f.clock.Now())
|
||||
call := len(f.restarts)
|
||||
failure := f.failOn[call]
|
||||
shouldPanic := f.panicOn == call
|
||||
hook := f.onRestart
|
||||
f.mu.Unlock()
|
||||
if f.events != nil {
|
||||
*f.events = append(*f.events, "restart")
|
||||
}
|
||||
if shouldPanic {
|
||||
panic("service control exploded")
|
||||
}
|
||||
if failure != nil {
|
||||
return failure
|
||||
}
|
||||
if hook != nil {
|
||||
hook(call)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeServiceControl) Status() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (f *fakeServiceControl) restartCount() int {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return len(f.restarts)
|
||||
}
|
||||
|
||||
type fakeHealthChecker struct {
|
||||
mu sync.Mutex
|
||||
healthy bool
|
||||
asked int
|
||||
}
|
||||
|
||||
var _ update.HealthChecker = &fakeHealthChecker{}
|
||||
|
||||
func (f *fakeHealthChecker) Healthy() bool {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.asked++
|
||||
return f.healthy
|
||||
}
|
||||
|
||||
type fsNode struct {
|
||||
kind string
|
||||
content []byte
|
||||
mode uint32
|
||||
target string
|
||||
owner string
|
||||
}
|
||||
|
||||
type fsOperation struct {
|
||||
name string
|
||||
path string
|
||||
resolved string
|
||||
}
|
||||
|
||||
var errFakeLoop = errors.New("too many levels of symbolic links")
|
||||
|
||||
type fakeHelperFS struct {
|
||||
mu sync.Mutex
|
||||
nodes map[string]*fsNode
|
||||
ops []fsOperation
|
||||
counts map[string]int
|
||||
failures map[string]map[int]error
|
||||
hooks map[string]map[int]func(*fakeHelperFS)
|
||||
tempDirs int
|
||||
events *[]string
|
||||
history map[string][]string
|
||||
}
|
||||
|
||||
var _ update.HelperFilesystem = &fakeHelperFS{}
|
||||
|
||||
func newFakeHelperFS() *fakeHelperFS {
|
||||
f := &fakeHelperFS{
|
||||
nodes: map[string]*fsNode{"/": {kind: "dir", mode: 0o755, owner: "root"}},
|
||||
counts: map[string]int{},
|
||||
failures: map[string]map[int]error{},
|
||||
hooks: map[string]map[int]func(*fakeHelperFS){},
|
||||
history: map[string][]string{},
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) failNth(operation string, call int, err error) {
|
||||
if f.failures[operation] == nil {
|
||||
f.failures[operation] = map[int]error{}
|
||||
}
|
||||
f.failures[operation][call] = err
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) after(operation string, call int, mutate func(*fakeHelperFS)) {
|
||||
if f.hooks[operation] == nil {
|
||||
f.hooks[operation] = map[int]func(*fakeHelperFS){}
|
||||
}
|
||||
f.hooks[operation][call] = mutate
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) putDir(target string) {
|
||||
target = path.Clean(target)
|
||||
for current := target; current != "/"; current = path.Dir(current) {
|
||||
if _, found := f.nodes[current]; !found {
|
||||
f.nodes[current] = &fsNode{kind: "dir", mode: 0o755, owner: "app"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) putFile(target string, content string) {
|
||||
target = path.Clean(target)
|
||||
f.putDir(path.Dir(target))
|
||||
f.nodes[target] = &fsNode{kind: "file", content: []byte(content), mode: 0o644, owner: "app"}
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) putSymlink(target string, destination string) {
|
||||
target = path.Clean(target)
|
||||
f.putDir(path.Dir(target))
|
||||
f.nodes[target] = &fsNode{kind: "symlink", target: destination, owner: "app"}
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) content(target string) (string, bool) {
|
||||
node, found := f.nodes[path.Clean(target)]
|
||||
if !found || node.kind != "file" {
|
||||
return "", false
|
||||
}
|
||||
return string(node.content), true
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) exists(target string) bool {
|
||||
_, found := f.nodes[path.Clean(target)]
|
||||
return found
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) mode(target string) uint32 {
|
||||
if node, found := f.nodes[path.Clean(target)]; found {
|
||||
return node.mode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) resolve(target string, followFinal bool) (string, error) {
|
||||
return f.resolveDepth(path.Clean(target), followFinal, 0)
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) resolveDepth(target string, followFinal bool, depth int) (string, error) {
|
||||
if depth > 8 {
|
||||
return "", errFakeLoop
|
||||
}
|
||||
parts := strings.Split(strings.Trim(target, "/"), "/")
|
||||
current := "/"
|
||||
for index, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
next := path.Join(current, part)
|
||||
node, found := f.nodes[next]
|
||||
last := index == len(parts)-1
|
||||
if found && node.kind == "symlink" && (!last || followFinal) {
|
||||
resolved, err := f.resolveDepth(node.target, true, depth+1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
current = resolved
|
||||
continue
|
||||
}
|
||||
current = next
|
||||
}
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) begin(operation string, target string, followFinal bool) (string, error) {
|
||||
f.counts[operation]++
|
||||
resolved, err := f.resolve(target, followFinal)
|
||||
f.ops = append(f.ops, fsOperation{name: operation, path: target, resolved: resolved})
|
||||
if f.events != nil {
|
||||
*f.events = append(*f.events, operation+" "+target)
|
||||
}
|
||||
if failure := f.failures[operation][f.counts[operation]]; failure != nil {
|
||||
return resolved, failure
|
||||
}
|
||||
return resolved, err
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) end(operation string) {
|
||||
if hook := f.hooks[operation][f.counts[operation]]; hook != nil {
|
||||
hook(f)
|
||||
}
|
||||
}
|
||||
|
||||
func missing(operation string, target string) error {
|
||||
return &fs.PathError{Op: operation, Path: target, Err: fs.ErrNotExist}
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) Lstat(target string) (update.FileInfo, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
defer f.end("lstat")
|
||||
resolved, err := f.begin("lstat", target, false)
|
||||
if err != nil {
|
||||
return update.FileInfo{}, err
|
||||
}
|
||||
node, found := f.nodes[resolved]
|
||||
if !found {
|
||||
return update.FileInfo{}, missing("lstat", target)
|
||||
}
|
||||
return update.FileInfo{IsSymlink: node.kind == "symlink", IsDir: node.kind == "dir", IsRegular: node.kind == "file"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) ReadFile(target string) ([]byte, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
defer f.end("read")
|
||||
resolved, err := f.begin("read", target, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node, found := f.nodes[resolved]
|
||||
if !found {
|
||||
return nil, missing("read", target)
|
||||
}
|
||||
if node.kind == "symlink" {
|
||||
return nil, errFakeLoop
|
||||
}
|
||||
if node.kind != "file" {
|
||||
return nil, errors.New("is a directory")
|
||||
}
|
||||
return append([]byte(nil), node.content...), nil
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) createNode(operation string, target string, content []byte, mode uint32) error {
|
||||
resolved, err := f.begin(operation, target, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, found := f.nodes[resolved]; found {
|
||||
return &fs.PathError{Op: operation, Path: target, Err: fs.ErrExist}
|
||||
}
|
||||
parent, found := f.nodes[path.Dir(resolved)]
|
||||
if !found || parent.kind != "dir" {
|
||||
return missing(operation, target)
|
||||
}
|
||||
f.nodes[resolved] = &fsNode{kind: "file", content: content, mode: mode, owner: "root"}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) CreateExclusive(target string, content []byte, mode uint32) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
defer f.end("create")
|
||||
return f.createNode("create", target, append([]byte(nil), content...), mode)
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) Copy(source string, destination string, mode uint32) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
defer f.end("copy")
|
||||
resolved, err := f.begin("copy", source, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, found := f.nodes[resolved]
|
||||
if !found {
|
||||
return missing("copy", source)
|
||||
}
|
||||
if node.kind == "symlink" {
|
||||
return errFakeLoop
|
||||
}
|
||||
if node.kind != "file" {
|
||||
return errors.New("not a regular file")
|
||||
}
|
||||
f.ops = append(f.ops, fsOperation{name: "copy-to", path: destination})
|
||||
return f.createNode("copy-to", destination, append([]byte(nil), node.content...), mode)
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) Rename(oldPath string, newPath string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
defer f.end("rename")
|
||||
from, err := f.begin("rename", oldPath, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to, err := f.resolve(newPath, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, found := f.nodes[from]
|
||||
if !found {
|
||||
return missing("rename", oldPath)
|
||||
}
|
||||
if existing, found := f.nodes[to]; found && existing.kind == "dir" {
|
||||
return errors.New("is a directory")
|
||||
}
|
||||
f.ops = append(f.ops, fsOperation{name: "rename-to", path: newPath, resolved: to})
|
||||
f.nodes[to] = node
|
||||
delete(f.nodes, from)
|
||||
f.history[to] = append(f.history[to], string(node.content))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) Remove(target string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
defer f.end("remove")
|
||||
resolved, err := f.begin("remove", target, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, found := f.nodes[resolved]; !found {
|
||||
return missing("remove", target)
|
||||
}
|
||||
delete(f.nodes, resolved)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) RemoveAll(target string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
defer f.end("removeall")
|
||||
resolved, err := f.begin("removeall", target, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prefix := resolved + "/"
|
||||
for name := range f.nodes {
|
||||
if name == resolved || strings.HasPrefix(name, prefix) {
|
||||
delete(f.nodes, name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) Chmod(target string, mode uint32) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
defer f.end("chmod")
|
||||
resolved, err := f.begin("chmod", target, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, found := f.nodes[resolved]
|
||||
if !found {
|
||||
return missing("chmod", target)
|
||||
}
|
||||
if node.kind == "symlink" {
|
||||
return errFakeLoop
|
||||
}
|
||||
node.mode = mode
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) MakePrivateTempDir() (string, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
defer f.end("tempdir")
|
||||
f.tempDirs++
|
||||
directory := "/tmp/renewcms-update-" + string(rune('0'+f.tempDirs))
|
||||
if _, err := f.begin("tempdir", directory, false); err != nil {
|
||||
return "", err
|
||||
}
|
||||
f.nodes["/tmp"] = &fsNode{kind: "dir", mode: 0o1777, owner: "root"}
|
||||
f.nodes[directory] = &fsNode{kind: "dir", mode: 0o700, owner: "root"}
|
||||
return directory, nil
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) touched(operations ...string) []fsOperation {
|
||||
wanted := map[string]bool{}
|
||||
for _, name := range operations {
|
||||
wanted[name] = true
|
||||
}
|
||||
var touched []fsOperation
|
||||
for _, operation := range f.ops {
|
||||
if wanted[operation.name] {
|
||||
touched = append(touched, operation)
|
||||
}
|
||||
}
|
||||
return touched
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) resolvedOutside(allowed ...string) []string {
|
||||
var outside []string
|
||||
for _, operation := range f.ops {
|
||||
if operation.resolved == "" {
|
||||
continue
|
||||
}
|
||||
inside := false
|
||||
for _, directory := range allowed {
|
||||
if operation.resolved == directory || strings.HasPrefix(operation.resolved, directory+"/") {
|
||||
inside = true
|
||||
}
|
||||
}
|
||||
if !inside {
|
||||
outside = append(outside, operation.name+" "+operation.resolved)
|
||||
}
|
||||
}
|
||||
sort.Strings(outside)
|
||||
return outside
|
||||
}
|
||||
|
||||
func (f *fakeHelperFS) operationCount(name string) int {
|
||||
return f.counts[name]
|
||||
}
|
||||
Reference in new issue
Block a user