diff --git a/go.mod b/go.mod index 082ddddaa..ce8033eb1 100644 --- a/go.mod +++ b/go.mod @@ -77,6 +77,7 @@ require ( go.opentelemetry.io/collector/scraper/scrapertest v0.135.0 go.opentelemetry.io/otel v1.38.0 go.uber.org/goleak v1.3.0 + go.uber.org/mock v0.6.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/mod v0.28.0 diff --git a/go.sum b/go.sum index 87b07f12e..b00efb901 100644 --- a/go.sum +++ b/go.sum @@ -953,6 +953,8 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= diff --git a/internal/config/config.go b/internal/config/config.go index cc72af59c..18f773f90 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -122,6 +122,7 @@ func ResolveConfig() (*Config, error) { Features: viperInstance.GetStringSlice(FeaturesKey), Labels: resolveLabels(), LibDir: viperInstance.GetString(LibDirPathKey), + ExternalDataSource: resolveExternalDataSource(), } defaultCollector(collector, config) @@ -426,6 +427,7 @@ func registerFlags() { registerCollectorFlags(fs) registerClientFlags(fs) registerDataPlaneFlags(fs) + registerExternalDataSourceFlags(fs) fs.SetNormalizeFunc(normalizeFunc) @@ -440,6 +442,30 @@ func registerFlags() { }) } +func registerExternalDataSourceFlags(fs *flag.FlagSet) { + fs.String( + ExternalDataSourceModeKey, + DefExternalDataSourceMode, + "Mode for external data source: 'direct' (HTTP/HTTPS) or 'helper'.", + ) + + fs.String( + ExternalDataSourceHelperPathKey, + DefExternalDataSourceHelperPath, + "Path to the helper executable for fetching external data sources.", + ) + fs.StringSlice( + ExternalDataSourceAllowDomainsKey, + []string{}, + "List of allowed domains for external data sources.", + ) + fs.Int64( + ExternalDataSourceMaxBytesKey, + DefExternalDataSourceMaxBytes, + "Maximum size in bytes for external data sources.", + ) +} + func registerDataPlaneFlags(fs *flag.FlagSet) { fs.Duration( NginxReloadMonitoringPeriodKey, @@ -1474,3 +1500,17 @@ func areCommandServerProxyTLSSettingsSet() bool { viperInstance.IsSet(CommandServerProxyTLSSkipVerifyKey) || viperInstance.IsSet(CommandServerProxyTLSServerNameKey) } + +func resolveExternalDataSource() *ExternalDataSource { + externalDataSource := &ExternalDataSource{ + Mode: viperInstance.GetString(ExternalDataSourceModeKey), + AllowedDomains: viperInstance.GetStringSlice(ExternalDataSourceAllowDomainsKey), + MaxBytes: viperInstance.GetInt64(ExternalDataSourceMaxBytesKey), + } + + externalDataSource.Helper = &HelperConfig{ + Path: viperInstance.GetString(ExternalDataSourceHelperPathKey), + } + + return externalDataSource +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index cce67b7ff..c4c14cf60 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1322,6 +1322,14 @@ func createConfig() *Config { config.FeatureCertificates, config.FeatureFileWatcher, config.FeatureMetrics, config.FeatureAPIAction, config.FeatureLogsNap, }, + ExternalDataSource: &ExternalDataSource{ + Mode: "", + AllowedDomains: nil, + Helper: &HelperConfig{ + Path: "", + }, + MaxBytes: 0, + }, } } diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 615c7bc8b..945f5b403 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -111,6 +111,10 @@ const ( // File defaults DefLibDir = "/var/lib/nginx-agent" + + DefExternalDataSourceMode = "" + DefExternalDataSourceHelperPath = "" + DefExternalDataSourceMaxBytes = 100 * 1024 * 1024 ) func DefaultFeatures() []string { diff --git a/internal/config/flags.go b/internal/config/flags.go index 3e51eb52b..1ec556c54 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -25,6 +25,7 @@ const ( InstanceHealthWatcherMonitoringFrequencyKey = "watchers_instance_health_watcher_monitoring_frequency" FileWatcherKey = "watchers_file_watcher" LibDirPathKey = "lib_dir" + ExternalDataSourceRootKey = "external_data_source" ) var ( @@ -137,6 +138,11 @@ var ( FileWatcherMonitoringFrequencyKey = pre(FileWatcherKey) + "monitoring_frequency" NginxExcludeFilesKey = pre(FileWatcherKey) + "exclude_files" + + ExternalDataSourceModeKey = pre(ExternalDataSourceRootKey) + "mode" + ExternalDataSourceHelperPathKey = pre(ExternalDataSourceRootKey) + "helper_path" + ExternalDataSourceMaxBytesKey = pre(ExternalDataSourceRootKey) + "max_bytes" + ExternalDataSourceAllowDomainsKey = pre(ExternalDataSourceRootKey) + "allowed_domains" ) func pre(prefixes ...string) string { diff --git a/internal/config/types.go b/internal/config/types.go index e1831dfae..a4c0838ff 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -37,20 +37,21 @@ func parseServerType(str string) (ServerType, bool) { type ( Config struct { - Command *Command `yaml:"command" mapstructure:"command"` - AuxiliaryCommand *Command `yaml:"auxiliary_command" mapstructure:"auxiliary_command"` - Log *Log `yaml:"log" mapstructure:"log"` - DataPlaneConfig *DataPlaneConfig `yaml:"data_plane_config" mapstructure:"data_plane_config"` - Client *Client `yaml:"client" mapstructure:"client"` - Collector *Collector `yaml:"collector" mapstructure:"collector"` - Watchers *Watchers `yaml:"watchers" mapstructure:"watchers"` - Labels map[string]any `yaml:"labels" mapstructure:"labels"` - Version string `yaml:"-"` - Path string `yaml:"-"` - UUID string `yaml:"-"` - LibDir string `yaml:"-"` - AllowedDirectories []string `yaml:"allowed_directories" mapstructure:"allowed_directories"` - Features []string `yaml:"features" mapstructure:"features"` + Command *Command `yaml:"command" mapstructure:"command"` + AuxiliaryCommand *Command `yaml:"auxiliary_command" mapstructure:"auxiliary_command"` + ExternalDataSource *ExternalDataSource `yaml:"external_data_source" mapstructure:"external_data_source"` + Log *Log `yaml:"log" mapstructure:"log"` + DataPlaneConfig *DataPlaneConfig `yaml:"data_plane_config" mapstructure:"data_plane_config"` + Client *Client `yaml:"client" mapstructure:"client"` + Collector *Collector `yaml:"collector" mapstructure:"collector"` + Watchers *Watchers `yaml:"watchers" mapstructure:"watchers"` + Labels map[string]any `yaml:"labels" mapstructure:"labels"` + Version string `yaml:"-"` + Path string `yaml:"-"` + UUID string `yaml:"-"` + LibDir string `yaml:"-"` + AllowedDirectories []string `yaml:"allowed_directories" mapstructure:"allowed_directories"` + Features []string `yaml:"features" mapstructure:"features"` } Log struct { @@ -350,6 +351,17 @@ type ( Token string `yaml:"token,omitempty" mapstructure:"token"` Timeout time.Duration `yaml:"timeout" mapstructure:"timeout"` } + + ExternalDataSource struct { + Helper *HelperConfig `yaml:"helper" mapstructure:"helper"` + Mode string `yaml:"mode" mapstructure:"mode"` + AllowedDomains []string `yaml:"allowed_domains" mapstructure:"allowed_domains"` + MaxBytes int64 `yaml:"max_bytes" mapstructure:"max_bytes"` + } + + HelperConfig struct { + Path string `yaml:"path" mapstructure:"path"` + } ) func (col *Collector) Validate(allowedDirectories []string) error { @@ -479,6 +491,24 @@ func (c *Config) IsCommandServerProxyConfigured() bool { return c.Command.Server.Proxy.URL != "" } +func (c *Config) IsDomainAllowed(hostname string) bool { + allowedDomains := c.ExternalDataSource.AllowedDomains + + for _, allowed := range allowedDomains { + // Handle wildcard domains like "*.vault.azure.com" + if strings.HasPrefix(allowed, "*.") { + suffix := strings.TrimPrefix(allowed, "*") + if strings.HasSuffix(hostname, suffix) { + return true + } + } else if hostname == allowed { + return true + } + } + + return false +} + // isAllowedDir checks if the given path is in the list of allowed directories. // It returns true if the path is allowed, false otherwise. // If the path is allowed but does not exist, it also logs a warning. diff --git a/internal/file/file_manager_service.go b/internal/file/file_manager_service.go index 6492e8c47..72cf295cc 100644 --- a/internal/file/file_manager_service.go +++ b/internal/file/file_manager_service.go @@ -12,9 +12,11 @@ import ( "errors" "fmt" "log/slog" + "net/url" "os" "path" "path/filepath" + "strconv" "sync" "google.golang.org/grpc" @@ -56,6 +58,12 @@ type ( ) (mpi.FileDataChunk_Content, error) WriteManifestFile(ctx context.Context, updatedFiles map[string]*model.ManifestFile, manifestDir, manifestPath string) (writeError error) + runHelper( + ctx context.Context, + helperPath string, + fileUrl string, + maxBytes int64, + ) (string, error) MoveFile(ctx context.Context, sourcePath, destPath string) error } @@ -156,6 +164,13 @@ func (fms *FileManagerService) ConfigApply(ctx context.Context, return model.Error, allowedErr } + isExternalFileReferenced := fms.isExternalFilePresent(fileOverview.GetFiles()) + if isExternalFileReferenced { + if errExternalFileErr := fms.processExternalFiles(ctx, fileOverview.GetFiles()); errExternalFileErr != nil { + return model.Error, errExternalFileErr + } + } + diffFiles, fileContent, compareErr := fms.DetermineFileActions( ctx, fms.currentFilesOnDisk, @@ -166,7 +181,7 @@ func (fms *FileManagerService) ConfigApply(ctx context.Context, return model.Error, compareErr } - if len(diffFiles) == 0 { + if len(diffFiles) == 0 && !isExternalFileReferenced { return model.NoChange, nil } @@ -595,6 +610,37 @@ func (fms *FileManagerService) checkAllowedDirectory(checkFiles []*mpi.File) err return nil } +func (fms *FileManagerService) processExternalFiles(ctx context.Context, fileList []*mpi.File) error { + if helperAllowedErr := fms.checkHelperDirectory(); helperAllowedErr != nil { + return helperAllowedErr + } + + return fms.downloadExternalFiles(ctx, fileList) +} + +func (fms *FileManagerService) isExternalFilePresent(checkFiles []*mpi.File) bool { + for _, file := range checkFiles { + if file.GetExternalDataSource() != nil && file.GetExternalDataSource().GetLocation() != "" { + slog.Debug("External file source is present in file overview", + "location", file.GetExternalDataSource().GetLocation()) + + return true + } + } + + return false +} + +func (fms *FileManagerService) checkHelperDirectory() error { + allowed := fms.agentConfig.IsDirectoryAllowed(fms.agentConfig.ExternalDataSource.Helper.Path) + if !allowed { + return fmt.Errorf("helper file is not present in allowed directories %s", + fms.agentConfig.ExternalDataSource.Helper.Path) + } + + return nil +} + func (fms *FileManagerService) convertToManifestFileMap( currentFiles map[string]*mpi.File, referenced bool, @@ -669,3 +715,84 @@ func ConvertToMapOfFileCache(convertFiles []*mpi.File) map[string]*model.FileCac return filesMap } + +func (fms *FileManagerService) downloadExternalFiles(ctx context.Context, files_list []*mpi.File) error { + downloadedFiles := make(map[string]string) + + for _, file := range files_list { + if file.GetExternalDataSource() == nil { + continue + } + location := file.GetExternalDataSource().GetLocation() + + if fms.agentConfig.ExternalDataSource.Mode != "helper" { + return fmt.Errorf("unsupported external data source mode: %s", fms.agentConfig.ExternalDataSource.Mode) + } + + if parsedURL, err := url.Parse(location); err != nil { + return fmt.Errorf("invalid URL %s: %w", location, err) + } else if !fms.agentConfig.IsDomainAllowed(parsedURL.Hostname()) { + return fmt.Errorf("domain %s is not in the allowed list", parsedURL.Hostname()) + } + + if _, ok := downloadedFiles[location]; ok { + slog.DebugContext(ctx, "File already downloaded from external source", "location", location) + continue + } + + if processErr := fms.processSingleExternalFile(ctx, file, location); processErr != nil { + return processErr + } + + downloadedFiles[location] = file.GetFileMeta().GetName() + } + + return nil +} + +func (fms *FileManagerService) processSingleExternalFile(ctx context.Context, file *mpi.File, location string) error { + tmpFilePath, downloadErr := fms.fileOperator.runHelper( + ctx, + fms.agentConfig.ExternalDataSource.Helper.Path, + location, + fms.agentConfig.ExternalDataSource.MaxBytes, + ) + if downloadErr != nil { + return fmt.Errorf("failed to download file from %s: %w", location, downloadErr) + } + + content, readErr := os.ReadFile(tmpFilePath) + if readErr != nil { + os.Remove(tmpFilePath) + return fmt.Errorf("failed to read downloaded file from %s: %w", tmpFilePath, readErr) + } + os.Remove(tmpFilePath) + + destPath := file.GetFileMeta().GetName() + destDir := filepath.Dir(destPath) + + if _, statErr := os.Stat(destDir); os.IsNotExist(statErr) { + if mkdirErr := os.MkdirAll(destDir, os.ModePerm); mkdirErr != nil { + return fmt.Errorf("failed to create destination directory %s: %w", destDir, mkdirErr) + } + } + + permission, parseErr := strconv.ParseUint(file.GetFileMeta().GetPermissions(), 8, 32) + if parseErr != nil { + slog.WarnContext(ctx, "failed to parse file permissions, using default 0644", "permissions", + file.GetFileMeta().GetPermissions()) + permission = 0o644 + } + + if writeErr := os.WriteFile(destPath, content, os.FileMode(permission)); writeErr != nil { + return fmt.Errorf("failed to write content to final file %s: %w", destPath, writeErr) + } + + file.FileMeta.Hash = files.GenerateHash(content) + file.FileMeta.Size = int64(len(content)) + file.Unmanaged = true + + slog.DebugContext(ctx, "Successfully downloaded file using helper", "location", location, "dest_path", destPath) + + return nil +} diff --git a/internal/file/file_manager_service_test.go b/internal/file/file_manager_service_test.go index 5a2644080..394ba22a8 100644 --- a/internal/file/file_manager_service_test.go +++ b/internal/file/file_manager_service_test.go @@ -15,8 +15,12 @@ import ( "path/filepath" "sync" "testing" + "time" + + "github.com/nginx/agent/v3/internal/config" "github.com/nginx/agent/v3/internal/model" + "go.uber.org/mock/gomock" "github.com/nginx/agent/v3/pkg/files" "google.golang.org/protobuf/types/known/timestamppb" @@ -30,6 +34,42 @@ import ( "github.com/stretchr/testify/require" ) +//nolint:unparam //setupTest is a helper function for tests that returns multiple values +func setupTest(t *testing.T) (*FileManagerService, *v1fakes.FakeFileServiceClient, string) { + t.Helper() + tempDir := t.TempDir() + fakeFileServiceClient := &v1fakes.FakeFileServiceClient{} + + // Default agentConfig with reusable settings + agentConfig := &config.Config{ + AllowedDirectories: []string{tempDir, "/tmp/local/etc/nginx"}, + ExternalDataSource: &config.ExternalDataSource{ + Helper: &config.HelperConfig{ + Path: filepath.Join(tempDir, "helperfile.txt"), + }, + Mode: "helper", + AllowedDomains: []string{"test.com"}, + MaxBytes: 1000, + }, + Client: &config.Client{ + Grpc: &config.GRPC{ + MaxFileSize: 1024, + }, + Backoff: &config.BackOff{ + InitialInterval: 500 * time.Millisecond, + MaxInterval: 10 * time.Second, + MaxElapsedTime: 20 * time.Second, + }, + }, + LibDir: tempDir, + } + + fileManagerService := NewFileManagerService(fakeFileServiceClient, agentConfig, &sync.RWMutex{}) + fileManagerService.manifestFilePath = filepath.Join(tempDir, "manifest.json") + + return fileManagerService, fakeFileServiceClient, tempDir +} + func TestFileManagerService_ConfigApply_Add(t *testing.T) { ctx := context.Background() tempDir := t.TempDir() @@ -1031,3 +1071,243 @@ func TestFileManagerService_createTempConfigDirectory(t *testing.T) { assert.Empty(t, dir) require.Error(t, err) } + +func TestFileManagerService_isExternalFilePresent(t *testing.T) { + fms := &FileManagerService{ + manifestLock: &sync.RWMutex{}, + } + + t.Run("Test 1 : ReturnsTrueWhenExternalFileIsPresent", func(t *testing.T) { + filesWithExternalSource := []*mpi.File{ + { + FileMeta: &mpi.FileMeta{Name: "config1.conf"}, + }, + { + FileMeta: &mpi.FileMeta{Name: "external-source-file.conf"}, + ExternalDataSource: &mpi.ExternalDataSource{Location: "http://example.com/file.txt"}, + }, + { + FileMeta: &mpi.FileMeta{Name: "config2.conf"}, + }, + } + + isPresent := fms.isExternalFilePresent(filesWithExternalSource) + assert.True(t, isPresent, "should return true because an external file is present") + }) + + t.Run("Test 2 : ReturnsFalseWhenNoExternalFileIsPresent", func(t *testing.T) { + filesWithoutExternalSource := []*mpi.File{ + { + FileMeta: &mpi.FileMeta{Name: "config1.conf"}, + }, + { + FileMeta: &mpi.FileMeta{Name: "config2.conf"}, + }, + } + + isPresent := fms.isExternalFilePresent(filesWithoutExternalSource) + assert.False(t, isPresent, "should return false because no external files are present") + }) + + t.Run("Test 3 : ReturnsFalseWhenSliceIsEmpty", func(t *testing.T) { + emptyFiles := []*mpi.File{} + + isPresent := fms.isExternalFilePresent(emptyFiles) + assert.False(t, isPresent, "should return false because the input slice is empty") + }) +} + +func TestFileManagerService_checkHelperDirectory_Allowed(t *testing.T) { + tempDir := t.TempDir() + helperPath := filepath.Join(tempDir, "helper_scripts", "my_helper") + + agentConfig := &config.Config{ + AllowedDirectories: []string{filepath.Dir(helperPath)}, + ExternalDataSource: &config.ExternalDataSource{ + Helper: &config.HelperConfig{ + Path: helperPath, + }, + }, + } + + fms := &FileManagerService{ + agentConfig: agentConfig, + } + + err := fms.checkHelperDirectory() + require.NoError(t, err) +} + +func TestFileManagerService_checkHelperDirectory_NotAllowed(t *testing.T) { + tempDir := t.TempDir() + helperPath := filepath.Join(tempDir, "my_helper") + + agentConfig := &config.Config{ + AllowedDirectories: []string{filepath.Join(tempDir, "some_other_dir")}, + ExternalDataSource: &config.ExternalDataSource{ + Helper: &config.HelperConfig{ + Path: helperPath, + }, + }, + } + + fms := &FileManagerService{ + agentConfig: agentConfig, + } + + err := fms.checkHelperDirectory() + require.Error(t, err) + expectedErrorMsg := "helper file is not present in allowed directories " + helperPath + assert.EqualError(t, err, expectedErrorMsg) +} + +func TestFileManagerService_downloadExternalFiles_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFileOperator := NewMockfileOperator(ctrl) + + fms, _, tempDir := setupTest(t) + fms.fileOperator = mockFileOperator + + destPath := filepath.Join(tempDir, "nginx.conf") + defer os.Remove(destPath) + + tmpFile, _ := os.CreateTemp(tempDir, "downloaded_file") + defer os.Remove(tmpFile.Name()) + _, err := tmpFile.WriteString("test content") + if err != nil { + t.Fatalf("Failed to write to temporary file: %v", err) + } + + mockFileOperator.EXPECT(). + runHelper( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(tmpFile.Name(), nil) + + fileMeta := &mpi.FileMeta{ + Name: destPath, + Permissions: "644", + } + file := &mpi.File{ + FileMeta: fileMeta, + ExternalDataSource: &mpi.ExternalDataSource{ + Location: "http://test.com/nginx.conf", + }, + } + + errDownloadExternalFile := fms.downloadExternalFiles(context.Background(), []*mpi.File{file}) + require.NoError(t, errDownloadExternalFile) +} + +func TestFileManagerService_downloadExternalFiles_DownloadFails(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFileOperator := NewMockfileOperator(ctrl) + + fms, _, _ := setupTest(t) + fms.fileOperator = mockFileOperator + + expectedErr := errors.New("helper download failed") + mockFileOperator.EXPECT(). + runHelper( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return("", expectedErr) + + file := &mpi.File{ + ExternalDataSource: &mpi.ExternalDataSource{ + Location: "http://test.com/nginx.conf", + }, + } + + err := fms.downloadExternalFiles(context.Background(), []*mpi.File{file}) + require.Error(t, err) + assert.EqualError(t, err, "failed to download file from http://test.com/nginx.conf: helper download failed") +} + +func TestFileManagerService_downloadExternalFiles_UnsupportedMode(t *testing.T) { + fms, _, _ := setupTest(t) + + file := &mpi.File{ + ExternalDataSource: &mpi.ExternalDataSource{ + Location: "http://test.com/nginx.conf", + }, + } + + fms.agentConfig.ExternalDataSource.Mode = "unsupported_mode" + + err := fms.downloadExternalFiles(context.Background(), []*mpi.File{file}) + require.Error(t, err) + assert.EqualError(t, err, "unsupported external data source mode: unsupported_mode") +} + +func TestFileManagerService_downloadExternalFiles_NotAllowedDomain(t *testing.T) { + fms, _, _ := setupTest(t) + + file := &mpi.File{ + ExternalDataSource: &mpi.ExternalDataSource{ + Location: "http://bad-domain.com/nginx.conf", + }, + } + + err := fms.downloadExternalFiles(context.Background(), []*mpi.File{file}) + require.Error(t, err) + assert.EqualError(t, err, "domain bad-domain.com is not in the allowed list") +} + +func TestFileManagerService_processSingleExternalFile_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFileOperator := NewMockfileOperator(ctrl) + + fms, _, tempDir := setupTest(t) + fms.fileOperator = mockFileOperator + + tmpFile, err := os.CreateTemp(tempDir, "downloaded_file") + require.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString("test content") + require.NoError(t, err) + tmpFile.Close() + + destPath := filepath.Join(tempDir, "nginx.conf") + + mockFileOperator.EXPECT(). + runHelper( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(tmpFile.Name(), nil) + + fileMeta := &mpi.FileMeta{ + Name: destPath, + Permissions: "644", + } + file := &mpi.File{ + FileMeta: fileMeta, + ExternalDataSource: &mpi.ExternalDataSource{ + Location: "http://test.com/nginx.conf", + }, + } + + err = fms.processSingleExternalFile(context.Background(), file, "http://test.com/nginx.conf") + + require.NoError(t, err) + + _, err = os.Stat(destPath) + require.NoError(t, err) + + content, err := os.ReadFile(destPath) + require.NoError(t, err) + assert.Equal(t, "test content", string(content)) +} diff --git a/internal/file/file_operator.go b/internal/file/file_operator.go index d765efc7d..9aebd76d7 100644 --- a/internal/file/file_operator.go +++ b/internal/file/file_operator.go @@ -7,12 +7,14 @@ package file import ( "bufio" + "bytes" "context" "encoding/json" "fmt" "io" "log/slog" "os" + "os/exec" "path" "sync" @@ -25,6 +27,18 @@ import ( mpi "github.com/nginx/agent/v3/api/grpc/mpi/v1" ) +var execCommandContext = exec.CommandContext + +const bufferSize = 4096 + +// Helper struct to unmarshal the JSON error from stderr. +type helperError struct { + Error string `json:"error"` + Message string `json:"message"` + Retryable bool `json:"retryable"` + Status int `json:"status"` +} + type FileOperator struct { manifestLock *sync.RWMutex } @@ -221,3 +235,91 @@ func closeFile(ctx context.Context, file *os.File) { slog.ErrorContext(ctx, "Error closing file", "error", err, "file", file.Name()) } } + +func (fo *FileOperator) runHelper( + ctx context.Context, + helperPath string, + url string, + maxBytes int64, +) (tmpFilePath string, err error) { + args := []string{"--url", url} + cmd := execCommandContext(ctx, helperPath, args...) + + tmpFile, createErr := os.CreateTemp("", "external-file-*.tmp") + if createErr != nil { + return "", fmt.Errorf("failed to create temp file: %w", createErr) + } + tmpFilePath = tmpFile.Name() + defer tmpFile.Close() + + defer func() { + if err != nil { + os.Remove(tmpFilePath) + } + }() + + stdoutPipe, pipeErr := cmd.StdoutPipe() + if pipeErr != nil { + return "", fmt.Errorf("failed to create stdout pipe: %w", pipeErr) + } + + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if startErr := cmd.Start(); startErr != nil { + return "", fmt.Errorf("failed to start helper process: %w", startErr) + } + + streamErr := fo.streamProcessOutput(stdoutPipe, tmpFile, maxBytes) + + waitErr := cmd.Wait() + + if streamErr != nil { + return "", streamErr + } + + if waitErr != nil { + var errObj helperError + if json.Unmarshal(stderr.Bytes(), &errObj) == nil { + return "", fmt.Errorf("helper process failed with error '%s' and message '%s'", + errObj.Error, errObj.Message) + } + + return "", fmt.Errorf("helper process failed with exit code %d, stderr: %s", + cmd.ProcessState.ExitCode(), stderr.String()) + } + + return tmpFilePath, nil +} + +func (fo *FileOperator) streamProcessOutput(stdout io.Reader, destFile io.Writer, maxBytes int64) error { + totalBytesRead := int64(0) + reader := bufio.NewReader(stdout) + chunk := make([]byte, bufferSize) + + for { + n, readErr := reader.Read(chunk) + + if readErr != nil { + if readErr == io.EOF { + return nil + } + + return fmt.Errorf("error reading from helper stdout: %w", readErr) + } + + if n == 0 { + continue + } + + totalBytesRead += int64(n) + if totalBytesRead > maxBytes { + return fmt.Errorf("downloaded file size (%d bytes) exceeds the maximum allowed size of %d bytes", + totalBytesRead, maxBytes) + } + + if _, writeErr := destFile.Write(chunk[:n]); writeErr != nil { + return fmt.Errorf("error writing to temp file: %w", writeErr) + } + } +} diff --git a/internal/file/file_operator_test.go b/internal/file/file_operator_test.go index 4a49fcdd1..c91a9b4cb 100644 --- a/internal/file/file_operator_test.go +++ b/internal/file/file_operator_test.go @@ -7,9 +7,11 @@ package file import ( "context" + "fmt" "os" "path" "path/filepath" + "strings" "sync" "testing" @@ -103,3 +105,50 @@ func TestFileOperator_MoveFile_destFileDoesNotExist(t *testing.T) { assert.FileExists(t, tempFile) assert.NoFileExists(t, newFile) } + +func TestFileOperator_runHelper(t *testing.T) { + fo := NewFileOperator(&sync.RWMutex{}) + ctx := context.Background() + tmpDir := t.TempDir() + helperPath := filepath.Join(tmpDir, "helper-script") + + createScript := func(content string, permissions os.FileMode) { + err := os.WriteFile(helperPath, []byte(content), permissions) + require.NoError(t, err) + } + + t.Run("Test 1 : Success", func(t *testing.T) { + script := "#!/bin/sh\nprintf 'test content'" + createScript(script, 0o755) + + tmpFilePath, err := fo.runHelper(ctx, helperPath, "http://example.com", 100) + require.NoError(t, err) + defer os.Remove(tmpFilePath) + + data, readErr := os.ReadFile(tmpFilePath) + require.NoError(t, readErr) + assert.Equal(t, "test content", string(data)) + }) + + t.Run("Test 2 : ExceedsMaxBytes", func(t *testing.T) { + largeContent := strings.Repeat("a", 101) + script := fmt.Sprintf("#!/bin/sh\nprintf '%%s' '%s'", largeContent) + createScript(script, 0o755) + + _, err := fo.runHelper(ctx, helperPath, "http://example.com", 100) + require.Error(t, err) + assert.Contains(t, err.Error(), "exceeds the maximum allowed size") + }) + + t.Run("Test 3 : JSONError", func(t *testing.T) { + jsonError := `{"error":"HelperExecutionError","message":"URL not found"}` + script := fmt.Sprintf("#!/bin/sh\nprintf '%%s' '%s' 1>&2\nexit 1", jsonError) + createScript(script, 0o755) + + _, err := fo.runHelper(ctx, helperPath, "http://example.com", 100) + require.Error(t, err) + assert.EqualError(t, err, "helper process failed with error 'HelperExecutionError' and message 'URL not found'") + }) + + os.RemoveAll(tmpDir) +} diff --git a/internal/file/filefakes/fake_file_operator.go b/internal/file/filefakes/fake_file_operator.go index 27e1a54da..9260bfee6 100644 --- a/internal/file/filefakes/fake_file_operator.go +++ b/internal/file/filefakes/fake_file_operator.go @@ -96,6 +96,22 @@ type FakeFileOperator struct { writeManifestFileReturnsOnCall map[int]struct { result1 error } + runHelperStub func(context.Context, string, string, int64) (string, error) + runHelperMutex sync.RWMutex + runHelperArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + arg4 int64 + } + runHelperReturns struct { + result1 string + result2 error + } + runHelperReturnsOnCall map[int]struct { + result1 string + result2 error + } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } @@ -490,6 +506,73 @@ func (fake *FakeFileOperator) WriteManifestFileReturnsOnCall(i int, result1 erro }{result1} } +func (fake *FakeFileOperator) runHelper(arg1 context.Context, arg2 string, arg3 string, arg4 int64) (string, error) { + fake.runHelperMutex.Lock() + ret, specificReturn := fake.runHelperReturnsOnCall[len(fake.runHelperArgsForCall)] + fake.runHelperArgsForCall = append(fake.runHelperArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + arg4 int64 + }{arg1, arg2, arg3, arg4}) + stub := fake.runHelperStub + fakeReturns := fake.runHelperReturns + fake.recordInvocation("runHelper", []interface{}{arg1, arg2, arg3, arg4}) + fake.runHelperMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeFileOperator) RunHelperCallCount() int { + fake.runHelperMutex.RLock() + defer fake.runHelperMutex.RUnlock() + return len(fake.runHelperArgsForCall) +} + +func (fake *FakeFileOperator) RunHelperCalls(stub func(context.Context, string, string, int64) (string, error)) { + fake.runHelperMutex.Lock() + defer fake.runHelperMutex.Unlock() + fake.runHelperStub = stub +} + +func (fake *FakeFileOperator) RunHelperArgsForCall(i int) (context.Context, string, string, int64) { + fake.runHelperMutex.RLock() + defer fake.runHelperMutex.RUnlock() + argsForCall := fake.runHelperArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 +} + +func (fake *FakeFileOperator) RunHelperReturns(result1 string, result2 error) { + fake.runHelperMutex.Lock() + defer fake.runHelperMutex.Unlock() + fake.runHelperStub = nil + fake.runHelperReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeFileOperator) RunHelperReturnsOnCall(i int, result1 string, result2 error) { + fake.runHelperMutex.Lock() + defer fake.runHelperMutex.Unlock() + fake.runHelperStub = nil + if fake.runHelperReturnsOnCall == nil { + fake.runHelperReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.runHelperReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + func (fake *FakeFileOperator) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() @@ -505,6 +588,8 @@ func (fake *FakeFileOperator) Invocations() map[string][][]interface{} { defer fake.writeChunkedFileMutex.RUnlock() fake.writeManifestFileMutex.RLock() defer fake.writeManifestFileMutex.RUnlock() + fake.runHelperMutex.RLock() + defer fake.runHelperMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/internal/file/mock_file_operator_test.go b/internal/file/mock_file_operator_test.go new file mode 100644 index 000000000..34b26ae03 --- /dev/null +++ b/internal/file/mock_file_operator_test.go @@ -0,0 +1,436 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: file_manager_service.go +// +// Generated by this command: +// +// mockgen -source=file_manager_service.go -destination=mock_file_operator_test.go +// + +// Package mock_file is a generated GoMock package. +package file + +import ( + bufio "bufio" + context "context" + reflect "reflect" + + v1 "github.com/nginx/agent/v3/api/grpc/mpi/v1" + model "github.com/nginx/agent/v3/internal/model" + gomock "go.uber.org/mock/gomock" + grpc "google.golang.org/grpc" +) + +// MockfileOperator is a mock of fileOperator interface. +type MockfileOperator struct { + ctrl *gomock.Controller + recorder *MockfileOperatorMockRecorder + isgomock struct{} +} + +// MockfileOperatorMockRecorder is the mock recorder for MockfileOperator. +type MockfileOperatorMockRecorder struct { + mock *MockfileOperator +} + +// NewMockfileOperator creates a new mock instance. +func NewMockfileOperator(ctrl *gomock.Controller) *MockfileOperator { + mock := &MockfileOperator{ctrl: ctrl} + mock.recorder = &MockfileOperatorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockfileOperator) EXPECT() *MockfileOperatorMockRecorder { + return m.recorder +} + +// CreateFileDirectories mocks base method. +func (m *MockfileOperator) CreateFileDirectories(ctx context.Context, fileName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateFileDirectories", ctx, fileName) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateFileDirectories indicates an expected call of CreateFileDirectories. +func (mr *MockfileOperatorMockRecorder) CreateFileDirectories(ctx, fileName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFileDirectories", reflect.TypeOf((*MockfileOperator)(nil).CreateFileDirectories), ctx, fileName) +} + +// MoveFile mocks base method. +func (m *MockfileOperator) MoveFile(ctx context.Context, sourcePath, destPath string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MoveFile", ctx, sourcePath, destPath) + ret0, _ := ret[0].(error) + return ret0 +} + +// MoveFile indicates an expected call of MoveFile. +func (mr *MockfileOperatorMockRecorder) MoveFile(ctx, sourcePath, destPath any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MoveFile", reflect.TypeOf((*MockfileOperator)(nil).MoveFile), ctx, sourcePath, destPath) +} + +// ReadChunk mocks base method. +func (m *MockfileOperator) ReadChunk(ctx context.Context, chunkSize uint32, reader *bufio.Reader, chunkID uint32) (v1.FileDataChunk_Content, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadChunk", ctx, chunkSize, reader, chunkID) + ret0, _ := ret[0].(v1.FileDataChunk_Content) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadChunk indicates an expected call of ReadChunk. +func (mr *MockfileOperatorMockRecorder) ReadChunk(ctx, chunkSize, reader, chunkID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadChunk", reflect.TypeOf((*MockfileOperator)(nil).ReadChunk), ctx, chunkSize, reader, chunkID) +} + +// Write mocks base method. +func (m *MockfileOperator) Write(ctx context.Context, fileContent []byte, fileName, filePermissions string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", ctx, fileContent, fileName, filePermissions) + ret0, _ := ret[0].(error) + return ret0 +} + +// Write indicates an expected call of Write. +func (mr *MockfileOperatorMockRecorder) Write(ctx, fileContent, fileName, filePermissions any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockfileOperator)(nil).Write), ctx, fileContent, fileName, filePermissions) +} + +// WriteChunkedFile mocks base method. +func (m *MockfileOperator) WriteChunkedFile(ctx context.Context, fileName, filePermissions string, header *v1.FileDataChunkHeader, stream grpc.ServerStreamingClient[v1.FileDataChunk]) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WriteChunkedFile", ctx, fileName, filePermissions, header, stream) + ret0, _ := ret[0].(error) + return ret0 +} + +// WriteChunkedFile indicates an expected call of WriteChunkedFile. +func (mr *MockfileOperatorMockRecorder) WriteChunkedFile(ctx, fileName, filePermissions, header, stream any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteChunkedFile", reflect.TypeOf((*MockfileOperator)(nil).WriteChunkedFile), ctx, fileName, filePermissions, header, stream) +} + +// WriteManifestFile mocks base method. +func (m *MockfileOperator) WriteManifestFile(ctx context.Context, updatedFiles map[string]*model.ManifestFile, manifestDir, manifestPath string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WriteManifestFile", ctx, updatedFiles, manifestDir, manifestPath) + ret0, _ := ret[0].(error) + return ret0 +} + +// WriteManifestFile indicates an expected call of WriteManifestFile. +func (mr *MockfileOperatorMockRecorder) WriteManifestFile(ctx, updatedFiles, manifestDir, manifestPath any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteManifestFile", reflect.TypeOf((*MockfileOperator)(nil).WriteManifestFile), ctx, updatedFiles, manifestDir, manifestPath) +} + +// runHelper mocks base method. +func (m *MockfileOperator) runHelper(ctx context.Context, helperPath, fileUrl string, maxBytes int64) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "runHelper", ctx, helperPath, fileUrl, maxBytes) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// runHelper indicates an expected call of runHelper. +func (mr *MockfileOperatorMockRecorder) runHelper(ctx, helperPath, fileUrl, maxBytes any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "runHelper", reflect.TypeOf((*MockfileOperator)(nil).runHelper), ctx, helperPath, fileUrl, maxBytes) +} + +// MockfileServiceOperatorInterface is a mock of fileServiceOperatorInterface interface. +type MockfileServiceOperatorInterface struct { + ctrl *gomock.Controller + recorder *MockfileServiceOperatorInterfaceMockRecorder + isgomock struct{} +} + +// MockfileServiceOperatorInterfaceMockRecorder is the mock recorder for MockfileServiceOperatorInterface. +type MockfileServiceOperatorInterfaceMockRecorder struct { + mock *MockfileServiceOperatorInterface +} + +// NewMockfileServiceOperatorInterface creates a new mock instance. +func NewMockfileServiceOperatorInterface(ctrl *gomock.Controller) *MockfileServiceOperatorInterface { + mock := &MockfileServiceOperatorInterface{ctrl: ctrl} + mock.recorder = &MockfileServiceOperatorInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockfileServiceOperatorInterface) EXPECT() *MockfileServiceOperatorInterfaceMockRecorder { + return m.recorder +} + +// ChunkedFile mocks base method. +func (m *MockfileServiceOperatorInterface) ChunkedFile(ctx context.Context, file *v1.File, tempFilePath, expectedHash string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ChunkedFile", ctx, file, tempFilePath, expectedHash) + ret0, _ := ret[0].(error) + return ret0 +} + +// ChunkedFile indicates an expected call of ChunkedFile. +func (mr *MockfileServiceOperatorInterfaceMockRecorder) ChunkedFile(ctx, file, tempFilePath, expectedHash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChunkedFile", reflect.TypeOf((*MockfileServiceOperatorInterface)(nil).ChunkedFile), ctx, file, tempFilePath, expectedHash) +} + +// File mocks base method. +func (m *MockfileServiceOperatorInterface) File(ctx context.Context, file *v1.File, tempFilePath, expectedHash string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "File", ctx, file, tempFilePath, expectedHash) + ret0, _ := ret[0].(error) + return ret0 +} + +// File indicates an expected call of File. +func (mr *MockfileServiceOperatorInterfaceMockRecorder) File(ctx, file, tempFilePath, expectedHash any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "File", reflect.TypeOf((*MockfileServiceOperatorInterface)(nil).File), ctx, file, tempFilePath, expectedHash) +} + +// IsConnected mocks base method. +func (m *MockfileServiceOperatorInterface) IsConnected() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsConnected") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsConnected indicates an expected call of IsConnected. +func (mr *MockfileServiceOperatorInterfaceMockRecorder) IsConnected() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnected", reflect.TypeOf((*MockfileServiceOperatorInterface)(nil).IsConnected)) +} + +// MoveFilesFromTempDirectory mocks base method. +func (m *MockfileServiceOperatorInterface) MoveFilesFromTempDirectory(ctx context.Context, fileAction *model.FileCache, tempDir string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MoveFilesFromTempDirectory", ctx, fileAction, tempDir) + ret0, _ := ret[0].(error) + return ret0 +} + +// MoveFilesFromTempDirectory indicates an expected call of MoveFilesFromTempDirectory. +func (mr *MockfileServiceOperatorInterfaceMockRecorder) MoveFilesFromTempDirectory(ctx, fileAction, tempDir any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MoveFilesFromTempDirectory", reflect.TypeOf((*MockfileServiceOperatorInterface)(nil).MoveFilesFromTempDirectory), ctx, fileAction, tempDir) +} + +// SetIsConnected mocks base method. +func (m *MockfileServiceOperatorInterface) SetIsConnected(isConnected bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetIsConnected", isConnected) +} + +// SetIsConnected indicates an expected call of SetIsConnected. +func (mr *MockfileServiceOperatorInterfaceMockRecorder) SetIsConnected(isConnected any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIsConnected", reflect.TypeOf((*MockfileServiceOperatorInterface)(nil).SetIsConnected), isConnected) +} + +// UpdateClient mocks base method. +func (m *MockfileServiceOperatorInterface) UpdateClient(ctx context.Context, fileServiceClient v1.FileServiceClient) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdateClient", ctx, fileServiceClient) +} + +// UpdateClient indicates an expected call of UpdateClient. +func (mr *MockfileServiceOperatorInterfaceMockRecorder) UpdateClient(ctx, fileServiceClient any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateClient", reflect.TypeOf((*MockfileServiceOperatorInterface)(nil).UpdateClient), ctx, fileServiceClient) +} + +// UpdateFile mocks base method. +func (m *MockfileServiceOperatorInterface) UpdateFile(ctx context.Context, instanceID string, fileToUpdate *v1.File) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateFile", ctx, instanceID, fileToUpdate) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateFile indicates an expected call of UpdateFile. +func (mr *MockfileServiceOperatorInterfaceMockRecorder) UpdateFile(ctx, instanceID, fileToUpdate any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFile", reflect.TypeOf((*MockfileServiceOperatorInterface)(nil).UpdateFile), ctx, instanceID, fileToUpdate) +} + +// UpdateOverview mocks base method. +func (m *MockfileServiceOperatorInterface) UpdateOverview(ctx context.Context, instanceID string, filesToUpdate []*v1.File, configPath string, iteration int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateOverview", ctx, instanceID, filesToUpdate, configPath, iteration) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateOverview indicates an expected call of UpdateOverview. +func (mr *MockfileServiceOperatorInterfaceMockRecorder) UpdateOverview(ctx, instanceID, filesToUpdate, configPath, iteration any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOverview", reflect.TypeOf((*MockfileServiceOperatorInterface)(nil).UpdateOverview), ctx, instanceID, filesToUpdate, configPath, iteration) +} + +// MockfileManagerServiceInterface is a mock of fileManagerServiceInterface interface. +type MockfileManagerServiceInterface struct { + ctrl *gomock.Controller + recorder *MockfileManagerServiceInterfaceMockRecorder + isgomock struct{} +} + +// MockfileManagerServiceInterfaceMockRecorder is the mock recorder for MockfileManagerServiceInterface. +type MockfileManagerServiceInterfaceMockRecorder struct { + mock *MockfileManagerServiceInterface +} + +// NewMockfileManagerServiceInterface creates a new mock instance. +func NewMockfileManagerServiceInterface(ctrl *gomock.Controller) *MockfileManagerServiceInterface { + mock := &MockfileManagerServiceInterface{ctrl: ctrl} + mock.recorder = &MockfileManagerServiceInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockfileManagerServiceInterface) EXPECT() *MockfileManagerServiceInterfaceMockRecorder { + return m.recorder +} + +// ClearCache mocks base method. +func (m *MockfileManagerServiceInterface) ClearCache() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ClearCache") +} + +// ClearCache indicates an expected call of ClearCache. +func (mr *MockfileManagerServiceInterfaceMockRecorder) ClearCache() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearCache", reflect.TypeOf((*MockfileManagerServiceInterface)(nil).ClearCache)) +} + +// ConfigApply mocks base method. +func (m *MockfileManagerServiceInterface) ConfigApply(ctx context.Context, configApplyRequest *v1.ConfigApplyRequest) (model.WriteStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConfigApply", ctx, configApplyRequest) + ret0, _ := ret[0].(model.WriteStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConfigApply indicates an expected call of ConfigApply. +func (mr *MockfileManagerServiceInterfaceMockRecorder) ConfigApply(ctx, configApplyRequest any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigApply", reflect.TypeOf((*MockfileManagerServiceInterface)(nil).ConfigApply), ctx, configApplyRequest) +} + +// ConfigUpdate mocks base method. +func (m *MockfileManagerServiceInterface) ConfigUpdate(ctx context.Context, nginxConfigContext *model.NginxConfigContext) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ConfigUpdate", ctx, nginxConfigContext) +} + +// ConfigUpdate indicates an expected call of ConfigUpdate. +func (mr *MockfileManagerServiceInterfaceMockRecorder) ConfigUpdate(ctx, nginxConfigContext any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigUpdate", reflect.TypeOf((*MockfileManagerServiceInterface)(nil).ConfigUpdate), ctx, nginxConfigContext) +} + +// ConfigUpload mocks base method. +func (m *MockfileManagerServiceInterface) ConfigUpload(ctx context.Context, configUploadRequest *v1.ConfigUploadRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConfigUpload", ctx, configUploadRequest) + ret0, _ := ret[0].(error) + return ret0 +} + +// ConfigUpload indicates an expected call of ConfigUpload. +func (mr *MockfileManagerServiceInterfaceMockRecorder) ConfigUpload(ctx, configUploadRequest any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigUpload", reflect.TypeOf((*MockfileManagerServiceInterface)(nil).ConfigUpload), ctx, configUploadRequest) +} + +// DetermineFileActions mocks base method. +func (m *MockfileManagerServiceInterface) DetermineFileActions(ctx context.Context, currentFiles map[string]*v1.File, modifiedFiles map[string]*model.FileCache) (map[string]*model.FileCache, map[string][]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DetermineFileActions", ctx, currentFiles, modifiedFiles) + ret0, _ := ret[0].(map[string]*model.FileCache) + ret1, _ := ret[1].(map[string][]byte) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// DetermineFileActions indicates an expected call of DetermineFileActions. +func (mr *MockfileManagerServiceInterfaceMockRecorder) DetermineFileActions(ctx, currentFiles, modifiedFiles any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DetermineFileActions", reflect.TypeOf((*MockfileManagerServiceInterface)(nil).DetermineFileActions), ctx, currentFiles, modifiedFiles) +} + +// IsConnected mocks base method. +func (m *MockfileManagerServiceInterface) IsConnected() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsConnected") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsConnected indicates an expected call of IsConnected. +func (mr *MockfileManagerServiceInterfaceMockRecorder) IsConnected() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnected", reflect.TypeOf((*MockfileManagerServiceInterface)(nil).IsConnected)) +} + +// ResetClient mocks base method. +func (m *MockfileManagerServiceInterface) ResetClient(ctx context.Context, fileServiceClient v1.FileServiceClient) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "ResetClient", ctx, fileServiceClient) +} + +// ResetClient indicates an expected call of ResetClient. +func (mr *MockfileManagerServiceInterfaceMockRecorder) ResetClient(ctx, fileServiceClient any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResetClient", reflect.TypeOf((*MockfileManagerServiceInterface)(nil).ResetClient), ctx, fileServiceClient) +} + +// Rollback mocks base method. +func (m *MockfileManagerServiceInterface) Rollback(ctx context.Context, instanceID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Rollback", ctx, instanceID) + ret0, _ := ret[0].(error) + return ret0 +} + +// Rollback indicates an expected call of Rollback. +func (mr *MockfileManagerServiceInterfaceMockRecorder) Rollback(ctx, instanceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockfileManagerServiceInterface)(nil).Rollback), ctx, instanceID) +} + +// SetIsConnected mocks base method. +func (m *MockfileManagerServiceInterface) SetIsConnected(isConnected bool) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetIsConnected", isConnected) +} + +// SetIsConnected indicates an expected call of SetIsConnected. +func (mr *MockfileManagerServiceInterfaceMockRecorder) SetIsConnected(isConnected any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIsConnected", reflect.TypeOf((*MockfileManagerServiceInterface)(nil).SetIsConnected), isConnected) +} + +// UpdateCurrentFilesOnDisk mocks base method. +func (m *MockfileManagerServiceInterface) UpdateCurrentFilesOnDisk(ctx context.Context, updateFiles map[string]*v1.File, referenced bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateCurrentFilesOnDisk", ctx, updateFiles, referenced) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateCurrentFilesOnDisk indicates an expected call of UpdateCurrentFilesOnDisk. +func (mr *MockfileManagerServiceInterfaceMockRecorder) UpdateCurrentFilesOnDisk(ctx, updateFiles, referenced any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCurrentFilesOnDisk", reflect.TypeOf((*MockfileManagerServiceInterface)(nil).UpdateCurrentFilesOnDisk), ctx, updateFiles, referenced) +} diff --git a/test/mock/grpc/mock_management_command_service.go b/test/mock/grpc/mock_management_command_service.go index f68c4c7cd..50f8779e9 100644 --- a/test/mock/grpc/mock_management_command_service.go +++ b/test/mock/grpc/mock_management_command_service.go @@ -84,8 +84,9 @@ func NewCommandService( // Adding a struct to represent the external data source. type ExternalDataSource struct { - FilePath string `json:"filePath"` - Location string `json:"location"` + FilePath string `json:"filePath"` + Location string `json:"location"` + Permissions string `json:"permissions"` } // Adding a struct for the request body of the config apply endpoint. @@ -574,10 +575,15 @@ func processConfigApplyRequestBody(c *gin.Context, initialFiles []*mpi.File) ([] file.ExternalDataSource = &mpi.ExternalDataSource{ Location: ed.Location, } + if file.GetFileMeta() == nil { + file.FileMeta = &mpi.FileMeta{} + } + file.FileMeta.Permissions = ed.Permissions } else { newFile := &mpi.File{ FileMeta: &mpi.FileMeta{ - Name: ed.FilePath, + Name: ed.FilePath, + Permissions: ed.Permissions, }, ExternalDataSource: &mpi.ExternalDataSource{ Location: ed.Location,