diff --git a/Makefile b/Makefile index e25fc30cc..620bfba05 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ BINARY_NAME := nginx-agent PROJECT_DIR = cmd/agent PROJECT_FILE = main.go COLLECTOR_PATH ?= /etc/nginx-agent/opentelemetry-collector-agent.yaml -MANIFEST_DIR ?= /var/lib/nginx-agent +LIB_DIR ?= /var/lib/nginx-agent DIRS = $(BUILD_DIR) $(TEST_BUILD_DIR) $(BUILD_DIR)/$(DOCS_DIR) $(BUILD_DIR)/$(DOCS_DIR)/$(PROTO_DIR) $(shell mkdir -p $(DIRS)) @@ -195,7 +195,7 @@ run: build ## Run code dev: ## Run agent executable @echo "🚀 Running App" - NGINX_AGENT_COLLECTOR_CONFIG_PATH=$(COLLECTOR_PATH) NGINX_AGENT_MANIFEST_DIR=$(MANIFEST_DIR) $(GORUN) -ldflags=$(DEBUG_LDFLAGS) $(PROJECT_DIR)/$(PROJECT_FILE) + NGINX_AGENT_COLLECTOR_CONFIG_PATH=$(COLLECTOR_PATH) NGINX_AGENT_LIB_DIR=$(LIB_DIR) $(GORUN) -ldflags=$(DEBUG_LDFLAGS) $(PROJECT_DIR)/$(PROJECT_FILE) race-condition-dev: ## Run agent executable with race condition detection @echo "🏎️ Running app with race condition detection enabled" diff --git a/internal/config/config.go b/internal/config/config.go index 3b05fa25a..cc72af59c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -121,7 +121,7 @@ func ResolveConfig() (*Config, error) { Watchers: resolveWatchers(), Features: viperInstance.GetStringSlice(FeaturesKey), Labels: resolveLabels(), - ManifestDir: viperInstance.GetString(ManifestDirPathKey), + LibDir: viperInstance.GetString(LibDirPathKey), } defaultCollector(collector, config) @@ -380,9 +380,9 @@ func registerFlags() { "If the default path doesn't exist, log messages are output to stdout/stderr.", ) fs.String( - ManifestDirPathKey, - DefManifestDir, - "Specifies the path to the directory containing the manifest files", + LibDirPathKey, + DefLibDir, + "Specifies the path to the nginx-agent lib directory", ) fs.StringSlice(AllowedDirectoriesKey, diff --git a/internal/config/defaults.go b/internal/config/defaults.go index cb456bfb0..615c7bc8b 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -110,7 +110,7 @@ const ( DefCollectorExtensionsHealthTLServerNameKey = "" // File defaults - DefManifestDir = "/var/lib/nginx-agent" + DefLibDir = "/var/lib/nginx-agent" ) func DefaultFeatures() []string { diff --git a/internal/config/flags.go b/internal/config/flags.go index b8e08dcd6..3e51eb52b 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -24,7 +24,7 @@ const ( InstanceWatcherMonitoringFrequencyKey = "watchers_instance_watcher_monitoring_frequency" InstanceHealthWatcherMonitoringFrequencyKey = "watchers_instance_health_watcher_monitoring_frequency" FileWatcherKey = "watchers_file_watcher" - ManifestDirPathKey = "manifest_dir" + LibDirPathKey = "lib_dir" ) var ( diff --git a/internal/config/types.go b/internal/config/types.go index a13d50180..e1831dfae 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -48,7 +48,7 @@ type ( Version string `yaml:"-"` Path string `yaml:"-"` UUID string `yaml:"-"` - ManifestDir string `yaml:"-"` + LibDir string `yaml:"-"` AllowedDirectories []string `yaml:"allowed_directories" mapstructure:"allowed_directories"` Features []string `yaml:"features" mapstructure:"features"` } diff --git a/internal/file/file_manager_service.go b/internal/file/file_manager_service.go index 3c7b51251..6492e8c47 100644 --- a/internal/file/file_manager_service.go +++ b/internal/file/file_manager_service.go @@ -13,6 +13,8 @@ import ( "fmt" "log/slog" "os" + "path" + "path/filepath" "sync" "google.golang.org/grpc" @@ -38,11 +40,11 @@ const ( type ( fileOperator interface { - Write(ctx context.Context, fileContent []byte, file *mpi.FileMeta) error - CreateFileDirectories(ctx context.Context, fileMeta *mpi.FileMeta, filePermission os.FileMode) error + Write(ctx context.Context, fileContent []byte, fileName, filePermissions string) error + CreateFileDirectories(ctx context.Context, fileName string) error WriteChunkedFile( ctx context.Context, - file *mpi.File, + fileName, filePermissions string, header *mpi.FileDataChunkHeader, stream grpc.ServerStreamingClient[mpi.FileDataChunk], ) error @@ -54,13 +56,14 @@ type ( ) (mpi.FileDataChunk_Content, error) WriteManifestFile(ctx context.Context, updatedFiles map[string]*model.ManifestFile, manifestDir, manifestPath string) (writeError error) + MoveFile(ctx context.Context, sourcePath, destPath string) error } fileServiceOperatorInterface interface { - File(ctx context.Context, file *mpi.File, fileActions map[string]*model.FileCache) error + File(ctx context.Context, file *mpi.File, tempFilePath, expectedHash string) error UpdateOverview(ctx context.Context, instanceID string, filesToUpdate []*mpi.File, configPath string, iteration int) error - ChunkedFile(ctx context.Context, file *mpi.File) error + ChunkedFile(ctx context.Context, file *mpi.File, tempFilePath, expectedHash string) error IsConnected() bool UpdateFile( ctx context.Context, @@ -68,6 +71,7 @@ type ( fileToUpdate *mpi.File, ) error SetIsConnected(isConnected bool) + MoveFilesFromTempDirectory(ctx context.Context, fileAction *model.FileCache, tempDir string) error UpdateClient(ctx context.Context, fileServiceClient mpi.FileServiceClient) } @@ -118,8 +122,8 @@ func NewFileManagerService(fileServiceClient mpi.FileServiceClient, agentConfig rollbackFileContents: make(map[string][]byte), currentFilesOnDisk: make(map[string]*mpi.File), previousManifestFiles: make(map[string]*model.ManifestFile), - manifestFilePath: agentConfig.ManifestDir + "/manifest.json", rollbackManifest: true, + manifestFilePath: agentConfig.LibDir + "/manifest.json", manifestLock: manifestLock, } } @@ -169,7 +173,12 @@ func (fms *FileManagerService) ConfigApply(ctx context.Context, fms.rollbackFileContents = fileContent fms.fileActions = diffFiles - fileErr := fms.executeFileActions(ctx) + tempDir, tempDirError := fms.createTempConfigDirectory(ctx) + if tempDirError != nil { + return model.Error, tempDirError + } + + fileErr := fms.executeFileActions(ctx, tempDir) if fileErr != nil { fms.rollbackManifest = false return model.RollbackRequired, fileErr @@ -208,15 +217,16 @@ func (fms *FileManagerService) Rollback(ctx context.Context, instanceID string) continue case model.Delete, model.Update: - content := fms.rollbackFileContents[fileAction.File.GetFileMeta().GetName()] - err := fms.fileOperator.Write(ctx, content, fileAction.File.GetFileMeta()) + fileMeta := fileAction.File.GetFileMeta() + content := fms.rollbackFileContents[fileMeta.GetName()] + err := fms.fileOperator.Write(ctx, content, fileMeta.GetName(), fileMeta.GetPermissions()) if err != nil { return err } // currentFilesOnDisk needs to be updated after rollback action is performed - fileAction.File.GetFileMeta().Hash = files.GenerateHash(content) - fms.currentFilesOnDisk[fileAction.File.GetFileMeta().GetName()] = fileAction.File + fileMeta.Hash = files.GenerateHash(content) + fms.currentFilesOnDisk[fileMeta.GetName()] = fileAction.File case model.Unchanged: fallthrough default: @@ -226,8 +236,9 @@ func (fms *FileManagerService) Rollback(ctx context.Context, instanceID string) if fms.rollbackManifest { slog.DebugContext(ctx, "Rolling back manifest file", "manifest_previous", fms.previousManifestFiles) - manifestFileErr := fms.fileOperator.WriteManifestFile(ctx, fms.previousManifestFiles, - fms.agentConfig.ManifestDir, fms.manifestFilePath) + manifestFileErr := fms.fileOperator.WriteManifestFile( + ctx, fms.previousManifestFiles, fms.agentConfig.LibDir, fms.manifestFilePath, + ) if manifestFileErr != nil { return manifestFileErr } @@ -443,7 +454,7 @@ func (fms *FileManagerService) UpdateManifestFile(ctx context.Context, updatedFiles = manifestFiles } - return fms.fileOperator.WriteManifestFile(ctx, updatedFiles, fms.agentConfig.ManifestDir, fms.manifestFilePath) + return fms.fileOperator.WriteManifestFile(ctx, updatedFiles, fms.agentConfig.LibDir, fms.manifestFilePath) } func (fms *FileManagerService) manifestFile() (map[string]*model.ManifestFile, map[string]*mpi.File, error) { @@ -474,37 +485,103 @@ func (fms *FileManagerService) manifestFile() (map[string]*model.ManifestFile, m return manifestFiles, fileMap, nil } -func (fms *FileManagerService) executeFileActions(ctx context.Context) error { +func (fms *FileManagerService) executeFileActions(ctx context.Context, tempDir string) (actionError error) { + // Download files to temporary location + downloadError := fms.downloadUpdatedFilesToTempLocation(ctx, tempDir) + if downloadError != nil { + return downloadError + } + + // Remove temp files if there is a failure moving or deleting files + actionError = fms.moveOrDeleteFiles(ctx, tempDir, actionError) + if actionError != nil { + fms.deleteTempFiles(ctx, tempDir) + } + + return actionError +} + +func (fms *FileManagerService) downloadUpdatedFilesToTempLocation( + ctx context.Context, tempDir string, +) (updateError error) { + for _, fileAction := range fms.fileActions { + if fileAction.Action == model.Add || fileAction.Action == model.Update { + tempFilePath := filepath.Join(tempDir, fileAction.File.GetFileMeta().GetName()) + + slog.DebugContext( + ctx, + "Downloading file to temp location", + "file", tempFilePath, + ) + + updateErr := fms.fileUpdate(ctx, fileAction.File, tempFilePath) + if updateErr != nil { + updateError = updateErr + break + } + } + } + + // Remove temp files if there is an error downloading any files + if updateError != nil { + fms.deleteTempFiles(ctx, tempDir) + } + + return updateError +} + +func (fms *FileManagerService) moveOrDeleteFiles(ctx context.Context, tempDir string, actionError error) error { +actionsLoop: for _, fileAction := range fms.fileActions { switch fileAction.Action { case model.Delete: - slog.DebugContext(ctx, "File action, deleting file", "file", fileAction.File.GetFileMeta().GetName()) + slog.DebugContext(ctx, "Deleting file", "file", fileAction.File.GetFileMeta().GetName()) if err := os.Remove(fileAction.File.GetFileMeta().GetName()); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("error deleting file: %s error: %w", + actionError = fmt.Errorf("error deleting file: %s error: %w", fileAction.File.GetFileMeta().GetName(), err) + + break actionsLoop } continue case model.Add, model.Update: - slog.DebugContext(ctx, "File action, add or update file", "file", fileAction.File.GetFileMeta().GetName()) - updateErr := fms.fileUpdate(ctx, fileAction.File) - if updateErr != nil { - return updateErr + err := fms.fileServiceOperator.MoveFilesFromTempDirectory(ctx, fileAction, tempDir) + if err != nil { + actionError = err + + break actionsLoop } case model.Unchanged: slog.DebugContext(ctx, "File unchanged") } } - return nil + return actionError } -func (fms *FileManagerService) fileUpdate(ctx context.Context, file *mpi.File) error { +func (fms *FileManagerService) deleteTempFiles(ctx context.Context, tempDir string) { + for _, fileAction := range fms.fileActions { + if fileAction.Action == model.Add || fileAction.Action == model.Update { + tempFile := path.Join(tempDir, fileAction.File.GetFileMeta().GetName()) + if err := os.Remove(tempFile); err != nil && !os.IsNotExist(err) { + slog.ErrorContext( + ctx, "Error deleting temp file", + "file", fileAction.File.GetFileMeta().GetName(), + "error", err, + ) + } + } + } +} + +func (fms *FileManagerService) fileUpdate(ctx context.Context, file *mpi.File, tempFilePath string) error { + expectedHash := fms.fileActions[file.GetFileMeta().GetName()].File.GetFileMeta().GetHash() + if file.GetFileMeta().GetSize() <= int64(fms.agentConfig.Client.Grpc.MaxFileSize) { - return fms.fileServiceOperator.File(ctx, file, fms.fileActions) + return fms.fileServiceOperator.File(ctx, file, tempFilePath, expectedHash) } - return fms.fileServiceOperator.ChunkedFile(ctx, file) + return fms.fileServiceOperator.ChunkedFile(ctx, file, tempFilePath, expectedHash) } func (fms *FileManagerService) checkAllowedDirectory(checkFiles []*mpi.File) error { @@ -566,6 +643,21 @@ func (fms *FileManagerService) convertToFile(manifestFile *model.ManifestFile) * } } +func (fms *FileManagerService) createTempConfigDirectory(ctx context.Context) (string, error) { + tempDir, tempDirError := os.MkdirTemp(fms.agentConfig.LibDir, "config") + if tempDirError != nil { + return "", fmt.Errorf("failed creating temp config directory: %w", tempDirError) + } + defer func(path string) { + err := os.RemoveAll(path) + if err != nil { + slog.ErrorContext(ctx, "error removing temp config directory", "path", path, "err", err) + } + }(tempDir) + + return tempDir, nil +} + // ConvertToMapOfFiles converts a list of files to a map of file caches (file and action) with the file name as the key func ConvertToMapOfFileCache(convertFiles []*mpi.File) map[string]*model.FileCache { filesMap := make(map[string]*model.FileCache) diff --git a/internal/file/file_manager_service_test.go b/internal/file/file_manager_service_test.go index 824730637..5a2644080 100644 --- a/internal/file/file_manager_service_test.go +++ b/internal/file/file_manager_service_test.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "os" + "path" "path/filepath" "sync" "testing" @@ -34,6 +35,7 @@ func TestFileManagerService_ConfigApply_Add(t *testing.T) { tempDir := t.TempDir() filePath := filepath.Join(tempDir, "nginx.conf") + fileContent := []byte("location /test {\n return 200 \"Test location\\n\";\n}") fileHash := files.GenerateHash(fileContent) defer helpers.RemoveFileWithErrorCheck(t, filePath) @@ -41,7 +43,7 @@ func TestFileManagerService_ConfigApply_Add(t *testing.T) { overview := protos.FileOverview(filePath, fileHash) manifestDirPath := tempDir - manifestFilePath := manifestDirPath + "/manifest.json" + manifestFilePath := filepath.Join(manifestDirPath, "manifest.json") helpers.CreateFileWithErrorCheck(t, manifestDirPath, "manifest.json") fakeFileServiceClient := &v1fakes.FakeFileServiceClient{} @@ -57,7 +59,7 @@ func TestFileManagerService_ConfigApply_Add(t *testing.T) { agentConfig.AllowedDirectories = []string{tempDir} fileManagerService := NewFileManagerService(fakeFileServiceClient, agentConfig, &sync.RWMutex{}) - fileManagerService.agentConfig.ManifestDir = manifestDirPath + fileManagerService.agentConfig.LibDir = manifestDirPath fileManagerService.manifestFilePath = manifestFilePath request := protos.CreateConfigApplyRequest(overview) @@ -99,13 +101,13 @@ func TestFileManagerService_ConfigApply_Add_LargeFile(t *testing.T) { } manifestDirPath := tempDir - manifestFilePath := manifestDirPath + "/manifest.json" + manifestFilePath := filepath.Join(manifestDirPath, "manifest.json") fakeFileServiceClient.GetFileStreamReturns(fakeServerStreamingClient, nil) agentConfig := types.AgentConfig() agentConfig.AllowedDirectories = []string{tempDir} fileManagerService := NewFileManagerService(fakeFileServiceClient, agentConfig, &sync.RWMutex{}) - fileManagerService.agentConfig.ManifestDir = manifestDirPath + fileManagerService.agentConfig.LibDir = manifestDirPath fileManagerService.manifestFilePath = manifestFilePath request := protos.CreateConfigApplyRequest(overview) @@ -165,7 +167,7 @@ func TestFileManagerService_ConfigApply_Update(t *testing.T) { agentConfig.AllowedDirectories = []string{tempDir} fileManagerService := NewFileManagerService(fakeFileServiceClient, agentConfig, &sync.RWMutex{}) - fileManagerService.agentConfig.ManifestDir = manifestDirPath + fileManagerService.agentConfig.LibDir = manifestDirPath fileManagerService.manifestFilePath = manifestFilePath err := fileManagerService.UpdateCurrentFilesOnDisk(ctx, filesOnDisk, false) require.NoError(t, err) @@ -215,7 +217,7 @@ func TestFileManagerService_ConfigApply_Delete(t *testing.T) { agentConfig.AllowedDirectories = []string{tempDir} fileManagerService := NewFileManagerService(fakeFileServiceClient, agentConfig, &sync.RWMutex{}) - fileManagerService.agentConfig.ManifestDir = manifestDirPath + fileManagerService.agentConfig.LibDir = manifestDirPath fileManagerService.manifestFilePath = manifestFilePath err := fileManagerService.UpdateCurrentFilesOnDisk(ctx, filesOnDisk, false) require.NoError(t, err) @@ -275,7 +277,7 @@ func TestFileManagerService_ConfigApply_Failed(t *testing.T) { agentConfig.AllowedDirectories = []string{tempDir} fileManagerService := NewFileManagerService(fakeFileServiceClient, agentConfig, &sync.RWMutex{}) - fileManagerService.agentConfig.ManifestDir = manifestDirPath + fileManagerService.agentConfig.LibDir = manifestDirPath fileManagerService.manifestFilePath = manifestFilePath request := protos.CreateConfigApplyRequest(overview) @@ -438,7 +440,7 @@ func TestFileManagerService_Rollback(t *testing.T) { fileManagerService := NewFileManagerService(fakeFileServiceClient, types.AgentConfig(), &sync.RWMutex{}) fileManagerService.rollbackFileContents = fileContentCache fileManagerService.fileActions = filesCache - fileManagerService.agentConfig.ManifestDir = manifestDirPath + fileManagerService.agentConfig.LibDir = manifestDirPath fileManagerService.manifestFilePath = manifestFilePath err := fileManagerService.Rollback(ctx, instanceID) @@ -618,7 +620,7 @@ func TestFileManagerService_DetermineFileActions(t *testing.T) { fakeFileServiceClient := &v1fakes.FakeFileServiceClient{} fileManagerService := NewFileManagerService(fakeFileServiceClient, types.AgentConfig(), &sync.RWMutex{}) - fileManagerService.agentConfig.ManifestDir = manifestDirPath + fileManagerService.agentConfig.LibDir = manifestDirPath fileManagerService.manifestFilePath = manifestFilePath require.NoError(tt, err) @@ -792,7 +794,7 @@ func TestFileManagerService_UpdateManifestFile(t *testing.T) { fakeFileServiceClient := &v1fakes.FakeFileServiceClient{} fileManagerService := NewFileManagerService(fakeFileServiceClient, types.AgentConfig(), &sync.RWMutex{}) - fileManagerService.agentConfig.ManifestDir = manifestDirPath + fileManagerService.agentConfig.LibDir = manifestDirPath fileManagerService.manifestFilePath = file.Name() manifestJSON, err := json.MarshalIndent(test.currentManifestFiles, "", " ") @@ -890,7 +892,7 @@ func TestFileManagerService_fileActions(t *testing.T) { fileManagerService.fileActions = filesCache - actionErr := fileManagerService.executeFileActions(ctx) + actionErr := fileManagerService.executeFileActions(ctx, os.TempDir()) require.NoError(t, actionErr) assert.FileExists(t, addFilePath) @@ -973,3 +975,59 @@ rQHX6DP4w6IwZY8JB8LS }) } } + +func TestFileManagerService_deleteTempFiles(t *testing.T) { + tempDir := t.TempDir() + tempFile := path.Join(tempDir, "/etc/nginx/nginx.conf") + + err := os.MkdirAll(path.Dir(tempFile), 0o755) + require.NoError(t, err) + + _, err = os.Create(tempFile) + require.NoError(t, err) + + fileManagerService := FileManagerService{ + fileActions: map[string]*model.FileCache{ + "/etc/nginx/nginx.conf": { + File: &mpi.File{ + FileMeta: &mpi.FileMeta{ + Name: "/etc/nginx/nginx.conf", + }, + }, + Action: model.Update, + }, + "/etc/nginx/test.conf": { + File: &mpi.File{ + FileMeta: &mpi.FileMeta{ + Name: "/etc/nginx/test.conf", + }, + }, + Action: model.Add, + }, + }, + } + + fileManagerService.deleteTempFiles(t.Context(), tempDir) + + assert.NoFileExists(t, tempFile) +} + +func TestFileManagerService_createTempConfigDirectory(t *testing.T) { + agentConfig := types.AgentConfig() + agentConfig.LibDir = t.TempDir() + + fileManagerService := FileManagerService{ + agentConfig: agentConfig, + } + + dir, err := fileManagerService.createTempConfigDirectory(t.Context()) + assert.NotEmpty(t, dir) + require.NoError(t, err) + + // Test for unknown directory path + agentConfig.LibDir = "/unknown/" + + dir, err = fileManagerService.createTempConfigDirectory(t.Context()) + assert.Empty(t, dir) + require.Error(t, err) +} diff --git a/internal/file/file_operator.go b/internal/file/file_operator.go index ef512cadb..d765efc7d 100644 --- a/internal/file/file_operator.go +++ b/internal/file/file_operator.go @@ -39,34 +39,33 @@ func NewFileOperator(manifestLock *sync.RWMutex) *FileOperator { } } -func (fo *FileOperator) Write(ctx context.Context, fileContent []byte, file *mpi.FileMeta) error { - filePermission := files.FileMode(file.GetPermissions()) - err := fo.CreateFileDirectories(ctx, file, filePermission) +func (fo *FileOperator) Write(ctx context.Context, fileContent []byte, fileName, filePermissions string) error { + filePermission := files.FileMode(filePermissions) + err := fo.CreateFileDirectories(ctx, fileName) if err != nil { return err } - writeErr := os.WriteFile(file.GetName(), fileContent, filePermission) + writeErr := os.WriteFile(fileName, fileContent, filePermission) if writeErr != nil { - return fmt.Errorf("error writing to file %s: %w", file.GetName(), writeErr) + return fmt.Errorf("error writing to file %s: %w", fileName, writeErr) } - slog.DebugContext(ctx, "Content written to file", "file_path", file.GetName()) + slog.DebugContext(ctx, "Content written to file", "file_path", fileName) return nil } func (fo *FileOperator) CreateFileDirectories( ctx context.Context, - fileMeta *mpi.FileMeta, - filePermission os.FileMode, + fileName string, ) error { - if _, err := os.Stat(fileMeta.GetName()); os.IsNotExist(err) { - parentDirectory := path.Dir(fileMeta.GetName()) + if _, err := os.Stat(fileName); os.IsNotExist(err) { + parentDirectory := path.Dir(fileName) slog.DebugContext( ctx, "File does not exist, creating parent directory", "directory_path", parentDirectory, ) - err = os.MkdirAll(parentDirectory, filePermission) + err = os.MkdirAll(parentDirectory, dirPerm) if err != nil { return fmt.Errorf("error creating directory %s: %w", parentDirectory, err) } @@ -77,23 +76,22 @@ func (fo *FileOperator) CreateFileDirectories( func (fo *FileOperator) WriteChunkedFile( ctx context.Context, - file *mpi.File, + fileName, filePermissions string, header *mpi.FileDataChunkHeader, stream grpc.ServerStreamingClient[mpi.FileDataChunk], ) error { - filePermissions := files.FileMode(file.GetFileMeta().GetPermissions()) - createFileDirectoriesError := fo.CreateFileDirectories(ctx, file.GetFileMeta(), filePermissions) + createFileDirectoriesError := fo.CreateFileDirectories(ctx, fileName) if createFileDirectoriesError != nil { return createFileDirectoriesError } - fileToWrite, createError := os.Create(file.GetFileMeta().GetName()) + fileToWrite, createError := os.Create(fileName) defer func() { closeError := fileToWrite.Close() if closeError != nil { slog.WarnContext( ctx, "Failed to close file", - "file", file.GetFileMeta().GetName(), + "file", fileName, "error", closeError, ) } @@ -102,7 +100,12 @@ func (fo *FileOperator) WriteChunkedFile( return createError } - slog.DebugContext(ctx, "Writing chunked file", "file", file.GetFileMeta().GetName()) + filePermission := files.FileMode(filePermissions) + if err := os.Chmod(fileName, filePermission); err != nil { + return fmt.Errorf("error setting permissions for %s file: %w", fileName, err) + } + + slog.DebugContext(ctx, "Writing chunked file", "file", fileName) for range header.GetChunks() { chunk, recvError := stream.Recv() if recvError != nil { @@ -111,7 +114,7 @@ func (fo *FileOperator) WriteChunkedFile( _, chunkWriteError := fileToWrite.Write(chunk.GetContent().GetData()) if chunkWriteError != nil { - return fmt.Errorf("error writing chunk to file %s: %w", file.GetFileMeta().GetName(), chunkWriteError) + return fmt.Errorf("error writing chunk to file %s: %w", fileName, chunkWriteError) } } @@ -149,8 +152,8 @@ func (fo *FileOperator) ReadChunk( return chunk, err } -func (fo *FileOperator) WriteManifestFile(ctx context.Context, updatedFiles map[string]*model.ManifestFile, manifestDir, - manifestPath string, +func (fo *FileOperator) WriteManifestFile( + ctx context.Context, updatedFiles map[string]*model.ManifestFile, manifestDir, manifestPath string, ) (writeError error) { slog.DebugContext(ctx, "Writing manifest file", "updated_files", updatedFiles) manifestJSON, err := json.MarshalIndent(updatedFiles, "", " ") @@ -183,3 +186,38 @@ func (fo *FileOperator) WriteManifestFile(ctx context.Context, updatedFiles map[ return writeError } + +func (fo *FileOperator) MoveFile(ctx context.Context, sourcePath, destPath string) error { + inputFile, err := os.Open(sourcePath) + if err != nil { + return err + } + + outputFile, err := os.Create(destPath) + if err != nil { + return err + } + defer closeFile(ctx, outputFile) + + _, err = io.Copy(outputFile, inputFile) + if err != nil { + closeFile(ctx, inputFile) + return err + } + + closeFile(ctx, inputFile) + + err = os.Remove(sourcePath) + if err != nil { + return err + } + + return nil +} + +func closeFile(ctx context.Context, file *os.File) { + err := file.Close() + if err != nil { + slog.ErrorContext(ctx, "Error closing file", "error", err, "file", file.Name()) + } +} diff --git a/internal/file/file_operator_test.go b/internal/file/file_operator_test.go index 78ea9a193..4a49fcdd1 100644 --- a/internal/file/file_operator_test.go +++ b/internal/file/file_operator_test.go @@ -8,6 +8,7 @@ package file import ( "context" "os" + "path" "path/filepath" "sync" "testing" @@ -15,6 +16,7 @@ import ( "github.com/nginx/agent/v3/pkg/files" "github.com/nginx/agent/v3/test/protos" + "github.com/nginx/agent/v3/internal/model" "github.com/nginx/agent/v3/test/helpers" "github.com/stretchr/testify/assert" @@ -33,7 +35,7 @@ func TestFileOperator_Write(t *testing.T) { fileMeta := protos.FileMeta(filePath, files.GenerateHash(fileContent)) - writeErr := fileOp.Write(ctx, fileContent, fileMeta) + writeErr := fileOp.Write(ctx, fileContent, fileMeta.GetName(), fileMeta.GetPermissions()) require.NoError(t, writeErr) assert.FileExists(t, filePath) @@ -41,3 +43,63 @@ func TestFileOperator_Write(t *testing.T) { require.NoError(t, readErr) assert.Equal(t, fileContent, data) } + +func TestFileOperator_WriteManifestFile_fileMissing(t *testing.T) { + tempDir := t.TempDir() + manifestPath := "/unknown/manifest.json" + + fileOperator := NewFileOperator(&sync.RWMutex{}) + err := fileOperator.WriteManifestFile(t.Context(), make(map[string]*model.ManifestFile), tempDir, manifestPath) + assert.Error(t, err) +} + +func TestFileOperator_MoveFile_fileExists(t *testing.T) { + tempDir := t.TempDir() + tempFile := path.Join(tempDir, "/etc/nginx/nginx.conf") + newFile := path.Join(tempDir, "/etc/nginx/new_test.conf") + + err := os.MkdirAll(path.Dir(tempFile), 0o755) + require.NoError(t, err) + + _, err = os.Create(tempFile) + require.NoError(t, err) + + fileOperator := NewFileOperator(&sync.RWMutex{}) + err = fileOperator.MoveFile(t.Context(), tempFile, newFile) + require.NoError(t, err) + + assert.NoFileExists(t, tempFile) + assert.FileExists(t, newFile) +} + +func TestFileOperator_MoveFile_sourceFileDoesNotExist(t *testing.T) { + tempDir := t.TempDir() + tempFile := path.Join(tempDir, "/etc/nginx/nginx.conf") + newFile := path.Join(tempDir, "/etc/nginx/new_test.conf") + + fileOperator := NewFileOperator(&sync.RWMutex{}) + err := fileOperator.MoveFile(t.Context(), tempFile, newFile) + require.Error(t, err) + + assert.NoFileExists(t, tempFile) + assert.NoFileExists(t, newFile) +} + +func TestFileOperator_MoveFile_destFileDoesNotExist(t *testing.T) { + tempDir := t.TempDir() + tempFile := path.Join(tempDir, "/etc/nginx/nginx.conf") + newFile := "/unknown/nginx/new_test.conf" + + err := os.MkdirAll(path.Dir(tempFile), 0o755) + require.NoError(t, err) + + _, err = os.Create(tempFile) + require.NoError(t, err) + + fileOperator := NewFileOperator(&sync.RWMutex{}) + err = fileOperator.MoveFile(t.Context(), tempFile, newFile) + require.Error(t, err) + + assert.FileExists(t, tempFile) + assert.NoFileExists(t, newFile) +} diff --git a/internal/file/file_service_operator.go b/internal/file/file_service_operator.go index 80d47ec0c..dd4b166fc 100644 --- a/internal/file/file_service_operator.go +++ b/internal/file/file_service_operator.go @@ -14,6 +14,7 @@ import ( "maps" "math" "os" + "path/filepath" "slices" "sync" "sync/atomic" @@ -31,8 +32,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) -// File service operator handles requests to the grpc file service - +// FileServiceOperator handles requests to the grpc file service type FileServiceOperator struct { fileServiceClient mpi.FileServiceClient agentConfig *config.Config @@ -69,8 +69,10 @@ func (fso *FileServiceOperator) IsConnected() bool { return fso.isConnected.Load() } -func (fso *FileServiceOperator) File(ctx context.Context, file *mpi.File, - fileActions map[string]*model.FileCache, +func (fso *FileServiceOperator) File( + ctx context.Context, + file *mpi.File, + tempFilePath, expectedHash string, ) error { slog.DebugContext(ctx, "Getting file", "file", file.GetFileMeta().GetName()) @@ -97,12 +99,16 @@ func (fso *FileServiceOperator) File(ctx context.Context, file *mpi.File, return fmt.Errorf("error getting file data for %s: %w", file.GetFileMeta(), getFileErr) } - if writeErr := fso.fileOperator.Write(ctx, getFileResp.GetContents().GetContents(), - file.GetFileMeta()); writeErr != nil { + if writeErr := fso.fileOperator.Write( + ctx, + getFileResp.GetContents().GetContents(), + tempFilePath, + file.GetFileMeta().GetPermissions(), + ); writeErr != nil { return writeErr } - return fso.validateFileHash(file.GetFileMeta().GetName(), fileActions) + return fso.validateFileHash(tempFilePath, expectedHash) } func (fso *FileServiceOperator) UpdateOverview( @@ -215,7 +221,9 @@ func (fso *FileServiceOperator) UpdateOverview( return err } -func (fso *FileServiceOperator) ChunkedFile(ctx context.Context, file *mpi.File) error { +func (fso *FileServiceOperator) ChunkedFile( + ctx context.Context, file *mpi.File, tempFilePath, expectedHash string, +) error { slog.DebugContext(ctx, "Getting chunked file", "file", file.GetFileMeta().GetName()) stream, err := fso.fileServiceClient.GetFileStream(ctx, &mpi.GetFileRequest{ @@ -240,12 +248,14 @@ func (fso *FileServiceOperator) ChunkedFile(ctx context.Context, file *mpi.File) header := headerChunk.GetHeader() - writeChunkedFileError := fso.fileOperator.WriteChunkedFile(ctx, file, header, stream) + writeChunkedFileError := fso.fileOperator.WriteChunkedFile( + ctx, tempFilePath, file.GetFileMeta().GetPermissions(), header, stream, + ) if writeChunkedFileError != nil { return writeChunkedFileError } - return nil + return fso.validateFileHash(tempFilePath, expectedHash) } func (fso *FileServiceOperator) UpdateFile( @@ -267,15 +277,47 @@ func (fso *FileServiceOperator) UpdateFile( return fso.sendUpdateFileStream(ctx, fileToUpdate, fso.agentConfig.Client.Grpc.FileChunkSize) } -func (fso *FileServiceOperator) validateFileHash(filePath string, fileActions map[string]*model.FileCache) error { +func (fso *FileServiceOperator) MoveFilesFromTempDirectory( + ctx context.Context, fileAction *model.FileCache, tempDir string, +) error { + fileName := fileAction.File.GetFileMeta().GetName() + slog.DebugContext(ctx, "Updating file", "file", fileName) + tempFilePath := filepath.Join(tempDir, fileName) + + // Create parent directories for the target file if they don't exist + if err := os.MkdirAll(filepath.Dir(fileName), dirPerm); err != nil { + return fmt.Errorf("failed to create directories for %s: %w", fileName, err) + } + + moveErr := fso.fileOperator.MoveFile(ctx, tempFilePath, fileName) + if moveErr != nil { + return fmt.Errorf("failed to move file: %w", moveErr) + } + + if removeError := os.Remove(tempFilePath); removeError != nil && !os.IsNotExist(removeError) { + slog.ErrorContext( + ctx, + "Error deleting temp file", + "file", fileName, + "error", removeError, + ) + } + + return fso.validateFileHash(fileName, fileAction.File.GetFileMeta().GetHash()) +} + +func (fso *FileServiceOperator) validateFileHash(filePath, expectedHash string) error { content, err := os.ReadFile(filePath) if err != nil { return err } fileHash := files.GenerateHash(content) - if fileHash != fileActions[filePath].File.GetFileMeta().GetHash() { - return fmt.Errorf("error writing file, file hash does not match for file %s", filePath) + if fileHash != expectedHash { + return fmt.Errorf( + "error writing file, file hash does not match for file %s, expected hash: %s actual hash: %s", + filePath, fileHash, expectedHash, + ) } return nil diff --git a/internal/file/filefakes/fake_file_operator.go b/internal/file/filefakes/fake_file_operator.go index 3b2b2ee6c..27e1a54da 100644 --- a/internal/file/filefakes/fake_file_operator.go +++ b/internal/file/filefakes/fake_file_operator.go @@ -4,7 +4,6 @@ package filefakes import ( "bufio" "context" - "io/fs" "sync" v1 "github.com/nginx/agent/v3/api/grpc/mpi/v1" @@ -13,12 +12,11 @@ import ( ) type FakeFileOperator struct { - CreateFileDirectoriesStub func(context.Context, *v1.FileMeta, fs.FileMode) error + CreateFileDirectoriesStub func(context.Context, string) error createFileDirectoriesMutex sync.RWMutex createFileDirectoriesArgsForCall []struct { arg1 context.Context - arg2 *v1.FileMeta - arg3 fs.FileMode + arg2 string } createFileDirectoriesReturns struct { result1 error @@ -26,6 +24,19 @@ type FakeFileOperator struct { createFileDirectoriesReturnsOnCall map[int]struct { result1 error } + MoveFileStub func(context.Context, string, string) error + moveFileMutex sync.RWMutex + moveFileArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + } + moveFileReturns struct { + result1 error + } + moveFileReturnsOnCall map[int]struct { + result1 error + } ReadChunkStub func(context.Context, uint32, *bufio.Reader, uint32) (v1.FileDataChunk_Content, error) readChunkMutex sync.RWMutex readChunkArgsForCall []struct { @@ -42,12 +53,13 @@ type FakeFileOperator struct { result1 v1.FileDataChunk_Content result2 error } - WriteStub func(context.Context, []byte, *v1.FileMeta) error + WriteStub func(context.Context, []byte, string, string) error writeMutex sync.RWMutex writeArgsForCall []struct { arg1 context.Context arg2 []byte - arg3 *v1.FileMeta + arg3 string + arg4 string } writeReturns struct { result1 error @@ -55,13 +67,14 @@ type FakeFileOperator struct { writeReturnsOnCall map[int]struct { result1 error } - WriteChunkedFileStub func(context.Context, *v1.File, *v1.FileDataChunkHeader, grpc.ServerStreamingClient[v1.FileDataChunk]) error + WriteChunkedFileStub func(context.Context, string, string, *v1.FileDataChunkHeader, grpc.ServerStreamingClient[v1.FileDataChunk]) error writeChunkedFileMutex sync.RWMutex writeChunkedFileArgsForCall []struct { arg1 context.Context - arg2 *v1.File - arg3 *v1.FileDataChunkHeader - arg4 grpc.ServerStreamingClient[v1.FileDataChunk] + arg2 string + arg3 string + arg4 *v1.FileDataChunkHeader + arg5 grpc.ServerStreamingClient[v1.FileDataChunk] } writeChunkedFileReturns struct { result1 error @@ -87,20 +100,19 @@ type FakeFileOperator struct { invocationsMutex sync.RWMutex } -func (fake *FakeFileOperator) CreateFileDirectories(arg1 context.Context, arg2 *v1.FileMeta, arg3 fs.FileMode) error { +func (fake *FakeFileOperator) CreateFileDirectories(arg1 context.Context, arg2 string) error { fake.createFileDirectoriesMutex.Lock() ret, specificReturn := fake.createFileDirectoriesReturnsOnCall[len(fake.createFileDirectoriesArgsForCall)] fake.createFileDirectoriesArgsForCall = append(fake.createFileDirectoriesArgsForCall, struct { arg1 context.Context - arg2 *v1.FileMeta - arg3 fs.FileMode - }{arg1, arg2, arg3}) + arg2 string + }{arg1, arg2}) stub := fake.CreateFileDirectoriesStub fakeReturns := fake.createFileDirectoriesReturns - fake.recordInvocation("CreateFileDirectories", []interface{}{arg1, arg2, arg3}) + fake.recordInvocation("CreateFileDirectories", []interface{}{arg1, arg2}) fake.createFileDirectoriesMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3) + return stub(arg1, arg2) } if specificReturn { return ret.result1 @@ -114,17 +126,17 @@ func (fake *FakeFileOperator) CreateFileDirectoriesCallCount() int { return len(fake.createFileDirectoriesArgsForCall) } -func (fake *FakeFileOperator) CreateFileDirectoriesCalls(stub func(context.Context, *v1.FileMeta, fs.FileMode) error) { +func (fake *FakeFileOperator) CreateFileDirectoriesCalls(stub func(context.Context, string) error) { fake.createFileDirectoriesMutex.Lock() defer fake.createFileDirectoriesMutex.Unlock() fake.CreateFileDirectoriesStub = stub } -func (fake *FakeFileOperator) CreateFileDirectoriesArgsForCall(i int) (context.Context, *v1.FileMeta, fs.FileMode) { +func (fake *FakeFileOperator) CreateFileDirectoriesArgsForCall(i int) (context.Context, string) { fake.createFileDirectoriesMutex.RLock() defer fake.createFileDirectoriesMutex.RUnlock() argsForCall := fake.createFileDirectoriesArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 + return argsForCall.arg1, argsForCall.arg2 } func (fake *FakeFileOperator) CreateFileDirectoriesReturns(result1 error) { @@ -150,6 +162,69 @@ func (fake *FakeFileOperator) CreateFileDirectoriesReturnsOnCall(i int, result1 }{result1} } +func (fake *FakeFileOperator) MoveFile(arg1 context.Context, arg2 string, arg3 string) error { + fake.moveFileMutex.Lock() + ret, specificReturn := fake.moveFileReturnsOnCall[len(fake.moveFileArgsForCall)] + fake.moveFileArgsForCall = append(fake.moveFileArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + }{arg1, arg2, arg3}) + stub := fake.MoveFileStub + fakeReturns := fake.moveFileReturns + fake.recordInvocation("MoveFile", []interface{}{arg1, arg2, arg3}) + fake.moveFileMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileOperator) MoveFileCallCount() int { + fake.moveFileMutex.RLock() + defer fake.moveFileMutex.RUnlock() + return len(fake.moveFileArgsForCall) +} + +func (fake *FakeFileOperator) MoveFileCalls(stub func(context.Context, string, string) error) { + fake.moveFileMutex.Lock() + defer fake.moveFileMutex.Unlock() + fake.MoveFileStub = stub +} + +func (fake *FakeFileOperator) MoveFileArgsForCall(i int) (context.Context, string, string) { + fake.moveFileMutex.RLock() + defer fake.moveFileMutex.RUnlock() + argsForCall := fake.moveFileArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 +} + +func (fake *FakeFileOperator) MoveFileReturns(result1 error) { + fake.moveFileMutex.Lock() + defer fake.moveFileMutex.Unlock() + fake.MoveFileStub = nil + fake.moveFileReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeFileOperator) MoveFileReturnsOnCall(i int, result1 error) { + fake.moveFileMutex.Lock() + defer fake.moveFileMutex.Unlock() + fake.MoveFileStub = nil + if fake.moveFileReturnsOnCall == nil { + fake.moveFileReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.moveFileReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeFileOperator) ReadChunk(arg1 context.Context, arg2 uint32, arg3 *bufio.Reader, arg4 uint32) (v1.FileDataChunk_Content, error) { fake.readChunkMutex.Lock() ret, specificReturn := fake.readChunkReturnsOnCall[len(fake.readChunkArgsForCall)] @@ -217,7 +292,7 @@ func (fake *FakeFileOperator) ReadChunkReturnsOnCall(i int, result1 v1.FileDataC }{result1, result2} } -func (fake *FakeFileOperator) Write(arg1 context.Context, arg2 []byte, arg3 *v1.FileMeta) error { +func (fake *FakeFileOperator) Write(arg1 context.Context, arg2 []byte, arg3 string, arg4 string) error { var arg2Copy []byte if arg2 != nil { arg2Copy = make([]byte, len(arg2)) @@ -228,14 +303,15 @@ func (fake *FakeFileOperator) Write(arg1 context.Context, arg2 []byte, arg3 *v1. fake.writeArgsForCall = append(fake.writeArgsForCall, struct { arg1 context.Context arg2 []byte - arg3 *v1.FileMeta - }{arg1, arg2Copy, arg3}) + arg3 string + arg4 string + }{arg1, arg2Copy, arg3, arg4}) stub := fake.WriteStub fakeReturns := fake.writeReturns - fake.recordInvocation("Write", []interface{}{arg1, arg2Copy, arg3}) + fake.recordInvocation("Write", []interface{}{arg1, arg2Copy, arg3, arg4}) fake.writeMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3) + return stub(arg1, arg2, arg3, arg4) } if specificReturn { return ret.result1 @@ -249,17 +325,17 @@ func (fake *FakeFileOperator) WriteCallCount() int { return len(fake.writeArgsForCall) } -func (fake *FakeFileOperator) WriteCalls(stub func(context.Context, []byte, *v1.FileMeta) error) { +func (fake *FakeFileOperator) WriteCalls(stub func(context.Context, []byte, string, string) error) { fake.writeMutex.Lock() defer fake.writeMutex.Unlock() fake.WriteStub = stub } -func (fake *FakeFileOperator) WriteArgsForCall(i int) (context.Context, []byte, *v1.FileMeta) { +func (fake *FakeFileOperator) WriteArgsForCall(i int) (context.Context, []byte, string, string) { fake.writeMutex.RLock() defer fake.writeMutex.RUnlock() argsForCall := fake.writeArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 } func (fake *FakeFileOperator) WriteReturns(result1 error) { @@ -285,21 +361,22 @@ func (fake *FakeFileOperator) WriteReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *FakeFileOperator) WriteChunkedFile(arg1 context.Context, arg2 *v1.File, arg3 *v1.FileDataChunkHeader, arg4 grpc.ServerStreamingClient[v1.FileDataChunk]) error { +func (fake *FakeFileOperator) WriteChunkedFile(arg1 context.Context, arg2 string, arg3 string, arg4 *v1.FileDataChunkHeader, arg5 grpc.ServerStreamingClient[v1.FileDataChunk]) error { fake.writeChunkedFileMutex.Lock() ret, specificReturn := fake.writeChunkedFileReturnsOnCall[len(fake.writeChunkedFileArgsForCall)] fake.writeChunkedFileArgsForCall = append(fake.writeChunkedFileArgsForCall, struct { arg1 context.Context - arg2 *v1.File - arg3 *v1.FileDataChunkHeader - arg4 grpc.ServerStreamingClient[v1.FileDataChunk] - }{arg1, arg2, arg3, arg4}) + arg2 string + arg3 string + arg4 *v1.FileDataChunkHeader + arg5 grpc.ServerStreamingClient[v1.FileDataChunk] + }{arg1, arg2, arg3, arg4, arg5}) stub := fake.WriteChunkedFileStub fakeReturns := fake.writeChunkedFileReturns - fake.recordInvocation("WriteChunkedFile", []interface{}{arg1, arg2, arg3, arg4}) + fake.recordInvocation("WriteChunkedFile", []interface{}{arg1, arg2, arg3, arg4, arg5}) fake.writeChunkedFileMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4) + return stub(arg1, arg2, arg3, arg4, arg5) } if specificReturn { return ret.result1 @@ -313,17 +390,17 @@ func (fake *FakeFileOperator) WriteChunkedFileCallCount() int { return len(fake.writeChunkedFileArgsForCall) } -func (fake *FakeFileOperator) WriteChunkedFileCalls(stub func(context.Context, *v1.File, *v1.FileDataChunkHeader, grpc.ServerStreamingClient[v1.FileDataChunk]) error) { +func (fake *FakeFileOperator) WriteChunkedFileCalls(stub func(context.Context, string, string, *v1.FileDataChunkHeader, grpc.ServerStreamingClient[v1.FileDataChunk]) error) { fake.writeChunkedFileMutex.Lock() defer fake.writeChunkedFileMutex.Unlock() fake.WriteChunkedFileStub = stub } -func (fake *FakeFileOperator) WriteChunkedFileArgsForCall(i int) (context.Context, *v1.File, *v1.FileDataChunkHeader, grpc.ServerStreamingClient[v1.FileDataChunk]) { +func (fake *FakeFileOperator) WriteChunkedFileArgsForCall(i int) (context.Context, string, string, *v1.FileDataChunkHeader, grpc.ServerStreamingClient[v1.FileDataChunk]) { fake.writeChunkedFileMutex.RLock() defer fake.writeChunkedFileMutex.RUnlock() argsForCall := fake.writeChunkedFileArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 } func (fake *FakeFileOperator) WriteChunkedFileReturns(result1 error) { @@ -418,6 +495,8 @@ func (fake *FakeFileOperator) Invocations() map[string][][]interface{} { defer fake.invocationsMutex.RUnlock() fake.createFileDirectoriesMutex.RLock() defer fake.createFileDirectoriesMutex.RUnlock() + fake.moveFileMutex.RLock() + defer fake.moveFileMutex.RUnlock() fake.readChunkMutex.RLock() defer fake.readChunkMutex.RUnlock() fake.writeMutex.RLock()