From 0c895c95eed9ac94c22765100c1b88f17be911e5 Mon Sep 17 00:00:00 2001 From: Roman Dmytrenko Date: Tue, 21 Oct 2025 00:09:55 +0100 Subject: [PATCH 1/5] feat(storage): add remote_startup_fetch_policy configuration option Add configurable policy for handling remote git fetch failures during startup. This allows Flipt to continue operating with stale local data when the remote repository is temporarily unavailable. Signed-off-by: Roman Dmytrenko Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Roman Dmytrenko --- config/flipt.schema.cue | 1 + config/flipt.schema.json | 4 + internal/config/config_test.go | 37 +-- internal/config/storage.go | 30 ++- .../storage/git_provided_with_directory.yml | 1 + internal/storage/environments/environments.go | 10 +- internal/storage/git/repository.go | 77 ++++-- internal/storage/git/repository_test.go | 232 ++++++++++++++++-- 8 files changed, 318 insertions(+), 74 deletions(-) diff --git a/config/flipt.schema.cue b/config/flipt.schema.cue index b27464a75d..326e236090 100644 --- a/config/flipt.schema.cue +++ b/config/flipt.schema.cue @@ -200,6 +200,7 @@ JsonPath: string #storage: [string]: { remote?: string + remote_startup_fetch_policy?: *"required" | "optional" backend?: { type: *"memory" | "local" path?: string diff --git a/config/flipt.schema.json b/config/flipt.schema.json index 09a7bfd260..f7cf5edc81 100644 --- a/config/flipt.schema.json +++ b/config/flipt.schema.json @@ -776,6 +776,10 @@ "remote": { "type": "string" }, + "remoteStartupFetchPolicy": { + "type": "string", + "enum": ["required", "optional"] + }, "backend": { "type": [ "object", diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 98e415d7fe..136ca5ed27 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -459,6 +459,7 @@ func TestLoad(t *testing.T) { Type: MemoryStorageBackendType, }, Remote: "https://github.com/flipt-io/flipt.git", + RemoteStartupFetchPolicy: "required", Branch: "main", PollInterval: 5 * time.Second, Credentials: "git", @@ -577,9 +578,10 @@ func TestLoad(t *testing.T) { Backend: StorageBackendConfig{ Type: MemoryStorageBackendType, }, - Remote: "git@github.com:foo/bar.git", - Branch: "main", - PollInterval: 30 * time.Second, + Remote: "git@github.com:foo/bar.git", + RemoteStartupFetchPolicy: "required", + Branch: "main", + PollInterval: 30 * time.Second, }, } return cfg @@ -596,9 +598,10 @@ func TestLoad(t *testing.T) { Type: LocalStorageBackendType, Path: "/path/to/gitdir", }, - Remote: "git@github.com:foo/bar.git", - Branch: "main", - PollInterval: 30 * time.Second, + Remote: "git@github.com:foo/bar.git", + RemoteStartupFetchPolicy: "optional", + Branch: "main", + PollInterval: 30 * time.Second, }, } return cfg @@ -634,10 +637,11 @@ func TestLoad(t *testing.T) { Backend: StorageBackendConfig{ Type: MemoryStorageBackendType, }, - Remote: "git@github.com:foo/bar.git", - Branch: "main", - PollInterval: 30 * time.Second, - Credentials: "git", + Remote: "git@github.com:foo/bar.git", + RemoteStartupFetchPolicy: "required", + Branch: "main", + PollInterval: 30 * time.Second, + Credentials: "git", }, } cfg.Credentials = CredentialsConfig{ @@ -888,9 +892,10 @@ func TestLoad(t *testing.T) { Backend: StorageBackendConfig{ Type: MemoryStorageBackendType, }, - Remote: "git@github.com:foo/bar.git", - Branch: "main", - PollInterval: 30 * time.Second, + Remote: "git@github.com:foo/bar.git", + RemoteStartupFetchPolicy: "required", + Branch: "main", + PollInterval: 30 * time.Second, Signature: SignatureConfig{ Name: "Flipt Bot", Email: "bot@flipt.io", @@ -1141,7 +1146,7 @@ OUTER: } } - return + return vals } func TestMarshalYAML(t *testing.T) { @@ -1516,7 +1521,7 @@ func TestLicenseConfig_validate(t *testing.T) { setupFile: func(t *testing.T) string { tmpDir := t.TempDir() licenseFile := filepath.Join(tmpDir, "license.cert") - err := os.WriteFile(licenseFile, []byte("test license content"), 0600) + err := os.WriteFile(licenseFile, []byte("test license content"), 0o600) require.NoError(t, err) return licenseFile }, @@ -1531,7 +1536,7 @@ func TestLicenseConfig_validate(t *testing.T) { setupFile: func(t *testing.T) string { tmpDir := t.TempDir() licenseFile := filepath.Join(tmpDir, "license.cert") - err := os.WriteFile(licenseFile, []byte("test license content"), 0600) + err := os.WriteFile(licenseFile, []byte("test license content"), 0o600) require.NoError(t, err) return licenseFile }, diff --git a/internal/config/storage.go b/internal/config/storage.go index bbdfc5bb50..6d1babbea2 100644 --- a/internal/config/storage.go +++ b/internal/config/storage.go @@ -60,6 +60,10 @@ func (s *StoragesConfig) setDefaults(v *viper.Viper) error { if getString("poll_interval") == "" { setDefault("poll_interval", "30s") } + + if getString("remote") != "" && getString("remote_startup_fetch_policy") == "" { + setDefault("remote_startup_fetch_policy", RemoteStartupFetchPolicyRequired) + } } return nil @@ -77,18 +81,26 @@ type StorageBackendConfig struct { Path string `json:"path,omitempty" mapstructure:"path" yaml:"path,omitempty"` } +type RemoteStartupFetchPolicy string + +const ( + RemoteStartupFetchPolicyRequired = RemoteStartupFetchPolicy("required") + RemoteStartupFetchPolicyOptional = RemoteStartupFetchPolicy("optional") +) + // StorageConfig contains fields which will configure the type of backend in which Flipt will serve // flag state. type StorageConfig struct { - Remote string `json:"remote,omitempty" mapstructure:"remote" yaml:"remote,omitempty"` - Backend StorageBackendConfig `json:"backend,omitempty" mapstructure:"backend" yaml:"backend,omitempty"` - Branch string `json:"branch,omitempty" mapstructure:"branch" yaml:"branch,omitempty"` - CaCertBytes string `json:"-" mapstructure:"ca_cert_bytes" yaml:"-"` - CaCertPath string `json:"-" mapstructure:"ca_cert_path" yaml:"-"` - PollInterval time.Duration `json:"pollInterval,omitempty" mapstructure:"poll_interval" yaml:"poll_interval,omitempty"` - InsecureSkipTLS bool `json:"-" mapstructure:"insecure_skip_tls" yaml:"-"` - Credentials string `json:"-" mapstructure:"credentials" yaml:"-"` - Signature SignatureConfig `json:"signature,omitempty" mapstructure:"signature,omitempty" yaml:"signature,omitempty"` + Remote string `json:"remote,omitempty" mapstructure:"remote" yaml:"remote,omitempty"` + RemoteStartupFetchPolicy RemoteStartupFetchPolicy `json:"remote_startup_fetch_policy,omitempty" mapstructure:"remote_startup_fetch_policy" yaml:"remote_startup_fetch_policy,omitempty"` + Backend StorageBackendConfig `json:"backend,omitempty" mapstructure:"backend" yaml:"backend,omitempty"` + Branch string `json:"branch,omitempty" mapstructure:"branch" yaml:"branch,omitempty"` + CaCertBytes string `json:"-" mapstructure:"ca_cert_bytes" yaml:"-"` + CaCertPath string `json:"-" mapstructure:"ca_cert_path" yaml:"-"` + PollInterval time.Duration `json:"pollInterval,omitempty" mapstructure:"poll_interval" yaml:"poll_interval,omitempty"` + InsecureSkipTLS bool `json:"-" mapstructure:"insecure_skip_tls" yaml:"-"` + Credentials string `json:"-" mapstructure:"credentials" yaml:"-"` + Signature SignatureConfig `json:"signature,omitempty" mapstructure:"signature,omitempty" yaml:"signature,omitempty"` } func (c *StorageConfig) validate() error { diff --git a/internal/config/testdata/storage/git_provided_with_directory.yml b/internal/config/testdata/storage/git_provided_with_directory.yml index 2e88896837..4cd10675e0 100644 --- a/internal/config/testdata/storage/git_provided_with_directory.yml +++ b/internal/config/testdata/storage/git_provided_with_directory.yml @@ -1,6 +1,7 @@ storage: default: remote: "git@github.com:foo/bar.git" + remote_startup_fetch_policy: optional backend: type: "local" path: "/path/to/gitdir" diff --git a/internal/storage/environments/environments.go b/internal/storage/environments/environments.go index c985606c6c..02cd33a963 100644 --- a/internal/storage/environments/environments.go +++ b/internal/storage/environments/environments.go @@ -147,6 +147,9 @@ func (rm *RepositoryManager) GetOrCreate(ctx context.Context, envConf *config.En zap.String("key_id", storage.Signature.KeyID)) } } + if storage.RemoteStartupFetchPolicy == config.RemoteStartupFetchPolicyOptional { + opts = append(opts, storagegit.WithOptionalRemoteStartupFetchPolicy()) + } newRepo, err := storagegit.NewRepository(ctx, logger, opts...) if err != nil { @@ -512,7 +515,12 @@ func NewStore(ctx context.Context, logger *zap.Logger, cfg *config.Config, secre // branched environments have been added for _, repo := range repoManager.repos { if err := repo.Fetch(ctx); err != nil { - if !errors.Is(err, transport.ErrEmptyRemoteRepository) && !errors.Is(err, git.ErrRemoteRefNotFound) { + switch { + case repo.IsRemoteStartupFetchOptional() && repo.IsConnectionRefused(err): + continue + case errors.Is(err, transport.ErrEmptyRemoteRepository) || errors.Is(err, git.ErrRemoteRefNotFound): + continue + default: return nil, err } } diff --git a/internal/storage/git/repository.go b/internal/storage/git/repository.go index 65482ad79b..f6375c31fe 100644 --- a/internal/storage/git/repository.go +++ b/internal/storage/git/repository.go @@ -9,6 +9,7 @@ import ( "slices" "strings" "sync" + "syscall" "time" "github.com/go-git/go-billy/v6/osfs" @@ -33,19 +34,20 @@ type Repository struct { logger *zap.Logger - mu sync.RWMutex - remote *config.RemoteConfig - defaultBranch string - auth transport.AuthMethod - insecureSkipTLS bool - caBundle []byte - localPath string - readme []byte - sigName string - sigEmail string - signer signing.Signer - maxOpenDescriptors int - isNormalRepo bool // true if opened with PlainOpen, false if bare repository + mu sync.RWMutex + remote *config.RemoteConfig + optionalRemoteStartupFetch bool + defaultBranch string + auth transport.AuthMethod + insecureSkipTLS bool + caBundle []byte + localPath string + readme []byte + sigName string + sigEmail string + signer signing.Signer + maxOpenDescriptors int + isNormalRepo bool // true if opened with PlainOpen, false if bare repository subs []Subscriber @@ -71,7 +73,7 @@ func NewRepository(ctx context.Context, logger *zap.Logger, opts ...containers.O logger.Debug("repository empty, attempting to add and push a README") // add initial readme if repo is empty if _, err := repo.UpdateAndPush(ctx, repo.defaultBranch, func(fs envsfs.Filesystem) (string, error) { - fi, err := fs.OpenFile("README.md", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644) + fi, err := fs.OpenFile("README.md", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644) if err != nil { return "", err } @@ -198,15 +200,28 @@ func newRepository(ctx context.Context, logger *zap.Logger, opts ...containers.O // do an initial fetch to setup remote tracking branches if err := r.Fetch(ctx); err != nil { - if !errors.Is(err, transport.ErrEmptyRemoteRepository) && !errors.Is(err, git.ErrRemoteRefNotFound) { - return nil, empty, fmt.Errorf("performing initial fetch: %w", err) + fetchErr := fmt.Errorf("performing initial fetch: %w", err) + switch { + case r.optionalRemoteStartupFetch && r.IsConnectionRefused(err): + // if optional, we check if the error is connection refused + // and there is non-empty repo and flags could be evaluated + objs, rerr := r.CommitObjects() + if rerr != nil { + return nil, empty, fetchErr + } + _, rerr = objs.Next() + objs.Close() + if rerr != nil { + return nil, empty, fetchErr + } + case errors.Is(err, transport.ErrEmptyRemoteRepository) || errors.Is(err, git.ErrRemoteRefNotFound): + // the remote was reachable but either its contents was completely empty + // or our default branch doesn't exist and so we decide to seed it + empty = true + logger.Debug("initial fetch empty", zap.String("reference", r.defaultBranch), zap.Error(err)) + default: + return nil, empty, fetchErr } - - // the remote was reachable but either its contents was completely empty - // or our default branch doesn't exist and so we decide to seed it - empty = true - - logger.Debug("initial fetch empty", zap.String("reference", r.defaultBranch), zap.Error(err)) } } @@ -296,6 +311,17 @@ func (r *Repository) fetchHeads() []string { return slices.Collect(maps.Keys(heads)) } +// IsConnectionRefused checks if the provided error is a connection refused error. +func (r *Repository) IsConnectionRefused(err error) bool { + return errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, syscall.EHOSTUNREACH) || + errors.Is(err, syscall.ENETUNREACH) || errors.Is(err, syscall.EHOSTDOWN) +} + +// IsRemoteStartupFetchOptional returns true if the startup fetch is optional. +func (r *Repository) IsRemoteStartupFetchOptional() bool { + return r.optionalRemoteStartupFetch +} + // Fetch does a fetch for the requested head names on a configured remote. // If the remote is not defined, then it is a silent noop. // Iff specific is explicitly requested then only the heads in specific are fetched. @@ -830,6 +856,13 @@ func WithMaxOpenDescriptors(n int) containers.Option[Repository] { } } +// WithOptionalRemoteStartupFetchPolicy sets the optional remote startup fetch policy. +func WithOptionalRemoteStartupFetchPolicy() containers.Option[Repository] { + return func(r *Repository) { + r.optionalRemoteStartupFetch = true + } +} + const defaultReadmeContents = `Flipt Configuration Repository ============================== diff --git a/internal/storage/git/repository_test.go b/internal/storage/git/repository_test.go index 4013ec5e8f..87d8166597 100644 --- a/internal/storage/git/repository_test.go +++ b/internal/storage/git/repository_test.go @@ -1,9 +1,10 @@ package git import ( - "context" + "net" "os" "path/filepath" + "syscall" "testing" "time" @@ -19,7 +20,7 @@ func TestNewRepository_EmptyDirectory(t *testing.T) { tempDir := t.TempDir() logger := zap.NewNop() - repo, empty, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo, empty, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.NotNil(t, repo) assert.True(t, empty, "empty directory should be marked as empty") @@ -31,7 +32,7 @@ func TestNewRepository_NonExistentDirectory(t *testing.T) { nonExistentDir := filepath.Join(tempDir, "nonexistent") logger := zap.NewNop() - repo, empty, err := newRepository(context.Background(), logger, WithFilesystemStorage(nonExistentDir)) + repo, empty, err := newRepository(t.Context(), logger, WithFilesystemStorage(nonExistentDir)) require.NoError(t, err) require.NotNil(t, repo) assert.True(t, empty, "non-existent directory should be marked as empty") @@ -43,7 +44,7 @@ func TestNewRepository_DirectoryWithFiles_NoGitRepo(t *testing.T) { logger := zap.NewNop() // Create some content files - require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "production"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "production"), 0o755)) featuresFile := filepath.Join(tempDir, "production", "features.yaml") content := `namespace: key: production @@ -54,9 +55,9 @@ flags: type: VARIANT_FLAG_TYPE description: A test flag enabled: true` - require.NoError(t, os.WriteFile(featuresFile, []byte(content), 0600)) + require.NoError(t, os.WriteFile(featuresFile, []byte(content), 0o600)) - repo, empty, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo, empty, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.NotNil(t, repo) assert.True(t, empty, "directory with files but no git repo should be marked as empty for initial commit") @@ -72,7 +73,7 @@ func TestNewRepository_NormalGitRepository(t *testing.T) { require.NoError(t, err) // Create and commit some content - require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "production"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "production"), 0o755)) featuresFile := filepath.Join(tempDir, "production", "features.yaml") content := `namespace: key: production @@ -83,7 +84,7 @@ flags: type: VARIANT_FLAG_TYPE description: A test flag enabled: true` - require.NoError(t, os.WriteFile(featuresFile, []byte(content), 0600)) + require.NoError(t, os.WriteFile(featuresFile, []byte(content), 0o600)) // Commit the files plainRepo, err := git.PlainOpen(tempDir) @@ -102,7 +103,7 @@ flags: require.NoError(t, err) // Test opening with Flipt - repo, empty, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo, empty, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.NotNil(t, repo) assert.False(t, empty, "normal git repo with commits should not be marked as empty") @@ -122,7 +123,7 @@ func TestNewRepository_NormalGitRepository_NoCommits(t *testing.T) { _, err := git.PlainInit(tempDir, false) require.NoError(t, err) - repo, empty, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo, empty, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.NotNil(t, repo) assert.True(t, empty, "normal git repo without commits should be marked as empty") @@ -134,14 +135,14 @@ func TestNewRepository_BareGitRepository(t *testing.T) { logger := zap.NewNop() // Create a bare Git repository by initializing with custom storage - repo1, empty1, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo1, empty1, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.NotNil(t, repo1) assert.True(t, empty1, "initial bare repository should be empty") assert.False(t, repo1.isNormalRepo, "should be bare repository") // Try to reopen the empty bare repository (no files added yet) - repo2, empty2, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo2, empty2, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.NotNil(t, repo2) // The empty status may vary when reopening existing repositories depending on internal Git state @@ -155,14 +156,14 @@ func TestNewRepository_FilesWithNonGitBareRepository(t *testing.T) { logger := zap.NewNop() // Create a bare repository first - repo1, empty1, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo1, empty1, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.NotNil(t, repo1) assert.True(t, empty1, "initial bare repository should be empty") assert.False(t, repo1.isNormalRepo, "should be bare repository") // Now add some files to the temp directory (simulating files in storage path) - require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "production"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "production"), 0o755)) featuresFile := filepath.Join(tempDir, "production", "features.yaml") content := `namespace: key: production @@ -171,10 +172,10 @@ flags: - key: test-flag name: Test Flag type: VARIANT_FLAG_TYPE` - require.NoError(t, os.WriteFile(featuresFile, []byte(content), 0600)) + require.NoError(t, os.WriteFile(featuresFile, []byte(content), 0o600)) // Try to reopen - should detect existing bare repository instead of treating as normal repo - repo2, empty2, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo2, empty2, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.NotNil(t, repo2) // The behavior here depends on whether the bare repository has been initialized with objects @@ -192,7 +193,7 @@ func TestUpdateWorkingDirectory(t *testing.T) { require.NoError(t, err) // Create initial content and commit - require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "production"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(tempDir, "production"), 0o755)) featuresFile := filepath.Join(tempDir, "production", "features.yaml") initialContent := `namespace: key: production @@ -201,7 +202,7 @@ flags: - key: test-flag name: Test Flag enabled: false` - require.NoError(t, os.WriteFile(featuresFile, []byte(initialContent), 0600)) + require.NoError(t, os.WriteFile(featuresFile, []byte(initialContent), 0o600)) worktree, err := plainRepo.Worktree() require.NoError(t, err) @@ -224,7 +225,7 @@ flags: - key: test-flag name: Test Flag enabled: true` - require.NoError(t, os.WriteFile(featuresFile, []byte(updatedContent), 0600)) + require.NoError(t, os.WriteFile(featuresFile, []byte(updatedContent), 0o600)) _, err = worktree.Add("production/features.yaml") require.NoError(t, err) commit2, err := worktree.Commit("Update flag", &git.CommitOptions{ @@ -237,12 +238,12 @@ flags: require.NoError(t, err) // Create Flipt repository instance - repo, _, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.True(t, repo.isNormalRepo, "should be normal repository") // Test updating working directory to first commit - err = repo.updateWorkingDirectory(context.Background(), commit1) + err = repo.updateWorkingDirectory(t.Context(), commit1) require.NoError(t, err) // Verify file content matches first commit @@ -251,7 +252,7 @@ flags: assert.Contains(t, string(content), "enabled: false", "working directory should show first commit content") // Test updating working directory to second commit - err = repo.updateWorkingDirectory(context.Background(), commit2) + err = repo.updateWorkingDirectory(t.Context(), commit2) require.NoError(t, err) // Verify file content matches second commit @@ -265,12 +266,12 @@ func TestUpdateWorkingDirectory_BareRepository(t *testing.T) { logger := zap.NewNop() // Create a bare repository - repo, _, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.False(t, repo.isNormalRepo, "should be bare repository") // updateWorkingDirectory should be a no-op for bare repositories - err = repo.updateWorkingDirectory(context.Background(), plumbing.ZeroHash) + err = repo.updateWorkingDirectory(t.Context(), plumbing.ZeroHash) assert.NoError(t, err, "updateWorkingDirectory should not error on bare repositories") } @@ -278,7 +279,7 @@ func TestRepositoryDefaults(t *testing.T) { tempDir := t.TempDir() logger := zap.NewNop() - repo, _, err := newRepository(context.Background(), logger, WithFilesystemStorage(tempDir)) + repo, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.NotNil(t, repo) @@ -291,7 +292,7 @@ func TestRepositoryWithCustomBranch(t *testing.T) { tempDir := t.TempDir() logger := zap.NewNop() - repo, _, err := newRepository(context.Background(), logger, + repo, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir), WithDefaultBranch("develop")) require.NoError(t, err) @@ -299,3 +300,182 @@ func TestRepositoryWithCustomBranch(t *testing.T) { assert.Equal(t, "develop", repo.defaultBranch, "should use custom default branch") } + +func TestRemoteStartupPolicy_Required(t *testing.T) { + tempDir := t.TempDir() + logger := zap.NewNop() + + // Create a repository with required fetch policy (default) + repo, _, err := newRepository(t.Context(), logger, + WithFilesystemStorage(tempDir)) + require.NoError(t, err) + require.NotNil(t, repo) + + assert.False(t, repo.optionalRemoteStartupFetch, "should set fetch policy to required") +} + +func TestRemoteStartupPolicy_Optional(t *testing.T) { + tempDir := t.TempDir() + logger := zap.NewNop() + + // Create a repository with optional fetch policy + repo, _, err := newRepository(t.Context(), logger, + WithFilesystemStorage(tempDir), + WithOptionalRemoteStartupFetchPolicy(), + ) + require.NoError(t, err) + require.NotNil(t, repo) + + assert.True(t, repo.optionalRemoteStartupFetch, "should set fetch policy to optional") +} + +func TestIsConnectionRefused(t *testing.T) { + tempDir := t.TempDir() + logger := zap.NewNop() + + repo, _, err := newRepository(t.Context(), logger, + WithFilesystemStorage(tempDir)) + require.NoError(t, err) + + tests := []struct { + name string + err error + expected bool + }{ + { + name: "nil error", + err: nil, + expected: false, + }, + { + name: "generic error", + err: assert.AnError, + expected: false, + }, + { + name: "connection refused wrapped in net.OpError", + err: &net.OpError{Op: "dial", Net: "tcp", Err: &os.SyscallError{Syscall: "connect", Err: syscall.ECONNREFUSED}}, + expected: true, + }, + { + name: "different syscall error", + err: &os.SyscallError{Syscall: "connect", Err: syscall.ETIMEDOUT}, + expected: false, + }, + { + name: "net.OpError with non-connection-refused error", + err: &net.OpError{Op: "dial", Net: "tcp", Err: &os.SyscallError{Syscall: "connect", Err: syscall.EADDRINUSE}}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := repo.IsConnectionRefused(tt.err) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestFetch_NoRemote(t *testing.T) { + tempDir := t.TempDir() + logger := zap.NewNop() + + // Create repository without remote + repo, _, err := newRepository(t.Context(), logger, + WithFilesystemStorage(tempDir)) + require.NoError(t, err) + + // Fetch should be a no-op when no remote is configured + err = repo.Fetch(t.Context()) + assert.NoError(t, err, "fetch without remote should succeed silently") +} + +func TestFetch_RequiredPolicy_WithConnectionRefused(t *testing.T) { + tempDir := t.TempDir() + logger := zap.NewNop() + + // Create repository with required fetch policy and invalid remote + _, _, err := newRepository(t.Context(), logger, + WithFilesystemStorage(tempDir), + WithRemote("origin", "http://localhost:1/invalid-repo.git"), + ) + require.Error(t, err) +} + +func TestFetch_OptionalPolicy_WithConnectionRefusedAndCommits(t *testing.T) { + tempDir := t.TempDir() + logger := zap.NewNop() + + plainRepo, err := git.PlainInit(tempDir, false) + require.NoError(t, err) + + // Add a commit to the repository + worktree, err := plainRepo.Worktree() + require.NoError(t, err) + + // Create a file + testFile := filepath.Join(tempDir, "test.txt") + require.NoError(t, os.WriteFile(testFile, []byte("test content"), 0o600)) + + _, err = worktree.Add("test.txt") + require.NoError(t, err) + + _, err = worktree.Commit("Initial commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "Test User", + Email: "test@example.com", + When: time.Now(), + }, + }) + require.NoError(t, err) + + // Now create our Repository wrapper with optional policy and invalid remote + repo, _, err := newRepository(t.Context(), logger, + WithFilesystemStorage(tempDir), + WithRemote("origin", "http://localhost:1/invalid-repo.git"), + WithOptionalRemoteStartupFetchPolicy()) + require.NoError(t, err) + assert.True(t, repo.optionalRemoteStartupFetch) + assert.True(t, repo.IsRemoteStartupFetchOptional()) + + err = repo.Fetch(t.Context()) + assert.Error(t, err) +} + +func TestFetch_OptionalPolicy_WithConnectionRefusedAndNoCommits(t *testing.T) { + tempDir := t.TempDir() + logger := zap.NewNop() + + // Create repository with optional fetch policy and invalid remote, but no commits + repo, _, err := newRepository(t.Context(), logger, + WithFilesystemStorage(tempDir), + WithRemote("origin", "http://localhost:1/invalid-repo.git"), + WithOptionalRemoteStartupFetchPolicy()) + require.Error(t, err) + assert.Nil(t, repo) +} + +func TestFetch_OptionalPolicy_WithNonConnectionError(t *testing.T) { + tempDir := t.TempDir() + logger := zap.NewNop() + + // Create repository with optional fetch policy but a different type of error (not connection refused) + // Using an invalid URL format to trigger a different error + _, _, err := newRepository(t.Context(), logger, + WithFilesystemStorage(tempDir), + WithRemote("origin", "invalid://bad-url"), + WithOptionalRemoteStartupFetchPolicy()) + require.Error(t, err) +} + +func TestFetch_DefaultPolicy_BehavesAsRequired(t *testing.T) { + tempDir := t.TempDir() + logger := zap.NewNop() + + // Create repository without specifying policy (should default to required behavior) + _, _, err := newRepository(t.Context(), logger, + WithFilesystemStorage(tempDir), + WithRemote("origin", "http://localhost:1/invalid-repo.git")) + require.Error(t, err) +} From d66e557824cf0775d7eecf22dd284827baf1a0c7 Mon Sep 17 00:00:00 2001 From: Roman Dmytrenko Date: Wed, 22 Oct 2025 16:44:11 +0100 Subject: [PATCH 2/5] Update config/flipt.schema.cue Co-authored-by: Mark Phelps <209477+markphelps@users.noreply.github.com> Signed-off-by: Roman Dmytrenko --- config/flipt.schema.cue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/flipt.schema.cue b/config/flipt.schema.cue index 326e236090..6276c83b97 100644 --- a/config/flipt.schema.cue +++ b/config/flipt.schema.cue @@ -200,7 +200,7 @@ JsonPath: string #storage: [string]: { remote?: string - remote_startup_fetch_policy?: *"required" | "optional" + fetch_policy?: *"strict" | "lenient" backend?: { type: *"memory" | "local" path?: string From 508cd3b1ad067671b1f44000cd5afdc9e144c5bf Mon Sep 17 00:00:00 2001 From: Roman Dmytrenko Date: Wed, 22 Oct 2025 16:44:27 +0100 Subject: [PATCH 3/5] Update config/flipt.schema.json Co-authored-by: Mark Phelps <209477+markphelps@users.noreply.github.com> Signed-off-by: Roman Dmytrenko --- config/flipt.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/flipt.schema.json b/config/flipt.schema.json index f7cf5edc81..efc71c6a00 100644 --- a/config/flipt.schema.json +++ b/config/flipt.schema.json @@ -776,7 +776,7 @@ "remote": { "type": "string" }, - "remoteStartupFetchPolicy": { + "fetchPolicy": { "type": "string", "enum": ["required", "optional"] }, From 5642bcdd72df21a84b9504e68251bae40c4a18fa Mon Sep 17 00:00:00 2001 From: Roman Dmytrenko Date: Wed, 22 Oct 2025 16:44:35 +0100 Subject: [PATCH 4/5] Update internal/config/storage.go Co-authored-by: Mark Phelps <209477+markphelps@users.noreply.github.com> Signed-off-by: Roman Dmytrenko --- internal/config/storage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config/storage.go b/internal/config/storage.go index 6d1babbea2..c50f85c6dc 100644 --- a/internal/config/storage.go +++ b/internal/config/storage.go @@ -81,7 +81,7 @@ type StorageBackendConfig struct { Path string `json:"path,omitempty" mapstructure:"path" yaml:"path,omitempty"` } -type RemoteStartupFetchPolicy string +type FetchPolicy string const ( RemoteStartupFetchPolicyRequired = RemoteStartupFetchPolicy("required") From 68141904970151cd69eaa194318ebb7061d90ccf Mon Sep 17 00:00:00 2001 From: Roman Dmytrenko Date: Wed, 22 Oct 2025 17:13:51 +0100 Subject: [PATCH 5/5] renaming Signed-off-by: Roman Dmytrenko --- config/flipt.schema.json | 3 +- internal/config/config_test.go | 36 +++++++-------- internal/config/storage.go | 28 ++++++------ .../storage/git_provided_with_directory.yml | 2 +- internal/storage/environments/environments.go | 6 +-- internal/storage/git/repository.go | 44 +++++++++---------- internal/storage/git/repository_test.go | 44 +++++++++---------- 7 files changed, 82 insertions(+), 81 deletions(-) diff --git a/config/flipt.schema.json b/config/flipt.schema.json index efc71c6a00..33bb57cc58 100644 --- a/config/flipt.schema.json +++ b/config/flipt.schema.json @@ -778,7 +778,8 @@ }, "fetchPolicy": { "type": "string", - "enum": ["required", "optional"] + "enum": ["strict", "lenient"], + "default": "strict" }, "backend": { "type": [ diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 136ca5ed27..80e829f780 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -459,7 +459,7 @@ func TestLoad(t *testing.T) { Type: MemoryStorageBackendType, }, Remote: "https://github.com/flipt-io/flipt.git", - RemoteStartupFetchPolicy: "required", + FetchPolicy: "strict", Branch: "main", PollInterval: 5 * time.Second, Credentials: "git", @@ -578,10 +578,10 @@ func TestLoad(t *testing.T) { Backend: StorageBackendConfig{ Type: MemoryStorageBackendType, }, - Remote: "git@github.com:foo/bar.git", - RemoteStartupFetchPolicy: "required", - Branch: "main", - PollInterval: 30 * time.Second, + Remote: "git@github.com:foo/bar.git", + FetchPolicy: "strict", + Branch: "main", + PollInterval: 30 * time.Second, }, } return cfg @@ -598,10 +598,10 @@ func TestLoad(t *testing.T) { Type: LocalStorageBackendType, Path: "/path/to/gitdir", }, - Remote: "git@github.com:foo/bar.git", - RemoteStartupFetchPolicy: "optional", - Branch: "main", - PollInterval: 30 * time.Second, + Remote: "git@github.com:foo/bar.git", + FetchPolicy: "lenient", + Branch: "main", + PollInterval: 30 * time.Second, }, } return cfg @@ -637,11 +637,11 @@ func TestLoad(t *testing.T) { Backend: StorageBackendConfig{ Type: MemoryStorageBackendType, }, - Remote: "git@github.com:foo/bar.git", - RemoteStartupFetchPolicy: "required", - Branch: "main", - PollInterval: 30 * time.Second, - Credentials: "git", + Remote: "git@github.com:foo/bar.git", + FetchPolicy: "strict", + Branch: "main", + PollInterval: 30 * time.Second, + Credentials: "git", }, } cfg.Credentials = CredentialsConfig{ @@ -892,10 +892,10 @@ func TestLoad(t *testing.T) { Backend: StorageBackendConfig{ Type: MemoryStorageBackendType, }, - Remote: "git@github.com:foo/bar.git", - RemoteStartupFetchPolicy: "required", - Branch: "main", - PollInterval: 30 * time.Second, + Remote: "git@github.com:foo/bar.git", + FetchPolicy: "strict", + Branch: "main", + PollInterval: 30 * time.Second, Signature: SignatureConfig{ Name: "Flipt Bot", Email: "bot@flipt.io", diff --git a/internal/config/storage.go b/internal/config/storage.go index c50f85c6dc..1673bb50f2 100644 --- a/internal/config/storage.go +++ b/internal/config/storage.go @@ -61,8 +61,8 @@ func (s *StoragesConfig) setDefaults(v *viper.Viper) error { setDefault("poll_interval", "30s") } - if getString("remote") != "" && getString("remote_startup_fetch_policy") == "" { - setDefault("remote_startup_fetch_policy", RemoteStartupFetchPolicyRequired) + if getString("remote") != "" && getString("fetch_policy") == "" { + setDefault("fetch_policy", FetchPolicyStrict) } } @@ -84,23 +84,23 @@ type StorageBackendConfig struct { type FetchPolicy string const ( - RemoteStartupFetchPolicyRequired = RemoteStartupFetchPolicy("required") - RemoteStartupFetchPolicyOptional = RemoteStartupFetchPolicy("optional") + FetchPolicyStrict = FetchPolicy("strict") + FetchPolicyLenient = FetchPolicy("lenient") ) // StorageConfig contains fields which will configure the type of backend in which Flipt will serve // flag state. type StorageConfig struct { - Remote string `json:"remote,omitempty" mapstructure:"remote" yaml:"remote,omitempty"` - RemoteStartupFetchPolicy RemoteStartupFetchPolicy `json:"remote_startup_fetch_policy,omitempty" mapstructure:"remote_startup_fetch_policy" yaml:"remote_startup_fetch_policy,omitempty"` - Backend StorageBackendConfig `json:"backend,omitempty" mapstructure:"backend" yaml:"backend,omitempty"` - Branch string `json:"branch,omitempty" mapstructure:"branch" yaml:"branch,omitempty"` - CaCertBytes string `json:"-" mapstructure:"ca_cert_bytes" yaml:"-"` - CaCertPath string `json:"-" mapstructure:"ca_cert_path" yaml:"-"` - PollInterval time.Duration `json:"pollInterval,omitempty" mapstructure:"poll_interval" yaml:"poll_interval,omitempty"` - InsecureSkipTLS bool `json:"-" mapstructure:"insecure_skip_tls" yaml:"-"` - Credentials string `json:"-" mapstructure:"credentials" yaml:"-"` - Signature SignatureConfig `json:"signature,omitempty" mapstructure:"signature,omitempty" yaml:"signature,omitempty"` + Remote string `json:"remote,omitempty" mapstructure:"remote" yaml:"remote,omitempty"` + FetchPolicy FetchPolicy `json:"fetch_policy,omitempty" mapstructure:"fetch_policy" yaml:"fetch_policy,omitempty"` + Backend StorageBackendConfig `json:"backend,omitempty" mapstructure:"backend" yaml:"backend,omitempty"` + Branch string `json:"branch,omitempty" mapstructure:"branch" yaml:"branch,omitempty"` + CaCertBytes string `json:"-" mapstructure:"ca_cert_bytes" yaml:"-"` + CaCertPath string `json:"-" mapstructure:"ca_cert_path" yaml:"-"` + PollInterval time.Duration `json:"pollInterval,omitempty" mapstructure:"poll_interval" yaml:"poll_interval,omitempty"` + InsecureSkipTLS bool `json:"-" mapstructure:"insecure_skip_tls" yaml:"-"` + Credentials string `json:"-" mapstructure:"credentials" yaml:"-"` + Signature SignatureConfig `json:"signature,omitempty" mapstructure:"signature,omitempty" yaml:"signature,omitempty"` } func (c *StorageConfig) validate() error { diff --git a/internal/config/testdata/storage/git_provided_with_directory.yml b/internal/config/testdata/storage/git_provided_with_directory.yml index 4cd10675e0..5d1e255f36 100644 --- a/internal/config/testdata/storage/git_provided_with_directory.yml +++ b/internal/config/testdata/storage/git_provided_with_directory.yml @@ -1,7 +1,7 @@ storage: default: remote: "git@github.com:foo/bar.git" - remote_startup_fetch_policy: optional + fetch_policy: lenient backend: type: "local" path: "/path/to/gitdir" diff --git a/internal/storage/environments/environments.go b/internal/storage/environments/environments.go index 02cd33a963..f496d0f492 100644 --- a/internal/storage/environments/environments.go +++ b/internal/storage/environments/environments.go @@ -147,8 +147,8 @@ func (rm *RepositoryManager) GetOrCreate(ctx context.Context, envConf *config.En zap.String("key_id", storage.Signature.KeyID)) } } - if storage.RemoteStartupFetchPolicy == config.RemoteStartupFetchPolicyOptional { - opts = append(opts, storagegit.WithOptionalRemoteStartupFetchPolicy()) + if storage.FetchPolicy == config.FetchPolicyLenient { + opts = append(opts, storagegit.WithLenientFetchPolicy()) } newRepo, err := storagegit.NewRepository(ctx, logger, opts...) @@ -516,7 +516,7 @@ func NewStore(ctx context.Context, logger *zap.Logger, cfg *config.Config, secre for _, repo := range repoManager.repos { if err := repo.Fetch(ctx); err != nil { switch { - case repo.IsRemoteStartupFetchOptional() && repo.IsConnectionRefused(err): + case repo.HasLenientFetchPolicy() && repo.IsConnectionRefused(err): continue case errors.Is(err, transport.ErrEmptyRemoteRepository) || errors.Is(err, git.ErrRemoteRefNotFound): continue diff --git a/internal/storage/git/repository.go b/internal/storage/git/repository.go index f6375c31fe..799b086cdf 100644 --- a/internal/storage/git/repository.go +++ b/internal/storage/git/repository.go @@ -34,20 +34,20 @@ type Repository struct { logger *zap.Logger - mu sync.RWMutex - remote *config.RemoteConfig - optionalRemoteStartupFetch bool - defaultBranch string - auth transport.AuthMethod - insecureSkipTLS bool - caBundle []byte - localPath string - readme []byte - sigName string - sigEmail string - signer signing.Signer - maxOpenDescriptors int - isNormalRepo bool // true if opened with PlainOpen, false if bare repository + mu sync.RWMutex + remote *config.RemoteConfig + lenientFetchPolicyEnabled bool + defaultBranch string + auth transport.AuthMethod + insecureSkipTLS bool + caBundle []byte + localPath string + readme []byte + sigName string + sigEmail string + signer signing.Signer + maxOpenDescriptors int + isNormalRepo bool // true if opened with PlainOpen, false if bare repository subs []Subscriber @@ -202,8 +202,8 @@ func newRepository(ctx context.Context, logger *zap.Logger, opts ...containers.O if err := r.Fetch(ctx); err != nil { fetchErr := fmt.Errorf("performing initial fetch: %w", err) switch { - case r.optionalRemoteStartupFetch && r.IsConnectionRefused(err): - // if optional, we check if the error is connection refused + case r.HasLenientFetchPolicy() && r.IsConnectionRefused(err): + // if lenient, we check if the error is connection refused // and there is non-empty repo and flags could be evaluated objs, rerr := r.CommitObjects() if rerr != nil { @@ -317,9 +317,9 @@ func (r *Repository) IsConnectionRefused(err error) bool { errors.Is(err, syscall.ENETUNREACH) || errors.Is(err, syscall.EHOSTDOWN) } -// IsRemoteStartupFetchOptional returns true if the startup fetch is optional. -func (r *Repository) IsRemoteStartupFetchOptional() bool { - return r.optionalRemoteStartupFetch +// HasLenientFetchPolicy returns true if the fetch policy set to lenient. +func (r *Repository) HasLenientFetchPolicy() bool { + return r.lenientFetchPolicyEnabled } // Fetch does a fetch for the requested head names on a configured remote. @@ -856,10 +856,10 @@ func WithMaxOpenDescriptors(n int) containers.Option[Repository] { } } -// WithOptionalRemoteStartupFetchPolicy sets the optional remote startup fetch policy. -func WithOptionalRemoteStartupFetchPolicy() containers.Option[Repository] { +// WithLenientFetchPolicy sets the lenient fetch policy which allows skip startup git fetch. +func WithLenientFetchPolicy() containers.Option[Repository] { return func(r *Repository) { - r.optionalRemoteStartupFetch = true + r.lenientFetchPolicyEnabled = true } } diff --git a/internal/storage/git/repository_test.go b/internal/storage/git/repository_test.go index 87d8166597..8aa0e8620f 100644 --- a/internal/storage/git/repository_test.go +++ b/internal/storage/git/repository_test.go @@ -301,32 +301,32 @@ func TestRepositoryWithCustomBranch(t *testing.T) { assert.Equal(t, "develop", repo.defaultBranch, "should use custom default branch") } -func TestRemoteStartupPolicy_Required(t *testing.T) { +func TestFetchPolicy_Strict(t *testing.T) { tempDir := t.TempDir() logger := zap.NewNop() - // Create a repository with required fetch policy (default) + // Create a repository with strict fetch policy (default) repo, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir)) require.NoError(t, err) require.NotNil(t, repo) - assert.False(t, repo.optionalRemoteStartupFetch, "should set fetch policy to required") + assert.False(t, repo.lenientFetchPolicyEnabled, "should set fetch policy to strict") } -func TestRemoteStartupPolicy_Optional(t *testing.T) { +func TestFetchPolicy_Lenient(t *testing.T) { tempDir := t.TempDir() logger := zap.NewNop() - // Create a repository with optional fetch policy + // Create a repository with lenient fetch policy repo, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir), - WithOptionalRemoteStartupFetchPolicy(), + WithLenientFetchPolicy(), ) require.NoError(t, err) require.NotNil(t, repo) - assert.True(t, repo.optionalRemoteStartupFetch, "should set fetch policy to optional") + assert.True(t, repo.lenientFetchPolicyEnabled, "should set fetch policy to lenient") } func TestIsConnectionRefused(t *testing.T) { @@ -391,11 +391,11 @@ func TestFetch_NoRemote(t *testing.T) { assert.NoError(t, err, "fetch without remote should succeed silently") } -func TestFetch_RequiredPolicy_WithConnectionRefused(t *testing.T) { +func TestFetch_StrictPolicy_WithConnectionRefused(t *testing.T) { tempDir := t.TempDir() logger := zap.NewNop() - // Create repository with required fetch policy and invalid remote + // Create repository with strict fetch policy and invalid remote _, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir), WithRemote("origin", "http://localhost:1/invalid-repo.git"), @@ -403,7 +403,7 @@ func TestFetch_RequiredPolicy_WithConnectionRefused(t *testing.T) { require.Error(t, err) } -func TestFetch_OptionalPolicy_WithConnectionRefusedAndCommits(t *testing.T) { +func TestFetch_LenientPolicy_WithConnectionRefusedAndCommits(t *testing.T) { tempDir := t.TempDir() logger := zap.NewNop() @@ -430,50 +430,50 @@ func TestFetch_OptionalPolicy_WithConnectionRefusedAndCommits(t *testing.T) { }) require.NoError(t, err) - // Now create our Repository wrapper with optional policy and invalid remote + // Now create our Repository wrapper with lenient policy and invalid remote repo, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir), WithRemote("origin", "http://localhost:1/invalid-repo.git"), - WithOptionalRemoteStartupFetchPolicy()) + WithLenientFetchPolicy()) require.NoError(t, err) - assert.True(t, repo.optionalRemoteStartupFetch) - assert.True(t, repo.IsRemoteStartupFetchOptional()) + assert.True(t, repo.lenientFetchPolicyEnabled) + assert.True(t, repo.HasLenientFetchPolicy()) err = repo.Fetch(t.Context()) assert.Error(t, err) } -func TestFetch_OptionalPolicy_WithConnectionRefusedAndNoCommits(t *testing.T) { +func TestFetch_LenientPolicy_WithConnectionRefusedAndNoCommits(t *testing.T) { tempDir := t.TempDir() logger := zap.NewNop() - // Create repository with optional fetch policy and invalid remote, but no commits + // Create repository with lenient fetch policy and invalid remote, but no commits repo, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir), WithRemote("origin", "http://localhost:1/invalid-repo.git"), - WithOptionalRemoteStartupFetchPolicy()) + WithLenientFetchPolicy()) require.Error(t, err) assert.Nil(t, repo) } -func TestFetch_OptionalPolicy_WithNonConnectionError(t *testing.T) { +func TestFetch_LenientPolicy_WithNonConnectionError(t *testing.T) { tempDir := t.TempDir() logger := zap.NewNop() - // Create repository with optional fetch policy but a different type of error (not connection refused) + // Create repository with lenient fetch policy but a different type of error (not connection refused) // Using an invalid URL format to trigger a different error _, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir), WithRemote("origin", "invalid://bad-url"), - WithOptionalRemoteStartupFetchPolicy()) + WithLenientFetchPolicy()) require.Error(t, err) } -func TestFetch_DefaultPolicy_BehavesAsRequired(t *testing.T) { +func TestFetch_DefaultPolicy_BehavesAsStrict(t *testing.T) { tempDir := t.TempDir() logger := zap.NewNop() - // Create repository without specifying policy (should default to required behavior) + // Create repository without specifying policy (should default to strict behavior) _, _, err := newRepository(t.Context(), logger, WithFilesystemStorage(tempDir), WithRemote("origin", "http://localhost:1/invalid-repo.git"))