forked from rook/rook
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcmdreporter.go
249 lines (212 loc) · 9.16 KB
/
cmdreporter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/*
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 (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"strings"
"syscall"
"github.com/coreos/pkg/capnslog"
"github.com/rook/rook/pkg/operator/k8sutil"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
const (
// CmdReporterAppName is the app name reported by cmd-reporter, notably on the ConfigMap's application label.
CmdReporterAppName = "rook-cmd-reporter"
// CmdReporterConfigMapStdoutKey defines the key in the ConfigMap where stdout is reported.
CmdReporterConfigMapStdoutKey = "stdout"
// CmdReporterConfigMapStderrKey defines the key in the ConfigMap where stderr is reported.
CmdReporterConfigMapStderrKey = "stderr"
// CmdReporterConfigMapRetcodeKey defines the key in the ConfigMap where the return code is reported.
CmdReporterConfigMapRetcodeKey = "retcode"
)
var (
logger = capnslog.NewPackageLogger("github.com/rook/rook", "job-reporter-cmd")
)
// CmdReporter is a process intended to be run in simple Kubernetes jobs. The CmdReporter runs a
// command in a job and stores the results in a ConfigMap which can be read by the operator.
type CmdReporter struct {
clientset kubernetes.Interface
cmd []string
args []string
configMapName string
namespace string
}
// NewCmdReporter creates a new CmdReporter and returns an error if cmd, configMapName, or Namespace aren't specified.
func NewCmdReporter(clientset kubernetes.Interface, cmd, args []string, configMapName, namespace string) (*CmdReporter, error) {
if clientset == nil {
return nil, fmt.Errorf("Kubernetes client interface was not specified")
}
if len(cmd) == 0 || cmd[0] == "" {
return nil, fmt.Errorf("cmd was not specified")
}
if configMapName == "" {
return nil, fmt.Errorf("the config map name was not specified")
}
if namespace == "" {
return nil, fmt.Errorf("the namespace must be specified")
}
return &CmdReporter{
clientset: clientset,
cmd: cmd,
args: args,
configMapName: configMapName,
namespace: namespace,
}, nil
}
// Create a simple representation struct for a command and its args so that Go's native JSON
// (un)marshalling can be used to convert a Kubernetes representation of command+args into a string
// representation automatically without the user having to fiddle with specifying their command+args
// in string form manually.
type commandRepresentation struct {
Cmd []string `json:"cmd"`
Args []string `json:"args"`
}
// CommandToCmdReporterFlagArgument converts a command and arguments in typical Kubernetes container format
// into a string representation of the command+args that is compatible with the job reporter's
// command line flag "--command".
// This only returns the argument to "--command" and not the "--command" text itself.
func CommandToCmdReporterFlagArgument(cmd []string, args []string) (string, error) {
r := &commandRepresentation{Cmd: cmd, Args: args}
b, err := json.Marshal(r)
if err != nil {
return "", fmt.Errorf("failed to marshal command+args into an argument string. %+v", err)
}
return string(b), nil
}
// CmdReporterFlagArgumentToCommand converts a string representation of a command compatible with the job
// reporter's command line flag "--command" into a command and arguments in typical Kubernetes
// container format, i.e., a list of command strings and a list of arguments.
// This function processes the argument to "--command" but not the "--command" text itself.
func CmdReporterFlagArgumentToCommand(flagArg string) (cmd []string, args []string, err error) {
b := []byte(flagArg)
r := &commandRepresentation{}
if err := json.Unmarshal(b, r); err != nil {
return []string{}, []string{}, fmt.Errorf("failed to unmarshal command from argument. %+v", err)
}
return r.Cmd, r.Args, nil
}
// Run a given command to completion, and store the Stdout, Stderr, and return code
// results of the command in a ConfigMap. If the ConfigMap already exists, the
// Stdout, Stderr, and return code data which may be present in the ConfigMap
// will be overwritten.
//
// If cmd-reporter succeeds in running the command to completion, no error is
// reported, even if the command's return code is nonzero (failure). Run will
// return an error if the command could not be run for any reason or if there was
// an error storing the command results into the ConfigMap. An application label
// is applied to the ConfigMap, and if the label already exists and has a
// different application's name name, this returns an error, as this may indicate
// that it is not safe for cmd-reporter to edit the ConfigMap.
func (r *CmdReporter) Run() error {
stdout, stderr, retcode, err := r.runCommand()
if err != nil {
return fmt.Errorf("system failed to run command. %+v", err)
}
if err := r.saveToConfigMap(stdout, stderr, retcode); err != nil {
return fmt.Errorf("failed to save command output to ConfigMap. %+v", err)
}
return nil
}
var execCommand = exec.Command
func (r *CmdReporter) runCommand() (stdout, stderr string, retcode int, err error) {
retcode = -1 // default retcode to -1
baseCmd := r.cmd[0]
fullArgs := append(r.cmd[1:], r.args...)
var capturedStdout bytes.Buffer
var capturedStderr bytes.Buffer
// Capture stdout and stderr, and also send both to the container stdout/stderr, similar to the
// 'tee' command
stdoutTee := io.MultiWriter(&capturedStdout, os.Stdout)
stderrTee := io.MultiWriter(&capturedStderr, os.Stdout)
c := execCommand(baseCmd, fullArgs...)
c.Stdout = stdoutTee
c.Stderr = stderrTee
cmdStr := fmt.Sprintf("%s %s", c.Path, strings.Join(c.Args, " "))
logger.Infof("running command: %s", cmdStr)
if err := c.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
// c.ProcessState.ExitCode is available with Go 1.12 and could replace if block below
if stat, ok := exitError.Sys().(syscall.WaitStatus); ok {
retcode = stat.ExitStatus()
}
// it's possible the above failed to parse the return code, so report the whole error
logger.Warningf("command finished unsuccessfully but return code could not be parsed. %+v", err)
} else {
return "", "", -1, fmt.Errorf("failed to run command [%s]. %+v", cmdStr, err)
}
} else {
retcode = 0
}
return string(capturedStdout.Bytes()), string(capturedStderr.Bytes()), retcode, nil
}
func (r *CmdReporter) saveToConfigMap(stdout, stderr string, retcode int) error {
retcodeStr := fmt.Sprintf("%d", retcode)
k8s := r.clientset
cm, err := k8s.CoreV1().ConfigMaps(r.namespace).Get(r.configMapName, metav1.GetOptions{})
if err != nil {
if !errors.IsNotFound(err) {
return fmt.Errorf("failed to determine if ConfigMap %s is preexisting. %+v", r.configMapName, err)
}
// the given config map doesn't exist yet, create it now
cm = &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: r.configMapName,
Namespace: r.namespace,
Labels: map[string]string{
k8sutil.AppAttr: CmdReporterAppName,
},
},
Data: map[string]string{
CmdReporterConfigMapStdoutKey: stdout,
CmdReporterConfigMapStderrKey: stderr,
CmdReporterConfigMapRetcodeKey: retcodeStr,
},
}
if _, err := k8s.CoreV1().ConfigMaps(r.namespace).Create(cm); err != nil {
return fmt.Errorf("failed to create ConfigMap %s. %+v", r.configMapName, err)
}
return nil
}
// if the operator has created the configmap with a different app name, we assume that we aren't
// allowed to modify the ConfigMap
if app, ok := cm.Labels[k8sutil.AppAttr]; !ok || (ok && app == "") {
// label is unset or set to empty string
cm.Labels[k8sutil.AppAttr] = CmdReporterAppName
} else if ok && app != "" && app != CmdReporterAppName {
// label is set and not equal to the cmd-reporter app name
return fmt.Errorf("ConfigMap [%s] already has label [%s] that differs from cmd-reporter's "+
"label [%s]; this may indicate that it is not safe for cmd-reporter to modify the ConfigMap.",
r.configMapName, fmt.Sprintf("%s=%s", k8sutil.AppAttr, app), fmt.Sprintf("%s=%s", k8sutil.AppAttr, CmdReporterAppName))
}
for _, k := range []string{CmdReporterConfigMapStdoutKey, CmdReporterConfigMapStderrKey, CmdReporterConfigMapRetcodeKey} {
if v, ok := cm.Data[k]; ok {
logger.Warningf("ConfigMap [%s] data key [%s] is already set to [%s] and will be overwritten.", r.configMapName, k, v)
}
}
// given configmap already exists, update it
cm.Data[CmdReporterConfigMapStdoutKey] = stdout
cm.Data[CmdReporterConfigMapStderrKey] = stderr
cm.Data[CmdReporterConfigMapRetcodeKey] = retcodeStr
if _, err := k8s.CoreV1().ConfigMaps(r.namespace).Update(cm); err != nil {
return fmt.Errorf("failed to update ConfigMap %s. %+v", r.configMapName, err)
}
return nil
}