Repository: rancher/fleet
Pull request #3249 (2025-01-24 11:35:34)

Increase runner CPUs for nightly CI workflow
This aligns runner specs with the regular end-to-end tests CI workflow, which runs against pull requests.

It may fix end-to-end test failures which, while not related to sharding, only happen with sharding enabled.

Refers to #3215


Patch:
diff --git a/.github/workflows/release-against-charts.yml b/.github/workflows/release-against-charts.yml
index a8741fea..66b7116b 100644
--- a/.github/workflows/release-against-charts.yml
+++ b/.github/workflows/release-against-charts.yml
@@ -5,7 +5,7 @@ on:
       charts_ref:
         description: "Submit PR against the following rancher/charts branch (e.g. dev-v2.7)"
         required: true
-        default: "dev-v2.9"
+        default: "dev-v2.10"
       prev_fleet:
         description: "Previous Fleet version (e.g. 0.6.0-rc.3)"
         required: true
@@ -47,7 +47,10 @@ jobs:
         with:
           go-version: '1.22.*'
       - name: Install dependencies
-        run: sudo snap install yq --channel=v4/stable
+        run: |
+          sudo apt-get update
+          sudo apt install -y --allow-change-held-packages snapd
+          sudo snap install yq --channel=v4/stable
       - name: Run release script
         run: |
           export CHARTS_DIR="${GITHUB_WORKSPACE}/charts"
diff --git a/.github/workflows/release-against-rancher.yml b/.github/workflows/release-against-rancher.yml
index aa76f6ff..680cea25 100644
--- a/.github/workflows/release-against-rancher.yml
+++ b/.github/workflows/release-against-rancher.yml
@@ -5,7 +5,7 @@ on:
       rancher_ref:
         description: "Submit PR against the following rancher/rancher branch (e.g. release/v2.7)"
         required: true
-        default: "release/v2.9"
+        default: "release/v2.10"
       new_fleet:
         description: "New Fleet version (e.g. 0.8.0-rc.3)"
         required: true
@@ -21,7 +21,7 @@ on:
       go_version:
         description: "Go version used for bumping the api. This should be the same version as in the go.mod file of the project."
         required: true
-        default: '1.22.*'
+        default: '1.23.*'
 
 env:
   GOARCH: amd64
diff --git a/.github/workflows/release-fleet.yml b/.github/workflows/release.yml
similarity index 60%
rename from .github/workflows/release-fleet.yml
rename to .github/workflows/release.yml
index 945f3212..32e8f1a0 100644
--- a/.github/workflows/release-fleet.yml
+++ b/.github/workflows/release.yml
@@ -22,8 +22,8 @@ jobs:
     runs-on: runs-on,runner=8cpu-linux-x64,mem=16,run-id=${{ github.run_id }}
 
     env:
-      IS_HOTFIX: ${{ contains(github.ref, '-hotfix-ch-') }}
-      REGISTRY: "${{ contains(github.ref, '-hotfix-ch-') && 'stgregistry.suse.com' || 'docker.io' }}"
+      IS_HOTFIX: ${{ contains(github.ref, '-hotfix-') }}
+      REGISTRY: "${{ contains(github.ref, '-hotfix-') && 'stgregistry.suse.com' || 'docker.io' }}"
 
     if: github.repository == 'rancher/fleet'
     steps:
@@ -45,12 +45,12 @@ jobs:
             secret/data/github/repo/${{ github.repository }}/fossa/credential token | FOSSA_API_KEY
 
       - name: Run FOSSA scan
-        uses: fossas/fossa-action@v1.4.0
+        uses: fossas/fossa-action@v1.5.0
         with:
           api-key: ${{ env.FOSSA_API_KEY }}
 
       - name: Run FOSSA tests
-        uses: fossas/fossa-action@v1.4.0
+        uses: fossas/fossa-action@v1.5.0
         with:
           api-key: ${{ env.FOSSA_API_KEY }}
           run-tests: false
@@ -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..990a36c6 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.
@@ -6704,12 +6776,6 @@ spec:
                         waiting to be applied.
                       type: integer
                   type: object
-                resourceErrors:
-                  description: ResourceErrors is a sorted list of errors from the
-                    resources.
-                  items:
-                    type: string
-                  type: array
                 resources:
                   description: Resources contains metadata about the resources of
                     each bundle.
@@ -6756,45 +6822,59 @@ spec:
                         nullable: true
                         type: string
                       perClusterState:
-                        description: PerClusterState is a list of states for each
-                          cluster. Derived from the summaries non-ready resources.
-                        items:
-                          description: ResourcePerClusterState is generated for each
-                            non-ready resource of the bundles.
-                          properties:
-                            clusterId:
-                              description: ClusterID is the id of the cluster.
-                              nullable: true
+                        description: PerClusterState contains lists of cluster IDs
+                          for every State for this resource
+                        nullable: true
+                        properties:
+                          missing:
+                            description: Missing is a list of cluster IDs for which
+                              this a resource is in Missing state
+                            items:
                               type: string
-                            error:
-                              description: Error is true if the resource is in an
-                                error state, copied from the bundle's summary for
-                                non-ready resources.
-                              type: boolean
-                            message:
-                              description: Message combines the messages from the
-                                bundle's summary. Messages are joined with the delimiter
-                                ';'.
-                              nullable: true
+                            type: array
+                          modified:
+                            description: Modified is a list of cluster IDs for which
+                              this a resource is in Modified state
+                            items:
                               type: string
-                            patch:
-                              description: Patch for modified resources.
-                              nullable: true
-                              type: object
-                              x-kubernetes-preserve-unknown-fields: true
-                            state:
-                              description: State is the state of the resource.
-                              nullable: true
+                            type: array
+                          notReady:
+                            description: NotReady is a list of cluster IDs for which
+                              this a resource is in NotReady state
+                            items:
                               type: string
-                            transitioning:
-                              description: 'Transitioning is true if the resource
-                                is in a transitioning state,
-
-                                copied from the bundle''s summary for non-ready resources.'
-                              type: boolean
-                          type: object
-                        nullable: true
-                        type: array
+                            type: array
+                          orphaned:
+                            description: Orphaned is a list of cluster IDs for which
+                              this a resource is in Orphaned state
+                            items:
+                              type: string
+                            type: array
+                          pending:
+                            description: Pending is a list of cluster IDs for which
+                              this a resource is in Pending state
+                            items:
+                              type: string
+                            type: array
+                          ready:
+                            description: Ready is a list of cluster IDs for which
+                              this a resource is in Ready state
+                            items:
+                              type: string
+                            type: array
+                          unknown:
+                            description: Unknown is a list of cluster IDs for which
+                              this a resource is in Unknown state
+                            items:
+                              type: string
+                            type: array
+                          waitApplied:
+                            description: WaitApplied is a list of cluster IDs for
+                              which this a resource is in WaitApplied state
+                            items:
+                              type: string
+                            type: array
+                        type: object
                       state:
                         description: State is the state of the resource, e.g. "Unknown",
                           "WaitApplied", "ErrApplied" or "Ready".
@@ -6807,6 +6887,8 @@ spec:
                         description: Type is the type of the resource, e.g. "apiextensions.k8s.io.customresourcedefinition"
                           or "configmap".
                         type: string
+                    required:
+                      - perClusterState
                     type: object
                   type: array
                 summary:
@@ -7307,6 +7389,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 +8291,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.
@@ -8539,12 +8657,6 @@ spec:
                         waiting to be applied.
                       type: integer
                   type: object
-                resourceErrors:
-                  description: ResourceErrors is a sorted list of errors from the
-                    resources.
-                  items:
-                    type: string
-                  type: array
                 resources:
                   description: Resources contains metadata about the resources of
                     each bundle.
@@ -8591,45 +8703,59 @@ spec:
                         nullable: true
                         type: string
                       perClusterState:
-                        description: PerClusterState is a list of states for each
-                          cluster. Derived from the summaries non-ready resources.
-                        items:
-                          description: ResourcePerClusterState is generated for each
-                            non-ready resource of the bundles.
-                          properties:
-                            clusterId:
-                              description: ClusterID is the id of the cluster.
-                              nullable: true
+                        description: PerClusterState contains lists of cluster IDs
+                          for every State for this resource
+                        nullable: true
+                        properties:
+                          missing:
+                            description: Missing is a list of cluster IDs for which
+                              this a resource is in Missing state
+                            items:
                               type: string
-                            error:
-                              description: Error is true if the resource is in an
-                                error state, copied from the bundle's summary for
-                                non-ready resources.
-                              type: boolean
-                            message:
-                              description: Message combines the messages from the
-                                bundle's summary. Messages are joined with the delimiter
-                                ';'.
-                              nullable: true
+                            type: array
+                          modified:
+                            description: Modified is a list of cluster IDs for which
+                              this a resource is in Modified state
+                            items:
                               type: string
-                            patch:
-                              description: Patch for modified resources.
-                              nullable: true
-                              type: object
-                              x-kubernetes-preserve-unknown-fields: true
-                            state:
-                              description: State is the state of the resource.
-                              nullable: true
+                            type: array
+                          notReady:
+                            description: NotReady is a list of cluster IDs for which
+                              this a resource is in NotReady state
+                            items:
                               type: string
-                            transitioning:
-                              description: 'Transitioning is true if the resource
-                                is in a transitioning state,
-
-                                copied from the bundle''s summary for non-ready resources.'
-                              type: boolean
-                          type: object
-                        nullable: true
-                        type: array
+                            type: array
+                          orphaned:
+                            description: Orphaned is a list of cluster IDs for which
+                              this a resource is in Orphaned state
+                            items:
+                              type: string
+                            type: array
+                          pending:
+                            description: Pending is a list of cluster IDs for which
+                              this a resource is in Pending state
+                            items:
+                              type: string
+                            type: array
+                          ready:
+                            description: Ready is a list of cluster IDs for which
+                              this a resource is in Ready state
+                            items:
+                              type: string
+                            type: array
+                          unknown:
+                            description: Unknown is a list of cluster IDs for which
+                              this a resource is in Unknown state
+                            items:
+                              type: string
+                            type: array
+                          waitApplied:
+                            description: WaitApplied is a list of cluster IDs for
+                              which this a resource is in WaitApplied state
+                            items:
+                              type: string
+                            type: array
+                        type: object
                       state:
                         description: State is the state of the resource, e.g. "Unknown",
                           "WaitApplied", "ErrApplied" or "Ready".
@@ -8642,6 +8768,8 @@ spec:
                         description: Type is the type of the resource, e.g. "apiextensions.k8s.io.customresourcedefinition"
                           or "configmap".
                         type: string
+                    required:
+                      - perClusterState
                     type: object
                   type: array
                 summary:
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/dev/import-images-k3d b/dev/import-images-k3d
index d1d8bb29..98a34e4a 100755
--- a/dev/import-images-k3d
+++ b/dev/import-images-k3d
@@ -8,20 +8,15 @@ upstream_ctx="${FLEET_E2E_CLUSTER-k3d-upstream}"
 # The single downstream cluster to import the agent image to.
 downstream_ctx="${FLEET_E2E_CLUSTER_DOWNSTREAM-k3d-downstream1}"
 
-# If multi-cluster is enabled, import the agent image to all downstream clusters.
-FLEET_E2E_DS_CLUSTER_COUNT="${FLEET_E2E_DS_CLUSTER_COUNT:-1}"
-
 k3d image import rancher/fleet:dev rancher/fleet-agent:dev -m direct -c "${upstream_ctx#k3d-}"
 
+downstream_keyword="${downstream_ctx#k3d-}"
+downstream_keyword="${downstream_keyword%[0-9]*}"
 if [ "$upstream_ctx" != "$downstream_ctx" ]; then
-  if [ "$FLEET_E2E_DS_CLUSTER_COUNT" -gt 1 ]; then
-    for cluster in $(k3d cluster list -o json | \
-        jq -r ".[].name | select(. | contains(\"${downstream_ctx#k3d-}\"))"); do
-      k3d image import rancher/fleet-agent:dev -m direct -c "${cluster}"
-    done
-  else
-    k3d image import rancher/fleet-agent:dev -m direct -c "${downstream_ctx#k3d-}"
-  fi
+  for cluster in $(k3d cluster list -o json | \
+      jq -r ".[].name | select(. | contains(\"${downstream_keyword}\"))"); do
+    k3d image import rancher/fleet-agent:dev -m direct -c "${cluster}"
+  done
 else
   echo "not importing agent to any downstream clusters. Set FLEET_E2E_CLUSTER_DOWNSTREAM"
 fi
diff --git a/dev/k3d-clean b/dev/k3d-clean
index 1cdc14f1..01701cac 100755
--- a/dev/k3d-clean
+++ b/dev/k3d-clean
@@ -27,18 +27,18 @@ if [[ -n "$FLEET_E2E_CLUSTER_DOWNSTREAM" ]]; then
         exit 0
     fi
 
-    # Delete downstream cluster.
+    # Delete one downstream cluster.
     if [[ -z "$FLEET_E2E_DS_CLUSTER_COUNT" ]]; then
         k3d-cluster-delete "$FLEET_E2E_CLUSTER_DOWNSTREAM"
         exit 0
     fi
 
-    # Delete all downstream clusters.
+    # Delete all downstream clusters matching the pattern in $FLEET_E2E_CLUSTER_DOWNSTREAM.
     for ((i=1; i<FLEET_E2E_DS_CLUSTER_COUNT+1; i++)); do
         if [[ $i -eq 1 ]]; then
             k3d-cluster-delete "${FLEET_E2E_CLUSTER_DOWNSTREAM}"
         else
-            k3d-cluster-delete "${FLEET_E2E_CLUSTER_DOWNSTREAM}${i}"
+            k3d-cluster-delete "${FLEET_E2E_CLUSTER_DOWNSTREAM%[0-9]*}${i}"
         fi
     done
 fi
diff --git a/dev/remove-fleet b/dev/remove-fleet
index 2b5c6533..7f523221 100755
--- a/dev/remove-fleet
+++ b/dev/remove-fleet
@@ -16,6 +16,9 @@ for res in gitrepos.fleet.cattle.io bundledeployments.fleet.cattle.io bundles.fl
   kubectl get "$res" -A --no-headers | while read ns name  _; do kubectl patch "$res" -n "$ns" "$name" -p '{"metadata":{"finalizers":[]}}' --type=merge; done
 done
 
+# Remove finalizers on contents too
+kubectl get content -o=jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' | xargs -I{} kubectl patch content {} -p '{"metadata":{"finalizers":[]}}' --type=merge
+
 helm uninstall -n cattle-fleet-system fleet-crd
 
 # make sure crds are removed
diff --git a/go.mod b/go.mod
index e6185569..2a7c72d9 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,7 @@ module github.com/rancher/fleet
 
 go 1.23.0
 
-toolchain go1.23.2
+toolchain go1.23.5
 
 replace (
 	github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
@@ -15,10 +15,10 @@ require (
 	github.com/chartmuseum/helm-push v0.10.4
 	github.com/cheggaaa/pb v1.0.29
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
-	github.com/docker/docker v27.5.0+incompatible
-	github.com/evanphx/json-patch v5.9.0+incompatible
+	github.com/docker/docker v27.5.1+incompatible
+	github.com/evanphx/json-patch v5.9.11+incompatible
 	github.com/go-git/go-billy/v5 v5.6.2
-	github.com/go-git/go-git/v5 v5.13.1
+	github.com/go-git/go-git/v5 v5.13.2
 	github.com/go-logr/logr v1.4.2
 	github.com/go-playground/webhooks/v6 v6.4.0
 	github.com/gobwas/glob v0.2.3
@@ -36,9 +36,9 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.20.5
 	github.com/prometheus/client_model v0.6.1
-	github.com/prometheus/common v0.61.0
+	github.com/prometheus/common v0.62.0
 	github.com/rancher/fleet/pkg/apis v0.11.3
-	github.com/rancher/lasso v0.0.0-20241202185148-04649f379358
+	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
@@ -86,7 +86,7 @@ require (
 	github.com/Masterminds/goutils v1.1.1 // indirect
 	github.com/Masterminds/squirrel v1.5.4 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
-	github.com/ProtonMail/go-crypto v1.1.3 // indirect
+	github.com/ProtonMail/go-crypto v1.1.5 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
 	github.com/aws/aws-sdk-go v1.44.122 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
@@ -192,7 +192,7 @@ require (
 	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
 	github.com/otiai10/mint v1.6.3 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
-	github.com/pjbgf/sha1cd v0.3.0 // indirect
+	github.com/pjbgf/sha1cd v0.3.2 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
diff --git a/go.sum b/go.sum
index d36e6486..3713f9b9 100644
--- a/go.sum
+++ b/go.sum
@@ -634,8 +634,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
 github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
 github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
-github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
+github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
+github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
 github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
@@ -747,8 +747,8 @@ github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvD
 github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U=
-github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
+github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
 github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@@ -763,8 +763,8 @@ github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arX
 github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
-github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
+github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
+github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
 github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
 github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -784,8 +784,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
 github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
 github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
-github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
-github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
+github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
 github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
@@ -823,8 +823,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
 github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
-github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
+github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
+github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -1192,8 +1192,8 @@ github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2
 github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
 github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
 github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
-github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
+github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
+github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -1222,15 +1222,15 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p
 github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
-github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
-github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 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-20241202185148-04649f379358 h1:pJwgJXPt4fi0ysXsJcl28rvxhx/Z/9SNCDwFOEyeGu0=
-github.com/rancher/lasso v0.0.0-20241202185148-04649f379358/go.mod h1:IxgTBO55lziYhTEETyVKiT8/B5Rg92qYiRmcIIYoPgI=
+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/agent/controller/bundledeployment_controller.go b/internal/cmd/agent/controller/bundledeployment_controller.go
index a2d1762a..09a0b308 100644
--- a/internal/cmd/agent/controller/bundledeployment_controller.go
+++ b/internal/cmd/agent/controller/bundledeployment_controller.go
@@ -194,6 +194,10 @@ func (r *BundleDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Req
 
 func (r *BundleDeploymentReconciler) updateStatus(ctx context.Context, orig *fleetv1.BundleDeployment, obj *fleetv1.BundleDeployment) error {
 	statusPatch := client.MergeFrom(orig)
+	if patchData, err := statusPatch.Data(obj); err == nil && string(patchData) == "{}" {
+		// skip update if patch is empty
+		return nil
+	}
 	return r.Status().Patch(ctx, obj, statusPatch)
 }
 
diff --git a/internal/cmd/controller/finalize/finalize.go b/internal/cmd/controller/finalize/finalize.go
index b38491e9..336a949e 100644
--- a/internal/cmd/controller/finalize/finalize.go
+++ b/internal/cmd/controller/finalize/finalize.go
@@ -7,13 +7,14 @@ import (
 
 	"github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/wrangler/v3/pkg/kv"
-	"github.com/sirupsen/logrus"
+
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/util/retry"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/log"
 )
 
 const (
@@ -115,6 +116,8 @@ func PurgeContent(ctx context.Context, c client.Client, name, deplID string) err
 		return client.IgnoreNotFound(err)
 	}
 
+	logger := log.FromContext(ctx).WithName("purge-content").WithValues("contentID", contentID, "finalizerName", name)
+
 	nn := types.NamespacedName{Name: content.Name}
 	if controllerutil.ContainsFinalizer(content, name) {
 		err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
@@ -130,14 +133,14 @@ func PurgeContent(ctx context.Context, c client.Client, name, deplID string) err
 			return err
 		}
 
-		logrus.Infof("Removed finalizer %s from content resource %s", name, contentID)
+		logger.V(1).Info("Removed finalizer from content resource")
 	}
 
 	if len(content.Finalizers) == 0 {
 		if err := c.Delete(ctx, content); err != nil {
 			return err
 		}
-		logrus.Infof("Deleted content resource %s", contentID)
+		logger.V(1).Info("Deleted content resource")
 	}
 
 	return nil
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/bundle_controller.go b/internal/cmd/controller/reconciler/bundle_controller.go
index f1072118..5361c2c2 100644
--- a/internal/cmd/controller/reconciler/bundle_controller.go
+++ b/internal/cmd/controller/reconciler/bundle_controller.go
@@ -22,7 +22,6 @@ import (
 	"github.com/rancher/wrangler/v3/pkg/genericcondition"
 
 	corev1 "k8s.io/api/core/v1"
-	v1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -193,7 +192,7 @@ func (r *BundleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		bundle.Status.Conditions = []genericcondition.GenericCondition{
 			{
 				Type:           string(fleet.Ready),
-				Status:         v1.ConditionFalse,
+				Status:         corev1.ConditionFalse,
 				Message:        "Targeting error: " + err.Error(),
 				LastUpdateTime: metav1.Now().UTC().Format(time.RFC3339),
 			},
@@ -492,8 +491,12 @@ func experimentalHelmOpsEnabled() bool {
 func (r *BundleReconciler) updateStatus(ctx context.Context, orig *fleet.Bundle, bundle *fleet.Bundle) error {
 	logger := log.FromContext(ctx).WithName("bundle - updateStatus")
 	statusPatch := client.MergeFrom(orig)
-	err := r.Status().Patch(ctx, bundle, statusPatch)
-	if err != nil {
+
+	if patchData, err := statusPatch.Data(bundle); err == nil && string(patchData) == "{}" {
+		// skip update if patch is empty
+		return nil
+	}
+	if err := r.Status().Patch(ctx, bundle, statusPatch); err != nil {
 		logger.V(1).Info("Reconcile failed update to bundle status", "status", bundle.Status, "error", err)
 		return err
 	}
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 9c4126ea..c9d0949f 100644
--- a/internal/resourcestatus/resourcekey.go
+++ b/internal/resourcestatus/resourcekey.go
@@ -1,7 +1,6 @@
 package resourcestatus
 
 import (
-	"encoding/json"
 	"sort"
 	"strings"
 
@@ -9,51 +8,24 @@ import (
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 )
 
-func SetResources(list *fleet.BundleDeploymentList, status *fleet.StatusBase) {
-	r, errors := fromResources(list)
-	status.ResourceErrors = errors
-	status.Resources = merge(r)
+func SetResources(list []fleet.BundleDeployment, status *fleet.StatusBase) {
+	byCluster := fromResources(list)
+	status.Resources = aggregateResourceStatesClustersMap(byCluster)
 	status.ResourceCounts = sumResourceCounts(list)
 	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)
 }
 
-// merge takes a list of GitRepo resources and deduplicates resources deployed to multiple clusters,
-// ensuring that for such resources, the output contains a single resource entry with a field summarizing
-// its status on each cluster.
-func merge(resources []fleet.Resource) []fleet.Resource {
-	merged := map[string]fleet.Resource{}
-	for _, resource := range resources {
-		key := key(resource)
-		if existing, ok := merged[key]; ok {
-			existing.PerClusterState = append(existing.PerClusterState, resource.PerClusterState...)
-			merged[key] = existing
-		} else {
-			merged[key] = resource
-		}
-	}
-
-	var result []fleet.Resource
-	for _, resource := range merged {
-		result = append(result, resource)
-	}
-
-	sort.Slice(result, func(i, j int) bool {
-		return key(result[i]) < key(result[j])
-	})
-	return result
-}
-
 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{}
@@ -63,122 +35,84 @@ func resourceCountsPerCluster(list *fleet.BundleDeploymentList) map[string]*flee
 	return res
 }
 
-// fromResources inspects all bundledeployments for this GitRepo and returns a list of
-// Resources and error messages.
-//
-// It populates gitrepo status resources from bundleDeployments. BundleDeployment.Status.Resources is the list of deployed resources.
-func fromResources(list *fleet.BundleDeploymentList) ([]fleet.Resource, []string) {
-	var (
-		resources []fleet.Resource
-		errors    []string
-	)
-
-	for _, bd := range list.Items {
-		state := summary.GetDeploymentState(&bd)
-		bdResources := bundleDeploymentResources(bd)
-		incomplete, err := addState(bd, bdResources)
-
-		if len(err) > 0 {
-			incomplete = true
-			for _, err := range err {
-				errors = append(errors, err.Error())
-			}
-		}
-
-		for k, perCluster := range bdResources {
-			resource := toResourceState(k, perCluster, incomplete, string(state))
-			resources = append(resources, resource)
-		}
-	}
+type resourceStateEntry struct {
+	state      string
+	clusterID  string
+	incomplete bool
+}
 
-	sort.Strings(errors)
+type resourceStatesByResourceKey map[fleet.ResourceKey][]resourceStateEntry
 
-	return resources, errors
+func clusterID(bd fleet.BundleDeployment) string {
+	return bd.Labels[fleet.ClusterNamespaceLabel] + "/" + bd.Labels[fleet.ClusterLabel]
 }
 
-func toResourceState(k fleet.ResourceKey, perCluster []fleet.ResourcePerClusterState, incomplete bool, bdState string) fleet.Resource {
-	resource := fleet.Resource{
-		APIVersion:      k.APIVersion,
-		Kind:            k.Kind,
-		Namespace:       k.Namespace,
-		Name:            k.Name,
-		IncompleteState: incomplete,
-		PerClusterState: perCluster,
-	}
-	resource.Type, resource.ID = toType(resource)
-
-	for _, state := range perCluster {
-		resource.State = state.State
-		resource.Error = state.Error
-		resource.Transitioning = state.Transitioning
-		resource.Message = state.Message
-		break
-	}
+// 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(items []fleet.BundleDeployment) resourceStatesByResourceKey {
+	sort.Slice(items, func(i, j int) bool {
+		return clusterID(items[i]) < clusterID(items[j])
+	})
 
-	// fallback to state from gitrepo summary
-	if resource.State == "" {
-		if resource.IncompleteState {
-			if bdState != "" {
-				resource.State = bdState
-			} else {
-				resource.State = "Unknown"
-			}
-		} else if bdState != "" {
-			resource.State = bdState
-		} else {
-			resource.State = "Ready"
+	resources := make(resourceStatesByResourceKey)
+	for _, bd := range items {
+		for key, entry := range bundleDeploymentResources(bd) {
+			resources[key] = append(resources[key], entry)
 		}
 	}
 
-	sort.Slice(perCluster, func(i, j int) bool {
-		return perCluster[i].ClusterID < perCluster[j].ClusterID
-	})
-	return resource
+	return resources
+}
+
+func resourceId(namespace, name string) string {
+	if namespace != "" {
+		return namespace + "/" + name
+	}
+	return name
 }
 
-func toType(resource fleet.Resource) (string, string) {
-	group := strings.Split(resource.APIVersion, "/")[0]
+func toType(apiVersion, kind string) string {
+	group := strings.Split(apiVersion, "/")[0]
 	if group == "v1" {
 		group = ""
 	} else if len(group) > 0 {
 		group += "."
 	}
-	t := group + strings.ToLower(resource.Kind)
-	if resource.Namespace == "" {
-		return t, resource.Name
-	}
-	return t, resource.Namespace + "/" + resource.Name
+	return group + strings.ToLower(kind)
 }
 
-// addState adds per-cluster state information for nonReady and modified resources in a bundleDeployment.
-// It only adds up to 10 entries to not overwhelm the status.
-// It mutates resources and returns whether the reported state is incomplete and any errors encountered.
-func addState(bd fleet.BundleDeployment, resources map[fleet.ResourceKey][]fleet.ResourcePerClusterState) (bool, []error) {
-	var (
-		incomplete bool
-		errors     []error
-	)
-
-	if len(bd.Status.NonReadyStatus) >= 10 || len(bd.Status.ModifiedStatus) >= 10 {
-		incomplete = true
+func bundleDeploymentResources(bd fleet.BundleDeployment) map[fleet.ResourceKey]resourceStateEntry {
+	clusterID := bd.Labels[fleet.ClusterNamespaceLabel] + "/" + bd.Labels[fleet.ClusterLabel]
+	defaultState := string(summary.GetDeploymentState(&bd))
+	incomplete := bd.Status.IncompleteState
+
+	resources := make(map[fleet.ResourceKey]resourceStateEntry, len(bd.Status.Resources))
+	for _, bdResource := range bd.Status.Resources {
+		resourceKey := fleet.ResourceKey{
+			Kind:       bdResource.Kind,
+			APIVersion: bdResource.APIVersion,
+			Name:       bdResource.Name,
+			Namespace:  bdResource.Namespace,
+		}
+		resources[resourceKey] = resourceStateEntry{
+			state:      defaultState,
+			clusterID:  clusterID,
+			incomplete: incomplete,
+		}
 	}
 
-	cluster := bd.Labels[fleet.ClusterNamespaceLabel] + "/" + bd.Labels[fleet.ClusterLabel]
 	for _, nonReady := range bd.Status.NonReadyStatus {
-		key := fleet.ResourceKey{
+		resourceKey := fleet.ResourceKey{
 			Kind:       nonReady.Kind,
 			APIVersion: nonReady.APIVersion,
 			Namespace:  nonReady.Namespace,
 			Name:       nonReady.Name,
 		}
-		state := fleet.ResourcePerClusterState{
-			State:         nonReady.Summary.State,
-			Error:         nonReady.Summary.Error,
-			Transitioning: nonReady.Summary.Transitioning,
-			Message:       strings.Join(nonReady.Summary.Message, "; "),
-			ClusterID:     cluster,
+		resources[resourceKey] = resourceStateEntry{
+			state:      nonReady.Summary.State,
+			clusterID:  clusterID,
+			incomplete: incomplete,
 		}
-		appendState(resources, key, state)
 	}
 
 	for _, modified := range bd.Status.ModifiedStatus {
@@ -188,60 +122,84 @@ func addState(bd fleet.BundleDeployment, resources map[fleet.ResourceKey][]fleet
 			Namespace:  modified.Namespace,
 			Name:       modified.Name,
 		}
-		state := fleet.ResourcePerClusterState{
-			State:     "Modified",
-			ClusterID: cluster,
-		}
+		state := "Modified"
 		if modified.Delete {
-			state.State = "Orphaned"
+			state = "Orphaned"
 		} else if modified.Create {
-			state.State = "Missing"
-		} else if len(modified.Patch) > 0 {
-			state.Patch = &fleet.GenericMap{}
-			err := json.Unmarshal([]byte(modified.Patch), state.Patch)
-			if err != nil {
-				errors = append(errors, err)
-			}
+			state = "Missing"
+		}
+		resources[key] = resourceStateEntry{
+			state:      state,
+			clusterID:  clusterID,
+			incomplete: incomplete,
 		}
-		appendState(resources, key, state)
 	}
-	return incomplete, errors
+
+	return resources
 }
 
-func appendState(states map[fleet.ResourceKey][]fleet.ResourcePerClusterState, key fleet.ResourceKey, state fleet.ResourcePerClusterState) {
-	if existing, ok := states[key]; ok || key.Namespace != "" {
-		states[key] = append(existing, state)
-		return
-	}
+func aggregateResourceStatesClustersMap(resourceKeyStates resourceStatesByResourceKey) []fleet.Resource {
+	result := make([]fleet.Resource, 0, len(resourceKeyStates))
+	for resourceKey, entries := range resourceKeyStates {
+		resource := &fleet.Resource{
+			Kind:       resourceKey.Kind,
+			APIVersion: resourceKey.APIVersion,
+			Namespace:  resourceKey.Namespace,
+			Name:       resourceKey.Name,
+			State:      "Ready",
+			Type:       toType(resourceKey.APIVersion, resourceKey.Kind),
+			ID:         resourceId(resourceKey.Namespace, resourceKey.Name),
+		}
+
+		for _, entry := range entries {
+			if entry.incomplete {
+				resource.IncompleteState = true
+			}
+
+			appendToPerClusterState(&resource.PerClusterState, entry.state, entry.clusterID)
 
-	for k, existing := range states {
-		if k.Name == key.Name &&
-			k.APIVersion == key.APIVersion &&
-			k.Kind == key.Kind {
-			delete(states, key)
-			k.Namespace = ""
-			states[k] = append(existing, state)
+			// top-level state is set from first non "Ready" per-cluster state
+			if resource.State == "Ready" {
+				resource.State = entry.state
+			}
 		}
+
+		result = append(result, *resource)
 	}
+
+	sort.Slice(result, func(i, j int) bool {
+		return key(result[i]) < key(result[j])
+	})
+
+	return result
 }
 
-func bundleDeploymentResources(bd fleet.BundleDeployment) map[fleet.ResourceKey][]fleet.ResourcePerClusterState {
-	bdResources := map[fleet.ResourceKey][]fleet.ResourcePerClusterState{}
-	for _, resource := range bd.Status.Resources {
-		key := fleet.ResourceKey{
-			Kind:       resource.Kind,
-			APIVersion: resource.APIVersion,
-			Name:       resource.Name,
-			Namespace:  resource.Namespace,
-		}
-		bdResources[key] = []fleet.ResourcePerClusterState{}
+func appendToPerClusterState(states *fleet.PerClusterState, state, clusterID string) {
+	switch state {
+	case "Ready":
+		states.Ready = append(states.Ready, clusterID)
+	case "WaitApplied":
+		states.WaitApplied = append(states.WaitApplied, clusterID)
+	case "Pending":
+		states.Pending = append(states.Pending, clusterID)
+	case "Modified":
+		states.Modified = append(states.Modified, clusterID)
+	case "NotReady":
+		states.NotReady = append(states.NotReady, clusterID)
+	case "Orphaned":
+		states.Orphaned = append(states.Orphaned, clusterID)
+	case "Missing":
+		states.Missing = append(states.Missing, clusterID)
+	case "Unknown":
+		states.Unknown = append(states.Unknown, clusterID)
+	default:
+		// ignore
 	}
-	return bdResources
 }
 
-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/gitrepo_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
index 76d00319..5804f282 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/gitrepo_types.go
@@ -193,28 +193,6 @@ type GitRepoDisplay struct {
 	Error bool `json:"error,omitempty"`
 }
 
-// ResourcePerClusterState is generated for each non-ready resource of the bundles.
-type ResourcePerClusterState struct {
-	// State is the state of the resource.
-	// +nullable
-	State string `json:"state,omitempty"`
-	// Error is true if the resource is in an error state, copied from the bundle's summary for non-ready resources.
-	Error bool `json:"error,omitempty"`
-	// Transitioning is true if the resource is in a transitioning state,
-	// copied from the bundle's summary for non-ready resources.
-	Transitioning bool `json:"transitioning,omitempty"`
-	// Message combines the messages from the bundle's summary. Messages are joined with the delimiter ';'.
-	// +nullable
-	Message string `json:"message,omitempty"`
-	// Patch for modified resources.
-	// +nullable
-	// +kubebuilder:validation:XPreserveUnknownFields
-	Patch *GenericMap `json:"patch,omitempty"`
-	// ClusterID is the id of the cluster.
-	// +nullable
-	ClusterID string `json:"clusterId,omitempty"`
-}
-
 // CommitSpec specifies how to commit changes to the git repository
 type CommitSpec struct {
 	// AuthorName gives the name to provide when making a commit
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/resource_types.go b/pkg/apis/fleet.cattle.io/v1alpha1/resource_types.go
index 07af039d..6b1b92d6 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/resource_types.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/resource_types.go
@@ -32,9 +32,29 @@ type Resource struct {
 	// Message is the first message from the PerClusterStates.
 	// +nullable
 	Message string `json:"message,omitempty"`
-	// PerClusterState is a list of states for each cluster. Derived from the summaries non-ready resources.
+	// PerClusterState contains lists of cluster IDs for every State for this resource
 	// +nullable
-	PerClusterState []ResourcePerClusterState `json:"perClusterState,omitempty"`
+	PerClusterState PerClusterState `json:"perClusterState"`
+}
+
+// PerClusterState aggregates list of cluster IDs per state for a given Resource
+type PerClusterState struct {
+	// Ready is a list of cluster IDs for which this a resource is in Ready state
+	Ready []string `json:"ready,omitempty"`
+	// WaitApplied is a list of cluster IDs for which this a resource is in WaitApplied state
+	WaitApplied []string `json:"waitApplied,omitempty"`
+	// Pending is a list of cluster IDs for which this a resource is in Pending state
+	Pending []string `json:"pending,omitempty"`
+	// Modified is a list of cluster IDs for which this a resource is in Modified state
+	Modified []string `json:"modified,omitempty"`
+	// Orphaned is a list of cluster IDs for which this a resource is in Orphaned state
+	Orphaned []string `json:"orphaned,omitempty"`
+	// Missing is a list of cluster IDs for which this a resource is in Missing state
+	Missing []string `json:"missing,omitempty"`
+	// Unknown is a list of cluster IDs for which this a resource is in Unknown state
+	Unknown []string `json:"unknown,omitempty"`
+	// NotReady is a list of cluster IDs for which this a resource is in NotReady state
+	NotReady []string `json:"notReady,omitempty"`
 }
 
 // ResourceCounts contains the number of resources in each state.
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/status.go b/pkg/apis/fleet.cattle.io/v1alpha1/status.go
index 1866992d..98060504 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/status.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/status.go
@@ -21,8 +21,6 @@ type StatusBase struct {
 	Resources []Resource `json:"resources,omitempty"`
 	// ResourceCounts contains the number of resources in each state over all bundles.
 	ResourceCounts ResourceCounts `json:"resourceCounts,omitempty"`
-	// ResourceErrors is a sorted list of errors from the resources.
-	ResourceErrors []string `json:"resourceErrors,omitempty"`
 	// PerClusterResourceCounts contains the number of resources in each state over all bundles, per cluster.
 	PerClusterResourceCounts map[string]*ResourceCounts `json:"perClusterResourceCounts,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..73abaa75 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))
@@ -2041,72 +2048,102 @@ func (in *PartitionStatus) DeepCopy() *PartitionStatus {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Resource) DeepCopyInto(out *Resource) {
+func (in *PerClusterState) DeepCopyInto(out *PerClusterState) {
 	*out = *in
-	if in.PerClusterState != nil {
-		in, out := &in.PerClusterState, &out.PerClusterState
-		*out = make([]ResourcePerClusterState, len(*in))
-		for i := range *in {
-			(*in)[i].DeepCopyInto(&(*out)[i])
-		}
+	if in.Ready != nil {
+		in, out := &in.Ready, &out.Ready
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.WaitApplied != nil {
+		in, out := &in.WaitApplied, &out.WaitApplied
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Pending != nil {
+		in, out := &in.Pending, &out.Pending
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Modified != nil {
+		in, out := &in.Modified, &out.Modified
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Orphaned != nil {
+		in, out := &in.Orphaned, &out.Orphaned
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Missing != nil {
+		in, out := &in.Missing, &out.Missing
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Unknown != nil {
+		in, out := &in.Unknown, &out.Unknown
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.NotReady != nil {
+		in, out := &in.NotReady, &out.NotReady
+		*out = make([]string, len(*in))
+		copy(*out, *in)
 	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resource.
-func (in *Resource) DeepCopy() *Resource {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PerClusterState.
+func (in *PerClusterState) DeepCopy() *PerClusterState {
 	if in == nil {
 		return nil
 	}
-	out := new(Resource)
+	out := new(PerClusterState)
 	in.DeepCopyInto(out)
 	return out
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ResourceCounts) DeepCopyInto(out *ResourceCounts) {
+func (in *Resource) DeepCopyInto(out *Resource) {
 	*out = *in
+	in.PerClusterState.DeepCopyInto(&out.PerClusterState)
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceCounts.
-func (in *ResourceCounts) DeepCopy() *ResourceCounts {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resource.
+func (in *Resource) DeepCopy() *Resource {
 	if in == nil {
 		return nil
 	}
-	out := new(ResourceCounts)
+	out := new(Resource)
 	in.DeepCopyInto(out)
 	return out
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ResourceKey) DeepCopyInto(out *ResourceKey) {
+func (in *ResourceCounts) DeepCopyInto(out *ResourceCounts) {
 	*out = *in
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceKey.
-func (in *ResourceKey) DeepCopy() *ResourceKey {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceCounts.
+func (in *ResourceCounts) DeepCopy() *ResourceCounts {
 	if in == nil {
 		return nil
 	}
-	out := new(ResourceKey)
+	out := new(ResourceCounts)
 	in.DeepCopyInto(out)
 	return out
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *ResourcePerClusterState) DeepCopyInto(out *ResourcePerClusterState) {
+func (in *ResourceKey) DeepCopyInto(out *ResourceKey) {
 	*out = *in
-	if in.Patch != nil {
-		in, out := &in.Patch, &out.Patch
-		*out = (*in).DeepCopy()
-	}
 }
 
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourcePerClusterState.
-func (in *ResourcePerClusterState) DeepCopy() *ResourcePerClusterState {
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceKey.
+func (in *ResourceKey) DeepCopy() *ResourceKey {
 	if in == nil {
 		return nil
 	}
-	out := new(ResourcePerClusterState)
+	out := new(ResourceKey)
 	in.DeepCopyInto(out)
 	return out
 }
@@ -2197,11 +2234,6 @@ func (in *StatusBase) DeepCopyInto(out *StatusBase) {
 		}
 	}
 	out.ResourceCounts = in.ResourceCounts
-	if in.ResourceErrors != nil {
-		in, out := &in.ResourceErrors, &out.ResourceErrors
-		*out = make([]string, len(*in))
-		copy(*out, *in)
-	}
 	if in.PerClusterResourceCounts != nil {
 		in, out := &in.PerClusterResourceCounts, &out.PerClusterResourceCounts
 		*out = make(map[string]*ResourceCounts, len(*in))


Test patch:
diff --git a/.github/workflows/e2e-nightly-ci.yml b/.github/workflows/e2e-nightly-ci.yml
index fb4485f4..0e211119 100644
--- a/.github/workflows/e2e-nightly-ci.yml
+++ b/.github/workflows/e2e-nightly-ci.yml
@@ -13,7 +13,7 @@ env:
 
 jobs:
   e2e-fleet-nightly-test:
-    runs-on: runs-on,runner=8cpu-linux-x64,mem=16,run-id=${{ github.run_id }}
+    runs-on: runs-on,runner=16cpu-linux-x64,mem=16,run-id=${{ github.run_id }}
 
     strategy:
       matrix:
diff --git a/.github/workflows/release-against-test-charts.yml b/.github/workflows/release-against-test-charts.yml
index bb0b19e6..509d7cfd 100644
--- a/.github/workflows/release-against-test-charts.yml
+++ b/.github/workflows/release-against-test-charts.yml
@@ -12,7 +12,7 @@ on:
       charts_base_branch:
         description: "Use the following rancher/charts branch as a base (e.g. dev-v2.7)"
         required: true
-        default: "dev-v2.10"
+        default: "dev-v2.11"
       charts_repo:
         description: "Push to the following Rancher charts repo (which must exist)"
         required: true
@@ -41,7 +41,7 @@ jobs:
           ref=${tmp:-${{github.ref}}}
 
           tmp=${{github.event.inputs.charts_base_branch}}
-          charts_base_branch=${tmp:-'dev-v2.10'}
+          charts_base_branch=${tmp:-'dev-v2.11'}
 
           tmp=${{github.event.inputs.charts_repo}}
           charts_repo=${tmp:-fleetrepoci/charts}
@@ -153,7 +153,10 @@ jobs:
           fi
 
       - name: Install dependencies
-        run: sudo snap install yq --channel=v4/stable
+        run: |
+          sudo apt-get update
+          sudo apt install -y --allow-change-held-packages snapd
+          sudo snap install yq --channel=v4/stable
 
       - name: Run release script
         run: |
diff --git a/e2e/single-cluster/oci_registry_test.go b/e2e/single-cluster/oci_registry_test.go
index 612bea53..1c146fe8 100644
--- a/e2e/single-cluster/oci_registry_test.go
+++ b/e2e/single-cluster/oci_registry_test.go
@@ -291,7 +291,7 @@ var _ = Describe("Single Cluster Deployments using OCI registry", Label("oci-reg
 
 					// check for contentsID
 					contentsID, err := k.Namespace("fleet-local").Get("bundle", "sample-simple-chart-oci", `-o=jsonpath={.spec.contentsId}`)
-					Expect(err).ToNot(HaveOccurred())
+					g.Expect(err).ToNot(HaveOccurred())
 					g.Expect(contentsID).To(BeEmpty())
 
 					GinkgoWriter.Printf("ContentsID: %s\n", contentsID)
diff --git a/internal/cmd/agent/deployer/monitor/updatestatus.go b/internal/cmd/agent/deployer/monitor/updatestatus.go
index e3c95889..00334b05 100644
--- a/internal/cmd/agent/deployer/monitor/updatestatus.go
+++ b/internal/cmd/agent/deployer/monitor/updatestatus.go
@@ -341,12 +341,12 @@ func modified(ctx context.Context, c client.Client, plan desiredset.Plan, resour
 
 			if exists {
 				logger.Info("Resource of BundleDeployment not owned by us",
-					"name", key.Name,
-					"kind", kind,
-					"apiVersion", apiVersion,
-					"namespace", key.Namespace,
-					"labels", obj.GetLabels(),
-					"annotations", obj.GetAnnotations(),
+					"resourceName", key.Name,
+					"resourceKind", kind,
+					"resourceApiVersion", apiVersion,
+					"resourceNamespace", key.Namespace,
+					"resourceLabels", obj.GetLabels(),
+					"resourceAnnotations", obj.GetAnnotations(),
 				)
 			}
 
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 991acf72..882ac03c 100644
--- a/internal/resourcestatus/resourcekey_test.go
+++ b/internal/resourcestatus/resourcekey_test.go
@@ -12,132 +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",
-					},
-				},
-				Spec: fleet.BundleDeploymentSpec{
-					DeploymentID: "id2",
+	list := []fleet.BundleDeployment{
+		{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "bd1",
+				Namespace: "ns1-cluster1-ns",
+				Labels: map[string]string{
+					fleet.ClusterLabel:          "cluster1",
+					fleet.ClusterNamespaceLabel: "c-ns1",
 				},
-				Status: fleet.BundleDeploymentStatus{
-					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{
-					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{
-					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{
-					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,
+				},
 			},
 		},
 	}
@@ -160,15 +166,10 @@ func TestSetResources(t *testing.T) {
 		Error:           false,
 		Transitioning:   false,
 		Message:         "",
-		PerClusterState: []fleet.ResourcePerClusterState{
-			{
-				State:         "Pending",
-				ClusterID:     "c-ns2/cluster1",
-				Error:         true,
-				Transitioning: true,
-				Message:       "message1; message2",
-				Patch:         nil,
-			},
+		PerClusterState: fleet.PerClusterState{
+			Ready:       []string{"c-ns1/cluster2"},
+			WaitApplied: []string{"c-ns1/cluster1"},
+			Pending:     []string{"c-ns2/cluster1"},
 		},
 	})
 	assert.Contains(t, status.Resources, fleet.Resource{
@@ -185,11 +186,11 @@ func TestSetResources(t *testing.T) {
 		Error:           false,
 		Transitioning:   false,
 		Message:         "",
-		PerClusterState: []fleet.ResourcePerClusterState{},
+		PerClusterState: fleet.PerClusterState{
+			WaitApplied: []string{"c-ns1/cluster1"},
+		},
 	})
 
-	assert.Empty(t, status.ResourceErrors)
-
 	assert.Equal(t, fleet.ResourceCounts{
 		Ready:        2,
 		DesiredReady: 5,