github.com/bigcommerce/nomad@v0.9.3-bc/client/allocwatcher/alloc_watcher_test.go (about)

     1  package allocwatcher
     2  
     3  import (
     4  	"archive/tar"
     5  	"bytes"
     6  	"context"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	hclog "github.com/hashicorp/go-hclog"
    16  	"github.com/hashicorp/nomad/client/allocdir"
    17  	cstructs "github.com/hashicorp/nomad/client/structs"
    18  	"github.com/hashicorp/nomad/helper/testlog"
    19  	"github.com/hashicorp/nomad/nomad/mock"
    20  	"github.com/hashicorp/nomad/nomad/structs"
    21  	"github.com/hashicorp/nomad/testutil"
    22  	"github.com/stretchr/testify/require"
    23  )
    24  
    25  // fakeAllocRunner implements AllocRunnerMeta
    26  type fakeAllocRunner struct {
    27  	alloc       *structs.Allocation
    28  	AllocDir    *allocdir.AllocDir
    29  	Broadcaster *cstructs.AllocBroadcaster
    30  }
    31  
    32  // newFakeAllocRunner creates a new AllocRunnerMeta. Callers must call
    33  // AllocDir.Destroy() when finished.
    34  func newFakeAllocRunner(t *testing.T, logger hclog.Logger) *fakeAllocRunner {
    35  	alloc := mock.Alloc()
    36  	alloc.Job.TaskGroups[0].EphemeralDisk.Sticky = true
    37  	alloc.Job.TaskGroups[0].EphemeralDisk.Migrate = true
    38  
    39  	path, err := ioutil.TempDir("", "nomad_test_watcher")
    40  	require.NoError(t, err)
    41  
    42  	return &fakeAllocRunner{
    43  		alloc:       alloc,
    44  		AllocDir:    allocdir.NewAllocDir(logger, path),
    45  		Broadcaster: cstructs.NewAllocBroadcaster(logger),
    46  	}
    47  }
    48  
    49  func (f *fakeAllocRunner) GetAllocDir() *allocdir.AllocDir {
    50  	return f.AllocDir
    51  }
    52  
    53  func (f *fakeAllocRunner) Listener() *cstructs.AllocListener {
    54  	return f.Broadcaster.Listen()
    55  }
    56  
    57  func (f *fakeAllocRunner) Alloc() *structs.Allocation {
    58  	return f.alloc
    59  }
    60  
    61  // newConfig returns a new Config and cleanup func
    62  func newConfig(t *testing.T) (Config, func()) {
    63  	logger := testlog.HCLogger(t)
    64  
    65  	prevAR := newFakeAllocRunner(t, logger)
    66  
    67  	alloc := mock.Alloc()
    68  	alloc.PreviousAllocation = prevAR.Alloc().ID
    69  	alloc.Job.TaskGroups[0].EphemeralDisk.Sticky = true
    70  	alloc.Job.TaskGroups[0].EphemeralDisk.Migrate = true
    71  	alloc.Job.TaskGroups[0].Tasks[0].Driver = "mock_driver"
    72  
    73  	config := Config{
    74  		Alloc:          alloc,
    75  		PreviousRunner: prevAR,
    76  		RPC:            nil,
    77  		Config:         nil,
    78  		MigrateToken:   "fake_token",
    79  		Logger:         logger,
    80  	}
    81  
    82  	cleanup := func() {
    83  		prevAR.AllocDir.Destroy()
    84  	}
    85  
    86  	return config, cleanup
    87  }
    88  
    89  // TestPrevAlloc_Noop asserts that when no previous allocation is set the noop
    90  // implementation is returned that does not block or perform migrations.
    91  func TestPrevAlloc_Noop(t *testing.T) {
    92  	conf, cleanup := newConfig(t)
    93  	defer cleanup()
    94  
    95  	conf.Alloc.PreviousAllocation = ""
    96  
    97  	watcher, migrator := NewAllocWatcher(conf)
    98  	require.NotNil(t, watcher)
    99  	_, ok := migrator.(NoopPrevAlloc)
   100  	require.True(t, ok, "expected migrator to be NoopPrevAlloc")
   101  
   102  	done := make(chan int, 2)
   103  	go func() {
   104  		watcher.Wait(context.Background())
   105  		done <- 1
   106  		migrator.Migrate(context.Background(), nil)
   107  		done <- 1
   108  	}()
   109  	require.False(t, watcher.IsWaiting())
   110  	require.False(t, migrator.IsMigrating())
   111  	<-done
   112  	<-done
   113  }
   114  
   115  // TestPrevAlloc_LocalPrevAlloc_Block asserts that when a previous alloc runner
   116  // is set a localPrevAlloc will block on it.
   117  func TestPrevAlloc_LocalPrevAlloc_Block(t *testing.T) {
   118  	t.Parallel()
   119  	conf, cleanup := newConfig(t)
   120  
   121  	defer cleanup()
   122  
   123  	conf.Alloc.Job.TaskGroups[0].Tasks[0].Config = map[string]interface{}{
   124  		"run_for": "500ms",
   125  	}
   126  
   127  	_, waiter := NewAllocWatcher(conf)
   128  
   129  	// Wait in a goroutine with a context to make sure it exits at the right time
   130  	ctx, cancel := context.WithCancel(context.Background())
   131  	defer cancel()
   132  	go func() {
   133  		defer cancel()
   134  		waiter.Wait(ctx)
   135  	}()
   136  
   137  	// Assert watcher is waiting
   138  	testutil.WaitForResult(func() (bool, error) {
   139  		return waiter.IsWaiting(), fmt.Errorf("expected watcher to be waiting")
   140  	}, func(err error) {
   141  		t.Fatalf("error: %v", err)
   142  	})
   143  
   144  	// Broadcast a non-terminal alloc update to assert only terminal
   145  	// updates break out of waiting.
   146  	update := conf.PreviousRunner.Alloc().Copy()
   147  	update.DesiredStatus = structs.AllocDesiredStatusStop
   148  	update.ModifyIndex++
   149  	update.AllocModifyIndex++
   150  
   151  	broadcaster := conf.PreviousRunner.(*fakeAllocRunner).Broadcaster
   152  	err := broadcaster.Send(update)
   153  	require.NoError(t, err)
   154  
   155  	// Assert watcher is still waiting because alloc isn't terminal
   156  	testutil.WaitForResult(func() (bool, error) {
   157  		return waiter.IsWaiting(), fmt.Errorf("expected watcher to be waiting")
   158  	}, func(err error) {
   159  		t.Fatalf("error: %v", err)
   160  	})
   161  
   162  	// Stop the previous alloc and assert watcher stops blocking
   163  	update = update.Copy()
   164  	update.DesiredStatus = structs.AllocDesiredStatusStop
   165  	update.ClientStatus = structs.AllocClientStatusComplete
   166  	update.ModifyIndex++
   167  	update.AllocModifyIndex++
   168  
   169  	err = broadcaster.Send(update)
   170  	require.NoError(t, err)
   171  
   172  	testutil.WaitForResult(func() (bool, error) {
   173  		if waiter.IsWaiting() {
   174  			return false, fmt.Errorf("did not expect watcher to be waiting")
   175  		}
   176  		return !waiter.IsMigrating(), fmt.Errorf("did not expect watcher to be migrating")
   177  	}, func(err error) {
   178  		t.Fatalf("error: %v", err)
   179  	})
   180  }
   181  
   182  // TestPrevAlloc_LocalPrevAlloc_Terminated asserts that when a previous alloc
   183  // runner has already terminated the watcher does not block on the broadcaster.
   184  func TestPrevAlloc_LocalPrevAlloc_Terminated(t *testing.T) {
   185  	t.Parallel()
   186  	conf, cleanup := newConfig(t)
   187  	defer cleanup()
   188  
   189  	conf.PreviousRunner.Alloc().ClientStatus = structs.AllocClientStatusComplete
   190  
   191  	waiter, _ := NewAllocWatcher(conf)
   192  
   193  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   194  	defer cancel()
   195  
   196  	// Since prev alloc is terminal, Wait should exit immediately with no
   197  	// context error
   198  	require.NoError(t, waiter.Wait(ctx))
   199  }
   200  
   201  // TestPrevAlloc_StreamAllocDir_Error asserts that errors encountered while
   202  // streaming a tar cause the migration to be cancelled and no files are written
   203  // (migrations are atomic).
   204  func TestPrevAlloc_StreamAllocDir_Error(t *testing.T) {
   205  	t.Parallel()
   206  	dest, err := ioutil.TempDir("", "nomadtest-")
   207  	if err != nil {
   208  		t.Fatalf("err: %v", err)
   209  	}
   210  	defer os.RemoveAll(dest)
   211  
   212  	// This test only unit tests streamAllocDir so we only need a partially
   213  	// complete remotePrevAlloc
   214  	prevAlloc := &remotePrevAlloc{
   215  		logger:      testlog.HCLogger(t),
   216  		allocID:     "123",
   217  		prevAllocID: "abc",
   218  		migrate:     true,
   219  	}
   220  
   221  	tarBuf := bytes.NewBuffer(nil)
   222  	tw := tar.NewWriter(tarBuf)
   223  	fooHdr := tar.Header{
   224  		Name:     "foo.txt",
   225  		Mode:     0666,
   226  		Size:     1,
   227  		ModTime:  time.Now(),
   228  		Typeflag: tar.TypeReg,
   229  	}
   230  	err = tw.WriteHeader(&fooHdr)
   231  	if err != nil {
   232  		t.Fatalf("error writing file header: %v", err)
   233  	}
   234  	if _, err := tw.Write([]byte{'a'}); err != nil {
   235  		t.Fatalf("error writing file: %v", err)
   236  	}
   237  
   238  	// Now write the error file
   239  	contents := []byte("SENTINEL ERROR")
   240  	err = tw.WriteHeader(&tar.Header{
   241  		Name:       allocdir.SnapshotErrorFilename(prevAlloc.prevAllocID),
   242  		Mode:       0666,
   243  		Size:       int64(len(contents)),
   244  		AccessTime: allocdir.SnapshotErrorTime,
   245  		ChangeTime: allocdir.SnapshotErrorTime,
   246  		ModTime:    allocdir.SnapshotErrorTime,
   247  		Typeflag:   tar.TypeReg,
   248  	})
   249  	if err != nil {
   250  		t.Fatalf("error writing sentinel file header: %v", err)
   251  	}
   252  	if _, err := tw.Write(contents); err != nil {
   253  		t.Fatalf("error writing sentinel file: %v", err)
   254  	}
   255  
   256  	// Assert streamAllocDir fails
   257  	err = prevAlloc.streamAllocDir(context.Background(), ioutil.NopCloser(tarBuf), dest)
   258  	if err == nil {
   259  		t.Fatalf("expected an error from streamAllocDir")
   260  	}
   261  	if !strings.HasSuffix(err.Error(), string(contents)) {
   262  		t.Fatalf("expected error to end with %q but found: %v", string(contents), err)
   263  	}
   264  
   265  	// streamAllocDir leaves cleanup to the caller on error, so assert
   266  	// "foo.txt" was written
   267  	fi, err := os.Stat(filepath.Join(dest, "foo.txt"))
   268  	if err != nil {
   269  		t.Fatalf("error reading foo.txt: %v", err)
   270  	}
   271  	if fi.Size() != fooHdr.Size {
   272  		t.Fatalf("expected foo.txt to be size 1 but found %d", fi.Size())
   273  	}
   274  }