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  }