diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml
index 3dadbc35a6889cb689466d9331ff390c110db368..be98815688d224631cfbb42d88ee1e62c5387b27 100644
--- a/.github/workflows/e2e.yaml
+++ b/.github/workflows/e2e.yaml
@@ -29,14 +29,20 @@ jobs:
           version: v0.11.1
           image: kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6
           config: .github/kind/config.yaml # disable KIND-net
+      - name: Setup envtest
+        uses: fluxcd/pkg/actions/envtest@main
+        with:
+          version: "1.21.x"
       - name: Setup Calico for network policy
         run: |
           kubectl apply -f https://docs.projectcalico.org/v3.16/manifests/calico.yaml
           kubectl -n kube-system set env daemonset/calico-node FELIX_IGNORELOOSERPF=true
       - name: Setup Kustomize
         uses: fluxcd/pkg//actions/kustomize@main
-      - name: Run test
+      - name: Run tests
         run: make test
+      - name: Run e2e tests
+        run: TEST_KUBECONFIG=$HOME/.kube/config make e2e
       - name: Check if working tree is dirty
         run: |
           if [[ $(git diff --stat) != '' ]]; then
diff --git a/Makefile b/Makefile
index 7a674fff086c9c4148955ebc75dab71b7a36e6a6..fa499d5d84e949331ea0181aa7b27a7862910692 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,6 @@
 VERSION?=$(shell grep 'VERSION' cmd/flux/main.go | awk '{ print $$4 }' | tr -d '"')
 EMBEDDED_MANIFESTS_TARGET=cmd/flux/manifests
+TEST_KUBECONFIG?=/tmp/flux-e2e-test-kubeconfig
 
 rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2)) $(filter $(subst *,%,$(2)),$(d)))
 
@@ -14,9 +15,41 @@ fmt:
 vet:
 	go vet ./...
 
+setup-envtest:
+ifeq (, $(shell which setup-envtest))
+	@{ \
+	set -e ;\
+	SETUP_ENVTEST_TMP_DIR=$$(mktemp -d) ;\
+	cd $$SETUP_ENVTEST_TMP_DIR ;\
+	go mod init tmp ;\
+	go get sigs.k8s.io/controller-runtime/tools/setup-envtest@latest ;\
+	rm -rf $$SETUP_ENVTEST_TMP_DIR ;\
+	}
+SETUP_ENVTEST=$(GOBIN)/setup-envtest
+else
+SETUP_ENVTEST=$(shell which setup-envtest)
+endif
+
+setup-kind:
+	kind create cluster --name=flux-e2e-test --kubeconfig=$(TEST_KUBECONFIG) --config=.github/kind/config.yaml
+	kubectl --kubeconfig=$(TEST_KUBECONFIG) apply -f https://docs.projectcalico.org/v3.16/manifests/calico.yaml
+	kubectl --kubeconfig=$(TEST_KUBECONFIG) -n kube-system set env daemonset/calico-node FELIX_IGNORELOOSERPF=true
+
+cleanup-kind:
+	kind delete cluster --name=flux-e2e-test
+	rm $(TEST_KUBECONFIG)
+
 test: $(EMBEDDED_MANIFESTS_TARGET) tidy fmt vet
 	go test ./... -coverprofile cover.out
 
+e2e: $(EMBEDDED_MANIFESTS_TARGET) tidy fmt vet
+	TEST_KUBECONFIG=$(TEST_KUBECONFIG) go test ./cmd/flux/... -coverprofile cover.out --tags=e2e -parallel=1
+
+test-with-kind: setup-envtest
+	make setup-kind
+	make e2e
+	make cleanup-kind
+
 $(EMBEDDED_MANIFESTS_TARGET): $(call rwildcard,manifests/,*.yaml *.json)
 	./manifests/scripts/bundle.sh
 
diff --git a/cmd/flux/install_test.go b/cmd/flux/install_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..35d532f30e4ceb1a6d2bee598d91deb2eeab65cb
--- /dev/null
+++ b/cmd/flux/install_test.go
@@ -0,0 +1,54 @@
+// +build e2e
+
+package main
+
+import (
+	"testing"
+	"time"
+)
+
+func TestInstallNoArgs(t *testing.T) {
+	cmd := cmdTestCase{
+		args:            "install",
+		wantError:       false,
+		testClusterMode: ExistingClusterMode,
+		goldenFile:      "testdata/install/install_no_args.golden",
+	}
+	cmd.runTestCmd(t)
+
+	testUninstallSilent(t)
+	time.Sleep(30 * time.Second)
+}
+
+func TestInstallExtraComponents(t *testing.T) {
+	cmd := cmdTestCase{
+		args:            "install --components-extra=image-reflector-controller,image-automation-controller",
+		wantError:       false,
+		testClusterMode: ExistingClusterMode,
+		goldenFile:      "testdata/install/install_extra_components.golden",
+	}
+	cmd.runTestCmd(t)
+
+	testUninstallSilentForExtraComponents(t)
+	time.Sleep(30 * time.Second)
+}
+
+func testUninstallSilent(t *testing.T) {
+	cmd := cmdTestCase{
+		args:            "uninstall -s",
+		wantError:       false,
+		testClusterMode: ExistingClusterMode,
+		goldenFile:      "testdata/uninstall/uninstall.golden",
+	}
+	cmd.runTestCmd(t)
+}
+
+func testUninstallSilentForExtraComponents(t *testing.T) {
+	cmd := cmdTestCase{
+		args:            "uninstall -s",
+		wantError:       false,
+		testClusterMode: ExistingClusterMode,
+		goldenFile:      "testdata/uninstall/uninstall_extra_components.golden",
+	}
+	cmd.runTestCmd(t)
+}
diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go
index 832fb84b95e1173d769d3ae156ba9957d8bf2603..3a4b976263502af3aab38d3b231fb575c5843118 100644
--- a/cmd/flux/main_test.go
+++ b/cmd/flux/main_test.go
@@ -4,17 +4,23 @@ import (
 	"bufio"
 	"bytes"
 	"context"
+	"fmt"
 	"io"
+	"io/ioutil"
 	"os"
+	"path/filepath"
 	"testing"
+	"time"
 
 	"github.com/fluxcd/flux2/internal/utils"
 	"github.com/google/go-cmp/cmp"
 	"github.com/mattn/go-shellwords"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	k8syaml "k8s.io/apimachinery/pkg/util/yaml"
+	"k8s.io/client-go/tools/clientcmd"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/envtest"
 )
 
 func TestMain(m *testing.M) {
@@ -49,17 +55,18 @@ func readYamlObjects(objectFile string) ([]client.Object, error) {
 }
 
 // A KubeManager that can create objects that are subject to a test.
-type fakeKubeManager struct {
-	fakeClient client.WithWatch
+type testEnvKubeManager struct {
+	client  client.WithWatch
+	testEnv *envtest.Environment
 }
 
-func (m *fakeKubeManager) NewClient(kubeconfig string, kubecontext string) (client.WithWatch, error) {
-	return m.fakeClient, nil
+func (m *testEnvKubeManager) NewClient(kubeconfig string, kubecontext string) (client.WithWatch, error) {
+	return m.client, nil
 }
 
-func (m *fakeKubeManager) CreateObjects(clientObjects []client.Object) error {
+func (m *testEnvKubeManager) CreateObjects(clientObjects []client.Object) error {
 	for _, obj := range clientObjects {
-		err := m.fakeClient.Create(context.Background(), obj)
+		err := m.client.Create(context.Background(), obj)
 		if err != nil {
 			return err
 		}
@@ -67,32 +74,92 @@ func (m *fakeKubeManager) CreateObjects(clientObjects []client.Object) error {
 	return nil
 }
 
-func NewFakeKubeManager() *fakeKubeManager {
-	c := fakeclient.NewClientBuilder().WithScheme(utils.NewScheme()).Build()
-	return &fakeKubeManager{
-		fakeClient: c,
+func (m *testEnvKubeManager) Stop() error {
+	if m.testEnv == nil {
+		return fmt.Errorf("do nothing because testEnv is nil")
 	}
+	return m.testEnv.Stop()
 }
 
-// Run the command and return the captured output.
-func executeCommand(cmd string) (string, error) {
-	args, err := shellwords.Parse(cmd)
-	if err != nil {
-		return "", err
-	}
+func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager, error) {
+	switch testClusterMode {
+	case FakeClusterMode:
+		c := fakeclient.NewClientBuilder().WithScheme(utils.NewScheme()).Build()
+		return &testEnvKubeManager{
+			client: c,
+		}, nil
+	case TestEnvClusterMode:
+		useExistingCluster := false
+		testEnv := &envtest.Environment{
+			UseExistingCluster: &useExistingCluster,
+		}
+		cfg, err := testEnv.Start()
+		if err != nil {
+			return nil, err
+		}
+		user, err := testEnv.ControlPlane.AddUser(envtest.User{
+			Name:   "envtest-admin",
+			Groups: []string{"system:masters"},
+		}, nil)
+		if err != nil {
+			return nil, err
+		}
 
-	buf := new(bytes.Buffer)
+		kubeConfig, err := user.KubeConfig()
+		if err != nil {
+			return nil, err
+		}
 
-	rootCmd.SetOut(buf)
-	rootCmd.SetErr(buf)
-	rootCmd.SetArgs(args)
+		tmpFilename := filepath.Join("/tmp", "kubeconfig-"+time.Nanosecond.String())
+		ioutil.WriteFile(tmpFilename, kubeConfig, 0644)
+		rootArgs.kubeconfig = tmpFilename
+		k8sClient, err := client.NewWithWatch(cfg, client.Options{})
+		if err != nil {
+			return nil, err
+		}
+		return &testEnvKubeManager{
+			testEnv: testEnv,
+			client:  k8sClient,
+		}, nil
+	case ExistingClusterMode:
+		// TEST_KUBECONFIG is mandatory to prevent destroying a current cluster accidentally.
+		testKubeConfig := os.Getenv("TEST_KUBECONFIG")
+		if testKubeConfig == "" {
+			return nil, fmt.Errorf("environment variable TEST_KUBECONFIG is required to run tests against an existing cluster")
+		}
+		rootArgs.kubeconfig = testKubeConfig
 
-	_, err = rootCmd.ExecuteC()
-	result := buf.String()
+		useExistingCluster := true
+		config, err := clientcmd.BuildConfigFromFlags("", testKubeConfig)
+		testEnv := &envtest.Environment{
+			UseExistingCluster: &useExistingCluster,
+			Config:             config,
+		}
+		cfg, err := testEnv.Start()
+		if err != nil {
+			return nil, err
+		}
+		k8sClient, err := client.NewWithWatch(cfg, client.Options{})
+		if err != nil {
+			return nil, err
+		}
+		return &testEnvKubeManager{
+			testEnv: testEnv,
+			client:  k8sClient,
+		}, nil
+	}
 
-	return result, err
+	return nil, nil
 }
 
+type TestClusterMode int
+
+const (
+	FakeClusterMode = TestClusterMode(iota + 1)
+	TestEnvClusterMode
+	ExistingClusterMode
+)
+
 // Structure used for each test to load objects into kubernetes, run
 // commands and assert on the expected output.
 type cmdTestCase struct {
@@ -106,11 +173,20 @@ type cmdTestCase struct {
 	goldenFile string
 	// Filename that contains yaml objects to load into Kubernetes
 	objectFile string
+	// TestClusterMode to bootstrap and testing, default to Fake
+	testClusterMode TestClusterMode
 }
 
 func (cmd *cmdTestCase) runTestCmd(t *testing.T) {
-	km := NewFakeKubeManager()
-	rootCtx.kubeManager = km
+	km, err := NewTestEnvKubeManager(cmd.testClusterMode)
+	if err != nil {
+		t.Fatalf("Error creating kube manager: '%v'", err)
+	}
+
+	if km != nil {
+		rootCtx.kubeManager = km
+		defer km.Stop()
+	}
 
 	if cmd.objectFile != "" {
 		clientObjects, err := readYamlObjects(cmd.objectFile)
@@ -149,6 +225,27 @@ func (cmd *cmdTestCase) runTestCmd(t *testing.T) {
 	}
 }
 
+// Run the command and return the captured output.
+func executeCommand(cmd string) (string, error) {
+	args, err := shellwords.Parse(cmd)
+	if err != nil {
+		return "", err
+	}
+
+	buf := new(bytes.Buffer)
+
+	rootCmd.SetOut(buf)
+	rootCmd.SetErr(buf)
+	rootCmd.SetArgs(args)
+
+	logger.stderr = rootCmd.ErrOrStderr()
+
+	_, err = rootCmd.ExecuteC()
+	result := buf.String()
+
+	return result, err
+}
+
 func TestVersion(t *testing.T) {
 	cmd := cmdTestCase{
 		args:        "--version",
diff --git a/cmd/flux/testdata/install/install_extra_components.golden b/cmd/flux/testdata/install/install_extra_components.golden
new file mode 100644
index 0000000000000000000000000000000000000000..c11f58d38768352fd0ed41685b77fa66e0df5c6d
--- /dev/null
+++ b/cmd/flux/testdata/install/install_extra_components.golden
@@ -0,0 +1,11 @@
+✚ generating manifests
+✔ manifests build completed
+► installing components in flux-system namespace
+◎ verifying installation
+✔ helm-controller: deployment ready
+✔ image-automation-controller: deployment ready
+✔ image-reflector-controller: deployment ready
+✔ kustomize-controller: deployment ready
+✔ notification-controller: deployment ready
+✔ source-controller: deployment ready
+✔ install finished
diff --git a/cmd/flux/testdata/install/install_no_args.golden b/cmd/flux/testdata/install/install_no_args.golden
new file mode 100644
index 0000000000000000000000000000000000000000..8958465f2e9c5495f06d15fe786b8c59fbc8b81b
--- /dev/null
+++ b/cmd/flux/testdata/install/install_no_args.golden
@@ -0,0 +1,9 @@
+✚ generating manifests
+✔ manifests build completed
+► installing components in flux-system namespace
+◎ verifying installation
+✔ helm-controller: deployment ready
+✔ kustomize-controller: deployment ready
+✔ notification-controller: deployment ready
+✔ source-controller: deployment ready
+✔ install finished
diff --git a/cmd/flux/testdata/uninstall/uninstall.golden b/cmd/flux/testdata/uninstall/uninstall.golden
new file mode 100644
index 0000000000000000000000000000000000000000..e75944edd0fd70ce194341c68e73bce7d40b0345
--- /dev/null
+++ b/cmd/flux/testdata/uninstall/uninstall.golden
@@ -0,0 +1,31 @@
+► deleting components in flux-system namespace
+✔ Deployment/flux-system/helm-controller deleted 
+✔ Deployment/flux-system/kustomize-controller deleted 
+✔ Deployment/flux-system/notification-controller deleted 
+✔ Deployment/flux-system/source-controller deleted 
+✔ Service/flux-system/notification-controller deleted 
+✔ Service/flux-system/source-controller deleted 
+✔ Service/flux-system/webhook-receiver deleted 
+✔ NetworkPolicy/flux-system/allow-egress deleted 
+✔ NetworkPolicy/flux-system/allow-scraping deleted 
+✔ NetworkPolicy/flux-system/allow-webhooks deleted 
+✔ ServiceAccount/flux-system/helm-controller deleted 
+✔ ServiceAccount/flux-system/kustomize-controller deleted 
+✔ ServiceAccount/flux-system/notification-controller deleted 
+✔ ServiceAccount/flux-system/source-controller deleted 
+✔ ClusterRole/crd-controller-flux-system deleted 
+✔ ClusterRoleBinding/cluster-reconciler-flux-system deleted 
+✔ ClusterRoleBinding/crd-controller-flux-system deleted 
+► deleting toolkit.fluxcd.io finalizers in all namespaces
+► deleting toolkit.fluxcd.io custom resource definitions
+✔ CustomResourceDefinition/alerts.notification.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/buckets.source.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/gitrepositories.source.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/helmcharts.source.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/helmreleases.helm.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/helmrepositories.source.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/kustomizations.kustomize.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/providers.notification.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/receivers.notification.toolkit.fluxcd.io deleted 
+✔ Namespace/flux-system deleted 
+✔ uninstall finished
diff --git a/cmd/flux/testdata/uninstall/uninstall_extra_components.golden b/cmd/flux/testdata/uninstall/uninstall_extra_components.golden
new file mode 100644
index 0000000000000000000000000000000000000000..f81c6cf84bd4c9b4ebc06cda2f89171428d2fbe6
--- /dev/null
+++ b/cmd/flux/testdata/uninstall/uninstall_extra_components.golden
@@ -0,0 +1,38 @@
+► deleting components in flux-system namespace
+✔ Deployment/flux-system/helm-controller deleted 
+✔ Deployment/flux-system/image-automation-controller deleted 
+✔ Deployment/flux-system/image-reflector-controller deleted 
+✔ Deployment/flux-system/kustomize-controller deleted 
+✔ Deployment/flux-system/notification-controller deleted 
+✔ Deployment/flux-system/source-controller deleted 
+✔ Service/flux-system/notification-controller deleted 
+✔ Service/flux-system/source-controller deleted 
+✔ Service/flux-system/webhook-receiver deleted 
+✔ NetworkPolicy/flux-system/allow-egress deleted 
+✔ NetworkPolicy/flux-system/allow-scraping deleted 
+✔ NetworkPolicy/flux-system/allow-webhooks deleted 
+✔ ServiceAccount/flux-system/helm-controller deleted 
+✔ ServiceAccount/flux-system/image-automation-controller deleted 
+✔ ServiceAccount/flux-system/image-reflector-controller deleted 
+✔ ServiceAccount/flux-system/kustomize-controller deleted 
+✔ ServiceAccount/flux-system/notification-controller deleted 
+✔ ServiceAccount/flux-system/source-controller deleted 
+✔ ClusterRole/crd-controller-flux-system deleted 
+✔ ClusterRoleBinding/cluster-reconciler-flux-system deleted 
+✔ ClusterRoleBinding/crd-controller-flux-system deleted 
+► deleting toolkit.fluxcd.io finalizers in all namespaces
+► deleting toolkit.fluxcd.io custom resource definitions
+✔ CustomResourceDefinition/alerts.notification.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/buckets.source.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/gitrepositories.source.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/helmcharts.source.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/helmreleases.helm.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/helmrepositories.source.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/imagepolicies.image.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/imagerepositories.image.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/imageupdateautomations.image.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/kustomizations.kustomize.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/providers.notification.toolkit.fluxcd.io deleted 
+✔ CustomResourceDefinition/receivers.notification.toolkit.fluxcd.io deleted 
+✔ Namespace/flux-system deleted 
+✔ uninstall finished
diff --git a/cmd/flux/trace_test.go b/cmd/flux/trace_test.go
index d66a51aa6d8eefcd0211dc29d2744884a92a09e4..83847d232f469eea898d22bb8f2f19084012a4ff 100644
--- a/cmd/flux/trace_test.go
+++ b/cmd/flux/trace_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 package main
 
 import (
@@ -6,39 +8,43 @@ import (
 
 func TestTraceNoArgs(t *testing.T) {
 	cmd := cmdTestCase{
-		args:        "trace",
-		wantError:   true,
-		goldenValue: "object name is required",
+		args:            "trace",
+		testClusterMode: FakeClusterMode,
+		wantError:       true,
+		goldenValue:     "object name is required",
 	}
 	cmd.runTestCmd(t)
 }
 
 func TestTraceDeployment(t *testing.T) {
 	cmd := cmdTestCase{
-		args:       "trace podinfo -n podinfo --kind deployment --api-version=apps/v1",
-		wantError:  false,
-		goldenFile: "testdata/trace/deployment.txt",
-		objectFile: "testdata/trace/deployment.yaml",
+		args:            "trace podinfo -n podinfo --kind deployment --api-version=apps/v1",
+		testClusterMode: FakeClusterMode,
+		wantError:       false,
+		goldenFile:      "testdata/trace/deployment.txt",
+		objectFile:      "testdata/trace/deployment.yaml",
 	}
 	cmd.runTestCmd(t)
 }
 
 func TestTraceHelmRelease(t *testing.T) {
 	cmd := cmdTestCase{
-		args:       "trace podinfo -n podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1",
-		wantError:  false,
-		goldenFile: "testdata/trace/helmrelease.txt",
-		objectFile: "testdata/trace/helmrelease.yaml",
+		args:            "trace podinfo -n podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1",
+		testClusterMode: FakeClusterMode,
+		wantError:       false,
+		goldenFile:      "testdata/trace/helmrelease.txt",
+		objectFile:      "testdata/trace/helmrelease.yaml",
 	}
 	cmd.runTestCmd(t)
 }
 
 func TestTraceHelmReleaseMissingGitRef(t *testing.T) {
 	cmd := cmdTestCase{
-		args:       "trace podinfo -n podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1",
-		wantError:  false,
-		goldenFile: "testdata/trace/helmrelease-missing-git-ref.txt",
-		objectFile: "testdata/trace/helmrelease-missing-git-ref.yaml",
+		args:            "trace podinfo -n podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1",
+		testClusterMode: FakeClusterMode,
+		wantError:       false,
+		goldenFile:      "testdata/trace/helmrelease-missing-git-ref.txt",
+		objectFile:      "testdata/trace/helmrelease-missing-git-ref.yaml",
 	}
 	cmd.runTestCmd(t)
 }
diff --git a/go.sum b/go.sum
index 994c9974725be295cadbc443c5ef9b100d6014a0..b6cf5b58611f4d21c0725e1201464779dc128c77 100644
--- a/go.sum
+++ b/go.sum
@@ -115,6 +115,7 @@ github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZw
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
@@ -123,7 +124,9 @@ github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb
 github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw=
 github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
@@ -543,6 +546,7 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
 github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@@ -650,12 +654,14 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
 github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
 github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU=
+github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
 github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
@@ -664,6 +670,7 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
+github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -672,6 +679,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
 github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
@@ -1051,6 +1059,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=
 gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=
 gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
 gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
@@ -1204,6 +1213,7 @@ k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHD
 k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo=
 k8s.io/component-base v0.18.8/go.mod h1:00frPRDas29rx58pPCxNkhUfPbwajlyyvu8ruNgSErU=
 k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA=
+k8s.io/component-base v0.21.3 h1:4WuuXY3Npa+iFfi2aDRiOz+anhNvRfye0859ZgfC5Og=
 k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ=
 k8s.io/component-helpers v0.21.1/go.mod h1:FtC1flbiQlosHQrLrRUulnKxE4ajgWCGy/67fT2GRlQ=
 k8s.io/csi-translation-lib v0.18.8/go.mod h1:6cA6Btlzxy9s3QrS4BCZzQqclIWnTLr6Jx3H2ctAzY4=
diff --git a/internal/flags/crds_test.go b/internal/flags/crds_test.go
index be53a4615e7ffaed288ce0821a36836ae6439a58..9d20b095c8332f725521fa09ba8bc08218fb6800 100644
--- a/internal/flags/crds_test.go
+++ b/internal/flags/crds_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2021 The Flux authors
 
diff --git a/internal/flags/decryption_provider_test.go b/internal/flags/decryption_provider_test.go
index cc1b2f07f8e068b7c630b474f68cf4ec6a021800..86a0deba2f3929a42923535542c56c6732b14964 100644
--- a/internal/flags/decryption_provider_test.go
+++ b/internal/flags/decryption_provider_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux authors
 
diff --git a/internal/flags/ecdsa_curve_test.go b/internal/flags/ecdsa_curve_test.go
index d1b1f05e715d781331a61a4f7b9cc66bbbf23f9c..e58c8b8b74ca3c17c756da9b254a311f8c7e031b 100644
--- a/internal/flags/ecdsa_curve_test.go
+++ b/internal/flags/ecdsa_curve_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux authors
 
diff --git a/internal/flags/git_implementation_test.go b/internal/flags/git_implementation_test.go
index 6372d4bf136557ec89e5922db6c22d76d81709dd..e555dd277dc90ed47cd0cdc43a6bc8a9640902f7 100644
--- a/internal/flags/git_implementation_test.go
+++ b/internal/flags/git_implementation_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2021 The Flux authors
 
diff --git a/internal/flags/helm_chart_source_test.go b/internal/flags/helm_chart_source_test.go
index b484c983a833339b1554bc18f5d7f1c3c088c6bc..62b34ea6ae6cefaf91418c10e61c526e5eb165d1 100644
--- a/internal/flags/helm_chart_source_test.go
+++ b/internal/flags/helm_chart_source_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux authors
 
diff --git a/internal/flags/helm_release_values_test.go b/internal/flags/helm_release_values_test.go
index f6fc28e4935a874d7519d56b31444065f530a7e9..8e39204909943dfe242a9c1764048f1bd94ed14f 100644
--- a/internal/flags/helm_release_values_test.go
+++ b/internal/flags/helm_release_values_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux authors
 
diff --git a/internal/flags/kustomization_source_test.go b/internal/flags/kustomization_source_test.go
index b2950218f4cb86e9cf414cfb0cc4b169ce721a01..db5062d95f4700a8b9902b236e4ee1d58896e2e9 100644
--- a/internal/flags/kustomization_source_test.go
+++ b/internal/flags/kustomization_source_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux authors
 
diff --git a/internal/flags/log_level_test.go b/internal/flags/log_level_test.go
index bccf1ac1158ef1c4d41f55e7b28ae631d001eedf..a8ef1cae38d885ad6f951448afc6e5f3ca8a65f3 100644
--- a/internal/flags/log_level_test.go
+++ b/internal/flags/log_level_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux authors
 
diff --git a/internal/flags/public_key_algorithm_test.go b/internal/flags/public_key_algorithm_test.go
index 84e19c22b9dba4dbe4e8bf348500c0afefeb5a3c..e2b6923052131658555d871c2865372b3f77d17b 100644
--- a/internal/flags/public_key_algorithm_test.go
+++ b/internal/flags/public_key_algorithm_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux authors
 
diff --git a/internal/flags/rsa_key_bits_test.go b/internal/flags/rsa_key_bits_test.go
index f980527007cf377cc8100cf91aad864bbf95dcec..e0b15e7c8be8f5f7bcb52fc4f49c57dcafd9ca35 100644
--- a/internal/flags/rsa_key_bits_test.go
+++ b/internal/flags/rsa_key_bits_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux authors
 
diff --git a/internal/flags/safe_relative_path_test.go b/internal/flags/safe_relative_path_test.go
index 872085ad6fd7cdc5f90beb818432778242a5b4ef..8651436f80ecf407a7ae9758b689b47d135fdf47 100644
--- a/internal/flags/safe_relative_path_test.go
+++ b/internal/flags/safe_relative_path_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux authors
 
diff --git a/internal/flags/source_bucket_provider_test.go b/internal/flags/source_bucket_provider_test.go
index 05b4f7ce9966d393a878916369e201feeed111b3..5ec82e3520974ad343c1188fc5e78b52ed358cf5 100644
--- a/internal/flags/source_bucket_provider_test.go
+++ b/internal/flags/source_bucket_provider_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux authors
 
diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go
index 59ec97ae87d80fc4a288e8eab12336f56835d793..2e5d6542a078127c2655be7429add86e4dbaf2af 100644
--- a/internal/utils/utils_test.go
+++ b/internal/utils/utils_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2021 The Flux authors
 
diff --git a/pkg/manifestgen/sourcesecret/sourcesecret_test.go b/pkg/manifestgen/sourcesecret/sourcesecret_test.go
index 24ab8057925b3ec29ed1f232d5375811282ac290..b7e4e2c1af023563ad3615a02a829ccabd1d073a 100644
--- a/pkg/manifestgen/sourcesecret/sourcesecret_test.go
+++ b/pkg/manifestgen/sourcesecret/sourcesecret_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2021 The Flux authors
 
diff --git a/pkg/manifestgen/sync/sync_test.go b/pkg/manifestgen/sync/sync_test.go
index a39d09d99a641c123417220770eb04fe2b9c4af9..732442af51fdf3c13aa86f2689d3ebd1c4566650 100644
--- a/pkg/manifestgen/sync/sync_test.go
+++ b/pkg/manifestgen/sync/sync_test.go
@@ -1,3 +1,5 @@
+// +build !e2e
+
 /*
 Copyright 2020 The Flux CD contributors.
 
diff --git a/pkg/status/status.go b/pkg/status/status.go
index 1a5abc3746bdd4ee1af0fc342b622fbc506e4f17..b2894052ee4f67fa77029b579836e11166b457d9 100644
--- a/pkg/status/status.go
+++ b/pkg/status/status.go
@@ -19,6 +19,7 @@ package status
 import (
 	"context"
 	"fmt"
+	"sort"
 	"strings"
 	"time"
 
@@ -74,7 +75,13 @@ func (sc *StatusChecker) Assess(identifiers ...object.ObjMetadata) error {
 
 	<-done
 
-	for _, rs := range coll.ResourceStatuses {
+	// we use sorted identifiers to loop over the resource statuses because a Go's map is unordered.
+	// sorting identifiers by object's name makes sure that the logs look stable for every run
+	sort.SliceStable(identifiers, func(i, j int) bool {
+		return strings.Compare(identifiers[i].Name, identifiers[j].Name) < 0
+	})
+	for _, id := range identifiers {
+		rs := coll.ResourceStatuses[id]
 		switch rs.Status {
 		case status.CurrentStatus:
 			sc.logger.Successf("%s: %s ready", rs.Identifier.Name, strings.ToLower(rs.Identifier.GroupKind.Kind))