mirror of https://github.com/fluxcd/flux2.git
				
				
				
			Merge pull request #3299 from aryan9600/use-pkg-git
Refactor bootstrap process to use `fluxcd/pkg/git`pull/3323/head
						commit
						d4ba6c4f44
					
				@ -1,46 +0,0 @@
 | 
			
		||||
package git
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/ProtonMail/go-crypto/openpgp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Option is a some configuration that modifies options for a commit.
 | 
			
		||||
type Option interface {
 | 
			
		||||
	// ApplyToCommit applies this configuration to a given commit option.
 | 
			
		||||
	ApplyToCommit(*CommitOptions)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CommitOptions contains options for making a commit.
 | 
			
		||||
type CommitOptions struct {
 | 
			
		||||
	*GPGSigningInfo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GPGSigningInfo contains information for signing a commit.
 | 
			
		||||
type GPGSigningInfo struct {
 | 
			
		||||
	KeyRing    openpgp.EntityList
 | 
			
		||||
	Passphrase string
 | 
			
		||||
	KeyID      string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GpgSigningOption struct {
 | 
			
		||||
	*GPGSigningInfo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w GpgSigningOption) ApplyToCommit(in *CommitOptions) {
 | 
			
		||||
	in.GPGSigningInfo = w.GPGSigningInfo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func WithGpgSigningOption(keyRing openpgp.EntityList, passphrase, keyID string) Option {
 | 
			
		||||
	// Return nil if no path is set, even if other options are configured.
 | 
			
		||||
	if len(keyRing) == 0 {
 | 
			
		||||
		return GpgSigningOption{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return GpgSigningOption{
 | 
			
		||||
		GPGSigningInfo: &GPGSigningInfo{
 | 
			
		||||
			KeyRing:    keyRing,
 | 
			
		||||
			Passphrase: passphrase,
 | 
			
		||||
			KeyID:      keyID,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,52 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2021 The Flux authors
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package git
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrNoGitRepository = errors.New("no git repository")
 | 
			
		||||
	ErrNoStagedFiles   = errors.New("no staged files")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Author struct {
 | 
			
		||||
	Name  string
 | 
			
		||||
	Email string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Commit struct {
 | 
			
		||||
	Author
 | 
			
		||||
	Hash    string
 | 
			
		||||
	Message string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Git is an interface for basic Git operations on a single branch of a
 | 
			
		||||
// remote repository.
 | 
			
		||||
type Git interface {
 | 
			
		||||
	Init(url, branch string) (bool, error)
 | 
			
		||||
	Clone(ctx context.Context, url, branch string, caBundle []byte) (bool, error)
 | 
			
		||||
	Write(path string, reader io.Reader) error
 | 
			
		||||
	Commit(message Commit, options ...Option) (string, error)
 | 
			
		||||
	Push(ctx context.Context, caBundle []byte) error
 | 
			
		||||
	Status() (bool, error)
 | 
			
		||||
	Head() (string, error)
 | 
			
		||||
	Path() string
 | 
			
		||||
}
 | 
			
		||||
@ -1,286 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
Copyright 2021 The Flux authors
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
    http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
package gogit
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ProtonMail/go-crypto/openpgp"
 | 
			
		||||
	gogit "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/object"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing/transport"
 | 
			
		||||
 | 
			
		||||
	"github.com/fluxcd/flux2/pkg/bootstrap/git"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type GoGit struct {
 | 
			
		||||
	path       string
 | 
			
		||||
	auth       transport.AuthMethod
 | 
			
		||||
	repository *gogit.Repository
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CommitOptions struct {
 | 
			
		||||
	GpgKeyPath       string
 | 
			
		||||
	GpgKeyPassphrase string
 | 
			
		||||
	KeyID            string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(path string, auth transport.AuthMethod) *GoGit {
 | 
			
		||||
	return &GoGit{
 | 
			
		||||
		path: path,
 | 
			
		||||
		auth: auth,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GoGit) Init(url, branch string) (bool, error) {
 | 
			
		||||
	if g.repository != nil {
 | 
			
		||||
		return false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := gogit.PlainInit(g.path, false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	if _, err = r.CreateRemote(&config.RemoteConfig{
 | 
			
		||||
		Name: gogit.DefaultRemoteName,
 | 
			
		||||
		URLs: []string{url},
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	branchRef := plumbing.NewBranchReferenceName(branch)
 | 
			
		||||
	if err = r.CreateBranch(&config.Branch{
 | 
			
		||||
		Name:   branch,
 | 
			
		||||
		Remote: gogit.DefaultRemoteName,
 | 
			
		||||
		Merge:  branchRef,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	// PlainInit assumes the initial branch to always be master, we can
 | 
			
		||||
	// overwrite this by setting the reference of the Storer to a new
 | 
			
		||||
	// symbolic reference (as there are no commits yet) that points
 | 
			
		||||
	// the HEAD to our new branch.
 | 
			
		||||
	if err = r.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, branchRef)); err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	g.repository = r
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GoGit) Clone(ctx context.Context, url, branch string, caBundle []byte) (bool, error) {
 | 
			
		||||
	branchRef := plumbing.NewBranchReferenceName(branch)
 | 
			
		||||
	r, err := gogit.PlainCloneContext(ctx, g.path, false, &gogit.CloneOptions{
 | 
			
		||||
		URL:           url,
 | 
			
		||||
		Auth:          g.auth,
 | 
			
		||||
		RemoteName:    gogit.DefaultRemoteName,
 | 
			
		||||
		ReferenceName: branchRef,
 | 
			
		||||
		SingleBranch:  true,
 | 
			
		||||
 | 
			
		||||
		NoCheckout: false,
 | 
			
		||||
		Progress:   nil,
 | 
			
		||||
		Tags:       gogit.NoTags,
 | 
			
		||||
		CABundle:   caBundle,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == transport.ErrEmptyRemoteRepository || isRemoteBranchNotFoundErr(err, branchRef.String()) {
 | 
			
		||||
			return g.Init(url, branch)
 | 
			
		||||
		}
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	g.repository = r
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GoGit) Write(path string, reader io.Reader) error {
 | 
			
		||||
	if g.repository == nil {
 | 
			
		||||
		return git.ErrNoGitRepository
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wt, err := g.repository.Worktree()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	f, err := wt.Filesystem.Create(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(f, reader)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GoGit) Commit(message git.Commit, opts ...git.Option) (string, error) {
 | 
			
		||||
	if g.repository == nil {
 | 
			
		||||
		return "", git.ErrNoGitRepository
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wt, err := g.repository.Worktree()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	status, err := wt.Status()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// apply the options
 | 
			
		||||
	options := &git.CommitOptions{}
 | 
			
		||||
	for _, opt := range opts {
 | 
			
		||||
		opt.ApplyToCommit(options)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// go-git has [a bug](https://github.com/go-git/go-git/issues/253)
 | 
			
		||||
	// whereby it thinks broken symlinks to absolute paths are
 | 
			
		||||
	// modified. There's no circumstance in which we want to commit a
 | 
			
		||||
	// change to a broken symlink: so, detect and skip those.
 | 
			
		||||
	var changed bool
 | 
			
		||||
	for file, _ := range status {
 | 
			
		||||
		abspath := filepath.Join(g.path, file)
 | 
			
		||||
		info, err := os.Lstat(abspath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", fmt.Errorf("checking if %s is a symlink: %w", file, err)
 | 
			
		||||
		}
 | 
			
		||||
		if info.Mode()&os.ModeSymlink > 0 {
 | 
			
		||||
			// symlinks are OK; broken symlinks are probably a result
 | 
			
		||||
			// of the bug mentioned above, but not of interest in any
 | 
			
		||||
			// case.
 | 
			
		||||
			if _, err := os.Stat(abspath); os.IsNotExist(err) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		_, _ = wt.Add(file)
 | 
			
		||||
		changed = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !changed {
 | 
			
		||||
		head, err := g.repository.Head()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
		return head.Hash().String(), git.ErrNoStagedFiles
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commitOpts := &gogit.CommitOptions{
 | 
			
		||||
		Author: &object.Signature{
 | 
			
		||||
			Name:  message.Name,
 | 
			
		||||
			Email: message.Email,
 | 
			
		||||
			When:  time.Now(),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if options.GPGSigningInfo != nil {
 | 
			
		||||
		entity, err := getOpenPgpEntity(*options.GPGSigningInfo)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		commitOpts.SignKey = entity
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commit, err := wt.Commit(message.Message, commitOpts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return commit.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GoGit) Push(ctx context.Context, caBundle []byte) error {
 | 
			
		||||
	if g.repository == nil {
 | 
			
		||||
		return git.ErrNoGitRepository
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return g.repository.PushContext(ctx, &gogit.PushOptions{
 | 
			
		||||
		RemoteName: gogit.DefaultRemoteName,
 | 
			
		||||
		Auth:       g.auth,
 | 
			
		||||
		Progress:   nil,
 | 
			
		||||
		CABundle:   caBundle,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GoGit) Status() (bool, error) {
 | 
			
		||||
	if g.repository == nil {
 | 
			
		||||
		return false, git.ErrNoGitRepository
 | 
			
		||||
	}
 | 
			
		||||
	wt, err := g.repository.Worktree()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	status, err := wt.Status()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	return status.IsClean(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GoGit) Head() (string, error) {
 | 
			
		||||
	if g.repository == nil {
 | 
			
		||||
		return "", git.ErrNoGitRepository
 | 
			
		||||
	}
 | 
			
		||||
	head, err := g.repository.Head()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return head.Hash().String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GoGit) Path() string {
 | 
			
		||||
	return g.path
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func isRemoteBranchNotFoundErr(err error, ref string) bool {
 | 
			
		||||
	return strings.Contains(err.Error(), fmt.Sprintf("couldn't find remote ref %q", ref))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getOpenPgpEntity(info git.GPGSigningInfo) (*openpgp.Entity, error) {
 | 
			
		||||
	if len(info.KeyRing) == 0 {
 | 
			
		||||
		return nil, fmt.Errorf("empty GPG key ring")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var entity *openpgp.Entity
 | 
			
		||||
	if info.KeyID != "" {
 | 
			
		||||
		for _, ent := range info.KeyRing {
 | 
			
		||||
			if ent.PrimaryKey.KeyIdString() == info.KeyID {
 | 
			
		||||
				entity = ent
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if entity == nil {
 | 
			
		||||
			return nil, fmt.Errorf("no GPG private key matching key id '%s' found", info.KeyID)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		entity = info.KeyRing[0]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := entity.PrivateKey.Decrypt([]byte(info.Passphrase))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to decrypt GPG private key: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return entity, nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,80 +0,0 @@
 | 
			
		||||
//go:build unit
 | 
			
		||||
// +build unit
 | 
			
		||||
 | 
			
		||||
package gogit
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/ProtonMail/go-crypto/openpgp"
 | 
			
		||||
	"github.com/fluxcd/flux2/pkg/bootstrap/git"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGetOpenPgpEntity(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name       string
 | 
			
		||||
		keyPath    string
 | 
			
		||||
		passphrase string
 | 
			
		||||
		id         string
 | 
			
		||||
		expectErr  bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:       "no default key id given",
 | 
			
		||||
			keyPath:    "testdata/private.key",
 | 
			
		||||
			passphrase: "flux",
 | 
			
		||||
			id:         "",
 | 
			
		||||
			expectErr:  false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "key id given",
 | 
			
		||||
			keyPath:    "testdata/private.key",
 | 
			
		||||
			passphrase: "flux",
 | 
			
		||||
			id:         "0619327DBD777415",
 | 
			
		||||
			expectErr:  false,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "wrong key id",
 | 
			
		||||
			keyPath:    "testdata/private.key",
 | 
			
		||||
			passphrase: "flux",
 | 
			
		||||
			id:         "0619327DBD777416",
 | 
			
		||||
			expectErr:  true,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:       "wrong password",
 | 
			
		||||
			keyPath:    "testdata/private.key",
 | 
			
		||||
			passphrase: "fluxe",
 | 
			
		||||
			id:         "0619327DBD777415",
 | 
			
		||||
			expectErr:  true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			var entityList openpgp.EntityList
 | 
			
		||||
			if tt.keyPath != "" {
 | 
			
		||||
				r, err := os.Open(tt.keyPath)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Errorf("unexpected error: %s", err)
 | 
			
		||||
				}
 | 
			
		||||
				entityList, err = openpgp.ReadKeyRing(r)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Errorf("unexpected error: %s", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			gpgInfo := git.GPGSigningInfo{
 | 
			
		||||
				KeyRing:    entityList,
 | 
			
		||||
				Passphrase: tt.passphrase,
 | 
			
		||||
				KeyID:      tt.id,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			_, err := getOpenPgpEntity(gpgInfo)
 | 
			
		||||
			if err != nil && !tt.expectErr {
 | 
			
		||||
				t.Errorf("unexpected error: %s", err)
 | 
			
		||||
			}
 | 
			
		||||
			if err == nil && tt.expectErr {
 | 
			
		||||
				t.Errorf("expected error when %s", tt.name)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
											
												Binary file not shown.
											
										
									
								
					Loading…
					
					
				
		Reference in New Issue