Repository: linkerd/linkerd2
Pull request #4020 (2020-02-06 21:31:01)

Integration test flakiness: endpoint version mismatch warning
Updated regex for ignoring version mismatch warning events. It was only
applied for '-*upgrade' namespaces.

It is safe to ignore such warnings because the endpoint controller
retries when that happens, and if after many retries it still can't then
a different warning is thrown which is _not_ whitelisted and will make
the build fail.
https://github.com/kubernetes/kubernetes/blob/v1.16.6/pkg/controller/endpoint/endpoints_controller.go#L334-L348

This PR also removes logging matches on expected warnings, to avoid
cluttering the CI log.



Patch:
diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
index 48671c88d..e595201b9 100644
--- a/.github/workflows/workflow.yml
+++ b/.github/workflows/workflow.yml
@@ -17,19 +17,23 @@ on:
 # go_lint
 # js_unit_tests
 
-# Docker build and integration tests for every master/tag push and linkerd org PR
+# master/tag push and linkerd org PR: Docker build and integration tests
 #
 # docker_pull
 # docker_build
 # kind_setup
-#   -> kind_integration
+#   -> kind_integration_host
 #     -> kind_cleanup
 
+# Forked repository PR: Docker build and integration tests
+#
+# docker_build
+#   -> kind_integration_github
+
 # Docker deploy and cloud integration tests for every master/tag push
 #
 #       -> docker_deploy
 #         -> cloud_integration
-#           -> cloud_cleanup
 
 jobs:
 
@@ -135,18 +139,17 @@ jobs:
   # Docker build and kind integration tests run for:
   # - every master push
   # - every tag push
-  # - every PR from a linkerd org member
+  # - every PR
   #
 
   docker_pull:
     name: Docker pull
+    if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
     runs-on: ubuntu-18.04
     steps:
     - name: Checkout code
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       uses: actions/checkout@v2
     - name: Docker SSH setup
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       run: |
         mkdir -p ~/.ssh/
         touch ~/.ssh/id && chmod 600 ~/.ssh/id
@@ -155,7 +158,6 @@ jobs:
         echo "${{ secrets.DOCKER_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
         ssh linkerd-docker docker version
     - name: Docker pull
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       env:
         DOCKER_HOST: ssh://linkerd-docker
       run: |
@@ -165,9 +167,10 @@ jobs:
   docker_build:
     name: Docker build
     runs-on: ubuntu-18.04
+    env:
+      IMAGE_ARCHIVES_DIR: /home/runner/archives
     steps:
     - name: Checkout code
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       uses: actions/checkout@v2
     - name: Docker SSH setup
       if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
@@ -178,13 +181,30 @@ jobs:
         echo "${{ secrets.DOCKER_PRIVATE_KEY }}" > ~/.ssh/id
         echo "${{ secrets.DOCKER_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
         ssh linkerd-docker docker version
-    - name: Docker build
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
-      env:
-        DOCKER_HOST: ssh://linkerd-docker
+        echo ::set-env name=DOCKER_HOST::ssh://linkerd-docker
+    - name: Build docker images
       run: |
         export PATH="`pwd`/bin:$PATH"
         DOCKER_TRACE=1 bin/docker-build
+    - name: Create artifact with CLI and image archives
+      if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
+      run: |
+        mkdir -p $IMAGE_ARCHIVES_DIR
+        cp target/cli/linux/linkerd $IMAGE_ARCHIVES_DIR/
+        TAG="$(CI_FORCE_CLEAN=1 bin/root-tag)"
+        for img in controller grafana proxy web ; do
+          docker save "gcr.io/linkerd-io/$img:$TAG" > $IMAGE_ARCHIVES_DIR/$img.tar
+        done
+    # `with.path` values do not support environment variables yet, so an
+    # absolute path is used here.
+    #
+    # https://github.com/actions/upload-artifact/issues/8
+    - name: Upload artifact
+      if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
+      uses: actions/upload-artifact@v1
+      with:
+        name: image-archives
+        path: /home/runner/archives
 
   kind_setup:
     strategy:
@@ -192,13 +212,12 @@ jobs:
       matrix:
         integration_test: [deep, upgrade, helm, helm_upgrade, custom_domain, external_issuer]
     name: Cluster setup (${{ matrix.integration_test }})
+    if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
     runs-on: ubuntu-18.04
     steps:
     - name: Checkout code
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       uses: actions/checkout@v2
     - name: Docker SSH setup
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       run: |
         mkdir -p ~/.ssh/
         touch ~/.ssh/id && chmod 600 ~/.ssh/id
@@ -207,7 +226,6 @@ jobs:
         echo "${{ secrets.DOCKER_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
         ssh linkerd-docker docker version
     - name: Kind cluster setup
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       env:
         DOCKER_HOST: ssh://linkerd-docker
       run: |
@@ -225,20 +243,27 @@ jobs:
             bin/kind create cluster --name=$KIND_CLUSTER --wait=2m --verbosity 3
         fi
 
-  kind_integration:
+  #
+  # Integration tests that run on the docker host are limited by the fact that
+  # while they all run in separate KinD clusters, they are on the same
+  # machine. The `strategy` context cannot be conditionally set, so there are
+  # separate `kind_integration_*` jobs. This allows the `kind_integration_github`
+  # job to run it's entire matrix in parallel.
+  #
+
+  kind_integration_host:
     strategy:
       max-parallel: 3
       matrix:
         integration_test: [deep, upgrade, helm, helm_upgrade, custom_domain, external_issuer]
     needs: [docker_pull, docker_build, kind_setup]
-    name: Integration tests (${{ matrix.integration_test }})
+    name: Host int. tests (${{ matrix.integration_test }})
+    if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
     runs-on: ubuntu-18.04
     steps:
     - name: Checkout code
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       uses: actions/checkout@v2
     - name: Docker SSH setup
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       run: |
         mkdir -p ~/.ssh/
         touch ~/.ssh/id && chmod 600 ~/.ssh/id
@@ -246,8 +271,14 @@ jobs:
         echo "${{ secrets.DOCKER_PRIVATE_KEY }}" > ~/.ssh/id
         echo "${{ secrets.DOCKER_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
         ssh linkerd-docker docker version
+    - name: Try to load cached Go modules
+      uses: actions/cache@v1
+      with:
+        path: ~/go/pkg/mod
+        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        restore-keys: |
+          ${{ runner.os }}-go-
     - name: Kind load docker images
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       run: |
         TAG="$(CI_FORCE_CLEAN=1 bin/root-tag)"
         export KIND_CLUSTER=github-$TAG-${{ matrix.integration_test }}
@@ -262,7 +293,6 @@ jobs:
           done
         EOF
     - name: Install linkerd CLI
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       env:
         DOCKER_HOST: ssh://linkerd-docker
       run: |
@@ -275,7 +305,6 @@ jobs:
         [[ "$TAG" == "$($HOME/.linkerd version --short --client)" ]]
         echo "Installed Linkerd CLI version: $TAG"
     - name: Run integration tests
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       env:
         DOCKER_HOST: ssh://linkerd-docker
         GITCOOKIE_SH: ${{ secrets.GITCOOKIE_SH }}
@@ -302,18 +331,65 @@ jobs:
           ${{ matrix.integration_test }}_integration_tests
         )
 
+  kind_integration_github:
+    strategy:
+      matrix:
+        integration_test: [deep, upgrade, helm, helm_upgrade, custom_domain, external_issuer]
+    needs: [docker_build]
+    name: GitHub int. tests (${{ matrix.integration_test }})
+    if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
+    runs-on: ubuntu-18.04
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@v2
+    - name: Download image archives
+      uses: actions/download-artifact@v1
+      with:
+        name: image-archives
+    - name: Try to load cached Go modules
+      uses: actions/cache@v1
+      with:
+        path: ~/go/pkg/mod
+        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+        restore-keys: |
+          ${{ runner.os }}-go-
+    - name: Setup KinD (default)
+      if: matrix.integration_test != 'custom_domain'
+      uses: engineerd/setup-kind@v0.3.0
+      with:
+        version: "v0.6.1"
+    - name: Setup KinD (custom_domain)
+      if: matrix.integration_test == 'custom_domain'
+      uses: engineerd/setup-kind@v0.3.0
+      with:
+        config: test/testdata/custom_cluster_domain_config.yaml
+        version: "v0.6.1"
+    - name: Load image archives into KinD
+      run: |
+        for img in controller grafana proxy web; do
+          kind load image-archive image-archives/$img.tar
+        done
+    - name: Install linkerd CLI
+      run: |
+        cp image-archives/linkerd $HOME/.linkerd
+        chmod +x $HOME/.linkerd
+    - name: Run integration tests
+      run: |
+        . bin/_test-run.sh
+        init_test_run $HOME/.linkerd
+        ${{ matrix.integration_test }}_integration_tests
+
   kind_cleanup:
-    if: always()
     strategy:
       fail-fast: false # always attempt to cleanup all clusters
       matrix:
         integration_test: [deep, upgrade, helm, helm_upgrade, custom_domain, external_issuer]
-    needs: [kind_integration]
+    needs: [kind_integration_host]
     name: Cluster cleanup (${{ matrix.integration_test }})
+    if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork)
     runs-on: ubuntu-18.04
     steps:
     - name: Checkout code
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       uses: actions/checkout@v2
     # for debugging
     - name: Dump env
@@ -328,7 +404,6 @@ jobs:
         JOB_CONTEXT: ${{ toJson(job) }}
       run: echo "$JOB_CONTEXT"
     - name: Docker SSH setup
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       run: |
         mkdir -p ~/.ssh/
         touch ~/.ssh/id && chmod 600 ~/.ssh/id
@@ -337,7 +412,6 @@ jobs:
         echo "${{ secrets.DOCKER_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
         ssh linkerd-docker docker version
     - name: Kind cluster cleanup
-      if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
       env:
         DOCKER_HOST: ssh://linkerd-docker
       run: |
@@ -353,38 +427,19 @@ jobs:
 
   docker_deploy:
     name: Docker deploy
+    if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
     runs-on: ubuntu-18.04
-    needs: [validate_go_deps, go_unit_tests, go_lint, js_unit_tests, kind_integration, kind_cleanup]
+    needs: [validate_go_deps, go_unit_tests, go_lint, js_unit_tests, kind_integration_host, kind_cleanup]
     steps:
     - name: Checkout code
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
       uses: actions/checkout@v2
     - name: Configure gcloud
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
-      env:
-        CLOUDSDK_CORE_DISABLE_PROMPTS: "1"
-        CLOUD_SDK_SERVICE_ACCOUNT_KEY: ${{ secrets.CLOUD_SDK_SERVICE_ACCOUNT_KEY }}
-        GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
-        GCP_ZONE: ${{ secrets.GCP_ZONE }}
-        GKE_CLUSTER: ${{ secrets.GKE_CLUSTER }}
-      run: |
-        # Install gcloud and kubectl.
-        echo "$CLOUD_SDK_SERVICE_ACCOUNT_KEY" > .gcp.json
-        dir="${CLOUDSDK_INSTALL_DIR:-${HOME}}/google-cloud-sdk"
-        (
-          . bin/_gcp.sh ;
-          install_gcloud "$dir"
-          gcloud components install kubectl
-          # Configure gcloud with a service account.
-          set_gcloud_config "$GCP_PROJECT" "$GCP_ZONE" "$GKE_CLUSTER"
-          # Get a kubernetes context.
-          get_k8s_ctx "$GCP_PROJECT" "$GCP_ZONE" "$GKE_CLUSTER"
-        )
-        . "$dir/path.bash.inc"
-        gcloud auth configure-docker
-        bin/kubectl version --short
+      uses: linkerd/linkerd2-action-gcloud@v1.0.0
+      with:
+        cloud_sdk_service_account_key: ${{ secrets.CLOUD_SDK_SERVICE_ACCOUNT_KEY }}
+        gcp_project: ${{ secrets.GCP_PROJECT }}
+        gcp_zone: ${{ secrets.GCP_ZONE }}
     - name: Docker SSH setup
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
       run: |
         mkdir -p ~/.ssh/
         touch ~/.ssh/id && chmod 600 ~/.ssh/id
@@ -393,7 +448,6 @@ jobs:
         echo "${{ secrets.DOCKER_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
         ssh linkerd-docker docker version
     - name: Docker push
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
       env:
         DOCKER_HOST: ssh://linkerd-docker
       run: |
@@ -406,38 +460,14 @@ jobs:
 
   cloud_integration:
     name: Cloud integration tests
+    if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
     runs-on: ubuntu-18.04
     needs: [docker_deploy]
     steps:
     - name: Checkout code
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
       uses: actions/checkout@v2
-    - name: Configure gcloud
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
-      env:
-        CLOUDSDK_CORE_DISABLE_PROMPTS: "1"
-        CLOUD_SDK_SERVICE_ACCOUNT_KEY: ${{ secrets.CLOUD_SDK_SERVICE_ACCOUNT_KEY }}
-        GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
-        GCP_ZONE: ${{ secrets.GCP_ZONE }}
-        GKE_CLUSTER: ${{ secrets.GKE_CLUSTER }}
-      run: |
-        # Install gcloud and kubectl.
-        echo "$CLOUD_SDK_SERVICE_ACCOUNT_KEY" > .gcp.json
-        dir="${CLOUDSDK_INSTALL_DIR:-${HOME}}/google-cloud-sdk"
-        (
-          . bin/_gcp.sh ;
-          install_gcloud "$dir"
-          gcloud components install kubectl
-          # Configure gcloud with a service account.
-          set_gcloud_config "$GCP_PROJECT" "$GCP_ZONE" "$GKE_CLUSTER"
-          # Get a kubernetes context.
-          get_k8s_ctx "$GCP_PROJECT" "$GCP_ZONE" "$GKE_CLUSTER"
-        )
-        . "$dir/path.bash.inc"
-        gcloud auth configure-docker
-        bin/kubectl version --short
     - name: Install linkerd CLI
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
+      id: install_cli
       run: |
         TAG="$(CI_FORCE_CLEAN=1 bin/root-tag)"
         image="gcr.io/linkerd-io/cli-bin:$TAG"
@@ -447,8 +477,16 @@ jobs:
         # validate CLI version matches the repo
         [[ "$TAG" == "$($HOME/.linkerd version --short --client)" ]]
         echo "Installed Linkerd CLI version: $TAG"
+        echo "::set-output name=tag::$TAG"
+    - name: Create GKE cluster
+      uses: linkerd/linkerd2-action-gcloud@v1.0.0
+      with:
+        cloud_sdk_service_account_key: ${{ secrets.CLOUD_SDK_SERVICE_ACCOUNT_KEY }}
+        gcp_project: ${{ secrets.GCP_PROJECT }}
+        gcp_zone: ${{ secrets.GCP_ZONE }}
+        create: true
+        name: testing-${{ steps.install_cli.outputs.tag }}-${{ github.run_id }}
     - name: Run integration tests
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
       env:
         GITCOOKIE_SH: ${{ secrets.GITCOOKIE_SH }}
       run: |
@@ -457,92 +495,39 @@ jobs:
         version="$($HOME/.linkerd version --client --short | tr -cd '[:alnum:]-')"
         bin/test-run $HOME/.linkerd linkerd-$version
     - name: CNI tests
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
       run: |
         export TAG="$($HOME/.linkerd version --client --short)"
         go test -cover -race -v -mod=readonly ./cni-plugin/test -integration-tests
 
-  cloud_cleanup:
-    if: always()
-    name: Cloud cleanup
-    runs-on: ubuntu-18.04
-    needs: [cloud_integration]
-    steps:
-    - name: Checkout code
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
-      uses: actions/checkout@v2
-    - name: Configure gcloud
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
-      env:
-        CLOUDSDK_CORE_DISABLE_PROMPTS: "1"
-        CLOUD_SDK_SERVICE_ACCOUNT_KEY: ${{ secrets.CLOUD_SDK_SERVICE_ACCOUNT_KEY }}
-        GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
-        GCP_ZONE: ${{ secrets.GCP_ZONE }}
-        GKE_CLUSTER: ${{ secrets.GKE_CLUSTER }}
-      run: |
-        # Install gcloud and kubectl.
-        echo "$CLOUD_SDK_SERVICE_ACCOUNT_KEY" > .gcp.json
-        dir="${CLOUDSDK_INSTALL_DIR:-${HOME}}/google-cloud-sdk"
-        (. bin/_gcp.sh ;
-          install_gcloud "$dir"
-          gcloud components install kubectl
-          # Configure gcloud with a service account.
-          set_gcloud_config "$GCP_PROJECT" "$GCP_ZONE" "$GKE_CLUSTER"
-          # Get a kubernetes context.
-          get_k8s_ctx "$GCP_PROJECT" "$GCP_ZONE" "$GKE_CLUSTER"
-        )
-        . "$dir/path.bash.inc"
-        gcloud auth configure-docker
-        bin/kubectl version --short
-    - name: Cleanup
-      if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
-      run: |
-        export PATH="`pwd`/bin:$PATH"
-        bin/test-cleanup
-
-  #
   # Helm chart artifact deploy run for:
   # - every tag push
   #
 
   chart_deploy:
     name: Helm chart deploy
+    if: startsWith(github.ref, 'refs/tags')
     runs-on: ubuntu-18.04
     needs: [cloud_integration]
     steps:
     - name: Checkout code
-      if: startsWith(github.ref, 'refs/tags')
       uses: actions/checkout@v2
     - name: Configure gsutils
-      if: startsWith(github.ref, 'refs/tags')
-      env:
-        CLOUDSDK_CORE_DISABLE_PROMPTS: "1"
-        CLOUD_SDK_SERVICE_ACCOUNT_KEY: ${{ secrets.LINKERD_SITE_TOKEN }}
-        GCP_PROJECT: ${{ secrets.LINKERD_SITE_PROJECT }}
-        GCP_ZONE: ${{ secrets.LINKERD_SITE_ZONE }}
-        GKE_CLUSTER: ${{ secrets.GKE_CLUSTER }}
-      run: |
-        # Install gcloud and gsutil.
-        echo "$CLOUD_SDK_SERVICE_ACCOUNT_KEY" > .gcp.json
-        dir="${CLOUDSDK_INSTALL_DIR:-${HOME}}/google-cloud-sdk"
-        (
-          . bin/_gcp.sh ;
-          install_gcloud "$dir"
-          gcloud components install gsutil
-          # Configure gcloud with a service account.
-          set_gcloud_config "$GCP_PROJECT" "$GCP_ZONE" "$GKE_CLUSTER"
-        )
-        . "$dir/path.bash.inc"
-        mkdir -p target/helm
+      uses: linkerd/linkerd2-action-gcloud@v1.0.0
+      with:
+        cloud_sdk_service_account_key: ${{ secrets.CLOUD_SDK_SERVICE_ACCOUNT_KEY }}
+        gcp_project: ${{ secrets.GCP_PROJECT }}
+        gcp_zone: ${{ secrets.GCP_ZONE }}
     - name: Edge Helm chart creation and upload
       if: startsWith(github.ref, 'refs/tags/edge')
       run: |
+        mkdir -p target/helm
         gsutil cp gs://helm.linkerd.io/edge/index.yaml target/helm/index-pre.yaml
         bin/helm-build package
         gsutil rsync target/helm gs://helm.linkerd.io/edge
     - name: Stable Helm chart creation and upload
       if: startsWith(github.ref, 'refs/tags/stable')
       run: |
+        mkdir -p target/helm
         gsutil cp gs://helm.linkerd.io/stable/index.yaml target/helm/index-pre.yaml
         bin/helm-build package
         gsutil rsync target/helm gs://helm.linkerd.io/stable
diff --git a/.gitignore b/.gitignore
index 449714e41..1ed553166 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@ tmp.discovery
 **/CMakeLists.txt
 *.iml
 web/web
-web/app/node_modules
+**/node_modules
 web/app/dist
 web/app/yarn-error.log
 .gorun
diff --git a/Dockerfile-proxy b/Dockerfile-proxy
index 0ae139182..7d0da11f4 100644
--- a/Dockerfile-proxy
+++ b/Dockerfile-proxy
@@ -9,7 +9,7 @@ RUN (proxy=$(bin/fetch-proxy $(cat proxy-version)) && \
     mv "$proxy" linkerd2-proxy)
 
 ## compile proxy-identity agent
-FROM gcr.io/linkerd-io/go-deps:8d2ce4bf as golang
+FROM gcr.io/linkerd-io/go-deps:93ea34a8 as golang
 WORKDIR /linkerd-build
 COPY pkg/flags pkg/flags
 COPY pkg/tls pkg/tls
diff --git a/bin/_gcp.sh b/bin/_gcp.sh
deleted file mode 100644
index 56fd29515..000000000
--- a/bin/_gcp.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-set -eu
-
-install_gcloud() {
-    dir="$1"
-
-    export CLOUDSDK_CORE_DISABLE_PROMPTS=1
-    if [ -d "$dir/bin" ]; then
-        . "$dir/path.bash.inc"
-        gcloud components update
-    else
-        rm -rf "$dir"
-        curl https://sdk.cloud.google.com | bash
-        . "$dir/path.bash.inc"
-    fi
-}
-
-set_gcloud_config() {
-    project="$1"
-    zone="$2"
-    cluster="$3"
-
-    gcloud auth activate-service-account --key-file .gcp.json
-    gcloud config set core/project "$project"
-    gcloud config set compute/zone "$zone"
-    gcloud config set container/cluster "$cluster"
-}
-
-get_k8s_ctx() {
-    project="$1"
-    zone="$2"
-    cluster="$3"
-
-    for c in $(kubectl config get-clusters |sed 1d) ; do
-        if [ "$c" = "gke_${project}_${zone}_${cluster}" ]; then
-            return 0
-        fi
-    done
-
-    gcloud container clusters get-credentials "$cluster"
-}
diff --git a/charts/linkerd2-cni/templates/cni-plugin.yaml b/charts/linkerd2-cni/templates/cni-plugin.yaml
index 003a39965..4521750d1 100644
--- a/charts/linkerd2-cni/templates/cni-plugin.yaml
+++ b/charts/linkerd2-cni/templates/cni-plugin.yaml
@@ -25,7 +25,7 @@ metadata:
   annotations:
     {{.Values.proxyInjectAnnotation}}: {{.Values.proxyInjectDisabled}}
   labels:
-    {{.Values.linkerdNamespaceLabel}}: "true"
+    {{.Values.cniResourceLabel}}: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
 apiVersion: policy/v1beta1
@@ -33,7 +33,7 @@ kind: PodSecurityPolicy
 metadata:
   name: linkerd-{{.Values.namespace}}-cni
   labels:
-    {{.Values.cniResourceAnnotation}}: "true"
+    {{.Values.cniResourceLabel}}: "true"
 spec:
   allowPrivilegeEscalation: false
   fsGroup:
@@ -55,7 +55,7 @@ metadata:
   name: linkerd-cni
   namespace: {{.Values.namespace}}
   labels:
-    {{.Values.cniResourceAnnotation}}: "true"
+    {{.Values.cniResourceLabel}}: "true"
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -63,7 +63,7 @@ metadata:
   name: linkerd-cni
   namespace: {{.Values.namespace}}
   labels:
-    {{.Values.cniResourceAnnotation}}: "true"
+    {{.Values.cniResourceLabel}}: "true"
 rules:
 - apiGroups: ['extensions', 'policy']
   resources: ['podsecuritypolicies']
@@ -77,7 +77,7 @@ metadata:
   name: linkerd-cni
   namespace: {{.Values.namespace}}
   labels:
-    {{.Values.cniResourceAnnotation}}: "true"
+    {{.Values.cniResourceLabel}}: "true"
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: Role
@@ -92,7 +92,7 @@ apiVersion: rbac.authorization.k8s.io/v1beta1
 metadata:
   name: linkerd-cni
   labels:
-    {{.Values.cniResourceAnnotation}}: "true"
+    {{.Values.cniResourceLabel}}: "true"
 rules:
 - apiGroups: [""]
   resources: ["pods", "nodes", "namespaces"]
@@ -103,7 +103,7 @@ kind: ClusterRoleBinding
 metadata:
   name: linkerd-cni
   labels:
-    {{.Values.cniResourceAnnotation}}: "true"
+    {{.Values.cniResourceLabel}}: "true"
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: ClusterRole
@@ -119,7 +119,7 @@ metadata:
   name: linkerd-cni-config
   namespace: {{.Values.namespace}}
   labels:
-    {{.Values.cniResourceAnnotation}}: "true"
+    {{.Values.cniResourceLabel}}: "true"
 data:
   dest_cni_net_dir: "{{.Values.destCNINetDir}}"
   dest_cni_bin_dir: "{{.Values.destCNIBinDir}}"
@@ -161,7 +161,7 @@ metadata:
   namespace: {{.Values.namespace}}
   labels:
     k8s-app: linkerd-cni
-    {{.Values.cniResourceAnnotation}}: "true"
+    {{.Values.cniResourceLabel}}: "true"
   annotations:
     {{.Values.createdByAnnotation}}: {{.Values.cliVersion}}
 spec:
diff --git a/charts/linkerd2-cni/values.yaml b/charts/linkerd2-cni/values.yaml
index 3a3c502f9..adcc92b11 100644
--- a/charts/linkerd2-cni/values.yaml
+++ b/charts/linkerd2-cni/values.yaml
@@ -1,5 +1,5 @@
 namespace: linkerd-cni
-cniResourceAnnotation: linkerd.io/cni-resource
+cniResourceLabel: linkerd.io/cni-resource
 inboundProxyPort: 4143
 outboundProxyPort: 4140
 ignoreInboundPorts: ""
@@ -17,4 +17,3 @@ useWaitFlag:      false
 # namespace annotation and labels - do not edit
 proxyInjectAnnotation: linkerd.io/inject
 proxyInjectDisabled: disabled
-linkerdNamespaceLabel: linkerd.io/is-control-plane
diff --git a/cli/Dockerfile-bin b/cli/Dockerfile-bin
index ced183955..c465d05df 100644
--- a/cli/Dockerfile-bin
+++ b/cli/Dockerfile-bin
@@ -1,5 +1,5 @@
 ## compile binaries
-FROM gcr.io/linkerd-io/go-deps:8d2ce4bf as golang
+FROM gcr.io/linkerd-io/go-deps:93ea34a8 as golang
 WORKDIR /linkerd-build
 COPY cli cli
 COPY charts charts
diff --git a/cli/cmd/install-cni-plugin.go b/cli/cmd/install-cni-plugin.go
index 54eda64ae..f8edae2f7 100644
--- a/cli/cmd/install-cni-plugin.go
+++ b/cli/cmd/install-cni-plugin.go
@@ -76,8 +76,8 @@ func newCmdInstallCNIPlugin() *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:   "install-cni [flags]",
-		Short: "Output Kubernetes configs to install Linkerd CNI (experimental)",
-		Long: `Output Kubernetes configs to install Linkerd CNI (experimental).
+		Short: "Output Kubernetes configs to install Linkerd CNI",
+		Long: `Output Kubernetes configs to install Linkerd CNI.
 
 This command installs a DaemonSet into the Linkerd control plane. The DaemonSet
 copies the necessary linkerd-cni plugin binaries and configs onto the host. It
diff --git a/cli/cmd/install.go b/cli/cmd/install.go
index 721fafbef..ba351fd80 100644
--- a/cli/cmd/install.go
+++ b/cli/cmd/install.go
@@ -499,7 +499,7 @@ func (options *installOptions) allStageFlagSet() *pflag.FlagSet {
 	flags := pflag.NewFlagSet("all-stage", pflag.ExitOnError)
 
 	flags.BoolVar(&options.cniEnabled, "linkerd-cni-enabled", options.cniEnabled,
-		"Experimental: Omit the NET_ADMIN capability in the PSP and the proxy-init container when injecting the proxy; requires the linkerd-cni plugin to already be installed",
+		"Omit the NET_ADMIN capability in the PSP and the proxy-init container when injecting the proxy; requires the linkerd-cni plugin to already be installed",
 	)
 
 	flags.BoolVar(
diff --git a/cni-plugin/Dockerfile b/cni-plugin/Dockerfile
index b229039b6..9ae99bcb2 100644
--- a/cni-plugin/Dockerfile
+++ b/cni-plugin/Dockerfile
@@ -1,5 +1,5 @@
 ## compile cni-plugin utility
-FROM gcr.io/linkerd-io/go-deps:8d2ce4bf as golang
+FROM gcr.io/linkerd-io/go-deps:93ea34a8 as golang
 WORKDIR /linkerd-build
 COPY pkg pkg
 COPY controller controller
diff --git a/controller/Dockerfile b/controller/Dockerfile
index 1c9dd7f65..b4cc08afc 100644
--- a/controller/Dockerfile
+++ b/controller/Dockerfile
@@ -1,5 +1,5 @@
 ## compile controller service
-FROM gcr.io/linkerd-io/go-deps:8d2ce4bf as golang
+FROM gcr.io/linkerd-io/go-deps:93ea34a8 as golang
 WORKDIR /linkerd-build
 COPY controller/gen controller/gen
 COPY pkg pkg
diff --git a/go.mod b/go.mod
index 3d5d222ee..dd73d025b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/linkerd/linkerd2
 
-go 1.13.4
+go 1.13
 
 require (
 	contrib.go.opencensus.io/exporter/ocagent v0.6.0
diff --git a/go.sum b/go.sum
index 48a0f60aa..518fec565 100644
--- a/go.sum
+++ b/go.sum
@@ -211,10 +211,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/linkerd/linkerd2-proxy-api v0.1.10 h1:zGwz2Fek7fdfCxyZW7jq8IE6sexeVFW5UFBQFL6MrP4=
 github.com/linkerd/linkerd2-proxy-api v0.1.10/go.mod h1:2WJHEYXoww5ALM6c1QspRFiROGZg08tGxCO1js0tTVA=
-github.com/linkerd/linkerd2-proxy-init v1.3.0 h1:kyrMpaNKGPfd7FA8O/6VGxsYXx++TxxZVgRL6knWSa8=
-github.com/linkerd/linkerd2-proxy-init v1.3.0/go.mod h1:lK/sORPZDP6aFhPZps+AHMKphX5v9bKVDNESllz18C0=
-github.com/linkerd/linkerd2-proxy-init v1.3.1-0.20200117170322-a7441ab79b25 h1:RVz/okrTQugk9GKUDY9mJbl8mKUh90jfxTu97IvBvjg=
-github.com/linkerd/linkerd2-proxy-init v1.3.1-0.20200117170322-a7441ab79b25/go.mod h1:lK/sORPZDP6aFhPZps+AHMKphX5v9bKVDNESllz18C0=
 github.com/linkerd/linkerd2-proxy-init v1.3.1 h1:KTvUvPlSKUkpzZM0L/EIk+EJaKk7dwygu0CP95fOyGI=
 github.com/linkerd/linkerd2-proxy-init v1.3.1/go.mod h1:lK/sORPZDP6aFhPZps+AHMKphX5v9bKVDNESllz18C0=
 github.com/linkerd/stern v0.0.0-20190907020106-201e8ccdff9c h1:v8RmXOGO65HHLqcDpVNGGe3krA8FptfxcYS3wSS8bJU=
diff --git a/pkg/charts/cni/values.go b/pkg/charts/cni/values.go
index c4881ce94..27583e52b 100644
--- a/pkg/charts/cni/values.go
+++ b/pkg/charts/cni/values.go
@@ -16,7 +16,7 @@ const (
 // Values contains the top-level elements in the cni Helm chart
 type Values struct {
 	Namespace             string `json:"namespace"`
-	CNIResourceAnnotation string `json:"cniResourceAnnotation"`
+	CNIResourceLabel      string `json:"cniResourceLabel"`
 	InboundProxyPort      uint   `json:"inboundProxyPort"`
 	OutboundProxyPort     uint   `json:"outboundProxyPort"`
 	IgnoreInboundPorts    string `json:"ignoreInboundPorts"`
@@ -33,7 +33,6 @@ type Values struct {
 	UseWaitFlag           bool   `json:"useWaitFlag"`
 	ProxyInjectAnnotation string `json:"proxyInjectAnnotation"`
 	ProxyInjectDisabled   string `json:"proxyInjectDisabled"`
-	LinkerdNamespaceLabel string `json:"linkerdNamespaceLabel"`
 }
 
 // NewValues returns a new instance of the Values type.
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 2bba7aa45..27cdd4d38 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -166,6 +166,15 @@ const HintBaseURL = "https://linkerd.io/checks/#"
 // TODO: Make this default value overridiable, e.g. by CLI flag
 const AllowedClockSkew = time.Minute + tls.DefaultClockSkewAllowance
 
+var linkerdHAControlPlaneComponents = []string{
+	"linkerd-controller",
+	"linkerd-destination",
+	"linkerd-identity",
+	"linkerd-proxy-injector",
+	"linkerd-sp-validator",
+	"linkerd-tap",
+}
+
 var (
 	retryWindow    = 5 * time.Second
 	requestTimeout = 30 * time.Second
@@ -1167,11 +1176,43 @@ func (hc *HealthChecker) allCategories() []category {
 						return &SkipError{Reason: "not run for non HA installs"}
 					},
 				},
+				{
+					description:   "multiple replicas of control plane pods",
+					hintAnchor:    "l5d-control-plane-replicas",
+					retryDeadline: hc.RetryDeadline,
+					warning:       true,
+					check: func(ctx context.Context) error {
+						if hc.isHA() {
+							return hc.checkMinReplicasAvailable()
+						}
+						return &SkipError{Reason: "not run for non HA installs"}
+					},
+				},
 			},
 		},
 	}
 }
 
+func (hc *HealthChecker) checkMinReplicasAvailable() error {
+	faulty := []string{}
+
+	for _, component := range linkerdHAControlPlaneComponents {
+		conf, err := hc.kubeAPI.AppsV1().Deployments(hc.ControlPlaneNamespace).Get(component, metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
+
+		if conf.Status.AvailableReplicas <= 1 {
+			faulty = append(faulty, component)
+		}
+	}
+
+	if len(faulty) > 0 {
+		return fmt.Errorf("not enough replicas available for %v", faulty)
+	}
+	return nil
+}
+
 func (hc *HealthChecker) issuerIdentity() string {
 	return fmt.Sprintf("identity.%s.%s", hc.ControlPlaneNamespace, hc.linkerdConfig.Global.IdentityContext.TrustDomain)
 }
diff --git a/web/Dockerfile b/web/Dockerfile
index 6de090f67..439831140 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -21,7 +21,7 @@ COPY web/app ./web/app
 RUN ./bin/web build
 
 ## compile go server
-FROM gcr.io/linkerd-io/go-deps:8d2ce4bf as golang
+FROM gcr.io/linkerd-io/go-deps:93ea34a8 as golang
 WORKDIR /linkerd-build
 RUN mkdir -p web
 COPY web/main.go web
diff --git a/web/app/.eslintrc b/web/app/.eslintrc
index 852a1cabd..82d6db977 100644
--- a/web/app/.eslintrc
+++ b/web/app/.eslintrc
@@ -83,7 +83,6 @@
     "react/jsx-props-no-spreading": 0,
 
     // These rules represent best practices that we're not adhering to, and should be enabled
-    "jsx-a11y/click-events-have-key-events": 0,
-    "no-param-reassign": 0
+    "no-param-reassign": ["error", { "props": false }]
   }
 }
diff --git a/web/app/js/components/Popover.jsx b/web/app/js/components/Popover.jsx
index 763d3c4b0..50f815149 100644
--- a/web/app/js/components/Popover.jsx
+++ b/web/app/js/components/Popover.jsx
@@ -21,6 +21,13 @@ class ClickablePopover extends React.Component {
     this.setState({ anchorEl: event.currentTarget });
   };
 
+  handleKeyPress = event => {
+    if (event.key === 'Enter') {
+      this.setState({ anchorEl: event.currentTarget });
+    }
+  }
+
+
   handleClose = () => {
     this.setState({ anchorEl: null });
   };
@@ -36,6 +43,7 @@ class ClickablePopover extends React.Component {
           aria-owns={open ? 'clickable-popover' : null}
           aria-haspopup="true"
           onClick={this.handleClick}
+          onKeyPress={this.handleKeyPress}
           role="button"
           tabIndex={0}>
           {baseContent}
diff --git a/web/app/js/components/util/ApiHelpers.jsx b/web/app/js/components/util/ApiHelpers.jsx
index 0ccea34e6..99a27dd0a 100644
--- a/web/app/js/components/util/ApiHelpers.jsx
+++ b/web/app/js/components/util/ApiHelpers.jsx
@@ -70,29 +70,32 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
 
   // for getting json api results
   const apiFetch = path => {
+    let path_ = path;
     if (!_isEmpty(pathPrefix)) {
-      path = `${pathPrefix}${path}`;
+      path_ = `${pathPrefix}${path_}`;
     }
 
-    return makeCancelable(fetch(path), r => r.json());
+    return makeCancelable(fetch(path_), r => r.json());
   };
 
   // for getting yaml api results
   const apiFetchYAML = path => {
+    let path_ = path;
     if (!_isEmpty(pathPrefix)) {
-      path = `${pathPrefix}${path}`;
+      path_ = `${pathPrefix}${path_}`;
     }
 
-    return makeCancelable(fetch(path), r => r.text());
+    return makeCancelable(fetch(path_), r => r.text());
   };
 
   // for getting non-json results
   const prefixedUrl = path => {
+    let path_ = path;
     if (!_isEmpty(pathPrefix)) {
-      path = `${pathPrefix}${path}`;
+      path_ = `${pathPrefix}${path_}`;
     }
 
-    return path;
+    return path_;
   };
 
   const getMetricsWindow = () => metricsWindow;
@@ -104,14 +107,15 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
   };
 
   const fetchMetrics = path => {
-    if (path.indexOf('window') === -1) {
-      if (path.indexOf('?') === -1) {
-        path = `${path}?window=${getMetricsWindow()}`;
+    let path_ = path;
+    if (path_.indexOf('window') === -1) {
+      if (path_.indexOf('?') === -1) {
+        path_ = `${path_}?window=${getMetricsWindow()}`;
       } else {
-        path = `${path}&window=${getMetricsWindow()}`;
+        path_ = `${path_}&window=${getMetricsWindow()}`;
       }
     }
-    return apiFetch(path);
+    return apiFetch(path_);
   };
 
   const fetchPods = namespace => {
diff --git a/web/app/js/components/util/TapUtils.jsx b/web/app/js/components/util/TapUtils.jsx
index bb3eee6cd..26d0e5676 100644
--- a/web/app/js/components/util/TapUtils.jsx
+++ b/web/app/js/components/util/TapUtils.jsx
@@ -159,12 +159,13 @@ const tapEventKey = (d, eventType) => {
   produce octets given an ip address
 */
 const decodeIPToOctets = ip => {
-  ip = parseInt(ip, 10);
+  const ip_ = parseInt(ip, 10);
+
   return [
-    (ip >> 24) & 255,
-    (ip >> 16) & 255,
-    (ip >> 8) & 255,
-    ip & 255,
+    (ip_ >> 24) & 255,
+    (ip_ >> 16) & 255,
+    (ip_ >> 8) & 255,
+    ip_ & 255,
   ];
 };
 
diff --git a/web/app/js/components/util/Utils.js b/web/app/js/components/util/Utils.js
index 2b6d882a0..4a3c21295 100644
--- a/web/app/js/components/util/Utils.js
+++ b/web/app/js/components/util/Utils.js
@@ -54,8 +54,9 @@ export const formatLatencyMs = m => {
 * Add commas to a number (converting it to a string in the process)
 */
 export function addCommas(nStr) {
-  nStr += '';
-  const x = nStr.split('.');
+  let nStr_ = nStr;
+  nStr_ += '';
+  const x = nStr_.split('.');
   let x1 = x[0];
   const x2 = x.length > 1 ? `.${x[1]}` : '';
   const rgx = /(\d+)(\d{3})/;
@@ -76,25 +77,26 @@ export const roundNumber = (num, dec) => {
 * Shorten and style number
 */
 export const styleNum = (number, unit = '', truncate = true) => {
-  if (Number.isNaN(number)) {
+  let number_ = number;
+  if (Number.isNaN(number_)) {
     return 'N/A';
   }
 
-  if (truncate && number > 999999999) {
-    number = roundNumber(number / 1000000000.0, 3);
-    return `${addCommas(number)}G${unit}`;
-  } else if (truncate && number > 999999) {
-    number = roundNumber(number / 1000000.0, 3);
-    return `${addCommas(number)}M${unit}`;
-  } else if (truncate && number > 999) {
-    number = roundNumber(number / 1000.0, 3);
-    return `${addCommas(number)}k${unit}`;
-  } else if (number > 999) {
-    number = roundNumber(number, 0);
-    return addCommas(number) + unit;
+  if (truncate && number_ > 999999999) {
+    number_ = roundNumber(number_ / 1000000000.0, 3);
+    return `${addCommas(number_)}G${unit}`;
+  } else if (truncate && number_ > 999999) {
+    number_ = roundNumber(number_ / 1000000.0, 3);
+    return `${addCommas(number_)}M${unit}`;
+  } else if (truncate && number_ > 999) {
+    number_ = roundNumber(number_ / 1000.0, 3);
+    return `${addCommas(number_)}k${unit}`;
+  } else if (number_ > 999) {
+    number_ = roundNumber(number_, 0);
+    return addCommas(number_) + unit;
   } else {
-    number = roundNumber(number, 2);
-    return addCommas(number) + unit;
+    number_ = roundNumber(number_, 2);
+    return addCommas(number_) + unit;
   }
 };
 
@@ -120,9 +122,9 @@ export const toClassName = name => {
 */
 export const regexFilterString = input => {
   // make input lower case and strip out unwanted characters
-  input = input.replace(/[^A-Z0-9/.\-_*]/gi, '').toLowerCase();
+  const input_ = input.replace(/[^A-Z0-9/.\-_*]/gi, '').toLowerCase();
   // replace "*" in input with wildcard
-  return new RegExp(input.replace(/[*]/g, '.+'));
+  return new RegExp(input_.replace(/[*]/g, '.+'));
 };
 
 /*
@@ -218,12 +220,12 @@ export const isResource = name => {
   produce octets given an ip address
 */
 const decodeIPToOctets = ip => {
-  ip = parseInt(ip, 10);
+  const ip_ = parseInt(ip, 10);
   return [
-    (ip >> 24) & 255,
-    (ip >> 16) & 255,
-    (ip >> 8) & 255,
-    ip & 255,
+    (ip_ >> 24) & 255,
+    (ip_ >> 16) & 255,
+    (ip_ >> 8) & 255,
+    ip_ & 255,
   ];
 };
 


Test patch:
diff --git a/bin/test-cleanup b/bin/test-cleanup
index 622b67302..4e84ddb4e 100755
--- a/bin/test-cleanup
+++ b/bin/test-cleanup
@@ -14,6 +14,12 @@ if [ -z "${namespaces_helm=$(kubectl --context=$k8s_context get ns -oname -l lin
   echo 'no helm namespaces found' >&2
 fi
 
+echo "cleaning up CNI namespaces in k8s-context [${k8s_context}]"
+
+if [ -z "${namespaces_cni=$(kubectl --context=$k8s_context get ns -oname -l linkerd.io/cni-resource)}" ]; then
+  echo 'no CNI namespaces found' >&2
+fi
+
 echo "cleaning up data-plane namespaces in k8s-context [${k8s_context}]"
 
 if [ -z "${namespaces_dataplane=$(kubectl --context=$k8s_context get ns -oname -l linkerd.io/is-test-data-plane)}" ]; then
@@ -52,8 +58,8 @@ if [ -z "${apiservices=$(kubectl --context=$k8s_context get apiservices -l linke
   echo 'no apiservices found' >&2
 fi
 
-if [[ $namespaces_controlplane || $namespaces_helm || $namespaces_dataplane || $clusterrolebindings || $clusterrolebindings_helm || $clusterroles || $webhookconfigs || $validatingconfigs || $podsecuritypolicies || $customresourcedefinitions || $apiservices ]]; then
-  kubectl --context=$k8s_context delete $namespaces_controlplane $namespaces_helm $namespaces_dataplane $clusterrolebindings $clusterrolebindings_helm $clusterroles $webhookconfigs $validatingconfigs $podsecuritypolicies $customresourcedefinitions $apiservices
+if [[ $namespaces_controlplane || $namespaces_cni || $namespaces_helm || $namespaces_dataplane || $clusterrolebindings || $clusterrolebindings_helm || $clusterroles || $webhookconfigs || $validatingconfigs || $podsecuritypolicies || $customresourcedefinitions || $apiservices ]]; then
+  kubectl --context=$k8s_context delete $namespaces_controlplane $namespaces_cni $namespaces_helm $namespaces_dataplane $clusterrolebindings $clusterrolebindings_helm $clusterroles $webhookconfigs $validatingconfigs $podsecuritypolicies $customresourcedefinitions $apiservices
 fi
 
 echo "cleaning up rolebindings in kube-system namespace in k8s-context [${k8s_context}]"
diff --git a/cli/cmd/install_cni_helm_test.go b/cli/cmd/install_cni_helm_test.go
index 98f9129a3..10a55c97b 100644
--- a/cli/cmd/install_cni_helm_test.go
+++ b/cli/cmd/install_cni_helm_test.go
@@ -34,8 +34,7 @@ func testRenderCniHelm(t *testing.T, chart *pb.Chart, goldenFileName string) {
 	overrideJSON :=
 		`{
 			"namespace": "linkerd-test",
-  			"controllerNamespaceLabel": "linkerd.io/control-plane-ns-test",
-  			"cniResourceAnnotation": "linkerd.io/cni-resource-test",
+  			"cniResourceLabel": "linkerd.io/cni-resource-test",
   			"inboundProxyPort": 1234,
   			"outboundProxyPort": 5678,
 			"createdByAnnotation": "linkerd.io/created-by-test",
diff --git a/cli/cmd/testdata/install-cni-plugin_default.golden b/cli/cmd/testdata/install-cni-plugin_default.golden
index 10237b8be..9985df0d6 100644
--- a/cli/cmd/testdata/install-cni-plugin_default.golden
+++ b/cli/cmd/testdata/install-cni-plugin_default.golden
@@ -5,7 +5,7 @@ metadata:
   annotations:
     linkerd.io/inject: disabled
   labels:
-    linkerd.io/is-control-plane: "true"
+    linkerd.io/cni-resource: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
 apiVersion: policy/v1beta1
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
index e2c177477..2f9314d9a 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
@@ -5,7 +5,7 @@ metadata:
   annotations:
     linkerd.io/inject: disabled
   labels:
-    linkerd.io/is-control-plane: "true"
+    linkerd.io/cni-resource: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
 apiVersion: policy/v1beta1
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
index 996fe69a3..8a9a422e3 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
@@ -5,7 +5,7 @@ metadata:
   annotations:
     linkerd.io/inject: disabled
   labels:
-    linkerd.io/is-control-plane: "true"
+    linkerd.io/cni-resource: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
 apiVersion: policy/v1beta1
diff --git a/cli/cmd/testdata/install_cni_helm_output.golden b/cli/cmd/testdata/install_cni_helm_output.golden
index 4a3a08274..4b178da9b 100644
--- a/cli/cmd/testdata/install_cni_helm_output.golden
+++ b/cli/cmd/testdata/install_cni_helm_output.golden
@@ -7,7 +7,7 @@ metadata:
   annotations:
     linkerd.io/inject: disabled
   labels:
-    linkerd.io/is-control-plane: "true"
+    linkerd.io/cni-resource-test: "true"
     config.linkerd.io/admission-webhooks: disabled
 ---
 apiVersion: policy/v1beta1
diff --git a/controller/api/destination/server_test.go b/controller/api/destination/server_test.go
index 086b8b246..b8b3fd796 100644
--- a/controller/api/destination/server_test.go
+++ b/controller/api/destination/server_test.go
@@ -226,9 +226,12 @@ func TestGetProfiles(t *testing.T) {
 		// gets the server profile first, it will send an update with that
 		// profile to the stream and then a second update when it gets the
 		// client profile.
-		if len(stream.updates) != 1 && len(stream.updates) != 2 {
-			// https://github.com/linkerd/linkerd2/issues/3332
-			t.Skipf("Expected 1 or 2 updates but got %d: %v", len(stream.updates), stream.updates)
+		// Additionally, under normal conditions the creation of resources by
+		// the fake API will generate notifications that are discarded after the
+		// stream.Cancel() call, but very rarely those notifications might come
+		// after, in which case we'll get a third update.
+		if len(stream.updates) == 0 || len(stream.updates) > 3 {
+			t.Fatalf("Expected 1 to 3 updates but got %d: %v", len(stream.updates), stream.updates)
 		}
 		lastUpdate := stream.updates[len(stream.updates)-1]
 		routes := lastUpdate.GetRoutes()
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index 1f45532c9..0ce06776e 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -3029,3 +3029,125 @@ func TestCniChecks(t *testing.T) {
 	}
 
 }
+
+func TestMinReplicaCheck(t *testing.T) {
+	hc := NewHealthChecker(
+		[]CategoryID{LinkerdHAChecks},
+		&Options{
+			ControlPlaneNamespace: "linkerd",
+		},
+	)
+
+	var err error
+
+	testCases := []struct {
+		controlPlaneResourceDefs []string
+		expected                 error
+	}{
+		{
+			controlPlaneResourceDefs: generateAllControlPlaneDef(&controlPlaneReplicaOptions{
+				controller:    1,
+				destination:   3,
+				identity:      3,
+				proxyInjector: 3,
+				spValidator:   1,
+				tap:           3,
+			}, t),
+			expected: fmt.Errorf("not enough replicas available for [linkerd-controller linkerd-sp-validator]"),
+		},
+		{
+			controlPlaneResourceDefs: generateAllControlPlaneDef(&controlPlaneReplicaOptions{
+				controller:    3,
+				destination:   2,
+				identity:      1,
+				proxyInjector: 1,
+				spValidator:   0,
+				tap:           3,
+			}, t),
+			expected: fmt.Errorf("not enough replicas available for [linkerd-identity linkerd-proxy-injector linkerd-sp-validator]"),
+		},
+		{
+			controlPlaneResourceDefs: generateAllControlPlaneDef(&controlPlaneReplicaOptions{
+				controller:    3,
+				destination:   2,
+				identity:      2,
+				proxyInjector: 3,
+				spValidator:   2,
+				tap:           3,
+			}, t),
+			expected: nil,
+		},
+	}
+
+	for i, tc := range testCases {
+		tc := tc //pin
+		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+			hc.kubeAPI, err = k8s.NewFakeAPI(tc.controlPlaneResourceDefs...)
+			if err != nil {
+				t.Fatal(err)
+			}
+			err = hc.checkMinReplicasAvailable()
+			if err == nil && tc.expected != nil {
+				t.Log("Expected error: nil")
+				t.Logf("Received error: %s\n", err)
+				t.Fatal("test case failed")
+			}
+			if err != nil {
+				if err.Error() != tc.expected.Error() {
+					t.Logf("Expected error: %s\n", tc.expected)
+					t.Logf("Received error: %s\n", err)
+					t.Fatal("test case failed")
+				}
+			}
+		})
+	}
+}
+
+type controlPlaneReplicaOptions struct {
+	controller    int
+	destination   int
+	identity      int
+	proxyInjector int
+	spValidator   int
+	tap           int
+}
+
+func getSingleControlPlaneDef(component string, availableReplicas int) string {
+	return fmt.Sprintf(`
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: %s
+  namespace: linkerd
+spec:
+  template:
+    spec:
+      containers:
+        - image: "hello-world"
+          name: test
+status:
+  availableReplicas: %d`, component, availableReplicas)
+}
+
+func generateAllControlPlaneDef(replicaOptions *controlPlaneReplicaOptions, t *testing.T) []string {
+	resourceDefs := []string{}
+	for _, component := range linkerdHAControlPlaneComponents {
+		switch component {
+		case "linkerd-controller":
+			resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.controller))
+		case "linkerd-destination":
+			resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.destination))
+		case "linkerd-identity":
+			resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.identity))
+		case "linkerd-sp-validator":
+			resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.spValidator))
+		case "linkerd-proxy-injector":
+			resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.proxyInjector))
+		case "linkerd-tap":
+			resourceDefs = append(resourceDefs, getSingleControlPlaneDef(component, replicaOptions.tap))
+		default:
+			t.Fatal("Could not find the resource")
+		}
+	}
+	return resourceDefs
+}
diff --git a/test/egress/egress_test.go b/test/egress/egress_test.go
index d942ebfd2..2216db3cc 100644
--- a/test/egress/egress_test.go
+++ b/test/egress/egress_test.go
@@ -1,10 +1,8 @@
 package egress
 
 import (
-	"encoding/json"
 	"fmt"
 	"os"
-	"strings"
 	"testing"
 
 	"github.com/linkerd/linkerd2/testutil"
@@ -16,14 +14,6 @@ import (
 
 var TestHelper *testutil.TestHelper
 
-var egressHTTPDeployments = []string{
-	"egress-test-https-post",
-	"egress-test-http-post",
-	"egress-test-https-get",
-	"egress-test-http-get",
-	"egress-test-not-www-get",
-}
-
 func TestMain(m *testing.M) {
 	TestHelper = testutil.NewTestHelper()
 	os.Exit(m.Run())
@@ -33,14 +23,6 @@ func TestMain(m *testing.M) {
 /// TEST EXECUTION ///
 //////////////////////
 
-// issue: https://github.com/linkerd/linkerd2/issues/2316
-//
-// The response from `http://httpbin.org/get` is non-deterministic--returning
-// either `http://..` or `https://..` for GET requests. As #2316 mentions,
-// this test should not have an external dependency on this endpoint. As a
-// workaround for edge-20.1.3, temporarily expect either `http` or `https` so
-// that the test is not completely disabled.
-
 func TestEgressHttp(t *testing.T) {
 	out, stderr, err := TestHelper.LinkerdRun("inject", "testdata/proxy.yaml")
 	if err != nil {
@@ -57,43 +39,31 @@ func TestEgressHttp(t *testing.T) {
 		t.Fatalf("Unexpected error: %v output:\n%s", err, out)
 	}
 
-	for _, deploy := range egressHTTPDeployments {
-		err = TestHelper.CheckPods(prefixedNs, deploy, 1)
-		if err != nil {
-			t.Fatalf("Unexpected error: %v", err)
-		}
+	err = TestHelper.CheckPods(prefixedNs, "egress-test", 1)
+	if err != nil {
+		t.Fatalf("Unexpected error: %v", err)
 	}
 
-	testCase := func(deployName, dnsName, protocolToUse, methodToUse string) {
-		testName := fmt.Sprintf("Can use egress to send %s request to %s (%s)", methodToUse, protocolToUse, deployName)
+	testCase := func(url, methodToUse string) {
+		testName := fmt.Sprintf("Can use egress to send %s request to (%s)", methodToUse, url)
 		t.Run(testName, func(t *testing.T) {
-			expectedURL := fmt.Sprintf("%s://%s/%s", protocolToUse, dnsName, strings.ToLower(methodToUse))
-
-			url, err := TestHelper.URLFor(prefixedNs, deployName, 8080)
-			if err != nil {
-				t.Fatalf("Failed to get proxy URL: %s", err)
+			cmd := []string{
+				"-n", prefixedNs, "exec", "deploy/egress-test", "-c", "http-egress",
+				"--", "curl", "-sko", "/dev/null", "-w", "%{http_code}", "-X", methodToUse, url,
 			}
-
-			output, err := TestHelper.HTTPGetURL(url)
+			out, err := TestHelper.Kubectl("", cmd...)
 			if err != nil {
-				t.Fatalf("Unexpected error: %v", err)
+				t.Fatalf("Failed to exec %s: %s (%s)", cmd, err, out)
 			}
-			var jsonResponse map[string]interface{}
-			json.Unmarshal([]byte(output), &jsonResponse)
 
-			payloadText := jsonResponse["payload"]
-			if payloadText == nil {
-				t.Fatalf("Expected [%s] request to [%s] to return a payload, got nil. Response:\n%s\n", methodToUse, expectedURL, output)
+			var status int
+			_, err = fmt.Sscanf(out, "%d", &status)
+			if err != nil {
+				t.Fatalf("Failed to parse status code (%s): %s", out, err)
 			}
 
-			var messagePayload map[string]interface{}
-			json.Unmarshal([]byte(payloadText.(string)), &messagePayload)
-
-			expectedResponseURL1 := fmt.Sprintf("http://%s/%s", dnsName, strings.ToLower(methodToUse))
-			expectedResponseURL2 := fmt.Sprintf("https://%s/%s", dnsName, strings.ToLower(methodToUse))
-			actualURL := messagePayload["url"]
-			if actualURL != expectedResponseURL1 && actualURL != expectedResponseURL2 {
-				t.Fatalf("Expecting response to say egress sent [%s] request to be either URL [%s] or URL [%s], but got [%s]. Response:\n%s\n", methodToUse, expectedResponseURL1, expectedResponseURL2, actualURL, output)
+			if status < 100 || status >= 500 {
+				t.Fatalf("Got HTTP error code: %d\n", status)
 			}
 		})
 	}
@@ -102,11 +72,11 @@ func TestEgressHttp(t *testing.T) {
 	methods := []string{"GET", "POST"}
 	for _, protocolToUse := range supportedProtocols {
 		for _, methodToUse := range methods {
-			serviceName := fmt.Sprintf("egress-test-%s-%s", protocolToUse, strings.ToLower(methodToUse))
-			testCase(serviceName, "www.httpbin.org", protocolToUse, methodToUse)
+			serviceName := fmt.Sprintf("%s://www.linkerd.io", protocolToUse)
+			testCase(serviceName, methodToUse)
 		}
 	}
 
 	// Test egress for a domain with fewer than 3 segments.
-	testCase("egress-test-not-www-get", "httpbin.org", "https", "GET")
+	testCase("http://linkerd.io", "GET")
 }
diff --git a/test/egress/testdata/proxy.yaml b/test/egress/testdata/proxy.yaml
index f222064b5..e7c134873 100644
--- a/test/egress/testdata/proxy.yaml
+++ b/test/egress/testdata/proxy.yaml
@@ -1,203 +1,18 @@
-#POST
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: egress-test-https-post
+  name: egress-test
 spec:
   replicas: 1
   selector:
     matchLabels:
-      app: egress-test-https-post
+      app: egress-test
   template:
     metadata:
       labels:
-        app: egress-test-https-post
+        app: egress-test
     spec:
       containers:
-      - name: http-egress-https-post
-        image: buoyantio/bb:v0.0.5
-        args:
-          - "http-egress"
-          - "--h1-server-port"
-          - "8080"
-          - "--url"
-          - "https://www.httpbin.org/post"
-          - "--method"
-          - "POST"
-        ports:
-        - containerPort: 8080
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: egress-test-https-post-svc
-spec:
-  selector:
-    app: egress-test-https-post
-  ports:
-  - name: http
-    port: 8080
-    targetPort: 8080
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: egress-test-http-post
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: egress-test-http-post
-  template:
-    metadata:
-      labels:
-        app: egress-test-http-post
-    spec:
-      containers:
-      - name: http-egress-http-post
-        image: buoyantio/bb:v0.0.5
-        args:
-          - "http-egress"
-          - "--h1-server-port"
-          - "8080"
-          - "--url"
-          - "http://www.httpbin.org/post"
-          - "--method"
-          - "POST"
-        ports:
-        - containerPort: 8080
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: egress-test-http-post-svc
-spec:
-  selector:
-    app: egress-test-http-post
-  ports:
-  - name: http
-    port: 8080
-    targetPort: 8080
-#GET
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: egress-test-https-get
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: egress-test-https-get
-  template:
-    metadata:
-      labels:
-        app: egress-test-https-get
-    spec:
-      containers:
-      - name: http-egress-https-get
-        image: buoyantio/bb:v0.0.5
-        args:
-          - "http-egress"
-          - "--h1-server-port"
-          - "8080"
-          - "--url"
-          - "https://www.httpbin.org/get"
-          - "--method"
-          - "GET"
-        ports:
-        - containerPort: 8080
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: egress-test-https-get-svc
-spec:
-  selector:
-    app: egress-test-https-get
-  ports:
-  - name: http
-    port: 8080
-    targetPort: 8080
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: egress-test-http-get
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: egress-test-http-get
-  template:
-    metadata:
-      labels:
-        app: egress-test-http-get
-    spec:
-      containers:
-      - name: http-egress-http-get
-        image: buoyantio/bb:v0.0.5
-        args:
-          - "http-egress"
-          - "--h1-server-port"
-          - "8080"
-          - "--url"
-          - "http://www.httpbin.org/get"
-          - "--method"
-          - "GET"
-        ports:
-        - containerPort: 8080
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: egress-test-http-get-svc
-spec:
-  selector:
-    app: egress-test-http-get
-  ports:
-  - name: http
-    port: 8080
-    targetPort: 8080
-## Non www domains
----
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: egress-test-not-www-get
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: egress-test-not-www-get
-  template:
-    metadata:
-      labels:
-        app: egress-test-not-www-get
-    spec:
-      containers:
-      - name: egress-test-not-www-get
-        image: buoyantio/bb:v0.0.5
-        args:
-          - "http-egress"
-          - "--h1-server-port"
-          - "8080"
-          - "--url"
-          - "https://httpbin.org/get"
-          - "--method"
-          - "GET"
-        ports:
-        - containerPort: 8080
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: egress-test-not-www-get-svc
-spec:
-  selector:
-    app: egress-test-not-www-get
-  ports:
-  - name: http
-    port: 8080
-    targetPort: 8080
+      - name: http-egress
+        image: gcr.io/linkerd-io/debug:stable-2.6.1
diff --git a/test/externalissuer/external_issuer_test.go b/test/externalissuer/external_issuer_test.go
index 12a03e4a2..75e97f7c8 100644
--- a/test/externalissuer/external_issuer_test.go
+++ b/test/externalissuer/external_issuer_test.go
@@ -52,13 +52,12 @@ func verifyInstallApp(t *testing.T) {
 		t.Fatalf("kubectl apply command failed\n%s", out)
 	}
 
-	// wait for deployment to start
 	if err := TestHelper.CheckPods(prefixedNs, TestAppBackendDeploymentName, 1); err != nil {
 		t.Error(err)
 	}
 
-	if err := TestHelper.CheckDeployment(prefixedNs, TestAppBackendDeploymentName, 1); err != nil {
-		t.Error(fmt.Errorf("Error validating deployment [%s]:\n%s", TestAppBackendDeploymentName, err))
+	if err := TestHelper.CheckPods(prefixedNs, "slow-cooker", 1); err != nil {
+		t.Error(err)
 	}
 }
 
diff --git a/test/install_test.go b/test/install_test.go
index a049475f0..a4fac2037 100644
--- a/test/install_test.go
+++ b/test/install_test.go
@@ -92,7 +92,7 @@ var (
 		`(Liveness|Readiness) probe failed: Get http://.*: dial tcp .*: connect: connection refused`,
 		`(Liveness|Readiness) probe failed: Get http://.*: read tcp .*: read: connection reset by peer`,
 		`(Liveness|Readiness) probe failed: Get http://.*: net/http: request canceled .*\(Client\.Timeout exceeded while awaiting headers\)`,
-		`Failed to update endpoint .*-upgrade/linkerd-.*: Operation cannot be fulfilled on endpoints "linkerd-.*": the object has been modified; please apply your changes to the latest version and try again`,
+		`Failed to update endpoint .*/linkerd-.*: Operation cannot be fulfilled on endpoints "linkerd-.*": the object has been modified; please apply your changes to the latest version and try again`,
 		`error killing pod: failed to "KillPodSandbox" for ".*" with KillPodSandboxError: "rpc error: code = Unknown desc = failed to destroy network for sandbox \\".*\\": could not teardown (ipv4|ipv6) dnat: running \[/usr/sbin/(iptables|ip6tables) -t nat -X CNI-DN-.* --wait\]: exit status 1: (iptables|ip6tables): No chain/target/match by that name\.\\n"`,
 	}, "|"))
 
@@ -334,15 +334,14 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 // These need to be updated (if there are changes) once a new stable is released
 func helmOverridesStable(root *tls.CA) []string {
 	return []string{
-		"--set", "Namespace=" + TestHelper.GetLinkerdNamespace(),
-		"--set", "ControllerLogLevel=debug",
-		"--set", "LinkerdVersion=" + TestHelper.UpgradeHelmFromVersion(),
-		"--set", "Proxy.Image.Version=" + TestHelper.UpgradeHelmFromVersion(),
-		"--set", "Identity.TrustDomain=cluster.local",
-		"--set", "Identity.TrustAnchorsPEM=" + root.Cred.Crt.EncodeCertificatePEM(),
-		"--set", "Identity.Issuer.TLS.CrtPEM=" + root.Cred.Crt.EncodeCertificatePEM(),
-		"--set", "Identity.Issuer.TLS.KeyPEM=" + root.Cred.EncodePrivateKeyPEM(),
-		"--set", "Identity.Issuer.CrtExpiry=" + root.Cred.Crt.Certificate.NotAfter.Format(time.RFC3339),
+		"--set", "controllerLogLevel=debug",
+		"--set", "global.linkerdVersion=" + TestHelper.UpgradeHelmFromVersion(),
+		"--set", "global.proxy.image.version=" + TestHelper.UpgradeHelmFromVersion(),
+		"--set", "global.identityTrustDomain=cluster.local",
+		"--set", "global.identityTrustAnchorsPEM=" + root.Cred.Crt.EncodeCertificatePEM(),
+		"--set", "identity.issuer.tls.crtPEM=" + root.Cred.Crt.EncodeCertificatePEM(),
+		"--set", "identity.issuer.tls.keyPEM=" + root.Cred.EncodePrivateKeyPEM(),
+		"--set", "identity.issuer.crtExpiry=" + root.Cred.Crt.Certificate.NotAfter.Format(time.RFC3339),
 	}
 }
 
@@ -755,9 +754,7 @@ func TestEvents(t *testing.T) {
 		}
 
 		evtStr := fmt.Sprintf("Reason: [%s] Object: [%s] Message: [%s]", e.Reason, e.InvolvedObject.Name, e.Message)
-		if knownEventWarningsRegex.MatchString(e.Message) {
-			t.Logf("Found known warning event: %s", evtStr)
-		} else {
+		if !knownEventWarningsRegex.MatchString(e.Message) {
 			unknownEvents = append(unknownEvents, evtStr)
 		}
 	}
diff --git a/testutil/kubernetes_helper.go b/testutil/kubernetes_helper.go
index 8176617d6..eaa332eee 100644
--- a/testutil/kubernetes_helper.go
+++ b/testutil/kubernetes_helper.go
@@ -107,7 +107,7 @@ func (h *KubernetesHelper) KubectlApply(stdin string, namespace string) (string,
 
 // Kubectl executes an arbitrary Kubectl command
 func (h *KubernetesHelper) Kubectl(stdin string, arg ...string) (string, error) {
-	withContext := append(arg, "--context="+h.k8sContext)
+	withContext := append([]string{"--context=" + h.k8sContext}, arg...)
 	cmd := exec.Command("kubectl", withContext...)
 	cmd.Stdin = strings.NewReader(stdin)
 	out, err := cmd.CombinedOutput()