github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/uniter/runner/runner_test.go (about)

     1  // Copyright 2012-2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package runner_test
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/juju/charm/v12/hooks"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/loggo"
    17  	envtesting "github.com/juju/testing"
    18  	jc "github.com/juju/testing/checkers"
    19  	"github.com/juju/utils/v3/exec"
    20  	gc "gopkg.in/check.v1"
    21  
    22  	"github.com/juju/juju/core/model"
    23  	"github.com/juju/juju/worker/common/charmrunner"
    24  	"github.com/juju/juju/worker/uniter/hook"
    25  	"github.com/juju/juju/worker/uniter/runner"
    26  	"github.com/juju/juju/worker/uniter/runner/context"
    27  	runnertesting "github.com/juju/juju/worker/uniter/runner/testing"
    28  )
    29  
    30  type RunCommandSuite struct {
    31  	ContextSuite
    32  }
    33  
    34  var _ = gc.Suite(&RunCommandSuite{})
    35  
    36  func (s *RunCommandSuite) TestRunCommandsEnvStdOutAndErrAndRC(c *gc.C) {
    37  	ctx, err := s.contextFactory.HookContext(hook.Info{Kind: hooks.ConfigChanged})
    38  	c.Assert(err, jc.ErrorIsNil)
    39  	paths := runnertesting.NewRealPaths(c)
    40  	r := runner.NewRunner(ctx, paths, nil)
    41  
    42  	// Ensure the current process env is passed through to the command.
    43  	s.PatchEnvironment("KUBERNETES_PORT", "443")
    44  
    45  	commands := `
    46  echo $JUJU_CHARM_DIR
    47  echo $FOO
    48  echo this is standard err >&2
    49  exit 42
    50  `
    51  	result, err := r.RunCommands(commands, runner.Operator)
    52  	c.Assert(err, jc.ErrorIsNil)
    53  
    54  	c.Assert(result.Code, gc.Equals, 42)
    55  	c.Assert(strings.ReplaceAll(string(result.Stdout), "\n", ""), gc.Equals, paths.GetCharmDir())
    56  	c.Assert(strings.TrimRight(string(result.Stderr), "\n"), gc.Equals, "this is standard err")
    57  	c.Assert(ctx.GetProcess(), gc.NotNil)
    58  }
    59  
    60  type RunHookSuite struct {
    61  	ContextSuite
    62  }
    63  
    64  var _ = gc.Suite(&RunHookSuite{})
    65  
    66  // LineBufferSize matches the constant used when creating
    67  // the bufio line reader.
    68  const lineBufferSize = 4096
    69  
    70  var runHookTests = []struct {
    71  	summary  string
    72  	relid    int
    73  	spec     hookSpec
    74  	err      string
    75  	hookType runner.HookHandlerType
    76  }{
    77  	{
    78  		summary:  "missing hook is not an error",
    79  		relid:    -1,
    80  		hookType: runner.InvalidHookHandler,
    81  	}, {
    82  		summary: "report error indicated by hook's exit status",
    83  		relid:   -1,
    84  		spec: hookSpec{
    85  			perm: 0700,
    86  			code: 99,
    87  		},
    88  		err:      "exit status 99",
    89  		hookType: runner.ExplicitHookHandler,
    90  	}, {
    91  		summary: "report error with invalid script",
    92  		relid:   -1,
    93  		spec: hookSpec{
    94  			perm:           0700,
    95  			code:           2,
    96  			missingShebang: true,
    97  		},
    98  		err:      "fork/exec.*: exec format error",
    99  		hookType: runner.ExplicitHookHandler,
   100  	}, {
   101  		summary: "report error with missing charm",
   102  		relid:   -1,
   103  		spec: hookSpec{
   104  			charmMissing: true,
   105  			perm:         0700,
   106  		},
   107  		err:      "charm missing from disk",
   108  		hookType: runner.InvalidHookHandler,
   109  	}, {
   110  		summary: "output logging",
   111  		relid:   -1,
   112  		spec: hookSpec{
   113  			perm:   0700,
   114  			stdout: "stdout",
   115  			stderr: "stderr",
   116  		},
   117  		hookType: runner.ExplicitHookHandler,
   118  	}, {
   119  		summary: "output logging with background process",
   120  		relid:   -1,
   121  		spec: hookSpec{
   122  			perm:       0700,
   123  			stdout:     "stdout",
   124  			background: "not printed",
   125  		},
   126  		hookType: runner.ExplicitHookHandler,
   127  	}, {
   128  		summary: "long line split",
   129  		relid:   -1,
   130  		spec: hookSpec{
   131  			perm:   0700,
   132  			stdout: strings.Repeat("a", lineBufferSize+10),
   133  		},
   134  		hookType: runner.ExplicitHookHandler,
   135  	},
   136  }
   137  
   138  type RestrictedWriter struct {
   139  	Module string // what Module should be included in the log buffer
   140  	Buffer bytes.Buffer
   141  }
   142  
   143  func (r *RestrictedWriter) Write(entry loggo.Entry) {
   144  	if strings.HasPrefix(entry.Module, r.Module) {
   145  		fmt.Fprintf(&r.Buffer, "%s %s %s\n", entry.Level.String(), entry.Module, entry.Message)
   146  	}
   147  }
   148  
   149  func (s *RunHookSuite) TestRunHook(c *gc.C) {
   150  	writer := &RestrictedWriter{Module: "unit.u/0.something-happened"}
   151  	c.Assert(loggo.RegisterWriter("test", writer), jc.ErrorIsNil)
   152  	for i, t := range runHookTests {
   153  		writer.Buffer.Reset()
   154  		c.Logf("\ntest %d of %d: %s; perm %v", i, len(runHookTests)+1, t.summary, t.spec.perm)
   155  		ctx, err := s.contextFactory.HookContext(hook.Info{Kind: hooks.ConfigChanged})
   156  		c.Assert(err, jc.ErrorIsNil)
   157  
   158  		paths := runnertesting.NewRealPaths(c)
   159  		rnr := runner.NewRunner(ctx, paths, nil)
   160  		var hookExists bool
   161  		if t.spec.perm != 0 {
   162  			spec := t.spec
   163  			spec.dir = "hooks"
   164  			spec.name = hookName
   165  			c.Logf("makeCharm %#v", spec)
   166  			makeCharm(c, spec, paths.GetCharmDir())
   167  			hookExists = true
   168  		} else if !t.spec.charmMissing {
   169  			makeCharmMetadata(c, paths.GetCharmDir())
   170  		}
   171  		t0 := time.Now()
   172  		hookType, err := rnr.RunHook("something-happened")
   173  		if t.err == "" && hookExists {
   174  			c.Assert(err, jc.ErrorIsNil)
   175  		} else if !hookExists {
   176  			c.Assert(charmrunner.IsMissingHookError(err), jc.IsTrue)
   177  		} else {
   178  			c.Assert(err, gc.ErrorMatches, t.err)
   179  		}
   180  		if t.spec.background != "" && time.Now().Sub(t0) > 5*time.Second {
   181  			c.Errorf("background process holding up hook execution")
   182  		}
   183  		c.Assert(hookType, gc.Equals, t.hookType)
   184  		if t.spec.stdout != "" {
   185  			if len(t.spec.stdout) < lineBufferSize {
   186  				c.Check(writer.Buffer.String(), jc.Contains,
   187  					fmt.Sprintf("DEBUG unit.u/0.something-happened %s\n", t.spec.stdout))
   188  			} else {
   189  				// Lines longer than lineBufferSize get split into multiple log messages
   190  				c.Check(writer.Buffer.String(), jc.Contains,
   191  					fmt.Sprintf("DEBUG unit.u/0.something-happened %s\n", t.spec.stdout[:lineBufferSize]))
   192  				c.Check(writer.Buffer.String(), jc.Contains,
   193  					fmt.Sprintf("DEBUG unit.u/0.something-happened %s\n", t.spec.stdout[lineBufferSize:]))
   194  			}
   195  		}
   196  		if t.spec.stderr != "" {
   197  			c.Check(writer.Buffer.String(), jc.Contains,
   198  				fmt.Sprintf("WARNING unit.u/0.something-happened %s\n", t.spec.stderr))
   199  		}
   200  	}
   201  }
   202  
   203  func (s *RunHookSuite) TestRunHookDispatchingHookHandler(c *gc.C) {
   204  	ctx, err := s.contextFactory.HookContext(hook.Info{Kind: hooks.ConfigChanged})
   205  	c.Assert(err, jc.ErrorIsNil)
   206  
   207  	paths := runnertesting.NewRealPaths(c)
   208  	rnr := runner.NewRunner(ctx, paths, nil)
   209  	spec := hookSpec{
   210  		name: "dispatch",
   211  		perm: 0700,
   212  	}
   213  	c.Logf("makeCharm %#v", spec)
   214  	makeCharm(c, spec, paths.GetCharmDir())
   215  
   216  	hookType, err := rnr.RunHook("something-happened")
   217  	c.Assert(err, jc.ErrorIsNil)
   218  	c.Assert(hookType, gc.Equals, runner.DispatchingHookHandler)
   219  }
   220  
   221  type MockContext struct {
   222  	context.Context
   223  	actionData      *context.ActionData
   224  	actionDataErr   error
   225  	actionParams    map[string]interface{}
   226  	actionParamsErr error
   227  	actionResults   map[string]interface{}
   228  	expectPid       int
   229  	flushBadge      string
   230  	flushFailure    error
   231  	flushResult     error
   232  	modelType       model.ModelType
   233  }
   234  
   235  func (ctx *MockContext) GetLogger(module string) loggo.Logger {
   236  	return loggo.GetLogger(module)
   237  }
   238  
   239  func (ctx *MockContext) UnitName() string {
   240  	return "some-unit/999"
   241  }
   242  
   243  func (ctx *MockContext) HookVars(
   244  	paths context.Paths,
   245  	_ bool,
   246  	envVars context.Environmenter,
   247  ) ([]string, error) {
   248  	path := envVars.Getenv("PATH")
   249  	newPath := fmt.Sprintf("PATH=pathypathpath;%s", path)
   250  	return []string{"VAR=value", newPath}, nil
   251  }
   252  
   253  func (ctx *MockContext) ActionData() (*context.ActionData, error) {
   254  	if ctx.actionData == nil {
   255  		return nil, errors.New("blam")
   256  	}
   257  	return ctx.actionData, ctx.actionDataErr
   258  }
   259  
   260  func (ctx *MockContext) SetProcess(process context.HookProcess) {
   261  	ctx.expectPid = process.Pid()
   262  }
   263  
   264  func (ctx *MockContext) Prepare() error {
   265  	return nil
   266  }
   267  
   268  func (ctx *MockContext) Flush(badge string, failure error) error {
   269  	ctx.flushBadge = badge
   270  	ctx.flushFailure = failure
   271  	return ctx.flushResult
   272  }
   273  
   274  func (ctx *MockContext) ActionParams() (map[string]interface{}, error) {
   275  	return ctx.actionParams, ctx.actionParamsErr
   276  }
   277  
   278  func (ctx *MockContext) UpdateActionResults(keys []string, value interface{}) error {
   279  	for _, key := range keys {
   280  		ctx.actionResults[key] = value
   281  	}
   282  	return nil
   283  }
   284  
   285  func (ctx *MockContext) ModelType() model.ModelType {
   286  	if ctx.modelType == "" {
   287  		return model.IAAS
   288  	}
   289  	return ctx.modelType
   290  }
   291  
   292  type RunMockContextSuite struct {
   293  	envtesting.IsolationSuite
   294  	paths runnertesting.RealPaths
   295  }
   296  
   297  var _ = gc.Suite(&RunMockContextSuite{})
   298  
   299  func (s *RunMockContextSuite) SetUpTest(c *gc.C) {
   300  	s.IsolationSuite.SetUpTest(c)
   301  	s.paths = runnertesting.NewRealPaths(c)
   302  }
   303  
   304  func (s *RunMockContextSuite) assertRecordedPid(c *gc.C, expectPid int) {
   305  	path := filepath.Join(s.paths.GetCharmDir(), "pid")
   306  	content, err := os.ReadFile(path)
   307  	c.Assert(err, jc.ErrorIsNil)
   308  	expectContent := fmt.Sprintf("%d", expectPid)
   309  	c.Assert(strings.TrimRight(string(content), "\r\n"), gc.Equals, expectContent)
   310  }
   311  
   312  func (s *RunMockContextSuite) TestRunHookFlushSuccess(c *gc.C) {
   313  	expectErr := errors.New("pew pew pew")
   314  	ctx := &MockContext{
   315  		flushResult: expectErr,
   316  	}
   317  	makeCharm(c, hookSpec{
   318  		dir:  "hooks",
   319  		name: hookName,
   320  		perm: 0700,
   321  	}, s.paths.GetCharmDir())
   322  	_, actualErr := runner.NewRunner(ctx, s.paths, nil).RunHook("something-happened")
   323  	c.Assert(actualErr, gc.Equals, expectErr)
   324  	c.Assert(ctx.flushBadge, gc.Equals, "something-happened")
   325  	c.Assert(ctx.flushFailure, gc.IsNil)
   326  	s.assertRecordedPid(c, ctx.expectPid)
   327  }
   328  
   329  func (s *RunMockContextSuite) TestRunHookFlushFailure(c *gc.C) {
   330  	expectErr := errors.New("pew pew pew")
   331  	ctx := &MockContext{
   332  		flushResult: expectErr,
   333  	}
   334  	makeCharm(c, hookSpec{
   335  		dir:  "hooks",
   336  		name: hookName,
   337  		perm: 0700,
   338  		code: 123,
   339  	}, s.paths.GetCharmDir())
   340  	_, actualErr := runner.NewRunner(ctx, s.paths, nil).RunHook("something-happened")
   341  	c.Assert(actualErr, gc.Equals, expectErr)
   342  	c.Assert(ctx.flushBadge, gc.Equals, "something-happened")
   343  	c.Assert(ctx.flushFailure, gc.ErrorMatches, "exit status 123")
   344  	s.assertRecordedPid(c, ctx.expectPid)
   345  }
   346  
   347  func (s *RunHookSuite) TestRunActionDispatchingHookHandler(c *gc.C) {
   348  	ctx := &MockContext{
   349  		actionData:    &context.ActionData{},
   350  		actionResults: map[string]interface{}{},
   351  	}
   352  
   353  	paths := runnertesting.NewRealPaths(c)
   354  	rnr := runner.NewRunner(ctx, paths, nil)
   355  	spec := hookSpec{
   356  		name: "dispatch",
   357  		perm: 0700,
   358  	}
   359  	c.Logf("makeCharm %#v", spec)
   360  	makeCharm(c, spec, paths.GetCharmDir())
   361  
   362  	hookType, err := rnr.RunAction("something-happened")
   363  	c.Assert(err, jc.ErrorIsNil)
   364  	c.Assert(hookType, gc.Equals, runner.DispatchingHookHandler)
   365  }
   366  
   367  func (s *RunMockContextSuite) TestRunActionFlushSuccess(c *gc.C) {
   368  	expectErr := errors.New("pew pew pew")
   369  	ctx := &MockContext{
   370  		flushResult:   expectErr,
   371  		actionData:    &context.ActionData{},
   372  		actionResults: map[string]interface{}{},
   373  	}
   374  	makeCharm(c, hookSpec{
   375  		dir:    "actions",
   376  		name:   hookName,
   377  		perm:   0700,
   378  		stdout: "hello",
   379  		stderr: "world",
   380  	}, s.paths.GetCharmDir())
   381  	hookType, actualErr := runner.NewRunner(ctx, s.paths, nil).RunAction("something-happened")
   382  	c.Assert(actualErr, gc.Equals, expectErr)
   383  	c.Assert(hookType, gc.Equals, runner.ExplicitHookHandler)
   384  	c.Assert(ctx.flushBadge, gc.Equals, "something-happened")
   385  	c.Assert(ctx.flushFailure, gc.IsNil)
   386  	s.assertRecordedPid(c, ctx.expectPid)
   387  	c.Assert(ctx.actionResults, jc.DeepEquals, map[string]interface{}{
   388  		"return-code": 0, "stderr": "world\n", "stdout": "hello\n",
   389  	})
   390  }
   391  
   392  func (s *RunMockContextSuite) TestRunActionFlushCharmActionsCAASSuccess(c *gc.C) {
   393  	expectErr := errors.New("pew pew pew")
   394  	ctx := &MockContext{
   395  		flushResult:   expectErr,
   396  		actionData:    &context.ActionData{},
   397  		actionResults: map[string]interface{}{},
   398  		modelType:     model.CAAS,
   399  	}
   400  	makeCharm(c, hookSpec{
   401  		dir:  "actions",
   402  		name: hookName,
   403  		perm: 0700,
   404  	}, s.paths.GetCharmDir())
   405  
   406  	execCount := 0
   407  	execFunc := func(params runner.ExecParams) (*exec.ExecResponse, error) {
   408  		execCount++
   409  		switch execCount {
   410  		case 1:
   411  			return &exec.ExecResponse{}, nil
   412  		case 2:
   413  			return &exec.ExecResponse{
   414  				Stdout: bytes.NewBufferString("hello").Bytes(),
   415  				Stderr: bytes.NewBufferString("world").Bytes(),
   416  			}, nil
   417  		}
   418  		c.Fatal("invalid count")
   419  		return nil, nil
   420  	}
   421  	_, actualErr := runner.NewRunner(ctx, s.paths, execFunc).RunAction("something-happened")
   422  	c.Assert(execCount, gc.Equals, 2)
   423  	c.Assert(actualErr, gc.Equals, expectErr)
   424  	c.Assert(ctx.flushBadge, gc.Equals, "something-happened")
   425  	c.Assert(ctx.flushFailure, gc.IsNil)
   426  	c.Assert(ctx.actionResults, jc.DeepEquals, map[string]interface{}{
   427  		"return-code": 0, "stderr": "world", "stdout": "hello",
   428  	})
   429  }
   430  
   431  func (s *RunMockContextSuite) TestRunActionFlushCharmActionsCAASFailed(c *gc.C) {
   432  	ctx := &MockContext{
   433  		flushResult: errors.New("pew pew pew"),
   434  		actionData:  &context.ActionData{},
   435  		modelType:   model.CAAS,
   436  	}
   437  	makeCharm(c, hookSpec{
   438  		dir:  "actions",
   439  		name: hookName,
   440  		perm: 0700,
   441  	}, s.paths.GetCharmDir())
   442  	execCount := 0
   443  	execFunc := func(params runner.ExecParams) (*exec.ExecResponse, error) {
   444  		execCount++
   445  		switch execCount {
   446  		case 1:
   447  			return &exec.ExecResponse{}, nil
   448  		case 2:
   449  			return nil, errors.Errorf("failed exec")
   450  		}
   451  		c.Fatal("invalid count")
   452  		return nil, nil
   453  	}
   454  	_, actualErr := runner.NewRunner(ctx, s.paths, execFunc).RunAction("something-happened")
   455  	c.Assert(execCount, gc.Equals, 2)
   456  	c.Assert(actualErr, gc.Equals, ctx.flushResult)
   457  	c.Assert(ctx.flushBadge, gc.Equals, "something-happened")
   458  	c.Assert(ctx.flushFailure, gc.ErrorMatches, "failed exec")
   459  }
   460  
   461  func (s *RunMockContextSuite) TestRunActionFlushFailure(c *gc.C) {
   462  	expectErr := errors.New("pew pew pew")
   463  	ctx := &MockContext{
   464  		flushResult:   expectErr,
   465  		actionData:    &context.ActionData{},
   466  		actionResults: map[string]interface{}{},
   467  	}
   468  	makeCharm(c, hookSpec{
   469  		dir:  "actions",
   470  		name: hookName,
   471  		perm: 0700,
   472  		code: 123,
   473  	}, s.paths.GetCharmDir())
   474  	_, actualErr := runner.NewRunner(ctx, s.paths, nil).RunAction("something-happened")
   475  	c.Assert(actualErr, gc.Equals, expectErr)
   476  	c.Assert(ctx.flushBadge, gc.Equals, "something-happened")
   477  	c.Assert(ctx.flushFailure, gc.ErrorMatches, "exit status 123")
   478  	s.assertRecordedPid(c, ctx.expectPid)
   479  }
   480  
   481  func (s *RunMockContextSuite) TestRunActionDataFailure(c *gc.C) {
   482  	expectErr := errors.New("stork")
   483  	ctx := &MockContext{
   484  		actionData:    &context.ActionData{},
   485  		actionDataErr: expectErr,
   486  	}
   487  	_, actualErr := runner.NewRunner(ctx, s.paths, nil).RunAction("juju-exec")
   488  	c.Assert(errors.Cause(actualErr), gc.Equals, expectErr)
   489  }
   490  
   491  func (s *RunMockContextSuite) TestRunActionSuccessful(c *gc.C) {
   492  	params := map[string]interface{}{
   493  		"command": "echo 1",
   494  		"timeout": 0,
   495  	}
   496  	ctx := &MockContext{
   497  		actionData: &context.ActionData{
   498  			Params: params,
   499  		},
   500  		actionParams:  params,
   501  		actionResults: map[string]interface{}{},
   502  	}
   503  	_, err := runner.NewRunner(ctx, s.paths, nil).RunAction("juju-exec")
   504  	c.Assert(err, jc.ErrorIsNil)
   505  	c.Assert(ctx.flushBadge, gc.Equals, "juju-exec")
   506  	c.Assert(ctx.flushFailure, gc.IsNil)
   507  	c.Assert(ctx.actionResults["return-code"], gc.Equals, 0)
   508  	c.Assert(strings.TrimRight(ctx.actionResults["stdout"].(string), "\r\n"), gc.Equals, "1")
   509  	c.Assert(ctx.actionResults["stderr"], gc.Equals, nil)
   510  }
   511  
   512  func (s *RunMockContextSuite) TestRunActionError(c *gc.C) {
   513  	params := map[string]interface{}{
   514  		"command": "echo 1\nexit 3",
   515  		"timeout": 0,
   516  	}
   517  	ctx := &MockContext{
   518  		actionData: &context.ActionData{
   519  			Params: params,
   520  		},
   521  		actionParams:  params,
   522  		actionResults: map[string]interface{}{},
   523  	}
   524  	_, err := runner.NewRunner(ctx, s.paths, nil).RunAction("juju-exec")
   525  	c.Assert(err, jc.ErrorIsNil)
   526  	c.Assert(ctx.flushBadge, gc.Equals, "juju-exec")
   527  	c.Assert(ctx.flushFailure, gc.IsNil)
   528  	c.Assert(ctx.actionResults["return-code"], gc.Equals, 3)
   529  	c.Assert(strings.TrimRight(ctx.actionResults["stdout"].(string), "\r\n"), gc.Equals, "1")
   530  	c.Assert(ctx.actionResults["stderr"], gc.Equals, nil)
   531  }
   532  
   533  func (s *RunMockContextSuite) TestRunActionCancelled(c *gc.C) {
   534  	timeout := 1 * time.Nanosecond
   535  	params := map[string]interface{}{
   536  		"command": "sleep 10",
   537  		"timeout": float64(timeout.Nanoseconds()),
   538  	}
   539  	ctx := &MockContext{
   540  		actionData: &context.ActionData{
   541  			Params: params,
   542  		},
   543  		actionParams:  params,
   544  		actionResults: map[string]interface{}{},
   545  	}
   546  	_, err := runner.NewRunner(ctx, s.paths, nil).RunAction("juju-exec")
   547  	c.Assert(err, jc.ErrorIsNil)
   548  	c.Assert(ctx.flushBadge, gc.Equals, "juju-exec")
   549  	c.Assert(ctx.flushFailure, gc.Equals, exec.ErrCancelled)
   550  	c.Assert(ctx.actionResults["return-code"], gc.Equals, 0)
   551  	c.Assert(ctx.actionResults["stdout"], gc.Equals, nil)
   552  	c.Assert(ctx.actionResults["stderr"], gc.Equals, nil)
   553  }
   554  
   555  func (s *RunMockContextSuite) TestRunCommandsFlushSuccess(c *gc.C) {
   556  	expectErr := errors.New("pew pew pew")
   557  	ctx := &MockContext{
   558  		flushResult: expectErr,
   559  	}
   560  	_, actualErr := runner.NewRunner(ctx, s.paths, nil).RunCommands(echoPidScript, runner.Operator)
   561  	c.Assert(actualErr, gc.Equals, expectErr)
   562  	c.Assert(ctx.flushBadge, gc.Equals, "run commands")
   563  	c.Assert(ctx.flushFailure, gc.IsNil)
   564  	s.assertRecordedPid(c, ctx.expectPid)
   565  }
   566  
   567  func (s *RunMockContextSuite) TestRunCommandsFlushFailure(c *gc.C) {
   568  	expectErr := errors.New("pew pew pew")
   569  	ctx := &MockContext{
   570  		flushResult: expectErr,
   571  	}
   572  	_, actualErr := runner.NewRunner(ctx, s.paths, nil).RunCommands(echoPidScript+"; exit 123", runner.Operator)
   573  	c.Assert(actualErr, gc.Equals, expectErr)
   574  	c.Assert(ctx.flushBadge, gc.Equals, "run commands")
   575  	c.Assert(ctx.flushFailure, gc.IsNil) // exit code in _ result, as tested elsewhere
   576  	s.assertRecordedPid(c, ctx.expectPid)
   577  }
   578  
   579  func (s *RunMockContextSuite) TestRunCommandsFlushSuccessWorkloadNoExec(c *gc.C) {
   580  	expectErr := errors.New("pew pew pew")
   581  	ctx := &MockContext{
   582  		flushResult: expectErr,
   583  		modelType:   model.CAAS,
   584  	}
   585  	_, actualErr := runner.NewRunner(ctx, s.paths, nil).RunCommands(echoPidScript, runner.Workload)
   586  	c.Assert(actualErr, gc.Equals, expectErr)
   587  	c.Assert(ctx.flushBadge, gc.Equals, "run commands")
   588  	c.Assert(ctx.flushFailure, gc.IsNil)
   589  	s.assertRecordedPid(c, ctx.expectPid)
   590  }
   591  
   592  func (s *RunMockContextSuite) TestRunCommandsFlushFailureWorkloadNoExec(c *gc.C) {
   593  	expectErr := errors.New("pew pew pew")
   594  	ctx := &MockContext{
   595  		flushResult: expectErr,
   596  		modelType:   model.CAAS,
   597  	}
   598  	_, actualErr := runner.NewRunner(ctx, s.paths, nil).RunCommands(echoPidScript+"; exit 123", runner.Workload)
   599  	c.Assert(actualErr, gc.Equals, expectErr)
   600  	c.Assert(ctx.flushBadge, gc.Equals, "run commands")
   601  	c.Assert(ctx.flushFailure, gc.IsNil) // exit code in _ result, as tested elsewhere
   602  	s.assertRecordedPid(c, ctx.expectPid)
   603  }
   604  
   605  func (s *RunMockContextSuite) TestRunCommandsFlushSuccessWorkload(c *gc.C) {
   606  	ctx := &MockContext{
   607  		modelType: model.CAAS,
   608  	}
   609  	execCount := 0
   610  	execFunc := func(params runner.ExecParams) (*exec.ExecResponse, error) {
   611  		execCount++
   612  		switch execCount {
   613  		case 1:
   614  			return &exec.ExecResponse{}, nil
   615  		case 2:
   616  			return &exec.ExecResponse{}, nil
   617  		}
   618  		c.Fatal("invalid count")
   619  		return nil, nil
   620  	}
   621  	_, actualErr := runner.NewRunner(ctx, s.paths, execFunc).RunCommands(echoPidScript, runner.Workload)
   622  	c.Assert(execCount, gc.Equals, 2)
   623  	c.Assert(actualErr, jc.ErrorIsNil)
   624  	c.Assert(ctx.flushBadge, gc.Equals, "run commands")
   625  	c.Assert(ctx.flushFailure, jc.ErrorIsNil)
   626  }
   627  
   628  func (s *RunMockContextSuite) TestRunCommandsFlushFailedWorkload(c *gc.C) {
   629  	expectErr := errors.New("pew pew pew")
   630  	ctx := &MockContext{
   631  		flushResult: expectErr,
   632  		modelType:   model.CAAS,
   633  	}
   634  	execCount := 0
   635  	execFunc := func(params runner.ExecParams) (*exec.ExecResponse, error) {
   636  		execCount++
   637  		switch execCount {
   638  		case 1:
   639  			return &exec.ExecResponse{}, nil
   640  		case 2:
   641  			return nil, errors.Errorf("failed exec")
   642  		}
   643  		c.Fatal("invalid count")
   644  		return nil, nil
   645  	}
   646  	_, actualErr := runner.NewRunner(ctx, s.paths, execFunc).RunCommands(echoPidScript, runner.Workload)
   647  	c.Assert(execCount, gc.Equals, 2)
   648  	c.Assert(actualErr, gc.Equals, expectErr)
   649  	c.Assert(ctx.flushBadge, gc.Equals, "run commands")
   650  	c.Assert(ctx.flushFailure, gc.ErrorMatches, "failed exec")
   651  }
   652  
   653  func (s *RunMockContextSuite) TestRunActionCAASSuccess(c *gc.C) {
   654  	params := map[string]interface{}{
   655  		"command":          "echo 1",
   656  		"timeout":          0,
   657  		"workload-context": true,
   658  	}
   659  	ctx := &MockContext{
   660  		modelType: model.CAAS,
   661  		actionData: &context.ActionData{
   662  			Params: params,
   663  		},
   664  		actionParams:  params,
   665  		actionResults: map[string]interface{}{},
   666  	}
   667  	execCount := 0
   668  	execFunc := func(params runner.ExecParams) (*exec.ExecResponse, error) {
   669  		execCount++
   670  		switch execCount {
   671  		case 1:
   672  			return &exec.ExecResponse{}, nil
   673  		case 2:
   674  			return &exec.ExecResponse{
   675  				Stdout: bytes.NewBufferString("1").Bytes(),
   676  			}, nil
   677  		}
   678  		c.Fatal("invalid count")
   679  		return nil, nil
   680  	}
   681  	_, err := runner.NewRunner(ctx, s.paths, execFunc).RunAction("juju-exec")
   682  	c.Assert(execCount, gc.Equals, 2)
   683  	c.Assert(err, jc.ErrorIsNil)
   684  	c.Assert(ctx.flushBadge, gc.Equals, "juju-exec")
   685  	c.Assert(ctx.actionResults["return-code"], gc.Equals, 0)
   686  	c.Assert(strings.TrimRight(ctx.actionResults["stdout"].(string), "\r\n"), gc.Equals, "1")
   687  	c.Assert(ctx.actionResults["stderr"], gc.Equals, nil)
   688  }
   689  
   690  func (s *RunMockContextSuite) TestRunActionCAASCorrectEnv(c *gc.C) {
   691  	params := map[string]interface{}{
   692  		"command":          "echo 1",
   693  		"timeout":          0,
   694  		"workload-context": true,
   695  	}
   696  	ctx := &MockContext{
   697  		modelType: model.CAAS,
   698  		actionData: &context.ActionData{
   699  			Params: params,
   700  		},
   701  		actionParams:  params,
   702  		actionResults: map[string]interface{}{},
   703  	}
   704  	execCount := 0
   705  	execFunc := func(params runner.ExecParams) (*exec.ExecResponse, error) {
   706  		execCount++
   707  		switch execCount {
   708  		case 1:
   709  			c.Assert(params.Commands, gc.DeepEquals, []string{"unset _; export"})
   710  			return &exec.ExecResponse{
   711  				Stdout: []byte(`
   712  export BLA='bla'
   713  export PATH='important-path'
   714  `[1:]),
   715  			}, nil
   716  		case 2:
   717  			path := ""
   718  			for _, v := range params.Env {
   719  				if strings.HasPrefix(v, "PATH=") {
   720  					path = v
   721  				}
   722  			}
   723  			c.Assert(path, gc.Equals, "PATH=pathypathpath;important-path")
   724  			return &exec.ExecResponse{
   725  				Stdout: bytes.NewBufferString("1").Bytes(),
   726  			}, nil
   727  		}
   728  		c.Fatal("invalid count")
   729  		return nil, nil
   730  	}
   731  	_, err := runner.NewRunner(ctx, s.paths, execFunc).RunAction("juju-exec")
   732  	c.Assert(execCount, gc.Equals, 2)
   733  	c.Assert(err, jc.ErrorIsNil)
   734  	c.Assert(ctx.flushBadge, gc.Equals, "juju-exec")
   735  	c.Assert(ctx.actionResults["return-code"], gc.Equals, 0)
   736  	c.Assert(strings.TrimRight(ctx.actionResults["stdout"].(string), "\r\n"), gc.Equals, "1")
   737  	c.Assert(ctx.actionResults["stderr"], gc.Equals, nil)
   738  }
   739  
   740  func (s *RunMockContextSuite) TestRunActionOnWorkloadIgnoredIAAS(c *gc.C) {
   741  	params := map[string]interface{}{
   742  		"command":          "echo 1",
   743  		"timeout":          0,
   744  		"workload-context": true,
   745  	}
   746  	ctx := &MockContext{
   747  		modelType: model.IAAS,
   748  		actionData: &context.ActionData{
   749  			Params: params,
   750  		},
   751  		actionParams:  params,
   752  		actionResults: map[string]interface{}{},
   753  	}
   754  	_, err := runner.NewRunner(ctx, s.paths, nil).RunAction("juju-exec")
   755  	c.Assert(err, jc.ErrorIsNil)
   756  	c.Assert(ctx.flushBadge, gc.Equals, "juju-exec")
   757  	c.Assert(ctx.flushFailure, gc.IsNil)
   758  	c.Assert(ctx.actionResults["return-code"], gc.Equals, 0)
   759  	c.Assert(strings.TrimRight(ctx.actionResults["stdout"].(string), "\r\n"), gc.Equals, "1")
   760  	c.Assert(ctx.actionResults["stderr"], gc.Equals, nil)
   761  }
   762  
   763  func (s *RunMockContextSuite) TestOperatorActionCAASSuccess(c *gc.C) {
   764  	expectErr := errors.New("pew pew pew")
   765  	params := map[string]interface{}{
   766  		"workload-context": false,
   767  	}
   768  	ctx := &MockContext{
   769  		modelType: model.CAAS,
   770  		actionData: &context.ActionData{
   771  			Params: params,
   772  		},
   773  		actionParams:  params,
   774  		actionResults: map[string]interface{}{},
   775  		flushResult:   expectErr}
   776  	makeCharm(c, hookSpec{
   777  		dir:    "actions",
   778  		name:   hookName,
   779  		perm:   0700,
   780  		stdout: "hello",
   781  		stderr: "world",
   782  	}, s.paths.GetCharmDir())
   783  	hookType, actualErr := runner.NewRunner(ctx, s.paths, nil).RunAction("something-happened")
   784  	c.Assert(actualErr, gc.Equals, expectErr)
   785  	c.Assert(hookType, gc.Equals, runner.ExplicitHookHandler)
   786  	c.Assert(ctx.flushBadge, gc.Equals, "something-happened")
   787  	c.Assert(ctx.flushFailure, gc.IsNil)
   788  	s.assertRecordedPid(c, ctx.expectPid)
   789  	c.Assert(ctx.actionResults, jc.DeepEquals, map[string]interface{}{
   790  		"return-code": 0, "stderr": "world\n", "stdout": "hello\n",
   791  	})
   792  }