github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/uniter/runner/debug/server_test.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package debug
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"runtime"
    14  	"strings"
    15  	"time"
    16  
    17  	jc "github.com/juju/testing/checkers"
    18  	gc "gopkg.in/check.v1"
    19  
    20  	"github.com/juju/juju/testing"
    21  )
    22  
    23  type DebugHooksServerSuite struct {
    24  	testing.BaseSuite
    25  	ctx     *HooksContext
    26  	fakebin string
    27  	tmpdir  string
    28  }
    29  
    30  var _ = gc.Suite(&DebugHooksServerSuite{})
    31  
    32  // echocommand outputs its name and arguments to stdout for verification,
    33  // and exits with the value of $EXIT_CODE
    34  var echocommand = `#!/bin/bash --norc
    35  echo $(basename $0) $@
    36  exit $EXIT_CODE
    37  `
    38  
    39  var fakecommands = []string{"sleep", "tmux"}
    40  
    41  func (s *DebugHooksServerSuite) SetUpTest(c *gc.C) {
    42  	if runtime.GOOS == "windows" {
    43  		c.Skip("bug 1403084: Currently debug does not work on windows")
    44  	}
    45  	s.fakebin = c.MkDir()
    46  
    47  	// Create a clean $TMPDIR for the debug hooks scripts.
    48  	s.tmpdir = filepath.Join(c.MkDir(), "debug-hooks")
    49  	err := os.RemoveAll(s.tmpdir)
    50  	c.Assert(err, jc.ErrorIsNil)
    51  	err = os.MkdirAll(s.tmpdir, 0755)
    52  	c.Assert(err, jc.ErrorIsNil)
    53  
    54  	s.PatchEnvPathPrepend(s.fakebin)
    55  	s.PatchEnvironment("TMPDIR", s.tmpdir)
    56  	s.PatchEnvironment("TEST_RESULT", "")
    57  	for _, name := range fakecommands {
    58  		err := ioutil.WriteFile(filepath.Join(s.fakebin, name), []byte(echocommand), 0777)
    59  		c.Assert(err, jc.ErrorIsNil)
    60  	}
    61  	s.ctx = NewHooksContext("foo/8")
    62  	s.ctx.FlockDir = c.MkDir()
    63  	s.PatchEnvironment("JUJU_UNIT_NAME", s.ctx.Unit)
    64  }
    65  
    66  func (s *DebugHooksServerSuite) TestFindSession(c *gc.C) {
    67  	// Test "tmux has-session" failure. The error
    68  	// message is the output of tmux has-session.
    69  	os.Setenv("EXIT_CODE", "1")
    70  	session, err := s.ctx.FindSession()
    71  	c.Assert(session, gc.IsNil)
    72  	c.Assert(err, gc.ErrorMatches, regexp.QuoteMeta("tmux has-session -t "+s.ctx.Unit+"\n"))
    73  	os.Setenv("EXIT_CODE", "")
    74  
    75  	// tmux session exists, but missing debug-hooks file: error.
    76  	session, err = s.ctx.FindSession()
    77  	c.Assert(session, gc.IsNil)
    78  	c.Assert(err, gc.NotNil)
    79  	c.Assert(err, jc.Satisfies, os.IsNotExist)
    80  
    81  	// Hooks file is present, empty.
    82  	err = ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777)
    83  	c.Assert(err, jc.ErrorIsNil)
    84  	session, err = s.ctx.FindSession()
    85  	c.Assert(session, gc.NotNil)
    86  	c.Assert(err, jc.ErrorIsNil)
    87  	// If session.hooks is empty, it'll match anything.
    88  	c.Assert(session.MatchHook(""), jc.IsTrue)
    89  	c.Assert(session.MatchHook("something"), jc.IsTrue)
    90  
    91  	// Hooks file is present, non-empty
    92  	err = ioutil.WriteFile(s.ctx.ClientFileLock(), []byte(`hooks: [foo, bar, baz]`), 0777)
    93  	c.Assert(err, jc.ErrorIsNil)
    94  	session, err = s.ctx.FindSession()
    95  	c.Assert(session, gc.NotNil)
    96  	c.Assert(err, jc.ErrorIsNil)
    97  	// session should only match "foo", "bar" or "baz".
    98  	c.Assert(session.MatchHook(""), jc.IsFalse)
    99  	c.Assert(session.MatchHook("something"), jc.IsFalse)
   100  	c.Assert(session.MatchHook("foo"), jc.IsTrue)
   101  	c.Assert(session.MatchHook("bar"), jc.IsTrue)
   102  	c.Assert(session.MatchHook("baz"), jc.IsTrue)
   103  	c.Assert(session.MatchHook("foo bar baz"), jc.IsFalse)
   104  }
   105  
   106  func (s *DebugHooksServerSuite) TestRunHookExceptional(c *gc.C) {
   107  	err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777)
   108  	c.Assert(err, jc.ErrorIsNil)
   109  	session, err := s.ctx.FindSession()
   110  	c.Assert(session, gc.NotNil)
   111  	c.Assert(err, jc.ErrorIsNil)
   112  
   113  	flockAcquired := make(chan struct{}, 1)
   114  	waitForFlock := func() {
   115  		select {
   116  		case <-flockAcquired:
   117  		case <-time.After(testing.ShortWait):
   118  			c.Fatalf("timed out waiting for hook to acquire flock")
   119  		}
   120  	}
   121  
   122  	// Run the hook in debug mode with no exit flock held.
   123  	// The exit flock will be acquired immediately, and the
   124  	// debug-hooks server process killed.
   125  	s.PatchValue(&waitClientExit, func(*ServerSession) {
   126  		flockAcquired <- struct{}{}
   127  	})
   128  	err = session.RunHook("myhook", s.tmpdir, os.Environ())
   129  	c.Assert(err, gc.ErrorMatches, "signal: [kK]illed")
   130  	waitForFlock()
   131  
   132  	// Run the hook in debug mode, simulating the holding
   133  	// of the exit flock. This simulates the client process
   134  	// starting but not cleanly exiting (normally the .pid
   135  	// file is updated, and the server waits on the client
   136  	// process' death).
   137  	ch := make(chan bool) // acquire the flock
   138  	var clientExited bool
   139  	s.PatchValue(&waitClientExit, func(*ServerSession) {
   140  		clientExited = <-ch
   141  		flockAcquired <- struct{}{}
   142  	})
   143  	go func() { ch <- true }() // asynchronously release the flock
   144  	err = session.RunHook("myhook", s.tmpdir, os.Environ())
   145  	waitForFlock()
   146  	c.Assert(clientExited, jc.IsTrue)
   147  	c.Assert(err, gc.ErrorMatches, "signal: [kK]illed")
   148  }
   149  
   150  func (s *DebugHooksServerSuite) TestRunHook(c *gc.C) {
   151  	err := ioutil.WriteFile(s.ctx.ClientFileLock(), []byte{}, 0777)
   152  	c.Assert(err, jc.ErrorIsNil)
   153  	var output bytes.Buffer
   154  	session, err := s.ctx.FindSessionWithWriter(&output)
   155  	c.Assert(session, gc.NotNil)
   156  	c.Assert(err, jc.ErrorIsNil)
   157  
   158  	flockRequestCh := make(chan chan struct{})
   159  	s.PatchValue(&waitClientExit, func(*ServerSession) {
   160  		<-<-flockRequestCh
   161  	})
   162  	defer close(flockRequestCh)
   163  
   164  	const hookName = "myhook"
   165  	runHookCh := make(chan error)
   166  	go func() {
   167  		runHookCh <- session.RunHook(hookName, s.tmpdir, os.Environ())
   168  	}()
   169  
   170  	flockCh := make(chan struct{})
   171  	select {
   172  	case flockRequestCh <- flockCh:
   173  	case <-time.After(testing.LongWait):
   174  		c.Fatal("timed out waiting for flock to be requested")
   175  	}
   176  	defer close(flockCh)
   177  
   178  	// Look for the debug hooks temporary dir, inside $TMPDIR.
   179  	tmpdir, err := os.Open(s.tmpdir)
   180  	if err != nil {
   181  		c.Fatalf("Failed to open $TMPDIR: %s", err)
   182  	}
   183  	defer tmpdir.Close()
   184  	entries, err := tmpdir.Readdir(-1)
   185  	if err != nil {
   186  		c.Fatalf("Failed to read $TMPDIR: %s", err)
   187  	}
   188  	c.Assert(entries, gc.HasLen, 1)
   189  	c.Assert(entries[0].IsDir(), jc.IsTrue)
   190  	c.Assert(strings.HasPrefix(entries[0].Name(), "juju-debug-hooks-"), jc.IsTrue)
   191  
   192  	debugDir := filepath.Join(s.tmpdir, entries[0].Name())
   193  	hookScript := filepath.Join(debugDir, "hook.sh")
   194  	_, err = os.Stat(hookScript)
   195  	c.Assert(err, jc.ErrorIsNil)
   196  
   197  	// Check that the debug hooks script exports the environment,
   198  	// and the values are as expected. When RunHook completes,
   199  	// it removes the temporary directory in which the scripts
   200  	// reside; so we must wait for it to be written before we
   201  	// wait for RunHook to return.
   202  	timeout := time.After(testing.LongWait)
   203  	envsh := filepath.Join(debugDir, "env.sh")
   204  	for {
   205  		// Wait for env.sh to show up, and have some content. If it exists and
   206  		// is size 0, we managed to see it at exactly the time it is being
   207  		// written.
   208  		if st, err := os.Stat(envsh); err == nil {
   209  			if st.Size() != 0 {
   210  				break
   211  			}
   212  		}
   213  		select {
   214  		case <-time.After(time.Millisecond):
   215  		case <-timeout:
   216  			c.Fatal("timed out waiting for env.sh to be written")
   217  		}
   218  	}
   219  	s.verifyEnvshFile(c, envsh, hookName)
   220  
   221  	// Write the hook.pid file, causing the debug hooks script to exit.
   222  	hookpid := filepath.Join(debugDir, "hook.pid")
   223  	err = ioutil.WriteFile(hookpid, []byte("not a pid"), 0777)
   224  	c.Assert(err, jc.ErrorIsNil)
   225  
   226  	// RunHook should complete without waiting to be
   227  	// killed, and despite the exit lock being held.
   228  	select {
   229  	case err := <-runHookCh:
   230  		c.Assert(err, jc.ErrorIsNil)
   231  	case <-time.After(testing.LongWait):
   232  		c.Fatal("RunHook did not complete")
   233  	}
   234  }
   235  
   236  func (s *DebugHooksServerSuite) verifyEnvshFile(c *gc.C, envshPath string, hookName string) {
   237  	data, err := ioutil.ReadFile(envshPath)
   238  	c.Assert(err, jc.ErrorIsNil)
   239  	contents := string(data)
   240  	c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_UNIT_NAME=%q", s.ctx.Unit))
   241  	c.Assert(contents, jc.Contains, fmt.Sprintf("JUJU_HOOK_NAME=%q", hookName))
   242  	c.Assert(contents, jc.Contains, fmt.Sprintf(`PS1="%s:%s %% "`, s.ctx.Unit, hookName))
   243  }