diff --git a/versioncontrol/binaries.go b/versioncontrol/binaries.go new file mode 100644 index 000000000..9669f7e66 --- /dev/null +++ b/versioncontrol/binaries.go @@ -0,0 +1,40 @@ +// Copyright (C) 2020 Storj Labs, Inc. +// See LICENSE for copying information. + +package versioncontrol + +// SupportedBinaries list of supported binary schemes. +var SupportedBinaries = []string{ + "identity_darwin_amd64", + "identity_freebsd_amd64", + "identity_linux_amd64", + "identity_linux_arm", + "identity_linux_arm64", + "identity_windows_amd64", + "storagenode-updater_linux_amd64", + "storagenode-updater_linux_arm", + "storagenode-updater_linux_arm64", + "storagenode-updater_windows_amd64", + "storagenode_freebsd_amd64", + "storagenode_linux_amd64", + "storagenode_linux_arm", + "storagenode_linux_arm64", + "storagenode_windows_amd64", + "uplink_darwin_amd64", + "uplink_freebsd_amd64", + "uplink_linux_amd64", + "uplink_linux_arm", + "uplink_linux_arm64", + "uplink_windows_amd64", +} + +// isBinarySupported check if binary scheme matching provided service, os and arch is supported. +func isBinarySupported(service, os, arch string) (string, bool) { + binary := service + "_" + os + "_" + arch + for _, supportedBinary := range SupportedBinaries { + if binary == supportedBinary { + return binary, true + } + } + return binary, false +} diff --git a/versioncontrol/peer.go b/versioncontrol/peer.go index 71d284795..3dd9c7b67 100644 --- a/versioncontrol/peer.go +++ b/versioncontrol/peer.go @@ -8,10 +8,13 @@ import ( "encoding/hex" "encoding/json" "errors" + "fmt" "net" "net/http" "reflect" + "strings" + "github.com/gorilla/mux" "github.com/zeebo/errs" "go.uber.org/zap" "golang.org/x/sync/errgroup" @@ -90,27 +93,13 @@ type Peer struct { Endpoint http.Server Listener net.Listener } + Versions version.AllowedVersions // response contains the byte version of current allowed versions response []byte } -// HandleGet contains the request handler for the version control web server. -func (peer *Peer) HandleGet(w http.ResponseWriter, r *http.Request) { - // Only handle GET Requests - if r.Method != http.MethodGet { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - w.Header().Set("Content-Type", "application/json") - _, err := w.Write(peer.response) - if err != nil { - peer.Log.Error("Error writing response to client.", zap.Error(err)) - } -} - // New creates a new VersionControl Server. func New(log *zap.Logger, config *Config) (peer *Peer, err error) { if err := config.Binary.ValidateRollouts(log); err != nil { @@ -147,7 +136,6 @@ func New(log *zap.Logger, config *Config) (peer *Peer, err error) { return &Peer{}, err } - peer.Versions.Processes = version.Processes{} peer.Versions.Processes.Satellite, err = configToProcess(config.Binary.Satellite) if err != nil { return nil, RolloutErr.Wrap(err) @@ -186,22 +174,101 @@ func New(log *zap.Logger, config *Config) (peer *Peer, err error) { peer.Log.Debug("Setting version info.", zap.ByteString("Value", peer.response)) - mux := http.NewServeMux() - mux.HandleFunc("/", peer.HandleGet) - peer.Server.Endpoint = http.Server{ - Handler: mux, + { + 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()) + } } - peer.Server.Listener, err = net.Listen("tcp", config.Address) - if err != nil { - return nil, errs.Combine(err, peer.Close()) - } return peer, nil } +// versionHandle handles all process versions request. +func (peer *Peer) versionHandle(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + _, err := w.Write(peer.response) + if err != nil { + 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"] + + var process version.Process + switch service { + case "satellite": + process = peer.Versions.Processes.Satellite + case "storagenode": + process = peer.Versions.Processes.Storagenode + case "storagenode-updater": + process = peer.Versions.Processes.StoragenodeUpdater + case "uplink": + process = peer.Versions.Processes.Uplink + case "gateway": + process = peer.Versions.Processes.Gateway + case "identity": + process = peer.Versions.Processes.Identity + 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)) + } +} + // 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 diff --git a/versioncontrol/peer_test.go b/versioncontrol/peer_test.go index ef02a2e24..ea0b152d1 100644 --- a/versioncontrol/peer_test.go +++ b/versioncontrol/peer_test.go @@ -4,14 +4,20 @@ package versioncontrol_test import ( + "context" "encoding/hex" + "io/ioutil" "math/rand" + "net/http" "reflect" + "strings" "testing" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" + "golang.org/x/sync/errgroup" + "storj.io/common/testcontext" "storj.io/storj/versioncontrol" ) @@ -65,6 +71,150 @@ var rolloutErrScenarios = []struct { }, } +func TestPeerEndpoint(t *testing.T) { + minimumVersion := "v0.0.1" + suggestedVersion := "v0.0.2" + + createURL := func(process, version string) string { + urlTmpl := "http://example.com/{version}/{process}_{os}_{arch}" + url := strings.Replace(urlTmpl, "{version}", version, 1) + url = strings.Replace(url, "{process}", process, 1) + return url + } + + config := &versioncontrol.Config{ + Address: "127.0.0.1:0", + Versions: versioncontrol.OldVersionConfig{ + Satellite: minimumVersion, + Storagenode: minimumVersion, + Uplink: minimumVersion, + Gateway: minimumVersion, + Identity: minimumVersion, + }, + Binary: versioncontrol.ProcessesConfig{ + Storagenode: versioncontrol.ProcessConfig{ + Minimum: versioncontrol.VersionConfig{ + Version: minimumVersion, + URL: createURL("storagenode", minimumVersion), + }, + Suggested: versioncontrol.VersionConfig{ + Version: suggestedVersion, + URL: createURL("storagenode", suggestedVersion), + }, + }, + StoragenodeUpdater: versioncontrol.ProcessConfig{ + Minimum: versioncontrol.VersionConfig{ + Version: minimumVersion, + URL: createURL("storagenode-updater", minimumVersion), + }, + Suggested: versioncontrol.VersionConfig{ + Version: suggestedVersion, + URL: createURL("storagenode-updater", suggestedVersion), + }, + }, + Uplink: versioncontrol.ProcessConfig{ + Minimum: versioncontrol.VersionConfig{ + Version: minimumVersion, + URL: createURL("uplink", minimumVersion), + }, + Suggested: versioncontrol.VersionConfig{ + Version: suggestedVersion, + URL: createURL("uplink", suggestedVersion), + }, + }, + Gateway: versioncontrol.ProcessConfig{ + Minimum: versioncontrol.VersionConfig{ + Version: minimumVersion, + URL: createURL("gateway", minimumVersion), + }, + Suggested: versioncontrol.VersionConfig{ + Version: suggestedVersion, + URL: createURL("gateway", suggestedVersion), + }, + }, + Identity: versioncontrol.ProcessConfig{ + Minimum: versioncontrol.VersionConfig{ + Version: minimumVersion, + URL: createURL("identity", minimumVersion), + }, + Suggested: versioncontrol.VersionConfig{ + Version: suggestedVersion, + URL: createURL("identity", suggestedVersion), + }, + }, + }, + } + + log := zaptest.NewLogger(t) + + peer, err := versioncontrol.New(log, config) + require.NoError(t, err) + require.NotNil(t, peer) + + testCtx := testcontext.New(t) + ctx, cancel := context.WithCancel(testCtx) + + var wg errgroup.Group + wg.Go(func() error { + return peer.Run(ctx) + }) + + defer testCtx.Check(peer.Close) + defer cancel() + + baseURL := "http://" + peer.Addr() + + t.Run("resolve process url", func(t *testing.T) { + queryTmpl := "processes/{service}/{version}/url?os={os}&arch={arch}" + + urls := make(map[string]string) + for _, supportedBinary := range versioncontrol.SupportedBinaries { + splitted := strings.SplitN(supportedBinary, "_", 3) + + service := splitted[0] + os := splitted[1] + arch := splitted[2] + + for _, versionType := range []string{"minimum", "suggested"} { + query := strings.Replace(queryTmpl, "{service}", service, 1) + query = strings.Replace(query, "{version}", versionType, 1) + query = strings.Replace(query, "{os}", os, 1) + query = strings.Replace(query, "{arch}", arch, 1) + + var url string + switch versionType { + case "minimum": + url = createURL(service, minimumVersion) + case "suggested": + url = createURL(service, suggestedVersion) + } + + url = strings.Replace(url, "{os}", os, 1) + url = strings.Replace(url, "{arch}", arch, 1) + urls[query] = url + } + } + + for query, url := range urls { + query, url := query, url + + t.Run(query, func(t *testing.T) { + resp, err := http.Get(baseURL + "/" + query) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + require.NotNil(t, b) + require.NoError(t, resp.Body.Close()) + + require.Equal(t, url, string(b)) + log.Debug(string(b)) + }) + } + }) +} + func TestPeer_Run(t *testing.T) { testVersion := "v0.0.1" testServiceVersions := versioncontrol.OldVersionConfig{