Repository: rancher/fleet
Pull request #2360 (2024-04-23 12:34:43)

Replace ImageScan and Bundle Labels With fleet.yaml Labels
…reflecting on Bundle or BundleDeployment.

<!-- Specify the issue ID that this pull request is solving -->
Refers to #1528
<!-- Make sure that the referenced issue provides steps to reproduce it -->

<!-- Describe the changes introduced by this pull request -->
Fixed the apply.go save method which was merging labels from the label into the cluster and the labels into the fleet.yaml (not deleting if we remove a label from the fleet.yaml)
Introduced a new fleetApplyOnline method to test the save method of the apply.go file
<!--
  integration test at ./integrationtests/cli/apply
-->


Patch:
diff --git a/.github/scripts/build-fleet-binaries.sh b/.github/scripts/build-fleet-binaries.sh
index a4158c74..4b914f23 100755
--- a/.github/scripts/build-fleet-binaries.sh
+++ b/.github/scripts/build-fleet-binaries.sh
@@ -13,7 +13,3 @@ go build -gcflags='all=-N -l' -o bin/fleetcontroller-linux-"$GOARCH" ./cmd/fleet
 # fleet agent
 go build -gcflags='all=-N -l' -o "bin/fleet-linux-$GOARCH" ./cmd/fleetcli
 go build -gcflags='all=-N -l' -o "bin/fleetagent-linux-$GOARCH" ./cmd/fleetagent
-
-# gitjob
-go build -gcflags='all=-N -l' -o "bin/gitcloner-linux-$GOARCH" ./cmd/gitcloner
-go build -gcflags='all=-N -l' -o "bin/gitjob-linux-$GOARCH" ./cmd/gitjob
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index 437ef004..ea2f3bf5 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -27,7 +27,7 @@ jobs:
           export PATH=$PATH:/home/runner/go/bin/
 
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v5.0.0
+        uses: golangci/golangci-lint-action@v5.1.0
         with:
           # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
           version: v1.57
diff --git a/.github/workflows/label-all-opened-issues.yml b/.github/workflows/label-all-opened-issues.yml
deleted file mode 100644
index b9c99b6d..00000000
--- a/.github/workflows/label-all-opened-issues.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-name: Label issue
-on:
-  issues:
-    types:
-      - opened
-      - reopened
-      
-permissions:
-  issues: write
-  
-jobs:
-  label_issues:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Label issue
-        id: run
-        run: gh issue edit -R ${GITHUB_REPOSITORY} --add-label ${LABEL} ${{ github.event.issue.number }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          LABEL: "team/fleet,area/fleet"
diff --git a/.github/workflows/nightly-ci.yml b/.github/workflows/nightly-ci.yml
index 5c80e3aa..396152ab 100644
--- a/.github/workflows/nightly-ci.yml
+++ b/.github/workflows/nightly-ci.yml
@@ -51,7 +51,7 @@ jobs:
           ./.github/scripts/build-fleet-binaries.sh
 
           cd e2e/testenv/infra
-          go build -o . ./...
+          go build
       -
         name: Build Docker Images
         run: |
diff --git a/.github/workflows/release-fleet.yml b/.github/workflows/release-fleet.yml
index 1a3573a1..35369b30 100644
--- a/.github/workflows/release-fleet.yml
+++ b/.github/workflows/release-fleet.yml
@@ -33,7 +33,7 @@ jobs:
           check-latest: true
 
       - name: Run golangci-lint
-        uses: golangci/golangci-lint-action@v5.0.0
+        uses: golangci/golangci-lint-action@v5.1.0
         with:
           # Required: the version of golangci-lint is required and must be specified without patch version: we always
           # use the latest patch version.
@@ -48,16 +48,16 @@ jobs:
             secret/data/github/repo/${{ github.repository }}/dockerhub/rancher/credentials password | DOCKER_PASSWORD ;
             secret/data/github/repo/${{ github.repository }}/fossa/credential token | FOSSA_API_KEY
 
-      - name: Run FOSSA scan
-        uses: fossas/fossa-action@v1.3.3
-        with:
-          api-key: ${{ secrets.FOSSA_API_KEY }}
+      # - name: Run FOSSA scan
+      #   uses: fossas/fossa-action@v1.3.3
+      #   with:
+      #     api-key: ${{ env.FOSSA_API_KEY }}
 
-      - name: Run FOSSA tests
-        uses: fossas/fossa-action@v1.3.3
-        with:
-          api-key: ${{ secrets.FOSSA_API_KEY }}
-          run-tests: true
+      # - name: Run FOSSA tests
+      #   uses: fossas/fossa-action@v1.3.3
+      #   with:
+      #     api-key: ${{ env.FOSSA_API_KEY }}
+      #     run-tests: true
 
       - name: Check for code changes
         run: |
@@ -98,7 +98,7 @@ jobs:
       - name: Upload charts to release
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          repo: "docker.io/rancher"
+          repo: "rancher"
         run: |
           version=$(echo '${{ steps.goreleaser.outputs.metadata }}' | jq -r '.version')
           tag=$(echo '${{ steps.goreleaser.outputs.metadata }}' | jq -r '.tag')
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 836632dc..e8109875 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -20,18 +20,6 @@ archives:
     builds:
       - fleet-agent
 
-  - id: fleet-gitcloner
-    format: binary
-    name_template: "{{ .Binary }}"
-    builds:
-      - fleet-gitcloner
-
-  - id: fleet-gitjob
-    format: binary
-    name_template: "{{ .Binary }}"
-    builds:
-      - fleet-gitjob
-
   - id: fleet-cli
     format: binary
     name_template: "{{ .Binary }}"
@@ -66,30 +54,6 @@ builds:
     - linux_amd64_v1
     - linux_arm64
     - windows_amd64
-  -
-    id: fleet-gitcloner
-    main: ./cmd/gitcloner
-    binary: gitcloner-{{ .Os }}-{{ .Arch }}{{ if .Arm }}64{{ end }}
-    no_unique_dist_dir: true
-    ldflags:
-      - -w -s
-      - -X github.com/rancher/fleet/pkg/version.GitCommit={{ .Commit }}
-      - -X github.com/rancher/fleet/pkg/version.Version={{ .Tag }}
-    targets:
-    - linux_amd64_v1
-    - linux_arm64
-  -
-    id: fleet-gitjob
-    main: ./cmd/gitjob
-    binary: gitjob-{{ .Os }}-{{ .Arch }}{{ if .Arm }}64{{ end }}
-    no_unique_dist_dir: true
-    ldflags:
-      - -w -s
-      - -X github.com/rancher/fleet/pkg/version.GitCommit={{ .Commit }}
-      - -X github.com/rancher/fleet/pkg/version.Version={{ .Tag }}
-    targets:
-    - linux_amd64_v1
-    - linux_arm64
   -
     id: fleet-cli
     main: ./cmd/fleetcli
@@ -130,8 +94,6 @@ dockers:
     ids:
     - fleet-controller
     - fleet-cli
-    - fleet-gitcloner
-    - fleet-gitjob
 
     # Templates of the Docker image names.
     image_templates:
@@ -159,8 +121,6 @@ dockers:
     ids:
     - fleet-controller
     - fleet-cli
-    - fleet-gitcloner
-    - fleet-gitjob
     image_templates:
     - "docker.io/rancher/fleet:{{ .Tag }}-linux-arm64"
     dockerfile: package/Dockerfile
diff --git a/README.md b/README.md
index 5b21dfd3..0cc21de7 100644
--- a/README.md
+++ b/README.md
@@ -37,9 +37,9 @@ Install the Fleet Helm charts (there's two because we separate out CRDs for ulti
 
 ```shell
 helm -n cattle-fleet-system install --create-namespace --wait \
-    fleet-crd https://github.com/rancher/fleet/releases/download/v0.9.3/fleet-crd-0.9.3.tgz
+    fleet-crd https://github.com/rancher/fleet/releases/download/v0.9.4/fleet-crd-0.9.4.tgz
 helm -n cattle-fleet-system install --create-namespace --wait \
-    fleet https://github.com/rancher/fleet/releases/download/v0.9.3/fleet-0.9.3.tgz
+    fleet https://github.com/rancher/fleet/releases/download/v0.9.4/fleet-0.9.4.tgz
 ```
 
 ## Add a Git Repo to watch
diff --git a/charts/fleet-agent/templates/deployment.yaml b/charts/fleet-agent/templates/deployment.yaml
index d9348424..2a3409ed 100644
--- a/charts/fleet-agent/templates/deployment.yaml
+++ b/charts/fleet-agent/templates/deployment.yaml
@@ -3,6 +3,7 @@ kind: StatefulSet
 metadata:
   name: fleet-agent
 spec:
+  serviceName: fleet-agent
   selector:
     matchLabels:
       app: fleet-agent
diff --git a/charts/fleet-agent/templates/service.yaml b/charts/fleet-agent/templates/service.yaml
new file mode 100644
index 00000000..36d35588
--- /dev/null
+++ b/charts/fleet-agent/templates/service.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: fleet-agent
+spec:
+    type: ClusterIP
+    clusterIP: None
+    selector:
+      app: fleet-agent
diff --git a/charts/fleet/templates/deployment_gitjob.yaml b/charts/fleet/templates/deployment_gitjob.yaml
index 699b9997..135a213e 100644
--- a/charts/fleet/templates/deployment_gitjob.yaml
+++ b/charts/fleet/templates/deployment_gitjob.yaml
@@ -17,6 +17,7 @@ spec:
         - image: "{{ template "system_default_registry" . }}{{ .Values.image.repository }}:{{ .Values.image.tag }}"
           name: gitjob
           args:
+          - fleetcontroller
           - gitjob
           - --gitjob-image
           - "{{ template "system_default_registry" . }}{{ .Values.image.repository }}:{{ .Values.image.tag }}"
diff --git a/cmd/gitcloner/main.go b/cmd/gitcloner/main.go
deleted file mode 100644
index 09d48e29..00000000
--- a/cmd/gitcloner/main.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package main
-
-import (
-	"os"
-
-	"github.com/rancher/fleet/cmd/gitcloner/cmd"
-	"github.com/rancher/fleet/cmd/gitcloner/gogit"
-	"github.com/sirupsen/logrus"
-)
-
-func main() {
-	logrus.Info("Starting to clone git repository")
-	cmd := cmd.New(gogit.NewCloner())
-	if err := cmd.Execute(); err != nil {
-		logrus.Errorf("Error cloning repository: %v", err)
-		os.Exit(1)
-	}
-}
diff --git a/dev/README.md b/dev/README.md
index c9979034..1977fae6 100644
--- a/dev/README.md
+++ b/dev/README.md
@@ -172,7 +172,7 @@ To build and run the infra setup command do:
 
 ```
 pushd e2e/testenv/infra
-  go build -o . ./...
+  go build
 popd
 ./e2e/testenv/infra/infra setup
 
@@ -258,9 +258,11 @@ fleet.
 FROM ubuntu:22.04
 
 ARG BUILDARCH=amd64
+ARG NVM_VERSION=v0.39.7
+ARG NODE_VERSION=20
 
 RUN apt update && apt upgrade -y
-RUN apt install -y wget curl git jq nodejs
+RUN apt install -y wget curl git jq zstd
 
 WORKDIR /tmp
 
@@ -281,6 +283,13 @@ RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/s
         chmod +x ./kubectl && \
         mv ./kubectl /usr/local/bin/kubectl
 
+RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh | bash && \
+        export NVM_DIR="$HOME/.nvm" && \
+        [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  && \
+        [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" && \
+        nvm install ${NODE_VERSION} && \
+        ln -s $(which node) /usr/bin/node && \
+        ln -s $(which npm) /usr/bin/npm
 ```
 
 Please also note, that the default behavior of `act` is to always pull images.
@@ -375,3 +384,113 @@ PR](https://github.com/nektos/act/pull/1988) already exists.
 
 A temporary workaround is to comment the step in the workflow file out which
 includes `tmate`.
+
+## Monitoring
+
+This sections describes how to add a monitoring stack to your development
+environment. It consists of Prometheus, kube-state-metrics, kube-operator,
+Node-Exporter, Alertmanager and Grafana. It does contain Grafana dashboards and
+Prometheus alerts, but it does not contain any Grafana dashboards or Prometheus
+alerts specific to Fleet.
+
+### Installation
+
+If you have a running system, run the following commands on the upstream
+cluster:
+
+```bash
+helm repo add prometheus-community \
+  https://prometheus-community.github.io/helm-charts
+helm repo update
+helm upgrade --install --create-namespace -n cattle-system-monitoring \
+  monitoring prometheus-community/kube-prometheus-stack
+```
+
+That alone suffices to get a working monitoring setup for the upstream cluster.
+But to connect it to the fleet-controller exported metrics, you need to add a
+service monitor. The service monitor is currently not part of the Helm chart.
+However, the necessary Kubernetes service resource is, unless you have disabled
+monitoring in the Helm chart when installing fleet.
+
+```yaml
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: monitoring-fleet-controller
+  namespace: cattle-fleet-system
+  labels:
+    release: monitoring # required to be recognized by the operator
+spec:
+  endpoints:
+  - honorLabels: true
+    path: /metrics
+    scheme: http
+    scrapeTimeout: 30s
+    port: metrics
+  jobLabel: fleet-controller
+  namespaceSelector:
+    matchNames:
+    - cattle-fleet-system
+  selector:
+    matchLabels:
+      app: fleet-controller
+
+```
+
+This configures Prometheus to scrape the metrics from the fleet-controller. By
+accessing the Prometheus UI, you can now see the metrics from the
+Fleet-Controller. They are all prefixed with `fleet_`, which you will need to
+enter into the expression field of the Graph page to to get auto-completion.
+
+> **__NOTE:__** The Prometheus UI can be used to check the Prometheus
+configuration (e.g., the scrape targets), scraped metrics, check alerts or
+draft them with PromQL or to create PromQL queries to be used in Grafana
+dashboards. It is not accessible by default and not meant to be shown to
+casual users.
+
+To access the Prometheus UI, you can forward the port of the Prometheus service
+to your local machine and access it.
+
+```bash
+kubectl port-forward -n cattle-system-monitoring \
+  svc/monitoring-kube-prometheus-prometheus 9090:9090
+```
+
+Alternatively, you can forward the port of the fleet-controller to your local
+machine. Then you can access the raw metrics at `http://localhost:8080/metrics`.
+
+```bash
+kubectl port-forward -n cattle-fleet-system \
+  svc/monitoring-fleet-controller 8080:8080
+```
+
+### Metrics
+
+There are metrics which will only be available when certain resources are
+created. To create those resources, you can use the following file. Since a
+`GitRepo` resource results in having `Bundle` and `BundleDeployment` resources,
+the `Cluster` resource is already available and the `ClusterGroup` resource is
+created by us, it is sufficient to create a `GitRepo` and `ClusterGroup` resource
+to see all the fleet specific metrics.
+
+```yaml
+kind: GitRepo
+apiVersion: fleet.cattle.io/v1alpha1
+metadata:
+  name: simple
+  namespace: fleet-local
+spec:
+  repo: https://github.com/rancher/fleet-examples
+  paths:
+  - simple
+---
+kind: ClusterGroup
+apiVersion: fleet.cattle.io/v1alpha1
+metadata:
+  name: local-group
+  namespace: fleet-local
+spec:
+  selector:
+    matchLabels:
+      name: local
+```
diff --git a/dev/build-fleet b/dev/build-fleet
index c8ea1cb5..c5eaa3f2 100755
--- a/dev/build-fleet
+++ b/dev/build-fleet
@@ -17,9 +17,6 @@ if ! git diff --quiet HEAD origin/main --  pkg/apis/fleet.cattle.io/v1alpha1; th
 fi
 
 export GOOS=linux
-# gitjob
-go build -gcflags='all=-N -l' -o "bin/gitcloner-linux-$GOARCH" ./cmd/gitcloner
-go build -gcflags='all=-N -l' -o "bin/gitjob-linux-$GOARCH" ./cmd/gitjob
 
 # fleet
 go build -gcflags='all=-N -l' -o "bin/fleet-linux-$GOARCH" ./cmd/fleetcli
diff --git a/go.mod b/go.mod
index 8c374afd..285ca426 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/Masterminds/sprig/v3 v3.2.3
 	github.com/cheggaaa/pb v1.0.29
 	github.com/davecgh/go-spew v1.1.1
-	github.com/docker/docker v26.1.0+incompatible
+	github.com/docker/docker v26.1.1+incompatible
 	github.com/evanphx/json-patch v5.9.0+incompatible
 	github.com/go-git/go-billy/v5 v5.5.0
 	github.com/go-git/go-git/v5 v5.12.0
@@ -27,14 +27,14 @@ require (
 	github.com/gorilla/mux v1.8.1
 	github.com/hashicorp/go-getter v1.7.4
 	github.com/jpillora/backoff v1.0.0
-	github.com/onsi/ginkgo/v2 v2.17.1
-	github.com/onsi/gomega v1.33.0
+	github.com/onsi/ginkgo/v2 v2.17.2
+	github.com/onsi/gomega v1.33.1
 	github.com/otiai10/copy v1.14.0
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.19.0
 	github.com/prometheus/client_model v0.6.1
 	github.com/prometheus/common v0.53.0
-	github.com/rancher/fleet/pkg/apis v0.10.0-rc.4
+	github.com/rancher/fleet/pkg/apis v0.10.0-rc.13
 	github.com/rancher/lasso v0.0.0-20240424194130-d87ec407d941
 	github.com/rancher/wrangler/v2 v2.1.4
 	github.com/reugn/go-quartz v0.11.2
@@ -49,15 +49,15 @@ require (
 	gopkg.in/yaml.v2 v2.4.0
 	gotest.tools v2.2.0+incompatible
 	helm.sh/helm/v3 v3.14.4
-	k8s.io/api v0.29.3
-	k8s.io/apiextensions-apiserver v0.29.2
-	k8s.io/apimachinery v0.29.3
-	k8s.io/cli-runtime v0.29.2
-	k8s.io/client-go v0.29.3
+	k8s.io/api v0.29.4
+	k8s.io/apiextensions-apiserver v0.29.4
+	k8s.io/apimachinery v0.29.4
+	k8s.io/cli-runtime v0.29.4
+	k8s.io/client-go v0.29.4
 	k8s.io/klog/v2 v2.120.1
-	k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108
-	k8s.io/kubectl v0.29.2
-	k8s.io/kubernetes v1.29.2
+	k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f
+	k8s.io/kubectl v0.29.4
+	k8s.io/kubernetes v1.29.4
 	sigs.k8s.io/cli-utils v0.34.0
 	sigs.k8s.io/controller-runtime v0.17.2
 	sigs.k8s.io/controller-tools v0.14.0
@@ -121,7 +121,7 @@ require (
 	github.com/go-openapi/jsonpointer v0.20.2 // indirect
 	github.com/go-openapi/jsonreference v0.20.4 // indirect
 	github.com/go-openapi/swag v0.22.9 // indirect
-	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
 	github.com/gobuffalo/flect v1.0.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -130,7 +130,7 @@ require (
 	github.com/google/btree v1.1.2 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
+	github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect
 	github.com/google/s2a-go v0.1.7 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
@@ -219,14 +219,14 @@ require (
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.26.0 // indirect
 	golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
-	golang.org/x/mod v0.16.0 // indirect
-	golang.org/x/net v0.23.0 // indirect
+	golang.org/x/mod v0.17.0 // indirect
+	golang.org/x/net v0.24.0 // indirect
 	golang.org/x/oauth2 v0.18.0 // indirect
 	golang.org/x/sys v0.19.0 // indirect
 	golang.org/x/term v0.19.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/time v0.5.0 // indirect
-	golang.org/x/tools v0.18.0 // indirect
+	golang.org/x/tools v0.20.0 // indirect
 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/api v0.126.0 // indirect
@@ -239,9 +239,9 @@ require (
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/apiserver v0.29.2 // indirect
-	k8s.io/code-generator v0.29.2 // indirect
-	k8s.io/component-base v0.29.2 // indirect
+	k8s.io/apiserver v0.29.4 // indirect
+	k8s.io/code-generator v0.29.4 // indirect
+	k8s.io/component-base v0.29.4 // indirect
 	k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 // indirect
 	k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
 	oras.land/oras-go v1.2.5 // indirect
diff --git a/go.sum b/go.sum
index 70ccc345..2157abbc 100644
--- a/go.sum
+++ b/go.sum
@@ -305,8 +305,8 @@ github.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbT
 github.com/docker/cli v25.0.1+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 v26.1.0+incompatible h1:W1G9MPNbskA6VZWL7b3ZljTh0pXI68FpINx0GKaOdaM=
-github.com/docker/docker v26.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v26.1.1+incompatible h1:oI+4kkAgIwwb54b9OC7Xc3hSgu1RlJA/Lln/DF72djQ=
+github.com/docker/docker v26.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
 github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
@@ -396,8 +396,8 @@ github.com/go-playground/webhooks/v6 v6.3.0/go.mod h1:GCocmfMtpJdkEOM1uG9p2nXzg1
 github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
 github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA=
 github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
 github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU=
@@ -502,8 +502,8 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
 github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
-github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
 github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
 github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
@@ -698,10 +698,10 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
-github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
-github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
-github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
-github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
+github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g=
+github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -930,8 +930,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
-golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -985,8 +985,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
-golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
-golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1199,8 +1199,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
-golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
+golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
+golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1469,33 +1469,33 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw=
-k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80=
-k8s.io/apiextensions-apiserver v0.29.2 h1:UK3xB5lOWSnhaCk0RFZ0LUacPZz9RY4wi/yt2Iu+btg=
-k8s.io/apiextensions-apiserver v0.29.2/go.mod h1:aLfYjpA5p3OwtqNXQFkhJ56TB+spV8Gc4wfMhUA3/b8=
-k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU=
-k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU=
-k8s.io/apiserver v0.29.2 h1:+Z9S0dSNr+CjnVXQePG8TcBWHr3Q7BmAr7NraHvsMiQ=
-k8s.io/apiserver v0.29.2/go.mod h1:B0LieKVoyU7ykQvPFm7XSdIHaCHSzCzQWPFa5bqbeMQ=
-k8s.io/cli-runtime v0.29.2 h1:smfsOcT4QujeghsNjECKN3lwyX9AwcFU0nvJ7sFN3ro=
-k8s.io/cli-runtime v0.29.2/go.mod h1:KLisYYfoqeNfO+MkTWvpqIyb1wpJmmFJhioA0xd4MW8=
-k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg=
-k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0=
-k8s.io/code-generator v0.29.2 h1:c9/iw2KnNpw2IRV+wwuG/Wns2TjPSgjWzbbjTevyiHI=
-k8s.io/code-generator v0.29.2/go.mod h1:FwFi3C9jCrmbPjekhaCYcYG1n07CYiW1+PAPCockaos=
-k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8=
-k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM=
+k8s.io/api v0.29.4 h1:WEnF/XdxuCxdG3ayHNRR8yH3cI1B/llkWBma6bq4R3w=
+k8s.io/api v0.29.4/go.mod h1:DetSv0t4FBTcEpfA84NJV3g9a7+rSzlUHk5ADAYHUv0=
+k8s.io/apiextensions-apiserver v0.29.4 h1:M7hbuHU/ckbibR7yPbe6DyNWgTFKNmZDbdZKD8q1Smk=
+k8s.io/apiextensions-apiserver v0.29.4/go.mod h1:TTDC9fB+0kHY2rogf5hgBR03KBKCwED+GHUsXGpR7SM=
+k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q=
+k8s.io/apimachinery v0.29.4/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y=
+k8s.io/apiserver v0.29.4 h1:wPwGOO58GQOpRiZu59P5eRoDcB7QtV+QBglkRiXwCiM=
+k8s.io/apiserver v0.29.4/go.mod h1:VqTF9t98HVfhKZVRohCPezsdUt9u2g3bHKftxGcXoRo=
+k8s.io/cli-runtime v0.29.4 h1:QvUrddBxVX6XFJ6z64cGpEk7e4bQduKweqbqq+qBd9g=
+k8s.io/cli-runtime v0.29.4/go.mod h1:NmklYuZ4DLfOr2XEIT8Nzl883KMZUCv7KMj3wMHayCA=
+k8s.io/client-go v0.29.4 h1:79ytIedxVfyXV8rpH3jCBW0u+un0fxHDwX5F9K8dPR8=
+k8s.io/client-go v0.29.4/go.mod h1:kC1thZQ4zQWYwldsfI088BbK6RkxK+aF5ebV8y9Q4tk=
+k8s.io/code-generator v0.29.4 h1:8ESudFNbY5/9BzB8KOEFG2uV9Q0AQxkc4mrQESr30Ks=
+k8s.io/code-generator v0.29.4/go.mod h1:7TYnI0dYItL2cKuhhgPSuF3WED9uMdELgbVXFfn/joE=
+k8s.io/component-base v0.29.4 h1:xeKzuuHI/1tjleu5jycDAcYbhAxeGHCQBZUY2eRIkOo=
+k8s.io/component-base v0.29.4/go.mod h1:pYjt+oEZP9gtmwSikwAJgfSBikqKX2gOqRat0QjmQt0=
 k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks=
 k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
 k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
 k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
-k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108 h1:Q8Z7VlGhcJgBHJHYugJ/K/7iB8a2eSxCyxdVjJp+lLY=
-k8s.io/kube-openapi v0.0.0-20240423202451-8948a665c108/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
-k8s.io/kubectl v0.29.2 h1:uaDYaBhumvkwz0S2XHt36fK0v5IdNgL7HyUniwb2IUo=
-k8s.io/kubectl v0.29.2/go.mod h1:BhizuYBGcKaHWyq+G7txGw2fXg576QbPrrnQdQDZgqI=
-k8s.io/kubernetes v1.29.2 h1:8hh1cntqdulanjQt7wSSSsJfBgOyx6fUdFWslvGL5m0=
-k8s.io/kubernetes v1.29.2/go.mod h1:xZPKU0yO0CBbLTnbd+XGyRmmtmaVuJykDb8gNCkeeUE=
+k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f h1:0LQagt0gDpKqvIkAMPaRGcXawNMouPECM1+F9BVxEaM=
+k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f/go.mod h1:S9tOR0FxgyusSNR+MboCuiDpVWkAifZvaYI1Q2ubgro=
+k8s.io/kubectl v0.29.4 h1:2LFrAznoDZjN8JFMSUcuhER5o+yjTLzWWbOiDzVjmd8=
+k8s.io/kubectl v0.29.4/go.mod h1:YTKRF9y1/ccqZ2bnpOWaJD8V7johKqZR/qOMq+0pfxU=
+k8s.io/kubernetes v1.29.4 h1:n4VCbX9cUhxHI+zw+m2iZlzT73/mrEJBHIMeauh9g4U=
+k8s.io/kubernetes v1.29.4/go.mod h1:28sDhcb87LX5z3GWAKYmLrhrifxi4W9bEWua4DRTIvk=
 k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
 k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
diff --git a/internal/cmd/cli/apply/apply.go b/internal/cmd/cli/apply/apply.go
index 5b8a3258..7faa1e41 100644
--- a/internal/cmd/cli/apply/apply.go
+++ b/internal/cmd/cli/apply/apply.go
@@ -249,8 +249,9 @@ func save(client Getter, bundle *fleet.Bundle, imageScans ...*fleet.ImageScan) e
 		return err
 	} else {
 		obj.Spec = bundle.Spec
-		obj.Annotations = mergeMap(obj.Annotations, bundle.Annotations)
-		obj.Labels = mergeMap(obj.Labels, bundle.Labels)
+		obj.Annotations = bundle.Annotations
+		obj.Labels = bundle.Labels
+
 		if _, err := c.Fleet.Bundle().Update(obj); err != nil {
 			return err
 		}
@@ -270,8 +271,8 @@ func save(client Getter, bundle *fleet.Bundle, imageScans ...*fleet.ImageScan) e
 			return err
 		} else {
 			obj.Spec = scan.Spec
-			obj.Annotations = mergeMap(obj.Annotations, bundle.Annotations)
-			obj.Labels = mergeMap(obj.Labels, bundle.Labels)
+			obj.Annotations = bundle.Annotations
+			obj.Labels = bundle.Labels
 			if _, err := c.Fleet.ImageScan().Update(obj); err != nil {
 				return err
 			}
@@ -281,17 +282,6 @@ func save(client Getter, bundle *fleet.Bundle, imageScans ...*fleet.ImageScan) e
 	return err
 }
 
-func mergeMap(a, b map[string]string) map[string]string {
-	result := map[string]string{}
-	for k, v := range a {
-		result[k] = v
-	}
-	for k, v := range b {
-		result[k] = v
-	}
-	return result
-}
-
 // shouldCreateBundleForThisPath returns true if a bundle should be created for this path. This happens when:
 // 1) Root path contains resources in the root directory or any subdirectory without a fleet.yaml.
 // 2) Or it is a subdirectory with a fleet.yaml
diff --git a/cmd/gitcloner/gogit/cloner.go b/internal/cmd/cli/gitcloner/cloner.go
similarity index 91%
rename from cmd/gitcloner/gogit/cloner.go
rename to internal/cmd/cli/gitcloner/cloner.go
index ffe471eb..511c5e32 100644
--- a/cmd/gitcloner/gogit/cloner.go
+++ b/internal/cmd/cli/gitcloner/cloner.go
@@ -1,11 +1,9 @@
-package gogit
+package gitcloner
 
 import (
 	"fmt"
 	"os"
 
-	"github.com/rancher/fleet/cmd/gitcloner/cmd"
-
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing"
 	"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
@@ -35,11 +33,11 @@ type Options struct {
 	CABundle        []byte
 }
 
-func NewCloner() *Cloner {
+func New() *Cloner {
 	return &Cloner{}
 }
 
-func (c *Cloner) CloneRepo(opts *cmd.Options) error {
+func (c *Cloner) CloneRepo(opts *GitCloner) error {
 	// Azure DevOps requires capabilities multi_ack / multi_ack_detailed,
 	// which are not fully implemented and by default are included in
 	// transport.UnsupportedCapabilities.
@@ -73,7 +71,7 @@ func (c *Cloner) CloneRepo(opts *cmd.Options) error {
 	return cloneRevision(opts, auth, caBundle)
 }
 
-func cloneBranch(opts *cmd.Options, auth transport.AuthMethod, caBundle []byte) error {
+func cloneBranch(opts *GitCloner, auth transport.AuthMethod, caBundle []byte) error {
 	_, err := plainClone(opts.Path, false, &git.CloneOptions{
 		URL:             opts.Repo,
 		Auth:            auth,
@@ -86,7 +84,7 @@ func cloneBranch(opts *cmd.Options, auth transport.AuthMethod, caBundle []byte)
 	return err
 }
 
-func cloneRevision(opts *cmd.Options, auth transport.AuthMethod, caBundle []byte) error {
+func cloneRevision(opts *GitCloner, auth transport.AuthMethod, caBundle []byte) error {
 	r, err := plainClone(opts.Path, false, &git.CloneOptions{
 		URL:             opts.Repo,
 		Auth:            auth,
@@ -118,7 +116,7 @@ func getCABundleFromFile(path string) ([]byte, error) {
 }
 
 // createAuthFromOpts adds auth for cloning git repos based on the parameters provided in opts.
-func createAuthFromOpts(opts *cmd.Options) (transport.AuthMethod, error) {
+func createAuthFromOpts(opts *GitCloner) (transport.AuthMethod, error) {
 	if opts.SSHPrivateKeyFile != "" {
 		privateKey, err := readFile(opts.SSHPrivateKeyFile)
 		if err != nil {
diff --git a/cmd/gitcloner/cmd/cmd.go b/internal/cmd/cli/gitcloner/cmd.go
similarity index 87%
rename from cmd/gitcloner/cmd/cmd.go
rename to internal/cmd/cli/gitcloner/cmd.go
index 4a51fbf5..2d219faf 100644
--- a/cmd/gitcloner/cmd/cmd.go
+++ b/internal/cmd/cli/gitcloner/cmd.go
@@ -1,14 +1,14 @@
-package cmd
+package gitcloner
 
 import (
 	"github.com/spf13/cobra"
 )
 
-type GitCloner interface {
-	CloneRepo(opts *Options) error
+type CloneGit interface {
+	CloneRepo(opts *GitCloner) error
 }
 
-type Options struct {
+type GitCloner struct {
 	Repo              string
 	Path              string
 	Branch            string
@@ -21,9 +21,9 @@ type Options struct {
 	KnownHostsFile    string
 }
 
-var opts *Options
+var opts *GitCloner
 
-func New(gitCloner GitCloner) *cobra.Command {
+func NewCmd(gitCloner CloneGit) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "gitcloner [REPO] [PATH]",
 		Short: "Clones a git repository",
@@ -35,7 +35,7 @@ func New(gitCloner GitCloner) *cobra.Command {
 			return gitCloner.CloneRepo(opts)
 		},
 	}
-	opts = &Options{}
+	opts = &GitCloner{}
 	cmd.Flags().StringVarP(&opts.Branch, "branch", "b", "", "git branch")
 	cmd.Flags().StringVar(&opts.Revision, "revision", "", "git revision")
 	cmd.Flags().StringVar(&opts.CABundleFile, "ca-bundle-file", "", "CA bundle file")
diff --git a/internal/cmd/cli/root.go b/internal/cmd/cli/root.go
index 9c855e47..3c9bb4ea 100644
--- a/internal/cmd/cli/root.go
+++ b/internal/cmd/cli/root.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/rancher/fleet/internal/client"
 	command "github.com/rancher/fleet/internal/cmd"
+	"github.com/rancher/fleet/internal/cmd/cli/gitcloner"
 	"github.com/rancher/fleet/pkg/version"
 )
 
@@ -32,6 +33,7 @@ func App() *cobra.Command {
 
 		NewTarget(),
 		NewDeploy(),
+		gitcloner.NewCmd(gitcloner.New()),
 	)
 
 	return root
diff --git a/internal/cmd/controller/agentmanagement/agent/manifest.go b/internal/cmd/controller/agentmanagement/agent/manifest.go
index 3c870d23..a09a4f62 100644
--- a/internal/cmd/controller/agentmanagement/agent/manifest.go
+++ b/internal/cmd/controller/agentmanagement/agent/manifest.go
@@ -115,9 +115,24 @@ func Manifest(namespace string, agentScope string, opts ManifestOptions) []runti
 		},
 	}
 
+	// StatefulSets require a headless service. The service is not used.
+	service := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      DefaultName,
+			Namespace: namespace,
+		},
+		Spec: corev1.ServiceSpec{
+			Selector: map[string]string{
+				"app": DefaultName,
+			},
+			Type:      corev1.ServiceTypeClusterIP,
+			ClusterIP: "None",
+		},
+	}
+
 	var objs []runtime.Object
 	objs = append(objs, clusterRole...)
-	objs = append(objs, admin, defaultSa, agent, networkPolicy)
+	objs = append(objs, admin, defaultSa, service, agent, networkPolicy)
 
 	return objs
 }
@@ -149,6 +164,7 @@ func agentApp(namespace string, agentScope string, opts ManifestOptions) *appsv1
 					"app": name,
 				},
 			},
+			ServiceName: name,
 			Template: corev1.PodTemplateSpec{
 				ObjectMeta: metav1.ObjectMeta{
 					Labels: map[string]string{
diff --git a/internal/cmd/controller/cleanup/controllers/cleanup/controller.go b/internal/cmd/controller/cleanup/controllers/cleanup/controller.go
index b161b301..6a6d891f 100644
--- a/internal/cmd/controller/cleanup/controllers/cleanup/controller.go
+++ b/internal/cmd/controller/cleanup/controllers/cleanup/controller.go
@@ -112,7 +112,7 @@ func (h *handler) cleanupNamespace(key string, obj *corev1.Namespace) (*corev1.N
 	// check if the cluster for this cluster namespace still exists, otherwise clean up the namespace
 	_, err := h.clusters.Get(obj.Annotations[fleet.ClusterNamespaceAnnotation], obj.Annotations[fleet.ClusterAnnotation])
 	if apierrors.IsNotFound(err) {
-		logrus.Infof("Cleaning up fleet-managed namespace %s", obj.Name)
+		logrus.Infof("Cleaning up fleet-managed namespace %q, cluster not found", obj.Name)
 
 		err = h.namespaces.Delete(key, nil)
 		return obj, err
@@ -141,15 +141,16 @@ func (h *handler) OnPurgeOrphaned(key string, bundle *fleet.Bundle) (*fleet.Bund
 	if bundle == nil {
 		return bundle, nil
 	}
-	logrus.Debugf("OnPurgeOrphaned for bundle '%s' change, checking if gitrepo still exists", bundle.Name)
 
 	repo := bundle.Labels[fleet.RepoLabel]
 	if repo == "" {
 		return nil, nil
 	}
+	logrus.Debugf("OnPurgeOrphaned for bundle '%s' change, checking if gitrepo still exists", bundle.Name)
 
 	_, err := h.gitRepo.Get(bundle.Namespace, repo)
 	if apierrors.IsNotFound(err) {
+		logrus.Infof("OnPurgeOrphaned for bundle '%s', gitrepo not found, delete bundle", bundle.Name)
 		return nil, h.bundles.Delete(bundle.Namespace, bundle.Name, nil)
 	} else if err != nil {
 		return nil, err
@@ -168,6 +169,7 @@ func (h *handler) OnPurgeOrphanedImageScan(key string, image *fleet.ImageScan) (
 
 	_, err := h.gitRepo.Get(image.Namespace, repo)
 	if apierrors.IsNotFound(err) {
+		logrus.Infof("OnPurgeOrphaned for imagescan '%s', gitrepo not found, delete imagescan", image.Name)
 		return nil, h.images.Delete(image.Namespace, image.Name, nil)
 	} else if err != nil {
 		return nil, err
diff --git a/cmd/gitjob/main.go b/internal/cmd/controller/gitops/operator.go
similarity index 54%
rename from cmd/gitjob/main.go
rename to internal/cmd/controller/gitops/operator.go
index 891d52fd..8560fc6d 100644
--- a/cmd/gitjob/main.go
+++ b/internal/cmd/controller/gitops/operator.go
@@ -1,19 +1,18 @@
-//go:generate bash ./scripts/controller-gen-generate.sh
-
-package main
+package gitops
 
 import (
 	"context"
-	"flag"
 	"fmt"
 	"net/http"
-	"os"
 	"time"
 
-	controller "github.com/rancher/fleet/internal/cmd/gitjob"
+	command "github.com/rancher/fleet/internal/cmd"
+	"github.com/rancher/fleet/internal/cmd/controller/gitops/reconciler"
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/fleet/pkg/git/poll"
+	"github.com/rancher/fleet/pkg/version"
 	"github.com/rancher/fleet/pkg/webhook"
+	"github.com/spf13/cobra"
 
 	"k8s.io/apimachinery/pkg/runtime"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -21,6 +20,7 @@ import (
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/cache"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	clog "sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
 
@@ -30,6 +30,7 @@ import (
 var (
 	scheme   = runtime.NewScheme()
 	setupLog = ctrl.Log.WithName("setup")
+	zopts    *zap.Options
 )
 
 func init() {
@@ -37,48 +38,61 @@ func init() {
 	utilruntime.Must(fleet.AddToScheme(scheme))
 }
 
-type flags struct {
-	metricsAddr          string
-	enableLeaderElection bool
-	image                string
-	listen               string
-	debug                bool
+type GitOperator struct {
+	command.DebugConfig
+	Kubeconfig           string `usage:"Kubeconfig file"`
+	Namespace            string `usage:"namespace to watch" default:"cattle-fleet-system" env:"NAMESPACE"`
+	MetricsAddr          string `name:"metrics-bind-address" default:":8081" usage:"The address the metric endpoint binds to."`
+	EnableLeaderElection bool   `name:"leader-elect" default:"true" usage:"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager."`
+	Image                string `name:"gitjob-image" default:"rancher/fleet:dev" usage:"The gitjob image that will be used in the generated job."`
+	Listen               string `default:":8080" usage:"The port the webhook listens."`
+}
+
+func App(zo *zap.Options) *cobra.Command {
+	zopts = zo
+	return command.Command(&GitOperator{}, cobra.Command{
+		Version: version.FriendlyVersion(),
+		Use:     "gitjob",
+	})
 }
 
-func main() {
-	ctx := ctrl.SetupSignalHandler()
-	if err := run(ctx); err != nil {
-		setupLog.Error(err, "error running gitjob")
-		os.Exit(1)
+func (g *GitOperator) PersistentPre(_ *cobra.Command, _ []string) error {
+	if err := g.SetupDebug(); err != nil {
+		return fmt.Errorf("failed to setup debug logging: %w", err)
 	}
+
+	return nil
 }
 
-func run(ctx context.Context) error {
-	namespace := os.Getenv("NAMESPACE")
-	flags := bindFlags()
+func (g *GitOperator) Run(cmd *cobra.Command, args []string) error {
+	ctx := clog.IntoContext(cmd.Context(), ctrl.Log.WithName("gitjob-reconciler"))
+	// TODO for compatibility, override zap opts with legacy debug opts. remove once manifests are updated.
+	zopts.Development = g.Debug
+	ctrl.SetLogger(zap.New(zap.UseFlagOptions(zopts)))
+
+	namespace := g.Namespace
 	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
 		Scheme: scheme,
 		Metrics: metricsserver.Options{
-			BindAddress: flags.metricsAddr,
+			BindAddress: g.MetricsAddr,
 		},
-		LeaderElection:          flags.enableLeaderElection,
+		LeaderElection:          g.EnableLeaderElection,
 		LeaderElectionID:        "gitjob-leader",
 		LeaderElectionNamespace: namespace,
 	})
 	if err != nil {
 		return err
 	}
-	reconciler := &controller.GitJobReconciler{
+	reconciler := &reconciler.GitJobReconciler{
 		Client:    mgr.GetClient(),
 		Scheme:    mgr.GetScheme(),
-		Image:     flags.image,
+		Image:     g.Image,
 		GitPoller: poll.NewHandler(ctx, mgr.GetClient()),
-		Log:       ctrl.Log.WithName("gitjob-reconciler"),
 	}
 
 	group := errgroup.Group{}
 	group.Go(func() error {
-		return startWebhook(ctx, namespace, flags.listen, mgr.GetClient(), mgr.GetCache())
+		return startWebhook(ctx, namespace, g.Listen, mgr.GetClient(), mgr.GetCache())
 	})
 	group.Go(func() error {
 		setupLog.Info("starting manager")
@@ -92,40 +106,6 @@ func run(ctx context.Context) error {
 	return group.Wait()
 }
 
-func bindFlags() flags {
-	var metricsAddr string
-	var enableLeaderElection bool
-	var image string
-	var listen string
-	var debug bool
-	flag.StringVar(&metricsAddr, "metrics-bind-address", ":8081", "The address the metric endpoint binds to.")
-	flag.StringVar(
-		&image,
-		"gitjob-image",
-		"rancher/fleet:dev",
-		"The gitjob image that will be used in the generated job.",
-	)
-	flag.StringVar(&listen, "listen", ":8080", "The port the webhook listens.")
-	flag.BoolVar(&enableLeaderElection, "leader-elect", true,
-		"Enable leader election for controller manager. "+
-			"Enabling this will ensure there is only one active controller manager.")
-	flag.BoolVar(&debug, "debug", false, "debug mode.")
-	opts := zap.Options{
-		Development: debug,
-	}
-	opts.BindFlags(flag.CommandLine)
-	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
-	flag.Parse()
-
-	return flags{
-		metricsAddr:          metricsAddr,
-		enableLeaderElection: enableLeaderElection,
-		image:                image,
-		listen:               listen,
-		debug:                debug,
-	}
-}
-
 func startWebhook(ctx context.Context, namespace string, addr string, client client.Client, cacheClient cache.Cache) error {
 	setupLog.Info("Setting up webhook listener")
 	handler, err := webhook.HandleHooks(ctx, namespace, client, cacheClient)
diff --git a/internal/cmd/gitjob/gitjob.go b/internal/cmd/controller/gitops/reconciler/gitjob_controller.go
similarity index 95%
rename from internal/cmd/gitjob/gitjob.go
rename to internal/cmd/controller/gitops/reconciler/gitjob_controller.go
index 292ae0ce..daa33ad1 100644
--- a/internal/cmd/gitjob/gitjob.go
+++ b/internal/cmd/controller/gitops/reconciler/gitjob_controller.go
@@ -1,4 +1,4 @@
-package gitjob
+package reconciler
 
 import (
 	"context"
@@ -9,14 +9,13 @@ import (
 	"strings"
 	"time"
 
-	"github.com/go-logr/logr"
-	"github.com/sirupsen/logrus"
-
 	grutil "github.com/rancher/fleet/internal/cmd/controller/gitrepo"
 	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+
 	"github.com/rancher/wrangler/v2/pkg/condition"
 	"github.com/rancher/wrangler/v2/pkg/kstatus"
 	"github.com/rancher/wrangler/v2/pkg/name"
+
 	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
@@ -31,6 +30,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/event"
+	"sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 )
 
@@ -56,7 +56,6 @@ type GitJobReconciler struct {
 	Scheme    *runtime.Scheme
 	Image     string
 	GitPoller GitPoller
-	Log       logr.Logger
 }
 
 func (r *GitJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
@@ -77,15 +76,22 @@ func (r *GitJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
 // For more details, check Reconcile and its Result here:
 // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile
 func (r *GitJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	logger := log.FromContext(ctx).WithName("gitjob")
 	var gitRepo v1alpha1.GitRepo
 
 	if err := r.Get(ctx, req.NamespacedName, &gitRepo); err != nil && !errors.IsNotFound(err) {
 		return ctrl.Result{}, err
 	} else if errors.IsNotFound(err) {
+		logger.V(1).Info("Gitrepo deleted, cleaning up poll jobs")
 		r.GitPoller.CleanUpGitRepoPollJobs(ctx)
 		return ctrl.Result{}, nil
 	}
 
+	logger = logger.WithValues("generation", gitRepo.Generation, "commit", gitRepo.Status.Commit)
+	ctx = log.IntoContext(ctx, logger)
+
+	logger.V(1).Info("Reconciling GitRepo")
+
 	r.GitPoller.AddOrModifyGitRepoPollJob(ctx, gitRepo)
 
 	var job batchv1.Job
@@ -109,7 +115,7 @@ func (r *GitJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 
 	if err = r.updateStatus(ctx, &gitRepo, &job); err != nil {
 		if errors.IsConflict(err) {
-			r.Log.Info("conflict updating status", "message", err)
+			logger.V(1).Info("conflict updating status, retrying", "message", err)
 			return ctrl.Result{Requeue: true}, nil // just retry, but don't show an error
 		}
 		return ctrl.Result{}, fmt.Errorf("error updating git job status: %v", err)
@@ -219,10 +225,11 @@ func (r *GitJobReconciler) updateStatus(ctx context.Context, gitRepo *v1alpha1.G
 }
 
 func (r *GitJobReconciler) deleteJobIfNeeded(ctx context.Context, gitRepo *v1alpha1.GitRepo, job *batchv1.Job) error {
+	logger := log.FromContext(ctx)
 	// if force delete is set, delete the job to make sure a new job is created
 	if gitRepo.Spec.ForceSyncGeneration != gitRepo.Status.UpdateGeneration {
 		gitRepo.Status.UpdateGeneration = gitRepo.Spec.ForceSyncGeneration
-		r.Log.Info("job deletion triggered because of ForceUpdateGeneration")
+		logger.Info("job deletion triggered because of ForceUpdateGeneration")
 		if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil && !errors.IsNotFound(err) {
 			return err
 		}
@@ -230,7 +237,7 @@ func (r *GitJobReconciler) deleteJobIfNeeded(ctx context.Context, gitRepo *v1alp
 
 	// k8s Jobs are immutable. Recreate the job if the GitRepo Spec has changed.
 	if gitRepo.Generation != gitRepo.Status.ObservedGeneration {
-		r.Log.Info("job deletion triggered because of generation change")
+		logger.Info("job deletion triggered because of generation change")
 		if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil && !errors.IsNotFound(err) {
 			return err
 		}
@@ -361,7 +368,8 @@ func (r *GitJobReconciler) computeJobSpec(ctx context.Context, gitrepo *v1alpha1
 	}
 
 	saName := name.SafeConcatName("git", gitrepo.Name)
-	args, envs := argsAndEnvs(gitrepo)
+	logger := log.FromContext(ctx)
+	args, envs := argsAndEnvs(gitrepo, logger.V(1).Enabled())
 
 	return &batchv1.JobSpec{
 		BackoffLimit: &two,
@@ -409,13 +417,13 @@ func (r *GitJobReconciler) computeJobSpec(ctx context.Context, gitrepo *v1alpha1
 	}, nil
 }
 
-func argsAndEnvs(gitrepo *v1alpha1.GitRepo) ([]string, []corev1.EnvVar) {
+func argsAndEnvs(gitrepo *v1alpha1.GitRepo, debug bool) ([]string, []corev1.EnvVar) {
 	args := []string{
 		"fleet",
 		"apply",
 	}
 
-	if logrus.IsLevelEnabled(logrus.DebugLevel) {
+	if debug {
 		args = append(args, "--debug", "--debug-level", "9")
 	}
 
@@ -610,7 +618,7 @@ func volumesFromSecret(
 }
 
 func (r *GitJobReconciler) generateInitContainer(ctx context.Context, obj *v1alpha1.GitRepo) (corev1.Container, error) {
-	args := []string{obj.Spec.Repo, "/workspace"}
+	args := []string{"gitcloner", obj.Spec.Repo, "/workspace"}
 	volumeMounts := []corev1.VolumeMount{
 		{
 			Name:      gitClonerVolumeName,
@@ -673,7 +681,7 @@ func (r *GitJobReconciler) generateInitContainer(ctx context.Context, obj *v1alp
 
 	return corev1.Container{
 		Command: []string{
-			"gitcloner",
+			"fleet",
 		},
 		Args:         args,
 		Image:        r.Image,
diff --git a/internal/cmd/controller/reconciler/cluster_controller.go b/internal/cmd/controller/reconciler/cluster_controller.go
index 9de92fef..6c05fc24 100644
--- a/internal/cmd/controller/reconciler/cluster_controller.go
+++ b/internal/cmd/controller/reconciler/cluster_controller.go
@@ -13,7 +13,6 @@ import (
 	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 	"github.com/rancher/fleet/pkg/durations"
 	"github.com/rancher/fleet/pkg/sharding"
-	"github.com/sirupsen/logrus"
 
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -170,8 +169,12 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 	}
 
 	if allReady && cluster.Status.ResourceCounts.Ready != cluster.Status.ResourceCounts.DesiredReady {
-		logrus.Debugf("Cluster %s/%s is not ready because not all gitrepos are ready: %d/%d, enqueue cluster again",
-			cluster.Namespace, cluster.Name, cluster.Status.ResourceCounts.Ready, cluster.Status.ResourceCounts.DesiredReady)
+		logger.V(1).Info("Cluster is not ready, because not all gitrepos are ready",
+			"namespace", cluster.Namespace,
+			"name", cluster.Name,
+			"ready", cluster.Status.ResourceCounts.Ready,
+			"desiredReady", cluster.Status.ResourceCounts.DesiredReady,
+		)
 
 		// Counts from gitrepo are out of sync with bundleDeployment state, retry in a number of seconds.
 		return ctrl.Result{
diff --git a/internal/cmd/controller/root.go b/internal/cmd/controller/root.go
index 2e81d1e8..bb1c19a2 100644
--- a/internal/cmd/controller/root.go
+++ b/internal/cmd/controller/root.go
@@ -15,6 +15,7 @@ import (
 
 	"github.com/rancher/fleet/internal/cmd/controller/agentmanagement"
 	"github.com/rancher/fleet/internal/cmd/controller/agentmanagement/agent"
+	"github.com/rancher/fleet/internal/cmd/controller/gitops"
 
 	"github.com/spf13/cobra"
 
@@ -156,8 +157,11 @@ func App() *cobra.Command {
 	ctrl.RegisterFlags(fs)
 	root.Flags().AddGoFlagSet(fs)
 
-	root.AddCommand(cleanup.App())
-	root.AddCommand(agentmanagement.App())
+	root.AddCommand(
+		cleanup.App(),
+		agentmanagement.App(),
+		gitops.App(&zopts),
+	)
 	return root
 }
 
diff --git a/internal/mocks/client_mock.go b/internal/mocks/client_mock.go
index 4e353f8e..159222f2 100644
--- a/internal/mocks/client_mock.go
+++ b/internal/mocks/client_mock.go
@@ -3,7 +3,7 @@
 //
 // Generated by this command:
 //
-//	mockgen --build_flags=--mod=mod -destination=../../../mocks/client_mock.go -package=mocks sigs.k8s.io/controller-runtime/pkg/client Client,SubResourceWriter
+//	mockgen --build_flags=--mod=mod -destination=../../../../mocks/client_mock.go -package=mocks sigs.k8s.io/controller-runtime/pkg/client Client,SubResourceWriter
 //
 
 // Package mocks is a generated GoMock package.
diff --git a/internal/mocks/poller_mock.go b/internal/mocks/poller_mock.go
index 7ad617b9..6e88fe09 100644
--- a/internal/mocks/poller_mock.go
+++ b/internal/mocks/poller_mock.go
@@ -1,9 +1,9 @@
 // Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/rancher/fleet/internal/cmd/gitjob (interfaces: GitPoller)
+// Source: github.com/rancher/fleet/internal/cmd/controller/gitops/reconciler (interfaces: GitPoller)
 //
 // Generated by this command:
 //
-//	mockgen --build_flags=--mod=mod -destination=../../mocks/poller_mock.go -package=mocks github.com/rancher/fleet/internal/cmd/gitjob GitPoller
+//	mockgen --build_flags=--mod=mod -destination=../../../../mocks/poller_mock.go -package=mocks github.com/rancher/fleet/internal/cmd/controller/gitops/reconciler GitPoller
 //
 
 // Package mocks is a generated GoMock package.
diff --git a/package/Dockerfile b/package/Dockerfile
index 009a650a..dc0306f7 100644
--- a/package/Dockerfile
+++ b/package/Dockerfile
@@ -3,32 +3,30 @@ ARG ARCH
 
 FROM --platform=linux/$ARCH registry.suse.com/bci/bci-base:15.5 AS base
 COPY package/log.sh /usr/bin/
-RUN zypper -n update && \
-    zypper -n install openssh-clients catatonit git-core && \
-    zypper -n clean -a
+RUN zypper rm -y container-suseconnect && \
+    zypper ar --priority=500 https://download.opensuse.org/repositories/Virtualization:containers/5.5/Virtualization:containers.repo && \
+    zypper --gpg-auto-import-keys ref && \
+    zypper -n update && \
+    zypper -n install --no-recommends openssh-clients tini git-core && \
+    zypper -n clean -a && \
+    rm -fr /var/log/zypp* /usr/share/doc
 
 FROM base AS copy_dapper
 ONBUILD ARG ARCH
 ONBUILD COPY bin/fleetcontroller-linux-$ARCH /usr/bin/fleetcontroller
 ONBUILD COPY bin/fleet-linux-$ARCH /usr/bin/fleet
-ONBUILD COPY bin/gitjob-linux-$ARCH /usr/bin/gitjob
-ONBUILD COPY bin/gitcloner-linux-$ARCH /usr/bin/gitcloner
 
 FROM base AS copy_buildx
 ONBUILD ARG TARGETARCH
 ONBUILD COPY bin/fleetcontroller-linux-$TARGETARCH /usr/bin/fleetcontroller
 ONBUILD COPY bin/fleet-linux-$TARGETARCH /usr/bin/fleet
-ONBUILD COPY bin/gitjob-linux-$TARGETARCH /usr/bin/gitjob
-ONBUILD COPY bin/gitcloner-linux-$TARGETARCH /usr/bin/gitcloner
 
 FROM base AS copy_goreleaser
 ONBUILD ARG ARCH
 ONBUILD COPY fleetcontroller-linux-$ARCH /usr/bin/fleetcontroller
 ONBUILD COPY fleet-linux-$ARCH /usr/bin/fleet
-ONBUILD COPY gitjob-linux-$ARCH /usr/bin/gitjob
-ONBUILD COPY gitcloner-linux-$ARCH /usr/bin/gitcloner
 
 FROM copy_${BUILD_ENV}
 USER 1000
-ENTRYPOINT ["catatonit", "--"]
+ENTRYPOINT ["tini", "--"]
 CMD ["fleetcontroller"]
diff --git a/pkg/apis/go.mod b/pkg/apis/go.mod
index 30f5ecb1..bceba340 100644
--- a/pkg/apis/go.mod
+++ b/pkg/apis/go.mod
@@ -5,7 +5,7 @@ go 1.22
 require (
 	github.com/rancher/wrangler/v2 v2.1.4
 	k8s.io/api v0.28.6
-	k8s.io/apimachinery v0.29.2
+	k8s.io/apimachinery v0.28.6
 	sigs.k8s.io/controller-runtime v0.16.3
 )
 
diff --git a/pkg/apis/go.sum b/pkg/apis/go.sum
index e3cfede1..ab774cfb 100644
--- a/pkg/apis/go.sum
+++ b/pkg/apis/go.sum
@@ -7,9 +7,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -29,10 +28,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
-github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
-github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
-github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
-github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
+github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
+github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
+github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
+github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rancher/wrangler/v2 v2.1.4 h1:ohov0i6A9dJHHO6sjfsH4Dqv93ZTdm5lIJVJdPzVdQc=
@@ -92,8 +91,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 k8s.io/api v0.28.6 h1:yy6u9CuIhmg55YvF/BavPBBXB+5QicB64njJXxVnzLo=
 k8s.io/api v0.28.6/go.mod h1:AM6Ys6g9MY3dl/XNaNfg/GePI0FT7WBGu8efU/lirAo=
-k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8=
-k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU=
+k8s.io/apimachinery v0.28.6 h1:RsTeR4z6S07srPg6XYrwXpTJVMXsjPXn0ODakMytSW0=
+k8s.io/apimachinery v0.28.6/go.mod h1:QFNX/kCl/EMT2WTSz8k4WLCv2XnkOLMaL8GAVRMdpsA=
 k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
 k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
 k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
diff --git a/pkg/git/fetch.go b/pkg/git/fetch.go
index 9890f647..27a98392 100644
--- a/pkg/git/fetch.go
+++ b/pkg/git/fetch.go
@@ -4,10 +4,12 @@ import (
 	"context"
 
 	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
 )
 
 const (
@@ -36,10 +38,11 @@ func (f *Fetch) LatestCommit(ctx context.Context, gitrepo *v1alpha1.GitRepo, cli
 		branch = "master"
 	}
 
-	git, err := newGit("", gitrepo.Spec.Repo, &options{
+	git, err := newGit(gitrepo.Spec.Repo, &options{
 		CABundle:          gitrepo.Spec.CABundle,
 		Credential:        &secret,
 		InsecureTLSVerify: gitrepo.Spec.InsecureSkipTLSverify,
+		log:               log.FromContext(ctx),
 	})
 	if err != nil {
 		return "", err
diff --git a/pkg/git/lsremote.go b/pkg/git/lsremote.go
index aca168de..31b3a470 100644
--- a/pkg/git/lsremote.go
+++ b/pkg/git/lsremote.go
@@ -17,9 +17,11 @@ import (
 	httpgit "github.com/go-git/go-git/v5/plumbing/transport/http"
 	gossh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 	"github.com/go-git/go-git/v5/storage/memory"
-	giturls "github.com/rancher/fleet/pkg/git-urls"
-	"github.com/sirupsen/logrus"
+	"github.com/go-logr/logr"
 	"golang.org/x/crypto/ssh"
+
+	giturls "github.com/rancher/fleet/pkg/git-urls"
+
 	corev1 "k8s.io/api/core/v1"
 )
 
@@ -28,9 +30,10 @@ type options struct {
 	CABundle          []byte
 	InsecureTLSVerify bool
 	Headers           map[string]string
+	log               logr.Logger
 }
 
-func newGit(directory, url string, opts *options) (*git, error) {
+func newGit(url string, opts *options) (*git, error) {
 	if err := validateURL(url); err != nil {
 		return nil, err
 	}
@@ -41,11 +44,12 @@ func newGit(directory, url string, opts *options) (*git, error) {
 
 	g := &git{
 		URL:               url,
-		Directory:         directory,
+		Directory:         "",
 		caBundle:          opts.CABundle,
 		insecureTLSVerify: opts.InsecureTLSVerify,
 		secret:            opts.Credential,
 		headers:           opts.Headers,
+		log:               opts.log.WithValues("url", url),
 	}
 	return g, g.setCredential(opts.Credential)
 }
@@ -58,6 +62,7 @@ type git struct {
 	secret            *corev1.Secret
 	headers           map[string]string
 	auth              transport.AuthMethod
+	log               logr.Logger
 }
 
 // LsRemote runs ls-remote on git repo and returns the HEAD commit SHA
@@ -158,14 +163,14 @@ func (g *git) remoteSHAChanged(branch, sha string) (bool, error) {
 
 	client, err := g.httpClientWithCreds()
 	if err != nil {
-		logrus.Warnf("Problem creating http client to check git remote sha of repo [%v]: %v", g.URL, err)
+		g.log.Error(err, "Problem creating http client to check git remote sha of repo")
 		return true, nil
 	}
 	defer client.CloseIdleConnections()
 
 	req, err := http.NewRequest("GET", formattedURL, nil)
 	if err != nil {
-		logrus.Warnf("Problem creating request to check git remote sha of repo [%v]: %v", g.URL, err)
+		g.log.Error(err, "Problem creating request to check git remote sha of repo")
 		return true, nil
 	}
 
diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go
index 4f5cdd4e..e96520ba 100644
--- a/pkg/webhook/webhook.go
+++ b/pkg/webhook/webhook.go
@@ -9,29 +9,27 @@ import (
 	"strings"
 	"time"
 
-	goPlaygroundAzuredevops "github.com/go-playground/webhooks/v6/azuredevops"
-	"github.com/rancher/fleet/pkg/webhook/azuredevops"
-
 	"github.com/go-logr/logr"
-	ctrl "sigs.k8s.io/controller-runtime"
-
-	"github.com/gorilla/mux"
-	corev1 "k8s.io/api/core/v1"
-	kcache "k8s.io/client-go/tools/cache"
-	"k8s.io/client-go/util/retry"
-	"sigs.k8s.io/controller-runtime/pkg/cache"
-
+	goPlaygroundAzuredevops "github.com/go-playground/webhooks/v6/azuredevops"
 	gogsclient "github.com/gogits/go-gogs-client"
-	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
-	"github.com/sirupsen/logrus"
+	"github.com/gorilla/mux"
 	"gopkg.in/go-playground/webhooks.v5/bitbucket"
 	bitbucketserver "gopkg.in/go-playground/webhooks.v5/bitbucket-server"
 	"gopkg.in/go-playground/webhooks.v5/github"
 	"gopkg.in/go-playground/webhooks.v5/gitlab"
 	"gopkg.in/go-playground/webhooks.v5/gogs"
+
+	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+	"github.com/rancher/fleet/pkg/webhook/azuredevops"
+
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	ktypes "k8s.io/apimachinery/pkg/types"
+	kcache "k8s.io/client-go/tools/cache"
+	"k8s.io/client-go/util/retry"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/cache"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -172,14 +170,14 @@ func (w *Webhook) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
 	case r.Header.Get("X-Vss-Activityid") != "" || r.Header.Get("X-Vss-Subscriptionid") != "":
 		payload, err = w.azureDevops.Parse(r, goPlaygroundAzuredevops.GitPushEventType)
 	default:
-		logrus.Debug("Ignoring unknown webhook event")
+		w.log.V(1).Info("Ignoring unknown webhook event")
 		return
 	}
 
-	logrus.Debugf("Webhook payload %+v", payload)
+	w.log.V(1).Info("Webhook payload", "payload", payload)
 
 	if err != nil {
-		logAndReturn(rw, err)
+		w.logAndReturn(rw, err)
 		return
 	}
 
@@ -188,20 +186,20 @@ func (w *Webhook) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
 	var gitRepoList v1alpha1.GitRepoList
 	err = w.client.List(ctx, &gitRepoList, &client.ListOptions{LabelSelector: labels.Everything()})
 	if err != nil {
-		logAndReturn(rw, err)
+		w.logAndReturn(rw, err)
 		return
 	}
 
 	for _, repo := range repoURLs {
 		u, err := url.Parse(repo)
 		if err != nil {
-			logAndReturn(rw, err)
+			w.logAndReturn(rw, err)
 			return
 		}
 		regexpStr := `(?i)(http://|https://|\w+@|ssh://(\w+@)?)` + u.Hostname() + "(:[0-9]+|)[:/]" + u.Path[1:] + "(\\.git)?"
 		repoRegexp, err := regexp.Compile(regexpStr)
 		if err != nil {
-			logAndReturn(rw, err)
+			w.logAndReturn(rw, err)
 			return
 		}
 		for _, gitrepo := range gitRepoList.Items {
@@ -242,7 +240,7 @@ func (w *Webhook) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
 					}
 					return w.client.Status().Update(ctx, &gitRepoFromCluster)
 				}); err != nil {
-					logAndReturn(rw, err)
+					w.logAndReturn(rw, err)
 					return
 				}
 			}
@@ -294,8 +292,8 @@ func HandleHooks(ctx context.Context, namespace string, client client.Client, cl
 	return root, nil
 }
 
-func logAndReturn(rw http.ResponseWriter, err error) {
-	logrus.Errorf("Webhook processing failed: %s", err)
+func (w *Webhook) logAndReturn(rw http.ResponseWriter, err error) {
+	w.log.Error(err, "Webhook processing failed")
 	rw.WriteHeader(500)
 	_, _ = rw.Write([]byte(err.Error()))
 }


Test patch:
diff --git a/.github/workflows/e2e-ci.yml b/.github/workflows/e2e-ci.yml
index 6f4f661a..a7ceb396 100644
--- a/.github/workflows/e2e-ci.yml
+++ b/.github/workflows/e2e-ci.yml
@@ -48,7 +48,7 @@ jobs:
           ./.github/scripts/build-fleet-binaries.sh
 
           cd e2e/testenv/infra
-          go build -o . ./...
+          go build
       -
         name: Build Docker Images
         run: |
diff --git a/e2e/assets/gitrepo/gitrepo_sharded.yaml b/e2e/assets/gitrepo/gitrepo_sharded.yaml
index 369ec3b0..f58c9fae 100644
--- a/e2e/assets/gitrepo/gitrepo_sharded.yaml
+++ b/e2e/assets/gitrepo/gitrepo_sharded.yaml
@@ -10,4 +10,4 @@ spec:
   pollingInterval: {{.PollingInterval}}
   targetNamespace: {{.TargetNamespace}}
   paths:
-  - examples
+  - simple-chart
diff --git a/e2e/metrics/bundle_test.go b/e2e/metrics/bundle_test.go
index 91a836b0..c467895e 100644
--- a/e2e/metrics/bundle_test.go
+++ b/e2e/metrics/bundle_test.go
@@ -23,38 +23,9 @@ var _ = Describe("Bundle Metrics", Label("bundle"), func() {
 
 	var (
 		// kw is the kubectl command for namespace the workload is deployed to
-		kw        kubectl.Command
-		namespace string
-	)
-
-	BeforeEach(func() {
-		k = env.Kubectl.Namespace(env.Namespace)
-		namespace = testenv.NewNamespaceName(
-			gitRepoName,
-			rand.New(rand.NewSource(time.Now().UnixNano())),
-		)
-		kw = k.Namespace(namespace)
-
-		out, err := k.Create("ns", namespace)
-		Expect(err).ToNot(HaveOccurred(), out)
-
-		err = testenv.CreateGitRepo(
-			kw,
-			namespace,
-			gitRepoName,
-			branch,
-			"simple-manifest",
-		)
-		Expect(err).ToNot(HaveOccurred())
-
-		DeferCleanup(func() {
-			out, err = k.Delete("ns", namespace)
-			Expect(err).ToNot(HaveOccurred(), out)
-		})
-	})
-
-	When("collecting Bundle metrics", func() {
-		bundleMetricNames := map[string]map[string][]string{
+		kw                kubectl.Command
+		namespace         string
+		bundleMetricNames = map[string]map[string][]string{
 			"fleet_bundle_desired_ready": {},
 			"fleet_bundle_err_applied":   {},
 			"fleet_bundle_modified":      {},
@@ -75,65 +46,122 @@ var _ = Describe("Bundle Metrics", Label("bundle"), func() {
 				},
 			},
 		}
+	)
 
-		// checkMetrics checks that the metrics exist or not exist. Custom
-		// checks can be added by passing a function to check. This can be used
-		// to check for the value of the metrics.
-		checkMetrics := func(
-			gitRepoName string,
-			expectExists bool,
-			check func(metric *metrics.Metric) error,
-		) func() error {
-			return func() error {
-				et := metrics.NewExporterTest(metricsURL)
-				expectOne := func(metricName string, labels map[string]string) error {
-					metric, err := et.FindOneMetric(metricName, labels)
-					if expectExists && err != nil {
+	// metricsExist checks that the metrics exist. Custom checks can be
+	// added by passing a function to check. This can be used to check for
+	// the value of the metrics.
+	metricsExist := func(gitRepoName string, check func(metric *metrics.Metric) error) func() error {
+		return func() error {
+			et := metrics.NewExporterTest(metricsURL)
+			metrics, err := et.Get()
+			Expect(err).ToNot(HaveOccurred())
+
+			identityLabels := map[string]string{
+				"name":      gitRepoName,
+				"namespace": namespace,
+			}
+
+			for metricName, matchLabels := range bundleMetricNames {
+				labels := map[string]string{}
+				maps.Copy(labels, identityLabels)
+
+				if len(matchLabels) > 0 {
+					for labelName, labelValues := range matchLabels {
+						for _, labelValue := range labelValues {
+							labels[labelName] = labelValue
+							metric, err := et.FindOneMetric(metrics, metricName, labels)
+							if err != nil {
+								return err
+							}
+							if check != nil {
+								if err := check(metric); err != nil {
+									return err
+								}
+							}
+						}
+					}
+				} else {
+					metric, err := et.FindOneMetric(metrics, metricName, labels)
+					if err != nil {
 						return err
-					} else if !expectExists && err == nil {
-						return fmt.Errorf("metric %s found but not expected", metricName)
 					}
-
 					if check != nil {
-						err = check(metric)
-						if err != nil {
+						if err := check(metric); err != nil {
 							return err
 						}
 					}
-					return nil
 				}
+			}
+			return nil
+		}
+	}
+
+	// metricsMissing checks that the metrics do not exist.
+	metricsMissing := func(gitRepoName string) func() error {
+		return func() error {
+			et := metrics.NewExporterTest(metricsURL)
+			metrics, err := et.Get()
+			Expect(err).ToNot(HaveOccurred())
+
+			identityLabels := map[string]string{
+				"name":      gitRepoName,
+				"namespace": namespace,
+			}
 
-				for metricName, matchLabels := range bundleMetricNames {
-					identityLabels := map[string]string{
-						"name":      gitRepoName,
-						"namespace": namespace,
-					}
-					labels := map[string]string{}
-					maps.Copy(labels, identityLabels)
-
-					if len(matchLabels) > 0 {
-						for labelName, labelValues := range matchLabels {
-							for _, labelValue := range labelValues {
-								labels[labelName] = labelValue
-								err := expectOne(metricName, labels)
-								if err != nil {
-									return err
-								}
+			for metricName, matchLabels := range bundleMetricNames {
+				labels := map[string]string{}
+				maps.Copy(labels, identityLabels)
+
+				if len(matchLabels) > 0 {
+					for labelName, labelValues := range matchLabels {
+						for _, labelValue := range labelValues {
+							labels[labelName] = labelValue
+							if _, err := et.FindOneMetric(metrics, metricName, labels); err == nil {
+								return fmt.Errorf("metric %s found but not expected", metricName)
 							}
 						}
-					} else {
-						err := expectOne(metricName, labels)
-						if err != nil {
-							return err
-						}
 					}
+					return nil
+				}
+
+				if _, err := et.FindOneMetric(metrics, metricName, labels); err == nil {
+					return fmt.Errorf("metric %s found but not expected", metricName)
 				}
-				return nil
 			}
+			return nil
 		}
+	}
+
+	BeforeEach(func() {
+		k = env.Kubectl.Namespace(env.Namespace)
+		namespace = testenv.NewNamespaceName(
+			gitRepoName,
+			rand.New(rand.NewSource(time.Now().UnixNano())),
+		)
+		kw = k.Namespace(namespace)
+
+		out, err := k.Create("ns", namespace)
+		Expect(err).ToNot(HaveOccurred(), out)
+
+		err = testenv.CreateGitRepo(
+			kw,
+			namespace,
+			gitRepoName,
+			branch,
+			"simple-manifest",
+		)
+		Expect(err).ToNot(HaveOccurred())
+
+		DeferCleanup(func() {
+			out, err = k.Delete("ns", namespace)
+			Expect(err).ToNot(HaveOccurred(), out)
+		})
+	})
 
+	When("collecting Bundle metrics", func() {
 		It("should have one metric for each specified metric and label value", func() {
-			Eventually(checkMetrics(gitRepoName+"-simple-manifest", true, func(metric *metrics.Metric) error {
+			Eventually(metricsExist(gitRepoName+"-simple-manifest", func(metric *metrics.Metric) error {
 				// No cluster exists in the namespace where our GitRepo has been deployed, hence
 				// we expect the values of the metrics to be 0.
 				if value := metric.Gauge.GetValue(); value != float64(0) {
@@ -154,7 +182,7 @@ var _ = Describe("Bundle Metrics", Label("bundle"), func() {
 				Expect(err).ToNot(HaveOccurred(), out)
 				Expect(out).To(ContainSubstring("gitrepo.fleet.cattle.io/metrics patched"))
 
-				Eventually(checkMetrics(gitRepoName+"-simple-chart", true, func(metric *metrics.Metric) error {
+				Eventually(metricsExist(gitRepoName+"-simple-chart", func(metric *metrics.Metric) error {
 					if metric.LabelValue("paths") == "simple-manifest" {
 						return fmt.Errorf("path for metric %s unchanged", metric.Metric.String())
 					}
@@ -174,7 +202,7 @@ var _ = Describe("Bundle Metrics", Label("bundle"), func() {
 					return err
 				}).ShouldNot(HaveOccurred(), out)
 
-				Eventually(checkMetrics(gitRepoName, false, nil)).ShouldNot(HaveOccurred())
+				Eventually(metricsMissing(gitRepoName)).ShouldNot(HaveOccurred())
 			})
 		})
 	})
diff --git a/e2e/metrics/bundledeployment_test.go b/e2e/metrics/bundledeployment_test.go
index 875088c3..3e5dcff3 100644
--- a/e2e/metrics/bundledeployment_test.go
+++ b/e2e/metrics/bundledeployment_test.go
@@ -50,31 +50,74 @@ var _ = Describe("BundleDeployment Metrics", Label("bundledeployment"), func() {
 		})
 	})
 
-	When("testing BundleDeployment metrics", func() {
-		bundleDeploymentMetricNames := []string{
-			"fleet_bundledeployment_state",
-		}
-		bundleDeploymentMetricStates := []string{
-			"ErrApplied",
-			"Modified",
-			"NotReady",
-			"OutOfSync",
-			"Pending",
-			"Ready",
-			"WaitApplied",
-		}
-
-		It("should have exactly one metric for the BundleDeployment", func() {
+	bundleDeploymentMetricNames := []string{
+		"fleet_bundledeployment_state",
+	}
+	bundleDeploymentMetricStates := []string{
+		"ErrApplied",
+		"Modified",
+		"NotReady",
+		"OutOfSync",
+		"Pending",
+		"Ready",
+		"WaitApplied",
+	}
+
+	It("should have exactly one metric for the BundleDeployment", func() {
+		et := metrics.NewExporterTest(metricsURL)
+		Eventually(func() error {
+			metrics, err := et.Get()
+			Expect(err).ToNot(HaveOccurred())
+			for _, metricName := range bundleDeploymentMetricNames {
+				for _, state := range bundleDeploymentMetricStates {
+					_, err := et.FindOneMetric(
+						metrics,
+						metricName,
+						map[string]string{
+							"name":              objName + "-simple-manifest",
+							"cluster_namespace": namespace,
+							"state":             state,
+						},
+					)
+					if err != nil {
+						return err
+					}
+				}
+			}
+			return nil
+		}).ShouldNot(HaveOccurred())
+	})
+
+	When("the GitRepo (and therefore Bundle) is changed", Label("bundle-altered"), func() {
+		It("should not duplicate metrics if Bundle is updated", Label("bundle-update"), func() {
 			et := metrics.NewExporterTest(metricsURL)
+			out, err := kw.Patch(
+				"gitrepo", objName,
+				"--type=json",
+				"-p",
+				`[{"op": "replace", "path": "/spec/paths", "value": ["simple-chart"]}]`,
+			)
+			Expect(err).ToNot(HaveOccurred(), out)
+			Expect(out).To(ContainSubstring(fmt.Sprintf("gitrepo.fleet.cattle.io/%s patched", objName)))
+
+			// Wait for it to be changed and fetched.
+			Eventually(func() (string, error) {
+				return kw.Get("gitrepo", objName, "-o", "jsonpath={.status.commit}")
+			}).ShouldNot(BeEmpty())
+
+			// Expect still no metrics to be duplicated.
 			Eventually(func() error {
+				metrics, err := et.Get()
+				Expect(err).ToNot(HaveOccurred())
 				for _, metricName := range bundleDeploymentMetricNames {
-					for _, state := range bundleDeploymentMetricStates {
-						_, err := et.FindOneMetric(
+					for _, metricState := range bundleDeploymentMetricStates {
+						_, err = et.FindOneMetric(
+							metrics,
 							metricName,
 							map[string]string{
-								"name":              objName + "-simple-manifest",
+								"name":              objName + "-simple-chart",
 								"cluster_namespace": namespace,
-								"state":             state,
+								"state":             metricState,
 							},
 						)
 						if err != nil {
@@ -86,83 +129,36 @@ var _ = Describe("BundleDeployment Metrics", Label("bundledeployment"), func() {
 			}).ShouldNot(HaveOccurred())
 		})
 
-		Context(
-			"when the GitRepo (and therefore Bundle) is changed",
-			Label("bundle-altered"),
-			func() {
-				It(
-					"it should not duplicate metrics if Bundle is updated",
-					Label("bundle-update"),
-					func() {
-						et := metrics.NewExporterTest(metricsURL)
-						out, err := kw.Patch(
-							"gitrepo", objName,
-							"--type=json",
-							"-p",
-							`[{"op": "replace", "path": "/spec/paths", "value": ["simple-chart"]}]`,
-						)
-						Expect(err).ToNot(HaveOccurred(), out)
-						Expect(out).To(ContainSubstring(
-							fmt.Sprintf("gitrepo.fleet.cattle.io/%s patched", objName)))
-
-						// Wait for it to be changed and fetched.
-						Eventually(func() (string, error) {
-							return kw.Get("gitrepo", objName, "-o", "jsonpath={.status.commit}")
-						}).ShouldNot(BeEmpty())
-
-						// Expect still no metrics to be duplicated.
-						Eventually(func() error {
-							for _, metricName := range bundleDeploymentMetricNames {
-								for _, metricState := range bundleDeploymentMetricStates {
-									_, err = et.FindOneMetric(
-										metricName,
-										map[string]string{
-											"name":              objName + "-simple-chart",
-											"cluster_namespace": namespace,
-											"state":             metricState,
-										},
-									)
-									if err != nil {
-										return err
-									}
-								}
-							}
-							return nil
-						}).ShouldNot(HaveOccurred())
-					})
-
-				It("should not keep metrics if Bundle is deleted", Label("bundle-delete"), func() {
-					et := metrics.NewExporterTest(metricsURL)
-
-					objName := objName + "-simple-manifest"
-
-					Eventually(func() (string, error) {
-						return kw.Get("-A", "bundledeployment")
-					}).Should(ContainSubstring(objName))
-
-					var (
-						out string
-						err error
+		It("should not keep metrics if Bundle is deleted", Label("bundle-delete"), func() {
+			et := metrics.NewExporterTest(metricsURL)
+
+			objName := objName + "-simple-manifest"
+
+			Eventually(func() (string, error) {
+				return kw.Get("-A", "bundledeployment")
+			}).Should(ContainSubstring(objName))
+
+			out, err := kw.Delete("bundle", objName)
+			Expect(err).ToNot(HaveOccurred(), out)
+
+			Eventually(func() error {
+				metrics, err := et.Get()
+				Expect(err).ToNot(HaveOccurred())
+				for _, metricName := range bundleDeploymentMetricNames {
+					_, err := et.FindOneMetric(
+						metrics,
+						metricName,
+						map[string]string{
+							"name":      objName,
+							"namespace": namespace,
+						},
 					)
-					out, err = kw.Delete("bundle", objName)
-					Expect(err).ToNot(HaveOccurred(), out)
-
-					Eventually(func() error {
-						for _, metricName := range bundleDeploymentMetricNames {
-							_, err := et.FindOneMetric(
-								metricName,
-								map[string]string{
-									"name":      objName,
-									"namespace": namespace,
-								},
-							)
-							if err == nil {
-								return fmt.Errorf("metric %s found but not expected", metricName)
-							}
-						}
-						return nil
-					}).ShouldNot(HaveOccurred())
-				})
-			})
+					if err == nil {
+						return fmt.Errorf("metric %s found but not expected", metricName)
+					}
+				}
+				return nil
+			}).ShouldNot(HaveOccurred())
+		})
 	})
 })
diff --git a/e2e/metrics/cluster_test.go b/e2e/metrics/cluster_test.go
index 5d99cc98..15d9bd84 100644
--- a/e2e/metrics/cluster_test.go
+++ b/e2e/metrics/cluster_test.go
@@ -64,11 +64,7 @@ var _ = Describe("Cluster Metrics", Label("cluster"), func() {
 
 	It("should have metrics for all existing cluster resources", func() {
 		Eventually(func() error {
-			var (
-				clustersOut string
-				err         error
-			)
-			clustersOut, err = env.Kubectl.Get(
+			clustersOut, err := env.Kubectl.Get(
 				"-A", "clusters.fleet.cattle.io",
 				"-o", "json",
 			)
@@ -77,15 +73,17 @@ var _ = Describe("Cluster Metrics", Label("cluster"), func() {
 			var existingClusters clusters
 			err = json.Unmarshal([]byte(clustersOut), &existingClusters)
 			Expect(err).ToNot(HaveOccurred())
+			Expect(len(existingClusters)).ToNot(BeZero())
 
 			et := metrics.NewExporterTest(metricsURL)
-
-			Expect(len(existingClusters)).ToNot(BeZero())
+			metrics, err := et.Get()
+			Expect(err).ToNot(HaveOccurred())
 
 			for _, cluster := range existingClusters {
-				for metricName, expectedExist := range expectedMetricsExist {
+				for name, expectedExist := range expectedMetricsExist {
 					_, err := et.FindOneMetric(
-						metricName,
+						metrics,
+						name,
 						map[string]string{
 							"name":      cluster.Name,
 							"namespace": cluster.Namespace,
@@ -96,7 +94,7 @@ var _ = Describe("Cluster Metrics", Label("cluster"), func() {
 					} else if !expectedExist && err == nil {
 						return fmt.Errorf(
 							"expected metric %s not to exist, but it exists",
-							metricName,
+							name,
 						)
 					}
 				}
diff --git a/e2e/metrics/clustergroup_test.go b/e2e/metrics/clustergroup_test.go
index cf0f54e7..83addebd 100644
--- a/e2e/metrics/clustergroup_test.go
+++ b/e2e/metrics/clustergroup_test.go
@@ -84,26 +84,25 @@ var _ = Describe("Cluster Metrics", Label("clustergroup"), func() {
 
 		et := metrics.NewExporterTest(metricsURL)
 
-		Eventually(func() error {
-			identityLabels := map[string]string{
-				"name":      clusterGroupName,
-				"namespace": namespace,
-			}
+		identityLabels := map[string]string{
+			"name":      clusterGroupName,
+			"namespace": namespace,
+		}
 
+		Eventually(func() error {
+			metrics, err := et.Get()
+			Expect(err).ToNot(HaveOccurred())
 			for metricName, expectedLabels := range expectedMetricsExist {
 				if len(expectedLabels) == 0 {
-					_, err := et.FindOneMetric(metricName, identityLabels)
-					if err != nil {
+					if _, err := et.FindOneMetric(metrics, metricName, identityLabels); err != nil {
 						return err
 					}
-					Expect(err).ToNot(HaveOccurred())
 				} else {
 					for labelName, labelValues := range expectedLabels {
 						for _, labelValue := range labelValues {
 							labels := maps.Clone(identityLabels)
 							labels[labelName] = labelValue
-							_, err := et.FindOneMetric(metricName, labels)
-							if err != nil {
+							if _, err := et.FindOneMetric(metrics, metricName, labels); err != nil {
 								return err
 							}
 						}
diff --git a/e2e/metrics/exporter.go b/e2e/metrics/exporter.go
index 76f2dfd8..f4cbc254 100644
--- a/e2e/metrics/exporter.go
+++ b/e2e/metrics/exporter.go
@@ -19,9 +19,9 @@ func NewExporterTest(url string) *ExporterTest {
 	}
 }
 
-// getMetrics fetches the metrics from the Prometheus endpoint and returns them
+// Get fetches the metrics from the Prometheus endpoint and returns them
 // as a map of metric families.
-func (et *ExporterTest) getMetrics() (map[string]*dto.MetricFamily, error) {
+func (et *ExporterTest) Get() (map[string]*dto.MetricFamily, error) {
 	resp, err := http.Get(et.url)
 	if err != nil {
 		return nil, err
@@ -49,18 +49,14 @@ func (et *ExporterTest) getMetrics() (map[string]*dto.MetricFamily, error) {
 //
 // If labels is nil, only the name and namespace labels are checked.
 func (m *ExporterTest) FindOneMetric(
-	metricName string,
+	allMetrics map[string]*dto.MetricFamily,
+	name string,
 	labels map[string]string,
 ) (*Metric, error) {
-	allMetrics, err := m.getMetrics()
-	if err != nil {
-		return nil, fmt.Errorf("failed to get metrics: %w", err)
-	}
-
-	// Metric name exists.
-	mf, ok := allMetrics[metricName]
+	// check if name exists.
+	mf, ok := allMetrics[name]
 	if !ok {
-		return nil, fmt.Errorf("metric %q not found", metricName)
+		return nil, fmt.Errorf("metric %q not found", name)
 	}
 
 	var metrics []*dto.Metric
@@ -83,7 +79,7 @@ func (m *ExporterTest) FindOneMetric(
 	if len(metrics) != 1 {
 		return nil, fmt.Errorf(
 			"expected to find 1 metric for %s{%s}, got %d",
-			metricName,
+			name,
 			promLabels(labels),
 			len(metrics),
 		)
diff --git a/e2e/metrics/gitrepo_test.go b/e2e/metrics/gitrepo_test.go
index cd92079a..04675c6a 100644
--- a/e2e/metrics/gitrepo_test.go
+++ b/e2e/metrics/gitrepo_test.go
@@ -68,8 +68,11 @@ var _ = Describe("GitRepo Metrics", Label("gitrepo"), func() {
 		It("should have exactly one metric of each type for the gitrepo", func() {
 			et := metrics.NewExporterTest(metricsURL)
 			Eventually(func() error {
+				metrics, err := et.Get()
+				Expect(err).ToNot(HaveOccurred())
 				for _, metricName := range gitrepoMetricNames {
 					metric, err := et.FindOneMetric(
+						metrics,
 						metricName,
 						map[string]string{
 							"name":      objName,
@@ -104,8 +107,11 @@ var _ = Describe("GitRepo Metrics", Label("gitrepo"), func() {
 				var metric *metrics.Metric
 				// Expect still no metrics to be duplicated.
 				Eventually(func() error {
+					metrics, err := et.Get()
+					Expect(err).ToNot(HaveOccurred())
 					for _, metricName := range gitrepoMetricNames {
 						metric, err = et.FindOneMetric(
+							metrics,
 							metricName,
 							map[string]string{
 								"name":      objName,
@@ -130,8 +136,11 @@ var _ = Describe("GitRepo Metrics", Label("gitrepo"), func() {
 				Expect(err).ToNot(HaveOccurred(), out)
 
 				Eventually(func() error {
+					metrics, err := et.Get()
+					Expect(err).ToNot(HaveOccurred())
 					for _, metricName := range gitrepoMetricNames {
 						_, err := et.FindOneMetric(
+							metrics,
 							metricName,
 							map[string]string{
 								"name":      objName,
diff --git a/e2e/metrics/suite_test.go b/e2e/metrics/suite_test.go
index 30e65c45..3ba2be9f 100644
--- a/e2e/metrics/suite_test.go
+++ b/e2e/metrics/suite_test.go
@@ -28,33 +28,29 @@ var (
 )
 
 func setupLoadBalancer() {
-	if os.Getenv("METRICS_URL") != "" {
-		metricsURL = os.Getenv("METRICS_URL")
-	} else {
-		rs := rand.NewSource(time.Now().UnixNano())
-		port := rs.Int63()%1000 + 30000
-		loadBalancerName = testenv.AddRandomSuffix("fleetcontroller", rs)
+	rs := rand.NewSource(time.Now().UnixNano())
+	port := rs.Int63()%1000 + 30000
+	loadBalancerName = testenv.AddRandomSuffix("fleetcontroller", rs)
 
-		ks := k.Namespace("cattle-fleet-system")
-		err := testenv.ApplyTemplate(
-			ks,
-			testenv.AssetPath("metrics/fleetcontroller_service.yaml"),
-			map[string]interface{}{
-				"Name": loadBalancerName,
-				"Port": port,
-			},
-		)
-		Expect(err).ToNot(HaveOccurred())
+	ks := k.Namespace("cattle-fleet-system")
+	err := testenv.ApplyTemplate(
+		ks,
+		testenv.AssetPath("metrics/fleetcontroller_service.yaml"),
+		map[string]interface{}{
+			"Name": loadBalancerName,
+			"Port": port,
+		},
+	)
+	Expect(err).ToNot(HaveOccurred())
 
-		Eventually(func() (string, error) {
-			ip, err := ks.Get(
-				"service", loadBalancerName,
-				"-o", "jsonpath={.status.loadBalancer.ingress[0].ip}",
-			)
-			metricsURL = fmt.Sprintf("http://%s:%d/metrics", ip, port)
-			return ip, err
-		}).ShouldNot(BeEmpty())
-	}
+	Eventually(func() (string, error) {
+		ip, err := ks.Get(
+			"service", loadBalancerName,
+			"-o", "jsonpath={.status.loadBalancer.ingress[0].ip}",
+		)
+		metricsURL = fmt.Sprintf("http://%s:%d/metrics", ip, port)
+		return ip, err
+	}).ShouldNot(BeEmpty())
 }
 
 func tearDownLoadBalancer() {
@@ -68,11 +64,17 @@ var _ = BeforeSuite(func() {
 	SetDefaultEventuallyPollingInterval(time.Second)
 	testenv.SetRoot("../..")
 
-	setupLoadBalancer()
+	if os.Getenv("METRICS_URL") != "" {
+		metricsURL = os.Getenv("METRICS_URL")
+	} else {
+		setupLoadBalancer()
+	}
 
 	env = testenv.New()
 })
 
 var _ = AfterSuite(func() {
-	tearDownLoadBalancer()
+	if os.Getenv("METRICS_URL") == "" {
+		tearDownLoadBalancer()
+	}
 })
diff --git a/e2e/single-cluster/sharding_test.go b/e2e/single-cluster/sharding_test.go
index 456ca785..5b307425 100644
--- a/e2e/single-cluster/sharding_test.go
+++ b/e2e/single-cluster/sharding_test.go
@@ -3,29 +3,22 @@ package singlecluster_test
 import (
 	"fmt"
 	"math/rand"
-	"os"
-	"path"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	"github.com/onsi/gomega/matchers"
 	"github.com/rancher/fleet/e2e/testenv"
-	"github.com/rancher/fleet/e2e/testenv/githelper"
 	"github.com/rancher/fleet/e2e/testenv/kubectl"
 )
 
 var shards = []string{"shard1", "shard2", "shard3"}
 
-var _ = Describe("Filtering events by shard", Label("infra-setup"), Ordered, func() {
+var _ = Describe("Filtering events by shard", Label("sharding"), Ordered, func() {
 	var (
-		tmpDir           string
-		clonedir         string
-		k                kubectl.Command
-		gh               *githelper.Git
-		inClusterRepoURL string
-		gitrepoName      string
-		r                = rand.New(rand.NewSource(GinkgoRandomSeed()))
-		targetNamespace  string
+		k               kubectl.Command
+		gitrepoName     string
+		r               = rand.New(rand.NewSource(GinkgoRandomSeed()))
+		targetNamespace string
 	)
 
 	BeforeAll(func() {
@@ -45,22 +38,6 @@ var _ = Describe("Filtering events by shard", Label("infra-setup"), Ordered, fun
 			Expect(hasReconciledGitRepos).To(BeFalse())
 		}
 
-		// Build git repo URL reachable _within_ the cluster, for the GitRepo
-		host, err := githelper.BuildGitHostname(env.Namespace)
-		Expect(err).ToNot(HaveOccurred())
-
-		addr, err := githelper.GetExternalRepoAddr(env, port, repoName)
-		Expect(err).ToNot(HaveOccurred())
-		gh = githelper.NewHTTP(addr)
-
-		inClusterRepoURL = gh.GetInClusterURL(host, port, repoName)
-
-		tmpDir, _ = os.MkdirTemp("", "fleet-")
-		clonedir = path.Join(tmpDir, repoName)
-
-		_, err = gh.Create(clonedir, testenv.AssetPath("gitrepo/sleeper-chart"), "examples")
-		Expect(err).ToNot(HaveOccurred())
-
 		k = env.Kubectl.Namespace(env.Namespace)
 	})
 
@@ -82,8 +59,8 @@ var _ = Describe("Filtering events by shard", Label("infra-setup"), Ordered, fun
 						ShardID         string
 					}{
 						gitrepoName,
-						inClusterRepoURL,
-						gh.Branch,
+						"https://github.com/rancher/fleet-test-data",
+						"master",
 						"15s",           // default
 						targetNamespace, // to avoid conflicts with other tests
 						shard,
@@ -93,48 +70,50 @@ var _ = Describe("Filtering events by shard", Label("infra-setup"), Ordered, fun
 			})
 
 			It(fmt.Sprintf("deploys the gitrepo via the controller labeled with shard ID %s", shard), func() {
-				By("checking the pod exists")
+				By("checking the configmap exists")
 				Eventually(func() string {
-					out, _ := k.Namespace(targetNamespace).Get("pods")
+					out, _ := k.Namespace(targetNamespace).Get("configmaps")
 					return out
-				}).Should(ContainSubstring("sleeper-"))
+				}).Should(ContainSubstring("test-simple-chart-config"))
 
 				for _, s := range shards {
-					logs, err := k.Namespace("cattle-fleet-system").Logs(
-						"-l",
-						"app=fleet-controller",
-						"-l",
-						fmt.Sprintf("shard=%s", s),
-					)
-					Expect(err).ToNot(HaveOccurred())
-					regexMatcher := matchers.MatchRegexpMatcher{
-						Regexp: fmt.Sprintf(`Reconciling GitRepo.*"name":"%s"`, gitrepoName),
-					}
-					hasReconciledGitRepo, err := regexMatcher.Match(logs)
-					Expect(err).ToNot(HaveOccurred())
-					if s == shard {
-						Expect(hasReconciledGitRepo).To(BeTrueBecause(
-							"GitRepo %q labeled with shard %q should have been"+
-								" deployed by controller for shard %q in namespace %q",
-							gitrepoName,
-							shard,
-							shard,
-							env.Namespace,
-						))
-					} else {
-						Expect(hasReconciledGitRepo).To(BeFalseBecause(
-							"GitRepo %q labeled with shard %q should not have been"+
-								" deployed by controller for shard %q",
-							gitrepoName,
-							shard,
-							s,
-						))
-					}
+					Eventually(func(g Gomega) {
+						logs, err := k.Namespace("cattle-fleet-system").Logs(
+							"-l",
+							"app=fleet-controller",
+							"-l",
+							fmt.Sprintf("shard=%s", s),
+							"--tail=100",
+						)
+						g.Expect(err).ToNot(HaveOccurred())
+						regexMatcher := matchers.MatchRegexpMatcher{
+							Regexp: fmt.Sprintf(`Reconciling GitRepo.*"name":"%s"`, gitrepoName),
+						}
+						hasReconciledGitRepo, err := regexMatcher.Match(logs)
+						g.Expect(err).ToNot(HaveOccurred())
+						if s == shard {
+							g.Expect(hasReconciledGitRepo).To(BeTrueBecause(
+								"GitRepo %q labeled with shard %q should have been"+
+									" deployed by controller for shard %q in namespace %q",
+								gitrepoName,
+								shard,
+								shard,
+								env.Namespace,
+							))
+						} else {
+							g.Expect(hasReconciledGitRepo).To(BeFalseBecause(
+								"GitRepo %q labeled with shard %q should not have been"+
+									" deployed by controller for shard %q",
+								gitrepoName,
+								shard,
+								s,
+							))
+						}
+					}).Should(Succeed())
 				}
 			})
 
 			AfterEach(func() {
-				_ = os.RemoveAll(tmpDir)
 				_, _ = k.Delete("gitrepo", gitrepoName)
 			})
 		})
@@ -154,8 +133,8 @@ var _ = Describe("Filtering events by shard", Label("infra-setup"), Ordered, fun
 				ShardID         string
 			}{
 				gitrepoName,
-				inClusterRepoURL,
-				gh.Branch,
+				"https://github.com/rancher/fleet-test-data",
+				"master",
 				"15s",           // default
 				targetNamespace, // to avoid conflicts with other tests
 				"unknown",
@@ -164,11 +143,11 @@ var _ = Describe("Filtering events by shard", Label("infra-setup"), Ordered, fun
 		})
 
 		It("does not deploy the gitrepo", func() {
-			By("checking the pod does not exist")
+			By("checking the configmap does not exist")
 			Eventually(func() string {
-				out, _ := k.Namespace(targetNamespace).Get("pods")
+				out, _ := k.Namespace(targetNamespace).Get("configmaps")
 				return out
-			}).ShouldNot(ContainSubstring("sleeper-"))
+			}).ShouldNot(ContainSubstring("test-simple-chart-config"))
 
 			for _, s := range shards {
 				logs, err := k.Namespace("cattle-fleet-system").Logs(
@@ -176,6 +155,7 @@ var _ = Describe("Filtering events by shard", Label("infra-setup"), Ordered, fun
 					"app=fleet-controller",
 					"-l",
 					fmt.Sprintf("shard=%s", s),
+					"--tail=100",
 				)
 				Expect(err).ToNot(HaveOccurred())
 				regexMatcher := matchers.MatchRegexpMatcher{
@@ -197,7 +177,6 @@ var _ = Describe("Filtering events by shard", Label("infra-setup"), Ordered, fun
 		})
 
 		AfterEach(func() {
-			_ = os.RemoveAll(tmpDir)
 			_, _ = k.Delete("gitrepo", gitrepoName)
 		})
 	})
diff --git a/e2e/testenv/infra/go.mod b/e2e/testenv/infra/go.mod
deleted file mode 100644
index c10d9d5b..00000000
--- a/e2e/testenv/infra/go.mod
+++ /dev/null
@@ -1,65 +0,0 @@
-module github.com/rancher/fleet/e2e/testenv/infra
-
-go 1.22
-
-toolchain go1.22.2
-
-replace k8s.io/client-go => k8s.io/client-go v0.29.0
-
-require (
-	github.com/rancher/fleet v0.10.0-rc.11
-	github.com/spf13/cobra v1.8.0
-	golang.org/x/crypto v0.22.0
-	helm.sh/helm/v3 v3.14.4
-)
-
-require (
-	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
-	github.com/Masterminds/semver/v3 v3.2.1 // indirect
-	github.com/Microsoft/hcsshim v0.11.4 // indirect
-	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/cespare/xxhash/v2 v2.2.0 // indirect
-	github.com/containerd/containerd v1.7.12 // indirect
-	github.com/containerd/log v0.1.0 // indirect
-	github.com/distribution/reference v0.5.0 // indirect
-	github.com/docker/cli v25.0.1+incompatible // indirect
-	github.com/docker/distribution v2.8.3+incompatible // indirect
-	github.com/docker/docker v26.0.1+incompatible // indirect
-	github.com/docker/docker-credential-helpers v0.7.0 // indirect
-	github.com/docker/go-connections v0.5.0 // indirect
-	github.com/docker/go-metrics v0.0.1 // indirect
-	github.com/felixge/httpsnoop v1.0.4 // indirect
-	github.com/go-logr/logr v1.4.1 // indirect
-	github.com/go-logr/stdr v1.2.2 // indirect
-	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
-	github.com/golang/protobuf v1.5.3 // indirect
-	github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
-	github.com/gorilla/mux v1.8.1 // indirect
-	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/klauspost/compress v1.16.5 // indirect
-	github.com/moby/locker v1.0.1 // indirect
-	github.com/onsi/ginkgo/v2 v2.17.1 // indirect
-	github.com/opencontainers/go-digest v1.0.0 // indirect
-	github.com/opencontainers/image-spec v1.1.0 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
-	github.com/prometheus/client_golang v1.19.0 // indirect
-	github.com/prometheus/client_model v0.6.0 // indirect
-	github.com/prometheus/common v0.48.0 // indirect
-	github.com/prometheus/procfs v0.12.0 // indirect
-	github.com/sirupsen/logrus v1.9.3 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
-	go.opentelemetry.io/otel v1.24.0 // indirect
-	go.opentelemetry.io/otel/metric v1.24.0 // indirect
-	go.opentelemetry.io/otel/trace v1.24.0 // indirect
-	golang.org/x/net v0.22.0 // indirect
-	golang.org/x/sync v0.7.0 // indirect
-	golang.org/x/sys v0.19.0 // indirect
-	golang.org/x/tools v0.18.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
-	google.golang.org/grpc v1.58.3 // indirect
-	google.golang.org/protobuf v1.33.0 // indirect
-	k8s.io/client-go v12.0.0+incompatible // indirect
-	oras.land/oras-go v1.2.5 // indirect
-	sigs.k8s.io/yaml v1.4.0 // indirect
-)
diff --git a/e2e/testenv/infra/go.sum b/e2e/testenv/infra/go.sum
deleted file mode 100644
index 44fb376f..00000000
--- a/e2e/testenv/infra/go.sum
+++ /dev/null
@@ -1,250 +0,0 @@
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
-github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
-github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
-github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
-github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
-github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
-github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
-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/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
-github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
-github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
-github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
-github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
-github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
-github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
-github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
-github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
-github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0=
-github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk=
-github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
-github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
-github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
-github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
-github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc=
-github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI=
-github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
-github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbTO1lpcGSkU=
-github.com/docker/cli v25.0.1+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 v26.0.1+incompatible h1:t39Hm6lpXuXtgkF0dm1t9a5HkbUfdGy6XbWexmGr+hA=
-github.com/docker/docker v26.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
-github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
-github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
-github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
-github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
-github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
-github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
-github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
-github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
-github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
-github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
-github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=
-github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
-github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
-github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
-github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
-github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
-github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
-github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
-github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
-github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
-github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
-github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
-github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
-github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg=
-github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
-github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
-github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
-github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
-github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
-github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
-github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk=
-github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg=
-github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
-github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
-github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
-github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
-github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
-github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
-github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
-github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
-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.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
-github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
-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.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
-github.com/rancher/fleet v0.10.0-rc.11 h1:GKIb34Cf19ns4o4d86nSL/7R5CzOYjUayUugRykOV2E=
-github.com/rancher/fleet v0.10.0-rc.11/go.mod h1:kbqfnDrNNkOCgaF2G/RuoARG+FeN6PStHif/wU/4o+c=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
-github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
-github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
-github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
-github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
-github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=
-github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
-go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
-go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
-go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
-go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
-go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
-go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
-go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
-go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
-golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
-golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
-golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
-golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
-google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
-google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
-gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
-gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
-helm.sh/helm/v3 v3.14.4 h1:6FSpEfqyDalHq3kUr4gOMThhgY55kXUEjdQoyODYnrM=
-helm.sh/helm/v3 v3.14.4/go.mod h1:Tje7LL4gprZpuBNTbG34d1Xn5NmRT3OWfBRwpOSer9I=
-k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8=
-k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38=
-oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo=
-oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo=
-sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
-sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/integrationtests/cli/apply/apply_online_test.go b/integrationtests/cli/apply/apply_online_test.go
new file mode 100644
index 00000000..ce2fed3d
--- /dev/null
+++ b/integrationtests/cli/apply/apply_online_test.go
@@ -0,0 +1,105 @@
+//go:generate mockgen --build_flags=--mod=mod -destination=../../mocks/getter_mock.go -package=mocks github.com/rancher/fleet/internal/cmd/cli/apply Getter
+//go:generate mockgen --build_flags=--mod=mod -destination=../../mocks/fleet_controller_mock.go -package=mocks -mock_names=Interface=FleetInterface github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1 Interface
+
+package apply
+
+import (
+	"github.com/golang/mock/gomock"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/rancher/fleet/integrationtests/cli"
+	"github.com/rancher/fleet/integrationtests/mocks"
+	"github.com/rancher/fleet/internal/client"
+	"github.com/rancher/fleet/internal/cmd/cli/apply"
+	fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
+
+	"github.com/rancher/wrangler/v2/pkg/generic/fake"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var _ = Describe("Fleet apply online", Label("online"), func() {
+
+	var (
+		ctrl             *gomock.Controller
+		getter           *mocks.MockGetter
+		c                client.Client
+		fleetMock        *mocks.FleetInterface
+		bundleController *fake.MockControllerInterface[*fleet.Bundle, *fleet.BundleList]
+		name             string
+		dirs             []string
+		options          apply.Options
+		oldBundle        *fleet.Bundle
+		newBundle        *fleet.Bundle
+	)
+
+	JustBeforeEach(func() {
+		//Setting up all the needed mocked interfaces for the test
+		ctrl = gomock.NewController(GinkgoT())
+		getter = mocks.NewMockGetter(ctrl)
+		c = client.Client{
+			Fleet:     mocks.NewFleetInterface(ctrl),
+			Namespace: "foo",
+		}
+		bundleController = fake.NewMockControllerInterface[*fleet.Bundle, *fleet.BundleList](ctrl)
+		fleetMock = c.Fleet.(*mocks.FleetInterface)
+		getter.EXPECT().GetNamespace().Return("foo").AnyTimes()
+		getter.EXPECT().Get().Return(&c, nil).AnyTimes()
+		fleetMock.EXPECT().Bundle().Return(bundleController).AnyTimes()
+		bundleController.EXPECT().Get("foo", gomock.Any(), gomock.Any()).Return(oldBundle, nil).AnyTimes()
+		bundleController.EXPECT().List(gomock.Any(), gomock.Any()).Return(&fleet.BundleList{}, nil).AnyTimes()
+	})
+
+	When("We want to delete a label in the bundle from the cluster", func() {
+		BeforeEach(func() {
+			name = "labels_update"
+			dirs = []string{cli.AssetsPath + "labels_update"}
+			//bundle in the cluster
+			oldBundle = &fleet.Bundle{
+				ObjectMeta: metav1.ObjectMeta{
+					Labels: map[string]string{
+						"new":         "fleet-label2",
+						"new_changed": "fleet_label_changed",
+					},
+					Namespace: "foo",
+					Name:      "test_labels",
+				},
+			}
+			//bundle in the fleet.yaml file; some values are autofilled in the implementation (this is why there are not only the labels)
+			newBundle = &fleet.Bundle{
+				ObjectMeta: metav1.ObjectMeta{
+					Labels: map[string]string{
+						"new": "fleet-label2",
+					},
+					Namespace: "foo",
+					Name:      "test_labels",
+				},
+				Spec: fleet.BundleSpec{
+					Resources: []fleet.BundleResource{
+						{
+							Name:    "fleet.yaml",
+							Content: "labels:\n  new: fleet-label2",
+						},
+					},
+					Targets: []fleet.BundleTarget{
+						{
+							Name:         "default",
+							ClusterGroup: "default",
+						},
+					},
+				},
+			}
+		})
+
+		It("deletes labels present on the bundle but not in fleet.yaml", func() {
+			// Update is called with the actual output of the `apply` command here, hence we validate that its argument is what we expect.
+			bundleController.EXPECT().Update(newBundle).Return(newBundle, nil).AnyTimes()
+			err := fleetApplyOnline(getter, name, dirs, options)
+			Expect(err).NotTo(HaveOccurred())
+		})
+
+	})
+
+})
diff --git a/integrationtests/cli/apply/suite_test.go b/integrationtests/cli/apply/suite_test.go
index ea3ee080..f15e8d21 100644
--- a/integrationtests/cli/apply/suite_test.go
+++ b/integrationtests/cli/apply/suite_test.go
@@ -25,3 +25,8 @@ func fleetApply(name string, dirs []string, options apply.Options) error {
 	options.Output = buf
 	return apply.CreateBundles(context.Background(), client.NewGetter("", "", "fleet-local"), name, dirs, options)
 }
+
+// simulates fleet cli online execution, with mocked client
+func fleetApplyOnline(getter apply.Getter, name string, dirs []string, options apply.Options) error {
+	return apply.CreateBundles(context.Background(), getter, name, dirs, options)
+}
diff --git a/integrationtests/cli/assets/labels_update/fleet.yaml b/integrationtests/cli/assets/labels_update/fleet.yaml
new file mode 100644
index 00000000..d6a56c39
--- /dev/null
+++ b/integrationtests/cli/assets/labels_update/fleet.yaml
@@ -0,0 +1,2 @@
+labels:
+  new: fleet-label2
\ No newline at end of file
diff --git a/integrationtests/gitcloner/clone_test.go b/integrationtests/gitcloner/clone_test.go
index a3ee4fcc..592d5f48 100644
--- a/integrationtests/gitcloner/clone_test.go
+++ b/integrationtests/gitcloner/clone_test.go
@@ -14,8 +14,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/rancher/fleet/cmd/gitcloner/cmd"
-	"github.com/rancher/fleet/cmd/gitcloner/gogit"
+	"github.com/rancher/fleet/internal/cmd/cli/gitcloner"
 
 	dockercontainer "github.com/docker/docker/api/types/container"
 	dockermount "github.com/docker/docker/api/types/mount"
@@ -24,11 +23,12 @@ import (
 	"github.com/go-git/go-git/v5/plumbing/object"
 	httpgit "github.com/go-git/go-git/v5/plumbing/transport/http"
 	"github.com/gogits/go-gogs-client"
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
 	cp "github.com/otiai10/copy"
 	"github.com/testcontainers/testcontainers-go"
 	"golang.org/x/crypto/ssh"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
 )
 
 /*
@@ -53,7 +53,7 @@ var (
 var _ = Describe("Applying a git job gets content from git repo", Ordered, func() {
 
 	var (
-		opts          *cmd.Options
+		opts          *gitcloner.GitCloner
 		private       bool
 		cloneErr      error
 		tmp           string
@@ -87,14 +87,14 @@ var _ = Describe("Applying a git job gets content from git repo", Ordered, func(
 		When("cloning a public repo that contains a README.md file providing a branch", func() {
 			BeforeEach(func() {
 				private = false
-				opts = &cmd.Options{
+				opts = &gitcloner.GitCloner{
 					InsecureSkipTLS: true,
 					Branch:          "master",
 				}
 			})
 
 			JustBeforeEach(func() {
-				c := gogit.NewCloner()
+				c := gitcloner.New()
 				cloneErr = c.CloneRepo(opts)
 				Expect(cloneErr).NotTo(HaveOccurred())
 			})
@@ -108,13 +108,13 @@ var _ = Describe("Applying a git job gets content from git repo", Ordered, func(
 		When("cloning a public repo that contains a README.md file providing a revision", func() {
 			BeforeEach(func() {
 				private = false
-				opts = &cmd.Options{
+				opts = &gitcloner.GitCloner{
 					InsecureSkipTLS: true,
 				}
 			})
 
 			JustBeforeEach(func() {
-				c := gogit.NewCloner()
+				c := gitcloner.New()
 				opts.Revision = initialCommit
 				cloneErr = c.CloneRepo(opts)
 				Expect(cloneErr).NotTo(HaveOccurred())
@@ -130,13 +130,13 @@ var _ = Describe("Applying a git job gets content from git repo", Ordered, func(
 			When("No authentication is provided", func() {
 				BeforeEach(func() {
 					private = true
-					opts = &cmd.Options{
+					opts = &gitcloner.GitCloner{
 						InsecureSkipTLS: true,
 					}
 				})
 
 				JustBeforeEach(func() {
-					c := gogit.NewCloner()
+					c := gitcloner.New()
 					cloneErr = c.CloneRepo(opts)
 				})
 
@@ -148,7 +148,7 @@ var _ = Describe("Applying a git job gets content from git repo", Ordered, func(
 			When("Basic authentication is provided", func() {
 				BeforeEach(func() {
 					private = true
-					opts = &cmd.Options{
+					opts = &gitcloner.GitCloner{
 						InsecureSkipTLS: true,
 						Username:        gogsUser,
 						PasswordFile:    "assets/gogs/password",
@@ -156,7 +156,7 @@ var _ = Describe("Applying a git job gets content from git repo", Ordered, func(
 				})
 
 				JustBeforeEach(func() {
-					c := gogit.NewCloner()
+					c := gitcloner.New()
 					cloneErr = c.CloneRepo(opts)
 					Expect(cloneErr).NotTo(HaveOccurred())
 				})
@@ -171,11 +171,11 @@ var _ = Describe("Applying a git job gets content from git repo", Ordered, func(
 		When("Insecure skip tls is not true, and no caBundle is provided", func() {
 			BeforeEach(func() {
 				private = false
-				opts = &cmd.Options{}
+				opts = &gitcloner.GitCloner{}
 			})
 
 			JustBeforeEach(func() {
-				c := gogit.NewCloner()
+				c := gitcloner.New()
 				cloneErr = c.CloneRepo(opts)
 			})
 
@@ -189,7 +189,7 @@ var _ = Describe("Applying a git job gets content from git repo", Ordered, func(
 
 			BeforeEach(func() {
 				private = false
-				opts = &cmd.Options{}
+				opts = &gitcloner.GitCloner{}
 			})
 
 			AfterEach(func() {
@@ -205,7 +205,7 @@ var _ = Describe("Applying a git job gets content from git repo", Ordered, func(
 				Expect(err).NotTo(HaveOccurred())
 				opts.CABundleFile = caBundleFile.Name()
 
-				c := gogit.NewCloner()
+				c := gitcloner.New()
 				cloneErr = c.CloneRepo(opts)
 			})
 
@@ -249,7 +249,7 @@ var _ = Describe("Applying a git job gets content from git repo", Ordered, func(
 		When("the cloned repo is private and contains a README.md file", func() {
 			BeforeEach(func() {
 				private = true
-				opts = &cmd.Options{
+				opts = &gitcloner.GitCloner{
 					InsecureSkipTLS: true,
 				}
 			})
@@ -260,7 +260,7 @@ var _ = Describe("Applying a git job gets content from git repo", Ordered, func(
 			})
 
 			JustBeforeEach(func() {
-				c := gogit.NewCloner()
+				c := gitcloner.New()
 				Eventually(func() error {
 					return c.CloneRepo(opts)
 				}).ShouldNot(HaveOccurred())
diff --git a/integrationtests/gitjob/controller/suite_test.go b/integrationtests/gitjob/controller/suite_test.go
index d7ece82a..a335e236 100644
--- a/integrationtests/gitjob/controller/suite_test.go
+++ b/integrationtests/gitjob/controller/suite_test.go
@@ -11,7 +11,7 @@ import (
 
 	"go.uber.org/mock/gomock"
 
-	"github.com/rancher/fleet/internal/cmd/gitjob"
+	"github.com/rancher/fleet/internal/cmd/controller/gitops/reconciler"
 	"github.com/rancher/fleet/internal/mocks"
 	v1alpha1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
 
@@ -71,7 +71,7 @@ var _ = BeforeSuite(func() {
 	gitPollerMock.EXPECT().AddOrModifyGitRepoPollJob(gomock.Any(), gomock.Any()).AnyTimes()
 	gitPollerMock.EXPECT().CleanUpGitRepoPollJobs(gomock.Any()).AnyTimes()
 
-	err = (&gitjob.GitJobReconciler{
+	err = (&reconciler.GitJobReconciler{
 		Client:    mgr.GetClient(),
 		Scheme:    mgr.GetScheme(),
 		Image:     "image",
diff --git a/integrationtests/mocks/fleet_controller_mock.go b/integrationtests/mocks/fleet_controller_mock.go
new file mode 100644
index 00000000..a3371fa1
--- /dev/null
+++ b/integrationtests/mocks/fleet_controller_mock.go
@@ -0,0 +1,189 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1 (interfaces: Interface)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	v1alpha1 "github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1"
+)
+
+// FleetInterface is a mock of Interface interface.
+type FleetInterface struct {
+	ctrl     *gomock.Controller
+	recorder *FleetInterfaceMockRecorder
+}
+
+// FleetInterfaceMockRecorder is the mock recorder for FleetInterface.
+type FleetInterfaceMockRecorder struct {
+	mock *FleetInterface
+}
+
+// NewFleetInterface creates a new mock instance.
+func NewFleetInterface(ctrl *gomock.Controller) *FleetInterface {
+	mock := &FleetInterface{ctrl: ctrl}
+	mock.recorder = &FleetInterfaceMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *FleetInterface) EXPECT() *FleetInterfaceMockRecorder {
+	return m.recorder
+}
+
+// Bundle mocks base method.
+func (m *FleetInterface) Bundle() v1alpha1.BundleController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Bundle")
+	ret0, _ := ret[0].(v1alpha1.BundleController)
+	return ret0
+}
+
+// Bundle indicates an expected call of Bundle.
+func (mr *FleetInterfaceMockRecorder) Bundle() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bundle", reflect.TypeOf((*FleetInterface)(nil).Bundle))
+}
+
+// BundleDeployment mocks base method.
+func (m *FleetInterface) BundleDeployment() v1alpha1.BundleDeploymentController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BundleDeployment")
+	ret0, _ := ret[0].(v1alpha1.BundleDeploymentController)
+	return ret0
+}
+
+// BundleDeployment indicates an expected call of BundleDeployment.
+func (mr *FleetInterfaceMockRecorder) BundleDeployment() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BundleDeployment", reflect.TypeOf((*FleetInterface)(nil).BundleDeployment))
+}
+
+// BundleNamespaceMapping mocks base method.
+func (m *FleetInterface) BundleNamespaceMapping() v1alpha1.BundleNamespaceMappingController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "BundleNamespaceMapping")
+	ret0, _ := ret[0].(v1alpha1.BundleNamespaceMappingController)
+	return ret0
+}
+
+// BundleNamespaceMapping indicates an expected call of BundleNamespaceMapping.
+func (mr *FleetInterfaceMockRecorder) BundleNamespaceMapping() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BundleNamespaceMapping", reflect.TypeOf((*FleetInterface)(nil).BundleNamespaceMapping))
+}
+
+// Cluster mocks base method.
+func (m *FleetInterface) Cluster() v1alpha1.ClusterController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Cluster")
+	ret0, _ := ret[0].(v1alpha1.ClusterController)
+	return ret0
+}
+
+// Cluster indicates an expected call of Cluster.
+func (mr *FleetInterfaceMockRecorder) Cluster() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cluster", reflect.TypeOf((*FleetInterface)(nil).Cluster))
+}
+
+// ClusterGroup mocks base method.
+func (m *FleetInterface) ClusterGroup() v1alpha1.ClusterGroupController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ClusterGroup")
+	ret0, _ := ret[0].(v1alpha1.ClusterGroupController)
+	return ret0
+}
+
+// ClusterGroup indicates an expected call of ClusterGroup.
+func (mr *FleetInterfaceMockRecorder) ClusterGroup() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterGroup", reflect.TypeOf((*FleetInterface)(nil).ClusterGroup))
+}
+
+// ClusterRegistration mocks base method.
+func (m *FleetInterface) ClusterRegistration() v1alpha1.ClusterRegistrationController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ClusterRegistration")
+	ret0, _ := ret[0].(v1alpha1.ClusterRegistrationController)
+	return ret0
+}
+
+// ClusterRegistration indicates an expected call of ClusterRegistration.
+func (mr *FleetInterfaceMockRecorder) ClusterRegistration() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterRegistration", reflect.TypeOf((*FleetInterface)(nil).ClusterRegistration))
+}
+
+// ClusterRegistrationToken mocks base method.
+func (m *FleetInterface) ClusterRegistrationToken() v1alpha1.ClusterRegistrationTokenController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ClusterRegistrationToken")
+	ret0, _ := ret[0].(v1alpha1.ClusterRegistrationTokenController)
+	return ret0
+}
+
+// ClusterRegistrationToken indicates an expected call of ClusterRegistrationToken.
+func (mr *FleetInterfaceMockRecorder) ClusterRegistrationToken() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterRegistrationToken", reflect.TypeOf((*FleetInterface)(nil).ClusterRegistrationToken))
+}
+
+// Content mocks base method.
+func (m *FleetInterface) Content() v1alpha1.ContentController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Content")
+	ret0, _ := ret[0].(v1alpha1.ContentController)
+	return ret0
+}
+
+// Content indicates an expected call of Content.
+func (mr *FleetInterfaceMockRecorder) Content() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Content", reflect.TypeOf((*FleetInterface)(nil).Content))
+}
+
+// GitRepo mocks base method.
+func (m *FleetInterface) GitRepo() v1alpha1.GitRepoController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GitRepo")
+	ret0, _ := ret[0].(v1alpha1.GitRepoController)
+	return ret0
+}
+
+// GitRepo indicates an expected call of GitRepo.
+func (mr *FleetInterfaceMockRecorder) GitRepo() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitRepo", reflect.TypeOf((*FleetInterface)(nil).GitRepo))
+}
+
+// GitRepoRestriction mocks base method.
+func (m *FleetInterface) GitRepoRestriction() v1alpha1.GitRepoRestrictionController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GitRepoRestriction")
+	ret0, _ := ret[0].(v1alpha1.GitRepoRestrictionController)
+	return ret0
+}
+
+// GitRepoRestriction indicates an expected call of GitRepoRestriction.
+func (mr *FleetInterfaceMockRecorder) GitRepoRestriction() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GitRepoRestriction", reflect.TypeOf((*FleetInterface)(nil).GitRepoRestriction))
+}
+
+// ImageScan mocks base method.
+func (m *FleetInterface) ImageScan() v1alpha1.ImageScanController {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ImageScan")
+	ret0, _ := ret[0].(v1alpha1.ImageScanController)
+	return ret0
+}
+
+// ImageScan indicates an expected call of ImageScan.
+func (mr *FleetInterfaceMockRecorder) ImageScan() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImageScan", reflect.TypeOf((*FleetInterface)(nil).ImageScan))
+}
diff --git a/integrationtests/mocks/getter_mock.go b/integrationtests/mocks/getter_mock.go
new file mode 100644
index 00000000..add20daa
--- /dev/null
+++ b/integrationtests/mocks/getter_mock.go
@@ -0,0 +1,64 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/rancher/fleet/internal/cmd/cli/apply (interfaces: Getter)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+	client "github.com/rancher/fleet/internal/client"
+)
+
+// MockGetter is a mock of Getter interface.
+type MockGetter struct {
+	ctrl     *gomock.Controller
+	recorder *MockGetterMockRecorder
+}
+
+// MockGetterMockRecorder is the mock recorder for MockGetter.
+type MockGetterMockRecorder struct {
+	mock *MockGetter
+}
+
+// NewMockGetter creates a new mock instance.
+func NewMockGetter(ctrl *gomock.Controller) *MockGetter {
+	mock := &MockGetter{ctrl: ctrl}
+	mock.recorder = &MockGetterMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockGetter) EXPECT() *MockGetterMockRecorder {
+	return m.recorder
+}
+
+// Get mocks base method.
+func (m *MockGetter) Get() (*client.Client, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Get")
+	ret0, _ := ret[0].(*client.Client)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// Get indicates an expected call of Get.
+func (mr *MockGetterMockRecorder) Get() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGetter)(nil).Get))
+}
+
+// GetNamespace mocks base method.
+func (m *MockGetter) GetNamespace() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetNamespace")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// GetNamespace indicates an expected call of GetNamespace.
+func (mr *MockGetterMockRecorder) GetNamespace() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamespace", reflect.TypeOf((*MockGetter)(nil).GetNamespace))
+}
diff --git a/cmd/gitcloner/gogit/cloner_test.go b/internal/cmd/cli/gitcloner/cloner_test.go
similarity index 95%
rename from cmd/gitcloner/gogit/cloner_test.go
rename to internal/cmd/cli/gitcloner/cloner_test.go
index 68e9430d..8ca0e1ae 100644
--- a/cmd/gitcloner/gogit/cloner_test.go
+++ b/internal/cmd/cli/gitcloner/cloner_test.go
@@ -1,18 +1,16 @@
-package gogit
+package gitcloner
 
 import (
 	"errors"
 	"os"
 	"testing"
 
+	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
 	"github.com/go-git/go-git/v5/plumbing/transport"
-
-	"github.com/go-git/go-git/v5"
 	httpgit "github.com/go-git/go-git/v5/plumbing/transport/http"
 	gossh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 	"github.com/google/go-cmp/cmp"
-	"github.com/rancher/fleet/cmd/gitcloner/cmd"
 )
 
 func TestCloneRepo(t *testing.T) {
@@ -74,12 +72,12 @@ udiSlDctMM/X3ZM2JN5M1rtAJ2WR3ZQtmWbOjZAbG2Eq
 	}()
 
 	tests := map[string]struct {
-		opts              *cmd.Options
+		opts              *GitCloner
 		expectedCloneOpts *git.CloneOptions
 		expectedErr       error
 	}{
 		"branch no auth": {
-			opts: &cmd.Options{
+			opts: &GitCloner{
 				Repo:   "repo",
 				Path:   "path",
 				Branch: "master",
@@ -91,7 +89,7 @@ udiSlDctMM/X3ZM2JN5M1rtAJ2WR3ZQtmWbOjZAbG2Eq
 			},
 		},
 		"branch basic auth": {
-			opts: &cmd.Options{
+			opts: &GitCloner{
 				Repo:         "repo",
 				Path:         "path",
 				Branch:       "master",
@@ -109,7 +107,7 @@ udiSlDctMM/X3ZM2JN5M1rtAJ2WR3ZQtmWbOjZAbG2Eq
 			},
 		},
 		"branch ssh auth": {
-			opts: &cmd.Options{
+			opts: &GitCloner{
 				Repo:              "ssh://git@localhost/test/test-repo",
 				Path:              "path",
 				Branch:            "master",
@@ -123,7 +121,7 @@ udiSlDctMM/X3ZM2JN5M1rtAJ2WR3ZQtmWbOjZAbG2Eq
 			},
 		},
 		"password file does not exist": {
-			opts: &cmd.Options{
+			opts: &GitCloner{
 				Repo:         "repo",
 				Branch:       "master",
 				PasswordFile: "doesntexist",
@@ -133,7 +131,7 @@ udiSlDctMM/X3ZM2JN5M1rtAJ2WR3ZQtmWbOjZAbG2Eq
 			expectedErr:       errors.New("file not found"),
 		},
 		"ca file does not exist": {
-			opts: &cmd.Options{
+			opts: &GitCloner{
 				Repo:         "repo",
 				Branch:       "master",
 				CABundleFile: "doesntexist",
@@ -142,7 +140,7 @@ udiSlDctMM/X3ZM2JN5M1rtAJ2WR3ZQtmWbOjZAbG2Eq
 			expectedErr:       errors.New("file not found"),
 		},
 		"ssh private key file does not exist": {
-			opts: &cmd.Options{
+			opts: &GitCloner{
 				Repo:              "repo",
 				Branch:            "master",
 				SSHPrivateKeyFile: "doesntexist",
diff --git a/cmd/gitcloner/cmd/cmd_test.go b/internal/cmd/cli/gitcloner/cmd_test.go
similarity index 92%
rename from cmd/gitcloner/cmd/cmd_test.go
rename to internal/cmd/cli/gitcloner/cmd_test.go
index 89e3b79d..8d8c7480 100644
--- a/cmd/gitcloner/cmd/cmd_test.go
+++ b/internal/cmd/cli/gitcloner/cmd_test.go
@@ -1,4 +1,4 @@
-package cmd
+package gitcloner
 
 import (
 	"testing"
@@ -6,7 +6,7 @@ import (
 
 func TestArgsAreSet(t *testing.T) {
 	mock := &clonerMock{}
-	cmd := New(mock)
+	cmd := NewCmd(mock)
 	cmd.SetArgs([]string{"test-repo", "test-path", "--branch", "master", "--revision", "v0.1.0", "--ca-bundle-file", "caFile", "--username", "user",
 		"--password-file", "passwordFile", "--ssh-private-key-file", "sshFile", "--insecure-skip-tls", "--known-hosts-file", "knownFile"})
 	err := cmd.Execute()
@@ -43,10 +43,10 @@ func TestArgsAreSet(t *testing.T) {
 }
 
 type clonerMock struct {
-	opts *Options
+	opts *GitCloner
 }
 
-func (m *clonerMock) CloneRepo(opts *Options) error {
+func (m *clonerMock) CloneRepo(opts *GitCloner) error {
 	m.opts = opts
 
 	return nil
diff --git a/internal/cmd/gitjob/gitjob_test.go b/internal/cmd/controller/gitops/reconciler/gitjob_test.go
similarity index 94%
rename from internal/cmd/gitjob/gitjob_test.go
rename to internal/cmd/controller/gitops/reconciler/gitjob_test.go
index cfd0f2ca..1dc3941d 100644
--- a/internal/cmd/gitjob/gitjob_test.go
+++ b/internal/cmd/controller/gitops/reconciler/gitjob_test.go
@@ -1,7 +1,7 @@
-//go:generate mockgen --build_flags=--mod=mod -destination=../../mocks/poller_mock.go -package=mocks github.com/rancher/fleet/internal/cmd/gitjob GitPoller
-//go:generate mockgen --build_flags=--mod=mod -destination=../../mocks/client_mock.go -package=mocks sigs.k8s.io/controller-runtime/pkg/client Client,SubResourceWriter
+//go:generate mockgen --build_flags=--mod=mod -destination=../../../../mocks/poller_mock.go -package=mocks github.com/rancher/fleet/internal/cmd/controller/gitops/reconciler GitPoller
+//go:generate mockgen --build_flags=--mod=mod -destination=../../../../mocks/client_mock.go -package=mocks sigs.k8s.io/controller-runtime/pkg/client Client,SubResourceWriter
 
-package gitjob
+package reconciler
 
 import (
 	"context"
@@ -39,15 +39,14 @@ func TestReconcile_AddOrModifyGitRepoPollJobIsCalled_WhenGitRepoIsCreatedOrModif
 		},
 	}
 	namespacedName := types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}
-	ctx := context.TODO()
 	client := mocks.NewMockClient(mockCtrl)
 	statusClient := mocks.NewMockSubResourceWriter(mockCtrl)
-	statusClient.EXPECT().Update(ctx, gomock.Any())
-	client.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
+	statusClient.EXPECT().Update(gomock.Any(), gomock.Any())
+	client.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil)
 	client.EXPECT().Status().Return(statusClient)
 	poller := mocks.NewMockGitPoller(mockCtrl)
-	poller.EXPECT().AddOrModifyGitRepoPollJob(ctx, gomock.Any()).Times(1)
-	poller.EXPECT().CleanUpGitRepoPollJobs(ctx).Times(0)
+	poller.EXPECT().AddOrModifyGitRepoPollJob(gomock.Any(), gomock.Any()).Times(1)
+	poller.EXPECT().CleanUpGitRepoPollJobs(gomock.Any()).Times(0)
 
 	r := GitJobReconciler{
 		Client:    client,
@@ -55,6 +54,8 @@ func TestReconcile_AddOrModifyGitRepoPollJobIsCalled_WhenGitRepoIsCreatedOrModif
 		Image:     "",
 		GitPoller: poller,
 	}
+
+	ctx := context.TODO()
 	_, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: namespacedName})
 	if err != nil {
 		t.Errorf("unexpected error %v", err)
@@ -120,9 +121,9 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 			expectedInitContainers: []corev1.Container{
 				{
 					Command: []string{
-						"gitcloner",
+						"fleet",
 					},
-					Args:  []string{"repo", "/workspace", "--branch", "master"},
+					Args:  []string{"gitcloner", "repo", "/workspace", "--branch", "master"},
 					Image: "test",
 					Name:  "gitcloner-initializer",
 					VolumeMounts: []corev1.VolumeMount{
@@ -164,9 +165,9 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 			expectedInitContainers: []corev1.Container{
 				{
 					Command: []string{
-						"gitcloner",
+						"fleet",
 					},
-					Args:  []string{"repo", "/workspace", "--branch", "foo"},
+					Args:  []string{"gitcloner", "repo", "/workspace", "--branch", "foo"},
 					Image: "test",
 					Name:  "gitcloner-initializer",
 					VolumeMounts: []corev1.VolumeMount{
@@ -208,9 +209,9 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 			expectedInitContainers: []corev1.Container{
 				{
 					Command: []string{
-						"gitcloner",
+						"fleet",
 					},
-					Args:  []string{"repo", "/workspace", "--revision", "foo"},
+					Args:  []string{"gitcloner", "repo", "/workspace", "--revision", "foo"},
 					Image: "test",
 					Name:  "gitcloner-initializer",
 					VolumeMounts: []corev1.VolumeMount{
@@ -252,9 +253,10 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 			expectedInitContainers: []corev1.Container{
 				{
 					Command: []string{
-						"gitcloner",
+						"fleet",
 					},
 					Args: []string{
+						"gitcloner",
 						"repo",
 						"/workspace",
 						"--branch",
@@ -317,9 +319,10 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 			expectedInitContainers: []corev1.Container{
 				{
 					Command: []string{
-						"gitcloner",
+						"fleet",
 					},
 					Args: []string{
+						"gitcloner",
 						"repo",
 						"/workspace",
 						"--branch",
@@ -380,9 +383,10 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 			expectedInitContainers: []corev1.Container{
 				{
 					Command: []string{
-						"gitcloner",
+						"fleet",
 					},
 					Args: []string{
+						"gitcloner",
 						"repo",
 						"/workspace",
 						"--branch",
@@ -442,9 +446,10 @@ func TestNewJob(t *testing.T) { // nolint:funlen
 			expectedInitContainers: []corev1.Container{
 				{
 					Command: []string{
-						"gitcloner",
+						"fleet",
 					},
 					Args: []string{
+						"gitcloner",
 						"repo",
 						"/workspace",
 						"--branch",
diff --git a/internal/manifest/store_test.go b/internal/manifest/store_test.go
index d4024b68..e012bf54 100644
--- a/internal/manifest/store_test.go
+++ b/internal/manifest/store_test.go
@@ -1,7 +1,5 @@
 package manifest
 
-//go:generate mockgen --build_flags=--mod=mod -destination=../cmd/controller/mocks/client_mock.go -package=mocks sigs.k8s.io/controller-runtime/pkg/client Client
-
 import (
 	"bytes"
 	"context"