diff --git a/cmd/check.go b/cmd/check.go new file mode 100644 index 000000000..d2f9fa68f --- /dev/null +++ b/cmd/check.go @@ -0,0 +1,192 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "regexp" + "strings" + "text/tabwriter" + + "github.com/coreos/go-semver/semver" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(checkCmd) +} + +type requirement struct { + name string + command string + args []string + minVersion string + regexStr string + docsURL string +} + +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) +} + +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) + } +} + +// 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", + 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"}, + 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", "--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"}, + 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"}, + 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"}, + 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. + errors := []commandError{} + + 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{ + 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 := getSemver(r, 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) + 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) + os.Exit(1) + } + + fmt.Println() + }, +} 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=