Repository: rancher/fleet
Pull request #3278 (2025-01-30 11:57:46)

Add affected cluster to template error message
Follow-up on #2943
Part of #3277

<!-- Specify the issue ID that this pull request is solving -->
<!-- Make sure that the referenced issue provides steps to reproduce it -->

<!-- Describe the changes introduced by this pull request -->
![image](https://github.com/user-attachments/assets/e1dc5ee8-fbcd-49db-96c0-2de505a1baed)

<!--
  Please provide a unit, integration (`./integrationtests/`) or e2e (`./e2e/`) test if possible.
-->


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 43fa931e..32e8f1a0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -81,6 +81,10 @@ jobs:
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v3
 
+      - name: Install Cosign
+        uses: sigstore/cosign-installer@v3.7.0
+      - uses: rancherlabs/slsactl/actions/install-slsactl@v0.0.8
+
       - name: "Read Vault Secrets"
         uses: rancher-eio/read-vault-secrets@main
         with:
@@ -89,6 +93,14 @@ jobs:
             secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
             secret/data/github/repo/${{ github.repository }}/rancher-prime-stg-registry/credentials username | STAGE_REGISTRY_USERNAME ;
             secret/data/github/repo/${{ github.repository }}/rancher-prime-stg-registry/credentials password | STAGE_REGISTRY_PASSWORD ;
+            secret/data/github/repo/${{ github.repository }}/rancher-prime-registry/credentials username | PRIME_REGISTRY_USERNAME ;
+            secret/data/github/repo/${{ github.repository }}/rancher-prime-registry/credentials password | PRIME_REGISTRY_PASSWORD ;
+            secret/data/github/repo/${{ github.repository }}/rancher-prime-registry/credentials registry | PRIME_REGISTRY;
+
+      - name: Prepare Docker environment to support sbom/provenance parameters
+        run: |
+          sudo bash -c 'echo "{\"features\": {\"containerd-snapshotter\": true}}" > /etc/docker/daemon.json'
+          sudo systemctl restart docker
 
       - name: Log into Docker Container registry
         uses: docker/login-action@v3
@@ -105,6 +117,13 @@ jobs:
           password: ${{ env.STAGE_REGISTRY_PASSWORD }}
           registry: ${{ env.REGISTRY }}
 
+      - name: Log into Prime registry
+        uses: docker/login-action@v3
+        with:
+          username: ${{ env.PRIME_REGISTRY_USERNAME }}
+          password: ${{ env.PRIME_REGISTRY_PASSWORD }}
+          registry: ${{ env.PRIME_REGISTRY }}
+
       - name: Run GoReleaser
         uses: goreleaser/goreleaser-action@v6
         id: goreleaser
@@ -115,6 +134,47 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GORELEASER_CURRENT_TAG: ${{ github.ref_name }}
+          PRIME_REGISTRY: ${{ env.PRIME_REGISTRY }}
+
+      # Workaround until `docker manifest create` supports provenance meta data
+      # Background: https://stackoverflow.com/questions/75521775/buildx-docker-image-claims-to-be-a-manifest-list
+      - name: Create Docker manifest for Prime and sign it
+        shell: bash
+        run: |
+            for IMAGE in fleet fleet-agent; do
+              URL="${{ env.PRIME_REGISTRY }}/rancher/${IMAGE}:${{ github.ref_name }}"
+              docker buildx imagetools create -t "${URL}" \
+                "${URL}-linux-amd64" \
+                "${URL}-linux-arm64"
+
+              cosign sign --oidc-provider=github-actions --yes "${URL}"
+            done
+
+      - name: Attest provenance
+        shell: bash
+        run: |
+          for IMG_NAME in $(yq e '.dockers[].image_templates[0]' .goreleaser.yaml | grep PRIME_REGISTRY | sed "s/{{ .Env.PRIME_REGISTRY }}/${{ env.PRIME_REGISTRY }}/g" | sed "s/{{ .Tag }}/${{ github.ref_name }}/g"); do
+            # Extract Docker image reference plus digest from local image
+            URL=$(docker inspect --format='{{index .RepoDigests 0}}' ${IMG_NAME})
+
+            max_retries=3
+            retry_delay=5
+            i=0
+
+            while [ "${i}" -lt "${max_retries}" ]; do
+              if slsactl download provenance --format=slsav1 "${URL}" > provenance-slsav1.json; then
+                break
+              fi
+              if [ "${i}" -eq "$(( max_retries - 1 ))" ]; then
+                echo "ERROR: Failed to generate slsav1 provenance. Check whether the image is present in the Prime registry."
+                exit 1
+              fi
+              i=$(( i + 1 ))
+              sleep "${retry_delay}"
+            done
+
+            cosign attest --yes --predicate provenance-slsav1.json --type slsaprovenance1 "${URL}"
+          done
 
       - name: Upload charts to release
         env:
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 24425dd5..86fd1d3e 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -22,19 +22,19 @@ before:
 
 archives:
   - id: fleet-controller
-    format: binary
+    formats: [ "binary" ]
     name_template: "{{ .Binary }}"
     builds:
       - fleet-controller
 
   - id: fleet-agent
-    format: binary
+    formats: [ "binary" ]
     name_template: "{{ .Binary }}"
     builds:
       - fleet-agent
 
   - id: fleet-cli
-    format: binary
+    formats: [ "binary" ]
     name_template: "{{ .Binary }}"
     builds:
       - fleet-cli
@@ -82,12 +82,13 @@ builds:
     - windows_amd64
 
 snapshot:
-  name_template: "{{ .Tag }}-next"
+  version_template: "{{ .Tag }}-next"
 
 dockers:
   # fleet-controller images
   -
     use: buildx
+    id: fleet-amd64-public
 
     # GOOS of the built binaries/packages that should be used.
     goos: linux
@@ -121,6 +122,7 @@ dockers:
     extra_files: [ "package/log.sh" ]
   -
     use: buildx
+    id: fleet-arm64-public
     goos: linux
     goarch: arm64
     ids:
@@ -143,6 +145,7 @@ dockers:
   # fleet-agent images
   -
     use: buildx
+    id: fleet-agent-amd64-public
     goos: linux
     goarch: amd64
     ids:
@@ -162,6 +165,7 @@ dockers:
     - "--platform=linux/amd64"
   -
     use: buildx
+    id: fleet-agent-arm64-public
     goos: linux
     goarch: arm64
     ids:
@@ -180,14 +184,183 @@ dockers:
     - "--build-arg=BUILD_ENV=goreleaser"
     - "--platform=linux/arm64/v8"
 
+  # Prime fleet-controller images
+  -
+    use: buildx
+    id: fleet-amd64-private
+
+    # GOOS of the built binaries/packages that should be used.
+    goos: linux
+
+    # GOARCH of the built binaries/packages that should be used.
+    goarch: amd64
+
+    # IDs to filter the binaries/packages.
+    ids:
+    - fleet-controller
+    - fleet-cli
+
+    # Templates of the Docker image names.
+    image_templates:
+    - "{{ .Env.PRIME_REGISTRY }}/rancher/fleet:{{ .Tag }}-linux-amd64"
+
+    # Path to the Dockerfile (from the project root).
+    dockerfile: package/Dockerfile
+
+    # Template of the docker build flags.
+    build_flag_templates:
+    - "--pull"
+    - "--label=org.opencontainers.image.created={{.Date}}"
+    - "--label=org.opencontainers.image.title={{.ProjectName}}"
+    - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+    - "--label=org.opencontainers.image.version={{.Version}}"
+    - "--label=org.opencontainers.image.source=https://github.com/rancher/fleet"
+    - "--build-arg=ARCH=amd64"
+    - "--build-arg=BUILD_ENV=goreleaser"
+    - "--platform=linux/amd64"
+    - "--sbom=true"
+    - "--provenance=true"
+    - "--provenance=mode=max"
+    extra_files: [ "package/log.sh" ]
+  -
+    use: buildx
+    id: fleet-arm64-private
+    goos: linux
+    goarch: arm64
+    ids:
+    - fleet-controller
+    - fleet-cli
+    image_templates:
+    - "{{ .Env.PRIME_REGISTRY }}/rancher/fleet:{{ .Tag }}-linux-arm64"
+    dockerfile: package/Dockerfile
+    build_flag_templates:
+    - "--pull"
+    - "--label=org.opencontainers.image.created={{.Date}}"
+    - "--label=org.opencontainers.image.title={{.ProjectName}}"
+    - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+    - "--label=org.opencontainers.image.version={{.Version}}"
+    - "--label=org.opencontainers.image.source=https://github.com/rancher/fleet"
+    - "--build-arg=ARCH=arm64"
+    - "--build-arg=BUILD_ENV=goreleaser"
+    - "--platform=linux/arm64/v8"
+    - "--sbom=true"
+    - "--provenance=true"
+    - "--provenance=mode=max"
+    extra_files: [ "package/log.sh" ]
+  # fleet-agent images
+  -
+    use: buildx
+    id: fleet-agent-amd64-private
+    goos: linux
+    goarch: amd64
+    ids:
+    - fleet-agent
+    image_templates:
+    - "{{ .Env.PRIME_REGISTRY }}/rancher/fleet-agent:{{ .Tag }}-linux-amd64"
+    dockerfile: package/Dockerfile.agent
+    build_flag_templates:
+    - "--pull"
+    - "--label=org.opencontainers.image.created={{.Date}}"
+    - "--label=org.opencontainers.image.title={{.ProjectName}}"
+    - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+    - "--label=org.opencontainers.image.version={{.Version}}"
+    - "--label=org.opencontainers.image.source=https://github.com/rancher/fleet"
+    - "--build-arg=ARCH=amd64"
+    - "--build-arg=BUILD_ENV=goreleaser"
+    - "--platform=linux/amd64"
+    - "--sbom=true"
+    - "--provenance=true"
+    - "--provenance=mode=max"
+  -
+    use: buildx
+    id: fleet-agent-arm64-private
+    goos: linux
+    goarch: arm64
+    ids:
+    - fleet-agent
+    image_templates:
+    - "{{ .Env.PRIME_REGISTRY }}/rancher/fleet-agent:{{ .Tag }}-linux-arm64"
+    dockerfile: package/Dockerfile.agent
+    build_flag_templates:
+    - "--pull"
+    - "--label=org.opencontainers.image.created={{.Date}}"
+    - "--label=org.opencontainers.image.title={{.ProjectName}}"
+    - "--label=org.opencontainers.image.revision={{.FullCommit}}"
+    - "--label=org.opencontainers.image.version={{.Version}}"
+    - "--label=org.opencontainers.image.source=https://github.com/rancher/fleet"
+    - "--build-arg=ARCH=arm64"
+    - "--build-arg=BUILD_ENV=goreleaser"
+    - "--platform=linux/arm64/v8"
+    - "--sbom=true"
+    - "--provenance=true"
+    - "--provenance=mode=max"
+
+
 docker_manifests:
   # https://goreleaser.com/customization/docker_manifest/
   - name_template: "{{ .Env.REGISTRY }}/rancher/fleet:{{ .Tag }}"
+    id: fleet-manifest-public
     image_templates:
     - "{{ .Env.REGISTRY }}/rancher/fleet:{{ .Tag }}-linux-amd64"
     - "{{ .Env.REGISTRY }}/rancher/fleet:{{ .Tag }}-linux-arm64"
 
   - name_template: "{{ .Env.REGISTRY }}/rancher/fleet-agent:{{ .Tag }}"
+    id: fleet-agent-manifest-public
     image_templates:
     - "{{ .Env.REGISTRY }}/rancher/fleet-agent:{{ .Tag }}-linux-amd64"
     - "{{ .Env.REGISTRY }}/rancher/fleet-agent:{{ .Tag }}-linux-arm64"
+
+    # Disabled until `docker manifest create` supports provenance meta data
+    # - name_template: "{{ .Env.PRIME_REGISTRY }}/rancher/fleet:{{ .Tag }}"
+    #   id: fleet-manifest-private
+    #   image_templates:
+    #   - "{{ .Env.PRIME_REGISTRY }}/rancher/fleet:{{ .Tag }}-linux-amd64"
+    #   - "{{ .Env.PRIME_REGISTRY }}/rancher/fleet:{{ .Tag }}-linux-arm64"
+    #
+    #  - name_template: "{{ .Env.PRIME_REGISTRY }}/rancher/fleet-agent:{{ .Tag }}"
+    #    id: fleet-agent-manifest-private
+    #    image_templates:
+    #    - "{{ .Env.PRIME_REGISTRY }}/rancher/fleet-agent:{{ .Tag }}-linux-amd64"
+    #    - "{{ .Env.PRIME_REGISTRY }}/rancher/fleet-agent:{{ .Tag }}-linux-arm64"
+
+docker_signs:
+  - # ID of the sign config, must be unique.
+    # Only relevant if you want to produce some sort of signature file.
+    #
+    # Default: 'default'.
+    id: prime
+
+    # Path to the signature command.
+    #
+    # Default: 'cosign'.
+    cmd: cosign
+
+    # Command line arguments for the command.
+    #
+    # Default: ["sign", "--key=cosign.key", "${artifact}", "--yes"].
+    # Templates: allowed.
+    args:
+      - "sign"
+      - "--oidc-provider=github-actions"
+      - "--yes" # needed on cosign 2.0.0+
+      - "${artifact}@${digest}"
+
+    # Which artifacts to sign.
+    #
+    #   all:       all artifacts
+    #   none:      no signing
+    #   images:    only docker images
+    #   manifests: only docker manifests
+    #
+    # Default: 'none'.
+    artifacts: all
+
+    # IDs of the artifacts to sign.
+    ids:
+      - fleet-amd64-private
+      - fleet-arm64-private
+      - fleet-agent-amd64-private
+      - fleet-agent-arm64-private
+      # Disabled until `docker manifest create` supports provenance meta data
+      #  - fleet-manifest-private
+      #  - fleet-agent-manifest-private
\ No newline at end of file
diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml
index a6d752fe..a15cb88d 100644
--- a/charts/fleet-crd/templates/crds.yaml
+++ b/charts/fleet-crd/templates/crds.yaml
@@ -349,6 +349,24 @@ spec:
                           description: TakeOwnership makes helm skip the check for
                             its own annotations
                           type: boolean
+                        templateValues:
+                          additionalProperties:
+                            type: string
+                          description: 'Template Values passed to Helm. It is possible
+                            to specify the keys and values
+
+                            as go template strings. Unlike .values, content of each
+                            key will be templated
+
+                            first, before serializing to yaml. This allows to template
+                            complex values,
+
+                            like ranges and maps.
+
+                            templateValues keys have precedence over values keys in
+                            case of conflict.'
+                          nullable: true
+                          type: object
                         timeoutSeconds:
                           description: TimeoutSeconds is the time to wait for Helm
                             operations.
@@ -695,6 +713,24 @@ spec:
                           description: TakeOwnership makes helm skip the check for
                             its own annotations
                           type: boolean
+                        templateValues:
+                          additionalProperties:
+                            type: string
+                          description: 'Template Values passed to Helm. It is possible
+                            to specify the keys and values
+
+                            as go template strings. Unlike .values, content of each
+                            key will be templated
+
+                            first, before serializing to yaml. This allows to template
+                            complex values,
+
+                            like ranges and maps.
+
+                            templateValues keys have precedence over values keys in
+                            case of conflict.'
+                          nullable: true
+                          type: object
                         timeoutSeconds:
                           description: TimeoutSeconds is the time to wait for Helm
                             operations.
@@ -1568,6 +1604,24 @@ spec:
                       description: TakeOwnership makes helm skip the check for its
                         own annotations
                       type: boolean
+                    templateValues:
+                      additionalProperties:
+                        type: string
+                      description: 'Template Values passed to Helm. It is possible
+                        to specify the keys and values
+
+                        as go template strings. Unlike .values, content of each key
+                        will be templated
+
+                        first, before serializing to yaml. This allows to template
+                        complex values,
+
+                        like ranges and maps.
+
+                        templateValues keys have precedence over values keys in case
+                        of conflict.'
+                      nullable: true
+                      type: object
                     timeoutSeconds:
                       description: TimeoutSeconds is the time to wait for Helm operations.
                       type: integer
@@ -2433,6 +2487,24 @@ spec:
                             description: TakeOwnership makes helm skip the check for
                               its own annotations
                             type: boolean
+                          templateValues:
+                            additionalProperties:
+                              type: string
+                            description: 'Template Values passed to Helm. It is possible
+                              to specify the keys and values
+
+                              as go template strings. Unlike .values, content of each
+                              key will be templated
+
+                              first, before serializing to yaml. This allows to template
+                              complex values,
+
+                              like ranges and maps.
+
+                              templateValues keys have precedence over values keys
+                              in case of conflict.'
+                            nullable: true
+                            type: object
                           timeoutSeconds:
                             description: TimeoutSeconds is the time to wait for Helm
                               operations.
@@ -7307,6 +7379,24 @@ spec:
                       description: TakeOwnership makes helm skip the check for its
                         own annotations
                       type: boolean
+                    templateValues:
+                      additionalProperties:
+                        type: string
+                      description: 'Template Values passed to Helm. It is possible
+                        to specify the keys and values
+
+                        as go template strings. Unlike .values, content of each key
+                        will be templated
+
+                        first, before serializing to yaml. This allows to template
+                        complex values,
+
+                        like ranges and maps.
+
+                        templateValues keys have precedence over values keys in case
+                        of conflict.'
+                      nullable: true
+                      type: object
                     timeoutSeconds:
                       description: TimeoutSeconds is the time to wait for Helm operations.
                       type: integer
@@ -8191,6 +8281,24 @@ spec:
                             description: TakeOwnership makes helm skip the check for
                               its own annotations
                             type: boolean
+                          templateValues:
+                            additionalProperties:
+                              type: string
+                            description: 'Template Values passed to Helm. It is possible
+                              to specify the keys and values
+
+                              as go template strings. Unlike .values, content of each
+                              key will be templated
+
+                              first, before serializing to yaml. This allows to template
+                              complex values,
+
+                              like ranges and maps.
+
+                              templateValues keys have precedence over values keys
+                              in case of conflict.'
+                            nullable: true
+                            type: object
                           timeoutSeconds:
                             description: TimeoutSeconds is the time to wait for Helm
                               operations.
diff --git a/charts/fleet/templates/job_cleanup_clusterregistrations.yaml b/charts/fleet/templates/job_cleanup_clusterregistrations.yaml
index d039fd40..9bb67026 100644
--- a/charts/fleet/templates/job_cleanup_clusterregistrations.yaml
+++ b/charts/fleet/templates/job_cleanup_clusterregistrations.yaml
@@ -37,5 +37,8 @@ spec:
         - clusterregistration
       nodeSelector: {{ include "linux-node-selector" . | nindent 8 }}
       tolerations: {{ include "linux-node-tolerations" . | nindent 8 }}
+{{- if $.Values.tolerations }}
+{{ toYaml $.Values.tolerations | indent 8 }}
+{{- end }}
   backoffLimit: 1
 {{- end }}
diff --git a/charts/fleet/templates/job_cleanup_gitrepojobs.yaml b/charts/fleet/templates/job_cleanup_gitrepojobs.yaml
index c7233630..f1a3563a 100644
--- a/charts/fleet/templates/job_cleanup_gitrepojobs.yaml
+++ b/charts/fleet/templates/job_cleanup_gitrepojobs.yaml
@@ -40,5 +40,8 @@ spec:
             - gitjob
           nodeSelector: {{ include "linux-node-selector" . | nindent 12 }}
           tolerations: {{ include "linux-node-tolerations" . | nindent 12 }}
+{{- if $.Values.tolerations }}
+{{ toYaml $.Values.tolerations | indent 12 }}
+{{- end }}
       backoffLimit: 1
 {{- end }}
diff --git a/go.mod b/go.mod
index 7704ca28..2a7c72d9 100644
--- a/go.mod
+++ b/go.mod
@@ -38,7 +38,7 @@ require (
 	github.com/prometheus/client_model v0.6.1
 	github.com/prometheus/common v0.62.0
 	github.com/rancher/fleet/pkg/apis v0.11.3
-	github.com/rancher/lasso v0.0.0-20250123080302-9325fed68518
+	github.com/rancher/lasso v0.2.0
 	github.com/rancher/wrangler/v3 v3.1.0
 	github.com/reugn/go-quartz v0.13.0
 	github.com/sirupsen/logrus v1.9.3
diff --git a/go.sum b/go.sum
index 3019b5d9..3713f9b9 100644
--- a/go.sum
+++ b/go.sum
@@ -1229,8 +1229,8 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
 github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
 github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
-github.com/rancher/lasso v0.0.0-20250123080302-9325fed68518 h1:aElNUJg6kxTvs/e4yfMEkk0Dgzo/lyHl85xDqP+FC+A=
-github.com/rancher/lasso v0.0.0-20250123080302-9325fed68518/go.mod h1:stR7zYyew1IOnKYV5vFx1kXX5/pUoKeo5K5c78qAdV8=
+github.com/rancher/lasso v0.2.0 h1:0YaprDYRZNMQoG9/308ZI+oxvof2JUjo9rYT2It38L8=
+github.com/rancher/lasso v0.2.0/go.mod h1:stR7zYyew1IOnKYV5vFx1kXX5/pUoKeo5K5c78qAdV8=
 github.com/rancher/wrangler/v3 v3.1.0 h1:8ETBnQOEcZaR6WBmUSysWW7WnERBOiNTMJr4Dj3UG/s=
 github.com/rancher/wrangler/v3 v3.1.0/go.mod h1:gUPHS1ANs2NyByfeERHwkGiQ1rlIa8BpTJZtNSgMlZw=
 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
diff --git a/internal/cmd/controller/gitops/reconciler/status_controller.go b/internal/cmd/controller/gitops/reconciler/status_controller.go
index e99972a6..76e44f77 100644
--- a/internal/cmd/controller/gitops/reconciler/status_controller.go
+++ b/internal/cmd/controller/gitops/reconciler/status_controller.go
@@ -154,7 +154,7 @@ func setStatus(list *fleet.BundleDeploymentList, gitrepo *fleet.GitRepo) error {
 		return err
 	}
 
-	resourcestatus.SetResources(list, &gitrepo.Status.StatusBase)
+	resourcestatus.SetResources(list.Items, &gitrepo.Status.StatusBase)
 
 	summary.SetReadyConditions(&gitrepo.Status, "Bundle", gitrepo.Status.Summary)
 
diff --git a/internal/cmd/controller/helmops/reconciler/helmapp_status.go b/internal/cmd/controller/helmops/reconciler/helmapp_status.go
index 1b7f470b..38431175 100644
--- a/internal/cmd/controller/helmops/reconciler/helmapp_status.go
+++ b/internal/cmd/controller/helmops/reconciler/helmapp_status.go
@@ -121,7 +121,7 @@ func setStatusHelm(list *fleet.BundleDeploymentList, helmapp *fleet.HelmApp) err
 		return err
 	}
 
-	resourcestatus.SetResources(list, &helmapp.Status.StatusBase)
+	resourcestatus.SetResources(list.Items, &helmapp.Status.StatusBase)
 
 	summary.SetReadyConditions(&helmapp.Status, "Bundle", helmapp.Status.Summary)
 
diff --git a/internal/cmd/controller/options/calculate.go b/internal/cmd/controller/options/calculate.go
index 2f240cf3..3c182777 100644
--- a/internal/cmd/controller/options/calculate.go
+++ b/internal/cmd/controller/options/calculate.go
@@ -5,6 +5,7 @@ import (
 	"crypto/sha256"
 	"encoding/hex"
 	"encoding/json"
+	"maps"
 
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/wrangler/v3/pkg/data"
@@ -52,6 +53,11 @@ func Merge(base, custom fleet.BundleDeploymentOptions) fleet.BundleDeploymentOpt
 		} else if custom.Helm.Values != nil {
 			result.Helm.Values.Data = data.MergeMaps(result.Helm.Values.Data, custom.Helm.Values.Data)
 		}
+		if result.Helm.TemplateValues == nil {
+			result.Helm.TemplateValues = custom.Helm.TemplateValues
+		} else if custom.Helm.TemplateValues != nil {
+			maps.Copy(result.Helm.TemplateValues, custom.Helm.TemplateValues)
+		}
 		if custom.Helm.ValuesFrom != nil {
 			result.Helm.ValuesFrom = append(result.Helm.ValuesFrom, custom.Helm.ValuesFrom...)
 		}
diff --git a/internal/cmd/controller/reconciler/cluster_controller.go b/internal/cmd/controller/reconciler/cluster_controller.go
index b5e15161..01d89a66 100644
--- a/internal/cmd/controller/reconciler/cluster_controller.go
+++ b/internal/cmd/controller/reconciler/cluster_controller.go
@@ -141,9 +141,8 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 	// increased log level, this triggers a lot
 	logger.V(4).Info("Reconciling cluster, cleaning old bundledeployments and updating status", "oldDisplay", cluster.Status.Display)
 
-	bundleDeployments := &fleet.BundleDeploymentList{}
-	err = r.List(ctx, bundleDeployments, client.InNamespace(cluster.Status.Namespace))
-	if err != nil {
+	bundleDeploymentList := &fleet.BundleDeploymentList{}
+	if err = r.List(ctx, bundleDeploymentList, client.InNamespace(cluster.Status.Namespace)); err != nil {
 		return ctrl.Result{}, r.updateErrorStatus(ctx, req.NamespacedName, cluster.Status, err)
 	}
 
@@ -155,7 +154,7 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 	toDeleteBundles := indexByNamespacedName(cleanup)
 
 	// Delete BundleDeployments for Bundles being removed while getting a filtered items list
-	bundleDeployments.Items = slices.DeleteFunc(bundleDeployments.Items, func(bd fleet.BundleDeployment) bool {
+	bundleDeployments := slices.DeleteFunc(bundleDeploymentList.Items, func(bd fleet.BundleDeployment) bool {
 		bundleNamespace := bd.Labels[fleet.BundleNamespaceLabel]
 		bundleName := bd.Labels[fleet.BundleLabel]
 		if _, ok := toDeleteBundles[types.NamespacedName{Namespace: bundleNamespace, Name: bundleName}]; ok {
@@ -174,14 +173,14 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 	cluster.Status.ResourceCounts = fleet.ResourceCounts{}
 	cluster.Status.Summary = fleet.BundleSummary{}
 
-	sort.Slice(bundleDeployments.Items, func(i, j int) bool {
-		return bundleDeployments.Items[i].Name < bundleDeployments.Items[j].Name
+	sort.Slice(bundleDeployments, func(i, j int) bool {
+		return bundleDeployments[i].Name < bundleDeployments[j].Name
 	})
 
 	resourcestatus.SetClusterResources(bundleDeployments, cluster)
 
 	repos := map[types.NamespacedName]bool{}
-	for _, bd := range bundleDeployments.Items {
+	for _, bd := range bundleDeployments {
 		state := summary.GetDeploymentState(&bd)
 		summary.IncrementState(&cluster.Status.Summary, bd.Name, state, summary.MessageFromDeployment(&bd), bd.Status.ModifiedStatus, bd.Status.NonReadyStatus)
 		cluster.Status.Summary.DesiredReady++
diff --git a/internal/cmd/controller/target/builder.go b/internal/cmd/controller/target/builder.go
index 27f52980..b221db76 100644
--- a/internal/cmd/controller/target/builder.go
+++ b/internal/cmd/controller/target/builder.go
@@ -2,14 +2,17 @@ package target
 
 import (
 	"bytes"
+	"cmp"
 	"context"
 	"fmt"
+	"maps"
 	"sort"
 	"strings"
 	"text/template"
 
 	"github.com/Masterminds/sprig/v3"
 	"github.com/go-logr/logr"
+	"github.com/pkg/errors"
 
 	"github.com/rancher/fleet/internal/cmd/controller/options"
 	"github.com/rancher/fleet/internal/cmd/controller/target/matcher"
@@ -83,7 +86,7 @@ func (m *Manager) Targets(ctx context.Context, bundle *fleet.Bundle, manifestID
 			opts := options.Merge(bundle.Spec.BundleDeploymentOptions, targetOpts)
 			err = preprocessHelmValues(logger, &opts, &cluster)
 			if err != nil {
-				return nil, err
+				return nil, errors.Wrap(err, fmt.Sprintf("cluster %s in namespace %s", cluster.Name, cluster.Namespace))
 			}
 
 			deploymentID, err := options.DeploymentID(manifestID, opts)
@@ -182,10 +185,15 @@ func preprocessHelmValues(logger logr.Logger, opts *fleet.BundleDeploymentOption
 	}
 
 	opts.Helm = opts.Helm.DeepCopy()
-	if opts.Helm.Values == nil || opts.Helm.Values.Data == nil {
-		opts.Helm.Values = &fleet.GenericMap{
-			Data: map[string]interface{}{},
-		}
+	opts.Helm.Values = cmp.Or(opts.Helm.Values, &fleet.GenericMap{
+		Data: map[string]interface{}{},
+	})
+
+	if opts.Helm.Values.Data == nil {
+		opts.Helm.Values.Data = map[string]interface{}{}
+	}
+
+	if opts.Helm.TemplateValues == nil && len(opts.Helm.Values.Data) == 0 {
 		return nil
 	}
 
@@ -211,6 +219,14 @@ func preprocessHelmValues(logger logr.Logger, opts *fleet.BundleDeploymentOption
 		if err != nil {
 			return err
 		}
+
+		templatedData, err := processTemplateValuesData(opts.Helm.TemplateValues, values)
+		if err != nil {
+			return err
+		}
+
+		maps.Copy(opts.Helm.Values.Data, templatedData)
+
 		logger.V(4).Info("preProcess completed", "releaseName", opts.Helm.ReleaseName)
 	}
 
@@ -278,6 +294,38 @@ func tplFuncMap() template.FuncMap {
 	return f
 }
 
+func processTemplateValuesData(helmTemplateData map[string]string, templateContext map[string]interface{}) (map[string]interface{}, error) {
+	renderedValues := make(map[string]interface{}, len(helmTemplateData))
+
+	for k, v := range helmTemplateData {
+		// fleet.yaml must be valid yaml, however '{}[]' are YAML control
+		// characters and will be interpreted as JSON data structures. This
+		// causes issues when parsing the fleet.yaml so we change the delims
+		// for templating to '${ }'
+		tmpl := template.New("values").Funcs(tplFuncMap()).Option("missingkey=error").Delims("${", "}")
+		tmpl, err := tmpl.Parse(v)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse helm values template: %w", err)
+		}
+
+		var b bytes.Buffer
+		err = tmpl.Execute(&b, templateContext)
+		if err != nil {
+			return nil, fmt.Errorf("failed to render helm values template: %w", err)
+		}
+
+		var value interface{}
+		err = kyaml.Unmarshal(b.Bytes(), &value)
+		if err != nil {
+			return nil, fmt.Errorf("failed to interpret rendered template as helm values: %s, %v", b.String(), err)
+		}
+
+		renderedValues[k] = value
+	}
+
+	return renderedValues, nil
+}
+
 func processTemplateValues(helmValues map[string]interface{}, templateContext map[string]interface{}) (map[string]interface{}, error) {
 	data, err := kyaml.Marshal(helmValues)
 	if err != nil {
diff --git a/internal/resourcestatus/resourcekey.go b/internal/resourcestatus/resourcekey.go
index bb5bf1d1..f7c1aa72 100644
--- a/internal/resourcestatus/resourcekey.go
+++ b/internal/resourcestatus/resourcekey.go
@@ -9,7 +9,7 @@ import (
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 )
 
-func SetResources(list *fleet.BundleDeploymentList, status *fleet.StatusBase) {
+func SetResources(list []fleet.BundleDeployment, status *fleet.StatusBase) {
 	byCluster, errors := fromResources(list)
 	status.ResourceErrors = errors
 	status.Resources = aggregateResourceStatesClustersMap(byCluster)
@@ -17,7 +17,7 @@ func SetResources(list *fleet.BundleDeploymentList, status *fleet.StatusBase) {
 	status.PerClusterResourceCounts = resourceCountsPerCluster(list)
 }
 
-func SetClusterResources(list *fleet.BundleDeploymentList, cluster *fleet.Cluster) {
+func SetClusterResources(list []fleet.BundleDeployment, cluster *fleet.Cluster) {
 	cluster.Status.ResourceCounts = sumResourceCounts(list)
 }
 
@@ -25,9 +25,9 @@ func key(resource fleet.Resource) string {
 	return resource.Type + "/" + resource.ID
 }
 
-func resourceCountsPerCluster(list *fleet.BundleDeploymentList) map[string]*fleet.ResourceCounts {
+func resourceCountsPerCluster(items []fleet.BundleDeployment) map[string]*fleet.ResourceCounts {
 	res := make(map[string]*fleet.ResourceCounts)
-	for _, bd := range list.Items {
+	for _, bd := range items {
 		clusterID := bd.Labels[fleet.ClusterNamespaceLabel] + "/" + bd.Labels[fleet.ClusterLabel]
 		if _, ok := res[clusterID]; !ok {
 			res[clusterID] = &fleet.ResourceCounts{}
@@ -44,14 +44,21 @@ type resourceStateEntry struct {
 
 type resourceStatesByResourceKey map[fleet.ResourceKey][]resourceStateEntry
 
+func clusterID(bd fleet.BundleDeployment) string {
+	return bd.Labels[fleet.ClusterNamespaceLabel] + "/" + bd.Labels[fleet.ClusterLabel]
+}
+
 // fromResources inspects a list of BundleDeployments and returns a list of per-cluster states per resource keys.
 // It also returns a list of errors messages produced that may have occurred during processing
-func fromResources(list *fleet.BundleDeploymentList) (resourceStatesByResourceKey, []string) {
+func fromResources(items []fleet.BundleDeployment) (resourceStatesByResourceKey, []string) {
+	sort.Slice(items, func(i, j int) bool {
+		return clusterID(items[i]) < clusterID(items[j])
+	})
 	var (
 		errors    []string
 		resources = make(resourceStatesByResourceKey)
 	)
-	for _, bd := range list.Items {
+	for _, bd := range items {
 		clusterID := bd.Labels[fleet.ClusterNamespaceLabel] + "/" + bd.Labels[fleet.ClusterLabel]
 
 		bdResources, errs := bundleDeploymentResources(bd)
@@ -65,11 +72,6 @@ func fromResources(list *fleet.BundleDeploymentList) (resourceStatesByResourceKe
 			resources[key] = append(resources[key], resourceStateEntry{state, bd.Status.IncompleteState})
 		}
 	}
-	for _, entries := range resources {
-		sort.Slice(entries, func(i, j int) bool {
-			return entries[i].ClusterID < entries[j].ClusterID
-		})
-	}
 
 	sort.Strings(errors)
 
@@ -197,9 +199,9 @@ func aggregateResourceStatesClustersMap(resourceKeyStates resourceStatesByResour
 	return result
 }
 
-func sumResourceCounts(list *fleet.BundleDeploymentList) fleet.ResourceCounts {
+func sumResourceCounts(items []fleet.BundleDeployment) fleet.ResourceCounts {
 	var res fleet.ResourceCounts
-	for _, bd := range list.Items {
+	for _, bd := range items {
 		summary.IncrementResourceCounts(&res, bd.Status.ResourceCounts)
 	}
 	return res
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
index ecfb9f28..26af4d07 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/bundledeployment_types.go
@@ -211,6 +211,14 @@ type HelmOptions struct {
 	// +kubebuilder:validation:XPreserveUnknownFields
 	Values *GenericMap `json:"values,omitempty"`
 
+	// Template Values passed to Helm. It is possible to specify the keys and values
+	// as go template strings. Unlike .values, content of each key will be templated
+	// first, before serializing to yaml. This allows to template complex values,
+	// like ranges and maps.
+	// templateValues keys have precedence over values keys in case of conflict.
+	// +nullable
+	TemplateValues map[string]string `json:"templateValues,omitempty"`
+
 	// +nullable
 	// ValuesFrom loads the values from configmaps and secrets.
 	ValuesFrom []ValuesFrom `json:"valuesFrom,omitempty"`
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
index 78dd5f62..d78d397a 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated.deepcopy.go
@@ -1681,6 +1681,13 @@ func (in *HelmOptions) DeepCopyInto(out *HelmOptions) {
 		in, out := &in.Values, &out.Values
 		*out = (*in).DeepCopy()
 	}
+	if in.TemplateValues != nil {
+		in, out := &in.TemplateValues, &out.TemplateValues
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 	if in.ValuesFrom != nil {
 		in, out := &in.ValuesFrom, &out.ValuesFrom
 		*out = make([]ValuesFrom, len(*in))


Test patch:
diff --git a/internal/cmd/controller/target/target_test.go b/internal/cmd/controller/target/target_test.go
index da084c78..6ea1258e 100644
--- a/internal/cmd/controller/target/target_test.go
+++ b/internal/cmd/controller/target/target_test.go
@@ -112,6 +112,17 @@ func TestProcessLabelValues(t *testing.T) {
 const bundleYamlWithTemplate = `namespace: default
 helm:
   releaseName: labels
+  templateValues:
+    mapData: |
+      ${- range $key := .ClusterValues.items }
+      "${ $key }":
+        nested: "true"
+      ${- end}
+    listData: |
+      ${- range $key := .ClusterValues.items }
+      - "${ $key }":
+        nested: "true"
+      ${- end}
   values:
     clusterName: "${ .ClusterLabels.name }"
     fromAnnotation: "${ .ClusterAnnotations.testAnnotation }"
@@ -154,6 +165,10 @@ func TestProcessTemplateValues(t *testing.T) {
 				"thirdTier": "bar",
 			},
 		},
+		"items": []string{
+			"one",
+			"two",
+		},
 		"list": []string{
 			"alpha",
 			"beta",
@@ -187,7 +202,7 @@ func TestProcessTemplateValues(t *testing.T) {
 
 	templatedValues, err := processTemplateValues(bundle.Helm.Values.Data, values)
 	if err != nil {
-		t.Fatalf("error during label processing %v", err)
+		t.Fatalf("error during templated values processing %v", err)
 	}
 
 	clusterName, ok := templatedValues["clusterName"]
@@ -314,6 +329,70 @@ func TestProcessTemplateValues(t *testing.T) {
 		t.Fatal("join func was not right")
 	}
 
+	templatedValuesData, err := processTemplateValuesData(bundle.Helm.TemplateValues, values)
+	if err != nil {
+		t.Fatalf("error during templated values processing %v", err)
+	}
+
+	mapData, ok := templatedValuesData["mapData"].(map[string]interface{})
+	if !ok {
+		t.Fatal("mapData not found")
+	}
+
+	one, ok := mapData["one"]
+	if !ok {
+		t.Fatal("unable to find key one")
+	}
+
+	oneData, ok := one.(map[string]interface{})
+	if !ok {
+		t.Fatal("one key was not right")
+	}
+
+	if oneData["nested"].(string) != "true" {
+		t.Fatal("one value was not right")
+	}
+
+	two, ok := mapData["two"]
+	if !ok {
+		t.Fatal("unable to find key two")
+	}
+
+	twoData, ok := two.(map[string]interface{})
+	if !ok {
+		t.Fatal("two key was not right")
+	}
+
+	if twoData["nested"].(string) != "true" {
+		t.Fatal("two value was not right")
+	}
+
+	listData, ok := templatedValuesData["listData"].([]interface{})
+	if !ok {
+		t.Fatal("listData not found")
+	}
+
+	if len(listData) != 2 {
+		t.Fatal("unable to find all listData keys")
+	}
+
+	oneListData, ok := listData[0].(map[string]interface{})
+	if !ok {
+		t.Fatal("oneListData key is not right")
+	}
+
+	if oneListData["nested"] != "true" {
+		t.Fatal("oneListData item is missing")
+	}
+
+	twoListData, ok := listData[1].(map[string]interface{})
+	if !ok {
+		t.Fatal("twoListData key is not right")
+	}
+
+	if twoListData["nested"] != "true" {
+		t.Fatal("twoListData item is missing")
+	}
 }
 
 const clusterYamlWithTemplateValues = `apiVersion: fleet.cattle.io/v1alpha1
@@ -393,7 +472,7 @@ func TestDisablePreProcessFlagEnabled(t *testing.T) {
 			t.Fatalf("key %s not found", testCase.Key)
 		} else {
 			if field != testCase.ExpectedValue {
-				t.Fatalf("key %s was not the expected value. Expected: '%s' Actual: '%s'", testCase.Key, field, testCase.ExpectedValue)
+				t.Fatalf("key %s was not the expected value. Expected: '%s' Actual: '%s'", testCase.Key, testCase.ExpectedValue, field)
 			}
 		}
 
@@ -405,8 +484,11 @@ const bundleYamlWithDisablePreProcessDisabled = `namespace: default
 helm:
   disablePreprocess: false
   releaseName: labels
+  templateValues:
+    overridden: "something_templated"
   values:
     clusterName: "${ .ClusterName }"
+    overridden: ""
 `
 
 func TestDisablePreProcessFlagDisabled(t *testing.T) {
@@ -433,6 +515,17 @@ func TestDisablePreProcessFlagDisabled(t *testing.T) {
 		}
 	}
 
+	key = "overridden"
+	expectedValue = "something_templated"
+
+	if field, ok := valuesObj[key]; !ok {
+		t.Fatalf("key %s not found", key)
+	} else {
+		if field != expectedValue {
+			t.Fatalf("key %s was not the expected value. Expected: '%s' Actual: '%s'", key, field, expectedValue)
+		}
+	}
+
 }
 
 const bundleYamlWithDisablePreProcessMissing = `namespace: default
diff --git a/internal/resourcestatus/resourcekey_test.go b/internal/resourcestatus/resourcekey_test.go
index 6c106471..7d8dc965 100644
--- a/internal/resourcestatus/resourcekey_test.go
+++ b/internal/resourcestatus/resourcekey_test.go
@@ -12,140 +12,138 @@ import (
 )
 
 func TestSetResources(t *testing.T) {
-	list := &fleet.BundleDeploymentList{
-		Items: []fleet.BundleDeployment{
-			{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "bd1",
-					Namespace: "ns1-cluster1-ns",
-					Labels: map[string]string{
-						fleet.ClusterLabel:          "cluster1",
-						fleet.ClusterNamespaceLabel: "c-ns1",
-					},
+	list := []fleet.BundleDeployment{
+		{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "bd1",
+				Namespace: "ns1-cluster1-ns",
+				Labels: map[string]string{
+					fleet.ClusterLabel:          "cluster1",
+					fleet.ClusterNamespaceLabel: "c-ns1",
 				},
-				Spec: fleet.BundleDeploymentSpec{
-					DeploymentID: "id2",
-				},
-				Status: fleet.BundleDeploymentStatus{
-					Ready:               false,
-					NonModified:         true,
-					AppliedDeploymentID: "id1",
-					Resources: []fleet.BundleDeploymentResource{
-						{
-							Kind:       "Deployment",
-							APIVersion: "v1",
-							Name:       "web",
-							Namespace:  "default",
-						},
-						{
-							// extra service for one cluster
-							Kind:       "Service",
-							APIVersion: "v1",
-							Name:       "web-svc",
-							Namespace:  "default",
-						},
+			},
+			Spec: fleet.BundleDeploymentSpec{
+				DeploymentID: "id2",
+			},
+			Status: fleet.BundleDeploymentStatus{
+				Ready:               false,
+				NonModified:         true,
+				AppliedDeploymentID: "id1",
+				Resources: []fleet.BundleDeploymentResource{
+					{
+						Kind:       "Deployment",
+						APIVersion: "v1",
+						Name:       "web",
+						Namespace:  "default",
 					},
-					ResourceCounts: fleet.ResourceCounts{
-						DesiredReady: 2,
-						WaitApplied:  2,
+					{
+						// extra service for one cluster
+						Kind:       "Service",
+						APIVersion: "v1",
+						Name:       "web-svc",
+						Namespace:  "default",
 					},
 				},
-			},
-			{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "bd1",
-					Namespace: "ns1-cluster2-ns",
-					Labels: map[string]string{
-						fleet.ClusterLabel:          "cluster2",
-						fleet.ClusterNamespaceLabel: "c-ns1",
-					},
+				ResourceCounts: fleet.ResourceCounts{
+					DesiredReady: 2,
+					WaitApplied:  2,
 				},
-				Status: fleet.BundleDeploymentStatus{
-					Ready:       true,
-					NonModified: true,
-					Resources: []fleet.BundleDeploymentResource{
-						{
-							Kind:       "Deployment",
-							APIVersion: "v1",
-							Name:       "web",
-							Namespace:  "default",
-						},
-					},
-					ResourceCounts: fleet.ResourceCounts{
-						DesiredReady: 1,
-						Ready:        1,
-					},
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "bd1",
+				Namespace: "ns1-cluster2-ns",
+				Labels: map[string]string{
+					fleet.ClusterLabel:          "cluster2",
+					fleet.ClusterNamespaceLabel: "c-ns1",
 				},
 			},
-			{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "bd2",
-					Namespace: "ns1-cluster2-ns",
-					Labels: map[string]string{
-						fleet.ClusterLabel:          "cluster2",
-						fleet.ClusterNamespaceLabel: "c-ns1",
+			Status: fleet.BundleDeploymentStatus{
+				Ready:       true,
+				NonModified: true,
+				Resources: []fleet.BundleDeploymentResource{
+					{
+						Kind:       "Deployment",
+						APIVersion: "v1",
+						Name:       "web",
+						Namespace:  "default",
 					},
 				},
-				Status: fleet.BundleDeploymentStatus{
-					Ready:       true,
-					NonModified: true,
-					Resources: []fleet.BundleDeploymentResource{
-						{
-							Kind:       "ConfigMap",
-							APIVersion: "v1",
-							Name:       "cm-web",
-							Namespace:  "default",
-						},
-					},
-					ResourceCounts: fleet.ResourceCounts{
-						DesiredReady: 1,
-						Ready:        1,
-					},
+				ResourceCounts: fleet.ResourceCounts{
+					DesiredReady: 1,
+					Ready:        1,
 				},
 			},
-			{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      "bd1",
-					Namespace: "ns2-cluster1",
-					Labels: map[string]string{
-						fleet.ClusterLabel:          "cluster1",
-						fleet.ClusterNamespaceLabel: "c-ns2",
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "bd2",
+				Namespace: "ns1-cluster2-ns",
+				Labels: map[string]string{
+					fleet.ClusterLabel:          "cluster2",
+					fleet.ClusterNamespaceLabel: "c-ns1",
+				},
+			},
+			Status: fleet.BundleDeploymentStatus{
+				Ready:       true,
+				NonModified: true,
+				Resources: []fleet.BundleDeploymentResource{
+					{
+						Kind:       "ConfigMap",
+						APIVersion: "v1",
+						Name:       "cm-web",
+						Namespace:  "default",
 					},
 				},
-				Spec: fleet.BundleDeploymentSpec{
-					DeploymentID: "id2",
+				ResourceCounts: fleet.ResourceCounts{
+					DesiredReady: 1,
+					Ready:        1,
 				},
-				Status: fleet.BundleDeploymentStatus{
-					Ready:               false,
-					NonModified:         true,
-					AppliedDeploymentID: "id1",
-					NonReadyStatus: []fleet.NonReadyStatus{
-						{
-							Kind:       "Deployment",
-							APIVersion: "v1",
-							Name:       "web",
-							Namespace:  "default",
-							Summary: summary.Summary{
-								State:         "Pending",
-								Error:         true,
-								Transitioning: true,
-								Message:       []string{"message1", "message2"},
-							},
-						},
-					},
-					Resources: []fleet.BundleDeploymentResource{
-						{
-							Kind:       "Deployment",
-							APIVersion: "v1",
-							Name:       "web",
-							Namespace:  "default",
+			},
+		},
+		{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "bd1",
+				Namespace: "ns2-cluster1",
+				Labels: map[string]string{
+					fleet.ClusterLabel:          "cluster1",
+					fleet.ClusterNamespaceLabel: "c-ns2",
+				},
+			},
+			Spec: fleet.BundleDeploymentSpec{
+				DeploymentID: "id2",
+			},
+			Status: fleet.BundleDeploymentStatus{
+				Ready:               false,
+				NonModified:         true,
+				AppliedDeploymentID: "id1",
+				NonReadyStatus: []fleet.NonReadyStatus{
+					{
+						Kind:       "Deployment",
+						APIVersion: "v1",
+						Name:       "web",
+						Namespace:  "default",
+						Summary: summary.Summary{
+							State:         "Pending",
+							Error:         true,
+							Transitioning: true,
+							Message:       []string{"message1", "message2"},
 						},
 					},
-					ResourceCounts: fleet.ResourceCounts{
-						DesiredReady: 1,
-						NotReady:     1,
+				},
+				Resources: []fleet.BundleDeploymentResource{
+					{
+						Kind:       "Deployment",
+						APIVersion: "v1",
+						Name:       "web",
+						Namespace:  "default",
 					},
 				},
+				ResourceCounts: fleet.ResourceCounts{
+					DesiredReady: 1,
+					NotReady:     1,
+				},
 			},
 		},
 	}