github.com/secure-build/gitlab-runner@v12.5.0+incompatible/executors/docker/internal/volumes/manager_test.go (about)

     1  package volumes
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/mock"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"gitlab.com/gitlab-org/gitlab-runner/executors/docker/internal/volumes/parser"
    13  	"gitlab.com/gitlab-org/gitlab-runner/helpers/path"
    14  )
    15  
    16  func newDebugLoggerMock() *mockDebugLogger {
    17  	loggerMock := new(mockDebugLogger)
    18  	loggerMock.On("Debugln", mock.Anything)
    19  
    20  	return loggerMock
    21  }
    22  
    23  func TestErrVolumeAlreadyDefined(t *testing.T) {
    24  	err := NewErrVolumeAlreadyDefined("test-path")
    25  	assert.EqualError(t, err, `volume for container path "test-path" is already defined`)
    26  }
    27  
    28  func TestNewDefaultManager(t *testing.T) {
    29  	logger := newDebugLoggerMock()
    30  
    31  	m := NewManager(logger, nil, nil, ManagerConfig{})
    32  	assert.IsType(t, &manager{}, m)
    33  }
    34  
    35  func newDefaultManager(config ManagerConfig) *manager {
    36  	m := &manager{
    37  		logger:         newDebugLoggerMock(),
    38  		config:         config,
    39  		managedVolumes: make(map[string]bool, 0),
    40  	}
    41  
    42  	return m
    43  }
    44  
    45  func addCacheContainerManager(manager *manager) *MockCacheContainersManager {
    46  	containerManager := new(MockCacheContainersManager)
    47  
    48  	manager.cacheContainersManager = containerManager
    49  
    50  	return containerManager
    51  }
    52  
    53  func addParser(manager *manager) *parser.MockParser {
    54  	parserMock := new(parser.MockParser)
    55  	parserMock.On("Path").Return(path.NewUnixPath())
    56  
    57  	manager.parser = parserMock
    58  	return parserMock
    59  }
    60  
    61  func TestDefaultManager_CreateUserVolumes_HostVolume(t *testing.T) {
    62  	testCases := map[string]struct {
    63  		volume            string
    64  		parsedVolume      *parser.Volume
    65  		baseContainerPath string
    66  		expectedBinding   []string
    67  		expectedError     error
    68  	}{
    69  		"no volumes specified": {
    70  			volume:          "",
    71  			expectedBinding: []string{"/host:/duplicated"},
    72  		},
    73  		"volume with absolute path": {
    74  			volume:          "/host:/volume",
    75  			parsedVolume:    &parser.Volume{Source: "/host", Destination: "/volume"},
    76  			expectedBinding: []string{"/host:/duplicated", "/host:/volume"},
    77  		},
    78  		"volume with absolute path and with baseContainerPath specified": {
    79  			volume:            "/host:/volume",
    80  			parsedVolume:      &parser.Volume{Source: "/host", Destination: "/volume"},
    81  			baseContainerPath: "/builds",
    82  			expectedBinding:   []string{"/host:/duplicated", "/host:/volume"},
    83  		},
    84  		"volume without absolute path and without baseContainerPath specified": {
    85  			volume:          "/host:volume",
    86  			parsedVolume:    &parser.Volume{Source: "/host", Destination: "volume"},
    87  			expectedBinding: []string{"/host:/duplicated", "/host:volume"},
    88  		},
    89  		"volume without absolute path and with baseContainerPath specified": {
    90  			volume:            "/host:volume",
    91  			parsedVolume:      &parser.Volume{Source: "/host", Destination: "volume"},
    92  			baseContainerPath: "/builds/project",
    93  			expectedBinding:   []string{"/host:/duplicated", "/host:/builds/project/volume"},
    94  		},
    95  		"duplicated volume specification": {
    96  			volume:          "/host/new:/duplicated",
    97  			parsedVolume:    &parser.Volume{Source: "/host/new", Destination: "/duplicated"},
    98  			expectedBinding: []string{"/host:/duplicated"},
    99  			expectedError:   NewErrVolumeAlreadyDefined("/duplicated"),
   100  		},
   101  		"volume with mode specified": {
   102  			volume:          "/host/new:/my/path:ro",
   103  			parsedVolume:    &parser.Volume{Source: "/host/new", Destination: "/my/path", Mode: "ro"},
   104  			expectedBinding: []string{"/host:/duplicated", "/host/new:/my/path:ro"},
   105  		},
   106  		"root volume specified": {
   107  			volume:          "/host/new:/:ro",
   108  			parsedVolume:    &parser.Volume{Source: "/host/new", Destination: "/", Mode: "ro"},
   109  			expectedBinding: []string{"/host:/duplicated"},
   110  			expectedError:   errDirectoryIsRootPath,
   111  		},
   112  	}
   113  
   114  	for testName, testCase := range testCases {
   115  		t.Run(testName, func(t *testing.T) {
   116  			config := ManagerConfig{
   117  				BaseContainerPath: testCase.baseContainerPath,
   118  			}
   119  
   120  			m := newDefaultManager(config)
   121  
   122  			volumeParser := addParser(m)
   123  			defer volumeParser.AssertExpectations(t)
   124  
   125  			volumeParser.On("ParseVolume", "/host:/duplicated").
   126  				Return(&parser.Volume{Source: "/host", Destination: "/duplicated"}, nil).
   127  				Once()
   128  
   129  			err := m.Create("/host:/duplicated")
   130  			require.NoError(t, err)
   131  
   132  			if len(testCase.volume) > 0 {
   133  				volumeParser.On("ParseVolume", testCase.volume).
   134  					Return(testCase.parsedVolume, nil).
   135  					Once()
   136  			}
   137  
   138  			err = m.Create(testCase.volume)
   139  			assert.Equal(t, testCase.expectedError, err)
   140  			assert.Equal(t, testCase.expectedBinding, m.volumeBindings)
   141  		})
   142  	}
   143  }
   144  
   145  func TestDefaultManager_CreateUserVolumes_CacheVolume_Disabled(t *testing.T) {
   146  	expectedBinding := []string{"/host:/duplicated"}
   147  
   148  	testCases := map[string]struct {
   149  		volume            string
   150  		parsedVolume      *parser.Volume
   151  		baseContainerPath string
   152  
   153  		expectedCacheContainerIDs []string
   154  		expectedConfigVolume      string
   155  		expectedError             error
   156  	}{
   157  		"no volumes specified": {
   158  			volume: "",
   159  		},
   160  		"volume with absolute path, without baseContainerPath and with disableCache": {
   161  			volume:            "/volume",
   162  			parsedVolume:      &parser.Volume{Destination: "/volume"},
   163  			baseContainerPath: "",
   164  			expectedError:     ErrCacheVolumesDisabled,
   165  		},
   166  		"volume with absolute path, with baseContainerPath and with disableCache": {
   167  			volume:            "/volume",
   168  			parsedVolume:      &parser.Volume{Destination: "/volume"},
   169  			baseContainerPath: "/builds/project",
   170  			expectedError:     ErrCacheVolumesDisabled,
   171  		},
   172  		"volume without absolute path, without baseContainerPath and with disableCache": {
   173  			volume:        "volume",
   174  			parsedVolume:  &parser.Volume{Destination: "volume"},
   175  			expectedError: ErrCacheVolumesDisabled,
   176  		},
   177  		"volume without absolute path, with baseContainerPath and with disableCache": {
   178  			volume:            "volume",
   179  			parsedVolume:      &parser.Volume{Destination: "volume"},
   180  			baseContainerPath: "/builds/project",
   181  			expectedError:     ErrCacheVolumesDisabled,
   182  		},
   183  		"duplicated volume definition": {
   184  			volume:            "/duplicated",
   185  			parsedVolume:      &parser.Volume{Destination: "/duplicated"},
   186  			baseContainerPath: "",
   187  			expectedError:     ErrCacheVolumesDisabled,
   188  		},
   189  	}
   190  
   191  	for testName, testCase := range testCases {
   192  		t.Run(testName, func(t *testing.T) {
   193  			config := ManagerConfig{
   194  				BaseContainerPath: testCase.baseContainerPath,
   195  				DisableCache:      true,
   196  			}
   197  
   198  			m := newDefaultManager(config)
   199  
   200  			volumeParser := addParser(m)
   201  			defer volumeParser.AssertExpectations(t)
   202  
   203  			volumeParser.On("ParseVolume", "/host:/duplicated").
   204  				Return(&parser.Volume{Source: "/host", Destination: "/duplicated"}, nil).
   205  				Once()
   206  
   207  			err := m.Create("/host:/duplicated")
   208  			require.NoError(t, err)
   209  
   210  			if len(testCase.volume) > 0 {
   211  				volumeParser.On("ParseVolume", testCase.volume).
   212  					Return(testCase.parsedVolume, nil).
   213  					Once()
   214  			}
   215  
   216  			err = m.Create(testCase.volume)
   217  			assert.Equal(t, testCase.expectedError, err)
   218  			assert.Equal(t, expectedBinding, m.volumeBindings)
   219  		})
   220  	}
   221  }
   222  
   223  func TestDefaultManager_CreateUserVolumes_CacheVolume_HostBased(t *testing.T) {
   224  	testCases := map[string]struct {
   225  		volume            string
   226  		baseContainerPath string
   227  		cacheDir          string
   228  		uniqueName        string
   229  
   230  		expectedBinding           []string
   231  		expectedCacheContainerIDs []string
   232  		expectedConfigVolume      string
   233  		expectedError             error
   234  	}{
   235  		"volume with absolute path, without baseContainerPath and with cacheDir": {
   236  			volume:          "/volume",
   237  			cacheDir:        "/cache",
   238  			uniqueName:      "uniq",
   239  			expectedBinding: []string{"/host:/duplicated", "/cache/uniq/14331bf18c8e434c4b3f48a8c5cc79aa:/volume"},
   240  		},
   241  		"volume with absolute path, with baseContainerPath and with cacheDir": {
   242  			volume:            "/volume",
   243  			baseContainerPath: "/builds/project",
   244  			cacheDir:          "/cache",
   245  			uniqueName:        "uniq",
   246  			expectedBinding:   []string{"/host:/duplicated", "/cache/uniq/14331bf18c8e434c4b3f48a8c5cc79aa:/volume"},
   247  		},
   248  		"volume without absolute path, without baseContainerPath and with cacheDir": {
   249  			volume:          "volume",
   250  			cacheDir:        "/cache",
   251  			uniqueName:      "uniq",
   252  			expectedBinding: []string{"/host:/duplicated", "/cache/uniq/210ab9e731c9c36c2c38db15c28a8d1c:volume"},
   253  		},
   254  		"volume without absolute path, with baseContainerPath and with cacheDir": {
   255  			volume:            "volume",
   256  			baseContainerPath: "/builds/project",
   257  			cacheDir:          "/cache",
   258  			uniqueName:        "uniq",
   259  			expectedBinding:   []string{"/host:/duplicated", "/cache/uniq/f69aef9fb01e88e6213362a04877452d:/builds/project/volume"},
   260  		},
   261  		"duplicated volume definition": {
   262  			volume:          "/duplicated",
   263  			cacheDir:        "/cache",
   264  			uniqueName:      "uniq",
   265  			expectedBinding: []string{"/host:/duplicated"},
   266  			expectedError:   NewErrVolumeAlreadyDefined("/duplicated"),
   267  		},
   268  	}
   269  
   270  	for testName, testCase := range testCases {
   271  		t.Run(testName, func(t *testing.T) {
   272  			config := ManagerConfig{
   273  				BaseContainerPath: testCase.baseContainerPath,
   274  				DisableCache:      false,
   275  				CacheDir:          testCase.cacheDir,
   276  				UniqueName:        testCase.uniqueName,
   277  			}
   278  
   279  			m := newDefaultManager(config)
   280  
   281  			volumeParser := addParser(m)
   282  			defer volumeParser.AssertExpectations(t)
   283  
   284  			volumeParser.On("ParseVolume", "/host:/duplicated").
   285  				Return(&parser.Volume{Source: "/host", Destination: "/duplicated"}, nil).
   286  				Once()
   287  
   288  			err := m.Create("/host:/duplicated")
   289  			require.NoError(t, err)
   290  
   291  			volumeParser.On("ParseVolume", testCase.volume).
   292  				Return(&parser.Volume{Destination: testCase.volume}, nil).
   293  				Once()
   294  
   295  			err = m.Create(testCase.volume)
   296  			assert.Equal(t, testCase.expectedError, err)
   297  			assert.Equal(t, testCase.expectedBinding, m.volumeBindings)
   298  		})
   299  	}
   300  }
   301  
   302  func TestDefaultManager_CreateUserVolumes_CacheVolume_ContainerBased(t *testing.T) {
   303  	testCases := map[string]struct {
   304  		volume                   string
   305  		baseContainerPath        string
   306  		uniqueName               string
   307  		expectedContainerName    string
   308  		expectedContainerPath    string
   309  		existingContainerID      string
   310  		newContainerID           string
   311  		expectedCacheContainerID string
   312  		expectedError            error
   313  	}{
   314  		"volume with absolute path, without baseContainerPath and with existing container": {
   315  			volume:                   "/volume",
   316  			baseContainerPath:        "",
   317  			uniqueName:               "uniq",
   318  			expectedContainerName:    "uniq-cache-14331bf18c8e434c4b3f48a8c5cc79aa",
   319  			expectedContainerPath:    "/volume",
   320  			existingContainerID:      "existingContainerID",
   321  			expectedCacheContainerID: "existingContainerID",
   322  		},
   323  		"volume with absolute path, without baseContainerPath and with new container": {
   324  			volume:                   "/volume",
   325  			baseContainerPath:        "",
   326  			uniqueName:               "uniq",
   327  			expectedContainerName:    "uniq-cache-14331bf18c8e434c4b3f48a8c5cc79aa",
   328  			expectedContainerPath:    "/volume",
   329  			existingContainerID:      "",
   330  			newContainerID:           "newContainerID",
   331  			expectedCacheContainerID: "newContainerID",
   332  		},
   333  		"volume without absolute path, without baseContainerPath and with existing container": {
   334  			volume:                   "volume",
   335  			baseContainerPath:        "",
   336  			uniqueName:               "uniq",
   337  			expectedContainerName:    "uniq-cache-210ab9e731c9c36c2c38db15c28a8d1c",
   338  			expectedContainerPath:    "volume",
   339  			existingContainerID:      "existingContainerID",
   340  			expectedCacheContainerID: "existingContainerID",
   341  		},
   342  		"volume without absolute path, without baseContainerPath and with new container": {
   343  			volume:                   "volume",
   344  			baseContainerPath:        "",
   345  			uniqueName:               "uniq",
   346  			expectedContainerName:    "uniq-cache-210ab9e731c9c36c2c38db15c28a8d1c",
   347  			expectedContainerPath:    "volume",
   348  			existingContainerID:      "",
   349  			newContainerID:           "newContainerID",
   350  			expectedCacheContainerID: "newContainerID",
   351  		},
   352  		"volume without absolute path, with baseContainerPath and with existing container": {
   353  			volume:                   "volume",
   354  			baseContainerPath:        "/builds/project",
   355  			uniqueName:               "uniq",
   356  			expectedContainerName:    "uniq-cache-f69aef9fb01e88e6213362a04877452d",
   357  			expectedContainerPath:    "/builds/project/volume",
   358  			existingContainerID:      "existingContainerID",
   359  			expectedCacheContainerID: "existingContainerID",
   360  		},
   361  		"volume without absolute path, with baseContainerPath and with new container": {
   362  			volume:                   "volume",
   363  			baseContainerPath:        "/builds/project",
   364  			uniqueName:               "uniq",
   365  			expectedContainerName:    "uniq-cache-f69aef9fb01e88e6213362a04877452d",
   366  			expectedContainerPath:    "/builds/project/volume",
   367  			existingContainerID:      "",
   368  			newContainerID:           "newContainerID",
   369  			expectedCacheContainerID: "newContainerID",
   370  		},
   371  		"duplicated volume definition": {
   372  			volume:        "/duplicated",
   373  			uniqueName:    "uniq",
   374  			expectedError: NewErrVolumeAlreadyDefined("/duplicated"),
   375  		},
   376  	}
   377  
   378  	for testName, testCase := range testCases {
   379  		t.Run(testName, func(t *testing.T) {
   380  			config := ManagerConfig{
   381  				BaseContainerPath: testCase.baseContainerPath,
   382  				UniqueName:        testCase.uniqueName,
   383  				DisableCache:      false,
   384  			}
   385  
   386  			m := newDefaultManager(config)
   387  			containerManager := addCacheContainerManager(m)
   388  			volumeParser := addParser(m)
   389  
   390  			defer func() {
   391  				containerManager.AssertExpectations(t)
   392  				volumeParser.AssertExpectations(t)
   393  			}()
   394  
   395  			volumeParser.On("ParseVolume", "/host:/duplicated").
   396  				Return(&parser.Volume{Source: "/host", Destination: "/duplicated"}, nil).
   397  				Once()
   398  
   399  			err := m.Create("/host:/duplicated")
   400  			require.NoError(t, err)
   401  
   402  			if testCase.volume != "/duplicated" {
   403  				containerManager.On("FindOrCleanExisting", testCase.expectedContainerName, testCase.expectedContainerPath).
   404  					Return(testCase.existingContainerID).
   405  					Once()
   406  
   407  				if testCase.newContainerID != "" {
   408  					containerManager.On("Create", testCase.expectedContainerName, testCase.expectedContainerPath).
   409  						Return(testCase.newContainerID, nil).
   410  						Once()
   411  				}
   412  			}
   413  
   414  			volumeParser.On("ParseVolume", testCase.volume).
   415  				Return(&parser.Volume{Destination: testCase.volume}, nil).
   416  				Once()
   417  
   418  			err = m.Create(testCase.volume)
   419  			assert.Equal(t, testCase.expectedError, err)
   420  
   421  			if testCase.expectedCacheContainerID != "" {
   422  				assert.Contains(t, m.cacheContainerIDs, testCase.expectedCacheContainerID)
   423  			}
   424  		})
   425  	}
   426  }
   427  
   428  func TestDefaultManager_CreateUserVolumes_CacheVolume_ContainerBased_WithError(t *testing.T) {
   429  	config := ManagerConfig{
   430  		BaseContainerPath: "/builds/project",
   431  		UniqueName:        "unique",
   432  	}
   433  
   434  	m := newDefaultManager(config)
   435  	containerManager := addCacheContainerManager(m)
   436  	volumeParser := addParser(m)
   437  
   438  	defer func() {
   439  		containerManager.AssertExpectations(t)
   440  		volumeParser.AssertExpectations(t)
   441  	}()
   442  
   443  	containerManager.On("FindOrCleanExisting", "unique-cache-f69aef9fb01e88e6213362a04877452d", "/builds/project/volume").
   444  		Return("").
   445  		Once()
   446  
   447  	containerManager.On("Create", "unique-cache-f69aef9fb01e88e6213362a04877452d", "/builds/project/volume").
   448  		Return("", errors.New("test error")).
   449  		Once()
   450  
   451  	volumeParser.On("ParseVolume", "volume").
   452  		Return(&parser.Volume{Destination: "volume"}, nil).
   453  		Once()
   454  
   455  	err := m.Create("volume")
   456  	assert.Error(t, err)
   457  }
   458  
   459  func TestDefaultManager_CreateUserVolumes_ParserError(t *testing.T) {
   460  	m := newDefaultManager(ManagerConfig{})
   461  
   462  	volumeParser := new(parser.MockParser)
   463  	defer volumeParser.AssertExpectations(t)
   464  	m.parser = volumeParser
   465  
   466  	volumeParser.On("ParseVolume", "volume").
   467  		Return(nil, errors.New("parser-test-error")).
   468  		Once()
   469  
   470  	err := m.Create("volume")
   471  	assert.Error(t, err)
   472  }
   473  
   474  func TestDefaultManager_CreateTemporary(t *testing.T) {
   475  	testCases := map[string]struct {
   476  		volume                   string
   477  		newContainerID           string
   478  		returnedParsedVolume     *parser.Volume
   479  		containerCreateError     error
   480  		expectedContainerName    string
   481  		expectedContainerPath    string
   482  		expectedCacheContainerID string
   483  		expectedTmpContainerID   string
   484  		expectedError            error
   485  	}{
   486  		"volume created": {
   487  			volume:                   "volume",
   488  			returnedParsedVolume:     &parser.Volume{Destination: "volume"},
   489  			newContainerID:           "newContainerID",
   490  			expectedContainerName:    "uniq-cache-f69aef9fb01e88e6213362a04877452d",
   491  			expectedContainerPath:    "/builds/project/volume",
   492  			expectedCacheContainerID: "newContainerID",
   493  			expectedTmpContainerID:   "newContainerID",
   494  		},
   495  		"cache container creation error": {
   496  			volume:               "volume",
   497  			returnedParsedVolume: &parser.Volume{Destination: "volume"},
   498  			newContainerID:       "",
   499  			containerCreateError: errors.New("test-error"),
   500  			expectedError:        errors.New("test-error"),
   501  		},
   502  		"duplicated volume definition": {
   503  			volume:        "/duplicated",
   504  			expectedError: NewErrVolumeAlreadyDefined("/duplicated"),
   505  		},
   506  	}
   507  
   508  	for testName, testCase := range testCases {
   509  		t.Run(testName, func(t *testing.T) {
   510  			config := ManagerConfig{
   511  				BaseContainerPath: "/builds/project",
   512  				UniqueName:        "unique",
   513  			}
   514  
   515  			m := newDefaultManager(config)
   516  			containerManager := addCacheContainerManager(m)
   517  			volumeParser := addParser(m)
   518  
   519  			defer func() {
   520  				containerManager.AssertExpectations(t)
   521  				volumeParser.AssertExpectations(t)
   522  			}()
   523  
   524  			volumeParser.On("ParseVolume", "/host:/duplicated").
   525  				Return(&parser.Volume{Source: "/host", Destination: "/duplicated"}, nil).
   526  				Once()
   527  
   528  			err := m.Create("/host:/duplicated")
   529  			require.NoError(t, err)
   530  
   531  			if testCase.volume != "/duplicated" {
   532  				containerManager.On("FindOrCleanExisting", "unique-cache-f69aef9fb01e88e6213362a04877452d", "/builds/project/volume").
   533  					Return("").
   534  					Once()
   535  
   536  				containerManager.On("Create", "unique-cache-f69aef9fb01e88e6213362a04877452d", "/builds/project/volume").
   537  					Return(testCase.newContainerID, testCase.containerCreateError).
   538  					Once()
   539  			}
   540  
   541  			err = m.CreateTemporary(testCase.volume)
   542  			assert.Equal(t, testCase.expectedError, err)
   543  
   544  			if testCase.expectedCacheContainerID != "" {
   545  				assert.Contains(t, m.cacheContainerIDs, testCase.expectedCacheContainerID)
   546  			}
   547  
   548  			if testCase.expectedTmpContainerID != "" {
   549  				assert.Contains(t, m.tmpContainerIDs, testCase.expectedTmpContainerID)
   550  			}
   551  		})
   552  	}
   553  }
   554  
   555  func TestDefaultManager_Binds(t *testing.T) {
   556  	expectedElements := []string{"element1", "element2"}
   557  	m := &manager{
   558  		volumeBindings: expectedElements,
   559  	}
   560  
   561  	assert.Equal(t, expectedElements, m.Binds())
   562  }
   563  
   564  func TestDefaultManager_ContainerIDs(t *testing.T) {
   565  	expectedElements := []string{"element1", "element2"}
   566  	m := &manager{
   567  		cacheContainerIDs: expectedElements,
   568  	}
   569  
   570  	assert.Equal(t, expectedElements, m.ContainerIDs())
   571  }
   572  
   573  func TestDefaultManager_Cleanup(t *testing.T) {
   574  	ccManager := new(MockCacheContainersManager)
   575  	defer ccManager.AssertExpectations(t)
   576  
   577  	doneCh := make(chan bool, 1)
   578  
   579  	ccManager.On("Cleanup", mock.Anything, []string{"container-1"}).
   580  		Run(func(_ mock.Arguments) {
   581  			close(doneCh)
   582  		}).
   583  		Return(doneCh).
   584  		Once()
   585  
   586  	m := &manager{
   587  		cacheContainersManager: ccManager,
   588  		tmpContainerIDs:        []string{"container-1"},
   589  	}
   590  
   591  	done := m.Cleanup(context.Background())
   592  	<-done
   593  }