From 75eef00b92c0cb97437d69e47f05f7a8ed52c50a Mon Sep 17 00:00:00 2001 From: Peter Schroeter Date: Tue, 12 Feb 2019 14:41:32 -0800 Subject: [PATCH 1/2] PHX-1467 Re-add update command --- ChangeLog.md | 7 + Gopkg.lock | 135 ++++++++- Gopkg.toml | 5 + README.md | 14 +- cmd/right_pt/main.go | 18 +- cmd/right_pt/right_pt_suite_test.go | 13 + cmd/right_pt/update.go | 426 ++++++++++++++++++++++++++++ cmd/right_pt/update_test.go | 293 +++++++++++++++++++ cmd/right_pt/update_unix.go | 29 ++ cmd/right_pt/update_windows.go | 7 + 10 files changed, 934 insertions(+), 13 deletions(-) create mode 100644 ChangeLog.md create mode 100644 cmd/right_pt/right_pt_suite_test.go create mode 100644 cmd/right_pt/update.go create mode 100644 cmd/right_pt/update_test.go create mode 100644 cmd/right_pt/update_unix.go create mode 100644 cmd/right_pt/update_windows.go diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..cfccd4a --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,7 @@ +v1.0.1 / 2019-02-12 +------------------- +* Add in update code + +v1.0.0 / 2019-02-12 +------------------- +* Initial release diff --git a/Gopkg.lock b/Gopkg.lock index 4e03982..ae097ff 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -95,6 +95,40 @@ revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241" version = "v1.0.0" +[[projects]] + digest = "1:a1038ef593beb4771c8f0f9c26e8b00410acd800af5c6864651d9bf160ea1813" + name = "github.com/hpcloud/tail" + packages = [ + ".", + "ratelimiter", + "util", + "watch", + "winfile", + ] + pruneopts = "UT" + revision = "a30252cb686a21eb2d0b98132633053ec2f7f1e5" + version = "v1.0.0" + +[[projects]] + branch = "master" + digest = "1:f3b0cfd42716269884bf06df5d5be9e54161d5af0a2f0ccca08717928dd45fc5" + name = "github.com/inconshreveable/go-update" + packages = [ + ".", + "internal/binarydist", + "internal/osext", + ] + pruneopts = "UT" + revision = "8152e7eb6ccf8679a64582a66b78519688d156ad" + +[[projects]] + branch = "master" + digest = "1:caf6db28595425c0e0f2301a00257d11712f65c1878e12cffc42f6b9a9cf3f23" + name = "github.com/kardianos/osext" + packages = ["."] + pruneopts = "UT" + revision = "ae77be60afb1dcacde03767a8c37337fad28ac14" + [[projects]] digest = "1:c568d7727aa262c32bdf8a3f7db83614f7af0ed661474b24588de635c20024c7" name = "github.com/magiconair/properties" @@ -119,6 +153,55 @@ revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" version = "v1.1.2" +[[projects]] + digest = "1:5f4b78246f0bcb105b1e3b2b9e22b52a57cd02f57a8078572fe27c62f4a75ff7" + name = "github.com/onsi/ginkgo" + packages = [ + ".", + "config", + "internal/codelocation", + "internal/containernode", + "internal/failer", + "internal/leafnodes", + "internal/remote", + "internal/spec", + "internal/spec_iterator", + "internal/specrunner", + "internal/suite", + "internal/testingtproxy", + "internal/writer", + "reporters", + "reporters/stenographer", + "reporters/stenographer/support/go-colorable", + "reporters/stenographer/support/go-isatty", + "types", + ] + pruneopts = "UT" + revision = "2e1be8f7d90e9d3e3e58b0ce470f2f14d075406f" + version = "v1.7.0" + +[[projects]] + digest = "1:5a52ee2374f9d6582665cf84195058888faff1007302aa7f07abee1fda39bb54" + name = "github.com/onsi/gomega" + packages = [ + ".", + "format", + "gbytes", + "internal/assertion", + "internal/asyncassertion", + "internal/oraclematcher", + "internal/testingtsupport", + "matchers", + "matchers/support/goraph/bipartitegraph", + "matchers/support/goraph/edge", + "matchers/support/goraph/node", + "matchers/support/goraph/util", + "types", + ] + pruneopts = "UT" + revision = "65fb64232476ad9046e57c26cd0bff3d3a8dc6cd" + version = "v1.4.3" + [[projects]] digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e" name = "github.com/pelletier/go-toml" @@ -241,6 +324,18 @@ pruneopts = "UT" revision = "0641a9b7482cea5303951506bd3189d2896d2eda" +[[projects]] + branch = "master" + digest = "1:4939e20b972c22cd512abff0bf6ed8fc0e3e86aeb836d016be3323c6a901d99d" + name = "golang.org/x/net" + packages = [ + "html", + "html/atom", + "html/charset", + ] + pruneopts = "UT" + revision = "65e2d4e15006aab9813ff8769e768bbf4bb667a0" + [[projects]] branch = "master" digest = "1:db12953ac7a80656b159265fe13146959beaada45938a532f484a3aeb289462e" @@ -250,12 +345,26 @@ revision = "c6b37f3e92850b723493d63fd35aad34e19e048d" [[projects]] - digest = "1:8029e9743749d4be5bc9f7d42ea1659471767860f0cdc34d37c3111bd308a295" + digest = "1:4392fcf42d5cf0e3ff78c96b2acf8223d49e4fdc53eb77c99d2f8dfe4680e006" name = "golang.org/x/text" packages = [ + "encoding", + "encoding/charmap", + "encoding/htmlindex", + "encoding/internal", + "encoding/internal/identifier", + "encoding/japanese", + "encoding/korean", + "encoding/simplifiedchinese", + "encoding/traditionalchinese", + "encoding/unicode", "internal/gen", + "internal/tag", "internal/triegen", "internal/ucd", + "internal/utf8internal", + "language", + "runes", "transform", "unicode/cldr", "unicode/norm", @@ -285,6 +394,15 @@ pruneopts = "UT" revision = "0c44af741bb7ee876f4c02a8205654b087b73f51" +[[projects]] + digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd" + name = "gopkg.in/fsnotify.v1" + packages = ["."] + pruneopts = "UT" + revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" + source = "https://github.com/fsnotify/fsnotify/archive/v1.4.7.tar.gz" + version = "v1.4.7" + [[projects]] digest = "1:9935525a8c49b8434a0b0a54e1980e94a6fae73aaff45c5d33ba8dff69de123e" name = "gopkg.in/sourcemap.v1" @@ -296,6 +414,14 @@ revision = "6e83acea0053641eff084973fee085f0c193c61a" version = "v1.0.5" +[[projects]] + branch = "v1" + digest = "1:0caa92e17bc0b65a98c63e5bc76a9e844cd5e56493f8fdbb28fad101a16254d9" + name = "gopkg.in/tomb.v1" + packages = ["."] + pruneopts = "UT" + revision = "dd632973f1e7218eb1089048e0798ec9ae7dceb8" + [[projects]] digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96" name = "gopkg.in/yaml.v2" @@ -311,6 +437,11 @@ "github.com/alecthomas/kingpin", "github.com/douglaswth/rsrdp/win32", "github.com/go-yaml/yaml", + "github.com/inconshreveable/go-update", + "github.com/kardianos/osext", + "github.com/onsi/ginkgo", + "github.com/onsi/gomega", + "github.com/onsi/gomega/gbytes", "github.com/pkg/errors", "github.com/robertkrimen/otto", "github.com/robertkrimen/otto/underscore", @@ -319,6 +450,8 @@ "goa.design/goa/http", "goa.design/goa/security", "goa.design/plugins/cors", + "golang.org/x/sys/unix", + "gopkg.in/yaml.v2", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index d83643c..6124b80 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -53,6 +53,11 @@ branch = "master" name = "goa.design/plugins" +# https://github.com/golang/dep/issues/1799 +[[override]] + source = "https://github.com/fsnotify/fsnotify/archive/v1.4.7.tar.gz" + name = "gopkg.in/fsnotify.v1" + [prune] go-tests = true unused-packages = true diff --git a/README.md b/README.md index 42ad49f..2ac05d4 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,11 @@ `right_pt` is a command line tool to aid in the development and testing of RightScale Policies. The tool is able to syntax check, upload, and run Policies. -[![Travis CI Build Status](https://travis-ci.org/rightscale/right_pt.svg?branch=master)](https://travis-ci.org/rightscale/right_pt?branch=master) -[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/rightscale/right_pt?branch=master&svg=true)](https://ci.appveyor.com/project/RightScale/right-pt?branch=master) +[![Travis CI Build Status](https://travis-ci.com/rightscale/right_pt.svg?token=6Udhsz2ZbD68aBb7ApTx&branch=master)](https://travis-ci.com/rightscale/right_pt) * [Installation](#installation) * [Configuration](#configuration) -* [Managing RightScripts](#managing-rightscripts) - * [RightScript Usage](#rightscript-usage) -* [Managing ServerTemplates](#managing-servertemplates) - * [ServerTemplate Usage](#servertemplate-usage) +* [Usage](#usage) * [Contributors](#contributors) * [License](#license) @@ -80,9 +76,11 @@ right_pt script [] [...] ## Contributors This tool is maintained by [Douglas Thrift (douglaswth)](https://github.com/douglaswth), -[Peter Schroeter (psschroeter)](https://github.com/psschroeter) +[Peter Schroeter (psschroeter)](https://github.com/psschroeter), +[Avinash Bhashyam (avinashbhashyam-rs)](https://github.com/avinashbhashyam-rs) + ## License The `right_pt` source code is subject to the MIT license, see the -[LICENSE](https://github.com/douglaswth/right_pt/blob/master/LICENSE) file. +[LICENSE](https://github.com/rightscale/right_pt/blob/master/LICENSE) file. diff --git a/cmd/right_pt/main.go b/cmd/right_pt/main.go index 2eaa945..8993b10 100644 --- a/cmd/right_pt/main.go +++ b/cmd/right_pt/main.go @@ -80,12 +80,12 @@ Example: right_pt script max_snapshots.pt --result snapshots volumes=@ec2_volume configShowCmd = configCmd.Command("show", "Show configuration") // ----- Update right_st ----- -// updateCmd = app.Command("update", "Update "+app.Name+" executable") + updateCmd = app.Command("update", "Update "+app.Name+" executable") -// updateListCmd = updateCmd.Command("list", "List any available updates for the "+app.Name+" executable") + updateListCmd = updateCmd.Command("list", "List any available updates for the "+app.Name+" executable") -// updateApplyCmd = updateCmd.Command("apply", "Apply the latest update for the current major version or a specified major version") -// updateApplyMajorVersion = updateApplyCmd.Flag("major-version", "Major version to update to").Short('m').Int() + updateApplyCmd = updateCmd.Command("apply", "Apply the latest update for the current major version or a specified major version") + updateApplyMajorVersion = updateApplyCmd.Flag("major-version", "Major version to update to").Short('m').Int() ) func main() { @@ -160,6 +160,16 @@ func main() { if err != nil { fatalError("%s\n", err.Error()) } + case updateListCmd.FullCommand(): + err := UpdateList(VV, os.Stdout) + if err != nil { + fatalError("%s\n", err.Error()) + } + case updateApplyCmd.FullCommand(): + err := UpdateApply(VV, os.Stdout, *updateApplyMajorVersion, "") + if err != nil { + fatalError("%s\n", err.Error()) + } } } diff --git a/cmd/right_pt/right_pt_suite_test.go b/cmd/right_pt/right_pt_suite_test.go new file mode 100644 index 0000000..2831cd1 --- /dev/null +++ b/cmd/right_pt/right_pt_suite_test.go @@ -0,0 +1,13 @@ +package main_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestRightSt(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "RightPt Suite") +} diff --git a/cmd/right_pt/update.go b/cmd/right_pt/update.go new file mode 100644 index 0000000..2d4574e --- /dev/null +++ b/cmd/right_pt/update.go @@ -0,0 +1,426 @@ +package main + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path" + "regexp" + "runtime" + "sort" + "strconv" + + "github.com/inconshreveable/go-update" + "gopkg.in/yaml.v2" +) + +type Version struct { + Major int + Minor int + Patch int +} + +type LatestVersions struct { + Versions map[int]*Version + majorVersion int +} + +const ( + UpdateGithubBaseUrl = "https://github.com/rightscale/right_pt" + UpdateGithubReleasesUrl = UpdateGithubBaseUrl + "/releases" + UpdateGithubChangeLogUrl = UpdateGithubBaseUrl + "/blob/master/ChangeLog.md" +) + +var ( + UpdateBaseUrl = "https://binaries.rightscale.com/rsbin/right_pt" + + vvString = regexp.MustCompile(`^` + regexp.QuoteMeta(app.Name) + ` (v[0-9]+\.[0-9]+\.[0-9]+) -`) + versionString = regexp.MustCompile(`^v([0-9]+)\.([0-9]+)\.([0-9]+)$`) +) + +// UpdateGetCurrentVersion gets the current version struct from the version string defined in +// version.go/version_default.go. +func UpdateGetCurrentVersion(vv string) *Version { + submatches := vvString.FindStringSubmatch(vv) + // if the version string does not match it is a dev version and we cannot tell what actual version it is + if submatches == nil { + return nil + } + version, _ := NewVersion(submatches[1]) + return version +} + +// UpdateGetVersionUrl gets the URL for the version.yml file for right_pt from the rsbin bucket. +func UpdateGetVersionUrl() string { + return UpdateBaseUrl + "/version-" + runtime.GOOS + "-" + runtime.GOARCH + ".yml" +} + +// UpdateGetLatestVersions gets the latest versions struct by downloading and parsing the version.yml file for right_pt +// from the rsbin bucket. See version.sh and the Makefile upload target for how this file is created. +func UpdateGetLatestVersions() (*LatestVersions, error) { + // get the version.yml file over HTTP(S) + res, err := http.Get(UpdateGetVersionUrl()) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != 200 { + return nil, fmt.Errorf("Unexpected HTTP response getting %s: %s", UpdateGetVersionUrl(), res.Status) + } + versions, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + + // parse the version.yml file into a LatestVersions struct and return the result and any errors + var latest LatestVersions + err = yaml.UnmarshalStrict(versions, &latest) + return &latest, err +} + +// UpdateGetDownloadUrl gets the download URL of the latest version for a major version on the current operating system +// and architecture. +func UpdateGetDownloadUrl(majorVersion int) (string, *Version, error) { + latest, err := UpdateGetLatestVersions() + if err != nil { + return "", nil, err + } + + version, ok := latest.Versions[majorVersion] + if !ok { + return "", nil, fmt.Errorf("Major version not available: %d", majorVersion) + } + + ext := "tgz" + if runtime.GOOS == "windows" { + ext = "zip" + } + + return fmt.Sprintf("%s/%s/%s-%s-%s.%s", UpdateBaseUrl, version, app.Name, runtime.GOOS, runtime.GOARCH, ext), + version, nil +} + +// UpdateCheck checks if there is there are any updates available for the running current version of right_pt and prints +// out instructions to upgrade if so. It will not perform a version check for dev versions (anything that has a version +// other than vX.Y.Z). +func UpdateCheck(vv string, output io.Writer) { + currentVersion := UpdateGetCurrentVersion(vv) + // do not check for updates on non release versions + if currentVersion == nil { + return + } + latest, err := UpdateGetLatestVersions() + // we just ignore errors for update check + if err != nil { + return + } + + // ignore more errors and continue since it will still return the right_pt command name anyway + sudoCommand, _ := updateSudoCommand() + updateAvailable := false + + // check if there is a new version for our major version and output a message if there is + if latestForMajor, ok := latest.Versions[currentVersion.Major]; ok { + if latestForMajor.GreaterThan(currentVersion) { + fmt.Fprintf(output, "There is a new v%d version of %s (%s), to upgrade run:\n %s update apply\n", + latestForMajor.Major, app.Name, latestForMajor, sudoCommand) + updateAvailable = true + } + } + + // check if there is a new major version and output a message if there is + if latest.MajorVersion() > currentVersion.Major { + fmt.Fprintf(output, "There is a new major version of %s (%s), to upgrade run:\n %s update apply -m %d\n", + app.Name, latest.Versions[latest.MajorVersion()], sudoCommand, latest.MajorVersion()) + updateAvailable = true + } + + // print informational URLs if there is any update available + if updateAvailable { + fmt.Fprintf(output, "\nSee %s or\n%s for more information.\n", UpdateGithubChangeLogUrl, + UpdateGithubReleasesUrl) + } +} + +// UpdateList lists available versions based on the contents of the version.yml file for right_pt in the rsbin bucket +// and prints upgrade instructions if any are applicable. +func UpdateList(vv string, output io.Writer) error { + // get the current version from the version string, it will be nil if this is a dev version + currentVersion := UpdateGetCurrentVersion(vv) + latest, err := UpdateGetLatestVersions() + if err != nil { + return err + } + + // sort the major versions so we can iterate through them in order + majors := make([]int, 0, len(latest.Versions)) + for major, _ := range latest.Versions { + majors = append(majors, int(major)) + } + sort.Ints(majors) + + sudoCommand, err := updateSudoCommand() + if err != nil { + return err + } + + // print out the latest version for each major version + for _, major := range majors { + version := latest.Versions[major] + currentVersionEqual, latestForMajorGreater, latestGreater := false, false, false + // don't check for updates for dev versions + if currentVersion != nil { + switch { + case major == currentVersion.Major: + currentVersionEqual = version.EqualTo(currentVersion) + latestForMajorGreater = version.GreaterThan(currentVersion) + case major == latest.MajorVersion(): + latestGreater = version.GreaterThan(currentVersion) + } + } + + fmt.Fprintf(output, "The latest v%d version of %s is %s", major, app.Name, version) + switch { + case currentVersionEqual: + fmt.Fprintf(output, "; this is the version you are using!\n") + case latestForMajorGreater: + fmt.Fprintf(output, "; you are using %s, to upgrade run:\n %s update apply\n", currentVersion, + sudoCommand) + case latestGreater: + fmt.Fprintf(output, "; you are using %s, to upgrade run:\n %s update apply -m %d\n", currentVersion, + sudoCommand, major) + default: + fmt.Fprintf(output, ".\n") + } + } + fmt.Fprintf(output, "\nSee %s or\n%s for more information.\n", UpdateGithubChangeLogUrl, UpdateGithubReleasesUrl) + + return nil +} + +// UpdateApply applies an update by downloading the tgz or zip file for the current platform, extracting it, and +// replacing the current executable with the new executable contained within. If majorVersion is 0, the tgz or zip for +// the latest update of the current major version will be used, otherwise the specified major version will be used. If +// path is the empty string, the current executable will be replaced, otherwise it specifies the targetPath of the file +// to replace (this is only really used for testing). +func UpdateApply(vv string, output io.Writer, majorVersion int, targetPath string) error { + // get the current version from the version string, it will be nil if this is a dev version + currentVersion := UpdateGetCurrentVersion(vv) + if majorVersion == 0 && currentVersion != nil { + majorVersion = currentVersion.Major + } + + // get the URL of the archive for the latest version for the major version + url, version, err := UpdateGetDownloadUrl(majorVersion) + if err != nil { + return err + } + fmt.Fprintf(output, "Downloading %s %s from %s...\n", app.Name, version, url) + + // download the archive file from the URL + res, err := http.Get(url) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != 200 { + return fmt.Errorf("Unexpected HTTP response getting %s: %s", url, res.Status) + } + + // the new executable will need to be read from this reader which will come from somewhere inside the downloaded + // archive + var exe io.Reader + exeName := app.Name + if runtime.GOOS == "windows" { + exeName += ".exe" + } + + // get a reader for the new executable from the archive file which can be either a tgz (gzipped tar file) or a zip + switch path.Ext(url) { + case ".tgz": + // create a gzip reader from the archive file stream in the HTTP response + gzipReader, err := gzip.NewReader(res.Body) + if err != nil { + return err + } + defer gzipReader.Close() + + // create a tar reader from the gzip reader and iterate through its entries + tarReader := tar.NewReader(gzipReader) + for { + // try to get the next header from the tar file + header, err := tarReader.Next() + if err == io.EOF { + // if there was an EOF we've reached the end of the tar file + break + } else if err != nil { + return err + } + + // check if the current entry is for the new executable + info := header.FileInfo() + if !info.IsDir() && info.Name() == exeName { + // assign the current entry's reader to be the new executable and stop iterating through the tar file + exe = tarReader + break + } + } + case ".zip": + // create a temporary file to store the zip archive file + archive, err := ioutil.TempFile("", path.Base(url)+".") + if err != nil { + return err + } + defer func() { + // on Windows you cannot delete a file that has open handles + archive.Close() + os.Remove(archive.Name()) + }() + + // write out the zip file to the temporary file from the HTTP response + if _, err := io.Copy(archive, res.Body); err != nil { + return err + } + if err := archive.Close(); err != nil { + return err + } + + // create a zip reader from the temporary file + zipReader, err := zip.OpenReader(archive.Name()) + if err != nil { + return err + } + defer zipReader.Close() + + // iterate through the files in the zip file + for _, file := range zipReader.File { + // check if the current file is for the new executable + info := file.FileInfo() + if !info.IsDir() && info.Name() == exeName { + // open a reader for the current file in the zip file + contents, err := file.Open() + if err != nil { + return err + } + defer contents.Close() + + // assign the current entry's reader to be the new executable and stop iterating through the zip file + exe = contents + break + } + } + } + + // make sure we actually found the executable file in the archive + if exe == nil { + return fmt.Errorf("Could not find %s in archive: %s", exeName, url) + } + + // attempt to apply the new executable so it replaces the old one + if err := update.Apply(exe, update.Options{TargetPath: targetPath}); err != nil { + // attempt to roll back if the apply failed + if rollbackErr := update.RollbackError(err); rollbackErr != nil { + return rollbackErr + } + return err + } + fmt.Fprintf(output, "Successfully updated %s to %s!\n", app.Name, version) + + return nil +} + +// NewVersion creates a version struct from a version string of the form vX.Y.Z where X, Y, Z are the major, minor, and +// patch version numbers respectively. It returns a pointer to a version struct if sucessful or an error if there is a +// failure. +func NewVersion(version string) (*Version, error) { + submatches := versionString.FindStringSubmatch(version) + if submatches == nil { + return nil, fmt.Errorf("Invalid version string: %s", version) + } + major, _ := strconv.ParseUint(submatches[1], 0, 0) + minor, _ := strconv.ParseUint(submatches[2], 0, 0) + patch, _ := strconv.ParseUint(submatches[3], 0, 0) + return &Version{int(major), int(minor), int(patch)}, nil +} + +// CompareTo compares a version struct to another version struct. It returns a value less than 0 if the version struct +// is less than the other, 0 if the version struct is equal to the other, or a value greater than 0 if the version +// struct is greater than the other. +func (v *Version) CompareTo(ov *Version) int { + switch { + case v.Major < ov.Major: + return -1 + case v.Major > ov.Major: + return +1 + default: + switch { + case v.Minor < ov.Minor: + return -1 + case v.Minor > ov.Minor: + return +1 + default: + switch { + case v.Patch < ov.Patch: + return -1 + case v.Patch > ov.Patch: + return +1 + } + } + } + return 0 +} + +// EqualTo compares a version struct to another version struct in order to determine if the version struct is equal to +// the other or not. +func (v *Version) EqualTo(ov *Version) bool { + return v.CompareTo(ov) == 0 +} + +// LessThan compares a version struct to another version struct in order to determine if the version struct is less than +// the other or not. +func (v *Version) LessThan(ov *Version) bool { + return v.CompareTo(ov) < 0 +} + +// GreaterThan compares a version struct to another version struct in order to determine if the version struct is +// greater than the other or not. +func (v *Version) GreaterThan(ov *Version) bool { + return v.CompareTo(ov) > 0 +} + +// String returns the string representation of a version struct which is vX.Y.Z. +func (v *Version) String() string { + return fmt.Sprintf("v%d.%d.%d", v.Major, v.Minor, v.Patch) +} + +// UnmarshalYAML parses the YAML representation of a version struct into a version struct. The YAML representation of a +// version struct is the same as the string representation. +func (v *Version) UnmarshalYAML(unmarshal func(interface{}) error) error { + var value string + if err := unmarshal(&value); err != nil { + return err + } + version, err := NewVersion(value) + if err != nil { + return err + } + *v = *version + return nil +} + +// MajorVersion gets the latest major version from a latest versions struct. +func (l *LatestVersions) MajorVersion() int { + if l.majorVersion == 0 { + for major, _ := range l.Versions { + if major > l.majorVersion { + l.majorVersion = major + } + } + } + return l.majorVersion +} diff --git a/cmd/right_pt/update_test.go b/cmd/right_pt/update_test.go new file mode 100644 index 0000000..46c5e07 --- /dev/null +++ b/cmd/right_pt/update_test.go @@ -0,0 +1,293 @@ +package main_test + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "regexp" + "runtime" + + . "github.com/rightscale/right_pt/cmd/right_pt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("Update", func() { + Describe("Get current version", func() { + It("Gets a version from a tagged version", func() { + version := UpdateGetCurrentVersion("right_pt v98.76.54 - JUNK JUNK JUNK") + Expect(version).To(Equal(&Version{98, 76, 54})) + }) + + It("Gets no version for a dev version", func() { + version := UpdateGetCurrentVersion("right_pt master - JUNK JUNK JUNK") + Expect(version).To(BeNil()) + }) + }) + + Context("With a update versions URL", func() { + var ( + buffer *gbytes.Buffer + server *httptest.Server + newExeContent string + oldUpdateBaseUrl string + ) + + BeforeEach(func() { + buffer = gbytes.NewBuffer() + exeItem := "right_pt/right_pt" + if runtime.GOOS == "windows" { + exeItem += ".exe" + } + tgzPath := regexp.MustCompile(`^/v[0-9]+\.[0-9]+\.[0-9]+/right_pt-` + runtime.GOOS + `-` + runtime.GOARCH + + `\.tgz$`) + zipPath := regexp.MustCompile(`^/v[0-9]+\.[0-9]+\.[0-9]+/right_pt-` + runtime.GOOS + `-` + runtime.GOARCH + + `\.zip$`) + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.URL.Path == "/version-"+runtime.GOOS+"-"+runtime.GOARCH+".yml": + w.Write([]byte(`# Latest right_pt versions by major version (this file is used by right_pt's update check mechanism) +--- +versions: + 1: v1.2.3 + 2: v2.3.4 + 3: v3.4.5 +`)) + case tgzPath.MatchString(r.URL.Path): + gzipWriter := gzip.NewWriter(w) + tarWriter := tar.NewWriter(gzipWriter) + if err := tarWriter.WriteHeader(&tar.Header{Name: exeItem, Size: int64(len(newExeContent))}); err != nil { + panic(err) + } + if _, err := io.WriteString(tarWriter, newExeContent); err != nil { + panic(err) + } + if err := tarWriter.Close(); err != nil { + panic(err) + } + if err := gzipWriter.Close(); err != nil { + panic(err) + } + case zipPath.MatchString(r.URL.Path): + zipWriter := zip.NewWriter(w) + exeWriter, err := zipWriter.Create(exeItem) + if err != nil { + panic(err) + } + if _, err := io.WriteString(exeWriter, newExeContent); err != nil { + panic(err) + } + if err := zipWriter.Close(); err != nil { + panic(err) + } + default: + w.WriteHeader(http.StatusNotFound) + } + })) + newExeContent = "#!/bin/bash\necho 'This is the new version!'\n" + oldUpdateBaseUrl = UpdateBaseUrl + UpdateBaseUrl = server.URL + }) + + AfterEach(func() { + UpdateBaseUrl = oldUpdateBaseUrl + server.Close() + }) + + Describe("Get latest versions", func() { + It("Gets the latest versions", func() { + latest, err := UpdateGetLatestVersions() + Expect(err).NotTo(HaveOccurred()) + Expect(latest).To(Equal(&LatestVersions{ + Versions: map[int]*Version{ + 1: &Version{1, 2, 3}, + 2: &Version{2, 3, 4}, + 3: &Version{3, 4, 5}, + }, + })) + }) + }) + + Describe("Get download URL", func() { + var ext string + if runtime.GOOS == "windows" { + ext = "zip" + } else { + ext = "tgz" + } + + It("Gets the download URL for a major version", func() { + url, version, err := UpdateGetDownloadUrl(1) + Expect(err).NotTo(HaveOccurred()) + Expect(url).To(Equal(server.URL + "/v1.2.3/right_pt-" + runtime.GOOS + "-" + runtime.GOARCH + "." + ext)) + Expect(version).To(Equal(&Version{1, 2, 3})) + }) + + It("Returns an error for a nonexistent major version", func() { + url, version, err := UpdateGetDownloadUrl(0) + Expect(err).To(MatchError("Major version not available: 0")) + Expect(url).To(BeEmpty()) + Expect(version).To(BeNil()) + }) + }) + + Describe("Update check", func() { + It("Outputs nothing for a dev version", func() { + UpdateCheck("right_pt dev - JUNK JUNK JUNK", buffer) + Expect(buffer.Contents()).To(BeEmpty()) + }) + + It("Outputs nothing if there is no update", func() { + UpdateCheck("right_pt v3.4.5 - JUNK JUNK JUNK", buffer) + Expect(buffer.Contents()).To(BeEmpty()) + }) + + It("Outputs that there is a new version", func() { + UpdateCheck("right_pt v3.0.0 - JUNK JUNK JUNK", buffer) + Expect(buffer.Contents()).To(BeEquivalentTo(`There is a new v3 version of right_pt (v3.4.5), to upgrade run: + right_pt update apply + +See https://github.com/rightscale/right_pt/blob/master/ChangeLog.md or +https://github.com/rightscale/right_pt/releases for more information. +`)) + }) + + It("Outputs that there is a new major version", func() { + UpdateCheck("right_pt v2.3.4 - JUNK JUNK JUNK", buffer) + Expect(buffer.Contents()).To(BeEquivalentTo(`There is a new major version of right_pt (v3.4.5), to upgrade run: + right_pt update apply -m 3 + +See https://github.com/rightscale/right_pt/blob/master/ChangeLog.md or +https://github.com/rightscale/right_pt/releases for more information. +`)) + }) + + It("Outptus that there is a new version and new major version", func() { + UpdateCheck("right_pt v2.0.0 - JUNK JUNK JUNK", buffer) + Expect(buffer.Contents()).To(BeEquivalentTo(`There is a new v2 version of right_pt (v2.3.4), to upgrade run: + right_pt update apply +There is a new major version of right_pt (v3.4.5), to upgrade run: + right_pt update apply -m 3 + +See https://github.com/rightscale/right_pt/blob/master/ChangeLog.md or +https://github.com/rightscale/right_pt/releases for more information. +`)) + }) + }) + + Describe("Update list", func() { + It("Outputs the available versions for a dev version", func() { + Expect(UpdateList("right_pt dev - JUNK JUNK JUNK", buffer)).To(Succeed()) + Expect(buffer.Contents()).To(BeEquivalentTo(`The latest v1 version of right_pt is v1.2.3. +The latest v2 version of right_pt is v2.3.4. +The latest v3 version of right_pt is v3.4.5. + +See https://github.com/rightscale/right_pt/blob/master/ChangeLog.md or +https://github.com/rightscale/right_pt/releases for more information. +`)) + }) + + It("Outputs the available versions for an up to date version", func() { + Expect(UpdateList("right_pt v3.4.5 - JUNK JUNK JUNK", buffer)).To(Succeed()) + Expect(buffer.Contents()).To(BeEquivalentTo(`The latest v1 version of right_pt is v1.2.3. +The latest v2 version of right_pt is v2.3.4. +The latest v3 version of right_pt is v3.4.5; this is the version you are using! + +See https://github.com/rightscale/right_pt/blob/master/ChangeLog.md or +https://github.com/rightscale/right_pt/releases for more information. +`)) + }) + + It("Outputs the available versions when there is a new version", func() { + Expect(UpdateList("right_pt v3.0.0 - JUNK JUNK JUNK", buffer)).To(Succeed()) + Expect(buffer.Contents()).To(BeEquivalentTo(`The latest v1 version of right_pt is v1.2.3. +The latest v2 version of right_pt is v2.3.4. +The latest v3 version of right_pt is v3.4.5; you are using v3.0.0, to upgrade run: + right_pt update apply + +See https://github.com/rightscale/right_pt/blob/master/ChangeLog.md or +https://github.com/rightscale/right_pt/releases for more information. +`)) + }) + + It("Outputs the available versions when there is a new major version", func() { + Expect(UpdateList("right_pt v2.3.4 - JUNK JUNK JUNK", buffer)).To(Succeed()) + Expect(buffer.Contents()).To(BeEquivalentTo(`The latest v1 version of right_pt is v1.2.3. +The latest v2 version of right_pt is v2.3.4; this is the version you are using! +The latest v3 version of right_pt is v3.4.5; you are using v2.3.4, to upgrade run: + right_pt update apply -m 3 + +See https://github.com/rightscale/right_pt/blob/master/ChangeLog.md or +https://github.com/rightscale/right_pt/releases for more information. +`)) + }) + + It("Outputs the available versions when there is a new version and new major version", func() { + Expect(UpdateList("right_pt v2.0.0 - JUNK JUNK JUNK", buffer)).To(Succeed()) + Expect(buffer.Contents()).To(BeEquivalentTo(`The latest v1 version of right_pt is v1.2.3. +The latest v2 version of right_pt is v2.3.4; you are using v2.0.0, to upgrade run: + right_pt update apply +The latest v3 version of right_pt is v3.4.5; you are using v2.0.0, to upgrade run: + right_pt update apply -m 3 + +See https://github.com/rightscale/right_pt/blob/master/ChangeLog.md or +https://github.com/rightscale/right_pt/releases for more information. +`)) + }) + }) + + Describe("Update apply", func() { + var ( + tempDir string + exePath string + ) + + BeforeEach(func() { + var err error + tempDir, err = ioutil.TempDir("", "update") + if err != nil { + panic(err) + } + exePath = filepath.Join(tempDir, "right_pt") + err = ioutil.WriteFile(exePath, []byte("#!/bin/bash\necho 'This is the old version!'\n"), 0755) + if err != nil { + panic(err) + } + }) + + AfterEach(func() { + os.RemoveAll(tempDir) + }) + + It("Updates to the latest version for the current major version", func() { + Expect(UpdateApply("right_pt v2.0.0 - JUNK JUNK JUNK", buffer, 0, exePath)).To(Succeed()) + Expect(buffer.Contents()).To(MatchRegexp(`^Downloading right_pt v2\.3\.4 from %s/v2\.3\.4/right_pt-%s-%s\.(?:tgz|zip)\.\.\. +Successfully updated right_pt to v2\.3\.4! +$`, regexp.QuoteMeta(server.URL), runtime.GOOS, runtime.GOARCH)) + + exeContent, err := ioutil.ReadFile(exePath) + Expect(err).NotTo(HaveOccurred()) + Expect(exeContent).To(BeEquivalentTo(newExeContent)) + }) + + It("Updates to the latest version for a specific major version", func() { + Expect(UpdateApply("right_pt v2.0.0 - JUNK JUNK JUNK", buffer, 3, exePath)).To(Succeed()) + Expect(buffer.Contents()).To(MatchRegexp(`^Downloading right_pt v3\.4\.5 from %s/v3\.4\.5/right_pt-%s-%s\.(?:tgz|zip)\.\.\. +Successfully updated right_pt to v3\.4\.5! +$`, regexp.QuoteMeta(server.URL), runtime.GOOS, runtime.GOARCH)) + + exeContent, err := ioutil.ReadFile(exePath) + Expect(err).NotTo(HaveOccurred()) + Expect(exeContent).To(BeEquivalentTo(newExeContent)) + }) + }) + }) +}) diff --git a/cmd/right_pt/update_unix.go b/cmd/right_pt/update_unix.go new file mode 100644 index 0000000..ccb6e78 --- /dev/null +++ b/cmd/right_pt/update_unix.go @@ -0,0 +1,29 @@ +// +build !windows + +package main + +import ( + "syscall" + + "github.com/kardianos/osext" + "golang.org/x/sys/unix" +) + +// updateSudoCommand returns the right_pt command name with any sudo prefix if necessary. The command name is returned +// even if an error occurred. +func updateSudoCommand() (string, error) { + // get the full path to the right_pt executable so its access can be checked + exe, err := osext.Executable() + if err != nil { + return app.Name, err + } + + // check if the right_pt executable can be written by the current user + err = unix.Access(exe, unix.W_OK) + if err == syscall.EACCES { + // the right_pt executable cannot be written by the current user so prefix the command name with sudo + return "sudo " + app.Name, nil + } + + return app.Name, err +} diff --git a/cmd/right_pt/update_windows.go b/cmd/right_pt/update_windows.go new file mode 100644 index 0000000..be7a6fa --- /dev/null +++ b/cmd/right_pt/update_windows.go @@ -0,0 +1,7 @@ +package main + +// updateSudoCommand returns the right_pt command name with any sudo prefix if necessary. On Windows, it always returns +// just the command name since there is no direct sudo equivalent. +func updateSudoCommand() (string, error) { + return app.Name, nil +} From 450428ffc85fba86977688321e26a04e77b098f7 Mon Sep 17 00:00:00 2001 From: Douglas Thrift Date: Wed, 13 Feb 2019 10:52:34 -0600 Subject: [PATCH 2/2] PHX-1467 Fix up .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7a232a6..cb094cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ +vendor/ version.go version.yml build -./right_pt +/right_pt *.coverprofile *.bak *.old