Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/flipt.schema.cue
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ JsonPath: string

#storage: [string]: {
remote?: string
fetch_policy?: *"strict" | "lenient"
backend?: {
type: *"memory" | "local"
path?: string
Expand Down
5 changes: 5 additions & 0 deletions config/flipt.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,11 @@
"remote": {
"type": "string"
},
"fetchPolicy": {
"type": "string",
"enum": ["strict", "lenient"],
"default": "strict"
},
"backend": {
"type": [
"object",
Expand Down
11 changes: 8 additions & 3 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ func TestLoad(t *testing.T) {
Type: MemoryStorageBackendType,
},
Remote: "https://github.com/flipt-io/flipt.git",
FetchPolicy: "strict",
Branch: "main",
PollInterval: 5 * time.Second,
Credentials: "git",
Expand Down Expand Up @@ -578,6 +579,7 @@ func TestLoad(t *testing.T) {
Type: MemoryStorageBackendType,
},
Remote: "git@github.com:foo/bar.git",
FetchPolicy: "strict",
Branch: "main",
PollInterval: 30 * time.Second,
},
Expand All @@ -597,6 +599,7 @@ func TestLoad(t *testing.T) {
Path: "/path/to/gitdir",
},
Remote: "git@github.com:foo/bar.git",
FetchPolicy: "lenient",
Branch: "main",
PollInterval: 30 * time.Second,
},
Expand Down Expand Up @@ -635,6 +638,7 @@ func TestLoad(t *testing.T) {
Type: MemoryStorageBackendType,
},
Remote: "git@github.com:foo/bar.git",
FetchPolicy: "strict",
Branch: "main",
PollInterval: 30 * time.Second,
Credentials: "git",
Expand Down Expand Up @@ -889,6 +893,7 @@ func TestLoad(t *testing.T) {
Type: MemoryStorageBackendType,
},
Remote: "git@github.com:foo/bar.git",
FetchPolicy: "strict",
Branch: "main",
PollInterval: 30 * time.Second,
Signature: SignatureConfig{
Expand Down Expand Up @@ -1141,7 +1146,7 @@ OUTER:
}
}

return
return vals
}

func TestMarshalYAML(t *testing.T) {
Expand Down Expand Up @@ -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
},
Expand All @@ -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
},
Expand Down
12 changes: 12 additions & 0 deletions internal/config/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func (s *StoragesConfig) setDefaults(v *viper.Viper) error {
if getString("poll_interval") == "" {
setDefault("poll_interval", "30s")
}

if getString("remote") != "" && getString("fetch_policy") == "" {
setDefault("fetch_policy", FetchPolicyStrict)
}
}

return nil
Expand All @@ -77,10 +81,18 @@ type StorageBackendConfig struct {
Path string `json:"path,omitempty" mapstructure:"path" yaml:"path,omitempty"`
}

type FetchPolicy string

const (
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"`
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:"-"`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
storage:
default:
remote: "git@github.com:foo/bar.git"
fetch_policy: lenient
backend:
type: "local"
path: "/path/to/gitdir"
10 changes: 9 additions & 1 deletion internal/storage/environments/environments.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ func (rm *RepositoryManager) GetOrCreate(ctx context.Context, envConf *config.En
zap.String("key_id", storage.Signature.KeyID))
}
}
if storage.FetchPolicy == config.FetchPolicyLenient {
opts = append(opts, storagegit.WithLenientFetchPolicy())
}

newRepo, err := storagegit.NewRepository(ctx, logger, opts...)
if err != nil {
Expand Down Expand Up @@ -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.HasLenientFetchPolicy() && repo.IsConnectionRefused(err):
continue
case errors.Is(err, transport.ErrEmptyRemoteRepository) || errors.Is(err, git.ErrRemoteRefNotFound):
continue
default:
return nil, err
}
}
Expand Down
77 changes: 55 additions & 22 deletions internal/storage/git/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"slices"
"strings"
"sync"
"syscall"
"time"

"github.com/go-git/go-billy/v6/osfs"
Expand All @@ -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
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

Expand All @@ -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
}
Expand Down Expand Up @@ -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.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 {
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))
}
}

Expand Down Expand Up @@ -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)
}

// 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.
// 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.
Expand Down Expand Up @@ -830,6 +856,13 @@ func WithMaxOpenDescriptors(n int) 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.lenientFetchPolicyEnabled = true
}
}

const defaultReadmeContents = `Flipt Configuration Repository
==============================

Expand Down
Loading
Loading