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  }