github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/uniter/runner/runner.go (about) 1 // Copyright 2012-2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package runner 5 6 import ( 7 "encoding/base64" 8 "fmt" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "time" 13 "unicode/utf8" 14 15 "github.com/juju/clock" 16 "github.com/juju/cmd" 17 "github.com/juju/errors" 18 "github.com/juju/loggo" 19 jujuos "github.com/juju/os" 20 utilexec "github.com/juju/utils/exec" 21 22 "github.com/juju/juju/core/actions" 23 "github.com/juju/juju/worker/common/charmrunner" 24 "github.com/juju/juju/worker/uniter/runner/context" 25 "github.com/juju/juju/worker/uniter/runner/debug" 26 "github.com/juju/juju/worker/uniter/runner/jujuc" 27 ) 28 29 var logger = loggo.GetLogger("juju.worker.uniter.runner") 30 31 // Runner is responsible for invoking commands in a context. 32 type Runner interface { 33 34 // Context returns the context against which the runner executes. 35 Context() Context 36 37 // RunHook executes the hook with the supplied name. 38 RunHook(name string) error 39 40 // RunAction executes the action with the supplied name. 41 RunAction(name string) error 42 43 // RunCommands executes the supplied script. 44 RunCommands(commands string) (*utilexec.ExecResponse, error) 45 } 46 47 // Context exposes hooks.Context, and additional methods needed by Runner. 48 type Context interface { 49 jujuc.Context 50 Id() string 51 HookVars(paths context.Paths) ([]string, error) 52 ActionData() (*context.ActionData, error) 53 SetProcess(process context.HookProcess) 54 HasExecutionSetUnitStatus() bool 55 ResetExecutionSetUnitStatus() 56 57 Prepare() error 58 Flush(badge string, failure error) error 59 } 60 61 // NewRunner returns a Runner backed by the supplied context and paths. 62 func NewRunner(context Context, paths context.Paths) Runner { 63 return &runner{context, paths} 64 } 65 66 // runner implements Runner. 67 type runner struct { 68 context Context 69 paths context.Paths 70 } 71 72 func (runner *runner) Context() Context { 73 return runner.context 74 } 75 76 // RunCommands exists to satisfy the Runner interface. 77 func (runner *runner) RunCommands(commands string) (*utilexec.ExecResponse, error) { 78 result, err := runner.runCommandsWithTimeout(commands, 0, clock.WallClock) 79 return result, runner.context.Flush("run commands", err) 80 } 81 82 // runCommandsWithTimeout is a helper to abstract common code between run commands and 83 // juju-run as an action 84 func (runner *runner) runCommandsWithTimeout(commands string, timeout time.Duration, clock clock.Clock) (*utilexec.ExecResponse, error) { 85 srv, err := runner.startJujucServer() 86 if err != nil { 87 return nil, err 88 } 89 defer srv.Close() 90 91 env, err := runner.context.HookVars(runner.paths) 92 if err != nil { 93 return nil, errors.Trace(err) 94 } 95 command := utilexec.RunParams{ 96 Commands: commands, 97 WorkingDir: runner.paths.GetCharmDir(), 98 Environment: env, 99 Clock: clock, 100 } 101 102 err = command.Run() 103 if err != nil { 104 return nil, err 105 } 106 runner.context.SetProcess(hookProcess{command.Process()}) 107 108 var cancel chan struct{} 109 if timeout != 0 { 110 cancel = make(chan struct{}) 111 go func() { 112 <-clock.After(timeout) 113 close(cancel) 114 }() 115 } 116 117 // Block and wait for process to finish 118 return command.WaitWithCancel(cancel) 119 } 120 121 // runJujuRunAction is the function that executes when a juju-run action is ran. 122 func (runner *runner) runJujuRunAction() (err error) { 123 params, err := runner.context.ActionParams() 124 if err != nil { 125 return errors.Trace(err) 126 } 127 command, ok := params["command"].(string) 128 if !ok { 129 return errors.New("no command parameter to juju-run action") 130 } 131 132 // The timeout is passed in in nanoseconds(which are represented in go as int64) 133 // But due to serialization it comes out as float64 134 timeout, ok := params["timeout"].(float64) 135 if !ok { 136 logger.Debugf("unable to read juju-run action timeout, will continue running action without one") 137 } 138 139 results, err := runner.runCommandsWithTimeout(command, time.Duration(timeout), clock.WallClock) 140 141 if err != nil { 142 return runner.context.Flush("juju-run", err) 143 } 144 145 if err := runner.updateActionResults(results); err != nil { 146 return runner.context.Flush("juju-run", err) 147 } 148 149 return runner.context.Flush("juju-run", nil) 150 } 151 152 func encodeBytes(input []byte) (value string, encoding string) { 153 if utf8.Valid(input) { 154 value = string(input) 155 encoding = "utf8" 156 } else { 157 value = base64.StdEncoding.EncodeToString(input) 158 encoding = "base64" 159 } 160 return value, encoding 161 } 162 163 func (runner *runner) updateActionResults(results *utilexec.ExecResponse) error { 164 if err := runner.context.UpdateActionResults([]string{"Code"}, fmt.Sprintf("%d", results.Code)); err != nil { 165 return errors.Trace(err) 166 } 167 168 stdout, encoding := encodeBytes(results.Stdout) 169 if err := runner.context.UpdateActionResults([]string{"Stdout"}, stdout); err != nil { 170 return errors.Trace(err) 171 } 172 if encoding != "utf8" { 173 if err := runner.context.UpdateActionResults([]string{"StdoutEncoding"}, encoding); err != nil { 174 return errors.Trace(err) 175 } 176 } 177 178 stderr, encoding := encodeBytes(results.Stderr) 179 if err := runner.context.UpdateActionResults([]string{"Stderr"}, stderr); err != nil { 180 return errors.Trace(err) 181 } 182 if encoding != "utf8" { 183 if err := runner.context.UpdateActionResults([]string{"StderrEncoding"}, encoding); err != nil { 184 return errors.Trace(err) 185 } 186 } 187 188 return nil 189 } 190 191 // RunAction exists to satisfy the Runner interface. 192 func (runner *runner) RunAction(actionName string) error { 193 if _, err := runner.context.ActionData(); err != nil { 194 return errors.Trace(err) 195 } 196 if actionName == actions.JujuRunActionName { 197 return runner.runJujuRunAction() 198 } 199 return runner.runCharmHookWithLocation(actionName, "actions") 200 } 201 202 // RunHook exists to satisfy the Runner interface. 203 func (runner *runner) RunHook(hookName string) error { 204 return runner.runCharmHookWithLocation(hookName, "hooks") 205 } 206 207 func (runner *runner) runCharmHookWithLocation(hookName, charmLocation string) error { 208 srv, err := runner.startJujucServer() 209 if err != nil { 210 return err 211 } 212 defer srv.Close() 213 214 env, err := runner.context.HookVars(runner.paths) 215 if err != nil { 216 return errors.Trace(err) 217 } 218 if jujuos.HostOS() == jujuos.Windows { 219 // TODO(fwereade): somehow consolidate with utils/exec? 220 // We don't do this on the other code path, which uses exec.RunCommands, 221 // because that already has handling for windows environment requirements. 222 env = mergeWindowsEnvironment(env, os.Environ()) 223 } 224 225 debugctx := debug.NewHooksContext(runner.context.UnitName()) 226 if session, _ := debugctx.FindSession(); session != nil && session.MatchHook(hookName) { 227 logger.Infof("executing %s via debug-hooks", hookName) 228 err = session.RunHook(hookName, runner.paths.GetCharmDir(), env) 229 } else { 230 err = runner.runCharmHook(hookName, env, charmLocation) 231 } 232 return runner.context.Flush(hookName, err) 233 } 234 235 func (runner *runner) runCharmHook(hookName string, env []string, charmLocation string) error { 236 charmDir := runner.paths.GetCharmDir() 237 hook, err := searchHook(charmDir, filepath.Join(charmLocation, hookName)) 238 if err != nil { 239 return err 240 } 241 hookCmd := hookCommand(hook) 242 ps := exec.Command(hookCmd[0], hookCmd[1:]...) 243 ps.Env = env 244 ps.Dir = charmDir 245 outReader, outWriter, err := os.Pipe() 246 if err != nil { 247 return errors.Errorf("cannot make logging pipe: %v", err) 248 } 249 ps.Stdout = outWriter 250 ps.Stderr = outWriter 251 hookLogger := charmrunner.NewHookLogger(runner.getLogger(hookName), outReader) 252 go hookLogger.Run() 253 err = ps.Start() 254 outWriter.Close() 255 if err == nil { 256 // Record the *os.Process of the hook 257 runner.context.SetProcess(hookProcess{ps.Process}) 258 // Block until execution finishes 259 err = ps.Wait() 260 } 261 hookLogger.Stop() 262 return errors.Trace(err) 263 } 264 265 func (runner *runner) startJujucServer() (*jujuc.Server, error) { 266 // Prepare server. 267 getCmd := func(ctxId, cmdName string) (cmd.Command, error) { 268 if ctxId != runner.context.Id() { 269 return nil, errors.Errorf("expected context id %q, got %q", runner.context.Id(), ctxId) 270 } 271 return jujuc.NewCommand(runner.context, cmdName) 272 } 273 srv, err := jujuc.NewServer(getCmd, runner.paths.GetJujucSocket()) 274 if err != nil { 275 return nil, errors.Annotate(err, "starting jujuc server") 276 } 277 go srv.Run() 278 return srv, nil 279 } 280 281 func (runner *runner) getLogger(hookName string) loggo.Logger { 282 return loggo.GetLogger(fmt.Sprintf("unit.%s.%s", runner.context.UnitName(), hookName)) 283 } 284 285 type hookProcess struct { 286 *os.Process 287 } 288 289 func (p hookProcess) Pid() int { 290 return p.Process.Pid 291 }