storj/private/version/checker/service.go
2020-12-05 17:01:42 +01:00

176 lines
5.2 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package checker
import (
"context"
"fmt"
"sync"
"time"
"go.uber.org/zap"
"storj.io/common/sync2"
"storj.io/private/version"
)
// Config contains the necessary Information to check the Software Version.
type Config struct {
ClientConfig
CheckInterval time.Duration `help:"Interval to check the version" default:"0h15m0s"`
}
// Service contains the information and variables to ensure the Software is up to date.
//
// architecture: Service
type Service struct {
log *zap.Logger
config Config
client *Client
Info version.Info
service string
checked sync2.Fence
mu sync.Mutex
allowed bool
acceptedVersion version.SemVer
}
// NewService creates a Version Check Client with default configuration.
func NewService(log *zap.Logger, config Config, info version.Info, service string) (client *Service) {
return &Service{
log: log,
config: config,
client: New(config.ClientConfig),
Info: info,
service: service,
allowed: true,
}
}
// CheckProcessVersion is not meant to be used for peers but is meant to be
// used for other utilities.
func CheckProcessVersion(ctx context.Context, log *zap.Logger, config Config, info version.Info, service string) (err error) {
defer mon.Task()(&ctx)(&err)
_, err = NewService(log, config, info, service).CheckVersion(ctx)
return err
}
// IsAllowed returns whether if the Service is allowed to operate or not.
func (service *Service) IsAllowed(ctx context.Context) (version.SemVer, bool) {
if !service.checked.Wait(ctx) {
return version.SemVer{}, false
}
service.mu.Lock()
defer service.mu.Unlock()
return service.acceptedVersion, service.allowed
}
// CheckVersion checks to make sure the version is still relevant and returns suggested version, returning an error if not.
func (service *Service) CheckVersion(ctx context.Context) (latest version.SemVer, err error) {
defer mon.Task()(&ctx)(&err)
latest, allowed := service.checkVersion(ctx)
if !allowed {
return latest, fmt.Errorf("outdated software version (%s), please update", service.Info.Version.String())
}
return latest, nil
}
// checkVersion checks if the client is running latest/allowed version.
func (service *Service) checkVersion(ctx context.Context) (_ version.SemVer, allowed bool) {
var err error
defer mon.Task()(&ctx)(&err)
var minimum version.SemVer
defer func() {
service.mu.Lock()
service.allowed = allowed
if err == nil {
if minimum.Compare(service.acceptedVersion) >= 0 {
service.acceptedVersion = minimum
}
}
service.mu.Unlock()
service.checked.Release()
}()
allowedVersions, err := service.client.All(ctx)
if err != nil {
return service.acceptedVersion, true
}
suggestedVersion, err := allowedVersions.Processes.Storagenode.Suggested.SemVer()
if err != nil {
return service.acceptedVersion, true
}
service.mu.Lock()
isVersionActual := suggestedVersion.Compare(service.acceptedVersion)
service.mu.Unlock()
if isVersionActual < 0 {
minimum = service.Info.Version
return service.acceptedVersion, true
}
if !service.Info.Release {
minimum = service.Info.Version
return suggestedVersion, true
}
minimumOld, err := service.client.OldMinimum(ctx, service.service)
if err != nil {
// Log about the error, but dont crash the Service and allow further operation
service.log.Error("Failed to do periodic version check.", zap.Error(err))
return suggestedVersion, true
}
minimum, err = version.NewSemVer(minimumOld.String())
if err != nil {
service.log.Error("Failed to convert old sem version to sem version.")
return suggestedVersion, true
}
service.log.Debug("Allowed minimum version from control server.", zap.Stringer("Minimum Version", minimum.Version))
if isAcceptedVersion(service.Info.Version, minimumOld) {
service.log.Debug("Running on allowed version.", zap.Stringer("Version", service.Info.Version.Version))
return suggestedVersion, true
}
service.log.Warn("version not allowed/outdated",
zap.Stringer("current version", service.Info.Version.Version),
zap.Stringer("minimum allowed version", minimumOld),
)
return suggestedVersion, false
}
// GetCursor returns storagenode rollout cursor value.
func (service *Service) GetCursor(ctx context.Context) (_ version.RolloutBytes, err error) {
allowedVersions, err := service.client.All(ctx)
if err != nil {
return version.RolloutBytes{}, err
}
return allowedVersions.Processes.Storagenode.Rollout.Cursor, nil
}
// SetAcceptedVersion changes accepted version to specific for tests.
func (service *Service) SetAcceptedVersion(version version.SemVer) {
service.mu.Lock()
defer service.mu.Unlock()
service.acceptedVersion = version
}
// Checked returns whether the version has been updated.
func (service *Service) Checked() bool {
return service.checked.Released()
}
// isAcceptedVersion compares and checks if the passed version is greater/equal than the minimum required version.
func isAcceptedVersion(test version.SemVer, target version.OldSemVer) bool {
return test.Major > uint64(target.Major) || (test.Major == uint64(target.Major) && (test.Minor > uint64(target.Minor) || (test.Minor == uint64(target.Minor) && test.Patch >= uint64(target.Patch))))
}