diff --git a/internal/file/file_operator.go b/internal/file/file_operator.go index b28b0807b..c886b9494 100644 --- a/internal/file/file_operator.go +++ b/internal/file/file_operator.go @@ -211,6 +211,15 @@ func (fo *FileOperator) WriteManifestFile( } func (fo *FileOperator) MoveFile(ctx context.Context, sourcePath, destPath string) error { + info, statErr := os.Stat(sourcePath) + if statErr != nil { + return fmt.Errorf("failed to stat source file %s: %w", sourcePath, statErr) + } + if !info.Mode().IsRegular() { + return fmt.Errorf("cannot backup non-regular file %s (type: %s) — resolve file manually on the data plane", + sourcePath, info.Mode().Type()) + } + inputFile, openErr := os.Open(sourcePath) if openErr != nil { return fmt.Errorf("failed to open source file %s: %w", sourcePath, openErr) diff --git a/internal/file/file_operator_test.go b/internal/file/file_operator_test.go index a16c9140f..d91a2fce6 100644 --- a/internal/file/file_operator_test.go +++ b/internal/file/file_operator_test.go @@ -12,6 +12,7 @@ import ( "path" "path/filepath" "sync" + "syscall" "testing" "github.com/nginx/agent/v3/pkg/files" @@ -179,3 +180,19 @@ func TestFileOperator_MoveFile_destFileDoesNotExist(t *testing.T) { assert.FileExists(t, tempFile) assert.NoFileExists(t, newFile) } + +func TestFileOperator_MoveFile_sourceIsNonRegularFile(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + fifoPath := filepath.Join(tempDir, "hang.conf") + + err := syscall.Mkfifo(fifoPath, 0o644) + require.NoError(t, err) + + fileOperator := NewFileOperator(&sync.RWMutex{}) + err = fileOperator.MoveFile(t.Context(), fifoPath, filepath.Join(tempDir, "hang.conf.bak")) + + require.Error(t, err) + assert.Contains(t, err.Error(), "non-regular file") +} diff --git a/internal/file/file_service_operator.go b/internal/file/file_service_operator.go index 1ae0ef988..7dd8103fc 100644 --- a/internal/file/file_service_operator.go +++ b/internal/file/file_service_operator.go @@ -308,16 +308,22 @@ func (fso *FileServiceOperator) UpdateFile( // renameFile, renames (moves) file from tempDir to new location to update file. func (fso *FileServiceOperator) RenameFile( - ctx context.Context, source, desination string, + ctx context.Context, source, destination string, ) error { - slog.InfoContext(ctx, fmt.Sprintf("Renaming file %s to %s", source, desination)) + slog.InfoContext(ctx, fmt.Sprintf("Renaming file %s to %s", source, destination)) + + if info, err := os.Stat(destination); err == nil && !info.Mode().IsRegular() { + return fmt.Errorf("cannot write to non-regular destination file %s (type: %s) - "+ + "resolve file manually on the data plane", + destination, info.Mode().Type()) + } // Create parent directories for the target file if they don't exist - if err := os.MkdirAll(filepath.Dir(desination), dirPerm); err != nil { - return fmt.Errorf("failed to create directories for %s: %w", desination, err) + if err := os.MkdirAll(filepath.Dir(destination), dirPerm); err != nil { + return fmt.Errorf("failed to create directories for %s: %w", destination, err) } - moveErr := os.Rename(source, desination) + moveErr := os.Rename(source, destination) if moveErr != nil { return fmt.Errorf("failed to rename file: %w", moveErr) } diff --git a/internal/file/file_service_operator_test.go b/internal/file/file_service_operator_test.go index e86cbb9ea..5e2601b4a 100644 --- a/internal/file/file_service_operator_test.go +++ b/internal/file/file_service_operator_test.go @@ -11,6 +11,7 @@ import ( "path/filepath" "sync" "sync/atomic" + "syscall" "testing" "time" @@ -195,3 +196,40 @@ func TestFileManagerService_UpdateFile_LargeFile(t *testing.T) { helpers.RemoveFileWithErrorCheck(t, testFile.Name()) } + +func TestFileServiceOperator_RenameFile_success(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + src := filepath.Join(tempDir, "source.conf") + dest := filepath.Join(tempDir, "dest.conf") + + err := os.WriteFile(src, []byte("nginx config"), 0o600) + require.NoError(t, err) + + fso := NewFileServiceOperator(types.AgentConfig(), nil, &sync.RWMutex{}) + err = fso.RenameFile(t.Context(), src, dest) + + require.NoError(t, err) + assert.FileExists(t, dest) +} + +func TestFileServiceOperator_RenameFile_destIsNonRegularFile(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + src := filepath.Join(tempDir, "source.conf") + dest := filepath.Join(tempDir, "dest.conf") + + err := os.WriteFile(src, []byte("nginx config"), 0o600) + require.NoError(t, err) + + err = syscall.Mkfifo(dest, 0o644) + require.NoError(t, err) + + fso := NewFileServiceOperator(types.AgentConfig(), nil, &sync.RWMutex{}) + err = fso.RenameFile(t.Context(), src, dest) + + require.Error(t, err) + assert.Contains(t, err.Error(), "non-regular") +} diff --git a/internal/watcher/file/file_watcher_service.go b/internal/watcher/file/file_watcher_service.go index 4355c3d29..c2ed718ad 100644 --- a/internal/watcher/file/file_watcher_service.go +++ b/internal/watcher/file/file_watcher_service.go @@ -205,6 +205,17 @@ func (fws *FileWatcherService) handleEvent(ctx context.Context, event fsnotify.E return } + if event.Has(fsnotify.Create) || event.Has(fsnotify.Rename) { + if info, err := os.Stat(event.Name); err == nil && !info.Mode().IsRegular() { + slog.ErrorContext(ctx, + "Managed config file changed to un-supported type — "+ + "config apply will fail until file is reverted to a supported type manually on the data plane", + "path", event.Name, + "type", info.Mode().Type(), + ) + } + } + slog.DebugContext(ctx, "Processing FSNotify event", "event", event) fws.filesChanged.Store(true) } diff --git a/internal/watcher/file/file_watcher_service_test.go b/internal/watcher/file/file_watcher_service_test.go index 5802018ec..d886d4141 100644 --- a/internal/watcher/file/file_watcher_service_test.go +++ b/internal/watcher/file/file_watcher_service_test.go @@ -11,6 +11,7 @@ import ( "os" "path" "path/filepath" + "syscall" "testing" "time" @@ -320,3 +321,22 @@ func TestFileWatcherService_checkForUpdates(t *testing.T) { t.Fatalf("Expected file update event") } } + +func TestFileWatcherService_handleEvent_nonRegularFile(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + fifoPath := filepath.Join(tempDir, "hang.conf") + + err := syscall.Mkfifo(fifoPath, 0o644) + require.NoError(t, err) + + fws := NewFileWatcherService(types.AgentConfig()) + + fws.handleEvent(t.Context(), fsnotify.Event{ + Name: fifoPath, + Op: fsnotify.Create, + }) + + assert.True(t, fws.filesChanged.Load()) +}