Skip to content

Commit

Permalink
feat: support mutations to git source
Browse files Browse the repository at this point in the history
  • Loading branch information
GeorgeMac committed Jun 27, 2023
1 parent 36826e0 commit 33270b6
Show file tree
Hide file tree
Showing 10 changed files with 447 additions and 133 deletions.
52 changes: 48 additions & 4 deletions cmd/fidgit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,29 @@ package main

import (
"context"
"flag"
"net/http"
"net/url"
"os"

"github.com/go-git/go-git/v5/plumbing/transport"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"go.flipt.io/fidgit"
"go.flipt.io/fidgit/collections/flipt"
"go.flipt.io/fidgit/internal/source/git"
"go.flipt.io/fidgit/internal/source/local"
"golang.org/x/exp/slog"
)

var (
sourceType = flag.String("source", "local", "source type (local|git)")
gitRepo = flag.String("repository", "", "target upstream repository")
authBasic = flag.String("auth-basic", "", "basic authentication in the form username:password")
)

func main() {
flag.Parse()

logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
Expand All @@ -20,15 +33,46 @@ func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

manager := fidgit.NewService(local.New(ctx, "."))
var source fidgit.Source
switch *sourceType {
case "local":
source = local.New(ctx, ".")
case "git":
url, err := url.Parse(*gitRepo)
if err != nil {
logger.Error("Parsing Git URL", slog.String("url", *gitRepo), "error", err)
}

var auth transport.AuthMethod
if url.User.Username() != "" {
password, _ := url.User.Password()
auth = &githttp.BasicAuth{
Username: url.User.Username(),
Password: password,
}
}

// strip basic auth creds once they're configured
// via the transport auth
url.User = nil

source, err = git.NewSource(ctx, url.String(), git.WithAuth(auth))
if err != nil {
logger.Error("Building Git Source", "error", err)
os.Exit(1)
}
default:
logger.Error("Source Unknown", slog.String("source", *sourceType))

}

collection, err := fidgit.CollectionFor[flipt.Flag](context.Background(), &flipt.FlagCollectionFactory{})
manager, err := fidgit.NewService(source)
if err != nil {
slog.Error("Building Collection", "error", err)
slog.Error("Building Manager", "error", err)
os.Exit(1)
}

manager.RegisterCollection(collection)
manager.RegisterFactory(fidgit.FactoryFor[flipt.Flag](&flipt.FlagCollectionFactory{}))

manager.Start(context.Background())

Expand Down
94 changes: 46 additions & 48 deletions collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"fmt"
"io/fs"
"path"
"sync"

"golang.org/x/exp/slog"
)
Expand Down Expand Up @@ -35,10 +34,16 @@ func (t Type) String() string {
return path.Join(t.Group, t.Kind, t.Version)
}

type RuntimePutRequest[I Item] struct {
Namespace Namespace
Item *I
Revision *string
}

type Runtime[I Item] interface {
ListAll(context.Context) ([]*I, error)
Put(context.Context, Namespace, *I) ([]File, error)
Delete(context.Context, Namespace, ID) ([]File, error)
Put(context.Context, RuntimePutRequest[I]) ([]Change, error)
Delete(context.Context, Namespace, ID) ([]Change, error)
}

type namespace struct {
Expand All @@ -51,11 +56,9 @@ type Collection struct {
tagKeys []string
logger *slog.Logger

mu sync.RWMutex
updateSnapshot func(context.Context, fs.FS) error
index map[Namespace]*namespace
put func(context.Context, Namespace, []byte) ([]File, error)
del func(context.Context, Namespace, ID) ([]File, error)
index map[Namespace]*namespace
put func(context.Context, CollectionPutRequest) ([]Change, error)
del func(context.Context, Namespace, ID) ([]Change, error)
}

type RuntimeFactory[I Item] interface {
Expand All @@ -64,34 +67,36 @@ type RuntimeFactory[I Item] interface {
CollectionFor(context.Context, fs.FS) (Runtime[I], error)
}

func CollectionFor[I Item](ctx context.Context, f RuntimeFactory[I]) (*Collection, error) {
collection := Collection{
typ: f.GetType(),
tagKeys: f.GetTagKeys(),
logger: slog.With(
slog.String("system", "collection"),
slog.String("group", f.GetType().Group),
slog.String("kind", f.GetType().Kind),
slog.String("version", f.GetType().Version),
),
}
type FactoryFunc func(context.Context, fs.FS) (*Collection, error)

func FactoryFor[I Item](f RuntimeFactory[I]) (Type, FactoryFunc) {
return f.GetType(), func(ctx context.Context, ffs fs.FS) (*Collection, error) {
collection := Collection{
typ: f.GetType(),
tagKeys: f.GetTagKeys(),
logger: slog.With(
slog.String("system", "collection"),
slog.String("group", f.GetType().Group),
slog.String("kind", f.GetType().Kind),
slog.String("version", f.GetType().Version),
),
}

collection.updateSnapshot = func(ctx context.Context, ffs fs.FS) error {
r, err := f.CollectionFor(ctx, ffs)
if err != nil {
return err
return nil, err
}

all, err := r.ListAll(ctx)
if err != nil {
return err
return nil, err
}

index := map[Namespace]*namespace{}
for _, item := range all {
raw, err := json.Marshal(item)
if err != nil {
return err
return nil, err
}

ns, ok := index[(*item).GetNamespace()]
Expand All @@ -106,40 +111,36 @@ func CollectionFor[I Item](ctx context.Context, f RuntimeFactory[I]) (*Collectio
ns.index[(*item).GetID()] = raw
}

collection.mu.Lock()
defer collection.mu.Unlock()

collection.index = index
collection.del = r.Delete
collection.put = func(ctx context.Context, n Namespace, b []byte) ([]File, error) {
collection.put = func(ctx context.Context, req CollectionPutRequest) ([]Change, error) {
var i I
if err := json.Unmarshal(b, &i); err != nil {
if err := json.Unmarshal(req.Payload, &i); err != nil {
return nil, fmt.Errorf("putting item: %w", err)
}

collection.logger.Debug("Put",
slog.String("namespace", string(i.GetNamespace())),
slog.String("namespace", string(req.Namespace)),
slog.String("id", string(i.GetID())))

changes, err := r.Put(ctx, n, &i)
changes, err := r.Put(ctx, RuntimePutRequest[I]{
Namespace: req.Namespace,
Item: &i,
Revision: req.Revision,
})
if err != nil {
n, id := i.GetNamespace(), i.GetID()
n, id := req.Namespace, i.GetID()
return nil, fmt.Errorf("%s: item %s/%s: %w", f.GetType(), n, id, err)
}

return changes, nil
}

return nil
return &collection, nil
}

return &collection, nil
}

func (c *Collection) Get(ctx context.Context, n Namespace, id ID) ([]byte, error) {
c.mu.RLock()
defer c.mu.RUnlock()

c.logger.Debug("Get",
slog.String("namespace", string(n)),
slog.String("id", string(id)))
Expand All @@ -154,9 +155,6 @@ func (c *Collection) Get(ctx context.Context, n Namespace, id ID) ([]byte, error
}

func (c *Collection) List(ctx context.Context, n Namespace) ([]byte, error) {
c.mu.RLock()
defer c.mu.RUnlock()

c.logger.Debug("List",
slog.String("namespace", string(n)))

Expand All @@ -167,17 +165,17 @@ func (c *Collection) List(ctx context.Context, n Namespace) ([]byte, error) {
return nil, fmt.Errorf("%s: namespace %s: not found", c.typ, n)
}

func (c *Collection) Put(ctx context.Context, n Namespace, item []byte) ([]File, error) {
c.mu.RLock()
defer c.mu.RUnlock()

return c.put(ctx, n, item)
type CollectionPutRequest struct {
Namespace Namespace
Revision *string
Payload []byte
}

func (c *Collection) Delete(ctx context.Context, n Namespace, id ID) ([]File, error) {
c.mu.RLock()
defer c.mu.RUnlock()
func (c *Collection) Put(ctx context.Context, req CollectionPutRequest) ([]Change, error) {
return c.put(ctx, req)
}

func (c *Collection) Delete(ctx context.Context, n Namespace, id ID) ([]Change, error) {
c.logger.Debug("Delete",
slog.String("namespace", string(n)),
slog.String("id", string(id)))
Expand Down
35 changes: 26 additions & 9 deletions collections/flipt/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,11 @@ func (f *FlagCollection) ListAll(_ context.Context) ([]*Flag, error) {
return f.flags, nil
}

func (f *FlagCollection) Put(_ context.Context, ns fidgit.Namespace, flag *Flag) ([]fidgit.File, error) {
flag.Namespace = string(ns)
func (f *FlagCollection) Put(_ context.Context, req fidgit.RuntimePutRequest[Flag]) ([]fidgit.Change, error) {
flag := req.Item
flag.Namespace = string(req.Namespace)

doc, err := f.getDocument(ns)
doc, err := f.getDocument(req.Namespace)
if err != nil {
return nil, err
}
Expand All @@ -249,17 +250,24 @@ func (f *FlagCollection) Put(_ context.Context, ns fidgit.Namespace, flag *Flag)
flags = append(flags, ef)
}

action := "update"
if !found {
action = "create"
flags = append(flags, flag.Flag)
slices.SortFunc(flags, func(i, j *ext.Flag) bool {
return i.Key < j.Key
})
}

return updateDocument(doc.Document, doc.path, flags)
return updateDocument(
fmt.Sprintf("feat: %s flag %s/%s", action, req.Namespace, req.Item.Key),
doc.Document,
doc.path,
flags,
)
}

func (f *FlagCollection) Delete(_ context.Context, ns fidgit.Namespace, id fidgit.ID) ([]fidgit.File, error) {
func (f *FlagCollection) Delete(_ context.Context, ns fidgit.Namespace, id fidgit.ID) ([]fidgit.Change, error) {
doc, err := f.getDocument(ns)
if err != nil {
return nil, err
Expand All @@ -285,19 +293,28 @@ func (f *FlagCollection) Delete(_ context.Context, ns fidgit.Namespace, id fidgi
return nil, fmt.Errorf("flag %s/%s: not found", ns, id)
}

return updateDocument(doc.Document, doc.path, flags)
return updateDocument(
fmt.Sprintf("feat: deleting flag %s/%s", ns, id),
doc.Document,
doc.path,
flags,
)
}

func updateDocument(doc ext.Document, path string, flags []*ext.Flag) ([]fidgit.File, error) {
func updateDocument(message string, doc ext.Document, path string, flags []*ext.Flag) ([]fidgit.Change, error) {
doc.Flags = flags

buf := &bytes.Buffer{}
if err := yaml.NewEncoder(buf).Encode(&doc); err != nil {
return nil, err
}

return []fidgit.File{
{Path: path, Contents: buf.Bytes()},
return []fidgit.Change{
{
Message: message,
Path: path,
Contents: buf.Bytes(),
},
}, nil
}

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ require (
github.com/go-git/go-billy/v5 v5.4.1 // indirect
github.com/go-git/go-git/v5 v5.7.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/hashicorp/golang-lru/v2 v2.0.4 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2a
github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/hashicorp/golang-lru/v2 v2.0.4 h1:7GHuZcgid37q8o5i3QI9KMT4nCWQQ3Kx3Ov6bb9MfK0=
github.com/hashicorp/golang-lru/v2 v2.0.4/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
Expand Down
11 changes: 8 additions & 3 deletions internal/gitfs/gitfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,21 @@ func WithReference(ref plumbing.ReferenceName) containers.Option[Options] {
// from the provided git repository.
// By default the returned FS serves the content from the root tree
// for the commit at reference HEAD.
func NewFromRepo(repo *git.Repository, opts ...containers.Option[Options]) (FS, error) {
func NewFromRepo(repo *git.Repository, opts ...containers.Option[Options]) (FS, plumbing.Hash, error) {
o := Options{ref: plumbing.HEAD}
containers.ApplyAll(&o, opts...)

ref, err := repo.Reference(o.ref, true)
if err != nil {
return FS{}, fmt.Errorf("resolving reference (%q): %w", o.ref, err)
return FS{}, plumbing.ZeroHash, fmt.Errorf("resolving reference (%q): %w", o.ref, err)
}

return NewFromRepoHash(repo, ref.Hash())
fs, err := NewFromRepoHash(repo, ref.Hash())
if err != nil {
return FS{}, plumbing.ZeroHash, err
}

return fs, ref.Hash(), nil
}

// NewFromRepoHash is a convenience utility which constructs an instance of FS
Expand Down
Loading

0 comments on commit 33270b6

Please sign in to comment.