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