github.com/bigcommerce/nomad@v0.9.3-bc/drivers/docker/coordinator_test.go (about) 1 package docker 2 3 import ( 4 "fmt" 5 "sync" 6 "testing" 7 "time" 8 9 docker "github.com/fsouza/go-dockerclient" 10 "github.com/hashicorp/nomad/helper/testlog" 11 "github.com/hashicorp/nomad/helper/uuid" 12 "github.com/hashicorp/nomad/testutil" 13 ) 14 15 type mockImageClient struct { 16 pulled map[string]int 17 idToName map[string]string 18 removed map[string]int 19 pullDelay time.Duration 20 lock sync.Mutex 21 } 22 23 func newMockImageClient(idToName map[string]string, pullDelay time.Duration) *mockImageClient { 24 return &mockImageClient{ 25 pulled: make(map[string]int), 26 removed: make(map[string]int), 27 idToName: idToName, 28 pullDelay: pullDelay, 29 } 30 } 31 32 func (m *mockImageClient) PullImage(opts docker.PullImageOptions, auth docker.AuthConfiguration) error { 33 time.Sleep(m.pullDelay) 34 m.lock.Lock() 35 defer m.lock.Unlock() 36 m.pulled[opts.Repository]++ 37 return nil 38 } 39 40 func (m *mockImageClient) InspectImage(id string) (*docker.Image, error) { 41 m.lock.Lock() 42 defer m.lock.Unlock() 43 return &docker.Image{ 44 ID: m.idToName[id], 45 }, nil 46 } 47 48 func (m *mockImageClient) RemoveImage(id string) error { 49 m.lock.Lock() 50 defer m.lock.Unlock() 51 m.removed[id]++ 52 return nil 53 } 54 55 func TestDockerCoordinator_ConcurrentPulls(t *testing.T) { 56 t.Parallel() 57 image := "foo" 58 imageID := uuid.Generate() 59 mapping := map[string]string{imageID: image} 60 61 // Add a delay so we can get multiple queued up 62 mock := newMockImageClient(mapping, 10*time.Millisecond) 63 config := &dockerCoordinatorConfig{ 64 logger: testlog.HCLogger(t), 65 cleanup: true, 66 client: mock, 67 removeDelay: 100 * time.Millisecond, 68 } 69 70 // Create a coordinator 71 coordinator := newDockerCoordinator(config) 72 73 id, _ := coordinator.PullImage(image, nil, uuid.Generate(), nil) 74 for i := 0; i < 9; i++ { 75 go func() { 76 coordinator.PullImage(image, nil, uuid.Generate(), nil) 77 }() 78 } 79 80 testutil.WaitForResult(func() (bool, error) { 81 mock.lock.Lock() 82 defer mock.lock.Unlock() 83 p := mock.pulled[image] 84 if p >= 10 { 85 return false, fmt.Errorf("Wrong number of pulls: %d", p) 86 } 87 88 coordinator.imageLock.Lock() 89 defer coordinator.imageLock.Unlock() 90 // Check the reference count 91 if references := coordinator.imageRefCount[id]; len(references) != 10 { 92 return false, fmt.Errorf("Got reference count %d; want %d", len(references), 10) 93 } 94 95 // Ensure there is no pull future 96 if len(coordinator.pullFutures) != 0 { 97 return false, fmt.Errorf("Pull future exists after pull finished") 98 } 99 100 return true, nil 101 }, func(err error) { 102 t.Fatalf("err: %v", err) 103 }) 104 } 105 106 func TestDockerCoordinator_Pull_Remove(t *testing.T) { 107 t.Parallel() 108 image := "foo" 109 imageID := uuid.Generate() 110 mapping := map[string]string{imageID: image} 111 112 // Add a delay so we can get multiple queued up 113 mock := newMockImageClient(mapping, 10*time.Millisecond) 114 config := &dockerCoordinatorConfig{ 115 logger: testlog.HCLogger(t), 116 cleanup: true, 117 client: mock, 118 removeDelay: 1 * time.Millisecond, 119 } 120 121 // Create a coordinator 122 coordinator := newDockerCoordinator(config) 123 124 id := "" 125 callerIDs := make([]string, 10, 10) 126 for i := 0; i < 10; i++ { 127 callerIDs[i] = uuid.Generate() 128 id, _ = coordinator.PullImage(image, nil, callerIDs[i], nil) 129 } 130 131 // Check the reference count 132 if references := coordinator.imageRefCount[id]; len(references) != 10 { 133 t.Fatalf("Got reference count %d; want %d", len(references), 10) 134 } 135 136 // Remove some 137 for i := 0; i < 8; i++ { 138 coordinator.RemoveImage(id, callerIDs[i]) 139 } 140 141 // Check the reference count 142 if references := coordinator.imageRefCount[id]; len(references) != 2 { 143 t.Fatalf("Got reference count %d; want %d", len(references), 2) 144 } 145 146 // Remove all 147 for i := 8; i < 10; i++ { 148 coordinator.RemoveImage(id, callerIDs[i]) 149 } 150 151 // Check the reference count 152 if references := coordinator.imageRefCount[id]; len(references) != 0 { 153 t.Fatalf("Got reference count %d; want %d", len(references), 0) 154 } 155 156 // Check that only one delete happened 157 testutil.WaitForResult(func() (bool, error) { 158 mock.lock.Lock() 159 defer mock.lock.Unlock() 160 removes := mock.removed[id] 161 return removes == 1, fmt.Errorf("Wrong number of removes: %d", removes) 162 }, func(err error) { 163 t.Fatalf("err: %v", err) 164 }) 165 166 // Make sure there is no future still 167 coordinator.imageLock.Lock() 168 if _, ok := coordinator.deleteFuture[id]; ok { 169 t.Fatal("Got delete future") 170 } 171 coordinator.imageLock.Unlock() 172 } 173 174 func TestDockerCoordinator_Remove_Cancel(t *testing.T) { 175 t.Parallel() 176 image := "foo" 177 imageID := uuid.Generate() 178 mapping := map[string]string{imageID: image} 179 180 mock := newMockImageClient(mapping, 1*time.Millisecond) 181 config := &dockerCoordinatorConfig{ 182 logger: testlog.HCLogger(t), 183 cleanup: true, 184 client: mock, 185 removeDelay: 100 * time.Millisecond, 186 } 187 188 // Create a coordinator 189 coordinator := newDockerCoordinator(config) 190 callerID := uuid.Generate() 191 192 // Pull image 193 id, _ := coordinator.PullImage(image, nil, callerID, nil) 194 195 // Check the reference count 196 if references := coordinator.imageRefCount[id]; len(references) != 1 { 197 t.Fatalf("Got reference count %d; want %d", len(references), 1) 198 } 199 200 // Remove image 201 coordinator.RemoveImage(id, callerID) 202 203 // Check the reference count 204 if references := coordinator.imageRefCount[id]; len(references) != 0 { 205 t.Fatalf("Got reference count %d; want %d", len(references), 0) 206 } 207 208 // Pull image again within delay 209 id, _ = coordinator.PullImage(image, nil, callerID, nil) 210 211 // Check the reference count 212 if references := coordinator.imageRefCount[id]; len(references) != 1 { 213 t.Fatalf("Got reference count %d; want %d", len(references), 1) 214 } 215 216 // Check that only no delete happened 217 if removes := mock.removed[id]; removes != 0 { 218 t.Fatalf("Image deleted when it shouldn't have") 219 } 220 } 221 222 func TestDockerCoordinator_No_Cleanup(t *testing.T) { 223 t.Parallel() 224 image := "foo" 225 imageID := uuid.Generate() 226 mapping := map[string]string{imageID: image} 227 228 mock := newMockImageClient(mapping, 1*time.Millisecond) 229 config := &dockerCoordinatorConfig{ 230 logger: testlog.HCLogger(t), 231 cleanup: false, 232 client: mock, 233 removeDelay: 1 * time.Millisecond, 234 } 235 236 // Create a coordinator 237 coordinator := newDockerCoordinator(config) 238 callerID := uuid.Generate() 239 240 // Pull image 241 id, _ := coordinator.PullImage(image, nil, callerID, nil) 242 243 // Check the reference count 244 if references := coordinator.imageRefCount[id]; len(references) != 0 { 245 t.Fatalf("Got reference count %d; want %d", len(references), 0) 246 } 247 248 // Remove image 249 coordinator.RemoveImage(id, callerID) 250 251 // Check that only no delete happened 252 if removes := mock.removed[id]; removes != 0 { 253 t.Fatalf("Image deleted when it shouldn't have") 254 } 255 }