storj/internal/version/version.go
Bryan White d61b3688f7 internal/version: fix OldMinimum to use old semver type (#3373)
* fix version checker type assertion

* more fixing
2019-10-25 13:24:23 +02:00

270 lines
7.1 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package version
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"reflect"
"strconv"
"strings"
"time"
"github.com/blang/semver"
"github.com/zeebo/errs"
"storj.io/storj/pkg/pb"
"storj.io/storj/pkg/storj"
)
const quote = byte('"')
var (
// VerError is the error class for version-related errors.
VerError = errs.Class("version error")
// the following fields are set by linker flags. if any of them
// are set and fail to parse, the program will fail to start
buildTimestamp string // unix seconds since epoch
buildCommitHash string
buildVersion string // semantic version format
buildRelease string // true/false
// Build is a struct containing all relevant build information associated with the binary
Build Info
)
// Info is the versioning information for a binary
type Info struct {
// sync/atomic cache
commitHashCRC uint32
Timestamp time.Time `json:"timestamp,omitempty"`
CommitHash string `json:"commitHash,omitempty"`
Version SemVer `json:"version"`
Release bool `json:"release,omitempty"`
}
// SemVer represents a semantic version.
// TODO: replace with semver.Version
type SemVer struct {
semver.Version
}
// OldSemVer represents a semantic version.
// NB: this will be deprecated in favor of `SemVer`; these structs marshal to JSON differently.
type OldSemVer struct {
Major int64 `json:"major"`
Minor int64 `json:"minor"`
Patch int64 `json:"patch"`
}
// AllowedVersions provides the Minimum SemVer per Service.
// TODO: I don't think this name is representative of what this struct now holds.
type AllowedVersions struct {
Satellite OldSemVer
Storagenode OldSemVer
Uplink OldSemVer
Gateway OldSemVer
Identity OldSemVer
Processes Processes `json:"processes"`
}
// Processes describes versions for each binary.
// TODO: this name is inconsistent with the versioncontrol server pkg's analogue, `Versions`.
type Processes struct {
Satellite Process `json:"satellite"`
Storagenode Process `json:"storagenode"`
Uplink Process `json:"uplink"`
Gateway Process `json:"gateway"`
Identity Process `json:"identity"`
}
// Process versions for specific binary.
type Process struct {
Minimum Version `json:"minimum"`
Suggested Version `json:"suggested"`
Rollout Rollout `json:"rollout"`
}
// Version represents version and download URL for binary.
type Version struct {
Version string `json:"version"`
URL string `json:"url"`
}
// Rollout represents the state of a version rollout.
type Rollout struct {
Seed RolloutBytes `json:"seed"`
Cursor RolloutBytes `json:"cursor"`
}
// RolloutBytes implements json un/marshalling using hex de/encoding.
type RolloutBytes [32]byte
// MarshalJSON hex-encodes RolloutBytes and pre/appends JSON string literal quotes.
func (rb RolloutBytes) MarshalJSON() ([]byte, error) {
zeroRolloutBytes := RolloutBytes{}
if bytes.Equal(rb[:], zeroRolloutBytes[:]) {
return []byte{quote, quote}, nil
}
hexBytes := make([]byte, hex.EncodedLen(len(rb)))
hex.Encode(hexBytes, rb[:])
encoded := append([]byte{quote}, hexBytes...)
encoded = append(encoded, quote)
return encoded, nil
}
// UnmarshalJSON drops the JSON string literal quotes and hex-decodes RolloutBytes .
func (rb *RolloutBytes) UnmarshalJSON(b []byte) error {
if _, err := hex.Decode(rb[:], b[1:len(b)-1]); err != nil {
return VerError.Wrap(err)
}
return nil
}
// NewSemVer parses a given version and returns an instance of SemVer or
// an error if unable to parse the version.
func NewSemVer(v string) (SemVer, error) {
ver, err := semver.ParseTolerant(v)
if err != nil {
return SemVer{}, VerError.Wrap(err)
}
return SemVer{
Version: ver,
}, nil
}
// NewOldSemVer parses a given version and returns an instance of OldSemVer or
// an error if unable to parse the version.
func NewOldSemVer(v string) (OldSemVer, error) {
ver, err := NewSemVer(v)
if err != nil {
return OldSemVer{}, err
}
return OldSemVer{
Major: int64(ver.Major),
Minor: int64(ver.Minor),
Patch: int64(ver.Patch),
}, nil
}
// Compare compare two versions, return -1 if compared version is greater, 0 if equal and 1 if less.
func (sem *SemVer) Compare(version SemVer) int {
return sem.Version.Compare(version.Version)
}
// String converts the SemVer struct to a more easy to handle string
func (sem *SemVer) String() (version string) {
return fmt.Sprintf("v%d.%d.%d", sem.Major, sem.Minor, sem.Patch)
}
// IsZero checks if the semantic version is its zero value.
func (sem SemVer) IsZero() bool {
return reflect.ValueOf(sem).IsZero()
}
func (old OldSemVer) String() string {
return fmt.Sprintf("v%d.%d.%d", old.Major, old.Minor, old.Patch)
}
// SemVer converts a version struct into a semantic version struct.
func (ver *Version) SemVer() (SemVer, error) {
return NewSemVer(ver.Version)
}
// New creates Version_Info from a json byte array
func New(data []byte) (v Info, err error) {
err = json.Unmarshal(data, &v)
return v, VerError.Wrap(err)
}
// IsZero checks if the version struct is its zero value.
func (info Info) IsZero() bool {
return reflect.ValueOf(info).IsZero()
}
// Marshal converts the existing Version Info to any json byte array
func (info Info) Marshal() ([]byte, error) {
data, err := json.Marshal(info)
if err != nil {
return nil, VerError.Wrap(err)
}
return data, nil
}
// Proto converts an Info struct to a pb.NodeVersion
// TODO: shouldn't we just use pb.NodeVersion everywhere? gogoproto will let
// us make it match Info.
func (info Info) Proto() (*pb.NodeVersion, error) {
return &pb.NodeVersion{
Version: info.Version.String(),
CommitHash: info.CommitHash,
Timestamp: info.Timestamp,
Release: info.Release,
}, nil
}
// PercentageToCursor calculates the cursor value for the given percentage of nodes which should update.
func PercentageToCursor(pct int) RolloutBytes {
// NB: convert the max value to a number, multiply by the percentage, convert back.
var maxInt, maskInt big.Int
var maxBytes RolloutBytes
for i := 0; i < len(maxBytes); i++ {
maxBytes[i] = 255
}
maxInt.SetBytes(maxBytes[:])
maskInt.Div(maskInt.Mul(&maxInt, big.NewInt(int64(pct))), big.NewInt(100))
var cursor RolloutBytes
copy(cursor[:], maskInt.Bytes())
return cursor
}
// ShouldUpdate checks if for the the given rollout state, a user with the given nodeID should update.
func ShouldUpdate(rollout Rollout, nodeID storj.NodeID) bool {
hash := hmac.New(sha256.New, rollout.Seed[:])
_, err := hash.Write(nodeID[:])
if err != nil {
panic(err)
}
return bytes.Compare(hash.Sum(nil), rollout.Cursor[:]) <= 0
}
func init() {
if buildVersion == "" && buildTimestamp == "" && buildCommitHash == "" && buildRelease == "" {
return
}
timestamp, err := strconv.ParseInt(buildTimestamp, 10, 64)
if err != nil {
panic(VerError.Wrap(err))
}
Build = Info{
Timestamp: time.Unix(timestamp, 0),
CommitHash: buildCommitHash,
Release: strings.ToLower(buildRelease) == "true",
}
sv, err := NewSemVer(buildVersion)
if err != nil {
panic(err)
}
Build.Version = sv
if Build.Timestamp.Unix() == 0 || Build.CommitHash == "" {
Build.Release = false
}
}