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 }