Merge pull request #1854 from SomtochiAma/gpg-signing
Allow users to use gpg signing for bootstrap commits
This commit is contained in:
@@ -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
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
37
internal/bootstrap/git/commit_options.go
Normal file
37
internal/bootstrap/git/commit_options.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
66
internal/bootstrap/git/gogit/gogit_test.go
Normal file
66
internal/bootstrap/git/gogit/gogit_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
internal/bootstrap/git/gogit/testdata/private.key
vendored
Normal file
BIN
internal/bootstrap/git/gogit/testdata/private.key
vendored
Normal file
Binary file not shown.
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user