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,158 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
StateRestarting = "restarting"
|
||||
StateSucceeded = "succeeded"
|
||||
|
||||
ErrorKindRolledBack = "rolled_back"
|
||||
ErrorKindInstall = "install"
|
||||
|
||||
RestartKindUpdate = "update"
|
||||
RestartKindRestart = "restart"
|
||||
|
||||
OutcomeRunning = "running"
|
||||
OutcomeSucceeded = "succeeded"
|
||||
OutcomeRolledBack = "rolled_back"
|
||||
OutcomeFailed = "failed"
|
||||
|
||||
RunningResultMaxAge = 5 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRestartPending = errors.New("a restart is already pending or running")
|
||||
ErrRestartKindMismatch = errors.New("the requested restart does not match the current state")
|
||||
ErrNoRestartRequest = errors.New("there is no restart request")
|
||||
ErrSymlinkRefused = errors.New("a symbolic link was found where a regular file is required")
|
||||
ErrInvalidRestartTag = errors.New("the restart request names an invalid version")
|
||||
ErrHelperUnsupported = errors.New("the privileged helper needs Linux")
|
||||
)
|
||||
|
||||
var restartTagPattern = regexp.MustCompile(`^v[0-9]+(\.[0-9]+)*(-[0-9A-Za-z.]+)?$`)
|
||||
|
||||
type RestartRequest struct {
|
||||
Kind string `json:"kind"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
RequestedAt time.Time `json:"requested_at"`
|
||||
RequestedBy uint32 `json:"requested_by"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Outcome string `json:"outcome"`
|
||||
Kind string `json:"kind"`
|
||||
Tag string `json:"tag"`
|
||||
FromVersion string `json:"from_version"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
FinishedAt *time.Time `json:"finished_at"`
|
||||
}
|
||||
|
||||
type RunningInfo struct {
|
||||
Version string `json:"version"`
|
||||
Pid int `json:"pid"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
}
|
||||
|
||||
type HelperStatus struct {
|
||||
Version int `json:"version"`
|
||||
Service string `json:"service"`
|
||||
Binary string `json:"binary"`
|
||||
DataDir string `json:"data_dir"`
|
||||
InstalledAt time.Time `json:"installed_at"`
|
||||
}
|
||||
|
||||
type FileInfo struct {
|
||||
IsSymlink bool
|
||||
IsDir bool
|
||||
IsRegular bool
|
||||
}
|
||||
|
||||
type RestartRequestStore interface {
|
||||
Write(request RestartRequest) error
|
||||
Read() (RestartRequest, error)
|
||||
Delete() error
|
||||
}
|
||||
|
||||
type ResultStore interface {
|
||||
Read() (Result, bool)
|
||||
Write(result Result) error
|
||||
Delete() error
|
||||
}
|
||||
|
||||
type RunningInfoStore interface {
|
||||
Write(info RunningInfo) error
|
||||
Read() (RunningInfo, bool)
|
||||
}
|
||||
|
||||
type HelperFilesystem interface {
|
||||
Lstat(path string) (FileInfo, error)
|
||||
ReadFile(path string) ([]byte, error)
|
||||
CreateExclusive(path string, content []byte, mode uint32) error
|
||||
Copy(source string, destination string, mode uint32) error
|
||||
Rename(oldPath string, newPath string) error
|
||||
Remove(path string) error
|
||||
RemoveAll(path string) error
|
||||
Chmod(path string, mode uint32) error
|
||||
MakePrivateTempDir() (string, error)
|
||||
}
|
||||
|
||||
type ServiceControl interface {
|
||||
Restart() error
|
||||
Status() (bool, error)
|
||||
}
|
||||
|
||||
type HealthChecker interface {
|
||||
Healthy() bool
|
||||
}
|
||||
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
Sleep(duration time.Duration)
|
||||
}
|
||||
|
||||
func ValidateRestartTag(tag string, runningVersion string) error {
|
||||
if !restartTagPattern.MatchString(tag) {
|
||||
return ErrInvalidRestartTag
|
||||
}
|
||||
if _, err := ParseVersion(tag); err != nil || !IsNewer(runningVersion, tag) {
|
||||
return ErrInvalidRestartTag
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Result) IsRunning() bool {
|
||||
return r.Outcome == OutcomeRunning
|
||||
}
|
||||
|
||||
func (r Result) IsKnown() bool {
|
||||
switch r.Outcome {
|
||||
case OutcomeRunning, OutcomeSucceeded, OutcomeRolledBack, OutcomeFailed:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (i RunningInfo) IsNewerThan(previous RunningInfo, restartedAt time.Time) bool {
|
||||
return i.Pid != previous.Pid || i.StartedAt.After(restartedAt)
|
||||
}
|
||||
|
||||
const (
|
||||
RestartWindow = 60 * time.Second
|
||||
PollInterval = time.Second
|
||||
)
|
||||
|
||||
type SymlinkError struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (e SymlinkError) Error() string {
|
||||
return e.Path + " is a symbolic link"
|
||||
}
|
||||
|
||||
func (e SymlinkError) Is(target error) bool {
|
||||
return target == ErrSymlinkRefused
|
||||
}
|
||||
Reference in new issue
Block a user