Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fs/git): add support for cloning target repository to the local filesystem #3102

Merged
merged 4 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
36 changes: 36 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,9 @@ func TestLoad(t *testing.T) {
cfg.Storage = StorageConfig{
Type: GitStorageType,
Git: &Git{
Backend: GitBackend{
Type: GotBackendMemory,
},
Repository: "https://github.com/flipt-io/flipt.git",
Ref: "production",
RefType: GitRefTypeStatic,
Expand Down Expand Up @@ -817,6 +820,9 @@ func TestLoad(t *testing.T) {
cfg.Storage = StorageConfig{
Type: GitStorageType,
Git: &Git{
Backend: GitBackend{
Type: GotBackendMemory,
},
Ref: "main",
RefType: GitRefTypeStatic,
Repository: "[email protected]:foo/bar.git",
Expand All @@ -834,6 +840,9 @@ func TestLoad(t *testing.T) {
cfg.Storage = StorageConfig{
Type: GitStorageType,
Git: &Git{
Backend: GitBackend{
Type: GotBackendMemory,
},
Ref: "main",
RefType: GitRefTypeStatic,
Repository: "[email protected]:foo/bar.git",
Expand All @@ -852,6 +861,9 @@ func TestLoad(t *testing.T) {
cfg.Storage = StorageConfig{
Type: GitStorageType,
Git: &Git{
Backend: GitBackend{
Type: GotBackendMemory,
},
Ref: "main",
RefType: GitRefTypeSemver,
Repository: "[email protected]:foo/bar.git",
Expand All @@ -862,6 +874,27 @@ func TestLoad(t *testing.T) {
return cfg
},
},
{
name: "git config provided with ref_type",
path: "./testdata/storage/git_provided_with_backend_type.yml",
expected: func() *Config {
cfg := Default()
cfg.Storage = StorageConfig{
Type: GitStorageType,
Git: &Git{
Backend: GitBackend{
Type: GitBackendFilesystem,
Path: "/path/to/gitdir",
},
Ref: "main",
RefType: GitRefTypeStatic,
Repository: "[email protected]:foo/bar.git",
PollInterval: 30 * time.Second,
},
}
return cfg
},
},
{
name: "git repository not provided",
path: "./testdata/storage/invalid_git_repo_not_specified.yml",
Expand Down Expand Up @@ -900,6 +933,9 @@ func TestLoad(t *testing.T) {
cfg.Storage = StorageConfig{
Type: GitStorageType,
Git: &Git{
Backend: GitBackend{
Type: GotBackendMemory,
},
Ref: "main",
RefType: GitRefTypeStatic,
Repository: "[email protected]:foo/bar.git",
Expand Down
18 changes: 16 additions & 2 deletions internal/config/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func (c *StorageConfig) setDefaults(v *viper.Viper) error {
case string(LocalStorageType):
v.SetDefault("storage.local.path", ".")
case string(GitStorageType):
v.SetDefault("storage.git.backend.type", "memory")
v.SetDefault("storage.git.ref", "main")
v.SetDefault("storage.git.ref_type", "static")
v.SetDefault("storage.git.poll_interval", "30s")
Expand Down Expand Up @@ -164,11 +165,12 @@ const (
// Git contains configuration for referencing a git repository.
type Git struct {
Repository string `json:"repository,omitempty" mapstructure:"repository" yaml:"repository,omitempty"`
Backend GitBackend `json:"backend,omitempty" mapstructure:"backend" yaml:"backend,omitempty"`
Ref string `json:"ref,omitempty" mapstructure:"ref" yaml:"ref,omitempty"`
RefType GitRefType `json:"refType,omitempty" mapstructure:"ref_type" yaml:"ref_type,omitempty"`
Directory string `json:"directory,omitempty" mapstructure:"directory" yaml:"directory,omitempty"`
CaCertBytes string `json:"-" mapstructure:"ca_cert_bytes" yaml:"-" `
CaCertPath string `json:"-" mapstructure:"ca_cert_path" yaml:"-" `
CaCertBytes string `json:"-" mapstructure:"ca_cert_bytes" yaml:"-"`
CaCertPath string `json:"-" mapstructure:"ca_cert_path" yaml:"-"`
InsecureSkipTLS bool `json:"-" mapstructure:"insecure_skip_tls" yaml:"-"`
PollInterval time.Duration `json:"pollInterval,omitempty" mapstructure:"poll_interval" yaml:"poll_interval,omitempty"`
Authentication Authentication `json:"-" mapstructure:"authentication,omitempty" yaml:"-"`
Expand All @@ -186,6 +188,18 @@ func (g *Git) validate() error {
return nil
}

type GitBackendType string

const (
GotBackendMemory = GitBackendType("memory")
GeorgeMac marked this conversation as resolved.
Show resolved Hide resolved
GitBackendFilesystem = GitBackendType("filesystem")
)

type GitBackend struct {
Type GitBackendType `json:"type,omitempty" mapstructure:"type" yaml:"type,omitempty"`
Path string `json:"path,omitempty" mapstructure:"path" yaml:"path,omitempty"`
GeorgeMac marked this conversation as resolved.
Show resolved Hide resolved
}

// Object contains configuration of readonly object storage.
type Object struct {
Type ObjectSubStorageType `json:"type,omitempty" mapstructure:"type" yaml:"type,omitempty"`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
storage:
type: git
git:
repository: "[email protected]:foo/bar.git"
backend:
type: "filesystem"
path: "/path/to/gitdir"
20 changes: 18 additions & 2 deletions internal/storage/fs/git/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import (
"slices"
"sync"

"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/transport"
gitstorage "github.com/go-git/go-git/v5/storage"
"github.com/go-git/go-git/v5/storage/filesystem"
"github.com/go-git/go-git/v5/storage/memory"
"go.flipt.io/flipt/internal/containers"
"go.flipt.io/flipt/internal/gitfs"
Expand All @@ -35,6 +39,7 @@ type SnapshotStore struct {
*storagefs.Poller

logger *zap.Logger
storage gitstorage.Storer
url string
baseRef string
refTypeTag bool
Expand Down Expand Up @@ -110,12 +115,23 @@ func WithDirectory(directory string) containers.Option[SnapshotStore] {
}
}

// WithFilesystemStorage configures the Git repository to clone into
// the local filesystem, instead of the default which is in-memory.
// The provided path is location for the dotgit folder.
func WithFilesystemStorage(path string) containers.Option[SnapshotStore] {
return func(ss *SnapshotStore) {
fs := osfs.New(path)
ss.storage = filesystem.NewStorage(fs, cache.NewObjectLRUDefault())
}
}

// NewSnapshotStore constructs and configures a Store.
// The store uses the connection and credential details provided to build
// fs.FS implementations around a target git repository.
func NewSnapshotStore(ctx context.Context, logger *zap.Logger, url string, opts ...containers.Option[SnapshotStore]) (_ *SnapshotStore, err error) {
store := &SnapshotStore{
logger: logger.With(zap.String("repository", url)),
storage: memory.NewStorage(),
url: url,
baseRef: "main",
referenceResolver: staticResolver(),
Expand Down Expand Up @@ -147,7 +163,7 @@ func NewSnapshotStore(ctx context.Context, logger *zap.Logger, url string, opts
cloneOpts.SingleBranch = true
}

store.repo, err = git.Clone(memory.NewStorage(), nil, cloneOpts)
store.repo, err = git.Clone(store.storage, nil, cloneOpts)
if err != nil {
return nil, fmt.Errorf("performing initial clone: %w", err)
}
Expand All @@ -158,7 +174,7 @@ func NewSnapshotStore(ctx context.Context, logger *zap.Logger, url string, opts
}
} else {
// fetch single reference
store.repo, err = git.InitWithOptions(memory.NewStorage(), nil, git.InitOptions{
store.repo, err = git.InitWithOptions(store.storage, nil, git.InitOptions{
DefaultBranch: plumbing.Main,
})
if err != nil {
Expand Down
80 changes: 80 additions & 0 deletions internal/storage/fs/git/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,86 @@ flags:
}))
}

func Test_Store_View_WithFilesystemStorage(t *testing.T) {
ch := make(chan struct{})
store, skip := testStore(t, gitRepoURL,
WithFilesystemStorage(t.TempDir()),
WithPollOptions(
fs.WithInterval(time.Second),
fs.WithNotify(t, func(modified bool) {
if modified {
close(ch)
}
}),
))
if skip {
return
}

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

// pull repo
workdir := memfs.New()
repo, err := git.Clone(memory.NewStorage(), workdir, &git.CloneOptions{
Auth: &http.BasicAuth{Username: "root", Password: "password"},
URL: gitRepoURL,
RemoteName: "origin",
ReferenceName: plumbing.NewBranchReferenceName("main"),
})
require.NoError(t, err)

tree, err := repo.Worktree()
require.NoError(t, err)

require.NoError(t, tree.Checkout(&git.CheckoutOptions{
Branch: "refs/heads/main",
}))

// update features.yml
fi, err := workdir.OpenFile("features.yml", os.O_TRUNC|os.O_RDWR, os.ModePerm)
require.NoError(t, err)

updated := []byte(`namespace: production
flags:
- key: foo
name: Foo`)

_, err = fi.Write(updated)
require.NoError(t, err)
require.NoError(t, fi.Close())

// commit changes
_, err = tree.Commit("chore: update features.yml", &git.CommitOptions{
All: true,
Author: &object.Signature{Email: "[email protected]", Name: "dev"},
})
require.NoError(t, err)

// push new commit
require.NoError(t, repo.Push(&git.PushOptions{
Auth: &http.BasicAuth{Username: "root", Password: "password"},
RemoteName: "origin",
}))

// wait until the snapshot is updated or
// we timeout
select {
case <-ch:
case <-time.After(time.Minute):
t.Fatal("timed out waiting for snapshot")
}

require.NoError(t, err)

t.Log("received new snapshot")

require.NoError(t, store.View(ctx, "", func(s storage.ReadOnlyStore) error {
_, err = s.GetFlag(ctx, storage.NewResource("production", "foo"))
return err
}))
}

func Test_Store_View_WithRevision(t *testing.T) {
ch := make(chan struct{})
store, skip := testStore(t, gitRepoURL, WithPollOptions(
Expand Down
31 changes: 24 additions & 7 deletions internal/storage/fs/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
func NewStore(ctx context.Context, logger *zap.Logger, cfg *config.Config) (_ storage.Store, err error) {
switch cfg.Storage.Type {
case config.GitStorageType:
storage := cfg.Storage.Git
opts := []containers.Option[git.SnapshotStore]{
git.WithRef(cfg.Storage.Git.Ref),
git.WithPollOptions(
Expand All @@ -41,21 +42,37 @@ func NewStore(ctx context.Context, logger *zap.Logger, cfg *config.Config) (_ st
git.WithDirectory(cfg.Storage.Git.Directory),
}

if cfg.Storage.Git.RefType == config.GitRefTypeSemver {
if storage.RefType == config.GitRefTypeSemver {
opts = append(opts, git.WithSemverResolver())
}

if cfg.Storage.Git.CaCertBytes != "" {
opts = append(opts, git.WithCABundle([]byte(cfg.Storage.Git.CaCertBytes)))
} else if cfg.Storage.Git.CaCertPath != "" {
if bytes, err := os.ReadFile(cfg.Storage.Git.CaCertPath); err == nil {
switch storage.Backend.Type {
case config.GitBackendFilesystem:
path := storage.Backend.Path
if path == "" {
path, err = os.MkdirTemp(os.TempDir(), "flipt-git-*")
if err != nil {
return nil, fmt.Errorf("making tempory directory for git storage: %w", err)
}
}

opts = append(opts, git.WithFilesystemStorage(path))
logger = logger.With(zap.String("git_storage_type", "filesystem"), zap.String("git_storage_path", path))
case config.GotBackendMemory:
logger = logger.With(zap.String("git_storage_type", "memory"))
}

if storage.CaCertBytes != "" {
opts = append(opts, git.WithCABundle([]byte(storage.CaCertBytes)))
} else if storage.CaCertPath != "" {
if bytes, err := os.ReadFile(storage.CaCertPath); err == nil {
opts = append(opts, git.WithCABundle(bytes))
} else {
return nil, err
}
}

auth := cfg.Storage.Git.Authentication
auth := storage.Authentication
switch {
case auth.BasicAuth != nil:
opts = append(opts, git.WithAuth(&http.BasicAuth{
Expand Down Expand Up @@ -95,7 +112,7 @@ func NewStore(ctx context.Context, logger *zap.Logger, cfg *config.Config) (_ st
opts = append(opts, git.WithAuth(method))
}

snapStore, err := git.NewSnapshotStore(ctx, logger, cfg.Storage.Git.Repository, opts...)
snapStore, err := git.NewSnapshotStore(ctx, logger, storage.Repository, opts...)
if err != nil {
return nil, err
}
Expand Down
Loading