From 21efaf1fca7a32e75a961713d231dbbd4a86c8c9 Mon Sep 17 00:00:00 2001 From: "James O. D. Hunt" Date: Wed, 16 Sep 2020 12:01:09 +0100 Subject: [PATCH] runtime: make kata-check check for newer release Update `kata-check` to see if there is a newer version available for download. Useful for users installing static packages (without a package manager). Fixes: #734. Signed-off-by: James O. D. Hunt --- src/runtime/Makefile | 4 +- src/runtime/cli/config-generated.go.in | 3 + src/runtime/cli/kata-check.go | 86 ++++- src/runtime/cli/release.go | 410 ++++++++++++++++++++ src/runtime/cli/release_test.go | 498 +++++++++++++++++++++++++ 5 files changed, 998 insertions(+), 3 deletions(-) create mode 100644 src/runtime/cli/release.go create mode 100644 src/runtime/cli/release_test.go diff --git a/src/runtime/Makefile b/src/runtime/Makefile index 768f76a691..f4c2bf661b 100644 --- a/src/runtime/Makefile +++ b/src/runtime/Makefile @@ -43,7 +43,8 @@ include $(ARCH_FILE) PROJECT_TYPE = kata PROJECT_NAME = Kata Containers PROJECT_TAG = kata-containers -PROJECT_URL = https://github.com/kata-containers +PROJECT_ORG = $(PROJECT_TAG) +PROJECT_URL = https://github.com/$(PROJECT_ORG) PROJECT_BUG_URL = $(PROJECT_URL)/kata-containers/issues/new # list of scripts to install @@ -628,6 +629,7 @@ $(GENERATED_FILES): %: %.in $(MAKEFILE_LIST) VERSION .git-commit -e "s|@PKGRUNDIR@|$(PKGRUNDIR)|g" \ -e "s|@NETMONPATH@|$(NETMONPATH)|g" \ -e "s|@PROJECT_BUG_URL@|$(PROJECT_BUG_URL)|g" \ + -e "s|@PROJECT_ORG@|$(PROJECT_ORG)|g" \ -e "s|@PROJECT_URL@|$(PROJECT_URL)|g" \ -e "s|@PROJECT_NAME@|$(PROJECT_NAME)|g" \ -e "s|@PROJECT_TAG@|$(PROJECT_TAG)|g" \ diff --git a/src/runtime/cli/config-generated.go.in b/src/runtime/cli/config-generated.go.in index f0a9571b8d..41ce38ac3b 100644 --- a/src/runtime/cli/config-generated.go.in +++ b/src/runtime/cli/config-generated.go.in @@ -25,6 +25,9 @@ const projectPrefix = "@PROJECT_TYPE@" // original URL for this project const projectURL = "@PROJECT_URL@" +// Project URL's organisation name +const projectORG = "@PROJECT_ORG@" + const defaultRootDirectory = "@PKGRUNDIR@" // commit is the git commit the runtime is compiled from. diff --git a/src/runtime/cli/kata-check.go b/src/runtime/cli/kata-check.go index 31ebfe20fe..6755f52cfd 100644 --- a/src/runtime/cli/kata-check.go +++ b/src/runtime/cli/kata-check.go @@ -71,6 +71,9 @@ const ( genericCPUFlagsTag = "flags" // nolint: varcheck, unused, deadcode genericCPUVendorField = "vendor_id" // nolint: varcheck, unused, deadcode genericCPUModelField = "model name" // nolint: varcheck, unused, deadcode + + // If set, do not perform any network checks + noNetworkEnvVar = "KATA_CHECK_NO_NETWORK" ) // variables rather than consts to allow tests to modify them @@ -307,14 +310,71 @@ var kataCheckCLICommand = cli.Command{ Usage: "tests if system can run " + project, Flags: []cli.Flag{ cli.BoolFlag{ - Name: "verbose, v", - Usage: "display the list of checks performed", + Name: "check-version-only", + Usage: "Only compare the current and latest available versions (requires network, non-root only)", + }, + cli.BoolFlag{ + Name: "include-all-releases", + Usage: "Don't filter out pre-release release versions", + }, + cli.BoolFlag{ + Name: "no-network-checks, n", + Usage: "Do not run any checks using the network", + }, + cli.BoolFlag{ + Name: "only-list-releases", + Usage: "Only list newer available releases (non-root only)", }, cli.BoolFlag{ Name: "strict, s", Usage: "perform strict checking", }, + cli.BoolFlag{ + Name: "verbose, v", + Usage: "display the list of checks performed", + }, }, + Description: fmt.Sprintf(`tests if system can run %s and version is current. + +ENVIRONMENT VARIABLES: + +- %s: If set to any value, act as if "--no-network-checks" was specified. + +EXAMPLES: + +- Perform basic checks: + + $ %s %s + +- Local basic checks only: + + $ %s %s --no-network-checks + +- Perform further checks: + + $ sudo %s %s + +- Just check if a newer version is available: + + $ %s %s --check-version-only + +- List available releases (shows output in format "version;release-date;url"): + + $ %s %s --only-list-releases + +- List all available releases (includes pre-release versions): + + $ %s %s --only-list-releases --include-all-releases +`, + project, + noNetworkEnvVar, + name, checkCmd, + name, checkCmd, + name, checkCmd, + name, checkCmd, + name, checkCmd, + name, checkCmd, + ), Action: func(context *cli.Context) error { verbose := context.Bool("verbose") @@ -329,6 +389,28 @@ var kataCheckCLICommand = cli.Command{ span, _ := katautils.Trace(ctx, "kata-check") defer span.Finish() + if context.Bool("no-network-checks") == false && os.Getenv(noNetworkEnvVar) == "" { + cmd := RelCmdCheck + + if context.Bool("only-list-releases") { + cmd = RelCmdList + } + + if os.Geteuid() == 0 { + kataLog.Warn("Not running network checks as super user") + } else { + + err = HandleReleaseVersions(cmd, version, context.Bool("include-all-releases")) + if err != nil { + return err + } + } + } + + if context.Bool("check-version-only") || context.Bool("only-list-releases") { + return nil + } + runtimeConfig, ok := context.App.Metadata["runtimeConfig"].(oci.RuntimeConfig) if !ok { return errors.New("kata-check: cannot determine runtime config") diff --git a/src/runtime/cli/release.go b/src/runtime/cli/release.go new file mode 100644 index 0000000000..9174eaa2db --- /dev/null +++ b/src/runtime/cli/release.go @@ -0,0 +1,410 @@ +// Copyright (c) 2020 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/blang/semver" +) + +type ReleaseCmd int + +type releaseDetails struct { + version semver.Version + date string + url string + filename string +} + +const ( + // A release URL is expected to be prefixed with this value + projectAPIURL = "https://api.github.com/repos/" + projectORG + + releasesSuffix = "/releases" + downloadsSuffix = releasesSuffix + "/download" + + // Kata 1.x + kata1xRepo = "runtime" + kataLegacyReleaseURL = projectAPIURL + "/" + kata1xRepo + releasesSuffix + kataLegacyDownloadURL = projectURL + "/" + kata1xRepo + downloadsSuffix + + // Kata 2.x or newer + kata2xRepo = "kata-containers" + kataReleaseURL = projectAPIURL + "/" + kata2xRepo + releasesSuffix + kataDownloadURL = projectURL + "/" + kata2xRepo + downloadsSuffix + + // Environment variable that can be used to override a release URL + ReleaseURLEnvVar = "KATA_RELEASE_URL" + + RelCmdList ReleaseCmd = iota + RelCmdCheck ReleaseCmd = iota + + msgNoReleases = "No releases available" + msgNoNewerRelease = "No newer release available" + errNoNetChecksAsRoot = "No network checks allowed running as super user" +) + +func (c ReleaseCmd) Valid() bool { + switch c { + case RelCmdCheck, RelCmdList: + return true + default: + return false + } +} + +func downloadURLIsValid(url string) error { + if url == "" { + return errors.New("URL cannot be blank") + } + + if strings.HasPrefix(url, kataDownloadURL) || + strings.HasPrefix(url, kataLegacyDownloadURL) { + return nil + } + + return fmt.Errorf("Download URL %q is not valid", url) +} + +func releaseURLIsValid(url string) error { + if url == "" { + return errors.New("URL cannot be blank") + } + + if url == kataReleaseURL || url == kataLegacyReleaseURL { + return nil + } + + return fmt.Errorf("Release URL %q is not valid", url) +} + +func getReleaseURL(currentVersion semver.Version) (url string, err error) { + major := currentVersion.Major + + if major == 0 { + return "", fmt.Errorf("invalid current version: %v", currentVersion) + } else if major == 1 { + url = kataLegacyReleaseURL + } else { + url = kataReleaseURL + } + + if value := os.Getenv(ReleaseURLEnvVar); value != "" { + url = value + } + + if err := releaseURLIsValid(url); err != nil { + return "", err + } + + return url, nil +} + +func ignoreRelease(release releaseDetails, includeAll bool) bool { + if includeAll { + return false + } + + if len(release.version.Pre) > 0 { + // Pre-releases are ignored by default + return true + } + + return false +} + +// Returns a release version and release object from the specified map. +func makeRelease(release map[string]interface{}) (version string, details releaseDetails, err error) { + key := "tag_name" + + version, ok := release[key].(string) + if ok != true { + return "", details, fmt.Errorf("failed to find key %s in release data", key) + } + + if version == "" { + return "", details, fmt.Errorf("release version cannot be blank") + } + + releaseSemver, err := semver.Make(version) + if err != nil { + return "", details, fmt.Errorf("release %q has invalid semver version: %v", version, err) + } + + key = "assets" + + assetsArray, ok := release[key].([]interface{}) + if ok != true { + return "", details, fmt.Errorf("failed to find key %s in release version %q data", key, version) + } + + if len(assetsArray) == 0 { + // GitHub auto-creates the source assets, but binaries have to + // be built and uploaded for a release. + return "", details, fmt.Errorf("no binary assets for release %q", version) + } + + var createDate string + var filename string + var downloadURL string + + assets := assetsArray[0] + + key = "browser_download_url" + + downloadURL, ok = assets.(map[string]interface{})[key].(string) + if ok != true { + return "", details, fmt.Errorf("failed to find key %s in release version %q asset data", key, version) + } + + if err := downloadURLIsValid(downloadURL); err != nil { + return "", details, err + } + + key = "name" + + filename, ok = assets.(map[string]interface{})[key].(string) + if ok != true { + return "", details, fmt.Errorf("failed to find key %s in release version %q asset data", key, version) + } + + if filename == "" { + return "", details, fmt.Errorf("Release %q asset missing filename", version) + } + + key = "created_at" + + createDate, ok = assets.(map[string]interface{})[key].(string) + if ok != true { + return "", details, fmt.Errorf("failed to find key %s in release version %q asset data", key, version) + } + + if createDate == "" { + return "", details, fmt.Errorf("Release %q asset missing creation date", version) + } + + details = releaseDetails{ + version: releaseSemver, + date: createDate, + url: downloadURL, + filename: filename, + } + + return version, details, nil +} + +func readReleases(releasesArray []map[string]interface{}, includeAll bool) (versions []semver.Version, + releases map[string]releaseDetails) { + + releases = make(map[string]releaseDetails) + + for _, release := range releasesArray { + version, details, err := makeRelease(release) + + // Don't error if makeRelease() fails to construct a release. + // There are many reasons a release may not be considered + // valid, so just ignore the invalid ones. + if err != nil { + kataLog.WithField("version", version).WithError(err).Debug("ignoring invalid release version") + continue + } + + if ignoreRelease(details, includeAll) { + continue + } + + versions = append(versions, details.version) + releases[version] = details + } + + semver.Sort(versions) + + return versions, releases +} + +// Note: Assumes versions is sorted in ascending order +func findNewestRelease(currentVersion semver.Version, versions []semver.Version) (bool, semver.Version, error) { + var candidates []semver.Version + + if len(versions) == 0 { + return false, semver.Version{}, errors.New("no versions available") + } + + for _, version := range versions { + if currentVersion.GTE(version) { + // Ignore older releases (and the current one!) + continue + } + + candidates = append(candidates, version) + } + + count := len(candidates) + + if count == 0 { + return false, semver.Version{}, nil + } + + return true, candidates[count-1], nil +} + +func getReleases(releaseURL string, includeAll bool) ([]semver.Version, map[string]releaseDetails, error) { + kataLog.WithField("url", releaseURL).Info("Looking for releases") + + if os.Geteuid() == 0 { + return nil, nil, errors.New(errNoNetChecksAsRoot) + } + + client := &http.Client{} + + resp, err := client.Get(releaseURL) + if err != nil { + return nil, nil, err + } + + defer resp.Body.Close() + + releasesArray := []map[string]interface{}{} + + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read release details: %v", err) + } + + if err := json.Unmarshal(bytes, &releasesArray); err != nil { + return nil, nil, fmt.Errorf("failed to unpack release details: %v", err) + } + + versions, releases := readReleases(releasesArray, includeAll) + + return versions, releases, nil +} + +func getNewReleaseType(current semver.Version, latest semver.Version) (string, error) { + if current.GT(latest) { + return "", fmt.Errorf("current version %s newer than latest %s", current, latest) + } + + if current.EQ(latest) { + return "", fmt.Errorf("current version %s and latest are same", current) + } + + var desc string + + if latest.Major > current.Major { + if len(latest.Pre) > 0 { + desc = "major pre-release" + } else { + desc = "major" + } + } else if latest.Minor > current.Minor { + if len(latest.Pre) > 0 { + desc = "minor pre-release" + } else { + desc = "minor" + } + } else if latest.Patch > current.Patch { + if len(latest.Pre) > 0 { + desc = "patch pre-release" + } else { + desc = "patch" + } + } else if latest.Patch == current.Patch && len(latest.Pre) > 0 { + desc = "pre-release" + } else { + return "", fmt.Errorf("BUG: unhandled scenario: current version: %s, latest version: %v", current, latest) + } + + return desc, nil +} + +func showLatestRelease(output *os.File, current semver.Version, details releaseDetails) error { + latest := details.version + + desc, err := getNewReleaseType(current, latest) + if err != nil { + return err + } + + fmt.Fprintf(output, "Newer %s release available: %s (url: %v, date: %v)\n", + desc, + details.version, details.url, details.date) + + return nil +} + +func listReleases(output *os.File, current semver.Version, versions []semver.Version, releases map[string]releaseDetails) error { + for _, version := range versions { + details, ok := releases[version.String()] + if !ok { + return fmt.Errorf("Release %v has no details", version) + } + + fmt.Fprintf(output, "%s;%s;%s\n", version, details.date, details.url) + } + + return nil +} + +func HandleReleaseVersions(cmd ReleaseCmd, currentVersion string, includeAll bool) error { + if !cmd.Valid() { + return fmt.Errorf("invalid release command: %v", cmd) + } + + output := os.Stdout + + currentSemver, err := semver.Make(currentVersion) + if err != nil { + return fmt.Errorf("BUG: Current version of %s (%s) has invalid SemVer version: %v", name, currentVersion, err) + } + + releaseURL, err := getReleaseURL(currentSemver) + if err != nil { + return err + } + + versions, releases, err := getReleases(releaseURL, includeAll) + if err != nil { + return err + } + + if cmd == RelCmdList { + return listReleases(output, currentSemver, versions, releases) + } + + if len(versions) == 0 { + fmt.Fprintf(output, "%s\n", msgNoReleases) + return nil + } + + available, newest, err := findNewestRelease(currentSemver, versions) + if err != nil { + return err + } + + if !available { + fmt.Fprintf(output, "%s\n", msgNoNewerRelease) + return nil + } + + details, ok := releases[newest.String()] + if !ok { + return fmt.Errorf("Release %v has no details", newest) + } + + if err != nil { + return err + } + + return showLatestRelease(output, currentSemver, details) +} diff --git a/src/runtime/cli/release_test.go b/src/runtime/cli/release_test.go new file mode 100644 index 0000000000..34fbb251e0 --- /dev/null +++ b/src/runtime/cli/release_test.go @@ -0,0 +1,498 @@ +// Copyright (c) 2020 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +package main + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/blang/semver" + "github.com/stretchr/testify/assert" +) + +var currentSemver semver.Version +var expectedReleasesURL string + +func init() { + var err error + currentSemver, err = semver.Make(version) + + if err != nil { + panic(fmt.Sprintf("failed to create semver for testing: %v", err)) + } + + if currentSemver.Major == 1 { + expectedReleasesURL = kataLegacyReleaseURL + } else { + expectedReleasesURL = kataReleaseURL + } +} + +func TestReleaseCmd(t *testing.T) { + assert := assert.New(t) + + for i, value := range []ReleaseCmd{RelCmdCheck, RelCmdList} { + assert.True(value.Valid(), "test[%d]: %+v", i, value) + } + + for i, value := range []int{-1, 2, 42, 255} { + invalid := ReleaseCmd(i) + + assert.False(invalid.Valid(), "test[%d]: %+v", i, value) + } +} + +func TestGetReleaseURL(t *testing.T) { + assert := assert.New(t) + + const kata1xURL = "https://api.github.com/repos/kata-containers/runtime/releases" + const kata2xURL = "https://api.github.com/repos/kata-containers/kata-containers/releases" + + type testData struct { + currentVersion string + expectError bool + expectedURL string + } + + data := []testData{ + {"0.0.0", true, ""}, + {"1.0.0", false, kata1xURL}, + {"1.9999.9999", false, kata1xURL}, + {"2.0.0-alpha3", false, kata2xURL}, + {"2.9999.9999", false, kata2xURL}, + } + + for i, d := range data { + msg := fmt.Sprintf("test[%d]: %+v", i, d) + + ver, err := semver.Make(d.currentVersion) + msg = fmt.Sprintf("%s, version: %v, error: %v", msg, ver, err) + + assert.NoError(err, msg) + + url, err := getReleaseURL(ver) + if d.expectError { + assert.Error(err, msg) + } else { + assert.NoError(err, msg) + assert.Equal(url, d.expectedURL, msg) + assert.True(strings.HasPrefix(url, projectAPIURL), msg) + } + } + + url, err := getReleaseURL(currentSemver) + assert.NoError(err) + + assert.Equal(url, expectedReleasesURL) + + assert.True(strings.HasPrefix(url, projectAPIURL)) + +} + +func TestGetReleaseURLEnvVar(t *testing.T) { + assert := assert.New(t) + + type testData struct { + envVarValue string + expectError bool + expectedURL string + } + + data := []testData{ + {"", false, expectedReleasesURL}, + {"http://google.com", true, ""}, + {"https://katacontainers.io", true, ""}, + {"https://github.com/kata-containers/runtime/releases/latest", true, ""}, + {"https://github.com/kata-containers/kata-containers/releases/latest", true, ""}, + {expectedReleasesURL, false, expectedReleasesURL}, + } + + assert.Equal(os.Getenv("KATA_RELEASE_URL"), "") + defer os.Setenv("KATA_RELEASE_URL", "") + + for i, d := range data { + msg := fmt.Sprintf("test[%d]: %+v", i, d) + + err := os.Setenv("KATA_RELEASE_URL", d.envVarValue) + msg = fmt.Sprintf("%s, error: %v", msg, err) + + assert.NoError(err, msg) + + url, err := getReleaseURL(currentSemver) + if d.expectError { + assert.Errorf(err, msg) + } else { + assert.NoErrorf(err, msg) + assert.Equal(d.expectedURL, url, msg) + } + } +} + +func TestMakeRelease(t *testing.T) { + assert := assert.New(t) + + type testData struct { + release map[string]interface{} + expectError bool + expectedVersion string + expectedDetails releaseDetails + } + + invalidRel1 := map[string]interface{}{"foo": 1} + invalidRel2 := map[string]interface{}{"foo": "bar"} + invalidRel3 := map[string]interface{}{"foo": true} + + testDate := "2020-09-01T22:10:44Z" + testRelVersion := "1.2.3" + testFilename := "kata-static-1.12.0-alpha1-x86_64.tar.xz" + testURL := fmt.Sprintf("https://github.com/kata-containers/runtime/releases/download/%s/%s", testRelVersion, testFilename) + + testSemver, err := semver.Make(testRelVersion) + assert.NoError(err) + + invalidRelMissingVersion := map[string]interface{}{} + + invalidRelInvalidVersion := map[string]interface{}{ + "tag_name": "not.valid.semver", + } + + invalidRelMissingAssets := map[string]interface{}{ + "tag_name": testRelVersion, + "name": testFilename, + "assets": []interface{}{}, + } + + invalidAssetsMissingURL := []interface{}{ + map[string]interface{}{ + "name": testFilename, + "created_at": testDate, + }, + } + + invalidAssetsMissingFile := []interface{}{ + map[string]interface{}{ + "browser_download_url": testURL, + "created_at": testDate, + }, + } + + invalidAssetsMissingDate := []interface{}{ + map[string]interface{}{ + "name": testFilename, + "browser_download_url": testURL, + }, + } + + validAssets := []interface{}{ + map[string]interface{}{ + "browser_download_url": testURL, + "name": testFilename, + "created_at": testDate, + }, + } + + invalidRelAssetsMissingURL := map[string]interface{}{ + "tag_name": testRelVersion, + "name": testFilename, + "assets": invalidAssetsMissingURL, + } + + invalidRelAssetsMissingFile := map[string]interface{}{ + "tag_name": testRelVersion, + "name": testFilename, + "assets": invalidAssetsMissingFile, + } + + invalidRelAssetsMissingDate := map[string]interface{}{ + "tag_name": testRelVersion, + "name": testFilename, + "assets": invalidAssetsMissingDate, + } + + validRel := map[string]interface{}{ + "tag_name": testRelVersion, + "name": testFilename, + "assets": validAssets, + } + + validReleaseDetails := releaseDetails{ + version: testSemver, + date: testDate, + url: testURL, + filename: testFilename, + } + + data := []testData{ + {invalidRel1, true, "", releaseDetails{}}, + {invalidRel2, true, "", releaseDetails{}}, + {invalidRel3, true, "", releaseDetails{}}, + {invalidRelMissingVersion, true, "", releaseDetails{}}, + {invalidRelInvalidVersion, true, "", releaseDetails{}}, + {invalidRelMissingAssets, true, "", releaseDetails{}}, + {invalidRelAssetsMissingURL, true, "", releaseDetails{}}, + {invalidRelAssetsMissingFile, true, "", releaseDetails{}}, + {invalidRelAssetsMissingDate, true, "", releaseDetails{}}, + + {validRel, false, testRelVersion, validReleaseDetails}, + } + + for i, d := range data { + msg := fmt.Sprintf("test[%d]: %+v", i, d) + + version, details, err := makeRelease(d.release) + msg = fmt.Sprintf("%s, version: %v, details: %+v, error: %v", msg, version, details, err) + + if d.expectError { + assert.Error(err, msg) + continue + } + + assert.NoError(err, msg) + assert.Equal(d.expectedVersion, version, msg) + assert.Equal(d.expectedDetails, details, msg) + } +} + +func TestReleaseURLIsValid(t *testing.T) { + assert := assert.New(t) + + type testData struct { + url string + expectError bool + } + + data := []testData{ + {"", true}, + {"foo", true}, + {"foo bar", true}, + {"https://google.com", true}, + {projectAPIURL, true}, + + {kataLegacyReleaseURL, false}, + {kataReleaseURL, false}, + } + + for i, d := range data { + msg := fmt.Sprintf("test[%d]: %+v", i, d) + + err := releaseURLIsValid(d.url) + msg = fmt.Sprintf("%s, error: %v", msg, err) + + if d.expectError { + assert.Error(err, msg) + } else { + assert.NoError(err, msg) + } + } +} + +func TestDownloadURLIsValid(t *testing.T) { + assert := assert.New(t) + + type testData struct { + url string + expectError bool + } + + validKata1xDownload := "https://github.com/kata-containers/runtime/releases/download/1.12.0-alpha1/kata-static-1.12.0-alpha1-x86_64.tar.xz" + validKata2xDownload := "https://github.com/kata-containers/kata-containers/releases/download/2.0.0-alpha3/kata-static-2.0.0-alpha3-x86_64.tar.xz" + + data := []testData{ + {"", true}, + {"foo", true}, + {"foo bar", true}, + {"https://google.com", true}, + {projectURL, true}, + {validKata1xDownload, false}, + {validKata2xDownload, false}, + } + + for i, d := range data { + msg := fmt.Sprintf("test[%d]: %+v", i, d) + + err := downloadURLIsValid(d.url) + msg = fmt.Sprintf("%s, error: %v", msg, err) + + if d.expectError { + assert.Error(err, msg) + } else { + assert.NoError(err, msg) + } + } +} + +func TestIgnoreRelease(t *testing.T) { + assert := assert.New(t) + + type testData struct { + details releaseDetails + includeAll bool + expectIgnore bool + } + + verWithoutPreRelease, err := semver.Make("2.0.0") + assert.NoError(err) + + verWithPreRelease, err := semver.Make("2.0.0-alpha3") + assert.NoError(err) + + relWithoutPreRelease := releaseDetails{ + version: verWithoutPreRelease, + } + + relWithPreRelease := releaseDetails{ + version: verWithPreRelease, + } + + data := []testData{ + {relWithoutPreRelease, false, false}, + {relWithoutPreRelease, true, false}, + {relWithPreRelease, false, true}, + {relWithPreRelease, true, false}, + } + + for i, d := range data { + msg := fmt.Sprintf("test[%d]: %+v", i, d) + + ignore := ignoreRelease(d.details, d.includeAll) + + if d.expectIgnore { + assert.True(ignore, msg) + } else { + assert.False(ignore, msg) + } + } +} + +func TestGetReleases(t *testing.T) { + assert := assert.New(t) + + url := "foo" + expectedErrMsg := "unsupported protocol scheme" + + for _, includeAll := range []bool{true, false} { + euid := os.Geteuid() + + msg := fmt.Sprintf("includeAll: %v, euid: %v", includeAll, euid) + + _, _, err := getReleases(url, includeAll) + msg = fmt.Sprintf("%s, error: %v", msg, err) + + assert.Error(err, msg) + + if euid == 0 { + assert.Equal(err.Error(), errNoNetChecksAsRoot, msg) + } else { + assert.True(strings.Contains(err.Error(), expectedErrMsg), msg) + } + } +} + +func TestFindNewestRelease(t *testing.T) { + assert := assert.New(t) + + type testData struct { + currentVer semver.Version + versions []semver.Version + expectAvailable bool + expectVersion semver.Version + expectError bool + } + + ver1, err := semver.Make("1.11.1") + assert.NoError(err) + + ver2, err := semver.Make("1.11.3") + assert.NoError(err) + + ver3, err := semver.Make("2.0.0") + assert.NoError(err) + + data := []testData{ + {semver.Version{}, []semver.Version{}, false, semver.Version{}, true}, + {ver1, []semver.Version{}, false, semver.Version{}, true}, + {ver1, []semver.Version{ver1}, false, semver.Version{}, false}, + {ver2, []semver.Version{ver1}, false, semver.Version{}, false}, + {ver1, []semver.Version{ver2}, true, ver2, false}, + {ver1, []semver.Version{ver3}, true, ver3, false}, + {ver1, []semver.Version{ver2, ver3}, true, ver3, false}, + {ver2, []semver.Version{ver1, ver3}, true, ver3, false}, + {ver2, []semver.Version{ver1}, false, semver.Version{}, false}, + } + + for i, d := range data { + msg := fmt.Sprintf("test[%d]: %+v", i, d) + + available, version, err := findNewestRelease(d.currentVer, d.versions) + msg = fmt.Sprintf("%s, available: %v, version: %v, error: %v", msg, available, version, err) + + if d.expectError { + assert.Error(err, msg) + continue + } + + assert.NoError(err, msg) + + if !d.expectAvailable { + assert.False(available, msg) + continue + } + + assert.Equal(d.expectVersion, version) + } +} + +func TestGetNewReleaseType(t *testing.T) { + assert := assert.New(t) + + type testData struct { + currentVer string + latestVer string + expectError bool + result string + } + + data := []testData{ + {"2.0.0-alpha3", "1.0.0", true, ""}, + {"1.0.0", "1.0.0", true, ""}, + {"2.0.0", "1.0.0", true, ""}, + + {"1.0.0", "2.0.0", false, "major"}, + {"2.0.0-alpha3", "2.0.0-alpha4", false, "pre-release"}, + {"1.0.0", "2.0.0-alpha3", false, "major pre-release"}, + + {"1.0.0", "1.1.2", false, "minor"}, + {"1.0.0", "1.1.2-pre2", false, "minor pre-release"}, + {"1.0.0", "1.1.2-foo", false, "minor pre-release"}, + + {"1.0.0", "1.0.3", false, "patch"}, + {"1.0.0-beta29", "1.0.0-beta30", false, "pre-release"}, + {"1.0.0", "1.0.3-alpha99.1b", false, "patch pre-release"}, + } + + for i, d := range data { + msg := fmt.Sprintf("test[%d]: %+v", i, d) + + current, err := semver.Make(d.currentVer) + msg = fmt.Sprintf("%s, current: %v, error: %v", msg, current, err) + + assert.NoError(err, msg) + + latest, err := semver.Make(d.latestVer) + assert.NoError(err, msg) + + desc, err := getNewReleaseType(current, latest) + if d.expectError { + assert.Error(err, msg) + continue + } + + assert.NoError(err, msg) + assert.Equal(d.result, desc, msg) + } +}