cmdreporter.go 9.3 KB

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