From e80b8d3e441de0011e2b2c9435949aeaa50012d1 Mon Sep 17 00:00:00 2001 From: Direside Date: Fri, 15 May 2020 17:06:22 -0700 Subject: [PATCH 1/3] Added command to check your local installs. --- cmd/check.go | 246 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 cmd/check.go diff --git a/cmd/check.go b/cmd/check.go new file mode 100644 index 000000000..d107aa900 --- /dev/null +++ b/cmd/check.go @@ -0,0 +1,246 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "regexp" + "strconv" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(checkCmd) +} + +type requirement struct { + name string + command string + args []string + docsURL string + checker func([]byte) (string, error) +} + +type versionError struct { + errorText string +} + +type commandError struct { + Command string + ErrorText string + Suggestion string +} + +func (e *versionError) Error() string { + return fmt.Sprintf("%s", e.errorText) +} + +func (e *commandError) Error() string { + return fmt.Sprintf("%s", e.ErrorText) +} + +var checkCmd = &cobra.Command{ + Use: "check", + Short: "Print the check number of commit0", + Run: func(cmd *cobra.Command, args []string) { + // Add any new requirements to this slice. + required := []requirement{ + { + name: "AWS CLI\t\t", + command: "aws", + args: []string{"--version"}, + docsURL: "", + checker: func(output []byte) (string, error) { + ver := "" + re := regexp.MustCompile(`aws-cli/([0-9]+)\.([0-9]+)\.([0-9]+)`) + m := re.FindStringSubmatch(string(output)) + major, err := strconv.ParseInt(m[1], 0, 64) + if err != nil { + return ver, err + } + minor, err := strconv.ParseInt(m[2], 0, 64) + if err != nil { + return ver, err + } + patch, err := strconv.ParseInt(m[3], 0, 64) + if err != nil { + return ver, err + } + + ver = fmt.Sprintf("%d.%d.%d", major, minor, patch) + + if major < 1 || (major == 1 && minor < 16) { + return ver, &versionError{"Requires 1.16 or greater."} + } + + return ver, err + }}, + { + name: "Kubectl\t\t", + command: "kubectl", + args: []string{"version", "--client=true"}, + docsURL: "https://kubernetes.io/docs/tasks/tools/install-kubectl/", + checker: func(output []byte) (string, error) { + ver := "" + re := regexp.MustCompile(`version\.Info{Major:"([0-9]+)", Minor:"([0-9]+)"`) + m := re.FindStringSubmatch(string(output)) + major, err := strconv.ParseInt(m[1], 0, 64) + if err != nil { + return ver, err + } + minor, err := strconv.ParseInt(m[2], 0, 64) + if err != nil { + return ver, err + } + + ver = fmt.Sprintf("%d.%d", major, minor) + + if major < 1 || (major == 1 && minor < 12) { + return ver, &versionError{"Requires 2.12 or greater."} + } + + return ver, err + }, + }, + { + name: "Terraform\t", + command: "terraform", + args: []string{"version"}, + docsURL: "https://www.terraform.io/downloads.html", + checker: func(output []byte) (string, error) { + ver := "" + re := regexp.MustCompile(`Terraform v([0-9]+)\.([0-9]+)\.([0-9]+)`) + m := re.FindStringSubmatch(string(output)) + major, err := strconv.ParseInt(m[1], 0, 64) + if err != nil { + return ver, err + } + minor, err := strconv.ParseInt(m[2], 0, 64) + if err != nil { + return ver, err + } + patch, err := strconv.ParseInt(m[3], 0, 64) + if err != nil { + return ver, err + } + + ver = fmt.Sprintf("%d.%d.%d", major, minor, patch) + + if major < 0 || (major == 0 && minor < 12) { + return ver, &versionError{"Zero requires terraform 0.12 or greater."} + } + + return ver, err + }, + }, + { + name: "jq\t\t", + command: "jq", + args: []string{"--version"}, + docsURL: "https://stedolan.github.io/jq/download/", + checker: func(output []byte) (string, error) { + ver := "" + re := regexp.MustCompile(`jq-([0-9]+)\.([0-9]+)-`) + m := re.FindStringSubmatch(string(output)) + major, err := strconv.ParseInt(m[1], 0, 64) + if err != nil { + return ver, err + } + minor, err := strconv.ParseInt(m[2], 0, 64) + if err != nil { + return ver, err + } + + ver = fmt.Sprintf("%d.%d", major, minor) + + if major < 1 || (major == 1 && minor < 5) { + return ver, &versionError{"Requires jq version 1.15 or greater."} + } + + return ver, err + }}, + { + name: "Git\t\t", + command: "git", + args: []string{"version"}, + docsURL: "https://git-scm.com/book/en/v2/Getting-Started-Installing-Git", + checker: func(output []byte) (string, error) { + ver := "" + re := regexp.MustCompile(`git version ([0-9]+)\.([0-9]+)\.([0-9]+)`) + m := re.FindStringSubmatch(string(output)) + major, err := strconv.ParseInt(m[1], 0, 64) + if err != nil { + return ver, err + } + minor, err := strconv.ParseInt(m[2], 0, 64) + if err != nil { + return ver, err + } + patch, err := strconv.ParseInt(m[3], 0, 64) + if err != nil { + return ver, err + } + + ver = fmt.Sprintf("%d.%d.%d", major, minor, patch) + + if major < 2 || (major == 2 && minor < 12) { + return ver, &versionError{"Zero requires git version 2.12 or greater."} + } + + return ver, err + }}, + } + + // Store and errors from the commands we run. + errors := []commandError{} + + fmt.Println("Checking Zero Requirements...") + for _, r := range required { + fmt.Printf("%s", r.name) + out, err := exec.Command(r.command, r.args...).CombinedOutput() + if err != nil { + cerr := commandError{ + fmt.Sprintf("%s %s", r.command, strings.Join(r.args, " ")), + err.Error(), + r.docsURL, + } + errors = append(errors, cerr) + fmt.Printf("\033[0;31mFAIL\033[0m\t\t%s\n", "-") + continue + } + version, err := r.checker(out) + if err != nil { + cerr := commandError{ + r.command, + err.Error(), + r.docsURL, + } + errors = append(errors, cerr) + fmt.Printf("\033[0;31mFAIL\033[0m\t\t%s\n", version) + } else { + fmt.Printf("\033[0;32mPASS\033[0m\t\t%s\n", version) + } + } + + if len(errors) > 0 { + // initialize tabwriter + w := new(tabwriter.Writer) + + // minwidth, tabwidth, padding, padchar, flags + w.Init(os.Stdout, 10, 12, 2, ' ', 0) + + defer w.Flush() + + fmt.Fprintf(w, "\n %s\t%s\t%s\t", "Command", "Error", "Info") + fmt.Fprintf(w, "\n %s\t%s\t%s\t", "---------", "---------", "---------") + + for _, e := range errors { + fmt.Fprintf(w, "\n%s\t%s\t%s\t", e.Command, e.ErrorText, e.Suggestion) + } + } + fmt.Println() + }, +} From e2b83f49801eda6ac3696bf43a4f2bf1e7f5f59b Mon Sep 17 00:00:00 2001 From: Direside Date: Thu, 21 May 2020 15:07:03 -0700 Subject: [PATCH 2/3] Added exit code 1 on failed commands. --- cmd/check.go | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/cmd/check.go b/cmd/check.go index d107aa900..a094f3de6 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -42,6 +42,23 @@ func (e *commandError) Error() string { return fmt.Sprintf("%s", e.ErrorText) } +func printErrors(errors []commandError) { + // initialize tabwriter + w := new(tabwriter.Writer) + + // minwidth, tabwidth, padding, padchar, flags + w.Init(os.Stdout, 10, 12, 2, ' ', 0) + + defer w.Flush() + + fmt.Fprintf(w, "\n %s\t%s\t%s\t", "Command", "Error", "Info") + fmt.Fprintf(w, "\n %s\t%s\t%s\t", "---------", "---------", "---------") + + for _, e := range errors { + fmt.Fprintf(w, "\n%s\t%s\t%s\t", e.Command, e.ErrorText, e.Suggestion) + } +} + var checkCmd = &cobra.Command{ Use: "check", Short: "Print the check number of commit0", @@ -200,6 +217,8 @@ var checkCmd = &cobra.Command{ fmt.Println("Checking Zero Requirements...") for _, r := range required { fmt.Printf("%s", r.name) + // In future we could parse the stderr and stdout separately, but for now it's nice to see + // the full output on a failure. out, err := exec.Command(r.command, r.args...).CombinedOutput() if err != nil { cerr := commandError{ @@ -226,21 +245,10 @@ var checkCmd = &cobra.Command{ } if len(errors) > 0 { - // initialize tabwriter - w := new(tabwriter.Writer) - - // minwidth, tabwidth, padding, padchar, flags - w.Init(os.Stdout, 10, 12, 2, ' ', 0) - - defer w.Flush() - - fmt.Fprintf(w, "\n %s\t%s\t%s\t", "Command", "Error", "Info") - fmt.Fprintf(w, "\n %s\t%s\t%s\t", "---------", "---------", "---------") - - for _, e := range errors { - fmt.Fprintf(w, "\n%s\t%s\t%s\t", e.Command, e.ErrorText, e.Suggestion) - } + printErrors((errors)) + os.Exit(1) } + fmt.Println() }, } From 0be16e72593c11f81ecaadce02db5edff0afdd04 Mon Sep 17 00:00:00 2001 From: Direside Date: Thu, 21 May 2020 18:53:49 -0700 Subject: [PATCH 3/3] Switched to using semver instead of custom checker functions. --- cmd/check.go | 228 +++++++++++++++++++-------------------------------- go.mod | 1 + go.sum | 1 + 3 files changed, 85 insertions(+), 145 deletions(-) diff --git a/cmd/check.go b/cmd/check.go index a094f3de6..d2f9fa68f 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -5,10 +5,10 @@ import ( "os" "os/exec" "regexp" - "strconv" "strings" "text/tabwriter" + "github.com/coreos/go-semver/semver" "github.com/spf13/cobra" ) @@ -17,11 +17,12 @@ func init() { } type requirement struct { - name string - command string - args []string - docsURL string - checker func([]byte) (string, error) + name string + command string + args []string + minVersion string + regexStr string + docsURL string } type versionError struct { @@ -59,6 +60,37 @@ func printErrors(errors []commandError) { } } +// getSemver uses the regular expression from the requirement to parse the +// output of a command and extract the version from it. Returns the version +// or an error if the version string could not be parsed. +func getSemver(req requirement, out []byte) (*semver.Version, error) { + re := regexp.MustCompile(req.regexStr) + v := re.FindStringSubmatch(string(out)) + if len(v) < 4 { + return nil, &commandError{ + req.command, + "Could not find version number in output", + fmt.Sprintf("Try running %s %s locally and checking it works.", req.command, strings.Join(req.args, " ")), + } + } + versionString := fmt.Sprintf("%s.%s.%s", v[1], v[2], v[3]) + version, err := semver.NewVersion(versionString) + if err != nil { + return version, err + } + return version, nil +} + +// checkSemver validates that the version of a tool meets the minimum required +// version listed in your requirement. Returns a boolean. +// For more information on parsing semver, see semver.org +// If your tool doesn't do full semver then you may need to add custom logic +// to support it. +func checkSemver(req requirement, actualVersion *semver.Version) bool { + requiredVersion := semver.New(req.minVersion) + return actualVersion.LessThan(*requiredVersion) +} + var checkCmd = &cobra.Command{ Use: "check", Short: "Print the check number of commit0", @@ -66,149 +98,45 @@ var checkCmd = &cobra.Command{ // Add any new requirements to this slice. required := []requirement{ { - name: "AWS CLI\t\t", - command: "aws", - args: []string{"--version"}, - docsURL: "", - checker: func(output []byte) (string, error) { - ver := "" - re := regexp.MustCompile(`aws-cli/([0-9]+)\.([0-9]+)\.([0-9]+)`) - m := re.FindStringSubmatch(string(output)) - major, err := strconv.ParseInt(m[1], 0, 64) - if err != nil { - return ver, err - } - minor, err := strconv.ParseInt(m[2], 0, 64) - if err != nil { - return ver, err - } - patch, err := strconv.ParseInt(m[3], 0, 64) - if err != nil { - return ver, err - } - - ver = fmt.Sprintf("%d.%d.%d", major, minor, patch) - - if major < 1 || (major == 1 && minor < 16) { - return ver, &versionError{"Requires 1.16 or greater."} - } - - return ver, err - }}, + name: "AWS CLI\t\t", + command: "aws", + args: []string{"--version"}, + regexStr: `aws-cli\/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)`, + minVersion: "1.16.0", + docsURL: "https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html", + }, { - name: "Kubectl\t\t", - command: "kubectl", - args: []string{"version", "--client=true"}, - docsURL: "https://kubernetes.io/docs/tasks/tools/install-kubectl/", - checker: func(output []byte) (string, error) { - ver := "" - re := regexp.MustCompile(`version\.Info{Major:"([0-9]+)", Minor:"([0-9]+)"`) - m := re.FindStringSubmatch(string(output)) - major, err := strconv.ParseInt(m[1], 0, 64) - if err != nil { - return ver, err - } - minor, err := strconv.ParseInt(m[2], 0, 64) - if err != nil { - return ver, err - } - - ver = fmt.Sprintf("%d.%d", major, minor) - - if major < 1 || (major == 1 && minor < 12) { - return ver, &versionError{"Requires 2.12 or greater."} - } - - return ver, err - }, + name: "Kubectl\t\t", + command: "kubectl", + args: []string{"version", "--client=true", "--short"}, + regexStr: `Client Version: v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)`, + minVersion: "1.15.2", + docsURL: "https://kubernetes.io/docs/tasks/tools/install-kubectl/", }, { - name: "Terraform\t", - command: "terraform", - args: []string{"version"}, - docsURL: "https://www.terraform.io/downloads.html", - checker: func(output []byte) (string, error) { - ver := "" - re := regexp.MustCompile(`Terraform v([0-9]+)\.([0-9]+)\.([0-9]+)`) - m := re.FindStringSubmatch(string(output)) - major, err := strconv.ParseInt(m[1], 0, 64) - if err != nil { - return ver, err - } - minor, err := strconv.ParseInt(m[2], 0, 64) - if err != nil { - return ver, err - } - patch, err := strconv.ParseInt(m[3], 0, 64) - if err != nil { - return ver, err - } - - ver = fmt.Sprintf("%d.%d.%d", major, minor, patch) - - if major < 0 || (major == 0 && minor < 12) { - return ver, &versionError{"Zero requires terraform 0.12 or greater."} - } - - return ver, err - }, + name: "Terraform\t", + command: "terraform", + args: []string{"version"}, + regexStr: `Terraform v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)`, + minVersion: "0.12.0", + docsURL: "https://www.terraform.io/downloads.html", }, { - name: "jq\t\t", - command: "jq", - args: []string{"--version"}, - docsURL: "https://stedolan.github.io/jq/download/", - checker: func(output []byte) (string, error) { - ver := "" - re := regexp.MustCompile(`jq-([0-9]+)\.([0-9]+)-`) - m := re.FindStringSubmatch(string(output)) - major, err := strconv.ParseInt(m[1], 0, 64) - if err != nil { - return ver, err - } - minor, err := strconv.ParseInt(m[2], 0, 64) - if err != nil { - return ver, err - } - - ver = fmt.Sprintf("%d.%d", major, minor) - - if major < 1 || (major == 1 && minor < 5) { - return ver, &versionError{"Requires jq version 1.15 or greater."} - } - - return ver, err - }}, + name: "jq\t\t", + command: "jq", + args: []string{"--version"}, + regexStr: `jq-(0|[1-9]\d*)\.(0|[1-9]\d*)-(0|[1-9]\d*)`, + minVersion: "1.5.0", + docsURL: "https://stedolan.github.io/jq/download/", + }, { - name: "Git\t\t", - command: "git", - args: []string{"version"}, - docsURL: "https://git-scm.com/book/en/v2/Getting-Started-Installing-Git", - checker: func(output []byte) (string, error) { - ver := "" - re := regexp.MustCompile(`git version ([0-9]+)\.([0-9]+)\.([0-9]+)`) - m := re.FindStringSubmatch(string(output)) - major, err := strconv.ParseInt(m[1], 0, 64) - if err != nil { - return ver, err - } - minor, err := strconv.ParseInt(m[2], 0, 64) - if err != nil { - return ver, err - } - patch, err := strconv.ParseInt(m[3], 0, 64) - if err != nil { - return ver, err - } - - ver = fmt.Sprintf("%d.%d.%d", major, minor, patch) - - if major < 2 || (major == 2 && minor < 12) { - return ver, &versionError{"Zero requires git version 2.12 or greater."} - } - - return ver, err - }}, + name: "Git\t\t", + command: "git", + args: []string{"version"}, + regexStr: `^git version (0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)`, + minVersion: "2.17.1", + docsURL: "https://git-scm.com/book/en/v2/Getting-Started-Installing-Git", + }, } // Store and errors from the commands we run. @@ -230,7 +158,7 @@ var checkCmd = &cobra.Command{ fmt.Printf("\033[0;31mFAIL\033[0m\t\t%s\n", "-") continue } - version, err := r.checker(out) + version, err := getSemver(r, out) if err != nil { cerr := commandError{ r.command, @@ -239,13 +167,23 @@ var checkCmd = &cobra.Command{ } errors = append(errors, cerr) fmt.Printf("\033[0;31mFAIL\033[0m\t\t%s\n", version) + continue + } + if checkSemver(r, version) { + cerr := commandError{ + r.command, + fmt.Sprintf("Version does not meet required. Want: %s; Got: %s", r.minVersion, version), + r.docsURL, + } + errors = append(errors, cerr) + fmt.Printf("\033[0;31mFAIL\033[0m\t\t%s\n", version) } else { fmt.Printf("\033[0;32mPASS\033[0m\t\t%s\n", version) } } if len(errors) > 0 { - printErrors((errors)) + printErrors(errors) os.Exit(1) } diff --git a/go.mod b/go.mod index cec0ce412..dc96bf4f2 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/chzyer/logex v1.1.10 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect + github.com/coreos/go-semver v0.2.0 github.com/google/uuid v1.1.1 github.com/gorilla/handlers v1.4.2 github.com/gorilla/mux v1.7.3 diff --git a/go.sum b/go.sum index f4ee86ea4..dc0a80f90 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=