diff --git a/OWNERS b/OWNERS new file mode 100644 index 0000000000000000000000000000000000000000..b95e22b6e88beb0d607c8828450c5a0f12d2d321 --- /dev/null +++ b/OWNERS @@ -0,0 +1,3 @@ +approvers: + - lizrice + - jerbia diff --git a/cfg/config.yaml b/cfg/config.yaml index c8c89c9b647eeae87b84207b06a20f4386b309f2..07548acd2c30bbc6b81eb30cda3d9276a8743041 100644 --- a/cfg/config.yaml +++ b/cfg/config.yaml @@ -29,7 +29,7 @@ installation: conf: apiserver: /etc/kubernetes/apiserver scheduler: /etc/kubernetes/scheduler - controller-manager: /etc/kubernetes/apiserver + controller-manager: /etc/kubernetes/controller-manager node: bin: kubelet: kubelet diff --git a/cfg/master.yaml b/cfg/master.yaml index 6f2cc33b474a2ca01eab6f6c3b652b106c9dcd38..7005f9519f20bcbfdde1330b47a9fee0b01f41cc 100644 --- a/cfg/master.yaml +++ b/cfg/master.yaml @@ -479,19 +479,14 @@ groups: parameter to \"--experimental-encryption-provider-config=</path/to/EncryptionConfig/File>\"" scored: true -# TODO: provide flag to WARN of manual tasks which we can't automate. - id: 1.1.35 text: "Ensure that the encryption provider is set to aescbc (Scored)" audit: "ps -ef | grep $apiserverbin | grep -v grep" - tests: - test_items: - - flag: "requires manual intervention" - set: true + type: "manual" remediation: "Follow the Kubernetes documentation and configure a EncryptionConfig file. In this file, choose aescbc as the encryption provider" scored: true - - id: 1.2 text: "Scheduler" checks: @@ -573,7 +568,13 @@ groups: KUBE_CONTROLLER_MANAGER_ARGS parameter to include --root-ca-file=<file>" scored: true -# TODO: 1.3.6 is manual, provide way to WARN + - id: 1.3.6 + text: "Apply Security Context to Your Pods and Containers (Not Scored)" + type: "manual" + remediation: "Edit the /etc/kubernetes/controller-manager file on the master node and set the + KUBE_CONTROLLER_MANAGER_ARGS parameter to a value to include + \"--feature-gates=RotateKubeletServerCertificate=true\"" + scored: false - id: 1.3.7 text: " Ensure that the RotateKubeletServerCertificate argument is set to true (Scored)" @@ -751,6 +752,20 @@ groups: chmod 700 /var/lib/etcd/default.etcd" scored: true + - id: 1.4.12 + text: "Ensure that the etcd data directory ownership is set to etcd:etcd (Scored)" + audit: "ps -ef | grep $etcdbin | grep -v grep | grep -o data-dir=.* | cut -d= -f2 | xargs stat -c %U:%G" + tests: + test_items: + - flag: "etcd:etcd" + set: true + remediation: "On the etcd server node, get the etcd data directory, passed as an argument --data-dir , + from the below command:\n + ps -ef | grep etcd\n + Run the below command (based on the etcd data directory found above). For example,\n + chown etcd:etcd /var/lib/etcd/default.etcd" + scored: true + - id: 1.5 text: "etcd" checks: @@ -893,3 +908,65 @@ groups: remediation: "Follow the etcd documentation and create a dedicated certificate authority setup for the etcd service." scored: false + +- id: 1.6 + text: "General Security Primitives" + checks: + - id: 1.6.1 + text: "Ensure that the cluster-admin role is only used where required (Not Scored)" + type: "manual" + remediation: "Remove any unneeded clusterrolebindings: kubectl delete clusterrolebinding [name]" + scored: false + + - id: 1.6.2 + text: "Create Pod Security Policies for your cluster (Not Scored)" + type: "manual" + remediation: "Follow the documentation and create and enforce Pod Security Policies for your cluster. + Additionally, you could refer the \"CIS Security Benchmark for Docker\" and follow the + suggested Pod Security Policies for your environment." + scored: false + + - id: 1.6.3 + text: "Create administrative boundaries between resources using namespaces (Not Scored)" + type: "manual" + remediation: "Follow the documentation and create namespaces for objects in your deployment as you + need them." + scored: false + + - id: 1.6.4 + text: "Create network segmentation using Network Policies (Not Scored)" + type: "manual" + remediation: "Follow the documentation and create NetworkPolicy objects as you need them." + scored: false + + - id: 1.6.5 + text: "Ensure that the seccomp profile is set to docker/default in your pod definitions (Not Scored)" + type: "manual" + remediation: "Seccomp is an alpha feature currently. By default, all alpha features are disabled. So, you + would need to enable alpha features in the apiserver by passing \"--feature- + gates=AllAlpha=true\" argument.\n + Edit the $apiserverconf file on the master node and set the KUBE_API_ARGS + parameter to \"--feature-gates=AllAlpha=true\" + KUBE_API_ARGS=\"--feature-gates=AllAlpha=true\"" + scored: false + + - id: 1.6.6 + text: "Apply Security Context to Your Pods and Containers (Not Scored)" + type: "manual" + remediation: "Follow the Kubernetes documentation and apply security contexts to your pods. For a + suggested list of security contexts, you may refer to the CIS Security Benchmark for Docker + Containers." + scored: false + + - id: 1.6.7 + text: "Configure Image Provenance using ImagePolicyWebhook admission controller (Not Scored)" + type: "manual" + remediation: "Follow the Kubernetes documentation and setup image provenance." + scored: false + + - id: 1.6.8 + text: "Configure Network policies as appropriate (Not Scored)" + type: "manual" + remediation: "Follow the Kubernetes documentation and setup network policies as appropriate." + scored: false + diff --git a/check/check.go b/check/check.go index 86f3939a2edd55cd32b014eb454c2713766811bb..7a41b199c5c84a34993ae62b9e2291c0e2cf7e8d 100644 --- a/check/check.go +++ b/check/check.go @@ -18,10 +18,11 @@ import ( "bytes" "fmt" "io" - "os" "os/exec" "regexp" "strings" + + "github.com/golang/glog" ) // NodeType indicates the type of node (master, node, federated). @@ -61,6 +62,7 @@ type Check struct { ID string `yaml:"id" json:"id"` Text string Audit string `json:"omit"` + Type string `json:"type"` Commands []*exec.Cmd `json:"omit"` Tests *tests `json:"omit"` Set bool `json:"omit"` @@ -70,7 +72,13 @@ type Check struct { // Run executes the audit commands specified in a check and outputs // the results. -func (c *Check) Run(verbose bool) { +func (c *Check) Run() { + // If check type is manual, force result to WARN. + if c.Type == "manual" { + c.State = WARN + return + } + var out bytes.Buffer var errmsgs string @@ -147,9 +155,7 @@ func (c *Check) Run(verbose bool) { i++ } - if verbose && errmsgs != "" { - fmt.Fprintf(os.Stderr, "%s\n", errmsgs) - } + glog.V(2).Info("%s\n", errmsgs) res := c.Tests.execute(out.String()) if res { diff --git a/check/controls.go b/check/controls.go index 8f7530c8076b1a9330c1631583d28438c10b1bca..dfea0067178a0b1436669852993716d572104ae9 100644 --- a/check/controls.go +++ b/check/controls.go @@ -68,7 +68,7 @@ func NewControls(t NodeType, in []byte) (*Controls, error) { } // RunGroup runs all checks in a group. -func (controls *Controls) RunGroup(verbose bool, gids ...string) Summary { +func (controls *Controls) RunGroup(gids ...string) Summary { g := []*Group{} controls.Summary.Pass, controls.Summary.Fail, controls.Summary.Warn = 0, 0, 0 @@ -82,7 +82,7 @@ func (controls *Controls) RunGroup(verbose bool, gids ...string) Summary { for _, gid := range gids { if gid == group.ID { for _, check := range group.Checks { - check.Run(verbose) + check.Run() summarize(controls, check) } @@ -96,7 +96,7 @@ func (controls *Controls) RunGroup(verbose bool, gids ...string) Summary { } // RunChecks runs the checks with the supplied IDs. -func (controls *Controls) RunChecks(verbose bool, ids ...string) Summary { +func (controls *Controls) RunChecks(ids ...string) Summary { g := []*Group{} m := make(map[string]*Group) controls.Summary.Pass, controls.Summary.Fail, controls.Summary.Warn = 0, 0, 0 @@ -110,7 +110,7 @@ func (controls *Controls) RunChecks(verbose bool, ids ...string) Summary { for _, check := range group.Checks { for _, id := range ids { if id == check.ID { - check.Run(verbose) + check.Run() summarize(controls, check) // Check if we have already added this checks group. diff --git a/cmd/common.go b/cmd/common.go index ae8119de8554c1cbd2bedee83b1fe00842224431..d27dc1f9dbbdafe23f7459cd92764fa7ee37a60b 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -17,7 +17,6 @@ package cmd import ( "fmt" "io/ioutil" - "os" "strings" "github.com/aquasecurity/kube-bench/check" @@ -46,7 +45,8 @@ var ( errmsgs string // TODO: Consider specifying this in config file. - kubeVersion = "1.7.0" + kubeMajorVersion = "1" + kubeMinorVersion = "7" ) func runChecks(t check.NodeType) { @@ -59,7 +59,7 @@ func runChecks(t check.NodeType) { schedulerBin = viper.GetString("installation." + installation + ".master.bin.scheduler") schedulerConf = viper.GetString("installation." + installation + ".master.conf.scheduler") controllerManagerBin = viper.GetString("installation." + installation + ".master.bin.controller-manager") - controllerManagerConf = viper.GetString("installation." + installation + ".master.conf.controler-manager") + controllerManagerConf = viper.GetString("installation." + installation + ".master.conf.controller-manager") config = viper.GetString("installation." + installation + ".config") etcdBin = viper.GetString("etcd.bin") @@ -78,7 +78,8 @@ func runChecks(t check.NodeType) { fedControllerManagerBin = viper.GetString("installation." + installation + ".federated.bin.controller-manager") // Run kubernetes installation validation checks. - warns := verifyNodeType(t) + verifyKubeVersion(kubeMajorVersion, kubeMinorVersion) + verifyNodeType(t) switch t { case check.MASTER: @@ -91,8 +92,7 @@ func runChecks(t check.NodeType) { in, err := ioutil.ReadFile(file) if err != nil { - fmt.Fprintf(os.Stderr, "error opening %s controls file: %v\n", t, err) - os.Exit(1) + exitWithError(fmt.Errorf("error opening %s controls file: %v", t, err)) } // Variable substitutions. Replace all occurrences of variables in controls files. @@ -102,7 +102,6 @@ func runChecks(t check.NodeType) { s = strings.Replace(s, "$schedulerconf", schedulerConf, -1) s = strings.Replace(s, "$controllermanagerbin", controllerManagerBin, -1) s = strings.Replace(s, "$controllermanagerconf", controllerManagerConf, -1) - s = strings.Replace(s, "$controllermanagerconf", controllerManagerConf, -1) s = strings.Replace(s, "$config", config, -1) s = strings.Replace(s, "$etcdbin", etcdBin, -1) @@ -120,63 +119,47 @@ func runChecks(t check.NodeType) { controls, err := check.NewControls(t, []byte(s)) if err != nil { - fmt.Fprintf(os.Stderr, "error setting up %s controls: %v\n", t, err) - os.Exit(1) + exitWithError(fmt.Errorf("error setting up %s controls: %v", t, err)) } if groupList != "" && checkList == "" { ids := cleanIDs(groupList) - summary = controls.RunGroup(verbose, ids...) + summary = controls.RunGroup(ids...) } else if checkList != "" && groupList == "" { ids := cleanIDs(checkList) - summary = controls.RunChecks(verbose, ids...) + summary = controls.RunChecks(ids...) } else if checkList != "" && groupList != "" { - fmt.Fprintf(os.Stderr, "group option and check option can't be used together\n") - os.Exit(1) + exitWithError(fmt.Errorf("group option and check option can't be used together")) } else { - summary = controls.RunGroup(verbose) + summary = controls.RunGroup() } // if we successfully ran some tests and it's json format, ignore the warnings if (summary.Fail > 0 || summary.Warn > 0 || summary.Pass > 0) && jsonFmt { out, err := controls.JSON() if err != nil { - fmt.Fprintf(os.Stderr, "failed to output in JSON format: %v\n", err) - os.Exit(1) + exitWithError(fmt.Errorf("failed to output in JSON format: %v", err)) } fmt.Println(string(out)) } else { - prettyPrint(warns, controls, summary) + prettyPrint(controls, summary) } } // verifyNodeType checks the executables and config files are as expected // for the specified tests (master, node or federated). -func verifyNodeType(t check.NodeType) []string { - var w []string - // Always clear out error messages. - errmsgs = "" - +func verifyNodeType(t check.NodeType) { switch t { case check.MASTER: - w = append(w, verifyBin(apiserverBin, schedulerBin, controllerManagerBin)...) - w = append(w, verifyConf(apiserverConf, schedulerConf, controllerManagerConf)...) - w = append(w, verifyKubeVersion(apiserverBin)...) + verifyBin(apiserverBin, schedulerBin, controllerManagerBin) + verifyConf(apiserverConf, schedulerConf, controllerManagerConf) case check.NODE: - w = append(w, verifyBin(kubeletBin, proxyBin)...) - w = append(w, verifyConf(kubeletConf, proxyConf)...) - w = append(w, verifyKubeVersion(kubeletBin)...) + verifyBin(kubeletBin, proxyBin) + verifyConf(kubeletConf, proxyConf) case check.FEDERATED: - w = append(w, verifyBin(fedApiserverBin, fedControllerManagerBin)...) - w = append(w, verifyKubeVersion(fedApiserverBin)...) - } - - if verbose { - fmt.Fprintf(os.Stderr, "%s\n", errmsgs) + verifyBin(fedApiserverBin, fedControllerManagerBin) } - - return w } // colorPrint outputs the state in a specific colour, along with a message string @@ -186,13 +169,9 @@ func colorPrint(state check.State, s string) { } // prettyPrint outputs the results to stdout in human-readable format -func prettyPrint(warnings []string, r *check.Controls, summary check.Summary) { +func prettyPrint(r *check.Controls, summary check.Summary) { colorPrint(check.INFO, fmt.Sprintf("Using config file: %s\n", viper.ConfigFileUsed())) - for _, w := range warnings { - colorPrint(check.WARN, w) - } - colorPrint(check.INFO, fmt.Sprintf("%s %s\n", r.ID, r.Text)) for _, g := range r.Groups { colorPrint(check.INFO, fmt.Sprintf("%s %s\n", g.ID, g.Text)) diff --git a/cmd/root.go b/cmd/root.go index 5429c2c5649e6839de8718fe7309285c42293edc..aed8b422c030a4a7e26d322993b63e3e30087c95 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,6 +15,7 @@ package cmd import ( + goflag "flag" "fmt" "os" @@ -34,11 +35,12 @@ var ( nodeFile string federatedFile string + loud bool + kubeConfDir string etcdConfDir string flanneldConfDir string - verbose bool installation string ) @@ -52,6 +54,9 @@ var RootCmd = &cobra.Command{ // Execute adds all child commands to the root command sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { + goflag.Set("logtostderr", "true") + goflag.CommandLine.Parse([]string{}) + if err := RootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(-1) @@ -83,7 +88,11 @@ func init() { `Run all the checks under this comma-delimited list of groups. Example --group="1.1"`, ) RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is ./cfg/config.yaml)") - RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output (default false)") + + goflag.CommandLine.VisitAll(func(goflag *goflag.Flag) { + RootCmd.PersistentFlags().AddGoFlag(goflag) + }) + } // initConfig reads in config file and ENV variables if set. @@ -103,5 +112,4 @@ func initConfig() { colorPrint(check.FAIL, fmt.Sprintf("Failed to read config file: %v\n", err)) os.Exit(1) } - } diff --git a/cmd/util.go b/cmd/util.go index 6a1f790bff214db46c15c2ac33bb2afe0f71ff86..937e3e0f18c9c24842459854beba482e36908b14 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -4,10 +4,12 @@ import ( "fmt" "os" "os/exec" + "regexp" "strings" "github.com/aquasecurity/kube-bench/check" "github.com/fatih/color" + "github.com/golang/glog" ) var ( @@ -20,11 +22,35 @@ var ( } ) -func handleError(err error, context string) (errmsg string) { +func printlnWarn(msg string) { + fmt.Fprintf(os.Stderr, "[%s] %s\n", + colors[check.WARN].Sprintf("%s", check.WARN), + msg, + ) +} + +func sprintlnWarn(msg string) string { + return fmt.Sprintf("[%s] %s", + colors[check.WARN].Sprintf("%s", check.WARN), + msg, + ) +} + +func exitWithError(err error) { + fmt.Fprintf(os.Stderr, "\n%v\n", err) + os.Exit(1) +} + +func continueWithError(err error, msg string) string { if err != nil { - errmsg = fmt.Sprintf("%s, error: %s\n", context, err) + glog.V(1).Info(err) } - return + + if msg != "" { + fmt.Fprintf(os.Stderr, "%s\n", msg) + } + + return "" } func cleanIDs(list string) []string { @@ -38,76 +64,121 @@ func cleanIDs(list string) []string { return ids } -func verifyConf(confPath ...string) []string { - var w []string +func verifyConf(confPath ...string) { + var missing string + for _, c := range confPath { if _, err := os.Stat(c); err != nil && os.IsNotExist(err) { - w = append(w, fmt.Sprintf("config file %s does not exist\n", c)) + continueWithError(err, "") + missing += c + ", " } } - return w + if len(missing) > 0 { + missing = strings.Trim(missing, ", ") + printlnWarn(fmt.Sprintf("Missing kubernetes config files: %s", missing)) + } + } -func verifyBin(binPath ...string) []string { - var w []string - var binList string +func verifyBin(binPath ...string) { + var binSlice []string + var bin string + var missing string + var notRunning string // Construct proc name for ps(1) for _, b := range binPath { - binList += b + "," _, err := exec.LookPath(b) - errmsgs += handleError( - err, - fmt.Sprintf("%s: command not found in path", b), - ) + bin = bin + "," + b + binSlice = append(binSlice, b) + if err != nil { + missing += b + ", " + continueWithError(err, "") + } } + bin = strings.Trim(bin, ",") - binList = strings.Trim(binList, ",") - - // Run ps command - cmd := exec.Command("ps", "-C", binList, "-o", "cmd", "--no-headers") + cmd := exec.Command("ps", "-C", bin, "-o", "cmd", "--no-headers") out, err := cmd.Output() - errmsgs += handleError( - err, - fmt.Sprintf("failed to run: %s", cmd.Args), - ) + if err != nil { + continueWithError(fmt.Errorf("%s: %s", cmd.Args, err), "") + } - // Actual verification - for _, b := range binPath { + for _, b := range binSlice { matched := strings.Contains(string(out), b) if !matched { - w = append(w, fmt.Sprintf("%s is not running\n", b)) + notRunning += b + ", " } } - return w + if len(missing) > 0 { + missing = strings.Trim(missing, ", ") + printlnWarn(fmt.Sprintf("Missing kubernetes binaries: %s", missing)) + } + + if len(notRunning) > 0 { + notRunning = strings.Trim(notRunning, ", ") + printlnWarn(fmt.Sprintf("Kubernetes binaries not running: %s", notRunning)) + } } -func verifyKubeVersion(b string) []string { +func verifyKubeVersion(major string, minor string) { // These executables might not be on the user's path. - // TODO! Check the version number using kubectl, which is more likely to be on the path. - var w []string - _, err := exec.LookPath(b) - errmsgs += handleError( - err, - fmt.Sprintf("%s: command not found on path - version check skipped", b), - ) + _, err := exec.LookPath("kubectl") + if err != nil { + continueWithError(err, sprintlnWarn("Kubernetes version check skipped")) + return + } - // Check version - cmd := exec.Command(b, "--version") + cmd := exec.Command("kubectl", "version") out, err := cmd.Output() - errmsgs += handleError( - err, - fmt.Sprintf("failed to run:%s", cmd.Args), - ) + if err != nil { + s := fmt.Sprintf("Kubernetes version check skipped with error %v", err) + continueWithError(err, sprintlnWarn(s)) + return + } + + msg := checkVersion("Client", string(out), major, minor) + if msg != "" { + continueWithError(fmt.Errorf(msg), msg) + } - matched := strings.Contains(string(out), kubeVersion) - if !matched { - w = append(w, fmt.Sprintf("%s unsupported version\n", b)) + msg = checkVersion("Server", string(out), major, minor) + if msg != "" { + continueWithError(fmt.Errorf(msg), msg) } +} + +var regexVersionMajor = regexp.MustCompile("Major:\"([0-9]+)\"") +var regexVersionMinor = regexp.MustCompile("Minor:\"([0-9]+)\"") - return w +func checkVersion(x string, s string, expMajor string, expMinor string) string { + regexVersion, err := regexp.Compile(x + " Version: version.Info{(.*)}") + if err != nil { + return fmt.Sprintf("Error checking Kubernetes version: %v", err) + } + + ss := regexVersion.FindString(s) + major := versionMatch(regexVersionMajor, ss) + minor := versionMatch(regexVersionMinor, ss) + if major == "" || minor == "" { + return fmt.Sprintf("Couldn't find %s version from kubectl output '%s'", x, s) + } + + if major != expMajor || minor != expMinor { + return fmt.Sprintf("Unexpected %s version %s.%s", x, major, minor) + } + + return "" +} + +func versionMatch(r *regexp.Regexp, s string) string { + match := r.FindStringSubmatch(s) + if len(match) < 2 { + return "" + } + return match[1] } diff --git a/cmd/util_test.go b/cmd/util_test.go new file mode 100644 index 0000000000000000000000000000000000000000..74d955f780332758d4811af6349afa7cb249a01c --- /dev/null +++ b/cmd/util_test.go @@ -0,0 +1,77 @@ +// Copyright © 2017 Aqua Security Software Ltd. <info@aquasec.com> +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "regexp" + "testing" +) + +func TestCheckVersion(t *testing.T) { + kubeoutput := `Client Version: version.Info{Major:"1", Minor:"7", GitVersion:"v1.7.0", GitCommit:"d3ada0119e776222f11ec7945e6d860061339aad", GitTreeState:"clean", BuildDate:"2017-06-30T09:51:01Z", GoVersion:"go1.8.3", Compiler:"gc", Platform:"darwin/amd64"} + Server Version: version.Info{Major:"1", Minor:"7", GitVersion:"v1.7.0", GitCommit:"d3ada0119e776222f11ec7945e6d860061339aad", GitTreeState:"clean", BuildDate:"2017-07-26T00:12:31Z", GoVersion:"go1.8.3", Compiler:"gc", Platform:"linux/amd64"}` + cases := []struct { + t string + s string + major string + minor string + exp string + }{ + {t: "Client", s: kubeoutput, major: "1", minor: "7"}, + {t: "Server", s: kubeoutput, major: "1", minor: "7"}, + {t: "Client", s: kubeoutput, major: "1", minor: "6", exp: "Unexpected Client version 1.7"}, + {t: "Client", s: kubeoutput, major: "2", minor: "0", exp: "Unexpected Client version 1.7"}, + {t: "Server", s: "something unexpected", major: "2", minor: "0", exp: "Couldn't find Server version from kubectl output 'something unexpected'"}, + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + m := checkVersion(c.t, c.s, c.major, c.minor) + if m != c.exp { + t.Fatalf("Got: %s, expected: %s", m, c.exp) + } + }) + } + +} + +func TestVersionMatch(t *testing.T) { + minor := regexVersionMinor + major := regexVersionMajor + client := `Client Version: version.Info{Major:"1", Minor:"7", GitVersion:"v1.7.0", GitCommit:"d3ada0119e776222f11ec7945e6d860061339aad", GitTreeState:"clean", BuildDate:"2017-06-30T09:51:01Z", GoVersion:"go1.8.3", Compiler:"gc", Platform:"darwin/amd64"}` + server := `Server Version: version.Info{Major:"1", Minor:"7", GitVersion:"v1.7.0", GitCommit:"d3ada0119e776222f11ec7945e6d860061339aad", GitTreeState:"clean", BuildDate:"2017-07-26T00:12:31Z", GoVersion:"go1.8.3", Compiler:"gc", Platform:"linux/amd64"}` + + cases := []struct { + r *regexp.Regexp + s string + exp string + }{ + {r: major, s: server, exp: "1"}, + {r: minor, s: server, exp: "7"}, + {r: major, s: client, exp: "1"}, + {r: minor, s: client, exp: "7"}, + {r: major, s: "Some unexpected string"}, + {r: minor}, // Checking that we don't fall over if the string is empty + } + + for _, c := range cases { + t.Run("", func(t *testing.T) { + m := versionMatch(c.r, c.s) + if m != c.exp { + t.Fatalf("Got %s expected %s", m, c.exp) + } + }) + } +}