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 }