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