1
0
mirror of https://github.com/aquasecurity/kube-bench.git synced 2024-11-26 18:08:15 +00:00

Merge remote-tracking branch 'origin/master' into support for multiple

Kubernetes versions.
This commit is contained in:
Abubakr-Sadik Nii Nai Davis 2017-09-20 00:39:30 +00:00
commit d9e1eee2cd
12 changed files with 558 additions and 212 deletions

View File

@ -47,7 +47,10 @@ Flags:
## Configuration ## Configuration
Kubernetes config and binary file locations and names can vary from installation to installation, so these are configurable in the `cfg/config.yaml` file. Kubernetes config and binary file locations and names can vary from installation to installation, so these are configurable in the `cfg/config.yaml` file.
They also tend to vary according to which tool was used to install Kubernetes. You can use the `--installation` flag to pick up a different default set of file names and locations. Again these defaults are configurable through `cfg/config.yaml` (and pull requests to correct or add default file locations are especially welcome). For each type of node (*master*, *node* or *federated*) there is a list of components, and for each component there is a set of binaries (*bins*) and config files (*confs*) that kube-bench will look for (in the order they are listed). If your installation uses a different binary name or config file location for a Kubernetes component, you can add it to `cfg/config.yaml`.
* **bins** - If there is a *bins* list for a component, at least one of these binaries must be running. The tests will consider the parameters for the first binary in the list found to be running.
* **confs** - If one of the listed config files is found, this will be considered for the test. Tests can continue even if no config file is found. If no file is found at any of the listed locations, and a *defaultconf* location is given for the component, the test will give remediation advice using the *defaultconf* location.
## Test config YAML representation ## Test config YAML representation
The tests are represented as YAML documents (installed by default into ./cfg). The tests are represented as YAML documents (installed by default into ./cfg).

View File

@ -637,7 +637,7 @@ groups:
- id: 1.4.3 - id: 1.4.3
text: "Ensure that the config file permissions are set to 644 or more restrictive (Scored)" text: "Ensure that the config file permissions are set to 644 or more restrictive (Scored)"
audit: "/bin/sh -c 'if test -e $config; then stat -c %a $config; fi'" audit: "/bin/sh -c 'if test -e $kubernetesconf; then stat -c %a $kubernetesconf; fi'"
tests: tests:
bin_op: or bin_op: or
test_items: test_items:
@ -657,12 +657,12 @@ groups:
value: "600" value: "600"
set: true set: true
remediation: "Run the below command (based on the file location on your system) on the master node. remediation: "Run the below command (based on the file location on your system) on the master node.
\nFor example, chmod 644 $config" \nFor example, chmod 644 $kubernetesconf"
scored: true scored: true
- id: 1.4.4 - id: 1.4.4
text: "Ensure that the config file ownership is set to root:root (Scored)" text: "Ensure that the config file ownership is set to root:root (Scored)"
audit: "/bin/sh -c 'if test -e $config; then stat -c %U:%G $config; fi'" audit: "/bin/sh -c 'if test -e $kubernetesconf; then stat -c %U:%G $kubernetesconf; fi'"
tests: tests:
test_items: test_items:
- flag: "root:root" - flag: "root:root"
@ -671,7 +671,7 @@ groups:
value: "root:root" value: "root:root"
set: true set: true
remediation: "Run the below command (based on the file location on your system) on the master node. remediation: "Run the below command (based on the file location on your system) on the master node.
\nFor example, chown root:root $config" \nFor example, chown root:root $kubernetesconf"
scored: true scored: true
- id: 1.4.5 - id: 1.4.5

View File

@ -18,7 +18,7 @@ groups:
op: eq op: eq
value: false value: false
set: true set: true
remediation: "Edit the $config file on each node and set the KUBE_ALLOW_PRIV remediation: "Edit the $kubeletconf file on each node and set the KUBE_ALLOW_PRIV
parameter to \"--allow-privileged=false\"" parameter to \"--allow-privileged=false\""
scored: true scored: true
@ -200,7 +200,7 @@ groups:
op: eq op: eq
value: true value: true
set: true set: true
remediation: "Edit the /etc/kubernetes/kubelet file on each node and set the KUBELET_ARGS parameter remediation: "Edit the $kubeletconf file on each node and set the KUBELET_ARGS parameter
to a value to include \"--feature-gates=RotateKubeletClientCertificate=true\"." to a value to include \"--feature-gates=RotateKubeletClientCertificate=true\"."
scored: true scored: true
@ -214,7 +214,7 @@ groups:
op: eq op: eq
value: true value: true
set: true set: true
remediation: "Edit the /etc/kubernetes/kubelet file on each node and set the KUBELET_ARGS parameter remediation: "Edit the $kubeletconf file on each node and set the KUBELET_ARGS parameter
to a value to include \"--feature-gates=RotateKubeletServerCertificate=true\"." to a value to include \"--feature-gates=RotateKubeletServerCertificate=true\"."
scored: true scored: true
@ -223,7 +223,7 @@ groups:
checks: checks:
- id: 2.2.1 - id: 2.2.1
text: "Ensure that the config file permissions are set to 644 or more restrictive (Scored)" text: "Ensure that the config file permissions are set to 644 or more restrictive (Scored)"
audit: "/bin/sh -c 'if test -e $config; then stat -c %a $config; fi'" audit: "/bin/sh -c 'if test -e $kubernetesconf; then stat -c %a $kubernetesconf; fi'"
tests: tests:
bin_op: or bin_op: or
test_items: test_items:
@ -243,12 +243,12 @@ groups:
value: "600" value: "600"
set: true set: true
remediation: "Run the below command (based on the file location on your system) on the each worker node. remediation: "Run the below command (based on the file location on your system) on the each worker node.
\nFor example, chmod 644 $config" \nFor example, chmod 644 $kubernetesconf"
scored: true scored: true
- id: 2.2.2 - id: 2.2.2
text: "Ensure that the config file ownership is set to root:root (Scored)" text: "Ensure that the config file ownership is set to root:root (Scored)"
audit: "/bin/sh -c 'if test -e $config; then stat -c %U:%G $config; fi'" audit: "/bin/sh -c 'if test -e $kubernetesconf; then stat -c %U:%G $kubernetesconf; fi'"
tests: tests:
test_items: test_items:
- flag: "root:root" - flag: "root:root"
@ -257,7 +257,7 @@ groups:
value: root:root value: root:root
set: true set: true
remediation: "Run the below command (based on the file location on your system) on the each worker node. remediation: "Run the below command (based on the file location on your system) on the each worker node.
\nFor example, chown root:root $config" \nFor example, chown root:root $kubernetesconf"
scored: true scored: true
- id: 2.2.3 - id: 2.2.3

View File

@ -7,106 +7,112 @@
# nodeControls: ./cfg/node.yaml # nodeControls: ./cfg/node.yaml
# federatedControls: ./cfg/federated.yaml # federatedControls: ./cfg/federated.yaml
## Support components master:
etcd: components:
bin: etcd - apiserver
conf: /etc/etcd/etcd.conf - scheduler
- controllermanager
- etcd
- flanneld
# kubernetes is a component to cover the config file /etc/kubernetes/config that is referred to in the benchmark
- kubernetes
flanneld: kubernetes:
bin: flanneld defaultconf: /etc/kubernetes/config
conf: /etc/sysconfig/flanneld
# Installation apiserver:
# Configure kubernetes component binaries and paths to their configuration files. bins:
installation: - "kube-apiserver"
default: - "hyperkube apiserver"
config: /etc/kubernetes/config - "apiserver"
master: confs:
bin: - /etc/kubernetes/manifests/kube-apiserver.yaml
apiserver: apiserver - /etc/kubernetes/apiserver.conf
scheduler: scheduler - /etc/kubernetes/apiserver
controller-manager: controller-manager defaultconf: /etc/kubernetes/apiserver
conf:
apiserver: /etc/kubernetes/apiserver
scheduler: /etc/kubernetes/scheduler
controller-manager: /etc/kubernetes/controller-manager
node:
bin:
kubelet: kubelet
proxy: proxy
conf:
kubelet: /etc/kubernetes/kubelet
proxy: /etc/kubernetes/proxy
federated:
bin:
apiserver: federation-apiserver
controller-manager: federation-controller-manager
kops: scheduler:
config: /etc/kubernetes/config bins:
master: - "kube-scheduler"
bin: - "hyperkube scheduler"
apiserver: apiserver - "scheduler"
scheduler: scheduler confs:
controller-manager: controller-manager - /etc/kubernetes/manifests/kube-scheduler.yaml
conf: - /etc/kubernetes/scheduler.conf
apiserver: /etc/kubernetes/apiserver - /etc/kubernetes/scheduler
scheduler: /etc/kubernetes/scheduler defaultconf: /etc/kubernetes/scheduler
controller-manager: /etc/kubernetes/apiserver
node: controllermanager:
bin: bins:
kubelet: kubelet - "kube-controller-manager"
proxy: proxy - "hyperkube controller-manager"
conf: - "controller-manager"
kubelet: /etc/kubernetes/kubelet confs:
proxy: /etc/kubernetes/proxy - /etc/kubernetes/manifests/kube-controller-manager.yaml
federated: - /etc/kubernetes/controller-manager.conf
bin: - /etc/kubernetes/controller-manager
apiserver: federation-apiserver defaultconf: /etc/kubernetes/controller-manager
controller-manager: federation-controller-manager
etcd:
optional: true
bins:
- "etcd"
confs:
- /etc/kubernetes/manifests/etcd.yaml
- /etc/etcd/etcd.conf
defaultconf: /etc/etcd/etcd.conf
flanneld:
optional: true
bins:
- flanneld
defaultconf: /etc/sysconfig/flanneld
node:
components:
- kubelet
- proxy
# kubernetes is a component to cover the config file /etc/kubernetes/config that is referred to in the benchmark
- kubernetes
kubernetes:
defaultconf: /etc/kubernetes/config
kubelet:
bins:
- "hyperkube kubelet"
- "kubelet"
confs:
- /etc/kubernetes/kubelet.conf
- /etc/kubernetes/kubelet
defaultconf: "/etc/kubernetes/kubelet.conf"
proxy:
bins:
- "kube-proxy"
- "hyperkube proxy"
- "proxy"
confs:
- /etc/kubernetes/proxy.conf
- /etc/kubernetes/proxy
- /etc/kubernetes/addons/kube-proxy-daemonset.yaml
federated:
components:
- fedapiserver
- fedcontrollermanager
fedapiserver:
bins:
- "hyperkube federation-apiserver"
- "kube-federation-apiserver"
- "federation-apiserver"
fedcontrollermanager:
bins:
- "hyperkube federation-controller-manager"
- "kube-federation-controller-manager"
- "federation-controller-manager"
hyperkube:
config: /etc/kubernetes/config
master:
bin:
apiserver: hyperkube apiserver
scheduler: hyperkube scheduler
controller-manager: hyperkube controller-manager
conf:
apiserver: /etc/kubernetes/manifests/kube-apiserver.yaml
scheduler: /etc/kubernetes/manifests/kube-scheduler.yaml
controller-manager: /etc/kubernetes/manifests/kube-controller-manager.yaml
node:
bin:
kubelet: hyperkube kubelet
proxy: hyperkube proxy
conf:
kubelet: /etc/kubernetes/kubelet
proxy: /etc/kubernetes/addons/kube-proxy-daemonset.yaml
federated:
bin:
apiserver: hyperkube federation-apiserver
controller-manager: hyperkube federation-controller-manager
kubeadm:
config: /etc/kubernetes/config
master:
bin:
apiserver: kube-apiserver
scheduler: kube-scheduler
controller-manager: kube-controller-manager
conf:
apiserver: /etc/kubernetes/admin.conf
scheduler: /etc/kubernetes/scheduler.conf
controller-manager: /etc/kubernetes/controller-manager.conf
node:
bin:
kubelet: kubelet
proxy: kube-proxy
conf:
kubelet: /etc/kubernetes/kubelet.conf
proxy: /etc/kubernetes/proxy.conf
federated:
bin:
apiserver: kube-federation-apiserver
controller-manager: kube-federation-controller-manager

View File

@ -156,7 +156,9 @@ func (c *Check) Run() {
i++ i++
} }
glog.V(2).Info("%s\n", errmsgs) if errmsgs != "" {
glog.V(2).Info(errmsgs)
}
res := c.Tests.execute(out.String()) res := c.Tests.execute(out.String())
if res { if res {

View File

@ -116,3 +116,45 @@ groups:
op: eq op: eq
value: "600" value: "600"
set: true set: true
- id: 10
text: "flag value includes some value in a comma-separated list, value is last in list"
tests:
test_items:
- flag: "--admission-control"
compare:
op: has
value: RBAC
set: true
- id: 11
text: "flag value includes some value in a comma-separated list, value is first in list"
tests:
test_items:
- flag: "--admission-control"
compare:
op: has
value: WebHook
set: true
- id: 12
text: "flag value includes some value in a comma-separated list, value middle of list"
tests:
test_items:
- flag: "--admission-control"
compare:
op: has
value: Something
set: true
- id: 13
text: "flag value includes some value in a comma-separated list, value only one in list"
tests:
test_items:
- flag: "--admission-control"
compare:
op: has
value: Something
set: true

View File

@ -62,7 +62,8 @@ func (t *testItem) execute(s string) (result bool) {
// --flag=somevalue // --flag=somevalue
// --flag // --flag
// somevalue // somevalue
pttn := `(` + t.Flag + `)(=)*([^\s,]*) *` //pttn := `(` + t.Flag + `)(=)*([^\s,]*) *`
pttn := `(` + t.Flag + `)(=)*([^\s]*) *`
flagRe := regexp.MustCompile(pttn) flagRe := regexp.MustCompile(pttn)
vals := flagRe.FindStringSubmatch(s) vals := flagRe.FindStringSubmatch(s)

View File

@ -94,6 +94,22 @@ func TestTestExecute(t *testing.T) {
controls.Groups[0].Checks[9], controls.Groups[0].Checks[9],
"600", "600",
}, },
{
controls.Groups[0].Checks[10],
"2:45 ../kubernetes/kube-apiserver --option --admission-control=WebHook,RBAC ---audit-log-maxage=40",
},
{
controls.Groups[0].Checks[11],
"2:45 ../kubernetes/kube-apiserver --option --admission-control=WebHook,RBAC ---audit-log-maxage=40",
},
{
controls.Groups[0].Checks[12],
"2:45 ../kubernetes/kube-apiserver --option --admission-control=WebHook,Something,RBAC ---audit-log-maxage=40",
},
{
controls.Groups[0].Checks[13],
"2:45 ../kubernetes/kube-apiserver --option --admission-control=Something ---audit-log-maxage=40",
},
} }
for _, c := range cases { for _, c := range cases {

View File

@ -17,9 +17,9 @@ package cmd
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os"
"github.com/aquasecurity/kube-bench/check" "github.com/aquasecurity/kube-bench/check"
"github.com/golang/glog"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -52,34 +52,30 @@ var (
func runChecks(t check.NodeType) { func runChecks(t check.NodeType) {
var summary check.Summary var summary check.Summary
var file string var file string
var err error
var typeConf *viper.Viper
// Master variables glog.V(1).Info(fmt.Sprintf("Using config file: %s\n", viper.ConfigFileUsed()))
apiserverBin = viper.GetString("installation." + installation + ".master.bin.apiserver")
apiserverConf = viper.GetString("installation." + installation + ".master.conf.apiserver")
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.controller-manager")
config = viper.GetString("installation." + installation + ".config")
etcdBin = viper.GetString("etcd.bin") switch t {
etcdConf = viper.GetString("etcd.conf") case check.MASTER:
flanneldBin = viper.GetString("flanneld.bin") file = masterFile
flanneldConf = viper.GetString("flanneld.conf") typeConf = viper.Sub("master")
case check.NODE:
file = nodeFile
typeConf = viper.Sub("node")
case check.FEDERATED:
file = federatedFile
typeConf = viper.Sub("federated")
}
// Node variables // Get the set of exectuables and config files we care about on this type of node. This also
kubeletBin = viper.GetString("installation." + installation + ".node.bin.kubelet") // checks that the executables we need for the node type are running.
kubeletConf = viper.GetString("installation." + installation + ".node.conf.kubelet") binmap := getBinaries(typeConf)
proxyBin = viper.GetString("installation." + installation + ".node.bin.proxy") confmap := getConfigFiles(typeConf)
proxyConf = viper.GetString("installation." + installation + ".node.conf.proxy")
// Federated
fedApiserverBin = viper.GetString("installation." + installation + ".federated.bin.apiserver")
fedControllerManagerBin = viper.GetString("installation." + installation + ".federated.bin.controller-manager")
// Run kubernetes installation validation checks. // Run kubernetes installation validation checks.
verifyKubeVersion(kubeMajorVersion, kubeMinorVersion) verifyKubeVersion(kubeMajorVersion, kubeMinorVersion)
verifyNodeType(t)
switch t { switch t {
case check.MASTER: case check.MASTER:
@ -98,26 +94,9 @@ func runChecks(t check.NodeType) {
} }
// Variable substitutions. Replace all occurrences of variables in controls files. // Variable substitutions. Replace all occurrences of variables in controls files.
s := multiWordReplace(string(in), "$apiserverbin", apiserverBin) s := string(in)
s = multiWordReplace(s, "$apiserverconf", apiserverConf) s = makeSubstitutions(s, "bin", binmap)
s = multiWordReplace(s, "$schedulerbin", schedulerBin) s = makeSubstitutions(s, "conf", confmap)
s = multiWordReplace(s, "$schedulerconf", schedulerConf)
s = multiWordReplace(s, "$controllermanagerbin", controllerManagerBin)
s = multiWordReplace(s, "$controllermanagerconf", controllerManagerConf)
s = multiWordReplace(s, "$config", config)
s = multiWordReplace(s, "$etcdbin", etcdBin)
s = multiWordReplace(s, "$etcdconf", etcdConf)
s = multiWordReplace(s, "$flanneldbin", flanneldBin)
s = multiWordReplace(s, "$flanneldconf", flanneldConf)
s = multiWordReplace(s, "$kubeletbin", kubeletBin)
s = multiWordReplace(s, "$kubeletconf", kubeletConf)
s = multiWordReplace(s, "$proxybin", proxyBin)
s = multiWordReplace(s, "$proxyconf", proxyConf)
s = multiWordReplace(s, "$fedapiserverbin", fedApiserverBin)
s = multiWordReplace(s, "$fedcontrollermanagerbin", fedControllerManagerBin)
controls, err := check.NewControls(t, []byte(s)) controls, err := check.NewControls(t, []byte(s))
if err != nil { if err != nil {
@ -149,41 +128,6 @@ func runChecks(t check.NodeType) {
} }
} }
// verifyNodeType checks the executables and config files are as expected
// for the specified tests (master, node or federated).
func verifyNodeType(t check.NodeType) {
var bins []string
var confs []string
switch t {
case check.MASTER:
bins = []string{apiserverBin, schedulerBin, controllerManagerBin}
confs = []string{apiserverConf, schedulerConf, controllerManagerConf}
case check.NODE:
bins = []string{kubeletBin, proxyBin}
confs = []string{kubeletConf, proxyConf}
case check.FEDERATED:
bins = []string{fedApiserverBin, fedControllerManagerBin}
}
for _, bin := range bins {
if !verifyBin(bin, ps) {
printlnWarn(fmt.Sprintf("%s is not running", bin))
}
}
for _, conf := range confs {
_, err := os.Stat(conf)
if err != nil {
if os.IsNotExist(err) {
printlnWarn(fmt.Sprintf("Missing kubernetes config file: %s", conf))
} else {
exitWithError(fmt.Errorf("error looking for file %s: %v", conf, err))
}
}
}
}
// colorPrint outputs the state in a specific colour, along with a message string // colorPrint outputs the state in a specific colour, along with a message string
func colorPrint(state check.State, s string) { func colorPrint(state check.State, s string) {
colors[state].Printf("[%s] ", state) colors[state].Printf("[%s] ", state)
@ -192,8 +136,6 @@ func colorPrint(state check.State, s string) {
// prettyPrint outputs the results to stdout in human-readable format // prettyPrint outputs the results to stdout in human-readable format
func prettyPrint(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()))
colorPrint(check.INFO, fmt.Sprintf("%s %s\n", r.ID, r.Text)) colorPrint(check.INFO, fmt.Sprintf("%s %s\n", r.ID, r.Text))
for _, g := range r.Groups { for _, g := range r.Groups {
colorPrint(check.INFO, fmt.Sprintf("%s %s\n", g.ID, g.Text)) colorPrint(check.INFO, fmt.Sprintf("%s %s\n", g.ID, g.Text))

View File

@ -34,14 +34,6 @@ var (
masterFile string masterFile string
nodeFile string nodeFile string
federatedFile string federatedFile string
loud bool
kubeConfDir string
etcdConfDir string
flanneldConfDir string
installation string
) )
// RootCmd represents the base command when called without any subcommands // RootCmd represents the base command when called without any subcommands
@ -67,12 +59,6 @@ func init() {
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
RootCmd.PersistentFlags().BoolVar(&jsonFmt, "json", false, "Prints the results as JSON") RootCmd.PersistentFlags().BoolVar(&jsonFmt, "json", false, "Prints the results as JSON")
RootCmd.PersistentFlags().StringVar(
&installation,
"installation",
"default",
"Specify how kubernetes cluster was installed. Possible values are default,hyperkube,kops,kubeadm",
)
RootCmd.PersistentFlags().StringVarP( RootCmd.PersistentFlags().StringVarP(
&checkList, &checkList,
"check", "check",

View File

@ -10,6 +10,7 @@ import (
"github.com/aquasecurity/kube-bench/check" "github.com/aquasecurity/kube-bench/check"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/spf13/viper"
) )
var ( var (
@ -22,6 +23,14 @@ var (
} }
) )
var psFunc func(string) string
var statFunc func(string) (os.FileInfo, error)
func init() {
psFunc = ps
statFunc = os.Stat
}
func printlnWarn(msg string) { func printlnWarn(msg string) {
fmt.Fprintf(os.Stderr, "[%s] %s\n", fmt.Fprintf(os.Stderr, "[%s] %s\n",
colors[check.WARN].Sprintf("%s", check.WARN), colors[check.WARN].Sprintf("%s", check.WARN),
@ -43,7 +52,7 @@ func exitWithError(err error) {
func continueWithError(err error, msg string) string { func continueWithError(err error, msg string) string {
if err != nil { if err != nil {
glog.V(1).Info(err) glog.V(2).Info(err)
} }
if msg != "" { if msg != "" {
@ -75,8 +84,71 @@ func ps(proc string) string {
return string(out) return string(out)
} }
// getBinaries finds which of the set of candidate executables are running
func getBinaries(v *viper.Viper) map[string]string {
binmap := make(map[string]string)
for _, component := range v.GetStringSlice("components") {
s := v.Sub(component)
if s == nil {
continue
}
optional := s.GetBool("optional")
bins := s.GetStringSlice("bins")
if len(bins) > 0 {
bin, err := findExecutable(bins)
if err != nil && !optional {
exitWithError(fmt.Errorf("need %s executable but none of the candidates are running", component))
}
// Default the executable name that we'll substitute to the name of the component
if bin == "" {
bin = component
glog.V(2).Info(fmt.Sprintf("Component %s not running", component))
} else {
glog.V(2).Info(fmt.Sprintf("Component %s uses running binary %s", component, bin))
}
binmap[component] = bin
}
}
return binmap
}
// getConfigFiles finds which of the set of candidate config files exist
func getConfigFiles(v *viper.Viper) map[string]string {
confmap := make(map[string]string)
for _, component := range v.GetStringSlice("components") {
s := v.Sub(component)
if s == nil {
continue
}
// See if any of the candidate config files exist
conf := findConfigFile(s.GetStringSlice("confs"))
if conf == "" {
if s.IsSet("defaultconf") {
conf = s.GetString("defaultconf")
glog.V(2).Info(fmt.Sprintf("Using default config file name '%s' for component %s", conf, component))
} else {
// Default the config file name that we'll substitute to the name of the component
printlnWarn(fmt.Sprintf("Missing config file for %s", component))
conf = component
}
} else {
glog.V(2).Info(fmt.Sprintf("Component %s uses config file '%s'", component, conf))
}
confmap[component] = conf
}
return confmap
}
// verifyBin checks that the binary specified is running // verifyBin checks that the binary specified is running
func verifyBin(bin string, psFunc func(string) string) bool { func verifyBin(bin string) bool {
// Strip any quotes // Strip any quotes
bin = strings.Trim(bin, "'\"") bin = strings.Trim(bin, "'\"")
@ -87,7 +159,47 @@ func verifyBin(bin string, psFunc func(string) string) bool {
proc := strings.Fields(bin)[0] proc := strings.Fields(bin)[0]
out := psFunc(proc) out := psFunc(proc)
return strings.Contains(out, bin) // There could be multiple lines in the ps output
// The binary needs to be the first word in the ps output, except that it could be preceded by a path
// e.g. /usr/bin/kubelet is a match for kubelet
// but apiserver is not a match for kube-apiserver
reFirstWord := regexp.MustCompile(`^(\S*\/)*` + bin)
lines := strings.Split(out, "\n")
for _, l := range lines {
if reFirstWord.Match([]byte(l)) {
return true
}
}
return false
}
// fundConfigFile looks through a list of possible config files and finds the first one that exists
func findConfigFile(candidates []string) string {
for _, c := range candidates {
_, err := statFunc(c)
if err == nil {
return c
}
if !os.IsNotExist(err) {
exitWithError(fmt.Errorf("error looking for file %s: %v", c, err))
}
}
return ""
}
// findExecutable looks through a list of possible executable names and finds the first one that's running
func findExecutable(candidates []string) (string, error) {
for _, c := range candidates {
if verifyBin(c) {
return c, nil
} else {
glog.V(1).Info(fmt.Sprintf("executable '%s' not running", c))
}
}
return "", fmt.Errorf("no candidates running")
} }
func verifyKubeVersion(major string, minor string) { func verifyKubeVersion(major string, minor string) {
@ -194,3 +306,17 @@ func getKubeVersion() *version {
return ver return ver
} }
func makeSubstitutions(s string, ext string, m map[string]string) string {
for k, v := range m {
subst := "$" + k + ext
if v == "" {
glog.V(2).Info(fmt.Sprintf("No subsitution for '%s'\n", subst))
continue
}
glog.V(1).Info(fmt.Sprintf("Substituting %s with '%s'\n", subst, v))
s = multiWordReplace(s, subst, v)
}
return s
}

View File

@ -15,9 +15,13 @@
package cmd package cmd
import ( import (
"os"
"reflect"
"regexp" "regexp"
"strconv" "strconv"
"testing" "testing"
"github.com/spf13/viper"
) )
func TestCheckVersion(t *testing.T) { func TestCheckVersion(t *testing.T) {
@ -78,10 +82,19 @@ func TestVersionMatch(t *testing.T) {
} }
var g string var g string
var e []error
var eIndex int
func fakeps(proc string) string { func fakeps(proc string) string {
return g return g
} }
func fakestat(file string) (os.FileInfo, error) {
err := e[eIndex]
eIndex++
return nil, err
}
func TestVerifyBin(t *testing.T) { func TestVerifyBin(t *testing.T) {
cases := []struct { cases := []struct {
proc string proc string
@ -95,14 +108,110 @@ func TestVerifyBin(t *testing.T) {
{proc: "cmd", psOut: "cmd param1 param2", exp: true}, {proc: "cmd", psOut: "cmd param1 param2", exp: true},
{proc: "cmd param", psOut: "cmd param1 param2", exp: true}, {proc: "cmd param", psOut: "cmd param1 param2", exp: true},
{proc: "cmd param", psOut: "cmd", exp: false}, {proc: "cmd param", psOut: "cmd", exp: false},
{proc: "cmd", psOut: "cmd x \ncmd y", exp: true},
{proc: "cmd y", psOut: "cmd x \ncmd y", exp: true},
{proc: "cmd", psOut: "/usr/bin/cmd", exp: true},
{proc: "cmd", psOut: "kube-cmd", exp: false},
{proc: "cmd", psOut: "/usr/bin/kube-cmd", exp: false},
} }
psFunc = fakeps
for id, c := range cases {
t.Run(strconv.Itoa(id), func(t *testing.T) {
g = c.psOut
v := verifyBin(c.proc)
if v != c.exp {
t.Fatalf("Expected %v got %v", c.exp, v)
}
})
}
}
func TestFindExecutable(t *testing.T) {
cases := []struct {
candidates []string // list of executables we'd consider
psOut string // fake output from ps
exp string // the one we expect to find in the (fake) ps output
expErr bool
}{
{candidates: []string{"one", "two", "three"}, psOut: "two", exp: "two"},
{candidates: []string{"one", "two", "three"}, psOut: "two three", exp: "two"},
{candidates: []string{"one double", "two double", "three double"}, psOut: "two double is running", exp: "two double"},
{candidates: []string{"one", "two", "three"}, psOut: "blah", expErr: true},
{candidates: []string{"one double", "two double", "three double"}, psOut: "two", expErr: true},
{candidates: []string{"apiserver", "kube-apiserver"}, psOut: "kube-apiserver", exp: "kube-apiserver"},
{candidates: []string{"apiserver", "kube-apiserver", "hyperkube-apiserver"}, psOut: "kube-apiserver", exp: "kube-apiserver"},
}
psFunc = fakeps
for id, c := range cases {
t.Run(strconv.Itoa(id), func(t *testing.T) {
g = c.psOut
e, err := findExecutable(c.candidates)
if e != c.exp {
t.Fatalf("Expected %v got %v", c.exp, e)
}
if err == nil && c.expErr {
t.Fatalf("Expected error")
}
if err != nil && !c.expErr {
t.Fatalf("Didn't expect error: %v", err)
}
})
}
}
func TestGetBinaries(t *testing.T) {
cases := []struct {
config map[string]interface{}
psOut string
exp map[string]string
}{
{
config: map[string]interface{}{"components": []string{"apiserver"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}},
psOut: "kube-apiserver",
exp: map[string]string{"apiserver": "kube-apiserver"},
},
{
// "thing" is not in the list of components
config: map[string]interface{}{"components": []string{"apiserver"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}}},
psOut: "kube-apiserver thing",
exp: map[string]string{"apiserver": "kube-apiserver"},
},
{
// "anotherthing" in list of components but doesn't have a defintion
config: map[string]interface{}{"components": []string{"apiserver", "anotherthing"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}}},
psOut: "kube-apiserver thing",
exp: map[string]string{"apiserver": "kube-apiserver"},
},
{
// more than one component
config: map[string]interface{}{"components": []string{"apiserver", "thing"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}}},
psOut: "kube-apiserver \nthing",
exp: map[string]string{"apiserver": "kube-apiserver", "thing": "thing"},
},
{
// default binary to component name
config: map[string]interface{}{"components": []string{"apiserver", "thing"}, "apiserver": map[string]interface{}{"bins": []string{"apiserver", "kube-apiserver"}}, "thing": map[string]interface{}{"bins": []string{"something else", "thing"}, "optional": true}},
psOut: "kube-apiserver \notherthing some params",
exp: map[string]string{"apiserver": "kube-apiserver", "thing": "thing"},
},
}
v := viper.New()
psFunc = fakeps
for id, c := range cases { for id, c := range cases {
t.Run(strconv.Itoa(id), func(t *testing.T) { t.Run(strconv.Itoa(id), func(t *testing.T) {
g = c.psOut g = c.psOut
v := verifyBin(c.proc, fakeps) for k, val := range c.config {
if v != c.exp { v.Set(k, val)
t.Fatalf("Expected %v got %v", c.exp, v) }
m := getBinaries(v)
if !reflect.DeepEqual(m, c.exp) {
t.Fatalf("Got %v\nExpected %v", m, c.exp)
} }
}) })
} }
@ -142,5 +251,118 @@ func TestGetKubeVersion(t *testing.T) {
if ok, err := regexp.MatchString(`\d+.\d+`, ver.Server); !ok && err != nil { if ok, err := regexp.MatchString(`\d+.\d+`, ver.Server); !ok && err != nil {
t.Logf("Expected:%v got %v\n", "n.m", ver.Server) t.Logf("Expected:%v got %v\n", "n.m", ver.Server)
} }
}
}
func TestFindConfigFile(t *testing.T) {
cases := []struct {
input []string
statResults []error
exp string
}{
{input: []string{"myfile"}, statResults: []error{nil}, exp: "myfile"},
{input: []string{"thisfile", "thatfile"}, statResults: []error{os.ErrNotExist, nil}, exp: "thatfile"},
{input: []string{"thisfile", "thatfile"}, statResults: []error{os.ErrNotExist, os.ErrNotExist}, exp: ""},
}
statFunc = fakestat
for id, c := range cases {
t.Run(strconv.Itoa(id), func(t *testing.T) {
e = c.statResults
eIndex = 0
conf := findConfigFile(c.input)
if conf != c.exp {
t.Fatalf("Got %s expected %s", conf, c.exp)
}
})
}
}
func TestGetConfigFiles(t *testing.T) {
cases := []struct {
config map[string]interface{}
exp map[string]string
statResults []error
}{
{
config: map[string]interface{}{"components": []string{"apiserver"}, "apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}}},
statResults: []error{os.ErrNotExist, nil},
exp: map[string]string{"apiserver": "kube-apiserver"},
},
{
// Component "thing" isn't included in the list of components
config: map[string]interface{}{
"components": []string{"apiserver"},
"apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}},
"thing": map[string]interface{}{"confs": []string{"/my/file/thing"}}},
statResults: []error{os.ErrNotExist, nil},
exp: map[string]string{"apiserver": "kube-apiserver"},
},
{
// More than one component
config: map[string]interface{}{
"components": []string{"apiserver", "thing"},
"apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}},
"thing": map[string]interface{}{"confs": []string{"/my/file/thing"}}},
statResults: []error{os.ErrNotExist, nil, nil},
exp: map[string]string{"apiserver": "kube-apiserver", "thing": "/my/file/thing"},
},
{
// Default thing to specified default config
config: map[string]interface{}{
"components": []string{"apiserver", "thing"},
"apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}},
"thing": map[string]interface{}{"confs": []string{"/my/file/thing"}, "defaultconf": "another/thing"}},
statResults: []error{os.ErrNotExist, nil, os.ErrNotExist},
exp: map[string]string{"apiserver": "kube-apiserver", "thing": "another/thing"},
},
{
// Default thing to component name
config: map[string]interface{}{
"components": []string{"apiserver", "thing"},
"apiserver": map[string]interface{}{"confs": []string{"apiserver", "kube-apiserver"}},
"thing": map[string]interface{}{"confs": []string{"/my/file/thing"}}},
statResults: []error{os.ErrNotExist, nil, os.ErrNotExist},
exp: map[string]string{"apiserver": "kube-apiserver", "thing": "thing"},
},
}
v := viper.New()
statFunc = fakestat
for id, c := range cases {
t.Run(strconv.Itoa(id), func(t *testing.T) {
for k, val := range c.config {
v.Set(k, val)
}
e = c.statResults
eIndex = 0
m := getConfigFiles(v)
if !reflect.DeepEqual(m, c.exp) {
t.Fatalf("Got %v\nExpected %v", m, c.exp)
}
})
}
}
func TestMakeSubsitutions(t *testing.T) {
cases := []struct {
input string
subst map[string]string
exp string
}{
{input: "Replace $thisbin", subst: map[string]string{"this": "that"}, exp: "Replace that"},
{input: "Replace $thisbin", subst: map[string]string{"this": "that", "here": "there"}, exp: "Replace that"},
{input: "Replace $thisbin and $herebin", subst: map[string]string{"this": "that", "here": "there"}, exp: "Replace that and there"},
}
for _, c := range cases {
t.Run(c.input, func(t *testing.T) {
s := makeSubstitutions(c.input, "bin", c.subst)
if s != c.exp {
t.Fatalf("Got %s expected %s", s, c.exp)
}
})
} }
} }