Add gpg key path and passphrase as args

Signed-off-by: Somtochi Onyekwere <somtochionyekwere@gmail.com>
pull/1854/head
Somtochi Onyekwere 4 years ago
parent b9ceceada4
commit 0beab87f5b

@ -68,6 +68,10 @@ type bootstrapFlags struct {
authorName string authorName string
authorEmail string authorEmail string
gpgKeyPath string
gpgPassphrase string
gpgKeyID string
commitMessageAppendix string commitMessageAppendix string
} }
@ -119,6 +123,10 @@ func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorName, "author-name", "Flux", "author name for Git commits") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorName, "author-name", "Flux", "author name for Git commits")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorEmail, "author-email", "", "author email for Git commits") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorEmail, "author-email", "", "author email for Git commits")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyPath, "gpg-key", "", "path to secret gpg key for signing commits")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgPassphrase, "gpg-passphrase", "", "passphrase for decrypting secret gpg key")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyID, "gpg-key-id", "", "key id for selecting a particular key")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'")
bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.arch, "arch", bootstrapArgs.arch.Description()) bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.arch, "arch", bootstrapArgs.arch.Description())

@ -224,6 +224,7 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
bootstrap.WithPostGenerateSecretFunc(promptPublicKey), bootstrap.WithPostGenerateSecretFunc(promptPublicKey),
bootstrap.WithLogger(logger), bootstrap.WithLogger(logger),
bootstrap.WithCABundle(caBundle), bootstrap.WithCABundle(caBundle),
bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
// Setup bootstrapper with constructed configs // Setup bootstrapper with constructed configs

@ -4,6 +4,7 @@ go 1.16
require ( require (
github.com/Masterminds/semver/v3 v3.1.0 github.com/Masterminds/semver/v3 v3.1.0
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7
github.com/cyphar/filepath-securejoin v0.2.2 github.com/cyphar/filepath-securejoin v0.2.2
github.com/fluxcd/go-git-providers v0.1.1 github.com/fluxcd/go-git-providers v0.1.1
github.com/fluxcd/helm-controller/api v0.11.2 github.com/fluxcd/helm-controller/api v0.11.2

@ -53,6 +53,10 @@ type PlainGitBootstrapper struct {
author git.Author author git.Author
commitMessageAppendix string commitMessageAppendix string
gpgKeyPath string
gpgPassphrase string
gpgKeyID string
kubeconfig string kubeconfig string
kubecontext string kubecontext string
@ -142,6 +146,7 @@ func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifest
} }
// Git commit generated // Git commit generated
gpgOpts := git.WithGpgSigningOption(b.gpgKeyPath, b.gpgPassphrase, b.gpgKeyID)
commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version) commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version)
if b.commitMessageAppendix != "" { if b.commitMessageAppendix != "" {
commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix
@ -149,7 +154,7 @@ func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifest
commit, err := b.git.Commit(git.Commit{ commit, err := b.git.Commit(git.Commit{
Author: b.author, Author: b.author,
Message: commitMsg, Message: commitMsg,
}) }, gpgOpts)
if err != nil && err != git.ErrNoStagedFiles { if err != nil && err != git.ErrNoStagedFiles {
return fmt.Errorf("failed to commit sync manifests: %w", err) return fmt.Errorf("failed to commit sync manifests: %w", err)
} }
@ -306,6 +311,7 @@ func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options
b.logger.Successf("generated sync manifests") b.logger.Successf("generated sync manifests")
// Git commit generated // Git commit generated
gpgOpts := git.WithGpgSigningOption(b.gpgKeyPath, b.gpgPassphrase, b.gpgKeyID)
commitMsg := fmt.Sprintf("Add Flux sync manifests") commitMsg := fmt.Sprintf("Add Flux sync manifests")
if b.commitMessageAppendix != "" { if b.commitMessageAppendix != "" {
commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix
@ -313,7 +319,8 @@ func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options
commit, err := b.git.Commit(git.Commit{ commit, err := b.git.Commit(git.Commit{
Author: b.author, Author: b.author,
Message: commitMsg, Message: commitMsg,
}) }, gpgOpts)
if err != nil && err != git.ErrNoStagedFiles { if err != nil && err != git.ErrNoStagedFiles {
return fmt.Errorf("failed to commit sync manifests: %w", err) return fmt.Errorf("failed to commit sync manifests: %w", err)
} }

@ -0,0 +1,37 @@
package git
// 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 {
PrivateKeyPath string
Passphrase string
KeyID string
}
type GpgSigningOption struct {
*GPGSigningInfo
}
func (w GpgSigningOption) ApplyToCommit(in *CommitOptions) {
in.GPGSigningInfo = w.GPGSigningInfo
}
func WithGpgSigningOption(path, passphrase, keyID string) Option {
return GpgSigningOption{
GPGSigningInfo: &GPGSigningInfo{
PrivateKeyPath: path,
Passphrase: passphrase,
KeyID: keyID,
},
}
}

@ -44,7 +44,7 @@ type Git interface {
Init(url, branch string) (bool, error) Init(url, branch string) (bool, error)
Clone(ctx context.Context, url, branch string, caBundle []byte) (bool, error) Clone(ctx context.Context, url, branch string, caBundle []byte) (bool, error)
Write(path string, reader io.Reader) error Write(path string, reader io.Reader) error
Commit(message Commit) (string, error) Commit(message Commit, options ...Option) (string, error)
Push(ctx context.Context, caBundle []byte) error Push(ctx context.Context, caBundle []byte) error
Status() (bool, error) Status() (bool, error)
Head() (string, error) Head() (string, error)

@ -25,6 +25,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/ProtonMail/go-crypto/openpgp"
gogit "github.com/go-git/go-git/v5" gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
@ -40,6 +41,12 @@ type GoGit struct {
repository *gogit.Repository repository *gogit.Repository
} }
type CommitOptions struct {
GpgKeyPath string
GpgKeyPassphrase string
KeyID string
}
func New(path string, auth transport.AuthMethod) *GoGit { func New(path string, auth transport.AuthMethod) *GoGit {
return &GoGit{ return &GoGit{
path: path, path: path,
@ -127,7 +134,7 @@ func (g *GoGit) Write(path string, reader io.Reader) error {
return err return err
} }
func (g *GoGit) Commit(message git.Commit) (string, error) { func (g *GoGit) Commit(message git.Commit, opts ...git.Option) (string, error) {
if g.repository == nil { if g.repository == nil {
return "", git.ErrNoGitRepository return "", git.ErrNoGitRepository
} }
@ -142,6 +149,12 @@ func (g *GoGit) Commit(message git.Commit) (string, error) {
return "", err 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) // go-git has [a bug](https://github.com/go-git/go-git/issues/253)
// whereby it thinks broken symlinks to absolute paths are // whereby it thinks broken symlinks to absolute paths are
// modified. There's no circumstance in which we want to commit a // modified. There's no circumstance in which we want to commit a
@ -173,13 +186,24 @@ func (g *GoGit) Commit(message git.Commit) (string, error) {
return head.Hash().String(), git.ErrNoStagedFiles return head.Hash().String(), git.ErrNoStagedFiles
} }
commit, err := wt.Commit(message.Message, &gogit.CommitOptions{ commitOpts := &gogit.CommitOptions{
Author: &object.Signature{ Author: &object.Signature{
Name: message.Name, Name: message.Name,
Email: message.Email, Email: message.Email,
When: time.Now(), 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 { if err != nil {
return "", err return "", err
} }
@ -232,3 +256,41 @@ func (g *GoGit) Path() string {
func isRemoteBranchNotFoundErr(err error, ref string) bool { func isRemoteBranchNotFoundErr(err error, ref string) bool {
return strings.Contains(err.Error(), fmt.Sprintf("couldn't find remote ref %q", ref)) return strings.Contains(err.Error(), fmt.Sprintf("couldn't find remote ref %q", ref))
} }
func getOpenPgpEntity(info git.GPGSigningInfo) (*openpgp.Entity, error) {
r, err := os.Open(info.PrivateKeyPath)
if err != nil {
return nil, err
}
entityList, err := openpgp.ReadKeyRing(r)
if err != nil {
return nil, err
}
if len(entityList) == 0 {
return nil, fmt.Errorf("no entity formed")
}
var entity *openpgp.Entity
if info.KeyID != "" {
for _, ent := range entityList {
if ent.PrimaryKey.KeyIdString() == info.KeyID {
entity = ent
}
}
if entity == nil {
return nil, fmt.Errorf("no key matching the key id was found")
}
} else {
entity = entityList[0]
}
err = entity.PrivateKey.Decrypt([]byte(info.Passphrase))
if err != nil {
return nil, err
}
return entity, nil
}

@ -0,0 +1,66 @@
// +build unit
package gogit
import (
"testing"
"github.com/fluxcd/flux2/internal/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) {
gpgInfo := git.GPGSigningInfo{
PrivateKeyPath: tt.keyPath,
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)
}
})
}
}

@ -112,3 +112,27 @@ func (o loggerOption) applyGit(b *PlainGitBootstrapper) {
func (o loggerOption) applyGitProvider(b *GitProviderBootstrapper) { func (o loggerOption) applyGitProvider(b *GitProviderBootstrapper) {
b.logger = o.logger b.logger = o.logger
} }
func WithGitCommitSigning(path, passphrase, keyID string) Option {
return gitCommitSigningOption{
gpgKeyPath: path,
gpgPassphrase: passphrase,
gpgKeyID: keyID,
}
}
type gitCommitSigningOption struct {
gpgKeyPath string
gpgPassphrase string
gpgKeyID string
}
func (o gitCommitSigningOption) applyGit(b *PlainGitBootstrapper) {
b.gpgPassphrase = o.gpgPassphrase
b.gpgKeyPath = o.gpgKeyPath
b.gpgKeyID = o.gpgKeyID
}
func (o gitCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) {
o.applyGit(b.PlainGitBootstrapper)
}

Loading…
Cancel
Save