diff --git a/go.mod b/go.mod index 7a43554b..dd6bba3b 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/fluxcd/cli-utils v1.2.1 github.com/fluxcd/go-git-providers v0.26.0 github.com/fluxcd/helm-controller/api v1.5.5 - github.com/fluxcd/image-automation-controller/api v1.1.4 + github.com/fluxcd/image-automation-controller/api v1.0.1-0.20260529125431-20ebc65ab20f github.com/fluxcd/image-reflector-controller/api v1.1.2 github.com/fluxcd/kustomize-controller/api v1.8.5 github.com/fluxcd/notification-controller/api v1.8.4 @@ -23,7 +23,7 @@ require ( github.com/fluxcd/pkg/auth v0.51.0 github.com/fluxcd/pkg/chartutil v1.26.0 github.com/fluxcd/pkg/envsubst v1.7.0 - github.com/fluxcd/pkg/git v0.50.0 + github.com/fluxcd/pkg/git v0.52.0 github.com/fluxcd/pkg/kustomize v1.32.0 github.com/fluxcd/pkg/oci v0.66.0 github.com/fluxcd/pkg/runtime v0.108.0 @@ -31,7 +31,7 @@ require ( github.com/fluxcd/pkg/ssa v0.75.0 github.com/fluxcd/pkg/ssh v0.25.0 github.com/fluxcd/pkg/tar v1.2.0 - github.com/fluxcd/pkg/version v0.15.0 + github.com/fluxcd/pkg/version v0.16.0 github.com/fluxcd/source-controller/api v1.8.5 github.com/fluxcd/source-watcher/api/v2 v2.1.1 github.com/go-git/go-git/v5 v5.19.1 diff --git a/go.sum b/go.sum index e28b928c..fa996766 100644 --- a/go.sum +++ b/go.sum @@ -178,8 +178,8 @@ github.com/fluxcd/go-git-providers v0.26.0 h1:0DUsXc1nS9Fe4n8tXSEUCGemWzHShd66gm github.com/fluxcd/go-git-providers v0.26.0/go.mod h1:VJDKUOhZwNAIqDF5iPtIpTr/annsDbKMkPpWiDMBdpo= github.com/fluxcd/helm-controller/api v1.5.5 h1:xQA/9gbifMvZPGhSNKHsrkq829dI/yTBASVdYp9/s4Y= github.com/fluxcd/helm-controller/api v1.5.5/go.mod h1:lTgeUmtVYExMKp7mRDncsr4JwHTz3LFtLjRJZeR98lI= -github.com/fluxcd/image-automation-controller/api v1.1.4 h1:i78AwbcICXSX+a1MQwjNA1Uxxs1e3kfi3EJ21fWzb7w= -github.com/fluxcd/image-automation-controller/api v1.1.4/go.mod h1:lkD/drkD6Wc+2SDjVj5KqfozEucTLFexWgby/5ft660= +github.com/fluxcd/image-automation-controller/api v1.0.1-0.20260529125431-20ebc65ab20f h1:pjHh/w2xRd9u20J0c8H0EEoPAurJndm2XNF+/mem3EE= +github.com/fluxcd/image-automation-controller/api v1.0.1-0.20260529125431-20ebc65ab20f/go.mod h1:XNWgNSF7GVZgGx6qTI+8jhiH0S+a/hNtNvBwPcxOotw= github.com/fluxcd/image-reflector-controller/api v1.1.2 h1:VPwUgA8WyPVVs16uSkwvjOAY6pvTYgAb0fL90t0RKLE= github.com/fluxcd/image-reflector-controller/api v1.1.2/go.mod h1:j4JSIocL42HQ77Veg1t60sApOy+lng8/cbXHXGSnfi0= github.com/fluxcd/kustomize-controller/api v1.8.5 h1:4fGPh6foGVKUUbt5OjVzbC5iTyX+Q+NS50atPboDC4w= @@ -202,8 +202,8 @@ github.com/fluxcd/pkg/chartutil v1.26.0 h1:bgXwDKl2uGITSDxOXe4N5HTXAL0ilk4YVbPLU github.com/fluxcd/pkg/chartutil v1.26.0/go.mod h1:sWDcF//xpIwZ/MQupwTmuM/SgGCkfsTd9BVOQJ3cTjM= github.com/fluxcd/pkg/envsubst v1.7.0 h1:PL9Nj/V2fgaMR9KYZR7mEEw+vlYgP80nFZjOQQKAfJs= github.com/fluxcd/pkg/envsubst v1.7.0/go.mod h1:aoWeSIOamhqBZ3bHVj1GDwpdA10DXrI8yYbyjPiFly0= -github.com/fluxcd/pkg/git v0.50.0 h1:gbGmSTjQ6CxqFmT9ZkLfYh8kG7CHqc7gHoPqcygixK8= -github.com/fluxcd/pkg/git v0.50.0/go.mod h1:OgaHoS0iR0GuLl+f778X7NrGy1pDH7xcpF/nsCRgJ9g= +github.com/fluxcd/pkg/git v0.52.0 h1:dgsliHdaLADUcDO4pI0pc11N4dZ21NfDdhNcgRNuAkM= +github.com/fluxcd/pkg/git v0.52.0/go.mod h1:mOvFDxoiuz+Mm4Ux1wKeTTckvBgZFvbTK8lNxmVHzKs= github.com/fluxcd/pkg/gittestserver v0.29.0 h1:2j03zKVL6iVn6oiUuecG/O/3Q1pULWM9JrF/HSjkpnc= github.com/fluxcd/pkg/gittestserver v0.29.0/go.mod h1:O8151jV0ppBZTb9IUXMjxh6hZpkiuLq8JQHDBPOkZFw= github.com/fluxcd/pkg/kustomize v1.32.0 h1:5lLT2dgR+JrcoJHB7/K50o0AcJikKvXcRd3r7jIYZC8= @@ -220,8 +220,8 @@ github.com/fluxcd/pkg/ssh v0.25.0 h1:4Y9WmuNqyKvH759UznU5DGHRcOuoJ/dQM6sbsaDZYYM github.com/fluxcd/pkg/ssh v0.25.0/go.mod h1:Fli2Ogu4uaIVGbCy+r0vvZlMO0RfuInyNY1q2FVIx0o= github.com/fluxcd/pkg/tar v1.2.0 h1:T6WFB5M0YRHktlrgdKNskqpdp76TVDdWTOeuWz33CFs= github.com/fluxcd/pkg/tar v1.2.0/go.mod h1:Wlalp5vIVe+BbckkKkqExKcoHAeeWJPAzwK7ONeFcS0= -github.com/fluxcd/pkg/version v0.15.0 h1:E2Ju4i0vj8ZXLHKz/F4a8JTmDh7Jcg8okB0hK5rEoTM= -github.com/fluxcd/pkg/version v0.15.0/go.mod h1:LEHnvLMgbTk4kelF+JHHzaG77kY9uTWodMtadPRMEW8= +github.com/fluxcd/pkg/version v0.16.0 h1:VR9+143LAwbyUSAaMhiJHbfsiU+fTjA9L/3dr1ucfrI= +github.com/fluxcd/pkg/version v0.16.0/go.mod h1:2M/l90CmbDaD21JTh77hjwaUbd/YM96+Fo8x4fMdxLI= github.com/fluxcd/source-controller/api v1.8.5 h1:mLKc9YVMk46JCt1BQbkG6irkrpBZp95kiXh2+GYB6KQ= github.com/fluxcd/source-controller/api v1.8.5/go.mod h1:sio4t49RDx+S1etHRFAEEw8qfVuw0KKlOg8bRVlEYPM= github.com/fluxcd/source-watcher/api/v2 v2.1.1 h1:1LfT50ty+78MKKbschAZl28QbVqIyjaNq17KmW5wPJI= diff --git a/pkg/bootstrap/bootstrap_plain_git.go b/pkg/bootstrap/bootstrap_plain_git.go index f39b08b7..6ef3c32e 100644 --- a/pkg/bootstrap/bootstrap_plain_git.go +++ b/pkg/bootstrap/bootstrap_plain_git.go @@ -43,6 +43,7 @@ import ( "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/repository" + "github.com/fluxcd/pkg/git/signature" "github.com/fluxcd/pkg/kustomize/filesys" runclient "github.com/fluxcd/pkg/runtime/client" sourcev1 "github.com/fluxcd/source-controller/api/v1" @@ -67,6 +68,9 @@ type PlainGitBootstrapper struct { gpgPassphrase string gpgKeyID string + sshSigningKey []byte + sshSigningPassword []byte + restClientGetter genericclioptions.RESTClientGetter restClientOptions *runclient.Options @@ -155,24 +159,27 @@ func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifest b.logger.Successf("generated component manifests") // Write generated files and make a commit - var signer *openpgp.Entity - if b.gpgKeyRing != nil { - signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) - if err != nil { - return fmt.Errorf("failed to generate OpenPGP entity: %w", err) - } + signer, err := b.resolveSigner() + if err != nil { + return fmt.Errorf("failed to construct commit signer: %w", err) } commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version) if b.commitMessageAppendix != "" { commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix } + commitOpts := []repository.CommitOption{ + repository.WithFiles(map[string]io.Reader{ + manifests.Path: strings.NewReader(manifests.Content), + }), + } + if signer != nil { + commitOpts = append(commitOpts, repository.WithSigner(signer)) + } commit, err := b.gitClient.Commit(git.Commit{ Author: b.signature, Message: commitMsg, - }, repository.WithFiles(map[string]io.Reader{ - manifests.Path: strings.NewReader(manifests.Content), - }), repository.WithSigner(signer)) + }, commitOpts...) if err != nil && err != git.ErrNoStagedFiles { return fmt.Errorf("failed to commit component manifests: %w", err) } @@ -330,24 +337,27 @@ func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options b.logger.Successf("generated sync manifests") // Write generated files and make a commit - var signer *openpgp.Entity - if b.gpgKeyRing != nil { - signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) - if err != nil { - return fmt.Errorf("failed to generate OpenPGP entity: %w", err) - } + signer, err := b.resolveSigner() + if err != nil { + return fmt.Errorf("failed to construct commit signer: %w", err) } commitMsg := "Add Flux sync manifests" if b.commitMessageAppendix != "" { commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix } + commitOpts := []repository.CommitOption{ + repository.WithFiles(map[string]io.Reader{ + kusManifests.Path: strings.NewReader(kusManifests.Content), + }), + } + if signer != nil { + commitOpts = append(commitOpts, repository.WithSigner(signer)) + } commit, err := b.gitClient.Commit(git.Commit{ Author: b.signature, Message: commitMsg, - }, repository.WithFiles(map[string]io.Reader{ - kusManifests.Path: strings.NewReader(kusManifests.Content), - }), repository.WithSigner(signer)) + }, commitOpts...) if err != nil && err != git.ErrNoStagedFiles { return fmt.Errorf("failed to commit sync manifests: %w", err) } @@ -511,7 +521,33 @@ func (b *PlainGitBootstrapper) cleanGitRepoDir() error { return errors.Join(errs...) } -func getOpenPgpEntity(keyRing openpgp.EntityList, passphrase, keyID string) (*openpgp.Entity, error) { +// resolveSigner returns a signature.Signer derived from the configured +// commit-signing options, or (nil, nil) when no signing has been +// configured. GPG and SSH signing are mutually exclusive; if both have +// been configured the GPG path wins (the caller is responsible for +// rejecting the combination at flag-validation time). +func (b *PlainGitBootstrapper) resolveSigner() (signature.Signer, error) { + switch { + case b.gpgKeyRing != nil: + entity, err := SelectOpenPGPSigningEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) + if err != nil { + return nil, fmt.Errorf("failed to load OpenPGP signing entity: %w", err) + } + return signature.NewOpenPGPSigner(entity) + case len(b.sshSigningKey) > 0: + return signature.NewSSHSigner(b.sshSigningKey, b.sshSigningPassword) + default: + return nil, nil + } +} + +// SelectOpenPGPSigningEntity selects a single OpenPGP entity from the +// given keyring and decrypts its private key with the provided +// passphrase. When keyID is empty the keyring must contain exactly one +// entity; otherwise the entity with the matching 16-character key ID +// is selected. Returns an error if no matching entity is found, the +// matching entity has no private key, or decryption fails. +func SelectOpenPGPSigningEntity(keyRing openpgp.EntityList, passphrase, keyID string) (*openpgp.Entity, error) { if len(keyRing) == 0 { return nil, fmt.Errorf("empty GPG key ring") } diff --git a/pkg/bootstrap/options.go b/pkg/bootstrap/options.go index 27830f4a..c3cf9226 100644 --- a/pkg/bootstrap/options.go +++ b/pkg/bootstrap/options.go @@ -145,6 +145,34 @@ func (o gitCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) { o.applyGit(b.PlainGitBootstrapper) } +// WithSSHCommitSigning configures the bootstrapper to sign commits with +// an SSH private key. pem is the PEM-encoded private key (typically the +// OpenSSH "-----BEGIN OPENSSH PRIVATE KEY-----" format produced by +// ssh-keygen). password is the optional passphrase for the key; pass +// nil for an unencrypted key. +// +// WithSSHCommitSigning and WithGitCommitSigning are mutually exclusive; +// calling both is undefined behavior. The caller is responsible for +// rejecting that combination before constructing the bootstrapper (the +// flux CLI does this in bootstrapValidate). +func WithSSHCommitSigning(pem, password []byte) Option { + return sshCommitSigningOption{pem: pem, password: password} +} + +type sshCommitSigningOption struct { + pem []byte + password []byte +} + +func (o sshCommitSigningOption) applyGit(b *PlainGitBootstrapper) { + b.sshSigningKey = o.pem + b.sshSigningPassword = o.password +} + +func (o sshCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) { + o.applyGit(b.PlainGitBootstrapper) +} + func LoadEntityListFromPath(path string) (openpgp.EntityList, error) { if path == "" { return nil, nil