159 lines
3.7 KiB
Go
159 lines
3.7 KiB
Go
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
|
|
}
|