Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d23f40d5a | |||
| d7a6c0c5a9 | |||
| 98c1275ba2 | |||
| 55b66b1969 | |||
| ed42af10b2 | |||
| a987ea2b4d | |||
| 0500a6c14c | |||
| 10559934cc | |||
| 273acb1eed | |||
| c438a10efc | |||
| 7a53052d06 |
@@ -8,6 +8,6 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write # for reading and creating branches.
|
contents: write # for reading and creating branches.
|
||||||
pull-requests: write # for creating pull requests against release branches.
|
pull-requests: write # for creating pull requests against release branches.
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.10.0
|
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.11.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@f3ad4b56adec90eb5661af565cdebec997ad4bfb # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Create repository
|
- name: Create repository
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||||
- name: Create cluster
|
- name: Create cluster
|
||||||
id: create-cluster
|
id: create-cluster
|
||||||
uses: replicatedhq/replicated-actions/create-cluster@291bef61a059631e39e84f8470f86152171c4c20 # v1.26.0
|
uses: replicatedhq/replicated-actions/create-cluster@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
kubernetes-distribution: "k3s"
|
kubernetes-distribution: "k3s"
|
||||||
@@ -150,7 +150,7 @@ jobs:
|
|||||||
kubectl delete ns flux-system --wait
|
kubectl delete ns flux-system --wait
|
||||||
- name: Delete cluster
|
- name: Delete cluster
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: replicatedhq/replicated-actions/remove-cluster@291bef61a059631e39e84f8470f86152171c4c20 # v1.26.0
|
uses: replicatedhq/replicated-actions/remove-cluster@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
@@ -189,7 +189,7 @@ jobs:
|
|||||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@f3ad4b56adec90eb5661af565cdebec997ad4bfb # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Create repository
|
- name: Create repository
|
||||||
@@ -199,7 +199,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||||
- name: Create cluster
|
- name: Create cluster
|
||||||
id: create-cluster
|
id: create-cluster
|
||||||
uses: replicatedhq/replicated-actions/create-cluster@291bef61a059631e39e84f8470f86152171c4c20 # v1.26.0
|
uses: replicatedhq/replicated-actions/create-cluster@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
kubernetes-distribution: "openshift"
|
kubernetes-distribution: "openshift"
|
||||||
@@ -240,7 +240,7 @@ jobs:
|
|||||||
kubectl delete ns flux-system --wait
|
kubectl delete ns flux-system --wait
|
||||||
- name: Delete cluster
|
- name: Delete cluster
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: replicatedhq/replicated-actions/remove-cluster@291bef61a059631e39e84f8470f86152171c4c20 # v1.26.0
|
uses: replicatedhq/replicated-actions/remove-cluster@6803131db735f7cc067de88fa14237c7462b247a # v1.27.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ jobs:
|
|||||||
node_image: ghcr.io/fluxcd/kindest/node:v1.36.1-amd64
|
node_image: ghcr.io/fluxcd/kindest/node:v1.36.1-amd64
|
||||||
kubectl_version: v1.36.0
|
kubectl_version: v1.36.0
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@f3ad4b56adec90eb5661af565cdebec997ad4bfb # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Setup yq
|
- name: Setup yq
|
||||||
uses: fluxcd/pkg/actions/yq@f3ad4b56adec90eb5661af565cdebec997ad4bfb # main
|
uses: fluxcd/pkg/actions/yq@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Set outputs
|
- name: Set outputs
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/calico.yaml
|
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/calico.yaml
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@f3ad4b56adec90eb5661af565cdebec997ad4bfb # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@f3ad4b56adec90eb5661af565cdebec997ad4bfb # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
kustomize build manifests/crds > all-crds.yaml
|
kustomize build manifests/crds > all-crds.yaml
|
||||||
- name: Generate OpenAPI JSON schemas from CRDs
|
- name: Generate OpenAPI JSON schemas from CRDs
|
||||||
uses: fluxcd/pkg/actions/crdjsonschema@f3ad4b56adec90eb5661af565cdebec997ad4bfb # main
|
uses: fluxcd/pkg/actions/crdjsonschema@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
with:
|
with:
|
||||||
crd: all-crds.yaml
|
crd: all-crds.yaml
|
||||||
output: schemas
|
output: schemas
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@f3ad4b56adec90eb5661af565cdebec997ad4bfb # main
|
uses: fluxcd/pkg/actions/kustomize@5a7f3ce0de742b6c561a50f90940d81cf6fc698d # main
|
||||||
- name: Setup Flux CLI
|
- name: Setup Flux CLI
|
||||||
uses: ./action/
|
uses: ./action/
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read # for reading the repository code.
|
contents: read # for reading the repository code.
|
||||||
security-events: write # for uploading the CodeQL analysis results.
|
security-events: write # for uploading the CodeQL analysis results.
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.10.0
|
uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.11.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
fossa-token: ${{ secrets.FOSSA_TOKEN }}
|
fossa-token: ${{ secrets.FOSSA_TOKEN }}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read # for reading the labels file.
|
contents: read # for reading the labels file.
|
||||||
issues: write # for creating and updating labels.
|
issues: write # for creating and updating labels.
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.10.0
|
uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.11.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upgrade-fluxcd-pkg:
|
upgrade-fluxcd-pkg:
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.10.0
|
uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.11.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/git"
|
"github.com/fluxcd/pkg/git"
|
||||||
|
"github.com/fluxcd/pkg/git/signature"
|
||||||
"github.com/manifoldco/promptui"
|
"github.com/manifoldco/promptui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
@@ -30,6 +32,7 @@ import (
|
|||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
|
"github.com/fluxcd/flux2/v2/pkg/bootstrap"
|
||||||
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
|
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
|
||||||
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
|
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
|
||||||
)
|
)
|
||||||
@@ -79,6 +82,11 @@ type bootstrapFlags struct {
|
|||||||
gpgPassphrase string
|
gpgPassphrase string
|
||||||
gpgKeyID string
|
gpgKeyID string
|
||||||
|
|
||||||
|
sshSigningKeyFile string
|
||||||
|
sshSigningPassword string
|
||||||
|
sshSigningPassphrase string
|
||||||
|
sshSigningReusePrivateKey bool
|
||||||
|
|
||||||
force bool
|
force bool
|
||||||
|
|
||||||
commitMessageAppendix string
|
commitMessageAppendix string
|
||||||
@@ -139,6 +147,12 @@ func init() {
|
|||||||
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgPassphrase, "gpg-passphrase", "", "passphrase for decrypting GPG private key")
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgPassphrase, "gpg-passphrase", "", "passphrase for decrypting GPG private key")
|
||||||
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyID, "gpg-key-id", "", "key id for selecting a particular key")
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyID, "gpg-key-id", "", "key id for selecting a particular key")
|
||||||
|
|
||||||
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshSigningKeyFile, "ssh-signing-key-file", "", "path to an SSH private key file used for signing commits")
|
||||||
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshSigningPassword, "ssh-signing-password", "", "passphrase for decrypting SSH signing key")
|
||||||
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshSigningPassphrase, "ssh-signing-passphrase", "", "alias for --ssh-signing-password")
|
||||||
|
bootstrapCmd.PersistentFlags().MarkHidden("ssh-signing-passphrase")
|
||||||
|
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.sshSigningReusePrivateKey, "ssh-signing-reuse-private-key", false, "use the SSH transport key (--private-key-file) to sign commits")
|
||||||
|
|
||||||
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().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm")
|
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm")
|
||||||
@@ -195,6 +209,31 @@ func bootstrapValidate() error {
|
|||||||
return fmt.Errorf("invalid --registry-creds format, expected 'user:password'")
|
return fmt.Errorf("invalid --registry-creds format, expected 'user:password'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sshSigningSet := bootstrapArgs.sshSigningKeyFile != "" || bootstrapArgs.sshSigningReusePrivateKey
|
||||||
|
if bootstrapArgs.gpgKeyRingPath != "" && sshSigningSet {
|
||||||
|
return fmt.Errorf("--gpg-* and --ssh-signing-* are mutually exclusive; pick one signing format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" && bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
return fmt.Errorf("--ssh-signing-key-file and --ssh-signing-reuse-private-key are mutually exclusive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey && bootstrapArgs.privateKeyFile == "" {
|
||||||
|
return fmt.Errorf("--ssh-signing-reuse-private-key requires --private-key-file")
|
||||||
|
}
|
||||||
|
|
||||||
|
sshSigningPwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sshSigningPwd != "" && bootstrapArgs.sshSigningKeyFile == "" {
|
||||||
|
return fmt.Errorf("--ssh-signing-password requires --ssh-signing-key-file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := preflightSigningKey(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if len(bootstrapArgs.sshHostKeyAlgorithms) > 0 {
|
if len(bootstrapArgs.sshHostKeyAlgorithms) > 0 {
|
||||||
git.HostKeyAlgos = bootstrapArgs.sshHostKeyAlgorithms
|
git.HostKeyAlgos = bootstrapArgs.sshHostKeyAlgorithms
|
||||||
}
|
}
|
||||||
@@ -214,6 +253,57 @@ func mapTeamSlice(s []string, defaultPermission string) map[string]string {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preflightSigningKey reads and parses the configured signing key so
|
||||||
|
// malformed PEM, wrong passphrases, and unsupported SSH algorithms
|
||||||
|
// surface before any clone runs.
|
||||||
|
func preflightSigningKey() error {
|
||||||
|
switch {
|
||||||
|
case bootstrapArgs.gpgKeyRingPath != "":
|
||||||
|
ring, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid GPG signing key: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := bootstrap.SelectOpenPGPSigningEntity(ring, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID); err != nil {
|
||||||
|
return fmt.Errorf("invalid GPG signing key: %w", err)
|
||||||
|
}
|
||||||
|
case bootstrapArgs.sshSigningKeyFile != "":
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := signature.NewSSHSigner(pemBytes, []byte(pwd)); err != nil {
|
||||||
|
return fmt.Errorf("invalid SSH signing key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// effectiveSshSigningPassword resolves the SSH signing-key passphrase
|
||||||
|
// from --ssh-signing-password and its hidden alias
|
||||||
|
// --ssh-signing-passphrase. When both are set with the same value, the
|
||||||
|
// value is returned. When both are set with different non-empty values,
|
||||||
|
// an error is returned. When neither is set, an empty string is
|
||||||
|
// returned with no error.
|
||||||
|
func effectiveSshSigningPassword() (string, error) {
|
||||||
|
pw := bootstrapArgs.sshSigningPassword
|
||||||
|
alias := bootstrapArgs.sshSigningPassphrase
|
||||||
|
switch {
|
||||||
|
case pw != "" && alias != "":
|
||||||
|
if pw != alias {
|
||||||
|
return "", fmt.Errorf("--ssh-signing-password and --ssh-signing-passphrase are aliases; do not pass both")
|
||||||
|
}
|
||||||
|
return pw, nil
|
||||||
|
case pw == "" && alias != "":
|
||||||
|
return alias, nil
|
||||||
|
default:
|
||||||
|
return pw, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation.
|
// confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation.
|
||||||
// It returns a nil error if Flux is not installed or the user confirms overriding an existing installation
|
// It returns a nil error if Flux is not installed or the user confirms overriding an existing installation
|
||||||
func confirmBootstrap(ctx context.Context, kubeClient client.Client) error {
|
func confirmBootstrap(ctx context.Context, kubeClient client.Client) error {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
|
|
||||||
"github.com/fluxcd/pkg/git"
|
"github.com/fluxcd/pkg/git"
|
||||||
"github.com/fluxcd/pkg/git/gogit"
|
"github.com/fluxcd/pkg/git/gogit"
|
||||||
|
"github.com/fluxcd/pkg/git/signature"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
@@ -287,6 +288,31 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.privateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read transport private key for signing: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := signature.NewSSHSigner(pemBytes, []byte(gitArgs.password)); err != nil {
|
||||||
|
return fmt.Errorf("invalid signing key (reused from --private-key-file): %w", err)
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(gitArgs.password)))
|
||||||
|
}
|
||||||
|
|
||||||
// Setup bootstrapper with constructed configs
|
// Setup bootstrapper with constructed configs
|
||||||
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import (
|
|||||||
authutils "github.com/fluxcd/pkg/auth/utils"
|
authutils "github.com/fluxcd/pkg/auth/utils"
|
||||||
"github.com/fluxcd/pkg/git"
|
"github.com/fluxcd/pkg/git"
|
||||||
"github.com/fluxcd/pkg/git/gogit"
|
"github.com/fluxcd/pkg/git/gogit"
|
||||||
|
"github.com/fluxcd/pkg/git/signature"
|
||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
@@ -333,6 +334,33 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.privateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read transport private key for signing: %w", err)
|
||||||
|
}
|
||||||
|
// Reuse-path pre-flight: bootstrapValidate cannot run this check
|
||||||
|
// because the SSH transport password is subcommand-local.
|
||||||
|
if _, err := signature.NewSSHSigner(pemBytes, []byte(gitArgs.password)); err != nil {
|
||||||
|
return fmt.Errorf("invalid signing key (reused from --private-key-file): %w", err)
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(gitArgs.password)))
|
||||||
|
}
|
||||||
|
|
||||||
// Setup bootstrapper with constructed configs
|
// Setup bootstrapper with constructed configs
|
||||||
b, err := bootstrap.NewPlainGitProvider(gitClient, kubeClient, bootstrapOpts...)
|
b, err := bootstrap.NewPlainGitProvider(gitClient, kubeClient, bootstrapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -252,6 +252,12 @@ func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
bootstrap.WithLogger(logger),
|
bootstrap.WithLogger(logger),
|
||||||
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
return fmt.Errorf("--ssh-signing-reuse-private-key is not supported by 'bootstrap gitea'; " +
|
||||||
|
"that subcommand generates the SSH transport key in-process and has no operator-supplied key to reuse")
|
||||||
|
}
|
||||||
|
|
||||||
if bootstrapArgs.sshHostname != "" {
|
if bootstrapArgs.sshHostname != "" {
|
||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
|
||||||
}
|
}
|
||||||
@@ -265,6 +271,19 @@ func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
// Setup bootstrapper with constructed configs
|
// Setup bootstrapper with constructed configs
|
||||||
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -259,6 +259,12 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
bootstrap.WithLogger(logger),
|
bootstrap.WithLogger(logger),
|
||||||
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
return fmt.Errorf("--ssh-signing-reuse-private-key is not supported by 'bootstrap github'; " +
|
||||||
|
"that subcommand generates the SSH transport key in-process and has no operator-supplied key to reuse")
|
||||||
|
}
|
||||||
|
|
||||||
if bootstrapArgs.sshHostname != "" {
|
if bootstrapArgs.sshHostname != "" {
|
||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
|
||||||
}
|
}
|
||||||
@@ -272,6 +278,19 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
// Setup bootstrapper with constructed configs
|
// Setup bootstrapper with constructed configs
|
||||||
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/fluxcd/go-git-providers/gitprovider"
|
"github.com/fluxcd/go-git-providers/gitprovider"
|
||||||
"github.com/fluxcd/pkg/git"
|
"github.com/fluxcd/pkg/git"
|
||||||
"github.com/fluxcd/pkg/git/gogit"
|
"github.com/fluxcd/pkg/git/gogit"
|
||||||
|
"github.com/fluxcd/pkg/git/signature"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
@@ -321,6 +322,31 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningKeyFile != "" {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.sshSigningKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read SSH signing key file: %w", err)
|
||||||
|
}
|
||||||
|
pwd, err := effectiveSshSigningPassword()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(pwd)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if bootstrapArgs.sshSigningReusePrivateKey {
|
||||||
|
pemBytes, err := os.ReadFile(bootstrapArgs.privateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read transport private key for signing: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := signature.NewSSHSigner(pemBytes, []byte(gitArgs.password)); err != nil {
|
||||||
|
return fmt.Errorf("invalid signing key (reused from --private-key-file): %w", err)
|
||||||
|
}
|
||||||
|
bootstrapOpts = append(bootstrapOpts,
|
||||||
|
bootstrap.WithSSHCommitSigning(pemBytes, []byte(gitArgs.password)))
|
||||||
|
}
|
||||||
|
|
||||||
// Setup bootstrapper with constructed configs
|
// Setup bootstrapper with constructed configs
|
||||||
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBootstrapValidate_signingFlags(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
gpgRing string
|
||||||
|
gpgPass string
|
||||||
|
sshKey string
|
||||||
|
sshPass string
|
||||||
|
sshPassp string
|
||||||
|
privateKey string
|
||||||
|
reuse bool
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{name: "no signing flags is valid"},
|
||||||
|
{name: "GPG only is valid", gpgRing: "./testdata/bootstrap/gpg.pgp"},
|
||||||
|
{name: "SSH only is valid", sshKey: "./testdata/bootstrap/ed25519.private"},
|
||||||
|
{
|
||||||
|
name: "Reuse-private-key with private-key-file is valid",
|
||||||
|
privateKey: "./testdata/bootstrap/ed25519.private",
|
||||||
|
reuse: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GPG + SSH errors",
|
||||||
|
gpgRing: "./testdata/bootstrap/gpg.pgp",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519.private",
|
||||||
|
wantErr: "--gpg-* and --ssh-signing-* are mutually exclusive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GPG + reuse errors",
|
||||||
|
gpgRing: "./testdata/bootstrap/gpg.pgp",
|
||||||
|
privateKey: "./testdata/bootstrap/ed25519.private",
|
||||||
|
reuse: true,
|
||||||
|
wantErr: "--gpg-* and --ssh-signing-* are mutually exclusive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH key-file + reuse errors",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519.private",
|
||||||
|
privateKey: "./testdata/bootstrap/ed25519.private",
|
||||||
|
reuse: true,
|
||||||
|
wantErr: "--ssh-signing-key-file and --ssh-signing-reuse-private-key are mutually exclusive",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reuse without private-key-file errors",
|
||||||
|
reuse: true,
|
||||||
|
wantErr: "--ssh-signing-reuse-private-key requires --private-key-file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH password without key errors",
|
||||||
|
sshPass: "secret",
|
||||||
|
wantErr: "--ssh-signing-password requires --ssh-signing-key-file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH passphrase alias alone applies",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
|
||||||
|
sshPassp: "abcde12345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH password and passphrase with same value passes",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
|
||||||
|
sshPass: "abcde12345",
|
||||||
|
sshPassp: "abcde12345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH password and passphrase with different values errors",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
|
||||||
|
sshPass: "right",
|
||||||
|
sshPassp: "wrong",
|
||||||
|
wantErr: "are aliases; do not pass both",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH malformed key fails pre-flight",
|
||||||
|
sshKey: "./testdata/bootstrap/malformed.private",
|
||||||
|
wantErr: "invalid SSH signing key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SSH encrypted key without password fails pre-flight",
|
||||||
|
sshKey: "./testdata/bootstrap/ed25519-encrypted.private",
|
||||||
|
wantErr: "passphrase required",
|
||||||
|
},
|
||||||
|
// The GPG fixture used here is encrypted (passphrase: "right") so that
|
||||||
|
// passing the wrong passphrase exercises the Decrypt error path.
|
||||||
|
// An unencrypted key would make Decrypt a no-op regardless of the
|
||||||
|
// passphrase supplied.
|
||||||
|
{
|
||||||
|
name: "GPG with wrong passphrase fails pre-flight",
|
||||||
|
gpgRing: "./testdata/bootstrap/gpg-encrypted.pgp",
|
||||||
|
gpgPass: "wrong",
|
||||||
|
wantErr: "invalid GPG signing key",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
savedGpgRing := bootstrapArgs.gpgKeyRingPath
|
||||||
|
savedGpgPass := bootstrapArgs.gpgPassphrase
|
||||||
|
savedSshKey := bootstrapArgs.sshSigningKeyFile
|
||||||
|
savedSshPass := bootstrapArgs.sshSigningPassword
|
||||||
|
savedSshPassp := bootstrapArgs.sshSigningPassphrase
|
||||||
|
savedPrivKey := bootstrapArgs.privateKeyFile
|
||||||
|
savedReuse := bootstrapArgs.sshSigningReusePrivateKey
|
||||||
|
defer func() {
|
||||||
|
bootstrapArgs.gpgKeyRingPath = savedGpgRing
|
||||||
|
bootstrapArgs.gpgPassphrase = savedGpgPass
|
||||||
|
bootstrapArgs.sshSigningKeyFile = savedSshKey
|
||||||
|
bootstrapArgs.sshSigningPassword = savedSshPass
|
||||||
|
bootstrapArgs.sshSigningPassphrase = savedSshPassp
|
||||||
|
bootstrapArgs.privateKeyFile = savedPrivKey
|
||||||
|
bootstrapArgs.sshSigningReusePrivateKey = savedReuse
|
||||||
|
}()
|
||||||
|
|
||||||
|
bootstrapArgs.gpgKeyRingPath = tt.gpgRing
|
||||||
|
bootstrapArgs.gpgPassphrase = tt.gpgPass
|
||||||
|
bootstrapArgs.sshSigningKeyFile = tt.sshKey
|
||||||
|
bootstrapArgs.sshSigningPassword = tt.sshPass
|
||||||
|
bootstrapArgs.sshSigningPassphrase = tt.sshPassp
|
||||||
|
bootstrapArgs.privateKeyFile = tt.privateKey
|
||||||
|
bootstrapArgs.sshSigningReusePrivateKey = tt.reuse
|
||||||
|
|
||||||
|
err := bootstrapValidate()
|
||||||
|
if tt.wantErr == "" {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("expected error containing %q, got: %v", tt.wantErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
||||||
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,6 +76,8 @@ type imageUpdateFlags struct {
|
|||||||
commitTemplate string
|
commitTemplate string
|
||||||
authorName string
|
authorName string
|
||||||
authorEmail string
|
authorEmail string
|
||||||
|
signingKeySecret string
|
||||||
|
signingKeyType string
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageUpdateArgs = imageUpdateFlags{}
|
var imageUpdateArgs = imageUpdateFlags{}
|
||||||
@@ -89,6 +92,8 @@ func init() {
|
|||||||
flags.StringVar(&imageUpdateArgs.commitTemplate, "commit-template", "", "a template for commit messages")
|
flags.StringVar(&imageUpdateArgs.commitTemplate, "commit-template", "", "a template for commit messages")
|
||||||
flags.StringVar(&imageUpdateArgs.authorName, "author-name", "", "the name to use for commit author")
|
flags.StringVar(&imageUpdateArgs.authorName, "author-name", "", "the name to use for commit author")
|
||||||
flags.StringVar(&imageUpdateArgs.authorEmail, "author-email", "", "the email to use for commit author")
|
flags.StringVar(&imageUpdateArgs.authorEmail, "author-email", "", "the email to use for commit author")
|
||||||
|
flags.StringVar(&imageUpdateArgs.signingKeySecret, "signing-key-secret", "", "name of the Secret containing the signing key referenced in spec.git.commit.signingKey")
|
||||||
|
flags.StringVar(&imageUpdateArgs.signingKeyType, "signing-key-type", "", "signing-key format: gpg or ssh (defaults to gpg when --signing-key-secret is set)")
|
||||||
|
|
||||||
createImageCmd.AddCommand(createImageUpdateCmd)
|
createImageCmd.AddCommand(createImageUpdateCmd)
|
||||||
}
|
}
|
||||||
@@ -112,6 +117,15 @@ func createImageUpdateRun(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("the author email is required (--author-email)")
|
return fmt.Errorf("the author email is required (--author-email)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if imageUpdateArgs.signingKeyType != "" && imageUpdateArgs.signingKeySecret == "" {
|
||||||
|
return fmt.Errorf("--signing-key-type requires --signing-key-secret")
|
||||||
|
}
|
||||||
|
if imageUpdateArgs.signingKeyType != "" &&
|
||||||
|
imageUpdateArgs.signingKeyType != string(autov1.SigningKeyTypeGPG) &&
|
||||||
|
imageUpdateArgs.signingKeyType != string(autov1.SigningKeyTypeSSH) {
|
||||||
|
return fmt.Errorf("--signing-key-type must be one of: gpg, ssh")
|
||||||
|
}
|
||||||
|
|
||||||
labels, err := parseLabels()
|
labels, err := parseLabels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -163,6 +177,17 @@ func createImageUpdateRun(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if imageUpdateArgs.signingKeySecret != "" {
|
||||||
|
keyType := imageUpdateArgs.signingKeyType
|
||||||
|
if keyType == "" {
|
||||||
|
keyType = string(autov1.SigningKeyTypeGPG)
|
||||||
|
}
|
||||||
|
update.Spec.GitSpec.Commit.SigningKey = &autov1.SigningKey{
|
||||||
|
SecretRef: meta.LocalObjectReference{Name: imageUpdateArgs.signingKeySecret},
|
||||||
|
Type: autov1.SigningKeyType(keyType),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if createArgs.export {
|
if createArgs.export {
|
||||||
return printExport(exportImageUpdate(&update))
|
return printExport(exportImageUpdate(&update))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCreateImageUpdate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
assert assertFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no signing key",
|
||||||
|
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --interval=1m0s --namespace=flux-system --export",
|
||||||
|
assert: assertGoldenFile("./testdata/create_image_update/no-signing.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "signing secret without explicit type defaults to gpg",
|
||||||
|
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-secret=my-key --interval=1m0s --namespace=flux-system --export",
|
||||||
|
assert: assertGoldenFile("./testdata/create_image_update/signing-default-gpg.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ssh signing key",
|
||||||
|
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-secret=my-deploy-key --signing-key-type=ssh --interval=1m0s --namespace=flux-system --export",
|
||||||
|
assert: assertGoldenFile("./testdata/create_image_update/signing-ssh.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "signing-key-type without secret errors",
|
||||||
|
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-type=ssh --namespace=flux-system --export",
|
||||||
|
assert: assertError("--signing-key-type requires --signing-key-secret"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid signing-key-type errors",
|
||||||
|
args: "create image update flux-system --git-repo-ref=flux-system --checkout-branch=main --author-name=flux --author-email=flux@example.com --signing-key-secret=k --signing-key-type=pgp --namespace=flux-system --export",
|
||||||
|
assert: assertError("--signing-key-type must be one of: gpg, ssh"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args,
|
||||||
|
assert: tt.assert,
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDuUiEMA0
|
||||||
|
eUvKlmOsur2w9FAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIDF/w86ZQb5qmZtv
|
||||||
|
m1GvyLojiJdhmPtI9hJ9XPcP7HBoAAAAkG2cIOuSVdWInSC0P81ExiorUpiAGOjxxpgvKW
|
||||||
|
VYERfU1zU72Z/c9n1+z/IH5cJOhZ1vlqBO0rubl4s0KQFvY/LKcsc4N0x0uzpqrvcJP4tO
|
||||||
|
9VW8LrMnrPp7b6KVJPsbeSW1SBcUM24aCMzF4/wV03mN/Uqz30s+YgS9SU4Lz8AOkX58xX
|
||||||
|
yAV0gkmndIzZl+Og==
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACAWDldtCFdSMXIV1vLwXvRwk4eEmSoDCpxNkcbNph3dCAAAAIjjSDmx40g5
|
||||||
|
sQAAAAtzc2gtZWQyNTUxOQAAACAWDldtCFdSMXIV1vLwXvRwk4eEmSoDCpxNkcbNph3dCA
|
||||||
|
AAAEAGpzSFuLkCNDD49+tysxSFFwdOsRnDj67vDT9bfwoSDhYOV20IV1IxchXW8vBe9HCT
|
||||||
|
h4SZKgMKnE2Rxs2mHd0IAAAABHRlc3QB
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
not a real ssh key
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImageUpdateAutomation
|
||||||
|
metadata:
|
||||||
|
name: flux-system
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
git:
|
||||||
|
checkout:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
commit:
|
||||||
|
author:
|
||||||
|
email: flux@example.com
|
||||||
|
name: flux
|
||||||
|
interval: 1m0s
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: flux-system
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImageUpdateAutomation
|
||||||
|
metadata:
|
||||||
|
name: flux-system
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
git:
|
||||||
|
checkout:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
commit:
|
||||||
|
author:
|
||||||
|
email: flux@example.com
|
||||||
|
name: flux
|
||||||
|
signingKey:
|
||||||
|
secretRef:
|
||||||
|
name: my-key
|
||||||
|
type: gpg
|
||||||
|
interval: 1m0s
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: flux-system
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
apiVersion: image.toolkit.fluxcd.io/v1
|
||||||
|
kind: ImageUpdateAutomation
|
||||||
|
metadata:
|
||||||
|
name: flux-system
|
||||||
|
namespace: flux-system
|
||||||
|
spec:
|
||||||
|
git:
|
||||||
|
checkout:
|
||||||
|
ref:
|
||||||
|
branch: main
|
||||||
|
commit:
|
||||||
|
author:
|
||||||
|
email: flux@example.com
|
||||||
|
name: flux
|
||||||
|
signingKey:
|
||||||
|
secretRef:
|
||||||
|
name: my-deploy-key
|
||||||
|
type: ssh
|
||||||
|
interval: 1m0s
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: flux-system
|
||||||
+4
@@ -10,6 +10,10 @@ spec:
|
|||||||
author:
|
author:
|
||||||
email: fluxcdbot@users.noreply.github.com
|
email: fluxcdbot@users.noreply.github.com
|
||||||
name: fluxcdbot
|
name: fluxcdbot
|
||||||
|
signingKey:
|
||||||
|
secretRef:
|
||||||
|
name: my-signing-key
|
||||||
|
type: ssh
|
||||||
interval: 1m0s
|
interval: 1m0s
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: GitRepository
|
kind: GitRepository
|
||||||
|
|||||||
+4
@@ -67,6 +67,10 @@ spec:
|
|||||||
email: fluxcdbot@users.noreply.github.com
|
email: fluxcdbot@users.noreply.github.com
|
||||||
name: fluxcdbot
|
name: fluxcdbot
|
||||||
messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}'
|
messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}'
|
||||||
|
signingKey:
|
||||||
|
secretRef:
|
||||||
|
name: my-signing-key
|
||||||
|
type: ssh
|
||||||
update:
|
update:
|
||||||
path: ./clusters/my-cluster
|
path: ./clusters/my-cluster
|
||||||
strategy: Setters
|
strategy: Setters
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ require (
|
|||||||
github.com/fluxcd/cli-utils v1.2.1
|
github.com/fluxcd/cli-utils v1.2.1
|
||||||
github.com/fluxcd/go-git-providers v0.26.0
|
github.com/fluxcd/go-git-providers v0.26.0
|
||||||
github.com/fluxcd/helm-controller/api v1.5.5
|
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/image-reflector-controller/api v1.1.2
|
||||||
github.com/fluxcd/kustomize-controller/api v1.8.5
|
github.com/fluxcd/kustomize-controller/api v1.8.5
|
||||||
github.com/fluxcd/notification-controller/api v1.8.4
|
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/auth v0.51.0
|
||||||
github.com/fluxcd/pkg/chartutil v1.26.0
|
github.com/fluxcd/pkg/chartutil v1.26.0
|
||||||
github.com/fluxcd/pkg/envsubst v1.7.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/kustomize v1.32.0
|
||||||
github.com/fluxcd/pkg/oci v0.66.0
|
github.com/fluxcd/pkg/oci v0.66.0
|
||||||
github.com/fluxcd/pkg/runtime v0.108.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/ssa v0.75.0
|
||||||
github.com/fluxcd/pkg/ssh v0.25.0
|
github.com/fluxcd/pkg/ssh v0.25.0
|
||||||
github.com/fluxcd/pkg/tar v1.2.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-controller/api v1.8.5
|
||||||
github.com/fluxcd/source-watcher/api/v2 v2.1.1
|
github.com/fluxcd/source-watcher/api/v2 v2.1.1
|
||||||
github.com/go-git/go-git/v5 v5.19.1
|
github.com/go-git/go-git/v5 v5.19.1
|
||||||
|
|||||||
@@ -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/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 h1:xQA/9gbifMvZPGhSNKHsrkq829dI/yTBASVdYp9/s4Y=
|
||||||
github.com/fluxcd/helm-controller/api v1.5.5/go.mod h1:lTgeUmtVYExMKp7mRDncsr4JwHTz3LFtLjRJZeR98lI=
|
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.0.1-0.20260529125431-20ebc65ab20f h1:pjHh/w2xRd9u20J0c8H0EEoPAurJndm2XNF+/mem3EE=
|
||||||
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/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 h1:VPwUgA8WyPVVs16uSkwvjOAY6pvTYgAb0fL90t0RKLE=
|
||||||
github.com/fluxcd/image-reflector-controller/api v1.1.2/go.mod h1:j4JSIocL42HQ77Veg1t60sApOy+lng8/cbXHXGSnfi0=
|
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=
|
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/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 h1:PL9Nj/V2fgaMR9KYZR7mEEw+vlYgP80nFZjOQQKAfJs=
|
||||||
github.com/fluxcd/pkg/envsubst v1.7.0/go.mod h1:aoWeSIOamhqBZ3bHVj1GDwpdA10DXrI8yYbyjPiFly0=
|
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.52.0 h1:dgsliHdaLADUcDO4pI0pc11N4dZ21NfDdhNcgRNuAkM=
|
||||||
github.com/fluxcd/pkg/git v0.50.0/go.mod h1:OgaHoS0iR0GuLl+f778X7NrGy1pDH7xcpF/nsCRgJ9g=
|
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 h1:2j03zKVL6iVn6oiUuecG/O/3Q1pULWM9JrF/HSjkpnc=
|
||||||
github.com/fluxcd/pkg/gittestserver v0.29.0/go.mod h1:O8151jV0ppBZTb9IUXMjxh6hZpkiuLq8JQHDBPOkZFw=
|
github.com/fluxcd/pkg/gittestserver v0.29.0/go.mod h1:O8151jV0ppBZTb9IUXMjxh6hZpkiuLq8JQHDBPOkZFw=
|
||||||
github.com/fluxcd/pkg/kustomize v1.32.0 h1:5lLT2dgR+JrcoJHB7/K50o0AcJikKvXcRd3r7jIYZC8=
|
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/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 h1:T6WFB5M0YRHktlrgdKNskqpdp76TVDdWTOeuWz33CFs=
|
||||||
github.com/fluxcd/pkg/tar v1.2.0/go.mod h1:Wlalp5vIVe+BbckkKkqExKcoHAeeWJPAzwK7ONeFcS0=
|
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.16.0 h1:VR9+143LAwbyUSAaMhiJHbfsiU+fTjA9L/3dr1ucfrI=
|
||||||
github.com/fluxcd/pkg/version v0.15.0/go.mod h1:LEHnvLMgbTk4kelF+JHHzaG77kY9uTWodMtadPRMEW8=
|
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 h1:mLKc9YVMk46JCt1BQbkG6irkrpBZp95kiXh2+GYB6KQ=
|
||||||
github.com/fluxcd/source-controller/api v1.8.5/go.mod h1:sio4t49RDx+S1etHRFAEEw8qfVuw0KKlOg8bRVlEYPM=
|
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=
|
github.com/fluxcd/source-watcher/api/v2 v2.1.1 h1:1LfT50ty+78MKKbschAZl28QbVqIyjaNq17KmW5wPJI=
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
- op: add
|
||||||
|
path: /spec/versions/0/schema/openAPIV3Schema/properties/spec/properties/git/properties/commit/properties/signingKey/properties/type
|
||||||
|
value:
|
||||||
|
default: gpg
|
||||||
|
description: |-
|
||||||
|
Type selects the signing-key format expected in the referenced
|
||||||
|
Secret. Defaults to 'gpg'.
|
||||||
|
enum:
|
||||||
|
- gpg
|
||||||
|
- ssh
|
||||||
|
type: string
|
||||||
|
- op: add
|
||||||
|
path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/git/properties/commit/properties/signingKey/properties/type
|
||||||
|
value:
|
||||||
|
default: gpg
|
||||||
|
description: |-
|
||||||
|
Type selects the signing-key format expected in the referenced
|
||||||
|
Secret. Defaults to 'gpg'.
|
||||||
|
enum:
|
||||||
|
- gpg
|
||||||
|
- ssh
|
||||||
|
type: string
|
||||||
@@ -13,3 +13,9 @@ patches:
|
|||||||
kind: Deployment
|
kind: Deployment
|
||||||
name: image-automation-controller
|
name: image-automation-controller
|
||||||
path: patch.yaml
|
path: patch.yaml
|
||||||
|
- target:
|
||||||
|
group: apiextensions.k8s.io
|
||||||
|
version: v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
name: imageupdateautomations.image.toolkit.fluxcd.io
|
||||||
|
path: crd-signing-key-type-patch.yaml
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import (
|
|||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
"github.com/fluxcd/pkg/git"
|
"github.com/fluxcd/pkg/git"
|
||||||
"github.com/fluxcd/pkg/git/repository"
|
"github.com/fluxcd/pkg/git/repository"
|
||||||
|
"github.com/fluxcd/pkg/git/signature"
|
||||||
"github.com/fluxcd/pkg/kustomize/filesys"
|
"github.com/fluxcd/pkg/kustomize/filesys"
|
||||||
runclient "github.com/fluxcd/pkg/runtime/client"
|
runclient "github.com/fluxcd/pkg/runtime/client"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
@@ -67,6 +68,9 @@ type PlainGitBootstrapper struct {
|
|||||||
gpgPassphrase string
|
gpgPassphrase string
|
||||||
gpgKeyID string
|
gpgKeyID string
|
||||||
|
|
||||||
|
sshSigningKey []byte
|
||||||
|
sshSigningPassword []byte
|
||||||
|
|
||||||
restClientGetter genericclioptions.RESTClientGetter
|
restClientGetter genericclioptions.RESTClientGetter
|
||||||
restClientOptions *runclient.Options
|
restClientOptions *runclient.Options
|
||||||
|
|
||||||
@@ -155,24 +159,27 @@ func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifest
|
|||||||
b.logger.Successf("generated component manifests")
|
b.logger.Successf("generated component manifests")
|
||||||
|
|
||||||
// Write generated files and make a commit
|
// Write generated files and make a commit
|
||||||
var signer *openpgp.Entity
|
signer, err := b.resolveSigner()
|
||||||
if b.gpgKeyRing != nil {
|
|
||||||
signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to generate OpenPGP entity: %w", err)
|
return fmt.Errorf("failed to construct commit signer: %w", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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{
|
commit, err := b.gitClient.Commit(git.Commit{
|
||||||
Author: b.signature,
|
Author: b.signature,
|
||||||
Message: commitMsg,
|
Message: commitMsg,
|
||||||
}, repository.WithFiles(map[string]io.Reader{
|
}, commitOpts...)
|
||||||
manifests.Path: strings.NewReader(manifests.Content),
|
|
||||||
}), repository.WithSigner(signer))
|
|
||||||
if err != nil && err != git.ErrNoStagedFiles {
|
if err != nil && err != git.ErrNoStagedFiles {
|
||||||
return fmt.Errorf("failed to commit component manifests: %w", err)
|
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")
|
b.logger.Successf("generated sync manifests")
|
||||||
|
|
||||||
// Write generated files and make a commit
|
// Write generated files and make a commit
|
||||||
var signer *openpgp.Entity
|
signer, err := b.resolveSigner()
|
||||||
if b.gpgKeyRing != nil {
|
|
||||||
signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to generate OpenPGP entity: %w", err)
|
return fmt.Errorf("failed to construct commit signer: %w", err)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
commitMsg := "Add Flux sync manifests"
|
commitMsg := "Add Flux sync manifests"
|
||||||
if b.commitMessageAppendix != "" {
|
if b.commitMessageAppendix != "" {
|
||||||
commitMsg = commitMsg + "\n\n" + 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{
|
commit, err := b.gitClient.Commit(git.Commit{
|
||||||
Author: b.signature,
|
Author: b.signature,
|
||||||
Message: commitMsg,
|
Message: commitMsg,
|
||||||
}, repository.WithFiles(map[string]io.Reader{
|
}, commitOpts...)
|
||||||
kusManifests.Path: strings.NewReader(kusManifests.Content),
|
|
||||||
}), repository.WithSigner(signer))
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -511,7 +521,33 @@ func (b *PlainGitBootstrapper) cleanGitRepoDir() error {
|
|||||||
return errors.Join(errs...)
|
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 {
|
if len(keyRing) == 0 {
|
||||||
return nil, fmt.Errorf("empty GPG key ring")
|
return nil, fmt.Errorf("empty GPG key ring")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,23 @@ package bootstrap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/pem"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
|
"github.com/fluxcd/pkg/git"
|
||||||
|
gogit "github.com/fluxcd/pkg/git/gogit"
|
||||||
|
"github.com/fluxcd/pkg/git/repository"
|
||||||
|
"github.com/fluxcd/pkg/git/signature"
|
||||||
|
extgogit "github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
@@ -468,3 +481,135 @@ func Test_objectReconciled(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPlainGitBootstrapper_resolveSigner(t *testing.T) {
|
||||||
|
t.Run("no signing configured returns nil signer", func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
b := &PlainGitBootstrapper{}
|
||||||
|
signer, err := b.resolveSigner()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(signer).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GPG key ring returns an OpenPGP signer", func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
entity, err := openpgp.NewEntity("Alice", "test", "alice@example.com", nil)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
b := &PlainGitBootstrapper{gpgKeyRing: openpgp.EntityList{entity}}
|
||||||
|
signer, err := b.resolveSigner()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(signer).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SSH key returns an SSH signer", func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
block, err := gossh.MarshalPrivateKey(priv, "test ed25519 key")
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
pemBytes := pem.EncodeToMemory(block)
|
||||||
|
|
||||||
|
b := &PlainGitBootstrapper{sshSigningKey: pemBytes}
|
||||||
|
signer, err := b.resolveSigner()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(signer).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("encrypted SSH key without password errors", func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
block, err := gossh.MarshalPrivateKeyWithPassphrase(priv, "test ed25519 key", []byte("pw"))
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
pemBytes := pem.EncodeToMemory(block)
|
||||||
|
|
||||||
|
b := &PlainGitBootstrapper{sshSigningKey: pemBytes}
|
||||||
|
_, err = b.resolveSigner()
|
||||||
|
g.Expect(err).To(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GPG path takes precedence over SSH path", func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
entity, err := openpgp.NewEntity("Alice", "test", "alice@example.com", nil)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
b := &PlainGitBootstrapper{
|
||||||
|
gpgKeyRing: openpgp.EntityList{entity},
|
||||||
|
sshSigningKey: []byte("ignored"),
|
||||||
|
}
|
||||||
|
signer, err := b.resolveSigner()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(signer).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPlainGitBootstrapper_sshSignerProducesVerifiableCommit is an
|
||||||
|
// end-to-end wiring test. resolveSigner already has unit tests for
|
||||||
|
// dispatch behaviour, but nothing in pkg/bootstrap exercises the full
|
||||||
|
// path from sshSigningKey → resolveSigner → repository.WithSigner →
|
||||||
|
// gogit.Client.Commit → gpgsig header on the resulting commit object.
|
||||||
|
// This test drives that path and then verifies the signature via
|
||||||
|
// signature.VerifySSHSignature, catching regressions that the existing
|
||||||
|
// dispatcher unit tests would miss.
|
||||||
|
func TestPlainGitBootstrapper_sshSignerProducesVerifiableCommit(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
// Generate an ed25519 keypair and marshal the private key to PEM.
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
pemBlock, err := gossh.MarshalPrivateKey(priv, "test ed25519 key")
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
pemBytes := pem.EncodeToMemory(pemBlock)
|
||||||
|
|
||||||
|
// Resolve a Signer via the same path the bootstrap commit code uses.
|
||||||
|
b := &PlainGitBootstrapper{sshSigningKey: pemBytes}
|
||||||
|
signer, err := b.resolveSigner()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
g.Expect(signer).ToNot(BeNil())
|
||||||
|
|
||||||
|
// Initialise a gogit.Client against a fresh on-disk repo. Init sets
|
||||||
|
// the internal repository pointer so that Commit can operate.
|
||||||
|
tmp := t.TempDir()
|
||||||
|
gogitClient, err := gogit.NewClient(tmp, nil)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
// Use a file:// URL; Init only records the remote URL, it does not
|
||||||
|
// actually connect, so any syntactically valid URL works here.
|
||||||
|
g.Expect(gogitClient.Init(context.Background(), "file:///dev/null", git.DefaultBranch)).To(Succeed())
|
||||||
|
|
||||||
|
// Drive a commit through the same gogit pipeline bootstrap uses.
|
||||||
|
hash, err := gogitClient.Commit(
|
||||||
|
git.Commit{
|
||||||
|
Author: git.Signature{Name: "Test", Email: "test@example.com"},
|
||||||
|
Message: "ssh-signed test commit",
|
||||||
|
},
|
||||||
|
repository.WithFiles(map[string]io.Reader{
|
||||||
|
"signed-file": strings.NewReader("hello sshsig"),
|
||||||
|
}),
|
||||||
|
repository.WithSigner(signer),
|
||||||
|
)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// Read the commit object back via a plain go-git open of the same path.
|
||||||
|
repo, err := extgogit.PlainOpen(tmp)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
commit, err := repo.CommitObject(plumbing.NewHash(hash))
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
// The commit must carry an SSH signature header.
|
||||||
|
g.Expect(commit.PGPSignature).To(HavePrefix("-----BEGIN SSH SIGNATURE-----"))
|
||||||
|
|
||||||
|
// Reconstruct the canonical payload (commit without signature) and
|
||||||
|
// run the full cryptographic verification against the known public key.
|
||||||
|
encoded := &plumbing.MemoryObject{}
|
||||||
|
g.Expect(commit.EncodeWithoutSignature(encoded)).To(Succeed())
|
||||||
|
payloadReader, err := encoded.Reader()
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
payload, err := io.ReadAll(payloadReader)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
gosshPub, err := gossh.NewPublicKey(pub)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
authorizedKey := gossh.MarshalAuthorizedKey(gosshPub)
|
||||||
|
|
||||||
|
_, err = signature.VerifySSHSignature(commit.PGPSignature, payload, string(authorizedKey))
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
}
|
||||||
|
|||||||
@@ -145,6 +145,34 @@ func (o gitCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) {
|
|||||||
o.applyGit(b.PlainGitBootstrapper)
|
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) {
|
func LoadEntityListFromPath(path string) (openpgp.EntityList, error) {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user