mirror of https://github.com/fluxcd/flux2.git
refactor bootstrap process to use fluxcd/pkg/git
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>pull/3299/head
parent
a06652a374
commit
0a5048a56b
@ -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