From 3dfb02eb141e41bc1c420a974bea7cf625b773ab Mon Sep 17 00:00:00 2001
From: Ruslan Aliev <raliev@mirantis.com>
Date: Mon, 28 Jun 2021 15:49:12 -0500
Subject: [PATCH] Extend poller capabilities

This patch allows to wait for certain state when applying
particular resources.

Change-Id: I064cb49c8971f1edee870bc6c3c3dd1e428c73f0
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
Closes: #624
---
 go.mod                                        |   4 +-
 go.sum                                        |  13 +-
 .../v1.15.0/custom-resources.yaml             |   5 +
 .../function/phase-helpers/kustomization.yaml |   1 -
 .../wait_tigera/kubectl_wait_tigera.sh        |  29 --
 .../wait_tigera/kustomization.yaml            |   6 -
 manifests/phases/executors.yaml               |  18 +
 .../docker-test-site/phases/plan_patch.yaml   |   1 -
 .../hostgenerator/host-generation.yaml        |   1 -
 .../target/controlplane/kustomization.yaml    |   2 +-
 manifests/type/gating/phases/plan.yaml        |  10 -
 pkg/api/v1alpha1/kubernetes_apply_types.go    |  11 +-
 pkg/api/v1alpha1/zz_generated.deepcopy.go     |  25 +-
 pkg/cluster/command.go                        |  32 --
 pkg/cluster/command_test.go                   |  88 -----
 pkg/cluster/errors.go                         |  37 --
 pkg/cluster/expression_test.go                | 102 ------
 pkg/cluster/status.go                         | 336 ------------------
 pkg/cluster/status_test.go                    | 319 -----------------
 pkg/cluster/testdata/statusmap/crd.yaml       |  40 ---
 .../testdata/statusmap/kustomization.yaml     |   8 -
 .../testdata/statusmap/legacy-crd.yaml        |  42 ---
 .../testdata/statusmap/legacy-resource.yaml   |   7 -
 pkg/cluster/testdata/statusmap/missing.yaml   |   7 -
 .../testdata/statusmap/pending-resource.yaml  |   7 -
 .../testdata/statusmap/stable-resource.yaml   |   7 -
 pkg/cluster/testdata/statusmap/unknown.yaml   |   8 -
 pkg/k8s/applier/applier.go                    |  21 +-
 pkg/k8s/applier/applier_test.go               |   2 +-
 pkg/k8s/client/client.go                      | 135 -------
 pkg/k8s/client/client_test.go                 |  52 ---
 pkg/k8s/client/fake/fake.go                   | 146 --------
 pkg/k8s/client/testdata/kubeconfig.yaml       |  19 -
 pkg/k8s/poller/cluster_reader.go              |  70 ++++
 pkg/k8s/poller/cluster_reader_test.go         | 246 +++++++++++++
 pkg/{cluster => k8s/poller}/expression.go     |  36 +-
 pkg/k8s/poller/expression_test.go             |  77 ++++
 pkg/k8s/poller/poller.go                      | 122 +++----
 pkg/k8s/poller/poller_test.go                 | 301 +++++++++++++++-
 pkg/k8s/poller/status.go                      | 182 ++++++++++
 pkg/k8s/poller/status_test.go                 | 113 ++++++
 pkg/k8s/poller/testdata/kubeconfig.yaml       |  19 -
 pkg/phase/executors/k8s_applier.go            |   3 +-
 43 files changed, 1123 insertions(+), 1587 deletions(-)
 delete mode 100644 manifests/function/phase-helpers/wait_tigera/kubectl_wait_tigera.sh
 delete mode 100644 manifests/function/phase-helpers/wait_tigera/kustomization.yaml
 delete mode 100755 pkg/cluster/command_test.go
 delete mode 100644 pkg/cluster/errors.go
 delete mode 100644 pkg/cluster/expression_test.go
 delete mode 100644 pkg/cluster/status.go
 delete mode 100644 pkg/cluster/status_test.go
 delete mode 100644 pkg/cluster/testdata/statusmap/crd.yaml
 delete mode 100644 pkg/cluster/testdata/statusmap/kustomization.yaml
 delete mode 100644 pkg/cluster/testdata/statusmap/legacy-crd.yaml
 delete mode 100644 pkg/cluster/testdata/statusmap/legacy-resource.yaml
 delete mode 100644 pkg/cluster/testdata/statusmap/missing.yaml
 delete mode 100644 pkg/cluster/testdata/statusmap/pending-resource.yaml
 delete mode 100644 pkg/cluster/testdata/statusmap/stable-resource.yaml
 delete mode 100644 pkg/cluster/testdata/statusmap/unknown.yaml
 delete mode 100644 pkg/k8s/client/client.go
 delete mode 100644 pkg/k8s/client/client_test.go
 delete mode 100644 pkg/k8s/client/fake/fake.go
 delete mode 100644 pkg/k8s/client/testdata/kubeconfig.yaml
 create mode 100755 pkg/k8s/poller/cluster_reader.go
 create mode 100755 pkg/k8s/poller/cluster_reader_test.go
 rename pkg/{cluster => k8s/poller}/expression.go (59%)
 mode change 100644 => 100755
 create mode 100755 pkg/k8s/poller/expression_test.go
 create mode 100755 pkg/k8s/poller/status.go
 create mode 100755 pkg/k8s/poller/status_test.go
 delete mode 100755 pkg/k8s/poller/testdata/kubeconfig.yaml

diff --git a/go.mod b/go.mod
index ccedc5d85..82c479432 100644
--- a/go.mod
+++ b/go.mod
@@ -24,8 +24,8 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.6.1
 	golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
-	golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
-	golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
+	golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect
+	golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect
 	k8s.io/api v0.21.1
 	k8s.io/apiextensions-apiserver v0.21.1
 	k8s.io/apimachinery v0.21.1
diff --git a/go.sum b/go.sum
index e10c4899a..b17bee6c0 100644
--- a/go.sum
+++ b/go.sum
@@ -722,8 +722,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -786,9 +786,9 @@ golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
@@ -799,8 +799,9 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/manifests/function/cni/tigera-operator/v1.15.0/custom-resources.yaml b/manifests/function/cni/tigera-operator/v1.15.0/custom-resources.yaml
index 4cf8f5422..418b4dd0f 100644
--- a/manifests/function/cni/tigera-operator/v1.15.0/custom-resources.yaml
+++ b/manifests/function/cni/tigera-operator/v1.15.0/custom-resources.yaml
@@ -4,3 +4,8 @@ metadata:
   name: default
 spec:
   registry: "quay.io"
+---
+apiVersion: operator.tigera.io/v1
+kind: TigeraStatus
+metadata:
+  name: calico
diff --git a/manifests/function/phase-helpers/kustomization.yaml b/manifests/function/phase-helpers/kustomization.yaml
index 676f16f91..1ec5b3a79 100644
--- a/manifests/function/phase-helpers/kustomization.yaml
+++ b/manifests/function/phase-helpers/kustomization.yaml
@@ -1,7 +1,6 @@
 resources:
 - wait_node
 - get_pods
-- wait_tigera
 - wait_deploy
 - get_node
 - wait_pods_ready
diff --git a/manifests/function/phase-helpers/wait_tigera/kubectl_wait_tigera.sh b/manifests/function/phase-helpers/wait_tigera/kubectl_wait_tigera.sh
deleted file mode 100644
index b9b42ef63..000000000
--- a/manifests/function/phase-helpers/wait_tigera/kubectl_wait_tigera.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/sh
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-set -xe
-
-export TIMEOUT=${TIMEOUT:-1000}
-
-echo "Wait $TIMEOUT seconds for tigera status to be in Available state." 1>&2
-end=$(($(date +%s) + $TIMEOUT))
-
-until [ "$(kubectl --kubeconfig $KUBECONFIG --context $KCTL_CONTEXT wait --for=condition=Available --all tigerastatus 2>/dev/null)" ]; do
-  now=$(date +%s)
-  if [ $now -gt $end ]; then
-    echo "Tigera status is not ready before TIMEOUT=$TIMEOUT" 1>&2
-    exit 1
-  fi
-  sleep 10
-done
diff --git a/manifests/function/phase-helpers/wait_tigera/kustomization.yaml b/manifests/function/phase-helpers/wait_tigera/kustomization.yaml
deleted file mode 100644
index 364dfd4b8..000000000
--- a/manifests/function/phase-helpers/wait_tigera/kustomization.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-configMapGenerator:
-- name: kubectl-wait-tigera
-  options:
-    disableNameSuffixHash: true
-  files:
-  - script=kubectl_wait_tigera.sh
diff --git a/manifests/phases/executors.yaml b/manifests/phases/executors.yaml
index 656b3ccfd..52db341ea 100644
--- a/manifests/phases/executors.yaml
+++ b/manifests/phases/executors.yaml
@@ -8,6 +8,11 @@ metadata:
 config:
   waitOptions:
     timeout: 2600
+    conditions:
+      - apiVersion: metal3.io/v1alpha1
+        kind: BareMetalHost
+        jsonPath: "{.status.provisioning.state}"
+        value: "provisioned"
   pruneOptions:
     prune: false
 ---
@@ -21,6 +26,11 @@ config:
   waitOptions:
     timeout: 5000
     pollInterval: 30
+    conditions:
+      - apiVersion: metal3.io/v1alpha1
+        kind: BareMetalHost
+        jsonPath: "{.status.provisioning.state}"
+        value: "provisioned"
   pruneOptions:
     prune: false
 ---
@@ -33,6 +43,14 @@ metadata:
 config:
   waitOptions:
     timeout: 1000
+    conditions:
+      - apiVersion: operator.tigera.io/v1
+        kind: Installation
+        jsonPath: "{.status.computed}"
+      - apiVersion: operator.tigera.io/v1
+        kind: TigeraStatus
+        jsonPath: "{.status.conditions[?(@.type=='Available')].status}"
+        value: "True"
   pruneOptions:
     prune: false
 ---
diff --git a/manifests/site/docker-test-site/phases/plan_patch.yaml b/manifests/site/docker-test-site/phases/plan_patch.yaml
index 1bc317a3b..a1781a42d 100644
--- a/manifests/site/docker-test-site/phases/plan_patch.yaml
+++ b/manifests/site/docker-test-site/phases/plan_patch.yaml
@@ -12,7 +12,6 @@ phases:
   - name: kubectl-get-node-target
   - name: kubectl-get-pods-target
   - name: initinfra-networking-target
-  - name: kubectl-wait-tigera-target
   - name: kubectl-get-pods-target
   - name: clusterctl-init-target
   - name: kubectl-wait-pods-any-ephemeral
diff --git a/manifests/site/test-site/target/controlplane/hostgenerator/host-generation.yaml b/manifests/site/test-site/target/controlplane/hostgenerator/host-generation.yaml
index 665c8ae77..bd953d144 100644
--- a/manifests/site/test-site/target/controlplane/hostgenerator/host-generation.yaml
+++ b/manifests/site/test-site/target/controlplane/hostgenerator/host-generation.yaml
@@ -7,4 +7,3 @@ metadata:
   name: host-generation-catalogue
 hosts:
   m3:
-    - node02
diff --git a/manifests/site/test-site/target/controlplane/kustomization.yaml b/manifests/site/test-site/target/controlplane/kustomization.yaml
index 0608f5b46..8b0037e5e 100644
--- a/manifests/site/test-site/target/controlplane/kustomization.yaml
+++ b/manifests/site/test-site/target/controlplane/kustomization.yaml
@@ -1,7 +1,7 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 resources:
-  # TODO (dukov) It's recocommended to upload BareMetalHost objects separately
+  # TODO (dukov) It's recommended to upload BareMetalHost objects separately
   # otherwise nodes will hang in 'registering' state for quite a long time
   - ../../../../type/gating/target/controlplane
   - ../catalogues
diff --git a/manifests/type/gating/phases/plan.yaml b/manifests/type/gating/phases/plan.yaml
index 0f211e7c2..60d326777 100644
--- a/manifests/type/gating/phases/plan.yaml
+++ b/manifests/type/gating/phases/plan.yaml
@@ -18,11 +18,6 @@ phases:
   - name: kubectl-wait-pods-any-ephemeral
   # Deploy calico using tigera operator
   - name: initinfra-networking-ephemeral
-  # Wait for Calico to be deployed using tigera
-  # Scripts for this phase placed in manifests/function/phase-helpers/wait_tigera/
-  # To get ConfigMap for this phase, execute `airshipctl phase render --source config -k ConfigMap`
-  # and find ConfigMap with name kubectl-wait_tigera
-  - name: kubectl-wait-tigera-ephemeral
   # Deploy metal3.io components to ephemeral node
   - name: initinfra-ephemeral
   # Getting pods as debug information"
@@ -66,11 +61,6 @@ phases:
   - name: kubectl-get-pods-target
   # Deploy calico using tigera operator
   - name: initinfra-networking-target
-  # Wait for Calico to be deployed using tigera
-  # Scripts for this phase placed in manifests/function/phase-helpers/wait_tigera/
-  # To get ConfigMap for this phase, execute `airshipctl phase render --source config -k ConfigMap`
-  # and find ConfigMap with name kubectl-wait-tigera
-  - name: kubectl-wait-tigera-target
   # Deploy infra to cluster
   - name: initinfra-target
   # List all pods
diff --git a/pkg/api/v1alpha1/kubernetes_apply_types.go b/pkg/api/v1alpha1/kubernetes_apply_types.go
index 0fa08e4c2..5a399809e 100644
--- a/pkg/api/v1alpha1/kubernetes_apply_types.go
+++ b/pkg/api/v1alpha1/kubernetes_apply_types.go
@@ -39,10 +39,19 @@ type ApplyWaitOptions struct {
 	// Timeout in seconds
 	Timeout int `json:"timeout,omitempty"`
 	// PollInterval in seconds
-	PollInterval int `json:"pollInterval,omitempty"`
+	PollInterval int         `json:"pollInterval,omitempty"`
+	Conditions   []Condition `json:"conditions,omitempty"`
 }
 
 // ApplyPruneOptions provides instructions how to prune for kubernetes resources
 type ApplyPruneOptions struct {
 	Prune bool `json:"prune,omitempty"`
 }
+
+// Condition is a jsonpath for particular TypeMeta which indicates what state to wait
+type Condition struct {
+	metav1.TypeMeta `json:",inline"`
+	JSONPath        string `json:"jsonPath,omitempty"`
+	// Value is desired state to wait for, if no value specified - just existence of provided jsonPath will be checked
+	Value string `json:"value,omitempty"`
+}
diff --git a/pkg/api/v1alpha1/zz_generated.deepcopy.go b/pkg/api/v1alpha1/zz_generated.deepcopy.go
index 88b389d6c..1fc4198de 100644
--- a/pkg/api/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/api/v1alpha1/zz_generated.deepcopy.go
@@ -120,7 +120,7 @@ func (in AirshipctlFunctionImageRepoMap) DeepCopy() AirshipctlFunctionImageRepoM
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ApplyConfig) DeepCopyInto(out *ApplyConfig) {
 	*out = *in
-	out.WaitOptions = in.WaitOptions
+	in.WaitOptions.DeepCopyInto(&out.WaitOptions)
 	out.PruneOptions = in.PruneOptions
 }
 
@@ -152,6 +152,11 @@ func (in *ApplyPruneOptions) DeepCopy() *ApplyPruneOptions {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ApplyWaitOptions) DeepCopyInto(out *ApplyWaitOptions) {
 	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]Condition, len(*in))
+		copy(*out, *in)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplyWaitOptions.
@@ -531,6 +536,22 @@ func (in *ClusterctlOptions) DeepCopy() *ClusterctlOptions {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Condition) DeepCopyInto(out *Condition) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
+func (in *Condition) DeepCopy() *Condition {
+	if in == nil {
+		return nil
+	}
+	out := new(Condition)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *EndPointSpec) DeepCopyInto(out *EndPointSpec) {
 	*out = *in
@@ -1083,7 +1104,7 @@ func (in *KubernetesApply) DeepCopyInto(out *KubernetesApply) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
-	out.Config = in.Config
+	in.Config.DeepCopyInto(&out.Config)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesApply.
diff --git a/pkg/cluster/command.go b/pkg/cluster/command.go
index 21c8a539e..1aefcef6e 100755
--- a/pkg/cluster/command.go
+++ b/pkg/cluster/command.go
@@ -15,45 +15,13 @@
 package cluster
 
 import (
-	"fmt"
 	"io"
 
 	"opendev.org/airship/airshipctl/pkg/config"
 	"opendev.org/airship/airshipctl/pkg/k8s/kubeconfig"
-	"opendev.org/airship/airshipctl/pkg/log"
 	"opendev.org/airship/airshipctl/pkg/phase"
-	"opendev.org/airship/airshipctl/pkg/util"
 )
 
-// StatusRunner runs internal logic of cluster status command
-func StatusRunner(o StatusOptions, w io.Writer) error {
-	statusMap, docs, err := o.GetStatusMapDocs()
-	if err != nil {
-		return err
-	}
-
-	var errors []error
-	tw := util.NewTabWriter(w)
-	fmt.Fprintf(tw, "Kind\tName\tStatus\n")
-	for _, doc := range docs {
-		status, err := statusMap.GetStatusForResource(doc)
-		if err != nil {
-			errors = append(errors, err)
-		} else {
-			fmt.Fprintf(tw, "%s\t%s\t%s\n", doc.GetKind(), doc.GetName(), status)
-		}
-	}
-	tw.Flush()
-
-	if len(errors) > 0 {
-		log.Debug("The following errors occurred while requesting the status:")
-		for _, statusErr := range errors {
-			log.Debug(statusErr)
-		}
-	}
-	return nil
-}
-
 // GetKubeconfigCommand holds options for get kubeconfig command
 type GetKubeconfigCommand struct {
 	ClusterNames []string
diff --git a/pkg/cluster/command_test.go b/pkg/cluster/command_test.go
deleted file mode 100755
index 998e6af32..000000000
--- a/pkg/cluster/command_test.go
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package cluster_test
-
-import (
-	"bytes"
-	"fmt"
-	"regexp"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-
-	"opendev.org/airship/airshipctl/pkg/cluster"
-	"opendev.org/airship/airshipctl/pkg/document"
-	"opendev.org/airship/airshipctl/pkg/k8s/client/fake"
-	testdoc "opendev.org/airship/airshipctl/testutil/document"
-)
-
-type mockStatusOptions struct{}
-
-func getAllDocCfgs() []string {
-	return []string{
-		`apiVersion: "example.com/v1"
-kind: Resource
-metadata:
-  name: stable-resource
-  namespace: target-infra
-`,
-	}
-}
-
-func testFakeDocBundle() document.Bundle {
-	bundle := &testdoc.MockBundle{}
-	docCfgs := getAllDocCfgs()
-	allDocs := make([]document.Document, len(docCfgs))
-	for i, cfg := range docCfgs {
-		doc, err := document.NewDocumentFromBytes([]byte(cfg))
-		if err != nil {
-			return bundle
-		}
-		allDocs[i] = doc
-	}
-
-	bundle.On("GetAllDocuments").Return(allDocs, nil)
-
-	return bundle
-}
-
-func (o mockStatusOptions) GetStatusMapDocs() (*cluster.StatusMap, []document.Document, error) {
-	fakeClient := fake.NewClient(
-		fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
-		fake.WithDynamicObjects(makeResource("stable-resource", "stable")))
-	fakeSM, err := cluster.NewStatusMap(fakeClient)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	fakeDocBundle := testFakeDocBundle()
-	fakeDocs, err := fakeDocBundle.GetAllDocuments()
-	if err != nil {
-		return nil, nil, err
-	}
-	return fakeSM, fakeDocs, nil
-}
-
-func TestStatusRunner(t *testing.T) {
-	statusOptions := mockStatusOptions{}
-	b := bytes.NewBuffer(nil)
-	err := cluster.StatusRunner(statusOptions, b)
-	require.NoError(t, err)
-	expectedOutput := fmt.Sprintf("Kind Name Status Resource stable-resource Stable ")
-	space := regexp.MustCompile(`\s+`)
-	str := space.ReplaceAllString(b.String(), " ")
-	assert.Equal(t, expectedOutput, str)
-}
diff --git a/pkg/cluster/errors.go b/pkg/cluster/errors.go
deleted file mode 100644
index a0e67ee6b..000000000
--- a/pkg/cluster/errors.go
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package cluster
-
-import "fmt"
-
-// ErrInvalidStatusCheck denotes that something went wrong while handling a
-// status-check annotation.
-type ErrInvalidStatusCheck struct {
-	What string
-}
-
-func (err ErrInvalidStatusCheck) Error() string {
-	return fmt.Sprintf("invalid status-check: %s", err.What)
-}
-
-// ErrResourceNotFound is used when a resource is requested from a StatusMap,
-// but that resource can't be found
-type ErrResourceNotFound struct {
-	Resource string
-}
-
-func (err ErrResourceNotFound) Error() string {
-	return fmt.Sprintf("could not find a status for resource %q", err.Resource)
-}
diff --git a/pkg/cluster/expression_test.go b/pkg/cluster/expression_test.go
deleted file mode 100644
index 98d84557e..000000000
--- a/pkg/cluster/expression_test.go
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package cluster_test
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-
-	"opendev.org/airship/airshipctl/pkg/cluster"
-)
-
-func TestMatch(t *testing.T) {
-	tests := map[string]struct {
-		expression  cluster.Expression
-		object      *unstructured.Unstructured
-		expected    bool
-		expectedErr error
-	}{
-		"healthy-object-matches-healthy": {
-			expression: cluster.Expression{
-				Condition: `@.status.health=="ok"`,
-			},
-			object: &unstructured.Unstructured{
-				Object: map[string]interface{}{
-					"apiVersion": "testversion/v1",
-					"kind":       "TestObject",
-					"metadata": map[string]interface{}{
-						"name": "test-object",
-					},
-					"status": map[string]interface{}{
-						"health": "ok",
-					},
-				},
-			},
-			expected: true,
-		},
-		"unhealthy-object-matches-unhealthy": {
-			expression: cluster.Expression{
-				Condition: `@.status.health=="ok"`,
-			},
-			object: &unstructured.Unstructured{
-				Object: map[string]interface{}{
-					"apiVersion": "testversion/v1",
-					"kind":       "TestObject",
-					"metadata": map[string]interface{}{
-						"name": "test-object",
-					},
-					"status": map[string]interface{}{
-						"health": "not-ok",
-					},
-				},
-			},
-			expected: false,
-		},
-		"invalid-json-path-returns-error": {
-			expression: cluster.Expression{
-				Condition: `invalid JSON Path]`,
-			},
-			object: &unstructured.Unstructured{},
-			expectedErr: cluster.ErrInvalidStatusCheck{
-				What: `unable to parse jsonpath "invalid JSON Path]": ` +
-					`unrecognized character in action: U+005D ']'`,
-			},
-		},
-		"malformed-object-returns-error": {
-			expression: cluster.Expression{
-				Condition: `@.status.health=="ok"`,
-			},
-			object: &unstructured.Unstructured{},
-			expectedErr: cluster.ErrInvalidStatusCheck{
-				What: `failed to execute condition "@.status.health==\"ok\"" ` +
-					`on object &{map[]}: status is not found`,
-			},
-		},
-	}
-
-	for testName, tt := range tests {
-		tt := tt
-		t.Run(testName, func(t *testing.T) {
-			result, err := tt.expression.Match(tt.object)
-			assert.Equal(t, tt.expectedErr, err)
-			if tt.expectedErr == nil {
-				assert.Equal(t, tt.expected, result)
-			}
-		})
-	}
-}
diff --git a/pkg/cluster/status.go b/pkg/cluster/status.go
deleted file mode 100644
index b65dfcb86..000000000
--- a/pkg/cluster/status.go
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package cluster
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-
-	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/api/meta"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
-	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
-	"sigs.k8s.io/cli-utils/pkg/object"
-
-	"opendev.org/airship/airshipctl/pkg/config"
-	"opendev.org/airship/airshipctl/pkg/document"
-	"opendev.org/airship/airshipctl/pkg/k8s/client"
-)
-
-// A Status represents a kubernetes resource's state.
-type Status string
-
-// StatusOptions provides a way to get status map within all the documents in the bundle
-type StatusOptions interface {
-	GetStatusMapDocs() (*StatusMap, []document.Document, error)
-}
-
-type statusOptions struct {
-	CfgFactory    config.Factory
-	ClientFactory client.Factory
-	Kubeconfig    string
-}
-
-// NewStatusOptions constructs a new StatusOptions interface based on inner struct
-func NewStatusOptions(cfgFactory config.Factory, clientFactory client.Factory, kubeconfig string) StatusOptions {
-	return &statusOptions{CfgFactory: cfgFactory, ClientFactory: clientFactory, Kubeconfig: kubeconfig}
-}
-
-// GetStatusMapDocs returns status map within all the documents in the bundle
-func (o *statusOptions) GetStatusMapDocs() (*StatusMap, []document.Document, error) {
-	conf, err := o.CfgFactory()
-	if err != nil {
-		return nil, nil, err
-	}
-
-	manifest, err := conf.CurrentContextManifest()
-	if err != nil {
-		return nil, nil, err
-	}
-
-	docBundle, err := document.NewBundleByPath(manifest.GetTargetPath())
-	if err != nil {
-		return nil, nil, err
-	}
-
-	docs, err := docBundle.GetAllDocuments()
-	if err != nil {
-		return nil, nil, err
-	}
-
-	client, err := o.ClientFactory(conf.LoadedConfigPath(), o.Kubeconfig)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	statusMap, err := NewStatusMap(client)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	return statusMap, docs, nil
-}
-
-// StatusMap holds a mapping of schema.GroupVersionResource to various statuses
-// a resource may be in, as well as the Expression used to check for that
-// status.
-type StatusMap struct {
-	client     client.Interface
-	GkMapping  []schema.GroupKind
-	mapping    map[schema.GroupVersionResource]map[status.Status]Expression
-	restMapper *meta.DefaultRESTMapper
-}
-
-// NewStatusMap creates a cluster-wide StatusMap. It iterates over all
-// CustomResourceDefinitions in the cluster that are annotated with the
-// airshipit.org/status-check annotation and creates a mapping from the
-// GroupVersionResource to the various statuses and their associated
-// expressions.
-func NewStatusMap(client client.Interface) (*StatusMap, error) {
-	statusMap := &StatusMap{
-		client:     client,
-		mapping:    make(map[schema.GroupVersionResource]map[status.Status]Expression),
-		restMapper: meta.NewDefaultRESTMapper([]schema.GroupVersion{}),
-	}
-	client.ApiextensionsClientSet()
-	crds, err := statusMap.client.ApiextensionsClientSet().
-		ApiextensionsV1().
-		CustomResourceDefinitions().
-		List(context.Background(), metav1.ListOptions{})
-	if err != nil {
-		return nil, err
-	}
-
-	for _, crd := range crds.Items {
-		if err = statusMap.addCRD(crd); err != nil {
-			return nil, err
-		}
-	}
-
-	return statusMap, nil
-}
-
-// ReadStatus returns object status
-func (sm *StatusMap) ReadStatus(ctx context.Context, resource object.ObjMetadata) *event.ResourceStatus {
-	gk := resource.GroupKind
-	gvr, err := sm.restMapper.RESTMapping(gk, "v1")
-	if err != nil {
-		return handleResourceStatusError(resource, err)
-	}
-	options := metav1.GetOptions{}
-	object, err := sm.client.DynamicClient().Resource(gvr.Resource).
-		Namespace(resource.Namespace).Get(context.Background(), resource.Name, options)
-	if err != nil {
-		return handleResourceStatusError(resource, err)
-	}
-	return sm.ReadStatusForObject(ctx, object)
-}
-
-// ReadStatusForObject returns resource status for object.
-// Current status will be returned only if expression matched.
-func (sm *StatusMap) ReadStatusForObject(
-	ctx context.Context, resource *unstructured.Unstructured) *event.ResourceStatus {
-	identifier := object.ObjMetadata{
-		GroupKind: resource.GroupVersionKind().GroupKind(),
-		Name:      resource.GetName(),
-		Namespace: resource.GetNamespace(),
-	}
-	gvk := resource.GroupVersionKind()
-	restMapping, err := sm.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
-	if err != nil {
-		return &event.ResourceStatus{
-			Identifier: identifier,
-			Status:     status.UnknownStatus,
-			Error:      err,
-		}
-	}
-
-	gvr := restMapping.Resource
-
-	obj, err := sm.client.DynamicClient().Resource(gvr).Namespace(resource.GetNamespace()).
-		Get(context.Background(), resource.GetName(), metav1.GetOptions{})
-	if err != nil {
-		return &event.ResourceStatus{
-			Identifier: identifier,
-			Status:     status.UnknownStatus,
-			Error:      err,
-		}
-	}
-
-	// No need to check for existence - if there isn't a mapping for this
-	// resource, the following for loop won't run anyway
-	for currentstatus, expression := range sm.mapping[gvr] {
-		var matched bool
-		matched, err = expression.Match(obj)
-		if err != nil {
-			return &event.ResourceStatus{
-				Identifier: identifier,
-				Status:     status.UnknownStatus,
-				Error:      err,
-			}
-		}
-		if matched {
-			return &event.ResourceStatus{
-				Identifier: identifier,
-				Status:     currentstatus,
-				Resource:   resource,
-				Message:    fmt.Sprintf("%s is %s", resource.GroupVersionKind().Kind, currentstatus.String()),
-			}
-		}
-	}
-	return &event.ResourceStatus{
-		Identifier: identifier,
-		Status:     status.UnknownStatus,
-		Error:      nil,
-	}
-}
-
-// GetStatusForResource iterates over all of the stored conditions for the
-// resource and returns the first status whose conditions are met.
-func (sm *StatusMap) GetStatusForResource(resource document.Document) (status.Status, error) {
-	gvk := getGVK(resource)
-
-	restMapping, err := sm.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
-	if err != nil {
-		return "", ErrResourceNotFound{resource.GetName()}
-	}
-
-	gvr := restMapping.Resource
-	obj, err := sm.client.DynamicClient().Resource(gvr).Namespace(resource.GetNamespace()).
-		Get(context.Background(), resource.GetName(), metav1.GetOptions{})
-	if err != nil {
-		return "", err
-	}
-
-	// No need to check for existence - if there isn't a mapping for this
-	// resource, the following for loop won't run anyway
-	expressionMap := sm.mapping[gvr]
-	for status, expression := range expressionMap {
-		matched, err := expression.Match(obj)
-		if err != nil {
-			return "", err
-		}
-		if matched {
-			return status, nil
-		}
-	}
-
-	return status.UnknownStatus, nil
-}
-
-// addCRD adds the mappings from the CRD to its associated statuses
-func (sm *StatusMap) addCRD(crd apiextensions.CustomResourceDefinition) error {
-	annotations := crd.GetAnnotations()
-	rawStatusChecks, ok := annotations["airshipit.org/status-check"]
-	if !ok {
-		// This crd doesn't have a status-check
-		// annotation, so we should skip it.
-		return nil
-	}
-	statusChecks, err := parseStatusChecks(rawStatusChecks)
-	if err != nil {
-		return err
-	}
-
-	gvrs := getGVRs(crd)
-	for _, gvr := range gvrs {
-		sm.GkMapping = append(sm.GkMapping, crd.GroupVersionKind().GroupKind())
-		gvk := gvr.GroupVersion().WithKind(crd.Spec.Names.Kind)
-		gvrSingular := gvr.GroupVersion().WithResource(crd.Spec.Names.Singular)
-		sm.mapping[gvr] = statusChecks
-		sm.restMapper.AddSpecific(gvk, gvr, gvrSingular, meta.RESTScopeNamespace)
-	}
-
-	return nil
-}
-
-// getGVRs constructs a slice of schema.GroupVersionResource for
-// CustomResources defined by the CustomResourceDefinition.
-func getGVRs(crd apiextensions.CustomResourceDefinition) []schema.GroupVersionResource {
-	gvrs := make([]schema.GroupVersionResource, 0, len(crd.Spec.Versions))
-	for _, version := range crd.Spec.Versions {
-		gvr := schema.GroupVersionResource{
-			Group:    crd.Spec.Group,
-			Version:  version.Name,
-			Resource: crd.Spec.Names.Plural,
-		}
-		gvrs = append(gvrs, gvr)
-	}
-	return gvrs
-}
-
-// getGVK constructs a schema.GroupVersionKind for a document
-func getGVK(doc document.Document) schema.GroupVersionKind {
-	toSchemaGvk := schema.GroupVersionKind{
-		Group:   doc.GetGroup(),
-		Version: doc.GetVersion(),
-		Kind:    doc.GetKind(),
-	}
-	return toSchemaGvk
-}
-
-// parseStatusChecks takes a string containing a map of status names (e.g.
-// Healthy) to the JSONPath filters associated with the statuses, and returns
-// the Go object equivalent.
-func parseStatusChecks(raw string) (map[status.Status]Expression, error) {
-	type statusCheckType struct {
-		Status    string `json:"status"`
-		Condition string `json:"condition"`
-	}
-
-	var mappings []statusCheckType
-	if err := json.Unmarshal([]byte(raw), &mappings); err != nil {
-		return nil, ErrInvalidStatusCheck{
-			What: fmt.Sprintf("unable to parse jsonpath: %q: %v", raw, err.Error()),
-		}
-	}
-
-	expressionMap := make(map[status.Status]Expression)
-	for _, mapping := range mappings {
-		if mapping.Status == "" {
-			return nil, ErrInvalidStatusCheck{What: "missing status field"}
-		}
-
-		if mapping.Condition == "" {
-			return nil, ErrInvalidStatusCheck{What: "missing condition field"}
-		}
-
-		expressionMap[status.Status(mapping.Status)] = Expression{Condition: mapping.Condition}
-	}
-
-	return expressionMap, nil
-}
-
-// handleResourceStatusError construct the appropriate ResourceStatus
-// object based on the type of error.
-func handleResourceStatusError(identifier object.ObjMetadata, err error) *event.ResourceStatus {
-	if errors.IsNotFound(err) {
-		return &event.ResourceStatus{
-			Identifier: identifier,
-			Status:     status.NotFoundStatus,
-			Message:    "Resource not found",
-		}
-	}
-	return &event.ResourceStatus{
-		Identifier: identifier,
-		Status:     status.UnknownStatus,
-		Error:      err,
-	}
-}
diff --git a/pkg/cluster/status_test.go b/pkg/cluster/status_test.go
deleted file mode 100644
index 4ce27581e..000000000
--- a/pkg/cluster/status_test.go
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package cluster_test
-
-import (
-	"context"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
-	"sigs.k8s.io/cli-utils/pkg/object"
-
-	"opendev.org/airship/airshipctl/pkg/cluster"
-	"opendev.org/airship/airshipctl/pkg/config"
-	"opendev.org/airship/airshipctl/pkg/document"
-	"opendev.org/airship/airshipctl/pkg/k8s/client"
-	"opendev.org/airship/airshipctl/pkg/k8s/client/fake"
-	"opendev.org/airship/airshipctl/testutil"
-)
-
-func TestGetStatusMapDocs(t *testing.T) {
-	tests := []struct {
-		name      string
-		resources []runtime.Object
-		CRDs      []runtime.Object
-	}{
-		{
-			name: "get-status-map-docs-no-resources",
-		},
-		{
-			name: "get-status-map-docs-with-resources",
-			resources: []runtime.Object{
-				makeResource("stable-resource", "stable"),
-				makeResource("pending-resource", "pending"),
-			},
-			CRDs: []runtime.Object{
-				makeResourceCRD(annotationValidStatusCheck()),
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		tt := tt
-		settings := clusterStatusTestSettings()
-		fakeClient := fake.NewClient(
-			fake.WithDynamicObjects(tt.resources...),
-			fake.WithCRDs(tt.CRDs...))
-		clientFactory := func(_ string, _ string) (client.Interface, error) {
-			return fakeClient, nil
-		}
-		statusOptions := cluster.NewStatusOptions(func() (*config.Config, error) {
-			return settings, nil
-		}, clientFactory, "")
-
-		expectedSM, err := cluster.NewStatusMap(fakeClient)
-		require.NoError(t, err)
-		docBundle, err := document.NewBundleByPath(settings.Manifests["testManifest"].TargetPath)
-		require.NoError(t, err)
-		expectedDocs, err := docBundle.GetAllDocuments()
-		require.NoError(t, err)
-
-		sm, docs, err := statusOptions.GetStatusMapDocs()
-		require.NoError(t, err)
-		assert.Equal(t, expectedSM, sm)
-		assert.Equal(t, expectedDocs, docs)
-	}
-}
-
-func clusterStatusTestSettings() *config.Config {
-	return &config.Config{
-		Contexts: map[string]*config.Context{
-			"testContext": {Manifest: "testManifest"},
-		},
-		Manifests: map[string]*config.Manifest{
-			"testManifest": {TargetPath: "testdata/statusmap"},
-		},
-		CurrentContext: "testContext",
-	}
-}
-
-func TestNewStatusMap(t *testing.T) {
-	tests := []struct {
-		name   string
-		client *fake.Client
-		err    error
-	}{
-		{
-			name:   "no-failure-on-valid-status-check-annotation",
-			client: fake.NewClient(fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck()))),
-			err:    nil,
-		},
-		{
-			name:   "no-failure-when-missing-status-check-annotation",
-			client: fake.NewClient(fake.WithCRDs(makeResourceCRD(nil))),
-			err:    nil,
-		},
-		{
-			name:   "missing-status",
-			client: fake.NewClient(fake.WithCRDs(makeResourceCRD(annotationMissingStatus()))),
-			err:    cluster.ErrInvalidStatusCheck{What: "missing status field"},
-		},
-		{
-			name:   "missing-condition",
-			client: fake.NewClient(fake.WithCRDs(makeResourceCRD(annotationMissingCondition()))),
-			err:    cluster.ErrInvalidStatusCheck{What: "missing condition field"},
-		},
-		{
-			name:   "malformed-status-check",
-			client: fake.NewClient(fake.WithCRDs(makeResourceCRD(annotationMalformedStatusCheck()))),
-			err: cluster.ErrInvalidStatusCheck{What: `unable to parse jsonpath: ` +
-				`"{invalid json": invalid character 'i' looking for beginning of object key string`},
-		},
-	}
-
-	for _, tt := range tests {
-		tt := tt
-		_, err := cluster.NewStatusMap(tt.client)
-		assert.Equal(t, tt.err, err)
-	}
-}
-
-func TestGetStatusForResource(t *testing.T) {
-	tests := []struct {
-		name           string
-		selector       document.Selector
-		client         *fake.Client
-		expectedStatus status.Status
-		err            error
-	}{
-		{
-			name: "stable-resource-is-stable",
-			selector: document.NewSelector().
-				ByGvk("example.com", "v1", "Resource").
-				ByName("stable-resource"),
-			client: fake.NewClient(
-				fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
-				fake.WithDynamicObjects(makeResource("stable-resource", "stable")),
-			),
-			expectedStatus: status.Status("Stable"),
-		},
-		{
-			name: "pending-resource-is-pending",
-			selector: document.NewSelector().
-				ByGvk("example.com", "v1", "Resource").
-				ByName("pending-resource"),
-			client: fake.NewClient(
-				fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
-				fake.WithDynamicObjects(makeResource("pending-resource", "pending")),
-			),
-			expectedStatus: status.Status("Pending"),
-		},
-		{
-			name: "unknown-resource-is-unknown",
-			selector: document.NewSelector().
-				ByGvk("example.com", "v1", "Resource").
-				ByName("unknown"),
-			client: fake.NewClient(
-				fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
-				fake.WithDynamicObjects(makeResource("unknown", "unknown")),
-			),
-			expectedStatus: status.UnknownStatus,
-		},
-		{
-			name: "missing-resource-returns-error",
-			selector: document.NewSelector().
-				ByGvk("example.com", "v1", "Missing").
-				ByName("missing-resource"),
-			client: fake.NewClient(),
-			err:    cluster.ErrResourceNotFound{Resource: "missing-resource"},
-		},
-	}
-
-	for _, tt := range tests {
-		tt := tt
-
-		t.Run(tt.name, func(t *testing.T) {
-			bundle := testutil.NewTestBundle(t, "testdata/statusmap")
-			testStatusMap, err := cluster.NewStatusMap(tt.client)
-			require.NoError(t, err)
-
-			doc, err := bundle.SelectOne(tt.selector)
-			require.NoError(t, err)
-
-			actualStatus, err := testStatusMap.GetStatusForResource(doc)
-			if tt.err != nil {
-				assert.EqualError(t, err, tt.err.Error())
-				// We expected an error - no need to check anything else
-				return
-			}
-
-			require.NoError(t, err)
-			assert.Equal(t, tt.expectedStatus, actualStatus)
-		})
-	}
-}
-
-func TestReadStatus(t *testing.T) {
-	c := fake.NewClient(fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
-		fake.WithDynamicObjects(makeResource("pending-resource", "pending")))
-	statusMap, err := cluster.NewStatusMap(c)
-	require.NoError(t, err)
-	ctx := context.Background()
-	resource := object.ObjMetadata{Namespace: "target-infra",
-		Name: "pending-resource", GroupKind: schema.GroupKind{Group: "example.com", Kind: "Resource"}}
-	result := statusMap.ReadStatus(ctx, resource)
-	assert.Equal(t, "Pending", result.Status.String())
-}
-
-func makeResource(name, state string) *unstructured.Unstructured {
-	return &unstructured.Unstructured{
-		Object: map[string]interface{}{
-			"apiVersion": "example.com/v1",
-			"kind":       "Resource",
-			"metadata": map[string]interface{}{
-				"name":      name,
-				"namespace": "target-infra",
-			},
-			"status": map[string]interface{}{
-				"state": state,
-			},
-		},
-	}
-}
-
-func makeResourceCRD(annotations map[string]string) *apiextensionsv1.CustomResourceDefinition {
-	return &apiextensionsv1.CustomResourceDefinition{
-		TypeMeta: metav1.TypeMeta{
-			Kind:       "CustomResourceDefinition",
-			APIVersion: "apiextensions.k8s.io/v1",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name:        "resources.example.com",
-			Annotations: annotations,
-		},
-		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
-			Group: "example.com",
-			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
-				{
-					Name:    "v1",
-					Served:  true,
-					Storage: true,
-				},
-			},
-			// omitting the openAPIV3Schema for brevity
-			Scope: "Namespaced",
-			Names: apiextensionsv1.CustomResourceDefinitionNames{
-				Kind:     "Resource",
-				Plural:   "resources",
-				Singular: "resource",
-			},
-		},
-	}
-}
-
-func annotationValidStatusCheck() map[string]string {
-	return map[string]string{
-		"airshipit.org/status-check": `
-[
-  {
-    "status": "Stable",
-    "condition": "@.status.state==\"stable\""
-  },
-  {
-    "status": "Pending",
-    "condition": "@.status.state==\"pending\""
-  }
-]`,
-	}
-}
-
-func annotationMissingStatus() map[string]string {
-	return map[string]string{
-		"airshipit.org/status-check": `
-[
-  {
-    "condition": "@.status.state==\"stable\""
-  },
-  {
-    "condition": "@.status.state==\"pending\""
-  }
-]`,
-	}
-}
-
-func annotationMissingCondition() map[string]string {
-	return map[string]string{
-		"airshipit.org/status-check": `
-[
-  {
-    "status": "Stable"
-  },
-  {
-    "status": "Pending"
-  }
-]`,
-	}
-}
-
-func annotationMalformedStatusCheck() map[string]string {
-	return map[string]string{"airshipit.org/status-check": "{invalid json"}
-}
diff --git a/pkg/cluster/testdata/statusmap/crd.yaml b/pkg/cluster/testdata/statusmap/crd.yaml
deleted file mode 100644
index 929eeb969..000000000
--- a/pkg/cluster/testdata/statusmap/crd.yaml
+++ /dev/null
@@ -1,40 +0,0 @@
-# this CRD defines a type whose status can be checked using the condition in
-# the annotations
-apiVersion: apiextensions.k8s.io/v1
-kind: CustomResourceDefinition
-metadata:
-  name: resources.example.com
-  annotations:
-    airshipit.org/status-check: |
-      [
-        {
-          "status": "Stable",
-          "condition": "@.status.state==\"stable\""
-        },
-        {
-          "status": "Pending",
-          "condition": "@.status.state==\"pending\""
-        }
-      ]
-spec:
-  group: example.com
-  versions:
-    - name: v1
-      served: true
-      storage: true
-      schema:
-        openAPIV3Schema:
-          type: object
-          properties:
-            status:
-              type: object
-              properties:
-                state:
-                  type: string
-  scope: Namespaced
-  names:
-    plural: resources
-    singular: resource
-    kind: Resource
-    shortNames:
-    - rsc
diff --git a/pkg/cluster/testdata/statusmap/kustomization.yaml b/pkg/cluster/testdata/statusmap/kustomization.yaml
deleted file mode 100644
index f4b156e41..000000000
--- a/pkg/cluster/testdata/statusmap/kustomization.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-resources:
-  - crd.yaml
-  - stable-resource.yaml
-  - pending-resource.yaml
-  - missing.yaml
-  - unknown.yaml
-  - legacy-crd.yaml
-  - legacy-resource.yaml
diff --git a/pkg/cluster/testdata/statusmap/legacy-crd.yaml b/pkg/cluster/testdata/statusmap/legacy-crd.yaml
deleted file mode 100644
index b9e250046..000000000
--- a/pkg/cluster/testdata/statusmap/legacy-crd.yaml
+++ /dev/null
@@ -1,42 +0,0 @@
-# this is a legacy CRD which defines a type whose status can be checked using
-# the condition in the annotations
-# It is included in tests to assure backward compatibility
-apiVersion: apiextensions.k8s.io/v1beta1
-kind: CustomResourceDefinition
-metadata:
-  name: legacies.example.com
-  annotations:
-    airshipit.org/status-check: |
-      [
-        {
-          "status": "Stable",
-          "condition": "@.status.state==\"stable\""
-        },
-        {
-          "status": "Pending",
-          "condition": "@.status.state==\"pending\""
-        }
-      ]
-spec:
-  group: example.com
-  versions:
-    - name: v1
-      served: true
-      storage: true
-  scope: Namespaced
-  names:
-    plural: legacies
-    singular: legacy
-    kind: Legacy
-    shortNames:
-    - lgc
-  preserveUnknownFields: false
-  validation:
-    openAPIV3Schema:
-      type: object
-      properties:
-        status:
-          type: object
-          properties:
-            state:
-              type: string
diff --git a/pkg/cluster/testdata/statusmap/legacy-resource.yaml b/pkg/cluster/testdata/statusmap/legacy-resource.yaml
deleted file mode 100644
index e58b220c3..000000000
--- a/pkg/cluster/testdata/statusmap/legacy-resource.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-# this legacy-resource is stable because the fake version in the cluster will
-# have .status.state == "stable"
-apiVersion: "example.com/v1"
-kind: Legacy
-metadata:
-  name: stable-legacy
-  namespace: target-infra
diff --git a/pkg/cluster/testdata/statusmap/missing.yaml b/pkg/cluster/testdata/statusmap/missing.yaml
deleted file mode 100644
index 69eb40628..000000000
--- a/pkg/cluster/testdata/statusmap/missing.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-# This resource doesn't have a status-check defined by its CRD (which is also
-# missing for brevity). Requesting its status is an error
-apiVersion: "example.com/v1"
-kind: Missing
-metadata:
-  name: missing-resource
-  namespace: target-infra
diff --git a/pkg/cluster/testdata/statusmap/pending-resource.yaml b/pkg/cluster/testdata/statusmap/pending-resource.yaml
deleted file mode 100644
index 00c1f4223..000000000
--- a/pkg/cluster/testdata/statusmap/pending-resource.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-# this resource is pending because the fake version in the cluster will
-# have .status.state == "pending"
-apiVersion: "example.com/v1"
-kind: Resource
-metadata:
-  name: pending-resource
-  namespace: target-infra
diff --git a/pkg/cluster/testdata/statusmap/stable-resource.yaml b/pkg/cluster/testdata/statusmap/stable-resource.yaml
deleted file mode 100644
index bdfa1d833..000000000
--- a/pkg/cluster/testdata/statusmap/stable-resource.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-# this resource is stable because the fake version in the cluster will have
-# .status.state == "stable"
-apiVersion: "example.com/v1"
-kind: Resource
-metadata:
-  name: stable-resource
-  namespace: target-infra
diff --git a/pkg/cluster/testdata/statusmap/unknown.yaml b/pkg/cluster/testdata/statusmap/unknown.yaml
deleted file mode 100644
index 76c053a45..000000000
--- a/pkg/cluster/testdata/statusmap/unknown.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-# this resource is in an unknown state because the fake version in the cluster
-# will have .status.state == "unknown", which does not correlate to any of the
-# status checks in the CRD.
-apiVersion: "example.com/v1"
-kind: Resource
-metadata:
-  name: unknown
-  namespace: target-infra
diff --git a/pkg/k8s/applier/applier.go b/pkg/k8s/applier/applier.go
index 1c4aadb75..71621a4d3 100644
--- a/pkg/k8s/applier/applier.go
+++ b/pkg/k8s/applier/applier.go
@@ -25,7 +25,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	cmdutil "k8s.io/kubectl/pkg/cmd/util"
-	"k8s.io/kubectl/pkg/scheme"
 	cliapply "sigs.k8s.io/cli-utils/pkg/apply"
 	applyevent "sigs.k8s.io/cli-utils/pkg/apply/event"
 	"sigs.k8s.io/cli-utils/pkg/apply/poller"
@@ -33,9 +32,9 @@ import (
 	"sigs.k8s.io/cli-utils/pkg/inventory"
 	"sigs.k8s.io/cli-utils/pkg/manifestreader"
 	"sigs.k8s.io/cli-utils/pkg/provider"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/yaml"
 
+	"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
 	"opendev.org/airship/airshipctl/pkg/document"
 	"opendev.org/airship/airshipctl/pkg/events"
 	airpoller "opendev.org/airship/airshipctl/pkg/k8s/poller"
@@ -55,13 +54,14 @@ type Applier struct {
 	Poller                poller.Poller
 	ManifestReaderFactory utils.ManifestReaderFactory
 	eventChannel          chan events.Event
+	conditions            []v1alpha1.Condition
 }
 
 // ReaderFactory function that returns reader factory interface
 type ReaderFactory func(validate bool, bundle document.Bundle, factory cmdutil.Factory) manifestreader.ManifestReader
 
 // NewApplier returns instance of Applier
-func NewApplier(eventCh chan events.Event, f cmdutil.Factory) *Applier {
+func NewApplier(eventCh chan events.Event, f cmdutil.Factory, conditions []v1alpha1.Condition) *Applier {
 	cf := provider.NewProvider(f)
 	return &Applier{
 		Factory:               f,
@@ -70,6 +70,7 @@ func NewApplier(eventCh chan events.Event, f cmdutil.Factory) *Applier {
 			CliUtilsApplier: cliapply.NewApplier(cf),
 		},
 		eventChannel: eventCh,
+		conditions:   conditions,
 	}
 }
 
@@ -137,17 +138,10 @@ func (a *Applier) getObjects(
 	}
 
 	if a.Poller == nil {
-		var pErr error
-		config, pErr := a.Factory.ToRESTConfig()
-		if pErr != nil {
-			return nil, pErr
+		a.Poller, err = airpoller.NewStatusPoller(a.Factory, a.conditions...)
+		if err != nil {
+			return nil, err
 		}
-
-		c, pErr := client.New(config, client.Options{Scheme: scheme.Scheme, Mapper: mapper})
-		if pErr != nil {
-			return nil, pErr
-		}
-		a.Poller = airpoller.NewStatusPoller(c, mapper)
 	}
 
 	if err = a.Driver.Initialize(a.Poller); err != nil {
@@ -198,6 +192,7 @@ func cliApplyOptions(ao ApplyOptions) cliapply.Options {
 		ReconcileTimeout: ao.WaitTimeout,
 		NoPrune:          !ao.Prune,
 		DryRunStrategy:   ao.DryRunStrategy,
+		PollInterval:     ao.PollInterval,
 	}
 }
 
diff --git a/pkg/k8s/applier/applier_test.go b/pkg/k8s/applier/applier_test.go
index 981ca8a59..f79aedba4 100644
--- a/pkg/k8s/applier/applier_test.go
+++ b/pkg/k8s/applier/applier_test.go
@@ -112,7 +112,7 @@ func TestApplierRun(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			// create default applier
 			eventChan := make(chan events.Event)
-			a := applier.NewApplier(eventChan, f)
+			a := applier.NewApplier(eventChan, f, nil)
 			opts := applier.ApplyOptions{
 				WaitTimeout:    time.Second * 5,
 				BundleName:     "test-bundle",
diff --git a/pkg/k8s/client/client.go b/pkg/k8s/client/client.go
deleted file mode 100644
index 671cc414e..000000000
--- a/pkg/k8s/client/client.go
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package client
-
-import (
-	"path/filepath"
-
-	apix "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
-	"k8s.io/client-go/dynamic"
-	"k8s.io/client-go/kubernetes"
-	"k8s.io/client-go/tools/clientcmd"
-
-	"opendev.org/airship/airshipctl/pkg/k8s/kubectl"
-	k8sutils "opendev.org/airship/airshipctl/pkg/k8s/utils"
-)
-
-// Interface provides an abstraction layer to interactions with kubernetes
-// clusters by providing the following:
-// * A ClientSet which includes all kubernetes core objects with standard operations
-// * A DynamicClient which provides interactions with loosely typed kubernetes resources
-// * An ApiextensionsClientSet which provides interactions with CustomResourceDefinitions
-// * A Kubectl interface that is built on top of kubectl libraries and
-//   implements such kubectl subcommands as kubectl apply (more will be added)
-type Interface interface {
-	ClientSet() kubernetes.Interface
-	DynamicClient() dynamic.Interface
-	ApiextensionsClientSet() apix.Interface
-
-	Kubectl() kubectl.Interface
-}
-
-// Client is an implementation of Interface
-type Client struct {
-	clientSet     kubernetes.Interface
-	dynamicClient dynamic.Interface
-	apixClient    apix.Interface
-
-	kubectl kubectl.Interface
-}
-
-// Client implements Interface
-var _ Interface = &Client{}
-
-// Factory is a function which creates Interfaces
-type Factory func(airshipConfigPath string, kubeconfig string) (Interface, error)
-
-// DefaultClient is a factory which generates a default client
-var DefaultClient Factory = NewClient
-
-// NewClient creates a Client initialized from the passed in settings
-func NewClient(airshipConfigPath string, kubeconfig string) (Interface, error) {
-	client := new(Client)
-	var err error
-
-	// TODO add support for kubeconfig context, for now use current context
-	f := k8sutils.FactoryFromKubeConfig(kubeconfig, "")
-
-	pathToBufferDir := filepath.Dir(airshipConfigPath)
-	client.kubectl = kubectl.NewKubectl(f).WithBufferDir(pathToBufferDir)
-
-	client.clientSet, err = f.KubernetesClientSet()
-	if err != nil {
-		return nil, err
-	}
-
-	client.dynamicClient, err = f.DynamicClient()
-	if err != nil {
-		return nil, err
-	}
-
-	// kubectl factories can't create CRD clients...
-	kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
-	if err != nil {
-		return nil, err
-	}
-
-	client.apixClient, err = apix.NewForConfig(kubeConfig)
-	if err != nil {
-		return nil, err
-	}
-
-	return client, nil
-}
-
-// ClientSet returns the ClientSet interface
-func (c *Client) ClientSet() kubernetes.Interface {
-	return c.clientSet
-}
-
-// SetClientSet sets the ClientSet interface
-func (c *Client) SetClientSet(clientSet kubernetes.Interface) {
-	c.clientSet = clientSet
-}
-
-// DynamicClient returns the DynamicClient interface
-func (c *Client) DynamicClient() dynamic.Interface {
-	return c.dynamicClient
-}
-
-// SetDynamicClient sets the DynamicClient interface
-func (c *Client) SetDynamicClient(dynamicClient dynamic.Interface) {
-	c.dynamicClient = dynamicClient
-}
-
-// ApiextensionsClientSet returns the Apiextensions interface
-func (c *Client) ApiextensionsClientSet() apix.Interface {
-	return c.apixClient
-}
-
-// SetApiextensionsClientSet sets the ApiextensionsClientSet interface
-func (c *Client) SetApiextensionsClientSet(apixClient apix.Interface) {
-	c.apixClient = apixClient
-}
-
-// Kubectl returns the Kubectl interface
-func (c *Client) Kubectl() kubectl.Interface {
-	return c.kubectl
-}
-
-// SetKubectl sets the Kubectl interface
-func (c *Client) SetKubectl(kctl kubectl.Interface) {
-	c.kubectl = kctl
-}
diff --git a/pkg/k8s/client/client_test.go b/pkg/k8s/client/client_test.go
deleted file mode 100644
index f5eba96ce..000000000
--- a/pkg/k8s/client/client_test.go
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package client_test
-
-import (
-	"path/filepath"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-
-	"opendev.org/airship/airshipctl/pkg/k8s/client"
-	"opendev.org/airship/airshipctl/testutil"
-)
-
-const (
-	kubeconfigPath   = "testdata/kubeconfig.yaml"
-	airshipConfigDir = "testdata"
-)
-
-func TestNewClient(t *testing.T) {
-	conf, cleanup := testutil.InitConfig(t)
-	defer cleanup(t)
-
-	akp, err := filepath.Abs(kubeconfigPath)
-	require.NoError(t, err)
-
-	adir, err := filepath.Abs(airshipConfigDir)
-	require.NoError(t, err)
-
-	conf.SetLoadedConfigPath(adir)
-
-	client, err := client.NewClient(conf.LoadedConfigPath(), akp)
-	assert.NoError(t, err)
-	assert.NotNil(t, client)
-	assert.NotNil(t, client.ClientSet())
-	assert.NotNil(t, client.DynamicClient())
-	assert.NotNil(t, client.ApiextensionsClientSet())
-	assert.NotNil(t, client.Kubectl())
-}
diff --git a/pkg/k8s/client/fake/fake.go b/pkg/k8s/client/fake/fake.go
deleted file mode 100644
index 25df6bbd3..000000000
--- a/pkg/k8s/client/fake/fake.go
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
-     https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package fake
-
-import (
-	apix "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
-	apixFake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/client-go/dynamic"
-	dynamicFake "k8s.io/client-go/dynamic/fake"
-	"k8s.io/client-go/kubernetes"
-	kubernetesFake "k8s.io/client-go/kubernetes/fake"
-
-	"opendev.org/airship/airshipctl/pkg/k8s/client"
-	"opendev.org/airship/airshipctl/pkg/k8s/kubectl"
-	"opendev.org/airship/airshipctl/testutil/k8sutils"
-)
-
-// Client is an implementation of client.Interface meant for testing purposes.
-type Client struct {
-	mockClientSet              func() kubernetes.Interface
-	mockDynamicClient          func() dynamic.Interface
-	mockApiextensionsClientSet func() apix.Interface
-	mockKubectl                func() kubectl.Interface
-}
-
-var _ client.Interface = &Client{}
-
-// ClientSet is used to get a mocked implementation of a kubernetes clientset.
-// To initialize the mocked clientset to be returned, use the WithTypedObjects
-// ResourceAccumulator
-func (c *Client) ClientSet() kubernetes.Interface {
-	return c.mockClientSet()
-}
-
-// DynamicClient is used to get a mocked implementation of a dynamic client.
-// To initialize the mocked client to be returned, use the WithDynamicObjects
-// ResourceAccumulator.
-func (c *Client) DynamicClient() dynamic.Interface {
-	return c.mockDynamicClient()
-}
-
-// ApiextensionsClientSet is used to get a mocked implementation of an
-// Apiextensions clientset. To initialize the mocked client to be returned,
-// use the WithCRDs ResourceAccumulator
-func (c *Client) ApiextensionsClientSet() apix.Interface {
-	return c.mockApiextensionsClientSet()
-}
-
-// Kubectl is used to get a mocked implementation of a Kubectl client.
-// To initialize the mocked client to be returned, use the WithKubectl ResourceAccumulator
-func (c *Client) Kubectl() kubectl.Interface {
-	return c.mockKubectl()
-}
-
-// A ResourceAccumulator is an option meant to be passed to NewClient.
-// ResourceAccumulators can be mixed and matched to create a collection of
-// mocked clients, each having their own fake objects.
-type ResourceAccumulator func(*Client)
-
-// NewClient creates an instance of a Client. If no arguments are passed, the
-// returned Client will have fresh mocked kubernetes clients which will have no
-// prior knowledge of any resources.
-//
-// If prior knowledge of resources is desirable, NewClient should receive an
-// appropriate ResourceAccumulator initialized with the desired resources.
-func NewClient(resourceAccumulators ...ResourceAccumulator) *Client {
-	fakeClient := new(Client)
-	for _, accumulator := range resourceAccumulators {
-		accumulator(fakeClient)
-	}
-
-	if fakeClient.mockClientSet == nil {
-		fakeClient.mockClientSet = func() kubernetes.Interface {
-			return kubernetesFake.NewSimpleClientset()
-		}
-	}
-	if fakeClient.mockDynamicClient == nil {
-		fakeClient.mockDynamicClient = func() dynamic.Interface {
-			return dynamicFake.NewSimpleDynamicClient(runtime.NewScheme())
-		}
-	}
-	if fakeClient.mockApiextensionsClientSet == nil {
-		fakeClient.mockApiextensionsClientSet = func() apix.Interface {
-			return apixFake.NewSimpleClientset()
-		}
-	}
-	if fakeClient.mockKubectl == nil {
-		fakeClient.mockKubectl = func() kubectl.Interface {
-			return kubectl.NewKubectl(k8sutils.NewMockKubectlFactory())
-		}
-	}
-	return fakeClient
-}
-
-// WithTypedObjects returns a ResourceAccumulator with resources which would
-// normally be accessible through a kubernetes ClientSet (e.g. Pods,
-// Deployments, etc...).
-func WithTypedObjects(objs ...runtime.Object) ResourceAccumulator {
-	return func(c *Client) {
-		c.mockClientSet = func() kubernetes.Interface {
-			return kubernetesFake.NewSimpleClientset(objs...)
-		}
-	}
-}
-
-// WithCRDs returns a ResourceAccumulator with resources which would
-// normally be accessible through a kubernetes ApiextensionsClientSet (e.g. CRDs).
-func WithCRDs(objs ...runtime.Object) ResourceAccumulator {
-	return func(c *Client) {
-		c.mockApiextensionsClientSet = func() apix.Interface {
-			return apixFake.NewSimpleClientset(objs...)
-		}
-	}
-}
-
-// WithDynamicObjects returns a ResourceAccumulator with resources which would
-// normally be accessible through a kubernetes DynamicClient (e.g. unstructured.Unstructured).
-func WithDynamicObjects(objs ...runtime.Object) ResourceAccumulator {
-	return func(c *Client) {
-		c.mockDynamicClient = func() dynamic.Interface {
-			return dynamicFake.NewSimpleDynamicClient(runtime.NewScheme(), objs...)
-		}
-	}
-}
-
-// WithKubectl returns a ResourceAccumulator with an instance of a kubectl.Interface.
-func WithKubectl(kubectlInstance *kubectl.Kubectl) ResourceAccumulator {
-	return func(c *Client) {
-		c.mockKubectl = func() kubectl.Interface {
-			return kubectlInstance
-		}
-	}
-}
diff --git a/pkg/k8s/client/testdata/kubeconfig.yaml b/pkg/k8s/client/testdata/kubeconfig.yaml
deleted file mode 100644
index b0d205918..000000000
--- a/pkg/k8s/client/testdata/kubeconfig.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-apiVersion: v1
-clusters:
-- cluster:
-    certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1Ea3lPVEUzTURNd09Wb1hEVEk1TURreU5qRTNNRE13T1Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTUZyCkdxM0kyb2dZci81Y01Udy9Na1pORTNWQURzdEdyU240WjU2TDhPUGhMcUhDN2t1dno2dVpES3dCSGtGeTBNK2MKRXIzd2piUGE1aTV5NmkyMGtxSHBVMjdPZTA0dzBXV2s4N0RSZVlWaGNoZVJHRXoraWt3SndIcGRmMjJVemZNKwpkSDBzaUhuMVd6UnovYk4za3hMUzJlMnZ2U1Y3bmNubk1YRUd4OXV0MUY0NThHeWxxdmxXTUlWMzg5Q2didXFDCkcwcFdiMTBLM0RVZWdiT25Xa1FmSm5sTWRRVVZDUVdZZEZaaklrcWtkWi9hVTRobkNEV01oZXNWRnFNaDN3VVAKczhQay9BNWh1ZFFPbnFRNDVIWXZLdjZ5RjJWcDUyWExBRUx3NDJ4aVRKZlh0V1h4eHR6cU4wY1lyL2VxeS9XMQp1YVVGSW5xQjFVM0JFL1oxbmFrQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFKUUVKQVBLSkFjVDVuK3dsWGJsdU9mS0J3c2gKZTI4R1c5R2QwM0N0NGF3RzhzMXE1ZHNua2tpZmVTUENHVFZ1SXF6UTZDNmJaSk9SMDMvVEl5ejh6NDJnaitDVApjWUZXZkltM2RKTnpRL08xWkdySXZZNWdtcWJtWDlpV0JaU24rRytEOGxubzd2aGMvY0tBRFR5OTMvVU92MThuCkdhMnIrRGJJcHcyTWVBVEl2elpxRS9RWlVSQ25DMmdjUFhTVzFqN2h4R3o1a3ZNcGVDZTdQYVUvdVFvblVHSWsKZ2t6ZzI4NHQvREhUUzc4N1V1SUg5cXBaV09yTFNMOGFBeUxQUHhWSXBteGZmbWRETE9TS2VUemRlTmxoSitUMwowQlBVaHBQTlJBNTNJN0hRQjhVUDR2elNONTkzZ1VFbVlFQ2Jic2RYSzB6ZVR6SDdWWHR2Zmd5WTVWWT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
-    server: https://127.0.0.1:6443
-  name: dummycluster_ephemeral
-contexts:
-- context:
-    cluster: dummycluster_ephemeral
-    user: kubernetes-admin
-  name: dummy_cluster
-current-context: dummy_cluster
-kind: Config
-preferences: {}
-users:
-- name: kubernetes-admin
-  user:
-    client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJQXhEdzk2RUY4SXN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T1RBNU1qa3hOekF6TURsYUZ3MHlNREE1TWpneE56QXpNVEphTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXV6R0pZdlBaNkRvaTQyMUQKSzhXSmFaQ25OQWQycXo1cC8wNDJvRnpRUGJyQWd6RTJxWVZrek9MOHhBVmVSN1NONXdXb1RXRXlGOEVWN3JyLwo0K0hoSEdpcTVQbXF1SUZ5enpuNi9JWmM4alU5eEVmenZpa2NpckxmVTR2UlhKUXdWd2dBU05sMkFXQUloMmRECmRUcmpCQ2ZpS1dNSHlqMFJiSGFsc0J6T3BnVC9IVHYzR1F6blVRekZLdjJkajVWMU5rUy9ESGp5UlJKK0VMNlEKQlltR3NlZzVQNE5iQzllYnVpcG1NVEFxL0p1bU9vb2QrRmpMMm5acUw2Zkk2ZkJ0RjVPR2xwQ0IxWUo4ZnpDdApHUVFaN0hUSWJkYjJ0cDQzRlZPaHlRYlZjSHFUQTA0UEoxNSswV0F5bVVKVXo4WEE1NDRyL2J2NzRKY0pVUkZoCmFyWmlRd0lEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFMMmhIUmVibEl2VHJTMFNmUVg1RG9ueVVhNy84aTg1endVWApSd3dqdzFuS0U0NDJKbWZWRGZ5b0hRYUM4Ti9MQkxyUXM0U0lqU1JYdmFHU1dSQnRnT1RRV21Db1laMXdSbjdwCndDTXZQTERJdHNWWm90SEZpUFl2b1lHWFFUSXA3YlROMmg1OEJaaEZ3d25nWUovT04zeG1rd29IN1IxYmVxWEYKWHF1TTluekhESk41VlZub1lQR09yRHMwWlg1RnNxNGtWVU0wVExNQm9qN1ZIRDhmU0E5RjRYNU4yMldsZnNPMAo4aksrRFJDWTAyaHBrYTZQQ0pQS0lNOEJaMUFSMG9ZakZxT0plcXpPTjBqcnpYWHh4S2pHVFVUb1BldVA5dCtCCjJOMVA1TnI4a2oxM0lrend5Q1NZclFVN09ZM3ltZmJobHkrcXZxaFVFa014MlQ1SkpmQT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
-    client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdXpHSll2UFo2RG9pNDIxREs4V0phWkNuTkFkMnF6NXAvMDQyb0Z6UVBickFnekUyCnFZVmt6T0w4eEFWZVI3U041d1dvVFdFeUY4RVY3cnIvNCtIaEhHaXE1UG1xdUlGeXp6bjYvSVpjOGpVOXhFZnoKdmlrY2lyTGZVNHZSWEpRd1Z3Z0FTTmwyQVdBSWgyZERkVHJqQkNmaUtXTUh5ajBSYkhhbHNCek9wZ1QvSFR2MwpHUXpuVVF6Rkt2MmRqNVYxTmtTL0RIanlSUkorRUw2UUJZbUdzZWc1UDROYkM5ZWJ1aXBtTVRBcS9KdW1Pb29kCitGakwyblpxTDZmSTZmQnRGNU9HbHBDQjFZSjhmekN0R1FRWjdIVEliZGIydHA0M0ZWT2h5UWJWY0hxVEEwNFAKSjE1KzBXQXltVUpVejhYQTU0NHIvYnY3NEpjSlVSRmhhclppUXdJREFRQUJBb0lCQVFDU0pycjlaeVpiQ2dqegpSL3VKMFZEWCt2aVF4c01BTUZyUjJsOE1GV3NBeHk1SFA4Vk4xYmc5djN0YUVGYnI1U3hsa3lVMFJRNjNQU25DCm1uM3ZqZ3dVQWlScllnTEl5MGk0UXF5VFBOU1V4cnpTNHRxTFBjM3EvSDBnM2FrNGZ2cSsrS0JBUUlqQnloamUKbnVFc1JpMjRzT3NESlM2UDE5NGlzUC9yNEpIM1M5bFZGbkVuOGxUR2c0M1kvMFZoMXl0cnkvdDljWjR5ZUNpNwpjMHFEaTZZcXJZaFZhSW9RRW1VQjdsbHRFZkZzb3l4VDR6RTE5U3pVbkRoMmxjYTF1TzhqcmI4d2xHTzBoQ2JyClB1R1l2WFFQa3Q0VlNmalhvdGJ3d2lBNFRCVERCRzU1bHp6MmNKeS9zSS8zSHlYbEMxcTdXUmRuQVhhZ1F0VzkKOE9DZGRkb0JBb0dCQU5NcUNtSW94REtyckhZZFRxT1M1ZFN4cVMxL0NUN3ZYZ0pScXBqd2Y4WHA2WHo0KzIvTAozVXFaVDBEL3dGTkZkc1Z4eFYxMnNYMUdwMHFWZVlKRld5OVlCaHVSWGpTZ0ZEWldSY1Z1Y01sNVpPTmJsbmZGCjVKQ0xnNXFMZ1g5VTNSRnJrR3A0R241UDQxamg4TnhKVlhzZG5xWE9xNTFUK1RRT1UzdkpGQjc1QW9HQkFPTHcKalp1cnZtVkZyTHdaVGgvRDNpWll5SVV0ZUljZ2NKLzlzbTh6L0pPRmRIbFd4dGRHUFVzYVd1MnBTNEhvckFtbgpqTm4vSTluUXd3enZ3MWUzVVFPbUhMRjVBczk4VU5hbk5TQ0xNMW1yaXZHRXJ1VHFnTDM1bU41eFZPdTUxQU5JCm4yNkFtODBJT2JDeEtLa0R0ZXJSaFhHd3g5c1pONVJCbG9VRThZNGJBb0dBQ3ZsdVhMZWRxcng5VkE0bDNoNXUKVDJXRVUxYjgxZ1orcmtRc1I1S0lNWEw4cllBTElUNUpHKzFuendyN3BkaEFXZmFWdVV2SDRhamdYT0h6MUs5aQpFODNSVTNGMG9ldUg0V01PY1RwU0prWm0xZUlXcWRiaEVCb1FGdUlWTXRib1BsV0d4ZUhFRHJoOEtreGp4aThSCmdEcUQyajRwY1IzQ0g5QjJ5a0lqQjVFQ2dZRUExc0xXLys2enE1c1lNSm14K1JXZThhTXJmL3pjQnVTSU1LQWgKY0dNK0wwMG9RSHdDaUU4TVNqcVN1ajV3R214YUFuanhMb3ZwSFlRV1VmUEVaUW95UE1YQ2VhRVBLOU4xbk8xMwp0V2lHRytIZkIxaU5PazFCc0lhNFNDbndOM1FRVTFzeXBaeEgxT3hueS9LYmkvYmEvWEZ5VzNqMGFUK2YvVWxrCmJGV1ZVdWtDZ1lFQTBaMmRTTFlmTjV5eFNtYk5xMWVqZXdWd1BjRzQxR2hQclNUZEJxdHFac1doWGE3aDdLTWEKeHdvamh5SXpnTXNyK2tXODdlajhDQ2h0d21sQ1p5QU92QmdOZytncnJ1cEZLM3FOSkpKeU9YREdHckdpbzZmTQp5aXB3Q2tZVGVxRThpZ1J6UkI5QkdFUGY4eVpjMUtwdmZhUDVhM0lRZmxiV0czbGpUemNNZVZjPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
diff --git a/pkg/k8s/poller/cluster_reader.go b/pkg/k8s/poller/cluster_reader.go
new file mode 100755
index 000000000..285737b81
--- /dev/null
+++ b/pkg/k8s/poller/cluster_reader.go
@@ -0,0 +1,70 @@
+/*
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package poller
+
+import (
+	"context"
+	"strings"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/labels"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/clusterreader"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"opendev.org/airship/airshipctl/pkg/log"
+)
+
+const allowedApplyErrors = 3
+
+// CachingClusterReader is wrapper for kstatus.CachingClusterReader implementation
+type CachingClusterReader struct {
+	Cr          *clusterreader.CachingClusterReader
+	applyErrors []error
+}
+
+// Get is a wrapper for kstatus.CachingClusterReader Get method
+func (c *CachingClusterReader) Get(ctx context.Context, key client.ObjectKey, obj *unstructured.Unstructured) error {
+	return c.Cr.Get(ctx, key, obj)
+}
+
+// ListNamespaceScoped is a wrapper for kstatus.CachingClusterReader ListNamespaceScoped method
+func (c *CachingClusterReader) ListNamespaceScoped(
+	ctx context.Context,
+	list *unstructured.UnstructuredList,
+	namespace string,
+	selector labels.Selector) error {
+	return c.Cr.ListNamespaceScoped(ctx, list, namespace, selector)
+}
+
+// ListClusterScoped is a wrapper for kstatus.CachingClusterReader ListClusterScoped method
+func (c *CachingClusterReader) ListClusterScoped(
+	ctx context.Context,
+	list *unstructured.UnstructuredList,
+	selector labels.Selector) error {
+	return c.Cr.ListClusterScoped(ctx, list, selector)
+}
+
+// Sync is a wrapper for kstatus.CachingClusterReader Sync method, allows to filter specific errors
+func (c *CachingClusterReader) Sync(ctx context.Context) error {
+	err := c.Cr.Sync(ctx)
+	if err != nil && strings.Contains(err.Error(), "request timed out") {
+		c.applyErrors = append(c.applyErrors, err)
+		if len(c.applyErrors) <= allowedApplyErrors {
+			log.Printf("timeout error occurred during sync: '%v', skipping", err)
+			return nil
+		}
+	}
+	return err
+}
diff --git a/pkg/k8s/poller/cluster_reader_test.go b/pkg/k8s/poller/cluster_reader_test.go
new file mode 100755
index 000000000..a1a4738f0
--- /dev/null
+++ b/pkg/k8s/poller/cluster_reader_test.go
@@ -0,0 +1,246 @@
+/*
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package poller_test
+
+import (
+	"context"
+	"fmt"
+	"sort"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	appsv1 "k8s.io/api/apps/v1"
+	v1 "k8s.io/api/core/v1"
+	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/clusterreader"
+	"sigs.k8s.io/cli-utils/pkg/object"
+	"sigs.k8s.io/cli-utils/pkg/testutil"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"opendev.org/airship/airshipctl/pkg/k8s/poller"
+)
+
+var (
+	deploymentGVK = appsv1.SchemeGroupVersion.WithKind("Deployment")
+	rsGVK         = appsv1.SchemeGroupVersion.WithKind("ReplicaSet")
+	podGVK        = v1.SchemeGroupVersion.WithKind("Pod")
+)
+
+// gkNamespace contains information about a GroupVersionKind and a namespace.
+type gkNamespace struct {
+	GroupKind schema.GroupKind
+	Namespace string
+}
+
+func TestSync(t *testing.T) {
+	testCases := map[string]struct {
+		identifiers    []object.ObjMetadata
+		expectedSynced []gkNamespace
+	}{
+		"no identifiers": {
+			identifiers: []object.ObjMetadata{},
+		},
+		"same GVK in multiple namespaces": {
+			identifiers: []object.ObjMetadata{
+				{
+					GroupKind: deploymentGVK.GroupKind(),
+					Name:      "deployment",
+					Namespace: "Foo",
+				},
+				{
+					GroupKind: deploymentGVK.GroupKind(),
+					Name:      "deployment",
+					Namespace: "Bar",
+				},
+			},
+			expectedSynced: []gkNamespace{
+				{
+					GroupKind: deploymentGVK.GroupKind(),
+					Namespace: "Foo",
+				},
+				{
+					GroupKind: rsGVK.GroupKind(),
+					Namespace: "Foo",
+				},
+				{
+					GroupKind: podGVK.GroupKind(),
+					Namespace: "Foo",
+				},
+				{
+					GroupKind: deploymentGVK.GroupKind(),
+					Namespace: "Bar",
+				},
+				{
+					GroupKind: rsGVK.GroupKind(),
+					Namespace: "Bar",
+				},
+				{
+					GroupKind: podGVK.GroupKind(),
+					Namespace: "Bar",
+				},
+			},
+		},
+	}
+
+	fakeMapper := testutil.NewFakeRESTMapper(
+		appsv1.SchemeGroupVersion.WithKind("Deployment"),
+		appsv1.SchemeGroupVersion.WithKind("ReplicaSet"),
+		v1.SchemeGroupVersion.WithKind("Pod"),
+	)
+
+	for tn, tc := range testCases {
+		t.Run(tn, func(t *testing.T) {
+			fakeReader := &fakeReader{}
+
+			cr, err := clusterreader.NewCachingClusterReader(fakeReader, fakeMapper, tc.identifiers)
+			require.NoError(t, err)
+
+			clusterReader := &poller.CachingClusterReader{Cr: cr}
+			err = clusterReader.Sync(context.Background())
+			require.NoError(t, err)
+
+			synced := fakeReader.syncedGVKNamespaces
+			sortGVKNamespaces(synced)
+			expectedSynced := tc.expectedSynced
+			sortGVKNamespaces(expectedSynced)
+			require.Equal(t, expectedSynced, synced)
+		})
+	}
+}
+
+func TestSync_Errors(t *testing.T) {
+	testCases := map[string]struct {
+		mapper          meta.RESTMapper
+		readerError     error
+		expectSyncError bool
+		cacheError      bool
+		cacheErrorText  string
+	}{
+		"mapping and reader are successful": {
+			mapper: testutil.NewFakeRESTMapper(
+				apiextv1.SchemeGroupVersion.WithKind("CustomResourceDefinition"),
+			),
+			readerError:     nil,
+			expectSyncError: false,
+			cacheError:      false,
+		},
+		"reader returns NotFound error": {
+			mapper: testutil.NewFakeRESTMapper(
+				apiextv1.SchemeGroupVersion.WithKind("CustomResourceDefinition"),
+			),
+			readerError: errors.NewNotFound(schema.GroupResource{
+				Group:    "apiextensions.k8s.io",
+				Resource: "customresourcedefinitions",
+			}, "my-crd"),
+			expectSyncError: false,
+			cacheError:      true,
+			cacheErrorText:  "not found",
+		},
+		"reader returns request timed out other error": {
+			mapper: testutil.NewFakeRESTMapper(
+				apiextv1.SchemeGroupVersion.WithKind("CustomResourceDefinition"),
+			),
+			readerError:     errors.NewInternalError(fmt.Errorf("request timed out")),
+			expectSyncError: false,
+			cacheError:      true,
+			cacheErrorText:  "not found",
+		},
+		"reader returns other error": {
+			mapper: testutil.NewFakeRESTMapper(
+				apiextv1.SchemeGroupVersion.WithKind("CustomResourceDefinition"),
+			),
+			readerError:     errors.NewInternalError(fmt.Errorf("testing")),
+			expectSyncError: true,
+			cacheError:      true,
+			cacheErrorText:  "not found",
+		},
+		"mapping not found": {
+			mapper:          testutil.NewFakeRESTMapper(),
+			expectSyncError: false,
+			cacheError:      true,
+			cacheErrorText:  "no matches for kind",
+		},
+	}
+
+	for tn, tc := range testCases {
+		t.Run(tn, func(t *testing.T) {
+			identifiers := []object.ObjMetadata{
+				{
+					Name: "my-crd",
+					GroupKind: schema.GroupKind{
+						Group: "apiextensions.k8s.io",
+						Kind:  "CustomResourceDefinition",
+					},
+				},
+			}
+
+			fakeReader := &fakeReader{
+				err: tc.readerError,
+			}
+
+			cr, err := clusterreader.NewCachingClusterReader(fakeReader, tc.mapper, identifiers)
+			require.NoError(t, err)
+			clusterReader := poller.CachingClusterReader{Cr: cr}
+
+			err = clusterReader.Sync(context.Background())
+
+			if tc.expectSyncError {
+				require.Equal(t, tc.readerError, err)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+func sortGVKNamespaces(gvkNamespaces []gkNamespace) {
+	sort.Slice(gvkNamespaces, func(i, j int) bool {
+		if gvkNamespaces[i].GroupKind.String() != gvkNamespaces[j].GroupKind.String() {
+			return gvkNamespaces[i].GroupKind.String() < gvkNamespaces[j].GroupKind.String()
+		}
+		return gvkNamespaces[i].Namespace < gvkNamespaces[j].Namespace
+	})
+}
+
+type fakeReader struct {
+	syncedGVKNamespaces []gkNamespace
+	err                 error
+}
+
+func (f *fakeReader) Get(_ context.Context, _ client.ObjectKey, _ client.Object) error {
+	return nil
+}
+
+//nolint:gocritic
+func (f *fakeReader) List(_ context.Context, list client.ObjectList, opts ...client.ListOption) error {
+	var namespace string
+	for _, opt := range opts {
+		switch opt := opt.(type) {
+		case client.InNamespace:
+			namespace = string(opt)
+		}
+	}
+
+	gvk := list.GetObjectKind().GroupVersionKind()
+	f.syncedGVKNamespaces = append(f.syncedGVKNamespaces, gkNamespace{
+		GroupKind: gvk.GroupKind(),
+		Namespace: namespace,
+	})
+
+	return f.err
+}
diff --git a/pkg/cluster/expression.go b/pkg/k8s/poller/expression.go
old mode 100644
new mode 100755
similarity index 59%
rename from pkg/cluster/expression.go
rename to pkg/k8s/poller/expression.go
index bf58f683d..66961a03d
--- a/pkg/cluster/expression.go
+++ b/pkg/k8s/poller/expression.go
@@ -12,12 +12,11 @@
  limitations under the License.
 */
 
-package cluster
+package poller
 
 import (
 	"fmt"
 
-	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/util/jsonpath"
 )
 
@@ -26,7 +25,8 @@ import (
 type Expression struct {
 	// A Condition describes a JSONPath filter which is matched against an
 	// array containing a single resource.
-	Condition string `json:"condition"`
+	Condition string
+	Value     string
 
 	// jsonPath is used for the actual act of filtering on resources. It is
 	// stored within the Expression as a means of memoization.
@@ -36,36 +36,26 @@ type Expression struct {
 // Match returns true if the given object matches the parsed jsonpath object.
 // An error is returned if the Expression's condition is not a valid JSONPath
 // as defined here: https://goessner.net/articles/JsonPath.
-func (e *Expression) Match(obj runtime.Unstructured) (bool, error) {
-	// NOTE(howell): JSONPath filters only work on lists. This means that
-	// in order to check if a certain condition is met for obj, we need to
-	// put obj into an list, then see if the filter catches obj.
-	const listName = "items"
-
+func (e *Expression) Match(obj map[string]interface{}) (bool, error) {
 	// Parse lazily
 	if e.jsonPath == nil {
 		jp := jsonpath.New("status-check")
 
-		// The condition must be a filter on a list
-		itemAsArray := fmt.Sprintf("{$.%s[?(%s)]}", listName, e.Condition)
-		err := jp.Parse(itemAsArray)
+		err := jp.Parse(e.Condition)
 		if err != nil {
-			return false, ErrInvalidStatusCheck{
-				What: fmt.Sprintf("unable to parse jsonpath %q: %v", e.Condition, err.Error()),
-			}
+			return false, err
 		}
 		e.jsonPath = jp
 	}
 
-	// Filters only work on lists
-	list := map[string]interface{}{
-		listName: []interface{}{obj.UnstructuredContent()},
-	}
-	results, err := e.jsonPath.FindResults(list)
+	results, err := e.jsonPath.FindResults(obj)
 	if err != nil {
-		return false, ErrInvalidStatusCheck{
-			What: fmt.Sprintf("failed to execute condition %q on object %v: %v", e.Condition, obj, err),
-		}
+		return false, err
 	}
+
+	if e.Value != "" {
+		return len(results[0]) == 1 && fmt.Sprintf("%s", results[0][0].Interface()) == e.Value, nil
+	}
+
 	return len(results[0]) == 1, nil
 }
diff --git a/pkg/k8s/poller/expression_test.go b/pkg/k8s/poller/expression_test.go
new file mode 100755
index 000000000..118a0b120
--- /dev/null
+++ b/pkg/k8s/poller/expression_test.go
@@ -0,0 +1,77 @@
+/*
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package poller_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"opendev.org/airship/airshipctl/pkg/k8s/poller"
+)
+
+func TestNewExpression(t *testing.T) {
+	testCases := []struct {
+		name              string
+		condition         string
+		value             string
+		obj               map[string]interface{}
+		expectedResult    bool
+		expectedErrString string
+	}{
+		{
+			name:           "Success - value matched",
+			condition:      "{.status}",
+			obj:            map[string]interface{}{"status": "provisioned"},
+			value:          "provisioned",
+			expectedResult: true,
+		},
+		{
+			name:           "Success - empty value",
+			condition:      "{.status}",
+			obj:            map[string]interface{}{"status": "provisioned"},
+			expectedResult: true,
+		},
+		{
+			name:              "Failed - invalid condition",
+			condition:         "{*%.status}",
+			expectedErrString: "unrecognized character in action",
+		},
+		{
+			name:              "Failed - path not found in object",
+			condition:         "{.status}",
+			expectedErrString: "status is not found",
+		},
+	}
+	for _, test := range testCases {
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+			exp := poller.Expression{
+				Condition: tt.condition,
+				Value:     tt.value,
+			}
+
+			res, err := exp.Match(tt.obj)
+			assert.Equal(t, tt.expectedResult, res)
+			if test.expectedErrString != "" {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), test.expectedErrString)
+			} else {
+				require.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/pkg/k8s/poller/poller.go b/pkg/k8s/poller/poller.go
index 01683d150..6f8121587 100644
--- a/pkg/k8s/poller/poller.go
+++ b/pkg/k8s/poller/poller.go
@@ -16,13 +16,12 @@ package poller
 
 import (
 	"context"
-	"strings"
 
 	appsv1 "k8s.io/api/apps/v1"
 	"k8s.io/apimachinery/pkg/api/meta"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	cmdutil "k8s.io/kubectl/pkg/cmd/util"
+	"k8s.io/kubectl/pkg/scheme"
 	"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
 	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/clusterreader"
 	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
@@ -31,25 +30,43 @@ import (
 	"sigs.k8s.io/cli-utils/pkg/object"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	"opendev.org/airship/airshipctl/pkg/log"
+	"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
 )
 
-const allowedApplyErrors = 3
-
 // NewStatusPoller creates a new StatusPoller using the given clusterreader and mapper. The StatusPoller
 // will use the client for all calls to the cluster.
-func NewStatusPoller(reader client.Reader, mapper meta.RESTMapper) *StatusPoller {
+func NewStatusPoller(f cmdutil.Factory, conditions ...v1alpha1.Condition) (*StatusPoller, error) {
+	config, err := f.ToRESTConfig()
+	if err != nil {
+		return nil, err
+	}
+
+	mapper, err := f.ToRESTMapper()
+	if err != nil {
+		return nil, err
+	}
+
+	c, err := client.New(config, client.Options{Scheme: scheme.Scheme, Mapper: mapper})
+	if err != nil {
+		return nil, err
+	}
+
 	return &StatusPoller{
-		engine: &engine.PollerEngine{
-			Reader: reader,
+		Engine: &engine.PollerEngine{
+			Reader: c,
 			Mapper: mapper,
 		},
-	}
+		conditions: conditions,
+	}, nil
 }
 
 // StatusPoller provides functionality for polling a cluster for status for a set of resources.
 type StatusPoller struct {
-	engine *engine.PollerEngine
+	ClusterReaderFactoryFunc engine.ClusterReaderFactoryFunc
+	StatusReadersFactoryFunc engine.StatusReadersFactoryFunc
+
+	Engine     *engine.PollerEngine
+	conditions []v1alpha1.Condition
 }
 
 // Poll will create a new statusPollerRunner that will poll all the resources provided and report their status
@@ -57,10 +74,12 @@ type StatusPoller struct {
 // context passed in.
 func (s *StatusPoller) Poll(
 	ctx context.Context, identifiers []object.ObjMetadata, options polling.Options) <-chan event.Event {
-	return s.engine.Poll(ctx, identifiers, engine.Options{
-		PollInterval:             options.PollInterval,
-		ClusterReaderFactoryFunc: clusterReaderFactoryFunc(options.UseCache),
-		StatusReadersFactoryFunc: s.createStatusReaders,
+	return s.Engine.Poll(ctx, identifiers, engine.Options{
+		PollInterval: options.PollInterval,
+		ClusterReaderFactoryFunc: map[bool]engine.ClusterReaderFactoryFunc{true: clusterReaderFactoryFunc(options.UseCache),
+			false: s.ClusterReaderFactoryFunc}[s.ClusterReaderFactoryFunc == nil],
+		StatusReadersFactoryFunc: map[bool]engine.StatusReadersFactoryFunc{true: s.createStatusReadersFactory(),
+			false: s.StatusReadersFactoryFunc}[s.StatusReadersFactoryFunc == nil],
 	})
 }
 
@@ -69,19 +88,29 @@ func (s *StatusPoller) Poll(
 // a specific statusreaders.
 // TODO: We should consider making the registration more automatic instead of having to create each of them
 // here. Also, it might be worth creating them on demand.
-func (s *StatusPoller) createStatusReaders(reader engine.ClusterReader, mapper meta.RESTMapper) (
-	map[schema.GroupKind]engine.StatusReader, engine.StatusReader) {
-	defaultStatusReader := statusreaders.NewGenericStatusReader(reader, mapper)
-	replicaSetStatusReader := statusreaders.NewReplicaSetStatusReader(reader, mapper, defaultStatusReader)
-	deploymentStatusReader := statusreaders.NewDeploymentResourceReader(reader, mapper, replicaSetStatusReader)
-	statefulSetStatusReader := statusreaders.NewStatefulSetResourceReader(reader, mapper, defaultStatusReader)
+func (s *StatusPoller) createStatusReadersFactory() engine.StatusReadersFactoryFunc {
+	return func(reader engine.ClusterReader, mapper meta.RESTMapper) (
+		map[schema.GroupKind]engine.StatusReader, engine.StatusReader) {
+		defaultStatusReader := statusreaders.NewGenericStatusReader(reader, mapper)
+		replicaSetStatusReader := statusreaders.NewReplicaSetStatusReader(reader, mapper, defaultStatusReader)
+		deploymentStatusReader := statusreaders.NewDeploymentResourceReader(reader, mapper, replicaSetStatusReader)
+		statefulSetStatusReader := statusreaders.NewStatefulSetResourceReader(reader, mapper, defaultStatusReader)
 
-	statusReaders := map[schema.GroupKind]engine.StatusReader{
-		appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind():  deploymentStatusReader,
-		appsv1.SchemeGroupVersion.WithKind("StatefulSet").GroupKind(): statefulSetStatusReader,
-		appsv1.SchemeGroupVersion.WithKind("ReplicaSet").GroupKind():  replicaSetStatusReader,
+		statusReaders := map[schema.GroupKind]engine.StatusReader{
+			appsv1.SchemeGroupVersion.WithKind("Deployment").GroupKind():  deploymentStatusReader,
+			appsv1.SchemeGroupVersion.WithKind("StatefulSet").GroupKind(): statefulSetStatusReader,
+			appsv1.SchemeGroupVersion.WithKind("ReplicaSet").GroupKind():  replicaSetStatusReader,
+		}
+
+		if len(s.conditions) > 0 {
+			cr := NewCustomResourceReader(reader, mapper, s.conditions...)
+			for _, tm := range s.conditions {
+				statusReaders[tm.GroupVersionKind().GroupKind()] = cr
+			}
+		}
+
+		return statusReaders, defaultStatusReader
 	}
-	return statusReaders, defaultStatusReader
 }
 
 // clusterReaderFactoryFunc returns a factory function for creating an instance of a ClusterReader.
@@ -101,44 +130,3 @@ func clusterReaderFactoryFunc(useCache bool) engine.ClusterReaderFactoryFunc {
 		return &clusterreader.DirectClusterReader{Reader: r}, nil
 	}
 }
-
-// CachingClusterReader is wrapper for kstatus.CachingClusterReader implementation
-type CachingClusterReader struct {
-	Cr          *clusterreader.CachingClusterReader
-	applyErrors []error
-}
-
-// Get is a wrapper for kstatus.CachingClusterReader Get method
-func (c *CachingClusterReader) Get(ctx context.Context, key client.ObjectKey, obj *unstructured.Unstructured) error {
-	return c.Cr.Get(ctx, key, obj)
-}
-
-// ListNamespaceScoped is a wrapper for kstatus.CachingClusterReader ListNamespaceScoped method
-func (c *CachingClusterReader) ListNamespaceScoped(
-	ctx context.Context,
-	list *unstructured.UnstructuredList,
-	namespace string,
-	selector labels.Selector) error {
-	return c.Cr.ListNamespaceScoped(ctx, list, namespace, selector)
-}
-
-// ListClusterScoped is a wrapper for kstatus.CachingClusterReader ListClusterScoped method
-func (c *CachingClusterReader) ListClusterScoped(
-	ctx context.Context,
-	list *unstructured.UnstructuredList,
-	selector labels.Selector) error {
-	return c.Cr.ListClusterScoped(ctx, list, selector)
-}
-
-// Sync is a wrapper for kstatus.CachingClusterReader Sync method, allows to filter specific errors
-func (c *CachingClusterReader) Sync(ctx context.Context) error {
-	err := c.Cr.Sync(ctx)
-	if err != nil && strings.Contains(err.Error(), "request timed out") {
-		c.applyErrors = append(c.applyErrors, err)
-		if len(c.applyErrors) < allowedApplyErrors {
-			log.Printf("timeout error occurred during sync: '%v', skipping", err)
-			return nil
-		}
-	}
-	return err
-}
diff --git a/pkg/k8s/poller/poller_test.go b/pkg/k8s/poller/poller_test.go
index 26c804d11..bd72c247e 100755
--- a/pkg/k8s/poller/poller_test.go
+++ b/pkg/k8s/poller/poller_test.go
@@ -15,25 +15,304 @@
 package poller_test
 
 import (
+	"context"
+	"errors"
 	"testing"
+	"time"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	appsv1 "k8s.io/api/apps/v1"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/cli-runtime/pkg/resource"
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	cmdutil "k8s.io/kubectl/pkg/cmd/util"
+	"k8s.io/kubectl/pkg/util/openapi"
+	"k8s.io/kubectl/pkg/validation"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
+	kstatustestutil "sigs.k8s.io/cli-utils/pkg/kstatus/polling/testutil"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
+	"sigs.k8s.io/cli-utils/pkg/object"
+	"sigs.k8s.io/cli-utils/pkg/testutil"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"opendev.org/airship/airshipctl/pkg/k8s/poller"
-	k8sutils "opendev.org/airship/airshipctl/pkg/k8s/utils"
 )
 
 func TestNewStatusPoller(t *testing.T) {
-	f := k8sutils.FactoryFromKubeConfig("testdata/kubeconfig.yaml", "")
-	restConfig, err := f.ToRESTConfig()
-	require.NoError(t, err)
-	restMapper, err := f.ToRESTMapper()
-	require.NoError(t, err)
-	restClient, err := client.New(restConfig, client.Options{Mapper: restMapper})
-	require.NoError(t, err)
+	testCases := map[string]struct {
+		factory       cmdutil.Factory
+		expectedError bool
+	}{
+		"failed rest config": {
+			factory: &MockCmdUtilFactory{MockToRESTConfig: func() (*rest.Config, error) {
+				return nil, errors.New("rest config error")
+			}},
+			expectedError: true,
+		},
+		"failed rest mapper": {
+			factory: &MockCmdUtilFactory{MockToRESTConfig: func() (*rest.Config, error) {
+				return nil, nil
+			},
+				MockToRESTMapper: func() (meta.RESTMapper, error) {
+					return nil, errors.New("rest mapper error")
+				}},
+			expectedError: true,
+		},
+		"failed new client": {
+			factory: &MockCmdUtilFactory{MockToRESTConfig: func() (*rest.Config, error) {
+				return nil, nil
+			},
+				MockToRESTMapper: func() (meta.RESTMapper, error) {
+					return nil, nil
+				}},
+			expectedError: true,
+		},
+		"success new poller": {
+			factory: &MockCmdUtilFactory{MockToRESTConfig: func() (*rest.Config, error) {
+				return &rest.Config{}, nil
+			},
+				MockToRESTMapper: func() (meta.RESTMapper, error) {
+					return testutil.NewFakeRESTMapper(), nil
+				}},
+			expectedError: false,
+		},
+	}
 
-	a := poller.NewStatusPoller(restClient, restMapper)
-	assert.NotNil(t, a)
+	for tn, tc := range testCases {
+		t.Run(tn, func(t *testing.T) {
+			p, err := poller.NewStatusPoller(tc.factory)
+			if tc.expectedError {
+				require.Error(t, err)
+			} else {
+				require.NoError(t, err)
+				require.NotNil(t, p)
+			}
+		})
+	}
+}
+
+func TestStatusPollerRun(t *testing.T) {
+	testCases := map[string]struct {
+		identifiers              []object.ObjMetadata
+		ClusterReaderFactoryFunc engine.ClusterReaderFactoryFunc
+		StatusReadersFactoryFunc engine.StatusReadersFactoryFunc
+		defaultStatusReader      engine.StatusReader
+		expectedEventTypes       []event.EventType
+	}{
+		"single resource": {
+			identifiers: []object.ObjMetadata{
+				{
+					GroupKind: schema.GroupKind{
+						Group: "apps",
+						Kind:  "Deployment",
+					},
+					Name:      "foo",
+					Namespace: "bar",
+				},
+			},
+			defaultStatusReader: &fakeStatusReader{
+				resourceStatuses: map[schema.GroupKind][]status.Status{
+					schema.GroupKind{Group: "apps", Kind: "Deployment"}: { //nolint:gofmt
+						status.InProgressStatus,
+						status.CurrentStatus,
+					},
+				},
+				resourceStatusCount: make(map[schema.GroupKind]int),
+			},
+			expectedEventTypes: []event.EventType{
+				event.ResourceUpdateEvent,
+				event.ResourceUpdateEvent,
+			},
+			ClusterReaderFactoryFunc: func(_ client.Reader, _ meta.RESTMapper, _ []object.ObjMetadata) (
+				engine.ClusterReader, error) {
+				return kstatustestutil.NewNoopClusterReader(), nil
+			},
+			StatusReadersFactoryFunc: func(_ engine.ClusterReader, _ meta.RESTMapper) (
+				statusReaders map[schema.GroupKind]engine.StatusReader, defaultStatusReader engine.StatusReader) {
+				return make(map[schema.GroupKind]engine.StatusReader), &fakeStatusReader{
+					resourceStatuses: map[schema.GroupKind][]status.Status{
+						schema.GroupKind{Group: "apps", Kind: "Deployment"}: { //nolint:gofmt
+							status.InProgressStatus,
+							status.CurrentStatus,
+						},
+					},
+					resourceStatusCount: make(map[schema.GroupKind]int),
+				}
+			},
+		},
+		"multiple resources": {
+			identifiers: []object.ObjMetadata{
+				{
+					GroupKind: schema.GroupKind{
+						Group: "apps",
+						Kind:  "Deployment",
+					},
+					Name:      "foo",
+					Namespace: "default",
+				},
+				{
+					GroupKind: schema.GroupKind{
+						Group: "",
+						Kind:  "Service",
+					},
+					Name:      "bar",
+					Namespace: "default",
+				},
+			},
+			ClusterReaderFactoryFunc: func(_ client.Reader, _ meta.RESTMapper, _ []object.ObjMetadata) (
+				engine.ClusterReader, error) {
+				return kstatustestutil.NewNoopClusterReader(), nil
+			},
+			StatusReadersFactoryFunc: func(_ engine.ClusterReader, _ meta.RESTMapper) (
+				statusReaders map[schema.GroupKind]engine.StatusReader, defaultStatusReader engine.StatusReader) {
+				return make(map[schema.GroupKind]engine.StatusReader), &fakeStatusReader{
+					resourceStatuses: map[schema.GroupKind][]status.Status{
+						schema.GroupKind{Group: "apps", Kind: "Deployment"}: { //nolint:gofmt
+							status.InProgressStatus,
+							status.CurrentStatus,
+						},
+						schema.GroupKind{Group: "", Kind: "Service"}: { //nolint:gofmt
+							status.InProgressStatus,
+							status.InProgressStatus,
+							status.CurrentStatus,
+						},
+					},
+					resourceStatusCount: make(map[schema.GroupKind]int),
+				}
+			},
+			expectedEventTypes: []event.EventType{
+				event.ResourceUpdateEvent,
+				event.ResourceUpdateEvent,
+				event.ResourceUpdateEvent,
+				event.ResourceUpdateEvent,
+			},
+		},
+	}
+
+	for tn, tc := range testCases {
+		t.Run(tn, func(t *testing.T) {
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+
+			identifiers := tc.identifiers
+
+			fakeMapper := testutil.NewFakeRESTMapper(
+				appsv1.SchemeGroupVersion.WithKind("Deployment"),
+				v1.SchemeGroupVersion.WithKind("Service"),
+			)
+
+			e := poller.StatusPoller{
+				ClusterReaderFactoryFunc: tc.ClusterReaderFactoryFunc,
+				StatusReadersFactoryFunc: tc.StatusReadersFactoryFunc,
+				Engine:                   &engine.PollerEngine{Mapper: fakeMapper},
+			}
+
+			options := polling.Options{PollInterval: time.Second, UseCache: true}
+
+			eventChannel := e.Poll(ctx, identifiers, options)
+
+			var eventTypes []event.EventType
+			for ch := range eventChannel {
+				eventTypes = append(eventTypes, ch.EventType)
+				if len(eventTypes) == len(tc.expectedEventTypes) {
+					cancel()
+				}
+			}
+
+			require.Equal(t, tc.expectedEventTypes, eventTypes)
+		})
+	}
+}
+
+type fakeStatusReader struct {
+	resourceStatuses    map[schema.GroupKind][]status.Status
+	resourceStatusCount map[schema.GroupKind]int
+}
+
+func (f *fakeStatusReader) ReadStatus(_ context.Context, identifier object.ObjMetadata) *event.ResourceStatus {
+	count := f.resourceStatusCount[identifier.GroupKind]
+	resourceStatusSlice := f.resourceStatuses[identifier.GroupKind]
+	var resourceStatus status.Status
+	if len(resourceStatusSlice) > count {
+		resourceStatus = resourceStatusSlice[count]
+	} else {
+		resourceStatus = resourceStatusSlice[len(resourceStatusSlice)-1]
+	}
+	f.resourceStatusCount[identifier.GroupKind] = count + 1
+	return &event.ResourceStatus{
+		Identifier: identifier,
+		Status:     resourceStatus,
+	}
+}
+
+func (f *fakeStatusReader) ReadStatusForObject(_ context.Context, _ *unstructured.Unstructured) *event.ResourceStatus {
+	return nil
+}
+
+var _ cmdutil.Factory = &MockCmdUtilFactory{}
+
+type MockCmdUtilFactory struct {
+	MockToRESTConfig func() (*rest.Config, error)
+	MockToRESTMapper func() (meta.RESTMapper, error)
+}
+
+func (n *MockCmdUtilFactory) ToRESTConfig() (*rest.Config, error) {
+	return n.MockToRESTConfig()
+}
+
+func (n *MockCmdUtilFactory) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
+	return nil, nil
+}
+
+func (n *MockCmdUtilFactory) ToRESTMapper() (meta.RESTMapper, error) {
+	return n.MockToRESTMapper()
+}
+
+func (n *MockCmdUtilFactory) ToRawKubeConfigLoader() clientcmd.ClientConfig {
+	return nil
+}
+
+func (n *MockCmdUtilFactory) DynamicClient() (dynamic.Interface, error) {
+	return nil, nil
+}
+
+func (n *MockCmdUtilFactory) KubernetesClientSet() (*kubernetes.Clientset, error) {
+	return nil, nil
+}
+
+func (n *MockCmdUtilFactory) RESTClient() (*rest.RESTClient, error) {
+	return nil, nil
+}
+
+func (n *MockCmdUtilFactory) NewBuilder() *resource.Builder {
+	return nil
+}
+
+func (n *MockCmdUtilFactory) ClientForMapping(_ *meta.RESTMapping) (resource.RESTClient, error) {
+	return nil, nil
+}
+
+func (n *MockCmdUtilFactory) UnstructuredClientForMapping(_ *meta.RESTMapping) (resource.RESTClient, error) {
+	return nil, nil
+}
+
+func (n *MockCmdUtilFactory) Validator(_ bool) (validation.Schema, error) {
+	return nil, nil
+}
+
+func (n *MockCmdUtilFactory) OpenAPISchema() (openapi.Resources, error) {
+	return nil, nil
+}
+
+func (n *MockCmdUtilFactory) OpenAPIGetter() discovery.OpenAPISchemaInterface {
+	return nil
 }
diff --git a/pkg/k8s/poller/status.go b/pkg/k8s/poller/status.go
new file mode 100755
index 000000000..ac8a4fa7b
--- /dev/null
+++ b/pkg/k8s/poller/status.go
@@ -0,0 +1,182 @@
+/*
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package poller
+
+import (
+	"context"
+
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
+	"sigs.k8s.io/cli-utils/pkg/object"
+
+	"opendev.org/airship/airshipctl/pkg/api/v1alpha1"
+)
+
+// CustomResourceReader is a wrapper for clu-utils genericClusterReader struct
+type CustomResourceReader struct {
+	// Reader is an implementation of the ClusterReader interface. It provides a
+	// way for the StatusReader to fetch resources from the cluster.
+	Reader engine.ClusterReader
+	// Mapper provides a way to look up the resource types that are available
+	// in the cluster.
+	Mapper meta.RESTMapper
+
+	// StatusFunc is a function for computing status of object
+	StatusFunc func(u *unstructured.Unstructured) (*status.Result, error)
+	// CondMap is a map with stored jsonpath expressions per GK to compute custom status
+	CondMap map[schema.GroupKind]Expression
+}
+
+var _ engine.StatusReader = &CustomResourceReader{}
+
+// NewCustomResourceReader implements custom logic to retrieve resource's status
+func NewCustomResourceReader(reader engine.ClusterReader, mapper meta.RESTMapper,
+	conditions ...v1alpha1.Condition) engine.StatusReader {
+	condMap := make(map[schema.GroupKind]Expression)
+	for _, cond := range conditions {
+		condMap[cond.GroupVersionKind().GroupKind()] = Expression{
+			Condition: cond.JSONPath,
+			Value:     cond.Value,
+		}
+	}
+
+	return &CustomResourceReader{
+		Reader:     reader,
+		Mapper:     mapper,
+		StatusFunc: status.Compute,
+		CondMap:    condMap,
+	}
+}
+
+// ReadStatus will fetch the resource identified by the given identifier
+// from the cluster and return an ResourceStatus that will contain
+// information about the latest state of the resource, its computed status
+// and information about any generated resources.
+func (c *CustomResourceReader) ReadStatus(ctx context.Context, identifier object.ObjMetadata) *event.ResourceStatus {
+	obj, err := c.lookupResource(ctx, identifier)
+	if err != nil {
+		return handleResourceStatusError(identifier, err)
+	}
+	return c.ReadStatusForObject(ctx, obj)
+}
+
+// ReadStatusForObject is similar to ReadStatus, but instead of looking up the
+// resource based on an identifier, it will use the passed-in resource.
+func (c *CustomResourceReader) ReadStatusForObject(_ context.Context,
+	obj *unstructured.Unstructured) *event.ResourceStatus {
+	res, err := c.StatusFunc(obj)
+	if err != nil {
+		return &event.ResourceStatus{
+			Identifier: toIdentifier(obj),
+			Status:     status.UnknownStatus,
+			Error:      err,
+		}
+	}
+
+	if val, ok := c.CondMap[obj.GroupVersionKind().GroupKind()]; ok && res.Status == status.CurrentStatus {
+		b, err := val.Match(obj.UnstructuredContent())
+		if err != nil {
+			return &event.ResourceStatus{
+				Identifier: toIdentifier(obj),
+				Status:     status.UnknownStatus,
+				Error:      err,
+			}
+		}
+
+		if b {
+			return &event.ResourceStatus{
+				Identifier: toIdentifier(obj),
+				Status:     res.Status,
+				Resource:   obj,
+				Message:    res.Message,
+			}
+		}
+		return &event.ResourceStatus{
+			Identifier: toIdentifier(obj),
+			Status:     status.InProgressStatus,
+		}
+	}
+
+	return &event.ResourceStatus{
+		Identifier: toIdentifier(obj),
+		Status:     res.Status,
+		Resource:   obj,
+		Message:    res.Message,
+	}
+}
+
+// lookupResource looks up a resource with the given identifier. It will use the rest mapper to resolve
+// the version of the GroupKind given in the identifier.
+// If the resource is found, it is returned. If it is not found or something
+// went wrong, the function will return an error.
+func (c *CustomResourceReader) lookupResource(ctx context.Context,
+	identifier object.ObjMetadata) (*unstructured.Unstructured, error) {
+	groupVersionKind, err := gvk(identifier.GroupKind, c.Mapper)
+	if err != nil {
+		return nil, err
+	}
+
+	var u unstructured.Unstructured
+	u.SetGroupVersionKind(groupVersionKind)
+	key := types.NamespacedName{
+		Name:      identifier.Name,
+		Namespace: identifier.Namespace,
+	}
+	err = c.Reader.Get(ctx, key, &u)
+	if err != nil {
+		return nil, err
+	}
+	return &u, nil
+}
+
+// gvk looks up the GVK from a GroupKind using the rest mapper.
+func gvk(gk schema.GroupKind, mapper meta.RESTMapper) (schema.GroupVersionKind, error) {
+	mapping, err := mapper.RESTMapping(gk)
+	if err != nil {
+		return schema.GroupVersionKind{}, err
+	}
+	return mapping.GroupVersionKind, nil
+}
+
+func toIdentifier(u *unstructured.Unstructured) object.ObjMetadata {
+	return object.ObjMetadata{
+		GroupKind: u.GroupVersionKind().GroupKind(),
+		Name:      u.GetName(),
+		Namespace: u.GetNamespace(),
+	}
+}
+
+// handleResourceStatusError construct the appropriate ResourceStatus
+// object based on the type of error.
+func handleResourceStatusError(identifier object.ObjMetadata, err error) *event.ResourceStatus {
+	if errors.IsNotFound(err) {
+		return &event.ResourceStatus{
+			Identifier: identifier,
+			Status:     status.NotFoundStatus,
+			Message:    "Resource not found",
+		}
+	}
+	return &event.ResourceStatus{
+		Identifier: identifier,
+		Status:     status.UnknownStatus,
+		Error:      err,
+	}
+}
diff --git a/pkg/k8s/poller/status_test.go b/pkg/k8s/poller/status_test.go
new file mode 100755
index 000000000..bd52bc22e
--- /dev/null
+++ b/pkg/k8s/poller/status_test.go
@@ -0,0 +1,113 @@
+/*
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package poller_test
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/testutil"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
+	"sigs.k8s.io/cli-utils/pkg/object"
+	fakemapper "sigs.k8s.io/cli-utils/pkg/testutil"
+
+	"opendev.org/airship/airshipctl/pkg/k8s/poller"
+)
+
+var (
+	customGVK = schema.GroupVersionKind{
+		Group:   "custom.io",
+		Version: "v1beta1",
+		Kind:    "Custom",
+	}
+	name      = "Foo"
+	namespace = "default"
+)
+
+func TestGenericStatusReader(t *testing.T) {
+	testCases := map[string]struct {
+		result             *status.Result
+		err                error
+		expectedIdentifier object.ObjMetadata
+		expectedStatus     status.Status
+		condMap            map[schema.GroupKind]poller.Expression
+	}{
+		"successfully computes status": {
+			result: &status.Result{
+				Status:  status.InProgressStatus,
+				Message: "this is a test",
+			},
+			expectedIdentifier: object.ObjMetadata{
+				GroupKind: customGVK.GroupKind(),
+				Name:      name,
+				Namespace: namespace,
+			},
+			expectedStatus: status.InProgressStatus,
+		},
+		"successfully computes custom status": {
+			result: &status.Result{
+				Status:  status.CurrentStatus,
+				Message: "this is a test",
+			},
+			expectedIdentifier: object.ObjMetadata{
+				GroupKind: customGVK.GroupKind(),
+				Name:      name,
+				Namespace: namespace,
+			},
+			condMap: map[schema.GroupKind]poller.Expression{
+				customGVK.GroupKind(): {Condition: "{.metadata.name}", Value: "Bar"}},
+			expectedStatus: status.InProgressStatus,
+		},
+		"computing status fails": {
+			err: fmt.Errorf("this error is a test"),
+			expectedIdentifier: object.ObjMetadata{
+				GroupKind: customGVK.GroupKind(),
+				Name:      name,
+				Namespace: namespace,
+			},
+			expectedStatus: status.UnknownStatus,
+		},
+	}
+
+	for tn, tc := range testCases {
+		t.Run(tn, func(t *testing.T) {
+			fakeReader := testutil.NewNoopClusterReader()
+			fakeMapper := fakemapper.NewFakeRESTMapper()
+
+			resourceStatusReader := &poller.CustomResourceReader{
+				Reader: fakeReader,
+				Mapper: fakeMapper,
+				StatusFunc: func(u *unstructured.Unstructured) (*status.Result, error) {
+					return tc.result, tc.err
+				},
+				CondMap: tc.condMap,
+			}
+
+			o := &unstructured.Unstructured{}
+			o.SetGroupVersionKind(customGVK)
+			o.SetName(name)
+			o.SetNamespace(namespace)
+
+			resourceStatus := resourceStatusReader.ReadStatusForObject(context.Background(), o)
+
+			require.Equal(t, tc.expectedIdentifier, resourceStatus.Identifier)
+			require.Equal(t, tc.expectedStatus, resourceStatus.Status)
+		})
+	}
+}
diff --git a/pkg/k8s/poller/testdata/kubeconfig.yaml b/pkg/k8s/poller/testdata/kubeconfig.yaml
deleted file mode 100755
index 967864a76..000000000
--- a/pkg/k8s/poller/testdata/kubeconfig.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-apiVersion: v1
-clusters:
-- cluster:
-    certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJDZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRFNU1Ea3lPVEUzTURNd09Wb1hEVEk1TURreU5qRTNNRE13T1Zvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTUZyCkdxM0kyb2dZci81Y01Udy9Na1pORTNWQURzdEdyU240WjU2TDhPUGhMcUhDN2t1dno2dVpES3dCSGtGeTBNK2MKRXIzd2piUGE1aTV5NmkyMGtxSHBVMjdPZTA0dzBXV2s4N0RSZVlWaGNoZVJHRXoraWt3SndIcGRmMjJVemZNKwpkSDBzaUhuMVd6UnovYk4za3hMUzJlMnZ2U1Y3bmNubk1YRUd4OXV0MUY0NThHeWxxdmxXTUlWMzg5Q2didXFDCkcwcFdiMTBLM0RVZWdiT25Xa1FmSm5sTWRRVVZDUVdZZEZaaklrcWtkWi9hVTRobkNEV01oZXNWRnFNaDN3VVAKczhQay9BNWh1ZFFPbnFRNDVIWXZLdjZ5RjJWcDUyWExBRUx3NDJ4aVRKZlh0V1h4eHR6cU4wY1lyL2VxeS9XMQp1YVVGSW5xQjFVM0JFL1oxbmFrQ0F3RUFBYU1qTUNFd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFKUUVKQVBLSkFjVDVuK3dsWGJsdU9mS0J3c2gKZTI4R1c5R2QwM0N0NGF3RzhzMXE1ZHNua2tpZmVTUENHVFZ1SXF6UTZDNmJaSk9SMDMvVEl5ejh6NDJnaitDVApjWUZXZkltM2RKTnpRL08xWkdySXZZNWdtcWJtWDlpV0JaU24rRytEOGxubzd2aGMvY0tBRFR5OTMvVU92MThuCkdhMnIrRGJJcHcyTWVBVEl2elpxRS9RWlVSQ25DMmdjUFhTVzFqN2h4R3o1a3ZNcGVDZTdQYVUvdVFvblVHSWsKZ2t6ZzI4NHQvREhUUzc4N1V1SUg5cXBaV09yTFNMOGFBeUxQUHhWSXBteGZmbWRETE9TS2VUemRlTmxoSitUMwowQlBVaHBQTlJBNTNJN0hRQjhVUDR2elNONTkzZ1VFbVlFQ2Jic2RYSzB6ZVR6SDdWWHR2Zmd5WTVWWT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
-    server: https://10.0.1.7:6443
-  name: kubernetes_target
-contexts:
-- context:
-    cluster: kubernetes_target
-    user: kubernetes-admin
-  name: kubernetes-admin@kubernetes
-current-context: ""
-kind: Config
-preferences: {}
-users:
-- name: kubernetes-admin
-  user:
-    client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM4akNDQWRxZ0F3SUJBZ0lJQXhEdzk2RUY4SXN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T1RBNU1qa3hOekF6TURsYUZ3MHlNREE1TWpneE56QXpNVEphTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXV6R0pZdlBaNkRvaTQyMUQKSzhXSmFaQ25OQWQycXo1cC8wNDJvRnpRUGJyQWd6RTJxWVZrek9MOHhBVmVSN1NONXdXb1RXRXlGOEVWN3JyLwo0K0hoSEdpcTVQbXF1SUZ5enpuNi9JWmM4alU5eEVmenZpa2NpckxmVTR2UlhKUXdWd2dBU05sMkFXQUloMmRECmRUcmpCQ2ZpS1dNSHlqMFJiSGFsc0J6T3BnVC9IVHYzR1F6blVRekZLdjJkajVWMU5rUy9ESGp5UlJKK0VMNlEKQlltR3NlZzVQNE5iQzllYnVpcG1NVEFxL0p1bU9vb2QrRmpMMm5acUw2Zkk2ZkJ0RjVPR2xwQ0IxWUo4ZnpDdApHUVFaN0hUSWJkYjJ0cDQzRlZPaHlRYlZjSHFUQTA0UEoxNSswV0F5bVVKVXo4WEE1NDRyL2J2NzRKY0pVUkZoCmFyWmlRd0lEQVFBQm95Y3dKVEFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFMMmhIUmVibEl2VHJTMFNmUVg1RG9ueVVhNy84aTg1endVWApSd3dqdzFuS0U0NDJKbWZWRGZ5b0hRYUM4Ti9MQkxyUXM0U0lqU1JYdmFHU1dSQnRnT1RRV21Db1laMXdSbjdwCndDTXZQTERJdHNWWm90SEZpUFl2b1lHWFFUSXA3YlROMmg1OEJaaEZ3d25nWUovT04zeG1rd29IN1IxYmVxWEYKWHF1TTluekhESk41VlZub1lQR09yRHMwWlg1RnNxNGtWVU0wVExNQm9qN1ZIRDhmU0E5RjRYNU4yMldsZnNPMAo4aksrRFJDWTAyaHBrYTZQQ0pQS0lNOEJaMUFSMG9ZakZxT0plcXpPTjBqcnpYWHh4S2pHVFVUb1BldVA5dCtCCjJOMVA1TnI4a2oxM0lrend5Q1NZclFVN09ZM3ltZmJobHkrcXZxaFVFa014MlQ1SkpmQT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
-    client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdXpHSll2UFo2RG9pNDIxREs4V0phWkNuTkFkMnF6NXAvMDQyb0Z6UVBickFnekUyCnFZVmt6T0w4eEFWZVI3U041d1dvVFdFeUY4RVY3cnIvNCtIaEhHaXE1UG1xdUlGeXp6bjYvSVpjOGpVOXhFZnoKdmlrY2lyTGZVNHZSWEpRd1Z3Z0FTTmwyQVdBSWgyZERkVHJqQkNmaUtXTUh5ajBSYkhhbHNCek9wZ1QvSFR2MwpHUXpuVVF6Rkt2MmRqNVYxTmtTL0RIanlSUkorRUw2UUJZbUdzZWc1UDROYkM5ZWJ1aXBtTVRBcS9KdW1Pb29kCitGakwyblpxTDZmSTZmQnRGNU9HbHBDQjFZSjhmekN0R1FRWjdIVEliZGIydHA0M0ZWT2h5UWJWY0hxVEEwNFAKSjE1KzBXQXltVUpVejhYQTU0NHIvYnY3NEpjSlVSRmhhclppUXdJREFRQUJBb0lCQVFDU0pycjlaeVpiQ2dqegpSL3VKMFZEWCt2aVF4c01BTUZyUjJsOE1GV3NBeHk1SFA4Vk4xYmc5djN0YUVGYnI1U3hsa3lVMFJRNjNQU25DCm1uM3ZqZ3dVQWlScllnTEl5MGk0UXF5VFBOU1V4cnpTNHRxTFBjM3EvSDBnM2FrNGZ2cSsrS0JBUUlqQnloamUKbnVFc1JpMjRzT3NESlM2UDE5NGlzUC9yNEpIM1M5bFZGbkVuOGxUR2c0M1kvMFZoMXl0cnkvdDljWjR5ZUNpNwpjMHFEaTZZcXJZaFZhSW9RRW1VQjdsbHRFZkZzb3l4VDR6RTE5U3pVbkRoMmxjYTF1TzhqcmI4d2xHTzBoQ2JyClB1R1l2WFFQa3Q0VlNmalhvdGJ3d2lBNFRCVERCRzU1bHp6MmNKeS9zSS8zSHlYbEMxcTdXUmRuQVhhZ1F0VzkKOE9DZGRkb0JBb0dCQU5NcUNtSW94REtyckhZZFRxT1M1ZFN4cVMxL0NUN3ZYZ0pScXBqd2Y4WHA2WHo0KzIvTAozVXFaVDBEL3dGTkZkc1Z4eFYxMnNYMUdwMHFWZVlKRld5OVlCaHVSWGpTZ0ZEWldSY1Z1Y01sNVpPTmJsbmZGCjVKQ0xnNXFMZ1g5VTNSRnJrR3A0R241UDQxamg4TnhKVlhzZG5xWE9xNTFUK1RRT1UzdkpGQjc1QW9HQkFPTHcKalp1cnZtVkZyTHdaVGgvRDNpWll5SVV0ZUljZ2NKLzlzbTh6L0pPRmRIbFd4dGRHUFVzYVd1MnBTNEhvckFtbgpqTm4vSTluUXd3enZ3MWUzVVFPbUhMRjVBczk4VU5hbk5TQ0xNMW1yaXZHRXJ1VHFnTDM1bU41eFZPdTUxQU5JCm4yNkFtODBJT2JDeEtLa0R0ZXJSaFhHd3g5c1pONVJCbG9VRThZNGJBb0dBQ3ZsdVhMZWRxcng5VkE0bDNoNXUKVDJXRVUxYjgxZ1orcmtRc1I1S0lNWEw4cllBTElUNUpHKzFuendyN3BkaEFXZmFWdVV2SDRhamdYT0h6MUs5aQpFODNSVTNGMG9ldUg0V01PY1RwU0prWm0xZUlXcWRiaEVCb1FGdUlWTXRib1BsV0d4ZUhFRHJoOEtreGp4aThSCmdEcUQyajRwY1IzQ0g5QjJ5a0lqQjVFQ2dZRUExc0xXLys2enE1c1lNSm14K1JXZThhTXJmL3pjQnVTSU1LQWgKY0dNK0wwMG9RSHdDaUU4TVNqcVN1ajV3R214YUFuanhMb3ZwSFlRV1VmUEVaUW95UE1YQ2VhRVBLOU4xbk8xMwp0V2lHRytIZkIxaU5PazFCc0lhNFNDbndOM1FRVTFzeXBaeEgxT3hueS9LYmkvYmEvWEZ5VzNqMGFUK2YvVWxrCmJGV1ZVdWtDZ1lFQTBaMmRTTFlmTjV5eFNtYk5xMWVqZXdWd1BjRzQxR2hQclNUZEJxdHFac1doWGE3aDdLTWEKeHdvamh5SXpnTXNyK2tXODdlajhDQ2h0d21sQ1p5QU92QmdOZytncnJ1cEZLM3FOSkpKeU9YREdHckdpbzZmTQp5aXB3Q2tZVGVxRThpZ1J6UkI5QkdFUGY4eVpjMUtwdmZhUDVhM0lRZmxiV0czbGpUemNNZVZjPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
diff --git a/pkg/phase/executors/k8s_applier.go b/pkg/phase/executors/k8s_applier.go
index 47a06b1ca..5f9ead8bb 100644
--- a/pkg/phase/executors/k8s_applier.go
+++ b/pkg/phase/executors/k8s_applier.go
@@ -62,6 +62,7 @@ func NewKubeApplierExecutor(cfg ifc.ExecutorConfig) (ifc.Executor, error) {
 	if err != nil {
 		return nil, err
 	}
+
 	return &KubeApplierExecutor{
 		ExecutorBundle:   bundle,
 		BundleName:       cfg.PhaseName,
@@ -128,7 +129,7 @@ func (e *KubeApplierExecutor) prepareApplier(ch chan events.Event) (*k8sapplier.
 	e.cleanup = cleanup
 	log.Printf("Using kubeconfig at '%s' and context '%s'", path, context)
 	factory := utils.FactoryFromKubeConfig(path, context)
-	return k8sapplier.NewApplier(ch, factory), bundle, nil
+	return k8sapplier.NewApplier(ch, factory, e.apiObject.Config.WaitOptions.Conditions), bundle, nil
 }
 
 // Validate document set