2019-04-03 20:13:39 +01:00
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package versioncontrol
import (
"context"
2019-10-16 09:16:59 +01:00
"encoding/hex"
2019-04-03 20:13:39 +01:00
"encoding/json"
2020-04-16 16:50:22 +01:00
"errors"
2020-12-03 02:14:43 +00:00
"fmt"
2019-04-03 20:13:39 +01:00
"net"
"net/http"
2019-10-16 09:16:59 +01:00
"reflect"
2020-12-03 02:14:43 +00:00
"strings"
2023-05-16 21:25:22 +01:00
"sync"
"time"
2019-04-03 20:13:39 +01:00
2020-12-03 02:14:43 +00:00
"github.com/gorilla/mux"
2019-04-03 20:13:39 +01:00
"github.com/zeebo/errs"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
2019-12-27 11:48:47 +00:00
"storj.io/common/errs2"
2023-05-16 21:25:22 +01:00
"storj.io/common/sync2"
2020-03-23 19:30:31 +00:00
"storj.io/private/version"
2019-04-03 20:13:39 +01:00
)
2019-10-16 09:16:59 +01:00
// seedLength is the number of bytes in a rollout seed.
const seedLength = 32
var (
// RolloutErr defines the rollout config error class.
2021-04-28 09:06:17 +01:00
RolloutErr = errs . Class ( "rollout config" )
2019-10-16 09:16:59 +01:00
// EmptySeedErr is used when the rollout contains an empty seed value.
EmptySeedErr = RolloutErr . New ( "empty seed" )
)
// Config is all the configuration parameters for a Version Control Server.
2019-04-03 20:13:39 +01:00
type Config struct {
2023-05-16 21:25:22 +01:00
Address string ` user:"true" help:"public address to listen on" default:":8080" `
SafeRate float64 ` user:"true" help:"the safe daily fractional increase for a rollout (a value of .5 means 0 to 50% in 24 hours). 0 means immediate rollout." default:".2" `
RegenInterval time . Duration ` user:"true" help:"how long to go between recalculating the current cursors. 0 means on demand." default:"5m" `
2019-10-21 11:50:59 +01:00
Versions OldVersionConfig
2019-09-11 15:01:36 +01:00
2019-10-21 11:50:59 +01:00
Binary ProcessesConfig
2019-04-03 20:13:39 +01:00
}
2019-10-21 11:50:59 +01:00
// OldVersionConfig provides a list of allowed Versions per process.
2020-10-13 13:47:55 +01:00
//
// NB: use `ProcessesConfig` for newer code instead.
2019-10-21 11:50:59 +01:00
type OldVersionConfig struct {
2019-04-03 20:13:39 +01:00
Satellite string ` user:"true" help:"Allowed Satellite Versions" default:"v0.0.1" `
Storagenode string ` user:"true" help:"Allowed Storagenode Versions" default:"v0.0.1" `
Uplink string ` user:"true" help:"Allowed Uplink Versions" default:"v0.0.1" `
Gateway string ` user:"true" help:"Allowed Gateway Versions" default:"v0.0.1" `
2019-05-01 17:21:51 +01:00
Identity string ` user:"true" help:"Allowed Identity Versions" default:"v0.0.1" `
2019-04-03 20:13:39 +01:00
}
2019-10-21 11:50:59 +01:00
// ProcessesConfig represents versions configuration for all processes.
type ProcessesConfig struct {
2019-10-31 12:27:53 +00:00
Satellite ProcessConfig
Storagenode ProcessConfig
StoragenodeUpdater ProcessConfig
Uplink ProcessConfig
Gateway ProcessConfig
Identity ProcessConfig
2019-09-11 15:01:36 +01:00
}
2019-10-21 11:50:59 +01:00
// ProcessConfig represents versions configuration for a single process.
type ProcessConfig struct {
Minimum VersionConfig
Suggested VersionConfig
Rollout RolloutConfig
2019-09-11 15:01:36 +01:00
}
2019-10-21 11:50:59 +01:00
// VersionConfig single version configuration.
type VersionConfig struct {
2019-09-11 15:01:36 +01:00
Version string ` user:"true" help:"peer version" default:"v0.0.1" `
URL string ` user:"true" help:"URL for specific binary" default:"" `
}
2019-10-21 11:50:59 +01:00
// RolloutConfig represents the state of a version rollout configuration of a process.
type RolloutConfig struct {
2023-05-16 21:25:22 +01:00
Seed string ` user:"true" help:"random 32 byte, hex-encoded string" `
PreviousCursor int ` user:"true" help:"prior configuration's cursor value. if 100%, will be capped at the current cursor." default:"100" `
Cursor int ` user:"true" help:"percentage of nodes which should roll-out to the suggested version" default:"0" `
}
// response invariant: the struct or its data is never modified after creation.
type response struct {
versions version . AllowedVersions
// serialized contains the byte version of current allowed versions.
serialized [ ] byte
2019-10-16 09:16:59 +01:00
}
2019-04-03 20:13:39 +01:00
// Peer is the representation of a VersionControl Server.
2019-10-20 08:56:23 +01:00
//
// architecture: Peer
2019-04-03 20:13:39 +01:00
type Peer struct {
// core dependencies
Log * zap . Logger
// Web server
Server struct {
Endpoint http . Server
Listener net . Listener
}
2020-12-03 02:14:43 +00:00
2023-05-16 21:25:22 +01:00
config Config
initTime time . Time
2019-04-03 20:13:39 +01:00
2023-05-16 21:25:22 +01:00
regenLoop * sync2 . Cycle
mu sync . Mutex
response * response
2019-04-03 20:13:39 +01:00
}
// New creates a new VersionControl Server.
func New ( log * zap . Logger , config * Config ) ( peer * Peer , err error ) {
2019-10-16 09:16:59 +01:00
if err := config . Binary . ValidateRollouts ( log ) ; err != nil {
return nil , RolloutErr . Wrap ( err )
}
2019-04-03 20:13:39 +01:00
peer = & Peer {
2023-05-16 21:25:22 +01:00
Log : log ,
config : * config ,
initTime : time . Now ( ) ,
regenLoop : sync2 . NewCycle ( config . RegenInterval ) ,
2019-04-03 20:13:39 +01:00
}
2023-05-16 21:25:22 +01:00
err = peer . updateResponse ( )
2019-07-02 16:28:06 +01:00
if err != nil {
2023-05-16 21:25:22 +01:00
return nil , err
2019-07-02 16:28:06 +01:00
}
2019-04-03 20:13:39 +01:00
2023-05-16 21:25:22 +01:00
{
router := mux . NewRouter ( )
router . HandleFunc ( "/" , peer . versionHandle ) . Methods ( http . MethodGet )
router . HandleFunc ( "/processes/{service}/{version}/url" , peer . processURLHandle ) . Methods ( http . MethodGet )
peer . Server . Endpoint = http . Server {
Handler : router ,
}
peer . Server . Listener , err = net . Listen ( "tcp" , config . Address )
if err != nil {
return nil , errs . Combine ( err , peer . Close ( ) )
}
}
return peer , nil
}
func ( peer * Peer ) getResponse ( ) * response {
if peer . config . RegenInterval <= 0 && peer . config . SafeRate > 0 {
// generate on demand.
if err := peer . updateResponse ( ) ; err != nil {
peer . Log . Error ( "Error updating config." , zap . Error ( err ) )
}
}
peer . mu . Lock ( )
defer peer . mu . Unlock ( )
return peer . response
}
func ( peer * Peer ) updateResponse ( ) ( err error ) {
response , err := peer . config . generateResponse ( peer . initTime )
2019-07-02 16:28:06 +01:00
if err != nil {
2023-05-16 21:25:22 +01:00
peer . Log . Error ( "Error updating response." , zap . Error ( err ) )
return err
2019-07-02 16:28:06 +01:00
}
2019-04-03 20:13:39 +01:00
2023-05-16 21:25:22 +01:00
peer . Log . Debug ( "Setting version info." , zap . ByteString ( "Value" , response . serialized ) )
peer . mu . Lock ( )
defer peer . mu . Unlock ( )
peer . response = response
return nil
}
func ( config * Config ) generateResponse ( initTime time . Time ) ( rv * response , err error ) {
rv = & response { }
// Convert each Service's VersionConfig String to SemVer
rv . versions . Satellite , err = version . NewOldSemVer ( config . Versions . Satellite )
2019-07-02 16:28:06 +01:00
if err != nil {
2023-05-16 21:25:22 +01:00
return nil , err
2019-07-02 16:28:06 +01:00
}
2019-04-03 20:13:39 +01:00
2023-05-16 21:25:22 +01:00
rv . versions . Storagenode , err = version . NewOldSemVer ( config . Versions . Storagenode )
2019-07-02 16:28:06 +01:00
if err != nil {
2023-05-16 21:25:22 +01:00
return nil , err
2019-07-02 16:28:06 +01:00
}
2019-04-03 20:13:39 +01:00
2023-05-16 21:25:22 +01:00
rv . versions . Uplink , err = version . NewOldSemVer ( config . Versions . Uplink )
2019-07-02 16:28:06 +01:00
if err != nil {
2023-05-16 21:25:22 +01:00
return nil , err
2019-07-02 16:28:06 +01:00
}
2019-05-01 17:21:51 +01:00
2023-05-16 21:25:22 +01:00
rv . versions . Gateway , err = version . NewOldSemVer ( config . Versions . Gateway )
2019-10-16 09:16:59 +01:00
if err != nil {
2023-05-16 21:25:22 +01:00
return nil , err
2019-10-16 09:16:59 +01:00
}
2019-09-11 15:01:36 +01:00
2023-05-16 21:25:22 +01:00
rv . versions . Identity , err = version . NewOldSemVer ( config . Versions . Identity )
2019-10-16 09:16:59 +01:00
if err != nil {
2023-05-16 21:25:22 +01:00
return nil , err
2019-10-16 09:16:59 +01:00
}
2023-05-16 21:25:22 +01:00
rv . versions . Processes . Satellite , err = config . configToProcess ( initTime , config . Binary . Satellite )
2019-10-31 12:27:53 +00:00
if err != nil {
return nil , RolloutErr . Wrap ( err )
}
2023-05-16 21:25:22 +01:00
rv . versions . Processes . Storagenode , err = config . configToProcess ( initTime , config . Binary . Storagenode )
2019-10-16 09:16:59 +01:00
if err != nil {
return nil , RolloutErr . Wrap ( err )
}
2019-04-03 20:13:39 +01:00
2023-05-16 21:25:22 +01:00
rv . versions . Processes . StoragenodeUpdater , err = config . configToProcess ( initTime , config . Binary . StoragenodeUpdater )
2019-10-16 09:16:59 +01:00
if err != nil {
return nil , RolloutErr . Wrap ( err )
}
2023-05-16 21:25:22 +01:00
rv . versions . Processes . Uplink , err = config . configToProcess ( initTime , config . Binary . Uplink )
2019-10-16 09:16:59 +01:00
if err != nil {
return nil , RolloutErr . Wrap ( err )
}
2023-05-16 21:25:22 +01:00
rv . versions . Processes . Gateway , err = config . configToProcess ( initTime , config . Binary . Gateway )
2019-04-03 20:13:39 +01:00
if err != nil {
2020-10-13 14:49:33 +01:00
return nil , RolloutErr . Wrap ( err )
2019-04-03 20:13:39 +01:00
}
2023-05-16 21:25:22 +01:00
rv . versions . Processes . Identity , err = config . configToProcess ( initTime , config . Binary . Identity )
if err != nil {
return nil , RolloutErr . Wrap ( err )
}
2020-12-03 02:14:43 +00:00
2023-05-16 21:25:22 +01:00
rv . serialized , err = json . Marshal ( rv . versions )
if err != nil {
return nil , RolloutErr . Wrap ( err )
2019-04-03 20:13:39 +01:00
}
2023-05-16 21:25:22 +01:00
return rv , nil
2020-12-03 02:14:43 +00:00
}
// versionHandle handles all process versions request.
func ( peer * Peer ) versionHandle ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
2023-05-16 21:25:22 +01:00
_ , err := w . Write ( peer . getResponse ( ) . serialized )
2019-04-03 20:13:39 +01:00
if err != nil {
2020-12-03 02:14:43 +00:00
peer . Log . Error ( "Error writing response to client." , zap . Error ( err ) )
}
}
// processURLHandle handles process binary url resolving.
func ( peer * Peer ) processURLHandle ( w http . ResponseWriter , r * http . Request ) {
params := mux . Vars ( r )
service := params [ "service" ]
versionType := params [ "version" ]
2023-05-16 21:25:22 +01:00
response := peer . getResponse ( )
2020-12-03 02:14:43 +00:00
var process version . Process
switch service {
case "satellite" :
2023-05-16 21:25:22 +01:00
process = response . versions . Processes . Satellite
2020-12-03 02:14:43 +00:00
case "storagenode" :
2023-05-16 21:25:22 +01:00
process = response . versions . Processes . Storagenode
2020-12-03 02:14:43 +00:00
case "storagenode-updater" :
2023-05-16 21:25:22 +01:00
process = response . versions . Processes . StoragenodeUpdater
2020-12-03 02:14:43 +00:00
case "uplink" :
2023-05-16 21:25:22 +01:00
process = response . versions . Processes . Uplink
2020-12-03 02:14:43 +00:00
case "gateway" :
2023-05-16 21:25:22 +01:00
process = response . versions . Processes . Gateway
2020-12-03 02:14:43 +00:00
case "identity" :
2023-05-16 21:25:22 +01:00
process = response . versions . Processes . Identity
2020-12-03 02:14:43 +00:00
default :
http . Error ( w , "service does not exists" , http . StatusNotFound )
return
}
var url string
switch versionType {
case "minimum" :
url = process . Minimum . URL
case "suggested" :
url = process . Suggested . URL
default :
http . Error ( w , "invalid version, should be minimum or suggested" , http . StatusBadRequest )
return
}
query := r . URL . Query ( )
os := query . Get ( "os" )
if os == "" {
http . Error ( w , "goos is not specified" , http . StatusBadRequest )
return
}
arch := query . Get ( "arch" )
if arch == "" {
http . Error ( w , "goarch is not specified" , http . StatusBadRequest )
return
}
if scheme , ok := isBinarySupported ( service , os , arch ) ; ! ok {
http . Error ( w , fmt . Sprintf ( "binary scheme %s is not supported" , scheme ) , http . StatusNotFound )
return
}
url = strings . Replace ( url , "{os}" , os , 1 )
url = strings . Replace ( url , "{arch}" , arch , 1 )
w . Header ( ) . Set ( "Content-Type" , "text/plain" )
_ , err := w . Write ( [ ] byte ( url ) )
if err != nil {
peer . Log . Error ( "Error writing response to client." , zap . Error ( err ) )
2019-04-03 20:13:39 +01:00
}
}
// Run runs versioncontrol server until it's either closed or it errors.
func ( peer * Peer ) Run ( ctx context . Context ) ( err error ) {
ctx , cancel := context . WithCancel ( ctx )
var group errgroup . Group
group . Go ( func ( ) error {
<- ctx . Done ( )
2019-04-17 11:09:44 +01:00
return errs2 . IgnoreCanceled ( peer . Server . Endpoint . Shutdown ( ctx ) )
2019-04-03 20:13:39 +01:00
} )
group . Go ( func ( ) error {
defer cancel ( )
2020-04-13 10:31:17 +01:00
peer . Log . Info ( "Versioning server started." , zap . String ( "Address" , peer . Addr ( ) ) )
2020-04-16 16:50:22 +01:00
err := peer . Server . Endpoint . Serve ( peer . Server . Listener )
if errs2 . IsCanceled ( err ) || errors . Is ( err , http . ErrServerClosed ) {
err = nil
}
return err
2019-04-03 20:13:39 +01:00
} )
2023-05-16 21:25:22 +01:00
if peer . config . RegenInterval > 0 {
group . Go ( func ( ) error {
defer cancel ( )
return peer . regenLoop . Run ( ctx , func ( ctx context . Context ) error {
return peer . updateResponse ( )
} )
} )
}
2019-04-03 20:13:39 +01:00
return group . Wait ( )
}
// Close closes all the resources.
func ( peer * Peer ) Close ( ) ( err error ) {
return peer . Server . Endpoint . Close ( )
}
// Addr returns the public address.
func ( peer * Peer ) Addr ( ) string { return peer . Server . Listener . Addr ( ) . String ( ) }
2019-09-11 15:01:36 +01:00
2019-10-16 09:16:59 +01:00
// ValidateRollouts validates the rollout field of each field in the Versions struct.
2019-10-21 11:50:59 +01:00
func ( versions ProcessesConfig ) ValidateRollouts ( log * zap . Logger ) error {
2019-10-16 09:16:59 +01:00
value := reflect . ValueOf ( versions )
fieldCount := value . NumField ( )
validationErrs := errs . Group { }
2019-10-20 08:56:23 +01:00
for i := 0 ; i < fieldCount ; i ++ {
2019-10-21 11:50:59 +01:00
binary , ok := value . Field ( i ) . Interface ( ) . ( ProcessConfig )
2019-10-16 09:16:59 +01:00
if ! ok {
log . Warn ( "non-binary field in versions config struct" , zap . String ( "field name" , value . Type ( ) . Field ( i ) . Name ) )
continue
}
if err := binary . Rollout . Validate ( ) ; err != nil {
2020-07-14 14:04:38 +01:00
if errors . Is ( err , EmptySeedErr ) {
2019-10-16 09:16:59 +01:00
log . Warn ( err . Error ( ) , zap . String ( "binary" , value . Type ( ) . Field ( i ) . Name ) )
continue
}
validationErrs . Add ( err )
}
}
return validationErrs . Err ( )
}
// Validate validates the rollout seed and cursor config values.
2019-10-21 11:50:59 +01:00
func ( rollout RolloutConfig ) Validate ( ) error {
2019-10-16 09:16:59 +01:00
seedLen := len ( rollout . Seed )
if seedLen == 0 {
return EmptySeedErr
}
if seedLen != hex . EncodedLen ( seedLength ) {
return RolloutErr . New ( "invalid seed length: %d" , seedLen )
}
if rollout . Cursor < 0 || rollout . Cursor > 100 {
return RolloutErr . New ( "invalid cursor percentage: %d" , rollout . Cursor )
}
2023-05-16 21:25:22 +01:00
if rollout . PreviousCursor < 0 || rollout . PreviousCursor > 100 {
return RolloutErr . New ( "invalid previous cursor percentage: %d" , rollout . PreviousCursor )
}
2019-10-16 09:16:59 +01:00
if _ , err := hex . DecodeString ( rollout . Seed ) ; err != nil {
2023-05-16 21:25:22 +01:00
return RolloutErr . New ( "invalid seed: %q" , rollout . Seed )
2019-10-16 09:16:59 +01:00
}
return nil
}
2023-05-16 21:25:22 +01:00
func ( config * Config ) configToProcess ( initTime time . Time , binary ProcessConfig ) ( version . Process , error ) {
currentPercent := calculateRolloutCursor ( initTime , binary , config . SafeRate )
2019-10-16 09:16:59 +01:00
process := version . Process {
2019-09-11 15:01:36 +01:00
Minimum : version . Version {
Version : binary . Minimum . Version ,
URL : binary . Minimum . URL ,
} ,
Suggested : version . Version {
Version : binary . Suggested . Version ,
URL : binary . Suggested . URL ,
} ,
2019-10-16 09:16:59 +01:00
Rollout : version . Rollout {
2023-05-16 21:25:22 +01:00
Cursor : version . PercentageToCursor ( int ( currentPercent ) ) ,
2019-10-16 09:16:59 +01:00
} ,
}
seedBytes , err := hex . DecodeString ( binary . Rollout . Seed )
if err != nil {
return version . Process { } , err
2019-09-11 15:01:36 +01:00
}
2019-10-16 09:16:59 +01:00
copy ( process . Rollout . Seed [ : ] , seedBytes )
return process , nil
2019-09-11 15:01:36 +01:00
}
2023-05-16 21:25:22 +01:00
func calculateRolloutCursor ( initTime time . Time , binary ProcessConfig , safeRate float64 ) float64 {
targetPercent := float64 ( binary . Rollout . Cursor )
previousPercent := float64 ( binary . Rollout . PreviousCursor )
if previousPercent > targetPercent {
previousPercent = targetPercent
}
elapsed := time . Since ( initTime )
currentPercent := targetPercent
safePercentPerDay := safeRate * 100
if safePercentPerDay > 0 {
// first calculate targetTime:
targetTimeInDaysFromNow := ( targetPercent - previousPercent ) / safePercentPerDay
targetTime := time . Duration ( targetTimeInDaysFromNow * 24 * float64 ( time . Hour ) )
if targetTime > 0 {
// now calculate the current percent based on how close targetTime is.
currentPercent = clampedLinearInterp ( float64 ( elapsed ) / float64 ( targetTime ) , previousPercent , targetPercent )
}
}
return currentPercent
}
func clampedLinearInterp ( frac , low , high float64 ) float64 {
v := ( high - low ) * frac + low
if v < low {
return low
}
if v > high {
return high
}
return v
}