// 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) }