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