diff --git a/.github/labels.yaml b/.github/labels.yaml index e5fcef61..5821ac39 100644 --- a/.github/labels.yaml +++ b/.github/labels.yaml @@ -44,12 +44,12 @@ description: Feature request proposals in the RFC format color: '#D621C3' aliases: ['area/RFC'] -- name: backport:release/v2.5.x - description: To be backported to release/v2.5.x - color: '#ffd700' - name: backport:release/v2.6.x description: To be backported to release/v2.6.x color: '#ffd700' - name: backport:release/v2.7.x description: To be backported to release/v2.7.x color: '#ffd700' +- name: backport:release/v2.8.x + description: To be backported to release/v2.8.x + color: '#ffd700' diff --git a/.github/workflows/upgrade-fluxcd-pkg.yaml b/.github/workflows/upgrade-fluxcd-pkg.yaml index 659fd30a..c96199d1 100644 --- a/.github/workflows/upgrade-fluxcd-pkg.yaml +++ b/.github/workflows/upgrade-fluxcd-pkg.yaml @@ -3,6 +3,9 @@ name: upgrade-fluxcd-pkg on: workflow_dispatch: +permissions: + contents: read + jobs: upgrade-fluxcd-pkg: uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.9.0 diff --git a/cmd/flux/build_artifact.go b/cmd/flux/build_artifact.go index 9da0ca0e..1dd681f8 100644 --- a/cmd/flux/build_artifact.go +++ b/cmd/flux/build_artifact.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "github.com/spf13/cobra" @@ -48,9 +49,10 @@ from the given directory or a single manifest file.`, } type buildArtifactFlags struct { - output string - path string - ignorePaths []string + output string + path string + ignorePaths []string + resolveSymlinks bool } var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...) @@ -61,6 +63,7 @@ func init() { buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.") buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.") buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format") + buildArtifactCmd.Flags().BoolVar(&buildArtifactArgs.resolveSymlinks, "resolve-symlinks", false, "resolve symlinks by copying their targets into the artifact") buildCmd.AddCommand(buildArtifactCmd) } @@ -85,6 +88,15 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path) } + if buildArtifactArgs.resolveSymlinks { + resolved, cleanupDir, err := resolveSymlinks(path) + if err != nil { + return fmt.Errorf("resolving symlinks failed: %w", err) + } + defer os.RemoveAll(cleanupDir) + path = resolved + } + logger.Actionf("building artifact from %s", path) ociClient := oci.NewClient(oci.DefaultOptions()) @@ -96,6 +108,141 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error { return nil } +// resolveSymlinks creates a temporary directory with symlinks resolved to their +// real file contents. This allows building artifacts from symlink trees (e.g., +// those created by Nix) where the actual files live outside the source directory. +// It returns the resolved path and the temporary directory path for cleanup. +func resolveSymlinks(srcPath string) (string, string, error) { + absPath, err := filepath.Abs(srcPath) + if err != nil { + return "", "", err + } + + info, err := os.Stat(absPath) + if err != nil { + return "", "", err + } + + // For a single file, resolve the symlink and return the path to the + // copied file within the temp dir, preserving file semantics for callers. + if !info.IsDir() { + resolved, err := filepath.EvalSymlinks(absPath) + if err != nil { + return "", "", fmt.Errorf("resolving symlink for %s: %w", absPath, err) + } + tmpDir, err := os.MkdirTemp("", "flux-artifact-*") + if err != nil { + return "", "", err + } + dst := filepath.Join(tmpDir, filepath.Base(absPath)) + if err := copyFile(resolved, dst); err != nil { + os.RemoveAll(tmpDir) + return "", "", err + } + return dst, tmpDir, nil + } + + tmpDir, err := os.MkdirTemp("", "flux-artifact-*") + if err != nil { + return "", "", err + } + + visited := make(map[string]bool) + if err := copyDir(absPath, tmpDir, visited); err != nil { + os.RemoveAll(tmpDir) + return "", "", err + } + + return tmpDir, tmpDir, nil +} + +// copyDir recursively copies the contents of srcDir to dstDir, resolving any +// symlinks encountered along the way. The visited map tracks resolved real +// directory paths to detect and break symlink cycles. +func copyDir(srcDir, dstDir string, visited map[string]bool) error { + real, err := filepath.EvalSymlinks(srcDir) + if err != nil { + return fmt.Errorf("resolving symlink %s: %w", srcDir, err) + } + abs, err := filepath.Abs(real) + if err != nil { + return fmt.Errorf("getting absolute path for %s: %w", real, err) + } + if visited[abs] { + return nil // break the cycle + } + visited[abs] = true + defer delete(visited, abs) + entries, err := os.ReadDir(srcDir) + if err != nil { + return err + } + + for _, entry := range entries { + srcPath := filepath.Join(srcDir, entry.Name()) + dstPath := filepath.Join(dstDir, entry.Name()) + + // Resolve symlinks to get the real path and info. + realPath, err := filepath.EvalSymlinks(srcPath) + if err != nil { + return fmt.Errorf("resolving symlink %s: %w", srcPath, err) + } + realInfo, err := os.Stat(realPath) + if err != nil { + return fmt.Errorf("stat resolved path %s: %w", realPath, err) + } + + if realInfo.IsDir() { + if err := os.MkdirAll(dstPath, realInfo.Mode()); err != nil { + return err + } + // Recursively copy the resolved directory contents. + if err := copyDir(realPath, dstPath, visited); err != nil { + return err + } + continue + } + + if !realInfo.Mode().IsRegular() { + continue + } + + if err := copyFile(realPath, dstPath); err != nil { + return err + } + } + + return nil +} + +func copyFile(src, dst string) error { + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { + return err + } + + out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode()) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return err + } + return out.Close() +} + func saveReaderToFile(reader io.Reader) (string, error) { b, err := io.ReadAll(bufio.NewReader(reader)) if err != nil { diff --git a/cmd/flux/build_artifact_test.go b/cmd/flux/build_artifact_test.go index ba84186c..88ac278f 100644 --- a/cmd/flux/build_artifact_test.go +++ b/cmd/flux/build_artifact_test.go @@ -18,6 +18,7 @@ package main import ( "os" + "path/filepath" "strings" "testing" @@ -68,3 +69,149 @@ data: } } + +func Test_resolveSymlinks(t *testing.T) { + g := NewWithT(t) + + // Create source directory with a real file + srcDir := t.TempDir() + realFile := filepath.Join(srcDir, "real.yaml") + g.Expect(os.WriteFile(realFile, []byte("apiVersion: v1\nkind: Namespace\nmetadata:\n name: test\n"), 0o644)).To(Succeed()) + + // Create a directory with symlinks pointing to files outside it + symlinkDir := t.TempDir() + symlinkFile := filepath.Join(symlinkDir, "linked.yaml") + g.Expect(os.Symlink(realFile, symlinkFile)).To(Succeed()) + + // Also add a regular file in the symlink dir + regularFile := filepath.Join(symlinkDir, "regular.yaml") + g.Expect(os.WriteFile(regularFile, []byte("apiVersion: v1\nkind: ConfigMap\n"), 0o644)).To(Succeed()) + + // Create a symlinked subdirectory + subDir := filepath.Join(srcDir, "subdir") + g.Expect(os.MkdirAll(subDir, 0o755)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(subDir, "nested.yaml"), []byte("nested"), 0o644)).To(Succeed()) + g.Expect(os.Symlink(subDir, filepath.Join(symlinkDir, "linkeddir"))).To(Succeed()) + + // Resolve symlinks + resolved, cleanupDir, err := resolveSymlinks(symlinkDir) + g.Expect(err).To(BeNil()) + t.Cleanup(func() { os.RemoveAll(cleanupDir) }) + + // Verify the regular file was copied + content, err := os.ReadFile(filepath.Join(resolved, "regular.yaml")) + g.Expect(err).To(BeNil()) + g.Expect(string(content)).To(Equal("apiVersion: v1\nkind: ConfigMap\n")) + + // Verify the symlinked file was resolved and copied + content, err = os.ReadFile(filepath.Join(resolved, "linked.yaml")) + g.Expect(err).To(BeNil()) + g.Expect(string(content)).To(ContainSubstring("kind: Namespace")) + + // Verify that the resolved file is a regular file, not a symlink + info, err := os.Lstat(filepath.Join(resolved, "linked.yaml")) + g.Expect(err).To(BeNil()) + g.Expect(info.Mode().IsRegular()).To(BeTrue()) + + // Verify that the symlinked directory was resolved and its contents were copied + content, err = os.ReadFile(filepath.Join(resolved, "linkeddir", "nested.yaml")) + g.Expect(err).To(BeNil()) + g.Expect(string(content)).To(Equal("nested")) + + // Verify that the file inside the symlinked directory is a regular file + info, err = os.Lstat(filepath.Join(resolved, "linkeddir", "nested.yaml")) + g.Expect(err).To(BeNil()) + g.Expect(info.Mode().IsRegular()).To(BeTrue()) +} + +func Test_resolveSymlinks_singleFile(t *testing.T) { + g := NewWithT(t) + + // Create a real file + srcDir := t.TempDir() + realFile := filepath.Join(srcDir, "manifest.yaml") + g.Expect(os.WriteFile(realFile, []byte("kind: ConfigMap"), 0o644)).To(Succeed()) + + // Create a symlink to the real file + linkDir := t.TempDir() + linkFile := filepath.Join(linkDir, "link.yaml") + g.Expect(os.Symlink(realFile, linkFile)).To(Succeed()) + + // Resolve the single symlinked file + resolved, cleanupDir, err := resolveSymlinks(linkFile) + g.Expect(err).To(BeNil()) + t.Cleanup(func() { os.RemoveAll(cleanupDir) }) + + // The returned path should be a file, not a directory + info, err := os.Stat(resolved) + g.Expect(err).To(BeNil()) + g.Expect(info.IsDir()).To(BeFalse()) + + // Verify contents + content, err := os.ReadFile(resolved) + g.Expect(err).To(BeNil()) + g.Expect(string(content)).To(Equal("kind: ConfigMap")) +} + +func Test_resolveSymlinks_cycle(t *testing.T) { + g := NewWithT(t) + + // Create a directory with a symlink cycle: dir/link -> dir + dir := t.TempDir() + g.Expect(os.WriteFile(filepath.Join(dir, "file.yaml"), []byte("data"), 0o644)).To(Succeed()) + g.Expect(os.Symlink(dir, filepath.Join(dir, "cycle"))).To(Succeed()) + + // resolveSymlinks should not infinite-loop + resolved, cleanupDir, err := resolveSymlinks(dir) + g.Expect(err).To(BeNil()) + t.Cleanup(func() { os.RemoveAll(cleanupDir) }) + + // The file should be copied + content, err := os.ReadFile(filepath.Join(resolved, "file.yaml")) + g.Expect(err).To(BeNil()) + g.Expect(string(content)).To(Equal("data")) + + // The cycle directory should exist but not cause infinite nesting + _, err = os.Stat(filepath.Join(resolved, "cycle")) + g.Expect(err).To(BeNil()) + + // There should NOT be deeply nested cycle/cycle/cycle/... paths + _, err = os.Stat(filepath.Join(resolved, "cycle", "cycle", "cycle")) + g.Expect(os.IsNotExist(err)).To(BeTrue()) +} + +func Test_resolveSymlinks_multipleLinksSameTarget(t *testing.T) { + g := NewWithT(t) + + // Create source directory with a real file inside a dir + srcDir := t.TempDir() + targetDir := filepath.Join(srcDir, "target") + g.Expect(os.MkdirAll(targetDir, 0o755)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(targetDir, "file.yaml"), []byte("data"), 0o644)).To(Succeed()) + + // Create a directory with multiple symlinks pointing to targetDir + symlinkDir := t.TempDir() + + // Link 1 + link1 := filepath.Join(symlinkDir, "link1") + g.Expect(os.Symlink(targetDir, link1)).To(Succeed()) + + // Link 2 + link2 := filepath.Join(symlinkDir, "link2") + g.Expect(os.Symlink(targetDir, link2)).To(Succeed()) + + // Resolve symlinks + resolved, cleanupDir, err := resolveSymlinks(symlinkDir) + g.Expect(err).To(BeNil()) + t.Cleanup(func() { os.RemoveAll(cleanupDir) }) + + // Verify link1 has the file + content, err := os.ReadFile(filepath.Join(resolved, "link1", "file.yaml")) + g.Expect(err).To(BeNil()) + g.Expect(string(content)).To(Equal("data")) + + // Verify link2 ALSO has the file + content2, err := os.ReadFile(filepath.Join(resolved, "link2", "file.yaml")) + g.Expect(err).To(BeNil()) + g.Expect(string(content2)).To(Equal("data")) +} diff --git a/cmd/flux/create_kustomization.go b/cmd/flux/create_kustomization.go index a7ae93ab..45740c34 100644 --- a/cmd/flux/create_kustomization.go +++ b/cmd/flux/create_kustomization.go @@ -136,6 +136,9 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error { if !strings.HasPrefix(kustomizationArgs.path.String(), "./") { return fmt.Errorf("path must begin with ./") } + if kustomizationArgs.source.Name == "" { + return fmt.Errorf("source is required") + } if !createArgs.export { logger.Generatef("generating Kustomization") diff --git a/cmd/flux/create_kustomization_test.go b/cmd/flux/create_kustomization_test.go new file mode 100644 index 00000000..ee743816 --- /dev/null +++ b/cmd/flux/create_kustomization_test.go @@ -0,0 +1,48 @@ +//go:build unit +// +build unit + +/* +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 TestCreateKustomization(t *testing.T) { + tests := []struct { + name string + args string + assert assertFunc + }{ + { + // A user creating a kustomization without --source gets a confusing + // API-level error about spec.sourceRef.kind instead of a clear message. + name: "missing source", + args: "create kustomization my-app --path=./deploy --export", + assert: assertError("source is required"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tt.args, + assert: tt.assert, + } + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/create_receiver.go b/cmd/flux/create_receiver.go index ad6436cf..26000b2d 100644 --- a/cmd/flux/create_receiver.go +++ b/cmd/flux/create_receiver.go @@ -30,6 +30,7 @@ import ( notificationv1 "github.com/fluxcd/notification-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" ) @@ -49,7 +50,7 @@ var createReceiverCmd = &cobra.Command{ } type receiverFlags struct { - receiverType string + receiverType flags.ReceiverType secretRef string events []string resources []string @@ -58,7 +59,7 @@ type receiverFlags struct { var receiverArgs receiverFlags func init() { - createReceiverCmd.Flags().StringVar(&receiverArgs.receiverType, "type", "", "") + createReceiverCmd.Flags().Var(&receiverArgs.receiverType, "type", receiverArgs.receiverType.Description()) createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "") createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values") createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values") @@ -109,7 +110,7 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error { Labels: sourceLabels, }, Spec: notificationv1.ReceiverSpec{ - Type: receiverArgs.receiverType, + Type: receiverArgs.receiverType.String(), Events: receiverArgs.events, Resources: resources, SecretRef: meta.LocalObjectReference{ diff --git a/cmd/flux/create_secret.go b/cmd/flux/create_secret.go index 376fd24f..1a4cb025 100644 --- a/cmd/flux/create_secret.go +++ b/cmd/flux/create_secret.go @@ -56,6 +56,22 @@ func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.S } existing.StringData = secret.StringData + if secret.Annotations != nil { + if existing.Annotations == nil { + existing.Annotations = make(map[string]string) + } + for k, v := range secret.Annotations { + existing.Annotations[k] = v + } + } + if secret.Labels != nil { + if existing.Labels == nil { + existing.Labels = make(map[string]string) + } + for k, v := range secret.Labels { + existing.Labels[k] = v + } + } if err := kubeClient.Update(ctx, &existing); err != nil { return err } diff --git a/cmd/flux/create_secret_receiver.go b/cmd/flux/create_secret_receiver.go new file mode 100644 index 00000000..50eafbea --- /dev/null +++ b/cmd/flux/create_secret_receiver.go @@ -0,0 +1,134 @@ +/* +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 ( + "context" + "fmt" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" + + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" +) + +var createSecretReceiverCmd = &cobra.Command{ + Use: "receiver [name]", + Short: "Create or update a Kubernetes secret for a Receiver webhook", + Long: `The create secret receiver command generates a Kubernetes secret with +the token used for webhook payload validation and an annotation with the +computed webhook URL.`, + Example: ` # Create a receiver secret for a GitHub webhook + flux create secret receiver github-receiver \ + --namespace=my-namespace \ + --type=github \ + --hostname=flux.example.com \ + --export + + # Create a receiver secret for GCR with email claim + flux create secret receiver gcr-receiver \ + --namespace=my-namespace \ + --type=gcr \ + --hostname=flux.example.com \ + --email-claim=sa@project.iam.gserviceaccount.com \ + --export`, + RunE: createSecretReceiverCmdRun, +} + +type secretReceiverFlags struct { + receiverType flags.ReceiverType + token string + hostname string + emailClaim string + audienceClaim string +} + +var secretReceiverArgs secretReceiverFlags + +func init() { + createSecretReceiverCmd.Flags().Var(&secretReceiverArgs.receiverType, "type", secretReceiverArgs.receiverType.Description()) + createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.token, "token", "", "webhook token used for payload validation and URL computation, auto-generated if not specified") + createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.hostname, "hostname", "", "hostname for the webhook URL e.g. flux.example.com") + createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.emailClaim, "email-claim", "", "IAM service account email, required for gcr type") + createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.audienceClaim, "audience-claim", "", "custom OIDC token audience for gcr type, defaults to the webhook URL") + + createSecretCmd.AddCommand(createSecretReceiverCmd) +} + +func createSecretReceiverCmdRun(cmd *cobra.Command, args []string) error { + name := args[0] + + if secretReceiverArgs.receiverType == "" { + return fmt.Errorf("--type is required") + } + + if secretReceiverArgs.hostname == "" { + return fmt.Errorf("--hostname is required") + } + + if secretReceiverArgs.receiverType.String() == notificationv1.GCRReceiver && secretReceiverArgs.emailClaim == "" { + return fmt.Errorf("--email-claim is required for gcr receiver type") + } + + labels, err := parseLabels() + if err != nil { + return err + } + + opts := sourcesecret.Options{ + Name: name, + Namespace: *kubeconfigArgs.Namespace, + Labels: labels, + ReceiverType: secretReceiverArgs.receiverType.String(), + Token: secretReceiverArgs.token, + Hostname: secretReceiverArgs.hostname, + EmailClaim: secretReceiverArgs.emailClaim, + AudienceClaim: secretReceiverArgs.audienceClaim, + } + + secret, err := sourcesecret.GenerateReceiver(opts) + if err != nil { + return err + } + + if createArgs.export { + rootCmd.Println(secret.Content) + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + var s corev1.Secret + if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { + return err + } + if err := upsertSecret(ctx, kubeClient, s); err != nil { + return err + } + + logger.Actionf("receiver secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace) + return nil +} diff --git a/cmd/flux/create_secret_receiver_test.go b/cmd/flux/create_secret_receiver_test.go new file mode 100644 index 00000000..383f2371 --- /dev/null +++ b/cmd/flux/create_secret_receiver_test.go @@ -0,0 +1,74 @@ +/* +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 TestCreateReceiverSecret(t *testing.T) { + tests := []struct { + name string + args string + assert assertFunc + }{ + { + name: "missing type", + args: "create secret receiver test-secret --token=t --hostname=h", + assert: assertError("--type is required"), + }, + { + name: "invalid type", + args: "create secret receiver test-secret --type=invalid --token=t --hostname=h", + assert: assertError("invalid argument \"invalid\" for \"--type\" flag: receiver type 'invalid' is not supported, must be one of: generic, generic-hmac, github, gitlab, bitbucket, harbor, dockerhub, quay, gcr, nexus, acr, cdevents"), + }, + { + name: "missing hostname", + args: "create secret receiver test-secret --type=github --token=t", + assert: assertError("--hostname is required"), + }, + { + name: "gcr missing email-claim", + args: "create secret receiver test-secret --type=gcr --token=t --hostname=h", + assert: assertError("--email-claim is required for gcr receiver type"), + }, + { + name: "github receiver secret", + args: "create secret receiver receiver-secret --type=github --token=test-token --hostname=flux.example.com --namespace=my-namespace --export", + assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver.yaml"), + }, + { + name: "gcr receiver secret", + args: "create secret receiver gcr-secret --type=gcr --token=test-token --hostname=flux.example.com --email-claim=sa@project.iam.gserviceaccount.com --namespace=my-namespace --export", + assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver-gcr.yaml"), + }, + { + name: "gcr receiver secret with custom audience", + args: "create secret receiver gcr-secret --type=gcr --token=test-token --hostname=flux.example.com --email-claim=sa@project.iam.gserviceaccount.com --audience-claim=https://custom.audience.example.com --namespace=my-namespace --export", + assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tt.args, + assert: tt.assert, + } + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/get_helmrelease.go b/cmd/flux/get_helmrelease.go index c5a4b67e..08f88bd9 100644 --- a/cmd/flux/get_helmrelease.go +++ b/cmd/flux/get_helmrelease.go @@ -28,13 +28,22 @@ import ( helmv2 "github.com/fluxcd/helm-controller/api/v2" ) +type getHelmReleaseFlags struct { + showSource bool +} + +var getHrArgs getHelmReleaseFlags + var getHelmReleaseCmd = &cobra.Command{ Use: "helmreleases", Aliases: []string{"hr", "helmrelease"}, Short: "Get HelmRelease statuses", Long: "The get helmreleases command prints the statuses of the resources.", Example: ` # List all Helm releases and their status - flux get helmreleases`, + flux get helmreleases + + # List all Helm releases with source information + flux get helmreleases --show-source`, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ @@ -69,6 +78,7 @@ var getHelmReleaseCmd = &cobra.Command{ } func init() { + getHelmReleaseCmd.Flags().BoolVar(&getHrArgs.showSource, "show-source", false, "show the source reference for each helmrelease") getCmd.AddCommand(getHelmReleaseCmd) } @@ -79,16 +89,45 @@ func getHelmReleaseRevision(helmRelease helmv2.HelmRelease) string { return helmRelease.Status.LastAttemptedRevision } +func getHelmReleaseSource(item helmv2.HelmRelease) string { + if item.Spec.ChartRef != nil { + ns := item.Spec.ChartRef.Namespace + if ns == "" { + ns = item.GetNamespace() + } + return fmt.Sprintf("%s/%s/%s", + item.Spec.ChartRef.Kind, + ns, + item.Spec.ChartRef.Name) + } + ns := item.Spec.Chart.Spec.SourceRef.Namespace + if ns == "" { + ns = item.GetNamespace() + } + return fmt.Sprintf("%s/%s/%s", + item.Spec.Chart.Spec.SourceRef.Kind, + ns, + item.Spec.Chart.Spec.SourceRef.Name) +} + func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := a.Items[i] revision := getHelmReleaseRevision(item) status, msg := statusAndMessage(item.Status.Conditions) - return append(nameColumns(&item, includeNamespace, includeKind), + row := nameColumns(&item, includeNamespace, includeKind) + if getHrArgs.showSource { + row = append(row, getHelmReleaseSource(item)) + } + return append(row, revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a helmReleaseListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} + headers := []string{"Name"} + if getHrArgs.showSource { + headers = append(headers, "Source") + } + headers = append(headers, "Revision", "Suspended", "Ready", "Message") if includeNamespace { headers = append([]string{"Namespace"}, headers...) } diff --git a/cmd/flux/get_kustomization.go b/cmd/flux/get_kustomization.go index a95ec655..97a1d93b 100644 --- a/cmd/flux/get_kustomization.go +++ b/cmd/flux/get_kustomization.go @@ -30,13 +30,22 @@ import ( "github.com/fluxcd/flux2/v2/internal/utils" ) +type getKustomizationFlags struct { + showSource bool +} + +var getKsArgs getKustomizationFlags + var getKsCmd = &cobra.Command{ Use: "kustomizations", Aliases: []string{"ks", "kustomization"}, Short: "Get Kustomization statuses", Long: `The get kustomizations command prints the statuses of the resources.`, Example: ` # List all kustomizations and their status - flux get kustomizations`, + flux get kustomizations + + # List all kustomizations with source information + flux get kustomizations --show-source`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: func(cmd *cobra.Command, args []string) error { get := getCommand{ @@ -74,6 +83,7 @@ var getKsCmd = &cobra.Command{ } func init() { + getKsCmd.Flags().BoolVar(&getKsArgs.showSource, "show-source", false, "show the source reference for each kustomization") getCmd.AddCommand(getKsCmd) } @@ -83,12 +93,27 @@ func (a kustomizationListAdapter) summariseItem(i int, includeNamespace bool, in status, msg := statusAndMessage(item.Status.Conditions) revision = utils.TruncateHex(revision) msg = utils.TruncateHex(msg) - return append(nameColumns(&item, includeNamespace, includeKind), + row := nameColumns(&item, includeNamespace, includeKind) + if getKsArgs.showSource { + sourceNs := item.Spec.SourceRef.Namespace + if sourceNs == "" { + sourceNs = item.GetNamespace() + } + row = append(row, fmt.Sprintf("%s/%s/%s", + item.Spec.SourceRef.Kind, + sourceNs, + item.Spec.SourceRef.Name)) + } + return append(row, revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a kustomizationListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} + headers := []string{"Name"} + if getKsArgs.showSource { + headers = append(headers, "Source") + } + headers = append(headers, "Revision", "Suspended", "Ready", "Message") if includeNamespace { headers = append([]string{"Namespace"}, headers...) } diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go index 48c2549b..78159920 100644 --- a/cmd/flux/main_test.go +++ b/cmd/flux/main_test.go @@ -456,6 +456,7 @@ func resetCmdArgs() { secretGitArgs = NewSecretGitFlags() secretGitHubAppArgs = secretGitHubAppFlags{} secretProxyArgs = secretProxyFlags{} + secretReceiverArgs = secretReceiverFlags{} secretHelmArgs = secretHelmFlags{} secretTLSArgs = secretTLSFlags{} sourceBucketArgs = sourceBucketFlags{} diff --git a/cmd/flux/push_artifact.go b/cmd/flux/push_artifact.go index c37f0ef1..237c2593 100644 --- a/cmd/flux/push_artifact.go +++ b/cmd/flux/push_artifact.go @@ -103,17 +103,18 @@ The command can read the credentials from '~/.docker/config.json' but they can a } type pushArtifactFlags struct { - path string - source string - revision string - creds string - provider flags.SourceOCIProvider - ignorePaths []string - annotations []string - output string - debug bool - reproducible bool - insecure bool + path string + source string + revision string + creds string + provider flags.SourceOCIProvider + ignorePaths []string + annotations []string + output string + debug bool + reproducible bool + insecure bool + resolveSymlinks bool } var pushArtifactArgs = newPushArtifactFlags() @@ -137,6 +138,7 @@ func init() { pushArtifactCmd.Flags().BoolVarP(&pushArtifactArgs.debug, "debug", "", false, "display logs from underlying library") pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.reproducible, "reproducible", false, "ensure reproducible image digests by setting the created timestamp to '1970-01-01T00:00:00Z'") pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.insecure, "insecure-registry", false, "allows artifacts to be pushed without TLS") + pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.resolveSymlinks, "resolve-symlinks", false, "resolve symlinks by copying their targets into the artifact") pushCmd.AddCommand(pushArtifactCmd) } @@ -183,6 +185,15 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid path '%s', must point to an existing directory or file: %w", path, err) } + if pushArtifactArgs.resolveSymlinks { + resolved, cleanupDir, err := resolveSymlinks(path) + if err != nil { + return fmt.Errorf("resolving symlinks failed: %w", err) + } + defer os.RemoveAll(cleanupDir) + path = resolved + } + annotations := map[string]string{} for _, annotation := range pushArtifactArgs.annotations { kv := strings.Split(annotation, "=") diff --git a/cmd/flux/reconcile.go b/cmd/flux/reconcile.go index ffdcce91..9f0787bd 100644 --- a/cmd/flux/reconcile.go +++ b/cmd/flux/reconcile.go @@ -152,7 +152,14 @@ func reconciliationHandled(kubeClient client.Client, namespacedName types.Namesp return false, err } - return result.Status == kstatus.CurrentStatus, nil + switch result.Status { + case kstatus.CurrentStatus: + return true, nil + case kstatus.InProgressStatus: + return false, nil + default: + return false, fmt.Errorf("%s", result.Message) + } } } diff --git a/cmd/flux/resume.go b/cmd/flux/resume.go index fe23a411..e531ecee 100644 --- a/cmd/flux/resume.go +++ b/cmd/flux/resume.go @@ -126,6 +126,17 @@ func (resume resumeCommand) run(cmd *cobra.Command, args []string) error { resume.printMessage(reconcileResps) + // Return an error if any reconciliation failed + var failedCount int + for _, r := range reconcileResps { + if r.resumable != nil && r.err != nil { + failedCount++ + } + } + if failedCount > 0 { + return fmt.Errorf("reconciliation failed for %d %s(s)", failedCount, resume.kind) + } + return nil } diff --git a/cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml b/cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml new file mode 100644 index 00000000..9e52a56b --- /dev/null +++ b/cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + annotations: + notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4 + name: gcr-secret + namespace: my-namespace +stringData: + audience: https://custom.audience.example.com + email: sa@project.iam.gserviceaccount.com + token: test-token + diff --git a/cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr.yaml b/cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr.yaml new file mode 100644 index 00000000..ff4c88b4 --- /dev/null +++ b/cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + annotations: + notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4 + name: gcr-secret + namespace: my-namespace +stringData: + audience: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4 + email: sa@project.iam.gserviceaccount.com + token: test-token + diff --git a/cmd/flux/testdata/create_secret/receiver/secret-receiver.yaml b/cmd/flux/testdata/create_secret/receiver/secret-receiver.yaml new file mode 100644 index 00000000..f1f63759 --- /dev/null +++ b/cmd/flux/testdata/create_secret/receiver/secret-receiver.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + annotations: + notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/106120121d366c2f67e93200f6c1dbe938235eb588daa5e8c0516d3a77ac1dee + name: receiver-secret + namespace: my-namespace +stringData: + token: test-token + diff --git a/docs/release/release-notes-template.md b/docs/release/release-notes-template.md index 2a05d5c5..5ad01f56 100644 --- a/docs/release/release-notes-template.md +++ b/docs/release/release-notes-template.md @@ -26,6 +26,8 @@ The following template can be used for the GitHub release page: +ℹ️ Please follow the [Upgrade Procedure for Flux v2.7+](https://github.com/fluxcd/flux2/discussions/5572) for a smooth upgrade from Flux v2.6 to the latest version. + ### Fixes and improvements @@ -36,7 +38,7 @@ The following template can be used for the GitHub release page: ## Components changelog -- -controller [v](https://github.com/fluxcd/-controller/blob//CHANGELOG.md +- -controller [v](https://github.com/fluxcd/-controller/blob//CHANGELOG.md) ## CLI changelog diff --git a/go.mod b/go.mod index cecd0b06..b6a74595 100644 --- a/go.mod +++ b/go.mod @@ -15,23 +15,23 @@ require ( github.com/fluxcd/helm-controller/api v1.5.3 github.com/fluxcd/image-automation-controller/api v1.1.1 github.com/fluxcd/image-reflector-controller/api v1.1.1 - github.com/fluxcd/kustomize-controller/api v1.8.2 - github.com/fluxcd/notification-controller/api v1.8.2 - github.com/fluxcd/pkg/apis/event v0.24.1 - github.com/fluxcd/pkg/apis/meta v1.25.1 - github.com/fluxcd/pkg/auth v0.38.4 - github.com/fluxcd/pkg/chartutil v1.22.1 + github.com/fluxcd/kustomize-controller/api v1.8.3 + github.com/fluxcd/notification-controller/api v1.8.3 + github.com/fluxcd/pkg/apis/event v0.25.0 + github.com/fluxcd/pkg/apis/meta v1.26.0 + github.com/fluxcd/pkg/auth v0.40.0 + github.com/fluxcd/pkg/chartutil v1.23.0 github.com/fluxcd/pkg/envsubst v1.5.0 - github.com/fluxcd/pkg/git v0.43.1 - github.com/fluxcd/pkg/kustomize v1.27.1 - github.com/fluxcd/pkg/oci v0.60.1 - github.com/fluxcd/pkg/runtime v0.100.4 + github.com/fluxcd/pkg/git v0.46.0 + github.com/fluxcd/pkg/kustomize v1.28.0 + github.com/fluxcd/pkg/oci v0.63.0 + github.com/fluxcd/pkg/runtime v0.103.0 github.com/fluxcd/pkg/sourceignore v0.17.0 - github.com/fluxcd/pkg/ssa v0.67.3 + github.com/fluxcd/pkg/ssa v0.70.0 github.com/fluxcd/pkg/ssh v0.24.0 github.com/fluxcd/pkg/tar v0.17.0 - github.com/fluxcd/pkg/version v0.12.0 - github.com/fluxcd/source-controller/api v1.8.1 + github.com/fluxcd/pkg/version v0.14.0 + github.com/fluxcd/source-controller/api v1.8.2 github.com/fluxcd/source-watcher/api/v2 v2.1.1 github.com/go-git/go-git/v5 v5.16.5 github.com/go-logr/logr v1.4.3 @@ -128,7 +128,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect - github.com/fluxcd/pkg/apis/kustomize v1.15.1 // indirect + github.com/fluxcd/pkg/apis/kustomize v1.16.0 // indirect github.com/fluxcd/pkg/cache v0.13.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect diff --git a/go.sum b/go.sum index e720c911..971f9e3b 100644 --- a/go.sum +++ b/go.sum @@ -180,48 +180,48 @@ github.com/fluxcd/image-automation-controller/api v1.1.1 h1:uiu7kjdVoW8/461HOemX github.com/fluxcd/image-automation-controller/api v1.1.1/go.mod h1:lkD/drkD6Wc+2SDjVj5KqfozEucTLFexWgby/5ft660= github.com/fluxcd/image-reflector-controller/api v1.1.1 h1:4Bj1abzVnjj8+b/293kNeFMRJc+y2wO8Z12ReZ/gA0w= github.com/fluxcd/image-reflector-controller/api v1.1.1/go.mod h1:j4JSIocL42HQ77Veg1t60sApOy+lng8/cbXHXGSnfi0= -github.com/fluxcd/kustomize-controller/api v1.8.2 h1:LcFUjJccwNrhCo7pQBBneLAlHfZZcb58bWB2LnyFwag= -github.com/fluxcd/kustomize-controller/api v1.8.2/go.mod h1:c/mUPIffDDLg1EicXCJtX4N/rc+z5Zh0e/CXjhd7Dyc= -github.com/fluxcd/notification-controller/api v1.8.2 h1:TDrXohUC5Gh3BF+v2ux9/zEG1Ax8u49WDW+3Y6GiIEc= -github.com/fluxcd/notification-controller/api v1.8.2/go.mod h1:ozgJGQPy0dG5eOsLZlwAr6n0q/y6+TWd1fGOtavlXJA= +github.com/fluxcd/kustomize-controller/api v1.8.3 h1:Ux9AAOY0lkP6FgRg5/b/ITvRSy8lz6VBBaZ9bXmTLmI= +github.com/fluxcd/kustomize-controller/api v1.8.3/go.mod h1:c/mUPIffDDLg1EicXCJtX4N/rc+z5Zh0e/CXjhd7Dyc= +github.com/fluxcd/notification-controller/api v1.8.3 h1:edYpC/t4pNw/KQur189SRC1XtFNU597ooDTCrW90Xmw= +github.com/fluxcd/notification-controller/api v1.8.3/go.mod h1:ozgJGQPy0dG5eOsLZlwAr6n0q/y6+TWd1fGOtavlXJA= github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA= github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4= -github.com/fluxcd/pkg/apis/event v0.24.1 h1:TClVdn02aiq3sAl9BuzLjjTIxm3JJ83fJ9nchtBa4qg= -github.com/fluxcd/pkg/apis/event v0.24.1/go.mod h1:TlK8HWYrTwl0raqBRC+ROoNpYW5fdVnwcwOBOx5Kzw8= -github.com/fluxcd/pkg/apis/kustomize v1.15.1 h1:t9QZh+3ZS8EKmlxrnnbcKZcGTrg8FDvMF1T8BHMCuqI= -github.com/fluxcd/pkg/apis/kustomize v1.15.1/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI= -github.com/fluxcd/pkg/apis/meta v1.25.1 h1:WG1GIC/SOz0GjxT0uVuO6AMicQ3yFsk6bDozCnq+fto= -github.com/fluxcd/pkg/apis/meta v1.25.1/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0= -github.com/fluxcd/pkg/auth v0.38.4 h1:xVsJ1rakUm5zS2tOKguZOQc5g6wLgCNxW2a9exidd4M= -github.com/fluxcd/pkg/auth v0.38.4/go.mod h1:KTXOh770ukcyQfC8NavEFzm110ORSQRan0v/kjzgFXs= +github.com/fluxcd/pkg/apis/event v0.25.0 h1:zdwytvDhG+fk+Ywl5DOtv7TklkrVgM21WHm1f+YhleE= +github.com/fluxcd/pkg/apis/event v0.25.0/go.mod h1:TlK8HWYrTwl0raqBRC+ROoNpYW5fdVnwcwOBOx5Kzw8= +github.com/fluxcd/pkg/apis/kustomize v1.16.0 h1:PhWXEhqQqsisIpwp1/wHvTvo+MO+GGzsBPoN0ZnRE3Y= +github.com/fluxcd/pkg/apis/kustomize v1.16.0/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI= +github.com/fluxcd/pkg/apis/meta v1.26.0 h1:dxP1FfBpTCYso6odzRcltVnnRuBb2VyhhgV0VX9YbUE= +github.com/fluxcd/pkg/apis/meta v1.26.0/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0= +github.com/fluxcd/pkg/auth v0.40.0 h1:p6Kw6KH+z8oRqngKhmTt8ILKD/rC+8tP87a//kLZhi8= +github.com/fluxcd/pkg/auth v0.40.0/go.mod h1:Oq/hIEKUMTbL2bv5blf+EhC/jXXJLsOjIMtJj/AtG3Y= github.com/fluxcd/pkg/cache v0.13.0 h1:MqtlgOwIVcGKKgV422e39O+KFSVMWuExKeRaMDBjJlk= github.com/fluxcd/pkg/cache v0.13.0/go.mod h1:0xRZ1hitrIFQ6pl68ke2wZLbIqA2VLzY78HpDo9DVxs= -github.com/fluxcd/pkg/chartutil v1.22.1 h1:ufI9LJ4d5T79h9ruBQRoRcSmuI/KkcwEqWdxu/9Xub8= -github.com/fluxcd/pkg/chartutil v1.22.1/go.mod h1:4/2mpNLyfox3uey++hG21AePPsMWekdhSWAtSdDiubQ= +github.com/fluxcd/pkg/chartutil v1.23.0 h1:ohstQEVnrBIbN85FGu83hnmAohLl0PdOoPlsM6+cjyI= +github.com/fluxcd/pkg/chartutil v1.23.0/go.mod h1:kFhmD6DwBgRsvC1ilINsomargMi2WbqvSndWQLikkLc= github.com/fluxcd/pkg/envsubst v1.5.0 h1:S07mo+MkGhptdHA4pRze5HPKlc8tHxKswNdcMZi1WDY= github.com/fluxcd/pkg/envsubst v1.5.0/go.mod h1:c3a8DYI855sZUubHFYQbjfjop6Wu4/zg1cLyf7SnCes= -github.com/fluxcd/pkg/git v0.43.1 h1:lw29P44wueKzQk79KnYyvisfw//cxg0S4cDeTYx+Slo= -github.com/fluxcd/pkg/git v0.43.1/go.mod h1:3R/AjCe7ee7FqWcAG+2IiuJPOCxrGHF4SCGkuvKS6OQ= -github.com/fluxcd/pkg/gittestserver v0.25.1 h1:40Ridmy1xKxBM9ItDn012R4VKmaoDqzvGaC5g7xv+mw= -github.com/fluxcd/pkg/gittestserver v0.25.1/go.mod h1:7fybYb0yej1fFNiF1ohs0Jr0XzyaZQ/cRh3AFEoCtuc= -github.com/fluxcd/pkg/kustomize v1.27.1 h1:BLOBNLb2N5ObttZA8XJhZ2NqNY1ZjBqQtTpNlIx8/L4= -github.com/fluxcd/pkg/kustomize v1.27.1/go.mod h1:A2RQTe9woDPiwJDWFlkoP4oF9eX9DeXr89FEkKnSObk= -github.com/fluxcd/pkg/oci v0.60.1 h1:mT6WBX+MBIcczzEnw/W4cfXyt5JSRNhRoB/UnJ72K6M= -github.com/fluxcd/pkg/oci v0.60.1/go.mod h1:w2FGseUl3WGjwRMH/3h6MTI4gKahcBQtnGbn/TQVA34= -github.com/fluxcd/pkg/runtime v0.100.4 h1:rwvbeoeWN0BTJORJBISJJEkWn6DVfmWwynFl2GseWns= -github.com/fluxcd/pkg/runtime v0.100.4/go.mod h1:M6LjRJ1hIe2s6E2ykFfae1Xy/rLvOFQf2QquMKmN350= +github.com/fluxcd/pkg/git v0.46.0 h1:QMh0+ZzQ2jO6rIGj4ffR5trZ8g/cxvt8cVajReJ8Iyw= +github.com/fluxcd/pkg/git v0.46.0/go.mod h1:iHcIjx9c8zye3PQiajTJYxgOMRiy7WCs+hfLKDswpfI= +github.com/fluxcd/pkg/gittestserver v0.26.0 h1:+RZrCzFRsE+d5WaqAoqaPCEgcgv/jZp6+f7DS0+Ynb8= +github.com/fluxcd/pkg/gittestserver v0.26.0/go.mod h1:7fybYb0yej1fFNiF1ohs0Jr0XzyaZQ/cRh3AFEoCtuc= +github.com/fluxcd/pkg/kustomize v1.28.0 h1:0RuFVczJRabbt8frHZ/ql8aqte6BOOKk274O09l6/hE= +github.com/fluxcd/pkg/kustomize v1.28.0/go.mod h1:cW08mnngSP8MJYb6mDmMvxH8YjNATdiML0udb37dk+M= +github.com/fluxcd/pkg/oci v0.63.0 h1:ZPKTT2C+gWYjhP63xC76iTPdYE9w3ABcsDq77uhAgwo= +github.com/fluxcd/pkg/oci v0.63.0/go.mod h1:qMPz4njvm6hJzdyGSb8ydSqrapXxTQwJonxHIsdeXSQ= +github.com/fluxcd/pkg/runtime v0.103.0 h1:J5y5GPhWdkyqIUBlaI1FP2N02TtZmsjbWhhZubuTSFk= +github.com/fluxcd/pkg/runtime v0.103.0/go.mod h1:mbo2f3azo3yVQgm7XZGxQB6/2zvzQ5Wgtd8TjRRwwAw= github.com/fluxcd/pkg/sourceignore v0.17.0 h1:Z72nruRMhC15zIEpWoDrAcJcJ1El6QDnP/aRDfE4WOA= github.com/fluxcd/pkg/sourceignore v0.17.0/go.mod h1:3e/VmYLId0pI/H5sK7W9Ibif+j0Ahns9RxNjDMtTTfY= -github.com/fluxcd/pkg/ssa v0.67.3 h1:mjuhH5fNOYstkF6jB7EeaWmfnt5T272Cup8ZD9O8YBQ= -github.com/fluxcd/pkg/ssa v0.67.3/go.mod h1:6igtlt7/zF+nNFQpa5ZAkkvtpL6o36NRU39/PqqC+Bg= +github.com/fluxcd/pkg/ssa v0.70.0 h1:IBylYPiTK1IEdCC2DvjKXIhwQcbd5VufXA9WS3zO+tE= +github.com/fluxcd/pkg/ssa v0.70.0/go.mod h1:6igtlt7/zF+nNFQpa5ZAkkvtpL6o36NRU39/PqqC+Bg= github.com/fluxcd/pkg/ssh v0.24.0 h1:hrPlxs0hhXf32DRqs68VbsXs0XfQMphyRVIk0rYYJa4= github.com/fluxcd/pkg/ssh v0.24.0/go.mod h1:xWammEqalrpurpcMiixJRXtynRQtBEoqheyU5F/vWrg= github.com/fluxcd/pkg/tar v0.17.0 h1:uNxbFXy8ly8C7fJ8D7w3rjTNJFrb4Hp1aY/30XkfvxY= github.com/fluxcd/pkg/tar v0.17.0/go.mod h1:b1xyIRYDD0ket4SV5u0UXYv+ZdN/O/HmIO5jZQdHQls= -github.com/fluxcd/pkg/version v0.12.0 h1:MGbdbNf2D5wazMqAkNPn+Lh5j+oY0gxQJFTGyet5Hfc= -github.com/fluxcd/pkg/version v0.12.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8= -github.com/fluxcd/source-controller/api v1.8.1 h1:49HiJF5mNEdZTwueQMRahTVts35B+xhN5CsuOAL9gQ0= -github.com/fluxcd/source-controller/api v1.8.1/go.mod h1:HgZ6NSH1cyOE2jRoNwln1xEwr9ETvrLeiy1o4O04vQM= +github.com/fluxcd/pkg/version v0.14.0 h1:T3llSc8sUnsuFrW5ng2ePSfXwGXUKv0YG9QXf0ErhWw= +github.com/fluxcd/pkg/version v0.14.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8= +github.com/fluxcd/source-controller/api v1.8.2 h1:i0/6BeNCn+zRfX+gKh4PsFF2NBzBhwXt0wPImVlZObg= +github.com/fluxcd/source-controller/api v1.8.2/go.mod h1:HgZ6NSH1cyOE2jRoNwln1xEwr9ETvrLeiy1o4O04vQM= github.com/fluxcd/source-watcher/api/v2 v2.1.1 h1:1LfT50ty+78MKKbschAZl28QbVqIyjaNq17KmW5wPJI= github.com/fluxcd/source-watcher/api/v2 v2.1.1/go.mod h1:6M1BzBGQRoIuSenSQlfJHwMVVobFPiNPxXqfN0IILc4= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= diff --git a/internal/flags/receiver_type.go b/internal/flags/receiver_type.go new file mode 100644 index 00000000..42948b7a --- /dev/null +++ b/internal/flags/receiver_type.go @@ -0,0 +1,68 @@ +/* +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 flags + +import ( + "fmt" + "strings" + + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + + "github.com/fluxcd/flux2/v2/internal/utils" +) + +var supportedReceiverTypes = []string{ + notificationv1.GenericReceiver, + notificationv1.GenericHMACReceiver, + notificationv1.GitHubReceiver, + notificationv1.GitLabReceiver, + notificationv1.BitbucketReceiver, + notificationv1.HarborReceiver, + notificationv1.DockerHubReceiver, + notificationv1.QuayReceiver, + notificationv1.GCRReceiver, + notificationv1.NexusReceiver, + notificationv1.ACRReceiver, + notificationv1.CDEventsReceiver, +} + +type ReceiverType string + +func (r *ReceiverType) String() string { + return string(*r) +} + +func (r *ReceiverType) Set(str string) error { + if strings.TrimSpace(str) == "" { + return fmt.Errorf("no receiver type given, please specify %s", + r.Description()) + } + if !utils.ContainsItemString(supportedReceiverTypes, str) { + return fmt.Errorf("receiver type '%s' is not supported, must be one of: %s", + str, strings.Join(supportedReceiverTypes, ", ")) + } + *r = ReceiverType(str) + return nil +} + +func (r *ReceiverType) Type() string { + return strings.Join(supportedReceiverTypes, "|") +} + +func (r *ReceiverType) Description() string { + return "the receiver type" +} diff --git a/manifests/bases/kustomize-controller/kustomization.yaml b/manifests/bases/kustomize-controller/kustomization.yaml index f75f707d..2ad41b16 100644 --- a/manifests/bases/kustomize-controller/kustomization.yaml +++ b/manifests/bases/kustomize-controller/kustomization.yaml @@ -1,8 +1,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.2/kustomize-controller.crds.yaml -- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.2/kustomize-controller.deployment.yaml +- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.3/kustomize-controller.crds.yaml +- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.3/kustomize-controller.deployment.yaml - account.yaml transformers: - labels.yaml diff --git a/manifests/bases/notification-controller/kustomization.yaml b/manifests/bases/notification-controller/kustomization.yaml index 71c3de84..344c9e29 100644 --- a/manifests/bases/notification-controller/kustomization.yaml +++ b/manifests/bases/notification-controller/kustomization.yaml @@ -1,8 +1,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/notification-controller/releases/download/v1.8.2/notification-controller.crds.yaml -- https://github.com/fluxcd/notification-controller/releases/download/v1.8.2/notification-controller.deployment.yaml +- https://github.com/fluxcd/notification-controller/releases/download/v1.8.3/notification-controller.crds.yaml +- https://github.com/fluxcd/notification-controller/releases/download/v1.8.3/notification-controller.deployment.yaml - account.yaml transformers: - labels.yaml diff --git a/manifests/bases/source-controller/kustomization.yaml b/manifests/bases/source-controller/kustomization.yaml index eba59908..b2eef98d 100644 --- a/manifests/bases/source-controller/kustomization.yaml +++ b/manifests/bases/source-controller/kustomization.yaml @@ -1,8 +1,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/source-controller/releases/download/v1.8.1/source-controller.crds.yaml -- https://github.com/fluxcd/source-controller/releases/download/v1.8.1/source-controller.deployment.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.8.2/source-controller.crds.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.8.2/source-controller.deployment.yaml - account.yaml transformers: - labels.yaml diff --git a/manifests/crds/kustomization.yaml b/manifests/crds/kustomization.yaml index c29dc4ec..3f3f914e 100644 --- a/manifests/crds/kustomization.yaml +++ b/manifests/crds/kustomization.yaml @@ -1,10 +1,10 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/source-controller/releases/download/v1.8.1/source-controller.crds.yaml -- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.2/kustomize-controller.crds.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.8.2/source-controller.crds.yaml +- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.3/kustomize-controller.crds.yaml - https://github.com/fluxcd/helm-controller/releases/download/v1.5.3/helm-controller.crds.yaml -- https://github.com/fluxcd/notification-controller/releases/download/v1.8.2/notification-controller.crds.yaml +- https://github.com/fluxcd/notification-controller/releases/download/v1.8.3/notification-controller.crds.yaml - https://github.com/fluxcd/image-reflector-controller/releases/download/v1.1.1/image-reflector-controller.crds.yaml - https://github.com/fluxcd/image-automation-controller/releases/download/v1.1.1/image-automation-controller.crds.yaml - https://github.com/fluxcd/source-watcher/releases/download/v2.1.1/source-watcher.crds.yaml diff --git a/pkg/manifestgen/sourcesecret/options.go b/pkg/manifestgen/sourcesecret/options.go index a226a4db..9fe7b49d 100644 --- a/pkg/manifestgen/sourcesecret/options.go +++ b/pkg/manifestgen/sourcesecret/options.go @@ -42,6 +42,12 @@ const ( KnownHostsSecretKey = "known_hosts" BearerTokenKey = "bearerToken" TrustPolicyKey = "trustpolicy.json" + TokenSecretKey = "token" + EmailSecretKey = "email" + AudienceSecretKey = "audience" + + // WebhookURLAnnotation is the annotation key for the computed webhook URL. + WebhookURLAnnotation = "notification.toolkit.fluxcd.io/webhook" // Deprecated: Replaced by CACrtSecretKey, but kept for backwards // compatibility with deprecated TLS flags. @@ -82,6 +88,13 @@ type Options struct { GitHubAppInstallationID string GitHubAppPrivateKey string GitHubAppBaseURL string + + // Receiver options + ReceiverType string + Token string + Hostname string + EmailClaim string + AudienceClaim string } type VerificationCrt struct { diff --git a/pkg/manifestgen/sourcesecret/sourcesecret.go b/pkg/manifestgen/sourcesecret/sourcesecret.go index 709f8d01..54cca4e8 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret.go @@ -18,7 +18,10 @@ package sourcesecret import ( "bytes" + "crypto/rand" + "crypto/sha256" "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "net" @@ -260,6 +263,59 @@ func GenerateGitHubApp(options Options) (*manifestgen.Manifest, error) { return secretToManifest(secret, options) } +func GenerateReceiver(options Options) (*manifestgen.Manifest, error) { + token := options.Token + if token == "" { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return nil, fmt.Errorf("failed to generate random token: %w", err) + } + token = hex.EncodeToString(b) + } + + if options.Hostname == "" { + return nil, fmt.Errorf("hostname is required") + } + + // Compute the webhook path using the same algorithm as notification-controller. + // See: github.com/fluxcd/notification-controller/api/v1.Receiver.GetWebhookPath + digest := sha256.Sum256([]byte(token + options.Name + options.Namespace)) + webhookPath := fmt.Sprintf("/hook/%x", digest) + webhookURL := fmt.Sprintf("https://%s%s", options.Hostname, webhookPath) + + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: options.Name, + Namespace: options.Namespace, + Labels: options.Labels, + Annotations: map[string]string{ + WebhookURLAnnotation: webhookURL, + }, + }, + StringData: map[string]string{ + TokenSecretKey: token, + }, + } + + if options.ReceiverType == "gcr" { + if options.EmailClaim == "" { + return nil, fmt.Errorf("email-claim is required for gcr receiver type") + } + secret.StringData[EmailSecretKey] = options.EmailClaim + if options.AudienceClaim != "" { + secret.StringData[AudienceSecretKey] = options.AudienceClaim + } else { + secret.StringData[AudienceSecretKey] = webhookURL + } + } + + return secretToManifest(secret, options) +} + func LoadKeyPairFromPath(path, password string) (*ssh.KeyPair, error) { if path == "" { return nil, nil diff --git a/rfcs/0010-multi-tenant-workload-identity/README.md b/rfcs/0010-multi-tenant-workload-identity/README.md index 9ed342d9..920e402e 100644 --- a/rfcs/0010-multi-tenant-workload-identity/README.md +++ b/rfcs/0010-multi-tenant-workload-identity/README.md @@ -1,15 +1,10 @@ # RFC-0010 Multi-Tenant Workload Identity -**Status:** implementable - - +**Status:** implemented **Creation date:** 2025-02-22 -**Last update:** 2025-04-29 +**Last update:** 2026-03-13 ## Summary @@ -1420,10 +1415,11 @@ options to call `gcp.NewTokenSource()` and feed this token source to the `HelmRepository` and `HelmChart`, as well as for SOPS decryption in the `Kustomization` API and Azure Event Hubs in the `Provider` API. - - +* In Flux 2.7 object-level workload identity was introduced for all + the remaining APIs that support cloud providers, i.e. `Bucket`, + `GitRepository` and `ImageUpdateAutomation`, and also all the + remaining types for the `Provider` API, i.e. `azuredevops` and + `googlepubsub`. In addition, support for controller and + object-level workload identity was introduced for the + `Kustomization` and `HelmRelease` APIs for remote cluster + access. diff --git a/rfcs/0011-opentelemetry-tracing/README.md b/rfcs/0011-opentelemetry-tracing/README.md index 768e05a4..3dd5e851 100644 --- a/rfcs/0011-opentelemetry-tracing/README.md +++ b/rfcs/0011-opentelemetry-tracing/README.md @@ -1,15 +1,10 @@ # RFC-0011: OpenTelemetry Tracing -**Status:** provisional - - +**Status:** implemented **Creation date:** 2025-04-24 -**Last update:** 2025-08-13 +**Last update:** 2026-03-13 ## Summary The aim is to be able to collect traces via OpenTelemetry (OTel) across all Flux related objects, such as HelmReleases, Kustomizations and among others. These may be sent towards a tracing provider where may be potentially stored and visualized. Flux does not have any responsibility on storing and visualizing those, it keeps being completely stateless. Thereby, being seamless for the user, the implementation is going to be part of the already existing `Alert` API Type. Therefore, `EventSources` is going to discriminate the events belonging to the specific sources, which are going to be looked up to and send them out towards the `Provider` set. In this way, it could facilitate the observability and monitoring of Flux related objects. @@ -210,9 +205,4 @@ This design ensures trace continuity even in challenging distributed environment ## Implementation History - +* RFC implemented and generally available in Flux [v2.7.0](https://github.com/fluxcd/flux2/releases/tag/v2.7.0) diff --git a/rfcs/0012-external-artifact/README.md b/rfcs/0012-external-artifact/README.md index 602c4918..c8d21bbf 100644 --- a/rfcs/0012-external-artifact/README.md +++ b/rfcs/0012-external-artifact/README.md @@ -1,10 +1,10 @@ # RFC-0012 External Artifact -**Status:** provisional +**Status:** implemented **Creation date:** 2025-04-08 -**Last update:** 2025-09-03 +**Last update:** 2026-03-13 ## Summary @@ -319,9 +319,4 @@ control the adoption of the `ExternalArtifact` feature in their clusters. ## Implementation History - +* RFC implemented and generally available in Flux [v2.7.0](https://github.com/fluxcd/flux2/releases/tag/v2.7.0) diff --git a/tests/integration/go.mod b/tests/integration/go.mod index 1c4561af..9dd15723 100644 --- a/tests/integration/go.mod +++ b/tests/integration/go.mod @@ -11,10 +11,10 @@ require ( github.com/fluxcd/image-reflector-controller/api v1.0.4 github.com/fluxcd/kustomize-controller/api v1.7.3 github.com/fluxcd/notification-controller/api v1.7.5 - github.com/fluxcd/pkg/apis/event v0.24.1 - github.com/fluxcd/pkg/apis/meta v1.25.1 - github.com/fluxcd/pkg/git v0.43.1 - github.com/fluxcd/pkg/runtime v0.100.4 + github.com/fluxcd/pkg/apis/event v0.25.0 + github.com/fluxcd/pkg/apis/meta v1.26.0 + github.com/fluxcd/pkg/git v0.46.0 + github.com/fluxcd/pkg/runtime v0.103.0 github.com/fluxcd/source-controller/api v1.7.4 github.com/fluxcd/test-infra/tftestenv v0.0.0-20250626232827-e0ca9c3f8d7b github.com/go-git/go-git/v5 v5.16.5 @@ -66,9 +66,9 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect - github.com/fluxcd/pkg/apis/kustomize v1.15.1 // indirect + github.com/fluxcd/pkg/apis/kustomize v1.16.0 // indirect github.com/fluxcd/pkg/ssh v0.24.0 // indirect - github.com/fluxcd/pkg/version v0.12.0 // indirect + github.com/fluxcd/pkg/version v0.14.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.7.0 // indirect diff --git a/tests/integration/go.sum b/tests/integration/go.sum index 6b0e62eb..c4f3ba6d 100644 --- a/tests/integration/go.sum +++ b/tests/integration/go.sum @@ -136,22 +136,22 @@ github.com/fluxcd/notification-controller/api v1.7.5 h1:6CO5bKyjodiK9exQFOdBcz0X github.com/fluxcd/notification-controller/api v1.7.5/go.mod h1:IciwSg8Q0pVtdbsyDyEXx/MxBKWeagxAazpm64C8oCE= github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA= github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4= -github.com/fluxcd/pkg/apis/event v0.24.1 h1:TClVdn02aiq3sAl9BuzLjjTIxm3JJ83fJ9nchtBa4qg= -github.com/fluxcd/pkg/apis/event v0.24.1/go.mod h1:TlK8HWYrTwl0raqBRC+ROoNpYW5fdVnwcwOBOx5Kzw8= -github.com/fluxcd/pkg/apis/kustomize v1.15.1 h1:t9QZh+3ZS8EKmlxrnnbcKZcGTrg8FDvMF1T8BHMCuqI= -github.com/fluxcd/pkg/apis/kustomize v1.15.1/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI= -github.com/fluxcd/pkg/apis/meta v1.25.1 h1:WG1GIC/SOz0GjxT0uVuO6AMicQ3yFsk6bDozCnq+fto= -github.com/fluxcd/pkg/apis/meta v1.25.1/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0= -github.com/fluxcd/pkg/git v0.43.1 h1:lw29P44wueKzQk79KnYyvisfw//cxg0S4cDeTYx+Slo= -github.com/fluxcd/pkg/git v0.43.1/go.mod h1:3R/AjCe7ee7FqWcAG+2IiuJPOCxrGHF4SCGkuvKS6OQ= -github.com/fluxcd/pkg/gittestserver v0.25.1 h1:40Ridmy1xKxBM9ItDn012R4VKmaoDqzvGaC5g7xv+mw= -github.com/fluxcd/pkg/gittestserver v0.25.1/go.mod h1:7fybYb0yej1fFNiF1ohs0Jr0XzyaZQ/cRh3AFEoCtuc= -github.com/fluxcd/pkg/runtime v0.100.4 h1:rwvbeoeWN0BTJORJBISJJEkWn6DVfmWwynFl2GseWns= -github.com/fluxcd/pkg/runtime v0.100.4/go.mod h1:M6LjRJ1hIe2s6E2ykFfae1Xy/rLvOFQf2QquMKmN350= +github.com/fluxcd/pkg/apis/event v0.25.0 h1:zdwytvDhG+fk+Ywl5DOtv7TklkrVgM21WHm1f+YhleE= +github.com/fluxcd/pkg/apis/event v0.25.0/go.mod h1:TlK8HWYrTwl0raqBRC+ROoNpYW5fdVnwcwOBOx5Kzw8= +github.com/fluxcd/pkg/apis/kustomize v1.16.0 h1:PhWXEhqQqsisIpwp1/wHvTvo+MO+GGzsBPoN0ZnRE3Y= +github.com/fluxcd/pkg/apis/kustomize v1.16.0/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI= +github.com/fluxcd/pkg/apis/meta v1.26.0 h1:dxP1FfBpTCYso6odzRcltVnnRuBb2VyhhgV0VX9YbUE= +github.com/fluxcd/pkg/apis/meta v1.26.0/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0= +github.com/fluxcd/pkg/git v0.46.0 h1:QMh0+ZzQ2jO6rIGj4ffR5trZ8g/cxvt8cVajReJ8Iyw= +github.com/fluxcd/pkg/git v0.46.0/go.mod h1:iHcIjx9c8zye3PQiajTJYxgOMRiy7WCs+hfLKDswpfI= +github.com/fluxcd/pkg/gittestserver v0.26.0 h1:+RZrCzFRsE+d5WaqAoqaPCEgcgv/jZp6+f7DS0+Ynb8= +github.com/fluxcd/pkg/gittestserver v0.26.0/go.mod h1:7fybYb0yej1fFNiF1ohs0Jr0XzyaZQ/cRh3AFEoCtuc= +github.com/fluxcd/pkg/runtime v0.103.0 h1:J5y5GPhWdkyqIUBlaI1FP2N02TtZmsjbWhhZubuTSFk= +github.com/fluxcd/pkg/runtime v0.103.0/go.mod h1:mbo2f3azo3yVQgm7XZGxQB6/2zvzQ5Wgtd8TjRRwwAw= github.com/fluxcd/pkg/ssh v0.24.0 h1:hrPlxs0hhXf32DRqs68VbsXs0XfQMphyRVIk0rYYJa4= github.com/fluxcd/pkg/ssh v0.24.0/go.mod h1:xWammEqalrpurpcMiixJRXtynRQtBEoqheyU5F/vWrg= -github.com/fluxcd/pkg/version v0.12.0 h1:MGbdbNf2D5wazMqAkNPn+Lh5j+oY0gxQJFTGyet5Hfc= -github.com/fluxcd/pkg/version v0.12.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8= +github.com/fluxcd/pkg/version v0.14.0 h1:T3llSc8sUnsuFrW5ng2ePSfXwGXUKv0YG9QXf0ErhWw= +github.com/fluxcd/pkg/version v0.14.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8= github.com/fluxcd/source-controller/api v1.7.4 h1:+EOVnRA9LmLxOx7J273l7IOEU39m+Slt/nQGBy69ygs= github.com/fluxcd/source-controller/api v1.7.4/go.mod h1:ruf49LEgZRBfcP+eshl2n9SX1MfHayCcViAIGnZcaDY= github.com/fluxcd/test-infra/tftestenv v0.0.0-20250626232827-e0ca9c3f8d7b h1:FSPtvaVgL8azcyweqLmD71elAw4vozuXH/QvsJQ7tg0=