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 }