Files
my-rook-config/pkg/daemon/util/cmdreporter_test.go
T
Sébastien Han 121c2987e3 ceph: stop using tini
We don't need to use tini.
We don't have anything in the rook operator that would
either create zombie processes (no threads) or use
exec (to fork). The Go binary has a really good
signal handling mechanism.

Closes: https://github.com/rook/rook/issues/8794
Signed-off-by: Sébastien Han <seb@redhat.com>
2021-09-27 10:54:16 +02:00

288 lines
9.9 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 (
"context"
"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) {
ctx := context.TODO()
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(
ctx,
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) {
ctx := context.TODO()
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(ctx, 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(ctx, 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(ctx, 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{},
}
_, err := k8s.CoreV1().ConfigMaps("preexisting-namespace").Create(ctx, cm, metav1.CreateOptions{})
assert.NoError(t, err)
r, err := NewCmdReporter(ctx, 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(ctx, "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{},
}
_, err = k8s.CoreV1().ConfigMaps("preexisting-namespace").Create(ctx, cm, metav1.CreateOptions{})
assert.NoError(t, err)
r, err = NewCmdReporter(ctx, 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...) //nolint:gosec //Rook controls the input to the exec arguments
// 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.Fprint(os.Stdout, stdout)
}
if stderr != "" {
fmt.Fprint(os.Stderr, stderr)
}
if retcode != "" {
rc, err := strconv.Atoi(retcode)
if err != nil {
panic(err)
}
os.Exit(rc)
}
os.Exit(0)
}