github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/docker/coordinator_test.go (about) 1 package docker 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "testing" 8 "time" 9 10 docker "github.com/fsouza/go-dockerclient" 11 "github.com/hashicorp/nomad/ci" 12 "github.com/hashicorp/nomad/helper/testlog" 13 "github.com/hashicorp/nomad/helper/uuid" 14 "github.com/hashicorp/nomad/testutil" 15 "github.com/stretchr/testify/require" 16 ) 17 18 type mockImageClient struct { 19 pulled map[string]int 20 idToName map[string]string 21 removed map[string]int 22 pullDelay time.Duration 23 lock sync.Mutex 24 } 25 26 func newMockImageClient(idToName map[string]string, pullDelay time.Duration) *mockImageClient { 27 return &mockImageClient{ 28 pulled: make(map[string]int), 29 removed: make(map[string]int), 30 idToName: idToName, 31 pullDelay: pullDelay, 32 } 33 } 34 35 func (m *mockImageClient) PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error { 36 time.Sleep(m.pullDelay) 37 m.lock.Lock() 38 defer m.lock.Unlock() 39 m.pulled[opts.Repository]++ 40 return nil 41 } 42 43 func (m *mockImageClient) InspectImage(id string) (*docker.Image, error) { 44 m.lock.Lock() 45 defer m.lock.Unlock() 46 return &docker.Image{ 47 ID: m.idToName[id], 48 }, nil 49 } 50 51 func (m *mockImageClient) RemoveImage(id string) error { 52 m.lock.Lock() 53 defer m.lock.Unlock() 54 m.removed[id]++ 55 return nil 56 } 57 58 func TestDockerCoordinator_ConcurrentPulls(t *testing.T) { 59 ci.Parallel(t) 60 image := "foo" 61 imageID := uuid.Generate() 62 mapping := map[string]string{imageID: image} 63 64 // Add a delay so we can get multiple queued up 65 mock := newMockImageClient(mapping, 10*time.Millisecond) 66 config := &dockerCoordinatorConfig{ 67 ctx: context.Background(), 68 logger: testlog.HCLogger(t), 69 cleanup: true, 70 client: mock, 71 removeDelay: 100 * time.Millisecond, 72 } 73 74 // Create a coordinator 75 coordinator := newDockerCoordinator(config) 76 77 id, _ := coordinator.PullImage(image, nil, uuid.Generate(), nil, 5*time.Minute, 2*time.Minute) 78 for i := 0; i < 9; i++ { 79 go func() { 80 coordinator.PullImage(image, nil, uuid.Generate(), nil, 5*time.Minute, 2*time.Minute) 81 }() 82 } 83 84 testutil.WaitForResult(func() (bool, error) { 85 mock.lock.Lock() 86 defer mock.lock.Unlock() 87 p := mock.pulled[image] 88 if p >= 10 { 89 return false, fmt.Errorf("Wrong number of pulls: %d", p) 90 } 91 92 coordinator.imageLock.Lock() 93 defer coordinator.imageLock.Unlock() 94 // Check the reference count 95 if references := coordinator.imageRefCount[id]; len(references) != 10 { 96 return false, fmt.Errorf("Got reference count %d; want %d", len(references), 10) 97 } 98 99 // Ensure there is no pull future 100 if len(coordinator.pullFutures) != 0 { 101 return false, fmt.Errorf("Pull future exists after pull finished") 102 } 103 104 return true, nil 105 }, func(err error) { 106 t.Fatalf("err: %v", err) 107 }) 108 } 109 110 func TestDockerCoordinator_Pull_Remove(t *testing.T) { 111 ci.Parallel(t) 112 image := "foo" 113 imageID := uuid.Generate() 114 mapping := map[string]string{imageID: image} 115 116 // Add a delay so we can get multiple queued up 117 mock := newMockImageClient(mapping, 10*time.Millisecond) 118 config := &dockerCoordinatorConfig{ 119 ctx: context.Background(), 120 logger: testlog.HCLogger(t), 121 cleanup: true, 122 client: mock, 123 removeDelay: 1 * time.Millisecond, 124 } 125 126 // Create a coordinator 127 coordinator := newDockerCoordinator(config) 128 129 id := "" 130 callerIDs := make([]string, 10, 10) 131 for i := 0; i < 10; i++ { 132 callerIDs[i] = uuid.Generate() 133 id, _ = coordinator.PullImage(image, nil, callerIDs[i], nil, 5*time.Minute, 2*time.Minute) 134 } 135 136 // Check the reference count 137 if references := coordinator.imageRefCount[id]; len(references) != 10 { 138 t.Fatalf("Got reference count %d; want %d", len(references), 10) 139 } 140 141 // Remove some 142 for i := 0; i < 8; i++ { 143 coordinator.RemoveImage(id, callerIDs[i]) 144 } 145 146 // Check the reference count 147 if references := coordinator.imageRefCount[id]; len(references) != 2 { 148 t.Fatalf("Got reference count %d; want %d", len(references), 2) 149 } 150 151 // Remove all 152 for i := 8; i < 10; i++ { 153 coordinator.RemoveImage(id, callerIDs[i]) 154 } 155 156 // Check the reference count 157 if references := coordinator.imageRefCount[id]; len(references) != 0 { 158 t.Fatalf("Got reference count %d; want %d", len(references), 0) 159 } 160 161 // Check that only one delete happened 162 testutil.WaitForResult(func() (bool, error) { 163 mock.lock.Lock() 164 defer mock.lock.Unlock() 165 removes := mock.removed[id] 166 return removes == 1, fmt.Errorf("Wrong number of removes: %d", removes) 167 }, func(err error) { 168 t.Fatalf("err: %v", err) 169 }) 170 171 // Make sure there is no future still 172 testutil.WaitForResult(func() (bool, error) { 173 coordinator.imageLock.Lock() 174 defer coordinator.imageLock.Unlock() 175 _, ok := coordinator.deleteFuture[id] 176 return !ok, fmt.Errorf("got delete future") 177 }, func(err error) { 178 t.Fatalf("err: %v", err) 179 }) 180 181 } 182 183 func TestDockerCoordinator_Remove_Cancel(t *testing.T) { 184 ci.Parallel(t) 185 image := "foo" 186 imageID := uuid.Generate() 187 mapping := map[string]string{imageID: image} 188 189 mock := newMockImageClient(mapping, 1*time.Millisecond) 190 config := &dockerCoordinatorConfig{ 191 ctx: context.Background(), 192 logger: testlog.HCLogger(t), 193 cleanup: true, 194 client: mock, 195 removeDelay: 100 * time.Millisecond, 196 } 197 198 // Create a coordinator 199 coordinator := newDockerCoordinator(config) 200 callerID := uuid.Generate() 201 202 // Pull image 203 id, _ := coordinator.PullImage(image, nil, callerID, nil, 5*time.Minute, 2*time.Minute) 204 205 // Check the reference count 206 if references := coordinator.imageRefCount[id]; len(references) != 1 { 207 t.Fatalf("Got reference count %d; want %d", len(references), 1) 208 } 209 210 // Remove image 211 coordinator.RemoveImage(id, callerID) 212 213 // Check the reference count 214 if references := coordinator.imageRefCount[id]; len(references) != 0 { 215 t.Fatalf("Got reference count %d; want %d", len(references), 0) 216 } 217 218 // Pull image again within delay 219 id, _ = coordinator.PullImage(image, nil, callerID, nil, 5*time.Minute, 2*time.Minute) 220 221 // Check the reference count 222 if references := coordinator.imageRefCount[id]; len(references) != 1 { 223 t.Fatalf("Got reference count %d; want %d", len(references), 1) 224 } 225 226 // Check that only no delete happened 227 if removes := mock.removed[id]; removes != 0 { 228 t.Fatalf("Image deleted when it shouldn't have") 229 } 230 } 231 232 func TestDockerCoordinator_No_Cleanup(t *testing.T) { 233 ci.Parallel(t) 234 image := "foo" 235 imageID := uuid.Generate() 236 mapping := map[string]string{imageID: image} 237 238 mock := newMockImageClient(mapping, 1*time.Millisecond) 239 config := &dockerCoordinatorConfig{ 240 ctx: context.Background(), 241 logger: testlog.HCLogger(t), 242 cleanup: false, 243 client: mock, 244 removeDelay: 1 * time.Millisecond, 245 } 246 247 // Create a coordinator 248 coordinator := newDockerCoordinator(config) 249 callerID := uuid.Generate() 250 251 // Pull image 252 id, _ := coordinator.PullImage(image, nil, callerID, nil, 5*time.Minute, 2*time.Minute) 253 254 // Check the reference count 255 if references := coordinator.imageRefCount[id]; len(references) != 0 { 256 t.Fatalf("Got reference count %d; want %d", len(references), 0) 257 } 258 259 // Remove image 260 coordinator.RemoveImage(id, callerID) 261 262 // Check that only no delete happened 263 if removes := mock.removed[id]; removes != 0 { 264 t.Fatalf("Image deleted when it shouldn't have") 265 } 266 } 267 268 func TestDockerCoordinator_Cleanup_HonorsCtx(t *testing.T) { 269 ci.Parallel(t) 270 image1ID := uuid.Generate() 271 image2ID := uuid.Generate() 272 273 mapping := map[string]string{image1ID: "foo", image2ID: "bar"} 274 275 ctx, cancel := context.WithCancel(context.Background()) 276 defer cancel() 277 278 mock := newMockImageClient(mapping, 1*time.Millisecond) 279 config := &dockerCoordinatorConfig{ 280 ctx: ctx, 281 logger: testlog.HCLogger(t), 282 cleanup: true, 283 client: mock, 284 removeDelay: 1 * time.Millisecond, 285 } 286 287 // Create a coordinator 288 coordinator := newDockerCoordinator(config) 289 callerID := uuid.Generate() 290 291 // Pull image 292 id1, _ := coordinator.PullImage(image1ID, nil, callerID, nil, 5*time.Minute, 2*time.Minute) 293 require.Len(t, coordinator.imageRefCount[id1], 1, "image reference count") 294 295 id2, _ := coordinator.PullImage(image2ID, nil, callerID, nil, 5*time.Minute, 2*time.Minute) 296 require.Len(t, coordinator.imageRefCount[id2], 1, "image reference count") 297 298 // remove one image, cancel ctx, remove second, and assert only first image is cleanedup 299 // Remove image 300 coordinator.RemoveImage(id1, callerID) 301 testutil.WaitForResult(func() (bool, error) { 302 if _, ok := mock.removed[id1]; ok { 303 return true, nil 304 } 305 return false, fmt.Errorf("expected image to delete found %v", mock.removed) 306 }, func(err error) { 307 require.NoError(t, err) 308 }) 309 310 cancel() 311 coordinator.RemoveImage(id2, callerID) 312 313 // deletions occur in background, wait to ensure that 314 // the image isn't deleted after a timeout 315 time.Sleep(10 * time.Millisecond) 316 317 // Check that only no delete happened 318 require.Equal(t, map[string]int{id1: 1}, mock.removed, "removed images") 319 }