forked from rook/rook
cmd-reporter runs a command and puts the results of the command into a ConfigMap defined by its `--config-map-name` commandline flag. cmd-reporter is intended to be run as a Kubernetes job where an operator needs to run a single command and get return information back from the command which is more than just success/failure. The ConfigMap returned includes `stdout`, `stderr`, and `retcode` data from the command. The ConfigMap may be preexisting, in which case cmd-reporter will overwrite the return information in the ConfigMap. cmd-reporter labels the ConfigMap with `app=cmd-reporter`, and if the ConfigMap already has an app label which is different, cmd-reporter fails without modifying the ConfigMap so as not to overwrite another application's data in the event of a ConfigMap name+namespace collision. Signed-off-by: Blaine Gardner <blaine.gardner@suse.com>
282 lines
9.6 KiB
Go
282 lines
9.6 KiB
Go
/*
|
|
Copyright 2019 The Rook Authors. All rights reserved.
|
|
|
|
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 util
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rook/rook/pkg/operator/k8sutil"
|
|
"github.com/stretchr/testify/assert"
|
|
v1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
)
|
|
|
|
func TestCommandMarshallingUnmarshalling(t *testing.T) {
|
|
type args struct {
|
|
cmd []string
|
|
args []string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
}{
|
|
{name: "one command only", args: args{cmd: []string{"one"}, args: []string{}}},
|
|
{name: "no command or args", args: args{
|
|
cmd: []string{}, args: []string{}}},
|
|
{name: "no command w/ args", args: args{
|
|
cmd: []string{}, args: []string{"arg1", "arg2"}}},
|
|
{name: "one command w/ args", args: args{
|
|
cmd: []string{"one"}, args: []string{"arg1", "arg2"}}},
|
|
{name: "multi command only", args: args{
|
|
cmd: []string{"one", "two", "three"}, args: []string{}}},
|
|
{name: "multi command and arg", args: args{
|
|
cmd: []string{"one", "two", "three"}, args: []string{"arg1", "arg2", "--", "arg3"}}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
flagArg, err1 := CommandToCmdReporterFlagArgument(tt.args.cmd, tt.args.args)
|
|
if err1 != nil {
|
|
t.Errorf("CommandToFlagArgument() error = %+v, wanted no err", err1)
|
|
}
|
|
cmd, args, err2 := CmdReporterFlagArgumentToCommand(flagArg)
|
|
if err2 != nil {
|
|
t.Errorf("FlagArgumentToCommand() error = %+v, wanted no err", err2)
|
|
}
|
|
if err1 != nil || err2 != nil {
|
|
return
|
|
}
|
|
assert.Equal(t, tt.args.cmd, cmd)
|
|
assert.Equal(t, tt.args.args, args)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNew(t *testing.T) {
|
|
client := fake.NewSimpleClientset
|
|
type fields struct {
|
|
clientset kubernetes.Interface
|
|
cmd []string
|
|
args []string
|
|
configMapName string
|
|
namespace string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
wantErr bool
|
|
}{
|
|
{"all is well", fields{client(), []string{"cmd"}, []string{"args"}, "myConfigMap", "myNamespace"}, false},
|
|
{"no k8s client", fields{nil, []string{"cmd"}, []string{"args"}, "myConfigMap", "myNamespace"}, true},
|
|
{"no command", fields{client(), []string{}, []string{"args"}, "myConfigMap", "myNamespace"}, true},
|
|
{"empty command", fields{client(), []string{""}, []string{"args"}, "myConfigMap", "myNamespace"}, true},
|
|
{"three commands", fields{client(), []string{"one", "two", "three"}, []string{"args"}, "myConfigMap", "myNamespace"}, false},
|
|
{"no args", fields{client(), []string{"cmd"}, []string{}, "myConfigMap", "myNamespace"}, false},
|
|
{"empty arg", fields{client(), []string{"cmd"}, []string{""}, "myConfigMap", "myNamespace"}, false}, // an empty arg can still be valid
|
|
{"three args", fields{client(), []string{"cmd"}, []string{"arg1", "arg2", "arg3"}, "myConfigMap", "myNamespace"}, false},
|
|
{"no configmap name", fields{client(), []string{"cmd"}, []string{"args"}, "", "myNamespace"}, true},
|
|
{"no namespace", fields{client(), []string{"cmd"}, []string{"args"}, "myConfigMap", ""}, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r, err := NewCmdReporter(
|
|
tt.fields.clientset,
|
|
tt.fields.cmd,
|
|
tt.fields.args,
|
|
tt.fields.configMapName,
|
|
tt.fields.namespace,
|
|
)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Runner.Run() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if err == nil {
|
|
assert.NotNil(t, r)
|
|
assert.Equal(t, tt.fields.clientset, r.clientset)
|
|
assert.Equal(t, tt.fields.cmd, r.cmd)
|
|
assert.Equal(t, tt.fields.args, r.args)
|
|
assert.Equal(t, tt.fields.configMapName, r.configMapName)
|
|
assert.Equal(t, tt.fields.namespace, r.namespace)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunner_Run(t *testing.T) {
|
|
origExecCommand := execCommand
|
|
execCommand = mockExecCommand
|
|
defer func() { execCommand = origExecCommand }()
|
|
|
|
newClient := fake.NewSimpleClientset
|
|
|
|
verifyConfigMap := func(client kubernetes.Interface, stdout, stderr, retval, cmName, namespace string) {
|
|
cm, err := client.CoreV1().ConfigMaps(namespace).Get(cmName, metav1.GetOptions{})
|
|
fmt.Println("configmap:", cm)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, stdout, cm.Data[CmdReporterConfigMapStdoutKey])
|
|
assert.Equal(t, stderr, cm.Data[CmdReporterConfigMapStderrKey])
|
|
assert.Equal(t, retval, cm.Data[CmdReporterConfigMapRetcodeKey])
|
|
}
|
|
|
|
verifyCommand := func(cmd, args []string, command string) {
|
|
err := os.Setenv("GO_HELPER_PROCESS_PRINT_COMMAND", "1")
|
|
assert.NoError(t, err)
|
|
defer func() { os.Unsetenv("GO_HELPER_PROCESS_PRINT_COMMAND") }()
|
|
|
|
k8s := newClient()
|
|
r, err := NewCmdReporter(k8s, cmd, args, "command-configmap", "command-namespace")
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, r.Run())
|
|
|
|
verifyConfigMap(k8s, command, "", "0", "command-configmap", "command-namespace")
|
|
}
|
|
|
|
verifyCommand([]string{"grep"}, []string{"-e", ".*time"}, "grep -e .*time")
|
|
verifyCommand([]string{"ceph-volume", "inventory"}, []string{"--format=json-pretty"}, "ceph-volume inventory --format=json-pretty")
|
|
verifyCommand([]string{"ceph-volume", "lvm", "list"}, []string{}, "ceph-volume lvm list")
|
|
|
|
verifyOutputs := func(stdout, stderr, retcode string) {
|
|
err := os.Setenv("GO_HELPER_PROCESS_STDOUT", stdout)
|
|
assert.NoError(t, err)
|
|
defer func() { os.Unsetenv("GO_HELPER_PROCESS_STDOUT") }()
|
|
err = os.Setenv("GO_HELPER_PROCESS_STDERR", stderr)
|
|
assert.NoError(t, err)
|
|
defer func() { os.Unsetenv("GO_HELPER_PROCESS_STDERR") }()
|
|
err = os.Setenv("GO_HELPER_PROCESS_RETCODE", retcode)
|
|
assert.NoError(t, err)
|
|
defer func() { os.Unsetenv("GO_HELPER_PROCESS_RETCODE") }()
|
|
|
|
k8s := newClient()
|
|
r, err := NewCmdReporter(k8s, []string{"standin-cmd"}, []string{"--some", "arg"}, "outputs-configmap", "outputs-namespace")
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, r.Run())
|
|
|
|
verifyConfigMap(k8s, stdout, stderr, retcode, "outputs-configmap", "outputs-namespace")
|
|
}
|
|
|
|
verifyOutputs("", "", "0")
|
|
verifyOutputs("", "", "11")
|
|
verifyOutputs("this is my stdout", "", "0")
|
|
verifyOutputs("", "this is my stderr", "23")
|
|
verifyOutputs("this is ...", "... mixed outputs", "23")
|
|
|
|
// Verify that cmd-reporter won't overwrite a preexisting configmap with a different app name
|
|
k8s := newClient()
|
|
cm := &v1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "preexisting-configmap",
|
|
Namespace: "preexisting-namespace",
|
|
Labels: map[string]string{
|
|
k8sutil.AppAttr: "some-other-application",
|
|
},
|
|
},
|
|
Data: map[string]string{},
|
|
}
|
|
k8s.CoreV1().ConfigMaps("preexisting-namespace").Create(cm)
|
|
r, err := NewCmdReporter(k8s, []string{"some-command"}, []string{"some", "args"}, "preexisting-configmap", "preexisting-namespace")
|
|
assert.NoError(t, err)
|
|
assert.Error(t, r.Run())
|
|
cm, err = k8s.CoreV1().ConfigMaps("preexisting-namespace").Get("preexisting-configmap", metav1.GetOptions{})
|
|
assert.NoError(t, err)
|
|
assert.NotContains(t, cm.Data, CmdReporterConfigMapStdoutKey)
|
|
assert.NotContains(t, cm.Data, CmdReporterConfigMapStderrKey)
|
|
assert.NotContains(t, cm.Data, CmdReporterConfigMapRetcodeKey)
|
|
|
|
// Verify that cmd-reporter WILL overwrite a preexisting configmap with cmd-reporter's app name
|
|
k8s = newClient()
|
|
cm = &v1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "preexisting-configmap",
|
|
Namespace: "preexisting-namespace",
|
|
Labels: map[string]string{
|
|
k8sutil.AppAttr: CmdReporterAppName,
|
|
},
|
|
},
|
|
Data: map[string]string{},
|
|
}
|
|
k8s.CoreV1().ConfigMaps("preexisting-namespace").Create(cm)
|
|
r, err = NewCmdReporter(k8s, []string{"some-command"}, []string{"some", "args"}, "preexisting-configmap", "preexisting-namespace")
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, r.Run())
|
|
verifyConfigMap(k8s, "", "", "0", "preexisting-configmap", "preexisting-namespace")
|
|
}
|
|
|
|
// Inspired by: https://github.com/golang/go/blob/master/src/os/exec/exec_test.go
|
|
func mockExecCommand(command string, args ...string) *exec.Cmd {
|
|
cs := []string{"-test.run=TestCmdReporterHelperProcess", "--", command}
|
|
cs = append(cs, args...)
|
|
cmd := exec.Command(os.Args[0], cs...)
|
|
// the existing environment will contain variables which define the desired return from the
|
|
// fake command which will be run.
|
|
cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1")
|
|
return cmd
|
|
}
|
|
|
|
// TestHelperProcess isn't a real test. It's used as a helper process
|
|
// for TestParameterRun.
|
|
// Inspired by: https://github.com/golang/go/blob/master/src/os/exec/exec_test.go
|
|
func TestCmdReporterHelperProcess(*testing.T) {
|
|
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
|
return
|
|
}
|
|
|
|
args := os.Args
|
|
for len(args) > 0 {
|
|
if args[0] == "--" {
|
|
args = args[1:]
|
|
break
|
|
}
|
|
args = args[1:]
|
|
}
|
|
if len(args) == 0 {
|
|
fmt.Fprintf(os.Stderr, "No command\n")
|
|
os.Exit(2)
|
|
}
|
|
userCommand := args
|
|
|
|
// test should set these in its environment to control the output of the test commands
|
|
stdout := os.Getenv("GO_HELPER_PROCESS_STDOUT")
|
|
stderr := os.Getenv("GO_HELPER_PROCESS_STDERR")
|
|
retcode := os.Getenv("GO_HELPER_PROCESS_RETCODE")
|
|
|
|
if os.Getenv("GO_HELPER_PROCESS_PRINT_COMMAND") == "1" {
|
|
stdout = strings.Join(userCommand, " ")
|
|
stderr = ""
|
|
retcode = ""
|
|
}
|
|
|
|
if stdout != "" {
|
|
fmt.Fprintf(os.Stdout, stdout)
|
|
}
|
|
if stderr != "" {
|
|
fmt.Fprintf(os.Stderr, stderr)
|
|
}
|
|
if retcode != "" {
|
|
rc, err := strconv.Atoi(retcode)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
os.Exit(rc)
|
|
}
|
|
os.Exit(0)
|
|
}
|