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  }