From 165edb913367fb6ee2bd421628e526e9a941a72d Mon Sep 17 00:00:00 2001
From: Ruslan Aliev <raliev@mirantis.com>
Date: Wed, 24 Jan 2024 09:58:14 -0600
Subject: [PATCH] Initial commit

Change-Id: Iac0a2e0b278431d53645acafac5556e492f2fbb0
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
---
 .dockerignore                                 |   3 +
 .gitignore                                    |  26 +
 .zuul.yaml                                    |  89 +++
 Makefile                                      | 239 ++++++++
 PROJECT                                       |  20 +
 README.md                                     |  94 +++
 api/v1/armadachart_types.go                   | 309 ++++++++++
 api/v1/groupversion_info.go                   |  36 ++
 api/v1/zz_generated.deepcopy.go               | 293 +++++++++
 cmd/main.go                                   | 138 +++++
 .../armada.airshipit.org_armadacharts.yaml    | 242 ++++++++
 config/crd/kustomization.yaml                 |  21 +
 config/crd/kustomizeconfig.yaml               |  19 +
 .../patches/cainjection_in_armadacharts.yaml  |   7 +
 .../crd/patches/webhook_in_armadacharts.yaml  |  16 +
 config/default/kustomization.yaml             | 144 +++++
 config/default/manager_auth_proxy_patch.yaml  |  39 ++
 config/default/manager_config_patch.yaml      |  10 +
 config/manager/kustomization.yaml             |   8 +
 config/manager/manager.yaml                   | 102 ++++
 config/prometheus/kustomization.yaml          |   2 +
 config/prometheus/monitor.yaml                |  26 +
 config/rbac/armadachart_editor_role.yaml      |  31 +
 config/rbac/armadachart_viewer_role.yaml      |  27 +
 .../rbac/auth_proxy_client_clusterrole.yaml   |  16 +
 config/rbac/auth_proxy_role.yaml              |  24 +
 config/rbac/auth_proxy_role_binding.yaml      |  19 +
 config/rbac/auth_proxy_service.yaml           |  21 +
 config/rbac/cluster_role.yaml                 |  10 +
 config/rbac/cluster_role_binding.yaml         |  12 +
 config/rbac/kustomization.yaml                |  20 +
 config/rbac/leader_election_role.yaml         |  44 ++
 config/rbac/leader_election_role_binding.yaml |  19 +
 config/rbac/role.yaml                         |  32 +
 config/rbac/role_binding.yaml                 |  19 +
 config/rbac/service_account.yaml              |  12 +
 config/samples/armada_v1_armadachart.yaml     |  12 +
 config/samples/kustomization.yaml             |   4 +
 go.mod                                        | 155 +++++
 go.sum                                        | 574 ++++++++++++++++++
 hack/boilerplate.go.txt                       |  15 +
 .../armada-operator/Dockerfile.ubuntu_focal   |  36 ++
 internal/controller/armadachart_controller.go | 417 +++++++++++++
 internal/controller/suite_test.go             |  90 +++
 internal/kube/client.go                       | 213 +++++++
 internal/kube/wait.go                         | 382 ++++++++++++
 internal/runner/log_buffer.go                 |  85 +++
 internal/runner/runner.go                     | 169 ++++++
 tools/gate/playbooks/docker-image-build.yaml  | 126 ++++
 tools/gate/playbooks/vars.yaml                |  19 +
 .../tasks/disable-systemd-resolved.yaml       |  37 ++
 .../disable-systemd-resolved/tasks/main.yaml  |  15 +
 tools/image_tags.py                           | 126 ++++
 53 files changed, 4664 insertions(+)
 create mode 100644 .dockerignore
 create mode 100644 .gitignore
 create mode 100644 .zuul.yaml
 create mode 100644 Makefile
 create mode 100644 PROJECT
 create mode 100644 README.md
 create mode 100644 api/v1/armadachart_types.go
 create mode 100644 api/v1/groupversion_info.go
 create mode 100644 api/v1/zz_generated.deepcopy.go
 create mode 100644 cmd/main.go
 create mode 100644 config/crd/bases/armada.airshipit.org_armadacharts.yaml
 create mode 100644 config/crd/kustomization.yaml
 create mode 100644 config/crd/kustomizeconfig.yaml
 create mode 100644 config/crd/patches/cainjection_in_armadacharts.yaml
 create mode 100644 config/crd/patches/webhook_in_armadacharts.yaml
 create mode 100644 config/default/kustomization.yaml
 create mode 100644 config/default/manager_auth_proxy_patch.yaml
 create mode 100644 config/default/manager_config_patch.yaml
 create mode 100644 config/manager/kustomization.yaml
 create mode 100644 config/manager/manager.yaml
 create mode 100644 config/prometheus/kustomization.yaml
 create mode 100644 config/prometheus/monitor.yaml
 create mode 100644 config/rbac/armadachart_editor_role.yaml
 create mode 100644 config/rbac/armadachart_viewer_role.yaml
 create mode 100644 config/rbac/auth_proxy_client_clusterrole.yaml
 create mode 100644 config/rbac/auth_proxy_role.yaml
 create mode 100644 config/rbac/auth_proxy_role_binding.yaml
 create mode 100644 config/rbac/auth_proxy_service.yaml
 create mode 100644 config/rbac/cluster_role.yaml
 create mode 100644 config/rbac/cluster_role_binding.yaml
 create mode 100644 config/rbac/kustomization.yaml
 create mode 100644 config/rbac/leader_election_role.yaml
 create mode 100644 config/rbac/leader_election_role_binding.yaml
 create mode 100644 config/rbac/role.yaml
 create mode 100644 config/rbac/role_binding.yaml
 create mode 100644 config/rbac/service_account.yaml
 create mode 100644 config/samples/armada_v1_armadachart.yaml
 create mode 100644 config/samples/kustomization.yaml
 create mode 100644 go.mod
 create mode 100644 go.sum
 create mode 100644 hack/boilerplate.go.txt
 create mode 100644 images/armada-operator/Dockerfile.ubuntu_focal
 create mode 100644 internal/controller/armadachart_controller.go
 create mode 100644 internal/controller/suite_test.go
 create mode 100644 internal/kube/client.go
 create mode 100644 internal/kube/wait.go
 create mode 100644 internal/runner/log_buffer.go
 create mode 100644 internal/runner/runner.go
 create mode 100644 tools/gate/playbooks/docker-image-build.yaml
 create mode 100644 tools/gate/playbooks/vars.yaml
 create mode 100644 tools/gate/roles/disable-systemd-resolved/tasks/disable-systemd-resolved.yaml
 create mode 100644 tools/gate/roles/disable-systemd-resolved/tasks/main.yaml
 create mode 100644 tools/image_tags.py

diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..a3aab7a
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
+# Ignore build and test binaries.
+bin/
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7f02333
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+bin/*
+Dockerfile.cross
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Kubernetes Generated files - skip generated files, except for vendored files
+
+!vendor/**/zz_generated.*
+
+# editor and IDE paraphernalia
+.idea
+.vscode
+*.swp
+*.swo
+*~
diff --git a/.zuul.yaml b/.zuul.yaml
new file mode 100644
index 0000000..06ceb6e
--- /dev/null
+++ b/.zuul.yaml
@@ -0,0 +1,89 @@
+# 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.
+
+- project:
+    check:
+      jobs:
+        - armada-operator-docker-build-gate-ubuntu_focal
+
+    gate:
+      jobs:
+        - armada-operator-docker-build-gate-ubuntu_focal
+
+    post:
+      jobs:
+        - armada-operator-docker-publish-ubuntu_focal
+
+
+- nodeset:
+    name: armada-operator-single-node-focal
+    nodes:
+      - name: primary
+        label: ubuntu-focal
+
+
+- job:
+    name: armada-operator-docker-build-gate-ubuntu_focal
+    timeout: 3600
+    run: tools/gate/playbooks/docker-image-build.yaml
+    nodeset: armada-single-node-focal
+    vars:
+      publish: false
+      distro: ubuntu_focal
+      tags:
+        dynamic:
+          patch_set: true
+
+
+- job:
+    name: armada-operator-docker-publish-ubuntu_focal
+    timeout: 3600
+    run: tools/gate/playbooks/docker-image-build.yaml
+    nodeset: armada-single-node-focal
+    secrets:
+      - airship_armada_operator_quay_creds
+    vars:
+      publish: true
+      distro: ubuntu_focal
+      tags:
+        dynamic:
+          branch: true
+          commit: true
+        static:
+          - latest
+
+
+- secret:
+    name: airship_armada_operator_quay_creds
+    data:
+      username: !encrypted/pkcs1-oaep
+        - DjQA+Mkrg0oNfTcPFBCOwx0K+B7LsV9ceV7MK9C83sZDKUC5aKfhdn/myvKQKunIth4B8
+          y++q+ano+rk3fteyT5hAT73e59koN0EkHrMknqdm8C0AMXoGJ2ktZFwho0ehzj8WX43hW
+          67cWRYUHImOxuUn9oaMT11ZrDCBbAz1gKLRGPgtiYaEnKjaksiAcaBY1xIIjvr+DFbBZ3
+          CmZ39EKRwhJxfGBA7nKbC4fFTQpR9GQD6SBR5kz8J6nIzeywJ4KQQEoICb9kwa0y4us0D
+          efxej72cEI31FXGeV9Hm6YpaKSBL/Ko67rrBU2P8+kdqtI5mTKyj97yMwEMqPn6E2RkQZ
+          44l9OAOSJIYtHQyvdCfpoYhzojZRabpcKkgB1Lq///ysmRdDWA3CBTurIyR8zjJdGreaY
+          DYiWFen50tlAgRwewWgWEIqPnmEKYCGKLF5BAK49NQWkcr/2d+TQLcyE7IYWJtp1VEHnb
+          8t8gzvhwWcR0AKmxOMGO/TUuBw70nCl5FUl/lxBFVvlQS0eGM8ZcDvs3survIgjmHsyyI
+          6LGuB6Yh+Qbw+YmBxSsp75mm9D+v0mFtdxCgs9JMTPFI3d40CGhDbAXfCYHqTrh71pSCq
+          a+ZWtixKOqhmIY6T9QCyFvBPB2ozQbmfgIlrRcs5pcNzO/xQfGf8JDiIceG7nE=
+      password: !encrypted/pkcs1-oaep
+        - c3cKJlMVv6mXnkCPPVyfJ9Vra140TOZKIK041eaajHnqoxR+R2sI50PrvF3r0rqnCJ4hs
+          955XM9Idk5Y6urkkg5XAPDXLU7scBwN/WxO6KWadwtoxSFgn3sF/bctE55m18PZakUo3u
+          oB05pBRlS0NITf6PCvwi0CphJbJEayVsPuNGP2EqP5WJ8xOrRu48SiuOlF0UuTA05oCta
+          oYMG691bTt8EdDawaVpqtDcOcGEEqjXfizCoTiZhSYbtU8u86QXysdhKTFkPZiVd5kKiH
+          q3zKLH3DEjK2XdSv4vouarmgKFymJ1+1hfUdfR2zNiJDM7a1YjEkOKlC+MaTBbjaoL7TF
+          DfYgBkef10U+pp8VpM+zaU8/nJgeAn1rJzEm6IbYqW8Cd6N7IC1qLPU8GG1d0QtrYy7FP
+          xr/5aAtiGFQ9dyUuwqBgNGngu5RXKJdnXD0yaUQ2Ptju5fwUo/vnQId/K+L8KIOjjRd9h
+          SYsOer9caZOZ8Sin7udDzH/L/yFQQeu/HCZqWNtMJR49a2TvI31v75TtoY2Z2JhkEIGs7
+          PryaAxEZywk1xP701FLFsdIWPbpvu+TElNnbEc+dI6HXtdpV6hArHI1LY0qplLtNV7c/g
+          GRXIKFyn7yVMpdRMt9i+IncN2m/xskSIn4IOgcLbTD4giFDE2ZBdpEareD4kEA=
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4580d6c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,239 @@
+# APP INFO
+BUILD_DIR         := $(shell mktemp -d)
+DOCKER_REGISTRY   ?= quay.io
+IMAGE_PREFIX      ?= airshipit
+IMAGE_NAME        ?= armada-operator
+IMAGE_TAG         ?= latest
+PROXY             ?= http://proxy.foo.com:8000
+NO_PROXY          ?= localhost,127.0.0.1,.svc.cluster.local
+USE_PROXY         ?= false
+PUSH_IMAGE        ?= false
+# use this variable for image labels added in internal build process
+LABEL             ?= org.airshipit.build=community
+COMMIT            ?= $(shell git rev-parse HEAD)
+PYTHON            = python3
+CHARTS            := $(filter-out deps, $(patsubst charts/%/.,%,$(wildcard charts/*/.)))
+DISTRO            ?= ubuntu_focal
+IMAGE             := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${IMAGE_NAME}:${IMAGE_TAG}-${DISTRO}
+UBUNTU_BASE_IMAGE ?=
+
+# VERSION INFO
+GIT_COMMIT = $(shell git rev-parse HEAD)
+GIT_SHA    = $(shell git rev-parse --short HEAD)
+GIT_TAG    = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null)
+GIT_DIRTY  = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean")
+
+ifdef VERSION
+	DOCKER_VERSION = $(VERSION)
+endif
+
+SHELL = /bin/bash
+
+info:
+	@echo "Version:           ${VERSION}"
+	@echo "Git Tag:           ${GIT_TAG}"
+	@echo "Git Commit:        ${GIT_COMMIT}"
+	@echo "Git Tree State:    ${GIT_DIRTY}"
+	@echo "Docker Version:    ${DOCKER_VERSION}"
+	@echo "Registry:          ${DOCKER_REGISTRY}"
+# Image URL to use all building/pushing image targets
+IMG ?= quay.io/airshipit/armada-operator:latest
+#IMG ?= docker-open-nc.zc1.cti.att.com/upstream-local/raliev12/armada-controller:latest
+# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
+ENVTEST_K8S_VERSION = 1.28.0
+
+# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
+ifeq (,$(shell go env GOBIN))
+GOBIN=$(shell go env GOPATH)/bin
+else
+GOBIN=$(shell go env GOBIN)
+endif
+
+# CONTAINER_TOOL defines the container tool to be used for building images.
+# Be aware that the target commands are only tested with Docker which is
+# scaffolded by default. However, you might want to replace it to use other
+# tools. (i.e. podman)
+CONTAINER_TOOL ?= docker
+
+# Setting SHELL to bash allows bash commands to be executed by recipes.
+# Options are set to exit when a recipe line exits non-zero or a piped command fails.
+SHELL = /usr/bin/env bash -o pipefail
+.SHELLFLAGS = -ec
+
+.PHONY: all
+all: build
+
+##@ General
+
+# The help target prints out all targets with their descriptions organized
+# beneath their categories. The categories are represented by '##@' and the
+# target descriptions by '##'. The awk command is responsible for reading the
+# entire set of makefiles included in this invocation, looking for lines of the
+# file as xyz: ## something, and then pretty-format the target and help. Then,
+# if there's a line with ##@ something, that gets pretty-printed as a category.
+# More info on the usage of ANSI control characters for terminal formatting:
+# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
+# More info on the awk command:
+# http://linuxcommand.org/lc3_adv_awk.php
+
+.PHONY: help
+help: ## Display this help.
+	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
+
+##@ Development
+
+.PHONY: manifests
+manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
+	$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
+
+.PHONY: generate
+generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
+	$(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..."
+
+.PHONY: fmt
+fmt: ## Run go fmt against code.
+	go fmt ./...
+
+.PHONY: vet
+vet: ## Run go vet against code.
+	go vet ./...
+
+.PHONY: test
+test: manifests generate fmt vet envtest ## Run tests.
+	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
+
+##@ Build
+
+.PHONY: build
+build: manifests generate fmt vet ## Build manager binary.
+	go build -o bin/manager cmd/main.go
+
+.PHONY: run
+run: manifests generate fmt vet ## Run a controller from your host.
+	go run ./cmd/main.go
+
+# If you wish to build the manager image targeting other platforms you can use the --platform flag.
+# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
+# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
+.PHONY: docker-build
+docker-build: ## Build docker image with the manager.
+	$(CONTAINER_TOOL) build -t ${IMG} .
+
+.PHONY: docker-push
+docker-push: ## Push docker image with the manager.
+	$(CONTAINER_TOOL) push ${IMG}
+
+# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
+# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
+# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
+# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
+# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=<myregistry/image:<tag>> then the export will fail)
+# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
+PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
+.PHONY: docker-buildx
+docker-buildx: ## Build and push docker image for the manager for cross-platform support
+	# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
+	sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
+	- $(CONTAINER_TOOL) buildx create --name project-v3-builder
+	$(CONTAINER_TOOL) buildx use project-v3-builder
+	- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
+	- $(CONTAINER_TOOL) buildx rm project-v3-builder
+	rm Dockerfile.cross
+
+check-docker:
+	@if [ -z $$(which docker) ]; then \
+		echo "Missing \`docker\` client which is required for development"; \
+		exit 2; \
+	fi
+
+images: check-docker build_armada_operator
+
+_BASE_IMAGE_ARG := $(if $(UBUNTU_BASE_IMAGE),--build-arg FROM="${UBUNTU_BASE_IMAGE}" ,)
+
+build_armada_operator:
+ifeq ($(USE_PROXY), true)
+	docker build --network host -t $(IMAGE) --label $(LABEL) \
+		--label "org.opencontainers.image.revision=$(COMMIT)" \
+		--label "org.opencontainers.image.created=$(shell date --rfc-3339=seconds --utc)" \
+		--label "org.opencontainers.image.title=$(IMAGE_NAME)" \
+		-f images/armada-operator/Dockerfile.$(DISTRO) \
+		$(_BASE_IMAGE_ARG) \
+		--build-arg HELM_ARTIFACT_URL=$(HELM_ARTIFACT_URL) \
+		--build-arg http_proxy=$(PROXY) \
+		--build-arg https_proxy=$(PROXY) \
+		--build-arg HTTP_PROXY=$(PROXY) \
+		--build-arg HTTPS_PROXY=$(PROXY) \
+		--build-arg no_proxy=$(NO_PROXY) \
+		--build-arg NO_PROXY=$(NO_PROXY) .
+else
+	docker build --network host -t $(IMAGE) --label $(LABEL) \
+		--label "org.opencontainers.image.revision=$(COMMIT)" \
+		--label "org.opencontainers.image.created=$(shell date --rfc-3339=seconds --utc)" \
+		--label "org.opencontainers.image.title=$(IMAGE_NAME)" \
+		-f images/armada-operator/Dockerfile.$(DISTRO) \
+		$(_BASE_IMAGE_ARG) \
+		--build-arg HELM_ARTIFACT_URL=$(HELM_ARTIFACT_URL) .
+endif
+ifeq ($(PUSH_IMAGE), true)
+	docker push $(IMAGE)
+endif
+
+##@ Deployment
+
+ifndef ignore-not-found
+  ignore-not-found = false
+endif
+
+.PHONY: install
+install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
+	$(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f -
+
+.PHONY: uninstall
+uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
+	$(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -
+
+.PHONY: deploy
+deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
+	cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
+	$(KUSTOMIZE) build config/default | $(KUBECTL) apply -f -
+
+.PHONY: undeploy
+undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
+	$(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -
+
+##@ Build Dependencies
+
+## Location to install dependencies to
+LOCALBIN ?= $(shell pwd)/bin
+$(LOCALBIN):
+	mkdir -p $(LOCALBIN)
+
+## Tool Binaries
+KUBECTL ?= kubectl
+KUSTOMIZE ?= $(LOCALBIN)/kustomize
+CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
+ENVTEST ?= $(LOCALBIN)/setup-envtest
+
+## Tool Versions
+KUSTOMIZE_VERSION ?= v5.1.1
+CONTROLLER_TOOLS_VERSION ?= v0.13.0
+
+.PHONY: kustomize
+kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading.
+$(KUSTOMIZE): $(LOCALBIN)
+	@if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \
+		echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \
+		rm -rf $(LOCALBIN)/kustomize; \
+	fi
+	test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION)
+
+.PHONY: controller-gen
+controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten.
+$(CONTROLLER_GEN): $(LOCALBIN)
+	test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \
+	GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
+
+.PHONY: envtest
+envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
+$(ENVTEST): $(LOCALBIN)
+	test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
diff --git a/PROJECT b/PROJECT
new file mode 100644
index 0000000..0b2a6cf
--- /dev/null
+++ b/PROJECT
@@ -0,0 +1,20 @@
+# Code generated by tool. DO NOT EDIT.
+# This file is used to track the info used to scaffold your project
+# and allow the plugins properly work.
+# More info: https://book.kubebuilder.io/reference/project-config.html
+domain: airshipit.org
+layout:
+- go.kubebuilder.io/v4
+projectName: armada-operator
+repo: opendev.org/airship/armada-operator
+resources:
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: airshipit.org
+  group: armada
+  kind: ArmadaChart
+  path: opendev.org/airship/armada-operator/api/v1
+  version: v1
+version: "3"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4796c73
--- /dev/null
+++ b/README.md
@@ -0,0 +1,94 @@
+# armada-operator
+
+
+## Description
+// TODO(user): An in-depth paragraph about your project and overview of use
+
+## Getting Started
+You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster.
+**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows).
+
+### Running on the cluster
+1. Install Instances of Custom Resources:
+
+```sh
+kubectl apply -k config/samples/
+```
+
+2. Build and push your image to the location specified by `IMG`:
+
+```sh
+make docker-build docker-push IMG=<some-registry>/armada-operator:tag
+```
+
+3. Deploy the controller to the cluster with the image specified by `IMG`:
+
+```sh
+make deploy IMG=<some-registry>/armada-operator:tag
+```
+
+### Uninstall CRDs
+To delete the CRDs from the cluster:
+
+```sh
+make uninstall
+```
+
+### Undeploy controller
+UnDeploy the controller from the cluster:
+
+```sh
+make undeploy
+```
+
+## Contributing
+// TODO(user): Add detailed information on how you would like others to contribute to this project
+
+### How it works
+This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/).
+
+It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/),
+which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster.
+
+### Test It Out
+1. Install the CRDs into the cluster:
+
+```sh
+make install
+```
+
+2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running):
+
+```sh
+make run
+```
+
+**NOTE:** You can also run this in one step by running: `make install run`
+
+### Modifying the API definitions
+If you are editing the API definitions, generate the manifests such as CRs or CRDs using:
+
+```sh
+make manifests
+```
+
+**NOTE:** Run `make --help` for more information on all potential `make` targets
+
+More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)
+
+## License
+
+Copyright 2023.
+
+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.
+
diff --git a/api/v1/armadachart_types.go b/api/v1/armadachart_types.go
new file mode 100644
index 0000000..42099c1
--- /dev/null
+++ b/api/v1/armadachart_types.go
@@ -0,0 +1,309 @@
+/*
+Copyright 2023.
+
+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.
+*/
+
+package v1
+
+import (
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	apimeta "k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/runtime/serializer"
+	"k8s.io/client-go/rest"
+)
+
+const ArmadaChartKind = "ArmadaChart"
+const ArmadaChartAPIVersion = "armada.airshipit.org/v1"
+const ArmadaChartPlural = "armadacharts"
+const ArmadaChartGroup = "armada.airshipit.org"
+const ArmadaChartVersion = "v1"
+const ArmadaChartLabel = "armada.airshipit.org/release-name"
+const ArmadaChartFinalizer = "finalizers.armada.airshipit.org"
+
+const ReadyCondition string = "Ready"
+const ReconcilingCondition string = "Reconciling"
+const ProgressingReason string = "Progressing"
+
+// ArmadaChartSpec defines the specification of ArmadaChart
+type ArmadaChartSpec struct {
+	// ChartName is name of ArmadaChart
+	ChartName string `json:"chart_name,omitempty"`
+	// Namespace is a namespace for ArmadaChart
+	Namespace string `json:"namespace,omitempty"`
+	// Release is a name of corresponding Helm Release of ArmadaChart
+	Release string `json:"release,omitempty"`
+
+	// Source is a source location of Helm Chart *.tgz
+	Source ArmadaChartSource `json:"source,omitempty"`
+
+	// Values holds the values for this Helm release.
+	// +optional
+	Values *apiextensionsv1.JSON `json:"values,omitempty"`
+
+	// Wait holds the wait options  for this Helm release.
+	// +optional
+	Wait ArmadaChartWait `json:"wait,omitempty"`
+
+	// Test holds the test parameters for this Helm release.
+	// +optional
+	Test ArmadaChartTestOptions `json:"test,omitempty"`
+
+	// Upgrade holds the upgrade options for this Helm release.
+	// +optional
+	Upgrade ArmadaChartUpgrade `json:"upgrade,omitempty"`
+}
+
+// ArmadaChartSource defines the location options of Helm Release for ArmadaChart
+type ArmadaChartSource struct {
+	Location string `json:"location,omitempty"`
+	Subpath  string `json:"subpath,omitempty"`
+	Type     string `json:"type,omitempty"`
+}
+
+// ArmadaChartWait defines the wait options of ArmadaChart
+type ArmadaChartWait struct {
+	// Timeout is the time to wait for full reconciliation of Helm release.
+	// +optional
+	Timeout int `json:"timeout,omitempty"`
+
+	Labels map[string]string `json:"labels,omitempty"`
+
+	// +optional
+	ArmadaChartWaitResources []ArmadaChartWaitResource `json:"resources"`
+
+	// +optional
+	Native *ArmadaChartWaitNative `json:"native,omitempty"`
+}
+
+// ArmadaChartWaitResource defines the wait options of ArmadaChart
+type ArmadaChartWaitResource struct {
+	Type string `json:"type,omitempty"`
+
+	Labels map[string]string `json:"labels,omitempty"`
+
+	// +optional
+	MinReady string `json:"min_ready,omitempty"`
+}
+
+type ArmadaChartUpgrade struct {
+	PreUpgrade ArmadaChartPreUpgrade `json:"pre,omitempty"`
+}
+
+type ArmadaChartPreUpgrade struct {
+	Delete []ArmadaChartDeleteResource `json:"delete,omitempty"`
+}
+
+// ArmadaChartDeleteResource defines the delete options of ArmadaChart
+type ArmadaChartDeleteResource struct {
+	Type string `json:"type,omitempty"`
+
+	Labels map[string]string `json:"labels,omitempty"`
+}
+
+// ArmadaChartWaitNative defines the wait options of ArmadaChart
+type ArmadaChartWaitNative struct {
+	Enabled bool `json:"enabled,omitempty"`
+}
+
+// ArmadaChartTestOptions defines the test options of ArmadaChart
+type ArmadaChartTestOptions struct {
+	// Enabled is an example field of ArmadaChart. Edit armadachart_types.go to remove/update
+	Enabled bool `json:"enabled,omitempty"`
+}
+
+// ArmadaChartStatus defines the observed state of ArmadaChart
+type ArmadaChartStatus struct {
+	// ObservedGeneration is the last observed generation.
+	// +optional
+	ObservedGeneration int64 `json:"observedGeneration,omitempty"`
+
+	// Conditions holds the conditions for the ArmadaChart.
+	// +optional
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
+
+	// LastAppliedRevision is the revision of the last successfully applied source.
+	// +optional
+	LastAppliedRevision string `json:"lastAppliedRevision,omitempty"`
+
+	// LastAttemptedRevision is the revision of the last reconciliation attempt.
+	// +optional
+	LastAttemptedRevision string `json:"lastAttemptedRevision,omitempty"`
+
+	// LastAttemptedValuesChecksum is the SHA1 checksum of the values of the last
+	// reconciliation attempt.
+	// +optional
+	LastAttemptedValuesChecksum string `json:"lastAttemptedValuesChecksum,omitempty"`
+
+	// LastReleaseRevision is the revision of the last successful Helm release.
+	// +optional
+	LastReleaseRevision int `json:"lastReleaseRevision,omitempty"`
+
+	// HelmChart is the namespaced name of the HelmChart resource created by
+	// the controller for the ArmadaChart.
+	// +optional
+	HelmChart string `json:"helmChart,omitempty"`
+
+	// Failures is the reconciliation failure count against the latest desired
+	// state. It is reset after a successful reconciliation.
+	// +optional
+	Failures int64 `json:"failures,omitempty"`
+
+	// InstallFailures is the install failure count against the latest desired
+	// state. It is reset after a successful reconciliation.
+	// +optional
+	InstallFailures int64 `json:"installFailures,omitempty"`
+
+	// UpgradeFailures is the upgrade failure count against the latest desired
+	// state. It is reset after a successful reconciliation.
+	// +optional
+	UpgradeFailures int64 `json:"upgradeFailures,omitempty"`
+
+	// Tested is the bool value whether the Helm Release was successfully
+	// tested or not.
+	// +optional
+	Tested bool `json:"tested,omitempty"`
+}
+
+// ArmadaChartProgressing resets any failures and registers progress toward
+// reconciling the given ArmadaChart by setting the ReadyCondition to
+// 'Unknown' for ProgressingReason.
+func ArmadaChartProgressing(ac ArmadaChart) ArmadaChart {
+	ac.Status.Conditions = []metav1.Condition{}
+	newCondition := metav1.Condition{
+		Type:    ReadyCondition,
+		Status:  metav1.ConditionUnknown,
+		Reason:  ProgressingReason,
+		Message: "Reconciliation in progress",
+	}
+	apimeta.SetStatusCondition(ac.GetStatusConditions(), newCondition)
+	resetFailureCounts(&ac)
+	resetTested(&ac)
+	return ac
+}
+
+// ArmadaChartNotReady registers a failed reconciliation of the given ArmadaChart.
+func ArmadaChartNotReady(ac ArmadaChart, reason, message string) ArmadaChart {
+	newCondition := metav1.Condition{
+		Type:    ReadyCondition,
+		Status:  metav1.ConditionFalse,
+		Reason:  reason,
+		Message: message,
+	}
+	apimeta.SetStatusCondition(ac.GetStatusConditions(), newCondition)
+	ac.Status.Failures++
+	resetTested(&ac)
+	return ac
+}
+
+// ArmadaChartReady registers a successful reconciliation of the given ArmadaChart.
+func ArmadaChartReady(ac ArmadaChart) ArmadaChart {
+	newCondition := metav1.Condition{
+		Type:    ReadyCondition,
+		Status:  metav1.ConditionTrue,
+		Reason:  "ReconciliationSucceeded",
+		Message: "Release reconciliation succeeded",
+	}
+	apimeta.SetStatusCondition(ac.GetStatusConditions(), newCondition)
+	ac.Status.LastAppliedRevision = ac.Status.LastAttemptedRevision
+	resetFailureCounts(&ac)
+	setTested(&ac)
+	return ac
+}
+
+func resetFailureCounts(hr *ArmadaChart) {
+	hr.Status.Failures = 0
+	hr.Status.InstallFailures = 0
+	hr.Status.UpgradeFailures = 0
+}
+
+func resetTested(hr *ArmadaChart) {
+	hr.Status.Tested = false
+}
+
+func setTested(hr *ArmadaChart) {
+	hr.Status.Tested = true
+}
+
+func NewForConfigOrDie(c *rest.Config) *rest.RESTClient {
+	cs, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return cs
+}
+
+func NewForConfig(c *rest.Config) (*rest.RESTClient, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.UnversionedRESTClientFor(&config)
+
+	if err != nil {
+		return nil, err
+	}
+	return client, nil
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := schema.GroupVersion{Group: ArmadaChartGroup, Version: ArmadaChartVersion}
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+
+	sch := runtime.NewScheme()
+	if err := AddToScheme(sch); err != nil {
+		return err
+	}
+	sch.AddUnversionedTypes(gv, &ArmadaChart{}, &ArmadaChartList{})
+	config.NegotiatedSerializer = serializer.NewCodecFactory(sch)
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+//+kubebuilder:object:root=true
+//+kubebuilder:subresource:status
+
+// ArmadaChart is the Schema for the armadacharts API
+type ArmadaChart struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   ArmadaChartSpec   `json:"data,omitempty"`
+	Status ArmadaChartStatus `json:"status,omitempty"`
+}
+
+// GetStatusConditions returns a pointer to the Status.Conditions slice.
+func (in *ArmadaChart) GetStatusConditions() *[]metav1.Condition {
+	return &in.Status.Conditions
+}
+
+//+kubebuilder:object:root=true
+
+// ArmadaChartList contains a list of ArmadaChart
+type ArmadaChartList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []ArmadaChart `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&ArmadaChart{}, &ArmadaChartList{})
+}
diff --git a/api/v1/groupversion_info.go b/api/v1/groupversion_info.go
new file mode 100644
index 0000000..f34a140
--- /dev/null
+++ b/api/v1/groupversion_info.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2023.
+
+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.
+*/
+
+// Package v1 contains API Schema definitions for the armada v1 API group
+// +kubebuilder:object:generate=true
+// +groupName=armada.airshipit.org
+package v1
+
+import (
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+	// GroupVersion is group version used to register these objects
+	GroupVersion = schema.GroupVersion{Group: "armada.airshipit.org", Version: "v1"}
+
+	// SchemeBuilder is used to add go types to the GroupVersionKind scheme
+	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+	// AddToScheme adds the types in this group-version to the given scheme.
+	AddToScheme = SchemeBuilder.AddToScheme
+)
diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go
new file mode 100644
index 0000000..498abe5
--- /dev/null
+++ b/api/v1/zz_generated.deepcopy.go
@@ -0,0 +1,293 @@
+//go:build !ignore_autogenerated
+
+/*
+Copyright 2023.
+
+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.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1
+
+import (
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChart) DeepCopyInto(out *ArmadaChart) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChart.
+func (in *ArmadaChart) DeepCopy() *ArmadaChart {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChart)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ArmadaChart) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartDeleteResource) DeepCopyInto(out *ArmadaChartDeleteResource) {
+	*out = *in
+	if in.Labels != nil {
+		in, out := &in.Labels, &out.Labels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartDeleteResource.
+func (in *ArmadaChartDeleteResource) DeepCopy() *ArmadaChartDeleteResource {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartDeleteResource)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartList) DeepCopyInto(out *ArmadaChartList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]ArmadaChart, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartList.
+func (in *ArmadaChartList) DeepCopy() *ArmadaChartList {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ArmadaChartList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartPreUpgrade) DeepCopyInto(out *ArmadaChartPreUpgrade) {
+	*out = *in
+	if in.Delete != nil {
+		in, out := &in.Delete, &out.Delete
+		*out = make([]ArmadaChartDeleteResource, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartPreUpgrade.
+func (in *ArmadaChartPreUpgrade) DeepCopy() *ArmadaChartPreUpgrade {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartPreUpgrade)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartSource) DeepCopyInto(out *ArmadaChartSource) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartSource.
+func (in *ArmadaChartSource) DeepCopy() *ArmadaChartSource {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartSource)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartSpec) DeepCopyInto(out *ArmadaChartSpec) {
+	*out = *in
+	out.Source = in.Source
+	if in.Values != nil {
+		in, out := &in.Values, &out.Values
+		*out = new(apiextensionsv1.JSON)
+		(*in).DeepCopyInto(*out)
+	}
+	in.Wait.DeepCopyInto(&out.Wait)
+	out.Test = in.Test
+	in.Upgrade.DeepCopyInto(&out.Upgrade)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartSpec.
+func (in *ArmadaChartSpec) DeepCopy() *ArmadaChartSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartStatus) DeepCopyInto(out *ArmadaChartStatus) {
+	*out = *in
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]metav1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartStatus.
+func (in *ArmadaChartStatus) DeepCopy() *ArmadaChartStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartTestOptions) DeepCopyInto(out *ArmadaChartTestOptions) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartTestOptions.
+func (in *ArmadaChartTestOptions) DeepCopy() *ArmadaChartTestOptions {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartTestOptions)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartUpgrade) DeepCopyInto(out *ArmadaChartUpgrade) {
+	*out = *in
+	in.PreUpgrade.DeepCopyInto(&out.PreUpgrade)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartUpgrade.
+func (in *ArmadaChartUpgrade) DeepCopy() *ArmadaChartUpgrade {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartUpgrade)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartWait) DeepCopyInto(out *ArmadaChartWait) {
+	*out = *in
+	if in.Labels != nil {
+		in, out := &in.Labels, &out.Labels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	if in.ArmadaChartWaitResources != nil {
+		in, out := &in.ArmadaChartWaitResources, &out.ArmadaChartWaitResources
+		*out = make([]ArmadaChartWaitResource, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.Native != nil {
+		in, out := &in.Native, &out.Native
+		*out = new(ArmadaChartWaitNative)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartWait.
+func (in *ArmadaChartWait) DeepCopy() *ArmadaChartWait {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartWait)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartWaitNative) DeepCopyInto(out *ArmadaChartWaitNative) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartWaitNative.
+func (in *ArmadaChartWaitNative) DeepCopy() *ArmadaChartWaitNative {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartWaitNative)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArmadaChartWaitResource) DeepCopyInto(out *ArmadaChartWaitResource) {
+	*out = *in
+	if in.Labels != nil {
+		in, out := &in.Labels, &out.Labels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArmadaChartWaitResource.
+func (in *ArmadaChartWaitResource) DeepCopy() *ArmadaChartWaitResource {
+	if in == nil {
+		return nil
+	}
+	out := new(ArmadaChartWaitResource)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 0000000..063f6d4
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,138 @@
+/*
+Copyright 2023.
+
+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.
+*/
+
+package main
+
+import (
+	"flag"
+	"os"
+	"time"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
+	// to ensure that exec-entrypoint and run can make use of them.
+	_ "k8s.io/client-go/plugin/pkg/client/auth"
+	"k8s.io/klog/v2"
+	ctrl "sigs.k8s.io/controller-runtime"
+	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/config"
+	"sigs.k8s.io/controller-runtime/pkg/healthz"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+
+	armadav1 "opendev.org/airship/armada-operator/api/v1"
+	"opendev.org/airship/armada-operator/internal/controller"
+	//+kubebuilder:scaffold:imports
+)
+
+var (
+	scheme   = runtime.NewScheme()
+	setupLog = ctrl.Log.WithName("setup")
+)
+
+func init() {
+	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
+
+	utilruntime.Must(armadav1.AddToScheme(scheme))
+	//+kubebuilder:scaffold:scheme
+}
+
+func main() {
+	var metricsAddr string
+	var enableLeaderElection bool
+	var probeAddr string
+	var leaderElectNamespace string
+	flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
+	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
+	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
+		"Enable leader election for controller manager. "+
+			"Enabling this will ensure there is only one active controller manager.")
+	flag.StringVar(&leaderElectNamespace, "leader-elect-namespace", "", "Specify leader election namespace")
+	flag.StringVar(&leaderElectNamespace, "log-to-file", "", "Specify leader election namespace")
+	opts := zap.Options{
+		Development: true,
+	}
+	opts.BindFlags(flag.CommandLine)
+	flag.Parse()
+
+	managerLogger := zap.New()
+	ctrl.SetLogger(managerLogger)
+	klog.SetLoggerWithOptions(managerLogger.WithName("runtime"), klog.ContextualLogger(true))
+
+	leaseDuration := 35 * time.Second
+	renewDeadline := 30 * time.Second
+	retryPeriod := 5 * time.Second
+	managerOpts := ctrl.Options{
+		Scheme:                 scheme,
+		Metrics:                metricsserver.Options{BindAddress: metricsAddr},
+		HealthProbeBindAddress: probeAddr,
+		LeaderElection:         enableLeaderElection,
+		LeaderElectionID:       "cac83074.airshipit.org",
+		LeaseDuration:          &leaseDuration,
+		RenewDeadline:          &renewDeadline,
+		RetryPeriod:            &retryPeriod,
+		Controller: ctrlcfg.Controller{
+			MaxConcurrentReconciles: 20,
+		},
+		Logger: ctrl.Log,
+		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
+		// when the Manager ends. This requires the binary to immediately end when the
+		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
+		// speeds up voluntary leader transitions as the new leader don't have to wait
+		// LeaseDuration time first.
+		//
+		// In the default scaffold provided, the program ends immediately after
+		// the manager stops, so would be fine to enable this option. However,
+		// if you are doing or is intended to do any operation such as perform cleanups
+		// after the manager stops then its usage might be unsafe.
+		// LeaderElectionReleaseOnCancel: true,
+	}
+
+	if leaderElectNamespace != "" {
+		managerOpts.LeaderElectionNamespace = leaderElectNamespace
+	}
+
+	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), managerOpts)
+	if err != nil {
+		setupLog.Error(err, "unable to start manager")
+		os.Exit(1)
+	}
+
+	if err = (&controller.ArmadaChartReconciler{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "ArmadaChart")
+		os.Exit(1)
+	}
+	//+kubebuilder:scaffold:builder
+
+	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
+		setupLog.Error(err, "unable to set up health check")
+		os.Exit(1)
+	}
+	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
+		setupLog.Error(err, "unable to set up ready check")
+		os.Exit(1)
+	}
+
+	setupLog.Info("starting manager")
+	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
+		setupLog.Error(err, "problem running manager")
+		os.Exit(1)
+	}
+}
diff --git a/config/crd/bases/armada.airshipit.org_armadacharts.yaml b/config/crd/bases/armada.airshipit.org_armadacharts.yaml
new file mode 100644
index 0000000..1059a3b
--- /dev/null
+++ b/config/crd/bases/armada.airshipit.org_armadacharts.yaml
@@ -0,0 +1,242 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.13.0
+  name: armadacharts.armada.airshipit.org
+spec:
+  group: armada.airshipit.org
+  names:
+    kind: ArmadaChart
+    listKind: ArmadaChartList
+    plural: armadacharts
+    singular: armadachart
+  scope: Namespaced
+  versions:
+  - name: v1
+    schema:
+      openAPIV3Schema:
+        description: ArmadaChart is the Schema for the armadacharts API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          data:
+            description: ArmadaChartSpec defines the specification of ArmadaChart
+            properties:
+              chart_name:
+                description: ChartName is name of ArmadaChart
+                type: string
+              namespace:
+                description: Namespace is a namespace for ArmadaChart
+                type: string
+              release:
+                description: Release is a name of corresponding Helm Release of ArmadaChart
+                type: string
+              source:
+                description: Source is a source location of Helm Chart *.tgz
+                properties:
+                  location:
+                    type: string
+                  subpath:
+                    type: string
+                  type:
+                    type: string
+                type: object
+              test:
+                description: Test holds the test parameters for this Helm release.
+                properties:
+                  enabled:
+                    description: Enabled is an example field of ArmadaChart. Edit
+                      armadachart_types.go to remove/update
+                    type: boolean
+                type: object
+              upgrade:
+                description: Upgrade holds the upgrade options for this Helm release.
+                properties:
+                  pre:
+                    properties:
+                      delete:
+                        items:
+                          description: ArmadaChartDeleteResource defines the delete
+                            options of ArmadaChart
+                          properties:
+                            labels:
+                              additionalProperties:
+                                type: string
+                              type: object
+                            type:
+                              type: string
+                          type: object
+                        type: array
+                    type: object
+                type: object
+              values:
+                description: Values holds the values for this Helm release.
+                x-kubernetes-preserve-unknown-fields: true
+              wait:
+                description: Wait holds the wait options  for this Helm release.
+                properties:
+                  labels:
+                    additionalProperties:
+                      type: string
+                    type: object
+                  native:
+                    description: ArmadaChartWaitNative defines the wait options of
+                      ArmadaChart
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
+                  resources:
+                    items:
+                      description: ArmadaChartWaitResource defines the wait options
+                        of ArmadaChart
+                      properties:
+                        labels:
+                          additionalProperties:
+                            type: string
+                          type: object
+                        min_ready:
+                          type: string
+                        type:
+                          type: string
+                      type: object
+                    type: array
+                  timeout:
+                    description: Timeout is the time to wait for full reconciliation
+                      of Helm release.
+                    type: integer
+                type: object
+            type: object
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          status:
+            description: ArmadaChartStatus defines the observed state of ArmadaChart
+            properties:
+              conditions:
+                description: Conditions holds the conditions for the ArmadaChart.
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource. --- This struct is intended for direct
+                    use as an array at the field path .status.conditions.  For example,
+                    \n type FooStatus struct{ // Represents the observations of a
+                    foo's current state. // Known .status.conditions.type are: \"Available\",
+                    \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
+                    // +listType=map // +listMapKey=type Conditions []metav1.Condition
+                    `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+                    protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
+                  properties:
+                    lastTransitionTime:
+                      description: lastTransitionTime is the last time the condition
+                        transitioned from one status to another. This should be when
+                        the underlying condition changed.  If that is not known, then
+                        using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: message is a human readable message indicating
+                        details about the transition. This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: observedGeneration represents the .metadata.generation
+                        that the condition was set based upon. For instance, if .metadata.generation
+                        is currently 12, but the .status.conditions[x].observedGeneration
+                        is 9, the condition is out of date with respect to the current
+                        state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: reason contains a programmatic identifier indicating
+                        the reason for the condition's last transition. Producers
+                        of specific condition types may define expected values and
+                        meanings for this field, and whether the values are considered
+                        a guaranteed API. The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                        --- Many .condition.type values are consistent across resources
+                        like Available, but because arbitrary conditions can be useful
+                        (see .node.status.conditions), the ability to deconflict is
+                        important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
+              failures:
+                description: Failures is the reconciliation failure count against
+                  the latest desired state. It is reset after a successful reconciliation.
+                format: int64
+                type: integer
+              helmChart:
+                description: HelmChart is the namespaced name of the HelmChart resource
+                  created by the controller for the ArmadaChart.
+                type: string
+              installFailures:
+                description: InstallFailures is the install failure count against
+                  the latest desired state. It is reset after a successful reconciliation.
+                format: int64
+                type: integer
+              lastAppliedRevision:
+                description: LastAppliedRevision is the revision of the last successfully
+                  applied source.
+                type: string
+              lastAttemptedRevision:
+                description: LastAttemptedRevision is the revision of the last reconciliation
+                  attempt.
+                type: string
+              lastAttemptedValuesChecksum:
+                description: LastAttemptedValuesChecksum is the SHA1 checksum of the
+                  values of the last reconciliation attempt.
+                type: string
+              lastReleaseRevision:
+                description: LastReleaseRevision is the revision of the last successful
+                  Helm release.
+                type: integer
+              observedGeneration:
+                description: ObservedGeneration is the last observed generation.
+                format: int64
+                type: integer
+              tested:
+                description: Tested is the bool value whether the Helm Release was
+                  successfully tested or not.
+                type: boolean
+              upgradeFailures:
+                description: UpgradeFailures is the upgrade failure count against
+                  the latest desired state. It is reset after a successful reconciliation.
+                format: int64
+                type: integer
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
new file mode 100644
index 0000000..f74e87c
--- /dev/null
+++ b/config/crd/kustomization.yaml
@@ -0,0 +1,21 @@
+# This kustomization.yaml is not intended to be run by itself,
+# since it depends on service name and namespace that are out of this kustomize package.
+# It should be run by config/default
+resources:
+- bases/armada.airshipit.org_armadacharts.yaml
+#+kubebuilder:scaffold:crdkustomizeresource
+
+patches:
+# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
+# patches here are for enabling the conversion webhook for each CRD
+#- path: patches/webhook_in_armadacharts.yaml
+#+kubebuilder:scaffold:crdkustomizewebhookpatch
+
+# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
+# patches here are for enabling the CA injection for each CRD
+#- path: patches/cainjection_in_armadacharts.yaml
+#+kubebuilder:scaffold:crdkustomizecainjectionpatch
+
+# the following config is for teaching kustomize how to do kustomization for CRDs.
+configurations:
+- kustomizeconfig.yaml
diff --git a/config/crd/kustomizeconfig.yaml b/config/crd/kustomizeconfig.yaml
new file mode 100644
index 0000000..ec5c150
--- /dev/null
+++ b/config/crd/kustomizeconfig.yaml
@@ -0,0 +1,19 @@
+# This file is for teaching kustomize how to substitute name and namespace reference in CRD
+nameReference:
+- kind: Service
+  version: v1
+  fieldSpecs:
+  - kind: CustomResourceDefinition
+    version: v1
+    group: apiextensions.k8s.io
+    path: spec/conversion/webhook/clientConfig/service/name
+
+namespace:
+- kind: CustomResourceDefinition
+  version: v1
+  group: apiextensions.k8s.io
+  path: spec/conversion/webhook/clientConfig/service/namespace
+  create: false
+
+varReference:
+- path: metadata/annotations
diff --git a/config/crd/patches/cainjection_in_armadacharts.yaml b/config/crd/patches/cainjection_in_armadacharts.yaml
new file mode 100644
index 0000000..3d3e49b
--- /dev/null
+++ b/config/crd/patches/cainjection_in_armadacharts.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
+  name: armadacharts.armada.airshipit.org
diff --git a/config/crd/patches/webhook_in_armadacharts.yaml b/config/crd/patches/webhook_in_armadacharts.yaml
new file mode 100644
index 0000000..6b7a358
--- /dev/null
+++ b/config/crd/patches/webhook_in_armadacharts.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: armadacharts.armada.airshipit.org
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          namespace: system
+          name: webhook-service
+          path: /convert
+      conversionReviewVersions:
+      - v1
diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml
new file mode 100644
index 0000000..8f179e8
--- /dev/null
+++ b/config/default/kustomization.yaml
@@ -0,0 +1,144 @@
+# Adds namespace to all resources.
+namespace: armada-operator-system
+
+# Value of this field is prepended to the
+# names of all resources, e.g. a deployment named
+# "wordpress" becomes "alices-wordpress".
+# Note that it should also match with the prefix (text before '-') of the namespace
+# field above.
+namePrefix: armada-operator-
+
+# Labels to add to all resources and selectors.
+#labels:
+#- includeSelectors: true
+#  pairs:
+#    someName: someValue
+
+resources:
+- ../crd
+- ../rbac
+- ../manager
+# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
+# crd/kustomization.yaml
+#- ../webhook
+# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
+#- ../certmanager
+# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
+#- ../prometheus
+
+patchesStrategicMerge:
+# Protect the /metrics endpoint by putting it behind auth.
+# If you want your controller-manager to expose the /metrics
+# endpoint w/o any authn/z, please comment the following line.
+- manager_auth_proxy_patch.yaml
+
+
+
+# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
+# crd/kustomization.yaml
+#- manager_webhook_patch.yaml
+
+# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
+# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
+# 'CERTMANAGER' needs to be enabled to use ca injection
+#- webhookcainjection_patch.yaml
+
+# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
+# Uncomment the following replacements to add the cert-manager CA injection annotations
+#replacements:
+#  - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs
+#      kind: Certificate
+#      group: cert-manager.io
+#      version: v1
+#      name: serving-cert # this name should match the one in certificate.yaml
+#      fieldPath: .metadata.namespace # namespace of the certificate CR
+#    targets:
+#      - select:
+#          kind: ValidatingWebhookConfiguration
+#        fieldPaths:
+#          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+#        options:
+#          delimiter: '/'
+#          index: 0
+#          create: true
+#      - select:
+#          kind: MutatingWebhookConfiguration
+#        fieldPaths:
+#          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+#        options:
+#          delimiter: '/'
+#          index: 0
+#          create: true
+#      - select:
+#          kind: CustomResourceDefinition
+#        fieldPaths:
+#          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+#        options:
+#          delimiter: '/'
+#          index: 0
+#          create: true
+#  - source:
+#      kind: Certificate
+#      group: cert-manager.io
+#      version: v1
+#      name: serving-cert # this name should match the one in certificate.yaml
+#      fieldPath: .metadata.name
+#    targets:
+#      - select:
+#          kind: ValidatingWebhookConfiguration
+#        fieldPaths:
+#          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+#        options:
+#          delimiter: '/'
+#          index: 1
+#          create: true
+#      - select:
+#          kind: MutatingWebhookConfiguration
+#        fieldPaths:
+#          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+#        options:
+#          delimiter: '/'
+#          index: 1
+#          create: true
+#      - select:
+#          kind: CustomResourceDefinition
+#        fieldPaths:
+#          - .metadata.annotations.[cert-manager.io/inject-ca-from]
+#        options:
+#          delimiter: '/'
+#          index: 1
+#          create: true
+#  - source: # Add cert-manager annotation to the webhook Service
+#      kind: Service
+#      version: v1
+#      name: webhook-service
+#      fieldPath: .metadata.name # namespace of the service
+#    targets:
+#      - select:
+#          kind: Certificate
+#          group: cert-manager.io
+#          version: v1
+#        fieldPaths:
+#          - .spec.dnsNames.0
+#          - .spec.dnsNames.1
+#        options:
+#          delimiter: '.'
+#          index: 0
+#          create: true
+#  - source:
+#      kind: Service
+#      version: v1
+#      name: webhook-service
+#      fieldPath: .metadata.namespace # namespace of the service
+#    targets:
+#      - select:
+#          kind: Certificate
+#          group: cert-manager.io
+#          version: v1
+#        fieldPaths:
+#          - .spec.dnsNames.0
+#          - .spec.dnsNames.1
+#        options:
+#          delimiter: '.'
+#          index: 1
+#          create: true
diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml
new file mode 100644
index 0000000..73fad2a
--- /dev/null
+++ b/config/default/manager_auth_proxy_patch.yaml
@@ -0,0 +1,39 @@
+# This patch inject a sidecar container which is a HTTP proxy for the
+# controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: controller-manager
+  namespace: system
+spec:
+  template:
+    spec:
+      containers:
+      - name: kube-rbac-proxy
+        securityContext:
+          allowPrivilegeEscalation: false
+          capabilities:
+            drop:
+              - "ALL"
+        image: gcr.io/kubebuilder/kube-rbac-proxy:v0.14.1
+        args:
+        - "--secure-listen-address=0.0.0.0:8443"
+        - "--upstream=http://127.0.0.1:8080/"
+        - "--logtostderr=true"
+        - "--v=0"
+        ports:
+        - containerPort: 8443
+          protocol: TCP
+          name: https
+        resources:
+          limits:
+            cpu: 500m
+            memory: 128Mi
+          requests:
+            cpu: 5m
+            memory: 64Mi
+      - name: manager
+        args:
+        - "--health-probe-bind-address=:8081"
+        - "--metrics-bind-address=127.0.0.1:8080"
+        - "--leader-elect"
diff --git a/config/default/manager_config_patch.yaml b/config/default/manager_config_patch.yaml
new file mode 100644
index 0000000..f6f5891
--- /dev/null
+++ b/config/default/manager_config_patch.yaml
@@ -0,0 +1,10 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: controller-manager
+  namespace: system
+spec:
+  template:
+    spec:
+      containers:
+      - name: manager
diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml
new file mode 100644
index 0000000..1308ad1
--- /dev/null
+++ b/config/manager/kustomization.yaml
@@ -0,0 +1,8 @@
+resources:
+- manager.yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+images:
+- name: controller
+  newName: quay.io/raliev12/armada-controller
+  newTag: latest
diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml
new file mode 100644
index 0000000..b4876f4
--- /dev/null
+++ b/config/manager/manager.yaml
@@ -0,0 +1,102 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  labels:
+    control-plane: controller-manager
+    app.kubernetes.io/name: namespace
+    app.kubernetes.io/instance: system
+    app.kubernetes.io/component: manager
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: system
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: controller-manager
+  namespace: system
+  labels:
+    control-plane: controller-manager
+    app.kubernetes.io/name: deployment
+    app.kubernetes.io/instance: controller-manager
+    app.kubernetes.io/component: manager
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+spec:
+  selector:
+    matchLabels:
+      control-plane: controller-manager
+  replicas: 1
+  template:
+    metadata:
+      annotations:
+        kubectl.kubernetes.io/default-container: manager
+      labels:
+        control-plane: controller-manager
+    spec:
+      # TODO(user): Uncomment the following code to configure the nodeAffinity expression
+      # according to the platforms which are supported by your solution.
+      # It is considered best practice to support multiple architectures. You can
+      # build your manager image using the makefile target docker-buildx.
+      # affinity:
+      #   nodeAffinity:
+      #     requiredDuringSchedulingIgnoredDuringExecution:
+      #       nodeSelectorTerms:
+      #         - matchExpressions:
+      #           - key: kubernetes.io/arch
+      #             operator: In
+      #             values:
+      #               - amd64
+      #               - arm64
+      #               - ppc64le
+      #               - s390x
+      #           - key: kubernetes.io/os
+      #             operator: In
+      #             values:
+      #               - linux
+      securityContext:
+        runAsNonRoot: true
+        # TODO(user): For common cases that do not require escalating privileges
+        # it is recommended to ensure that all your Pods/Containers are restrictive.
+        # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
+        # Please uncomment the following code if your project does NOT have to work on old Kubernetes
+        # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
+        # seccompProfile:
+        #   type: RuntimeDefault
+      containers:
+      - command:
+        - /manager
+        args:
+        - --leader-elect
+        image: controller:latest
+        name: manager
+        securityContext:
+          allowPrivilegeEscalation: false
+          capabilities:
+            drop:
+              - "ALL"
+        livenessProbe:
+          httpGet:
+            path: /healthz
+            port: 8081
+          initialDelaySeconds: 15
+          periodSeconds: 20
+        readinessProbe:
+          httpGet:
+            path: /readyz
+            port: 8081
+          initialDelaySeconds: 5
+          periodSeconds: 10
+        # TODO(user): Configure the resources accordingly based on the project requirements.
+        # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
+        resources:
+          limits:
+            cpu: 500m
+            memory: 128Mi
+          requests:
+            cpu: 10m
+            memory: 64Mi
+      serviceAccountName: controller-manager
+      terminationGracePeriodSeconds: 10
diff --git a/config/prometheus/kustomization.yaml b/config/prometheus/kustomization.yaml
new file mode 100644
index 0000000..ed13716
--- /dev/null
+++ b/config/prometheus/kustomization.yaml
@@ -0,0 +1,2 @@
+resources:
+- monitor.yaml
diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml
new file mode 100644
index 0000000..2a1d357
--- /dev/null
+++ b/config/prometheus/monitor.yaml
@@ -0,0 +1,26 @@
+
+# Prometheus Monitor Service (Metrics)
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  labels:
+    control-plane: controller-manager
+    app.kubernetes.io/name: servicemonitor
+    app.kubernetes.io/instance: controller-manager-metrics-monitor
+    app.kubernetes.io/component: metrics
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: controller-manager-metrics-monitor
+  namespace: system
+spec:
+  endpoints:
+    - path: /metrics
+      port: https
+      scheme: https
+      bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
+      tlsConfig:
+        insecureSkipVerify: true
+  selector:
+    matchLabels:
+      control-plane: controller-manager
diff --git a/config/rbac/armadachart_editor_role.yaml b/config/rbac/armadachart_editor_role.yaml
new file mode 100644
index 0000000..f2892a8
--- /dev/null
+++ b/config/rbac/armadachart_editor_role.yaml
@@ -0,0 +1,31 @@
+# permissions for end users to edit armadacharts.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app.kubernetes.io/name: clusterrole
+    app.kubernetes.io/instance: armadachart-editor-role
+    app.kubernetes.io/component: rbac
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: armadachart-editor-role
+rules:
+- apiGroups:
+  - armada.airshipit.org
+  resources:
+  - armadacharts
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - armada.airshipit.org
+  resources:
+  - armadacharts/status
+  verbs:
+  - get
diff --git a/config/rbac/armadachart_viewer_role.yaml b/config/rbac/armadachart_viewer_role.yaml
new file mode 100644
index 0000000..91bbe07
--- /dev/null
+++ b/config/rbac/armadachart_viewer_role.yaml
@@ -0,0 +1,27 @@
+# permissions for end users to view armadacharts.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app.kubernetes.io/name: clusterrole
+    app.kubernetes.io/instance: armadachart-viewer-role
+    app.kubernetes.io/component: rbac
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: armadachart-viewer-role
+rules:
+- apiGroups:
+  - armada.airshipit.org
+  resources:
+  - armadacharts
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - armada.airshipit.org
+  resources:
+  - armadacharts/status
+  verbs:
+  - get
diff --git a/config/rbac/auth_proxy_client_clusterrole.yaml b/config/rbac/auth_proxy_client_clusterrole.yaml
new file mode 100644
index 0000000..2ab61a4
--- /dev/null
+++ b/config/rbac/auth_proxy_client_clusterrole.yaml
@@ -0,0 +1,16 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app.kubernetes.io/name: clusterrole
+    app.kubernetes.io/instance: metrics-reader
+    app.kubernetes.io/component: kube-rbac-proxy
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: metrics-reader
+rules:
+- nonResourceURLs:
+  - "/metrics"
+  verbs:
+  - get
diff --git a/config/rbac/auth_proxy_role.yaml b/config/rbac/auth_proxy_role.yaml
new file mode 100644
index 0000000..74a1f79
--- /dev/null
+++ b/config/rbac/auth_proxy_role.yaml
@@ -0,0 +1,24 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app.kubernetes.io/name: clusterrole
+    app.kubernetes.io/instance: proxy-role
+    app.kubernetes.io/component: kube-rbac-proxy
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: proxy-role
+rules:
+- apiGroups:
+  - authentication.k8s.io
+  resources:
+  - tokenreviews
+  verbs:
+  - create
+- apiGroups:
+  - authorization.k8s.io
+  resources:
+  - subjectaccessreviews
+  verbs:
+  - create
diff --git a/config/rbac/auth_proxy_role_binding.yaml b/config/rbac/auth_proxy_role_binding.yaml
new file mode 100644
index 0000000..3c54750
--- /dev/null
+++ b/config/rbac/auth_proxy_role_binding.yaml
@@ -0,0 +1,19 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  labels:
+    app.kubernetes.io/name: clusterrolebinding
+    app.kubernetes.io/instance: proxy-rolebinding
+    app.kubernetes.io/component: kube-rbac-proxy
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: proxy-rolebinding
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: proxy-role
+subjects:
+- kind: ServiceAccount
+  name: default
+  namespace: default
diff --git a/config/rbac/auth_proxy_service.yaml b/config/rbac/auth_proxy_service.yaml
new file mode 100644
index 0000000..1679bfe
--- /dev/null
+++ b/config/rbac/auth_proxy_service.yaml
@@ -0,0 +1,21 @@
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    control-plane: controller-manager
+    app.kubernetes.io/name: service
+    app.kubernetes.io/instance: controller-manager-metrics-service
+    app.kubernetes.io/component: kube-rbac-proxy
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: controller-manager-metrics-service
+  namespace: system
+spec:
+  ports:
+  - name: https
+    port: 8443
+    protocol: TCP
+    targetPort: https
+  selector:
+    control-plane: controller-manager
diff --git a/config/rbac/cluster_role.yaml b/config/rbac/cluster_role.yaml
new file mode 100644
index 0000000..6639817
--- /dev/null
+++ b/config/rbac/cluster_role.yaml
@@ -0,0 +1,10 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: reconciler-role
+rules:
+  - apiGroups: ['*']
+    resources: ['*']
+    verbs: ['*']
+  - nonResourceURLs: ['*']
+    verbs: ['*']
diff --git a/config/rbac/cluster_role_binding.yaml b/config/rbac/cluster_role_binding.yaml
new file mode 100644
index 0000000..4f6f87c
--- /dev/null
+++ b/config/rbac/cluster_role_binding.yaml
@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: reconciler-rolebinding
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: reconciler-role
+subjects:
+  - kind: ServiceAccount
+    name: controller-manager
+    namespace: system
diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml
new file mode 100644
index 0000000..b7ac46a
--- /dev/null
+++ b/config/rbac/kustomization.yaml
@@ -0,0 +1,20 @@
+resources:
+# All RBAC will be applied under this service account in
+# the deployment namespace. You may comment out this resource
+# if your manager will use a service account that exists at
+# runtime. Be sure to update RoleBinding and ClusterRoleBinding
+# subjects if changing service account names.
+- service_account.yaml
+#- role.yaml
+#- role_binding.yaml
+- leader_election_role.yaml
+- leader_election_role_binding.yaml
+- cluster_role.yaml
+- cluster_role_binding.yaml
+# Comment the following 4 lines if you want to disable
+# the auth proxy (https://github.com/brancz/kube-rbac-proxy)
+# which protects your /metrics endpoint.
+#- auth_proxy_service.yaml
+#- auth_proxy_role.yaml
+#- auth_proxy_role_binding.yaml
+#- auth_proxy_client_clusterrole.yaml
diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml
new file mode 100644
index 0000000..0668631
--- /dev/null
+++ b/config/rbac/leader_election_role.yaml
@@ -0,0 +1,44 @@
+# permissions to do leader election.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app.kubernetes.io/name: role
+    app.kubernetes.io/instance: leader-election-role
+    app.kubernetes.io/component: rbac
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: leader-election-role
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - list
+  - watch
+  - create
+  - update
+  - patch
+  - delete
+- apiGroups:
+  - coordination.k8s.io
+  resources:
+  - leases
+  verbs:
+  - get
+  - list
+  - watch
+  - create
+  - update
+  - patch
+  - delete
+- apiGroups:
+  - ""
+  resources:
+  - events
+  verbs:
+  - create
+  - patch
diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml
new file mode 100644
index 0000000..361a2e3
--- /dev/null
+++ b/config/rbac/leader_election_role_binding.yaml
@@ -0,0 +1,19 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  labels:
+    app.kubernetes.io/name: rolebinding
+    app.kubernetes.io/instance: leader-election-rolebinding
+    app.kubernetes.io/component: rbac
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: leader-election-rolebinding
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: leader-election-role
+subjects:
+- kind: ServiceAccount
+  name: default
+  namespace: default
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
new file mode 100644
index 0000000..d2d36bd
--- /dev/null
+++ b/config/rbac/role.yaml
@@ -0,0 +1,32 @@
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: manager-role
+rules:
+- apiGroups:
+  - armada.airshipit.org
+  resources:
+  - armadacharts
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - armada.airshipit.org
+  resources:
+  - armadacharts/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - armada.airshipit.org
+  resources:
+  - armadacharts/status
+  verbs:
+  - get
+  - patch
+  - update
diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml
new file mode 100644
index 0000000..41c96ff
--- /dev/null
+++ b/config/rbac/role_binding.yaml
@@ -0,0 +1,19 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  labels:
+    app.kubernetes.io/name: clusterrolebinding
+    app.kubernetes.io/instance: manager-rolebinding
+    app.kubernetes.io/component: rbac
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: manager-rolebinding
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: manager-role
+subjects:
+- kind: ServiceAccount
+  name: controller-manager
+  namespace: system
diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml
new file mode 100644
index 0000000..64c26f2
--- /dev/null
+++ b/config/rbac/service_account.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  labels:
+    app.kubernetes.io/name: serviceaccount
+    app.kubernetes.io/instance: controller-manager-sa
+    app.kubernetes.io/component: rbac
+    app.kubernetes.io/created-by: armada-operator
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+  name: controller-manager
+  namespace: system
diff --git a/config/samples/armada_v1_armadachart.yaml b/config/samples/armada_v1_armadachart.yaml
new file mode 100644
index 0000000..5780943
--- /dev/null
+++ b/config/samples/armada_v1_armadachart.yaml
@@ -0,0 +1,12 @@
+apiVersion: armada.airshipit.org/v1
+kind: ArmadaChart
+metadata:
+  labels:
+    app.kubernetes.io/name: armadachart
+    app.kubernetes.io/instance: armadachart-sample
+    app.kubernetes.io/part-of: armada-operator
+    app.kubernetes.io/managed-by: kustomize
+    app.kubernetes.io/created-by: armada-operator
+  name: armadachart-sample
+spec:
+  # TODO(user): Add fields here
diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml
new file mode 100644
index 0000000..aadc3ca
--- /dev/null
+++ b/config/samples/kustomization.yaml
@@ -0,0 +1,4 @@
+## Append samples of your project ##
+resources:
+- armada_v1_armadachart.yaml
+#+kubebuilder:scaffold:manifestskustomizesamples
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a666cce
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,155 @@
+module opendev.org/airship/armada-operator
+
+go 1.20
+
+require (
+	github.com/go-logr/logr v1.2.4
+	github.com/google/go-cmp v0.6.0
+	github.com/hashicorp/go-retryablehttp v0.7.4
+	github.com/onsi/ginkgo/v2 v2.11.0
+	github.com/onsi/gomega v1.27.10
+	helm.sh/helm/v3 v3.12.2
+	k8s.io/api v0.28.3
+	k8s.io/apiextensions-apiserver v0.28.3
+	k8s.io/apimachinery v0.28.3
+	k8s.io/cli-runtime v0.28.2
+	k8s.io/client-go v0.28.3
+	k8s.io/klog/v2 v2.100.1
+	sigs.k8s.io/controller-runtime v0.16.3
+)
+
+require (
+	github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
+	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
+	github.com/BurntSushi/toml v1.3.2 // indirect
+	github.com/MakeNowJust/heredoc v1.0.0 // indirect
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.2.1 // indirect
+	github.com/Masterminds/sprig/v3 v3.2.3 // indirect
+	github.com/Masterminds/squirrel v1.5.4 // indirect
+	github.com/Microsoft/hcsshim v0.11.0 // indirect
+	github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/chai2010/gettext-go v1.0.2 // indirect
+	github.com/containerd/containerd v1.7.6 // indirect
+	github.com/cyphar/filepath-securejoin v0.2.4 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/docker/cli v24.0.6+incompatible // indirect
+	github.com/docker/distribution v2.8.2+incompatible // indirect
+	github.com/docker/docker v24.0.6+incompatible // indirect
+	github.com/docker/docker-credential-helpers v0.7.0 // indirect
+	github.com/docker/go-connections v0.4.0 // indirect
+	github.com/docker/go-metrics v0.0.1 // indirect
+	github.com/docker/go-units v0.5.0 // indirect
+	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
+	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
+	github.com/evanphx/json-patch/v5 v5.6.0 // indirect
+	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
+	github.com/fatih/color v1.13.0 // indirect
+	github.com/fsnotify/fsnotify v1.6.0 // indirect
+	github.com/go-errors/errors v1.4.2 // indirect
+	github.com/go-gorp/gorp/v3 v3.1.0 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-logr/zapr v1.2.4 // indirect
+	github.com/go-openapi/jsonpointer v0.19.6 // indirect
+	github.com/go-openapi/jsonreference v0.20.2 // indirect
+	github.com/go-openapi/swag v0.22.3 // indirect
+	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+	github.com/gobwas/glob v0.2.3 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/protobuf v1.5.3 // indirect
+	github.com/google/btree v1.1.2 // indirect
+	github.com/google/gnostic-models v0.6.8 // indirect
+	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
+	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
+	github.com/google/uuid v1.3.0 // indirect
+	github.com/gorilla/mux v1.8.0 // indirect
+	github.com/gosuri/uitable v0.0.4 // indirect
+	github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
+	github.com/hashicorp/errwrap v1.1.0 // indirect
+	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/huandu/xstrings v1.4.0 // indirect
+	github.com/imdario/mergo v0.3.13 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/jmoiron/sqlx v1.3.5 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/compress v1.16.0 // indirect
+	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
+	github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
+	github.com/lib/pq v1.10.9 // indirect
+	github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/mattn/go-runewidth v0.0.9 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
+	github.com/moby/locker v1.0.1 // indirect
+	github.com/moby/spdystream v0.2.0 // indirect
+	github.com/moby/term v0.5.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
+	github.com/morikuni/aec v1.0.0 // indirect
+	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/opencontainers/go-digest v1.0.0 // indirect
+	github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
+	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/prometheus/client_golang v1.16.0 // indirect
+	github.com/prometheus/client_model v0.4.0 // indirect
+	github.com/prometheus/common v0.44.0 // indirect
+	github.com/prometheus/procfs v0.10.1 // indirect
+	github.com/rubenv/sql-migrate v1.5.2 // indirect
+	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	github.com/shopspring/decimal v1.3.1 // indirect
+	github.com/sirupsen/logrus v1.9.3 // indirect
+	github.com/spf13/cast v1.5.0 // indirect
+	github.com/spf13/cobra v1.7.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
+	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
+	github.com/xeipuuv/gojsonschema v1.2.0 // indirect
+	github.com/xlab/treeprint v1.2.0 // indirect
+	go.opentelemetry.io/otel v1.14.0 // indirect
+	go.opentelemetry.io/otel/trace v1.14.0 // indirect
+	go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	go.uber.org/zap v1.25.0 // indirect
+	golang.org/x/crypto v0.14.0 // indirect
+	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
+	golang.org/x/net v0.17.0 // indirect
+	golang.org/x/oauth2 v0.8.0 // indirect
+	golang.org/x/sync v0.3.0 // indirect
+	golang.org/x/sys v0.13.0 // indirect
+	golang.org/x/term v0.13.0 // indirect
+	golang.org/x/text v0.13.0 // indirect
+	golang.org/x/time v0.3.0 // indirect
+	golang.org/x/tools v0.9.3 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
+	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
+	google.golang.org/grpc v1.54.0 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	k8s.io/apiserver v0.28.3 // indirect
+	k8s.io/component-base v0.28.3 // indirect
+	k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
+	k8s.io/kubectl v0.28.2 // indirect
+	k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect
+	oras.land/oras-go v1.2.4 // indirect
+	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
+	sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
+	sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
+	sigs.k8s.io/yaml v1.3.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..d1e6531
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,574 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
+github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
+github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
+github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
+github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
+github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/hcsshim v0.11.0 h1:7EFNIY4igHEXUdj1zXgAyU3fLc7QfOKHbkldRVTBdiM=
+github.com/Microsoft/hcsshim v0.11.0/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM=
+github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
+github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
+github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
+github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
+github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
+github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
+github.com/containerd/containerd v1.7.6 h1:oNAVsnhPoy4BTPQivLgTzI9Oleml9l/+eYIDYXRCYo8=
+github.com/containerd/containerd v1.7.6/go.mod h1:SY6lrkkuJT40BVNO37tlYTSnKJnP5AXBc0fhx0q+TJ4=
+github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
+github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc=
+github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY=
+github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
+github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE=
+github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
+github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
+github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
+github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
+github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
+github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
+github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
+github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
+github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
+github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
+github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
+github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
+github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI=
+github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
+github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
+github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo=
+github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA=
+github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
+github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
+github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
+github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
+github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
+github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU=
+github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0=
+github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k=
+github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
+github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
+github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
+github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
+github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
+github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
+github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
+github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
+github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
+github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
+github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
+github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
+github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
+github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI=
+github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY=
+github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
+github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
+github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
+github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
+github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
+github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
+github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
+github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
+github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
+github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
+github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
+github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
+github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
+github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
+github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
+github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
+github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
+github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0=
+github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
+github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
+github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
+github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
+github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
+github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
+go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
+go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
+go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
+go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY=
+go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
+go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
+go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
+golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
+golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
+golang.org/x/text v0.3.0/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
+golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
+gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
+google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
+helm.sh/helm/v3 v3.12.2 h1:kFyDBr/mgJUlyGzVTCieG4wW0zmo7fcNRWK0+FKkxqU=
+helm.sh/helm/v3 v3.12.2/go.mod h1:v1PMayudIfZAvec3Wp4wAErensvK/rv5fu/xCiE6t3I=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM=
+k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc=
+k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08=
+k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc=
+k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A=
+k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8=
+k8s.io/apiserver v0.28.3 h1:8Ov47O1cMyeDzTXz0rwcfIIGAP/dP7L8rWbEljRcg5w=
+k8s.io/apiserver v0.28.3/go.mod h1:YIpM+9wngNAv8Ctt0rHG4vQuX/I5rvkEMtZtsxW2rNM=
+k8s.io/cli-runtime v0.28.2 h1:64meB2fDj10/ThIMEJLO29a1oujSm0GQmKzh1RtA/uk=
+k8s.io/cli-runtime v0.28.2/go.mod h1:bTpGOvpdsPtDKoyfG4EG041WIyFZLV9qq4rPlkyYfDA=
+k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4=
+k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo=
+k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI=
+k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8=
+k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
+k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
+k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
+k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
+k8s.io/kubectl v0.28.2 h1:fOWOtU6S0smdNjG1PB9WFbqEIMlkzU5ahyHkc7ESHgM=
+k8s.io/kubectl v0.28.2/go.mod h1:6EQWTPySF1fn7yKoQZHYf9TPwIl2AygHEcJoxFekr64=
+k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU=
+k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+oras.land/oras-go v1.2.4 h1:djpBY2/2Cs1PV87GSJlxv4voajVOMZxqqtq9AB8YNvY=
+oras.land/oras-go v1.2.4/go.mod h1:DYcGfb3YF1nKjcezfX2SNlDAeQFKSXmf+qrFmrh4324=
+sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4=
+sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
+sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
+sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0=
+sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY=
+sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U=
+sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag=
+sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk=
+sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt
new file mode 100644
index 0000000..65b8622
--- /dev/null
+++ b/hack/boilerplate.go.txt
@@ -0,0 +1,15 @@
+/*
+Copyright 2023.
+
+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.
+*/
\ No newline at end of file
diff --git a/images/armada-operator/Dockerfile.ubuntu_focal b/images/armada-operator/Dockerfile.ubuntu_focal
new file mode 100644
index 0000000..73ad0a1
--- /dev/null
+++ b/images/armada-operator/Dockerfile.ubuntu_focal
@@ -0,0 +1,36 @@
+ARG FROM=ubuntu:20.04
+# Build the manager binary
+FROM golang:1.20-bullseye as builder
+ARG TARGETOS
+ARG TARGETARCH
+
+WORKDIR /workspace
+# Copy the Go Modules manifests
+COPY go.mod go.mod
+COPY go.sum go.sum
+# cache deps before building and copying source so that we don't need to re-download as much
+# and so that source changes don't invalidate our downloaded layer
+RUN go mod download
+
+# Copy the go source
+COPY cmd/main.go cmd/main.go
+COPY api/ api/
+COPY internal/ internal/
+
+# Build
+# the GOARCH has not a default value to allow the binary be built according to the host where the command
+# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
+# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
+# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
+RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
+
+# Use distroless as minimal base image to package the manager binary
+# Refer to https://github.com/GoogleContainerTools/distroless for more details
+# NOTE: distroless has been switched to alpine
+#FROM gcr.io/distroless/static:nonroot
+FROM ${FROM}
+WORKDIR /
+COPY --from=builder /workspace/manager .
+USER 65532:65532
+
+ENTRYPOINT ["/manager"]
diff --git a/internal/controller/armadachart_controller.go b/internal/controller/armadachart_controller.go
new file mode 100644
index 0000000..62e9658
--- /dev/null
+++ b/internal/controller/armadachart_controller.go
@@ -0,0 +1,417 @@
+/*
+Copyright 2023.
+
+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.
+*/
+
+package controller
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/hashicorp/go-retryablehttp"
+	//"golang.org/x/exp/maps"
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/chart/loader"
+	"helm.sh/helm/v3/pkg/chartutil"
+	"helm.sh/helm/v3/pkg/release"
+	"helm.sh/helm/v3/pkg/storage/driver"
+	batchv1 "k8s.io/api/batch/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+	"k8s.io/client-go/rest"
+
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
+
+	armadav1 "opendev.org/airship/armada-operator/api/v1"
+	"opendev.org/airship/armada-operator/internal/kube"
+	"opendev.org/airship/armada-operator/internal/runner"
+)
+
+// ArmadaChartReconciler reconciles a ArmadaChart object
+type ArmadaChartReconciler struct {
+	client.Client
+
+	ControllerName string
+
+	Scheme     *runtime.Scheme
+	httpClient *retryablehttp.Client
+}
+
+//+kubebuilder:rbac:groups=armada.airshipit.org,resources=armadacharts,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=armada.airshipit.org,resources=armadacharts/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=armada.airshipit.org,resources=armadacharts/finalizers,verbs=update
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+func (r *ArmadaChartReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	start := time.Now()
+	log := ctrl.LoggerFrom(ctx)
+
+	log.Info("reconciling has started")
+
+	// Retrieve the custom resource
+	var ac armadav1.ArmadaChart
+	if err := r.Get(ctx, req.NamespacedName, &ac); err != nil {
+		return ctrl.Result{}, client.IgnoreNotFound(err)
+	}
+
+	// Add our finalizer if it does not exist
+	if !controllerutil.ContainsFinalizer(&ac, armadav1.ArmadaChartFinalizer) {
+		patch := client.MergeFrom(ac.DeepCopy())
+		controllerutil.AddFinalizer(&ac, armadav1.ArmadaChartFinalizer)
+		if err := r.Patch(ctx, &ac, patch); err != nil {
+			log.Error(err, "unable to register finalizer")
+			return ctrl.Result{}, err
+		}
+	}
+
+	// Examine if the object is under deletion
+	if !ac.ObjectMeta.DeletionTimestamp.IsZero() {
+		log.Info("object is under deletion, uninstalling corresponding helm release")
+		return r.reconcileDelete(ctx, &ac)
+	}
+
+	// Perform reconciliation
+	ac, result, err := r.reconcile(ctx, ac)
+	if updateStatusErr := r.patchStatus(ctx, &ac); updateStatusErr != nil {
+		log.Error(updateStatusErr, "unable to update status after reconciliation")
+		return ctrl.Result{Requeue: false}, updateStatusErr
+	}
+
+	// Log reconciliation duration
+	durationMsg := fmt.Sprintf("reconciliation finished in %s", time.Since(start).String())
+	if result.RequeueAfter > 0 {
+		durationMsg = fmt.Sprintf("%s, next run in %s", durationMsg, result.RequeueAfter.String())
+	}
+	log.Info(durationMsg)
+
+	return result, err
+}
+
+func (r *ArmadaChartReconciler) reconcile(ctx context.Context, ac armadav1.ArmadaChart) (armadav1.ArmadaChart, ctrl.Result, error) {
+	log := ctrl.LoggerFrom(ctx)
+
+	// Observe HelmRelease generation.
+	if ac.Status.ObservedGeneration != ac.Generation {
+		ac.Status.ObservedGeneration = ac.Generation
+		ac = armadav1.ArmadaChartProgressing(ac)
+		if updateStatusErr := r.patchStatus(ctx, &ac); updateStatusErr != nil {
+			log.Error(updateStatusErr, "unable to update status after generation update")
+			return ac, ctrl.Result{Requeue: true}, updateStatusErr
+		}
+	}
+
+	// Prepare values
+	var vals map[string]interface{}
+	if ac.Spec.Values != nil {
+		var vals_err error
+		vals, vals_err = chartutil.ReadValues(ac.Spec.Values.Raw)
+		if vals_err != nil {
+			return armadav1.ArmadaChartNotReady(ac, "InitFailed", vals_err.Error()), ctrl.Result{}, vals_err
+		}
+	}
+	// Load chart from artifact
+	chrt, err := r.loadHelmChart(ctx, ac, ac.Spec.Source.Location)
+	if err != nil {
+		return armadav1.ArmadaChartNotReady(ac, "ArtifactFailed", err.Error()), ctrl.Result{}, err
+	}
+
+	reconAc, reconErr := r.reconcileChart(ctx, *ac.DeepCopy(), chrt, vals)
+
+	return reconAc, ctrl.Result{}, reconErr
+}
+
+func (r *ArmadaChartReconciler) reconcileChart(ctx context.Context,
+	ac armadav1.ArmadaChart, chrt *chart.Chart, vals chartutil.Values) (armadav1.ArmadaChart, error) {
+
+	log := ctrl.LoggerFrom(ctx)
+	gettr, err := r.buildRESTClientGetter(ctx, ac)
+	if err != nil {
+		return armadav1.ArmadaChartNotReady(ac, "InitFailed", err.Error()), err
+	}
+
+	run, err := runner.NewRunner(gettr, ac.Namespace, log)
+	if err != nil {
+		return armadav1.ArmadaChartNotReady(ac, "InitFailed", "failed to initialize Helm action runner"), err
+	}
+
+	// Determine last release revision.
+	rel, observeLastReleaseErr := run.ObserveLastRelease(ac)
+	if observeLastReleaseErr != nil {
+		err = fmt.Errorf("failed to get last release revision: %w", observeLastReleaseErr)
+		return armadav1.ArmadaChartNotReady(ac, "GetLastReleaseFailed", "failed to get last release revision"), err
+	}
+
+	testRel := func() (armadav1.ArmadaChart, error) {
+		if ac.Spec.Test.Enabled && !ac.Status.Tested {
+			log.Info("performing tests")
+			rel, err = run.Test(ac)
+			if err != nil {
+				return armadav1.ArmadaChartNotReady(ac, "TestFailed", err.Error()), err
+			}
+		}
+		return armadav1.ArmadaChartReady(ac), err
+	}
+
+	if rel == nil {
+		log.Info("helm install has started")
+		rel, err = run.Install(ctx, ac, chrt, vals)
+	} else {
+		if rel.Info.Status == release.StatusDeployed && !isUpdateRequired(ctx, rel, chrt, vals) {
+			log.Info("no updates found, skipping upgrade")
+			return testRel()
+		}
+
+		if rel.Info.Status.IsPending() {
+			log.Info("warning: release in pending state, unlocking")
+			rel.SetStatus(release.StatusFailed, fmt.Sprintf("release unlocked from stale state"))
+		} else {
+			for _, delRes := range ac.Spec.Upgrade.PreUpgrade.Delete {
+				log.Info(fmt.Sprintf("deleting all %ss in %s ns with labels %v", delRes.Type, ac.Spec.Namespace, delRes.Labels))
+				switch delRes.Type {
+				case "", "job":
+					err = r.DeleteAllOf(ctx, &batchv1.Job{}, client.MatchingLabels(delRes.Labels), client.InNamespace(ac.Spec.Namespace))
+					if err != nil {
+						return armadav1.ArmadaChartNotReady(ac, "DeleteFailed", err.Error()), err
+					}
+				case "pod":
+					err = r.DeleteAllOf(ctx, &corev1.Pod{}, client.MatchingLabels(delRes.Labels), client.InNamespace(ac.Spec.Namespace))
+					if err != nil {
+						return armadav1.ArmadaChartNotReady(ac, "DeleteFailed", err.Error()), err
+					}
+				case "cronjob":
+					err = r.DeleteAllOf(ctx, &batchv1.CronJob{}, client.MatchingLabels(delRes.Labels), client.InNamespace(ac.Spec.Namespace))
+					if err != nil {
+						return armadav1.ArmadaChartNotReady(ac, "DeleteFailed", err.Error()), err
+					}
+				}
+			}
+		}
+
+		log.Info("helm upgrade has started")
+		rel, err = run.Upgrade(ctx, ac, chrt, vals)
+	}
+
+	if err != nil {
+		err = fmt.Errorf("failed to install/upgrade helm release: %s", err.Error())
+		return armadav1.ArmadaChartNotReady(ac, "InstallUpgradeFailed", err.Error()), err
+	}
+
+	if ac.Spec.Wait.Timeout > 0 && len(ac.Spec.Wait.Labels) > 0 {
+		log.Info("preparing to wait resources")
+		resCfg, err := gettr.ToRESTConfig()
+		if err != nil {
+			return armadav1.ArmadaChartNotReady(ac, "WaitFailed", err.Error()), err
+		}
+		err = r.waitRelease(ctx, resCfg, ac)
+		if err != nil {
+			return armadav1.ArmadaChartNotReady(ac, "WaitFailed", err.Error()), err
+		}
+	}
+
+	return testRel()
+}
+
+func (r *ArmadaChartReconciler) waitRelease(ctx context.Context, restCfg *rest.Config, hr armadav1.ArmadaChart) error {
+	log := ctrl.LoggerFrom(ctx)
+
+	if hr.Spec.Wait.ArmadaChartWaitResources == nil {
+		log.Info(fmt.Sprintf("there are no explicitly defined resources to wait: %s, using default ones", hr.Name))
+		hr.Spec.Wait.ArmadaChartWaitResources = []armadav1.ArmadaChartWaitResource{{Type: "job"}, {Type: "pod"}}
+	}
+	if len(hr.Spec.Wait.ArmadaChartWaitResources) == 0 {
+		log.Info(fmt.Sprintf("there are none resources to wait: %s", hr.Name))
+	}
+	if hr.Spec.Wait.Labels == nil {
+		hr.Spec.Wait.Labels = make(map[string]string)
+	}
+
+	for _, res := range hr.Spec.Wait.ArmadaChartWaitResources {
+		log.Info(fmt.Sprintf("processing wait resource %v", res))
+		if res.Labels == nil || len(res.Labels) == 0 {
+			res.Labels = make(map[string]string)
+		}
+
+		for kk, vv := range hr.Spec.Wait.Labels {
+			res.Labels[kk] = vv
+		}
+
+		if len(res.Labels) == 0 {
+			log.Info("no selectors applied, continuing...")
+			continue
+		}
+
+		log.Info(fmt.Sprintf("Resolved `wait.resources` list: %v", res))
+
+		var labelSelector string
+		for k, v := range res.Labels {
+			if len(labelSelector) > 0 {
+				labelSelector = fmt.Sprintf("%s,%s=%s", labelSelector, k, v)
+			} else {
+				labelSelector = fmt.Sprintf("%s=%s", k, v)
+			}
+		}
+
+		opts := kube.WaitOptions{
+			RestConfig:    *restCfg,
+			Namespace:     hr.Spec.Namespace,
+			LabelSelector: labelSelector,
+			ResourceType:  fmt.Sprintf("%ss", res.Type),
+			Timeout:       time.Second * time.Duration(hr.Spec.Wait.Timeout),
+			MinReady:      res.MinReady,
+			Logger:        log,
+		}
+		err := opts.Wait(ctx)
+		if err != nil {
+			return err
+		}
+	}
+
+	log.Info("all resources are ready")
+	return nil
+}
+
+// loadHelmChart attempts to download the artifact from the provided source,
+// loads it into a chart.Chart, and removes the downloaded artifact.
+// It returns the loaded chart.Chart on success, or an error.
+func (r *ArmadaChartReconciler) loadHelmChart(ctx context.Context, hr armadav1.ArmadaChart, source string) (*chart.Chart, error) {
+	log := ctrl.LoggerFrom(ctx)
+	f, err := os.CreateTemp("", fmt.Sprintf("%s-%s-*.tgz", hr.GetNamespace(), hr.GetName()))
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	defer os.Remove(f.Name())
+
+	req, err := retryablehttp.NewRequest(http.MethodGet, source, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create a new request: %w", err)
+	}
+
+	resp, err := r.httpClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to download artifact, error: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("artifact '%s' download failed (status code: %s)", source, resp.Status)
+	}
+
+	if _, err := io.Copy(f, resp.Body); err != nil {
+		return nil, err
+	}
+
+	log.Info(fmt.Sprintf("helm chart downloaded to %s", f.Name()))
+	return loader.Load(f.Name())
+}
+
+func (r *ArmadaChartReconciler) buildRESTClientGetter(_ context.Context, hr armadav1.ArmadaChart) (genericclioptions.RESTClientGetter, error) {
+	opts := []kube.Option{
+		kube.WithNamespace(hr.Spec.Namespace),
+		kube.WithPersistent(true),
+	}
+
+	return kube.NewInClusterMemoryRESTClientGetter(opts...)
+}
+
+func (r *ArmadaChartReconciler) patchStatus(ctx context.Context, ac *armadav1.ArmadaChart) error {
+	latest := &armadav1.ArmadaChart{}
+	if err := r.Client.Get(ctx, client.ObjectKeyFromObject(ac), latest); err != nil {
+		return err
+	}
+	patch := client.MergeFrom(latest.DeepCopy())
+	latest.Status = ac.Status
+	return r.Client.Status().Patch(ctx, latest, patch, client.FieldOwner(r.ControllerName))
+}
+
+// reconcileDelete deletes the Helm Release of the ArmadaChart,
+// and uninstalls the Helm release if the resource has not been suspended.
+// It only performs a Helm uninstall if the ServiceAccount to be impersonated
+// exists.
+func (r *ArmadaChartReconciler) reconcileDelete(ctx context.Context, ac *armadav1.ArmadaChart) (ctrl.Result, error) {
+	log := ctrl.LoggerFrom(ctx)
+
+	getter, err := r.buildRESTClientGetter(ctx, *ac)
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+	run, err := runner.NewRunner(getter, ac.Spec.Namespace, ctrl.LoggerFrom(ctx))
+	if err != nil {
+		return ctrl.Result{}, err
+	}
+	if err := run.Uninstall(*ac); err != nil && !errors.Is(err, driver.ErrReleaseNotFound) {
+		return ctrl.Result{}, err
+	}
+	log.Info("uninstalled Helm release for deleted resource")
+
+	// Remove our finalizer from the list and update it.
+	controllerutil.RemoveFinalizer(ac, armadav1.ArmadaChartFinalizer)
+	if err := r.Update(ctx, ac); err != nil {
+		return ctrl.Result{}, err
+	}
+
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *ArmadaChartReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	httpClient := retryablehttp.NewClient()
+	httpClient.RetryWaitMin = 5 * time.Second
+	httpClient.RetryWaitMax = 30 * time.Second
+	httpClient.RetryMax = 3
+	httpClient.Logger = nil
+	r.httpClient = httpClient
+
+	r.ControllerName = "armada-controller"
+
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&armadav1.ArmadaChart{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).Complete(r)
+}
+
+func isUpdateRequired(ctx context.Context, release *release.Release, chrt *chart.Chart, vals chartutil.Values) bool {
+	log := ctrl.LoggerFrom(ctx)
+
+	switch {
+	case !cmp.Equal(release.Chart.Templates, chrt.Templates, cmpopts.EquateEmpty()):
+		log.Info("There are chart template diffs found")
+		log.Info(cmp.Diff(release.Chart.Templates, chrt.Templates))
+		return true
+
+	//case !cmp.Equal(release.Chart.Values, chrt.Values, cmpopts.EquateEmpty()):
+	//	log.Info("There are CHART DEF VALUES diffs")
+	//	log.Info(cmp.Diff(release.Chart.Values, chrt.Values, cmpopts.EquateEmpty()))
+	//	return true
+
+	case !cmp.Equal(release.Config, vals.AsMap(), cmpopts.EquateEmpty()):
+		log.Info("There are CHART VALUES diffs")
+		log.Info(cmp.Diff(release.Config, vals.AsMap(), cmpopts.EquateEmpty()))
+		return true
+	}
+
+	return false
+}
diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go
new file mode 100644
index 0000000..2a1f7e5
--- /dev/null
+++ b/internal/controller/suite_test.go
@@ -0,0 +1,90 @@
+/*
+Copyright 2023.
+
+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.
+*/
+
+package controller
+
+import (
+	"fmt"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/envtest"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	armadav1 "opendev.org/airship/armada-operator/api/v1"
+	//+kubebuilder:scaffold:imports
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+
+var cfg *rest.Config
+var k8sClient client.Client
+var testEnv *envtest.Environment
+
+func TestControllers(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecs(t, "Controller Suite")
+}
+
+var _ = BeforeSuite(func() {
+	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+	By("bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "config", "crd", "bases")},
+		ErrorIfCRDPathMissing: true,
+
+		// The BinaryAssetsDirectory is only required if you want to run the tests directly
+		// without call the makefile target test. If not informed it will look for the
+		// default path defined in controller-runtime which is /usr/local/kubebuilder/.
+		// Note that you must have the required binaries setup under the bin directory to perform
+		// the tests directly. When we run make test it will be setup and used automatically.
+		BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s",
+			fmt.Sprintf("1.28.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
+	}
+
+	var err error
+	// cfg is defined in this file globally.
+	cfg, err = testEnv.Start()
+	Expect(err).NotTo(HaveOccurred())
+	Expect(cfg).NotTo(BeNil())
+
+	err = armadav1.AddToScheme(scheme.Scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	//+kubebuilder:scaffold:scheme
+
+	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+	Expect(err).NotTo(HaveOccurred())
+	Expect(k8sClient).NotTo(BeNil())
+
+})
+
+var _ = AfterSuite(func() {
+	By("tearing down the test environment")
+	err := testEnv.Stop()
+	Expect(err).NotTo(HaveOccurred())
+})
diff --git a/internal/kube/client.go b/internal/kube/client.go
new file mode 100644
index 0000000..d07e987
--- /dev/null
+++ b/internal/kube/client.go
@@ -0,0 +1,213 @@
+/*
+Copyright 2023.
+
+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.
+*/
+
+package kube
+
+import (
+	"fmt"
+	"sync"
+
+	"k8s.io/apimachinery/pkg/api/meta"
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/discovery/cached/memory"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/restmapper"
+	"k8s.io/client-go/tools/clientcmd"
+	ctrl "sigs.k8s.io/controller-runtime"
+)
+
+// Option is a function that configures an MemoryRESTClientGetter.
+type Option func(*MemoryRESTClientGetter)
+
+// WithNamespace sets the namespace to use for the client.
+func WithNamespace(namespace string) Option {
+	return func(c *MemoryRESTClientGetter) {
+		c.namespace = namespace
+	}
+}
+
+// WithPersistent sets whether the client should persist the underlying client
+// config, REST mapper, and discovery client.
+func WithPersistent(persist bool) Option {
+	return func(c *MemoryRESTClientGetter) {
+		c.persistent = persist
+	}
+}
+
+// MemoryRESTClientGetter is a resource.RESTClientGetter that uses an
+// in-memory REST config, REST mapper, and discovery client.
+// If configured, the client config, REST mapper, and discovery client are
+// lazily initialized, and cached for subsequent calls.
+type MemoryRESTClientGetter struct {
+	// namespace is the namespace to use for the client.
+	namespace string
+	// impersonate is the username to use for the client.
+	impersonate string
+	// persistent indicates whether the client should persist the restMapper,
+	// clientCfg, and discoveryClient. Rather than re-initializing them on
+	// every call, they will be cached and reused.
+	persistent bool
+
+	cfg *rest.Config
+
+	restMapper   meta.RESTMapper
+	restMapperMu sync.Mutex
+
+	discoveryClient discovery.CachedDiscoveryInterface
+	discoveryMu     sync.Mutex
+
+	clientCfg   clientcmd.ClientConfig
+	clientCfgMu sync.Mutex
+}
+
+// setDefaults sets the default values for the MemoryRESTClientGetter.
+func (c *MemoryRESTClientGetter) setDefaults() {
+	if c.namespace == "" {
+		c.namespace = "default"
+	}
+}
+
+// NewMemoryRESTClientGetter returns a new MemoryRESTClientGetter.
+func NewMemoryRESTClientGetter(cfg *rest.Config, opts ...Option) *MemoryRESTClientGetter {
+	g := &MemoryRESTClientGetter{
+		cfg: cfg,
+	}
+	for _, opts := range opts {
+		opts(g)
+	}
+	g.setDefaults()
+	return g
+}
+
+// NewInClusterMemoryRESTClientGetter returns a new MemoryRESTClientGetter
+// that uses the in-cluster REST config. It returns an error if the in-cluster
+// REST config cannot be obtained.
+func NewInClusterMemoryRESTClientGetter(opts ...Option) (*MemoryRESTClientGetter, error) {
+	cfg, err := ctrl.GetConfig()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get config for in-cluster REST client: %w", err)
+	}
+	return NewMemoryRESTClientGetter(cfg, opts...), nil
+}
+
+// ToRESTConfig returns the in-memory REST config.
+func (c *MemoryRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
+	if c.cfg == nil {
+		return nil, fmt.Errorf("MemoryRESTClientGetter has no REST config")
+	}
+	return c.cfg, nil
+}
+
+// ToDiscoveryClient returns a memory cached discovery client. Calling it
+// multiple times will return the same instance.
+func (c *MemoryRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
+	if c.persistent {
+		return c.toPersistentDiscoveryClient()
+	}
+	return c.toDiscoveryClient()
+}
+
+func (c *MemoryRESTClientGetter) toPersistentDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
+	c.discoveryMu.Lock()
+	defer c.discoveryMu.Unlock()
+
+	if c.discoveryClient == nil {
+		discoveryClient, err := c.toDiscoveryClient()
+		if err != nil {
+			return nil, err
+		}
+		c.discoveryClient = discoveryClient
+	}
+	return c.discoveryClient, nil
+}
+
+func (c *MemoryRESTClientGetter) toDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
+	config, err := c.ToRESTConfig()
+	if err != nil {
+		return nil, err
+	}
+
+	discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
+	if err != nil {
+		return nil, err
+	}
+	return memory.NewMemCacheClient(discoveryClient), nil
+}
+
+// ToRESTMapper returns a meta.RESTMapper using the discovery client. Calling
+// it multiple times will return the same instance.
+func (c *MemoryRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
+	if c.persistent {
+		return c.toPersistentRESTMapper()
+	}
+	return c.toRESTMapper()
+}
+
+func (c *MemoryRESTClientGetter) toPersistentRESTMapper() (meta.RESTMapper, error) {
+	c.restMapperMu.Lock()
+	defer c.restMapperMu.Unlock()
+
+	if c.restMapper == nil {
+		restMapper, err := c.toRESTMapper()
+		if err != nil {
+			return nil, err
+		}
+		c.restMapper = restMapper
+	}
+	return c.restMapper, nil
+}
+
+func (c *MemoryRESTClientGetter) toRESTMapper() (meta.RESTMapper, error) {
+	discoveryClient, err := c.ToDiscoveryClient()
+	if err != nil {
+		return nil, err
+	}
+	mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
+	return restmapper.NewShortcutExpander(mapper, discoveryClient), nil
+}
+
+// ToRawKubeConfigLoader returns a clientcmd.ClientConfig using
+// clientcmd.DefaultClientConfig. With clientcmd.ClusterDefaults, namespace, and
+// impersonate configured as overwrites.
+func (c *MemoryRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
+	if c.persistent {
+		return c.toPersistentRawKubeConfigLoader()
+	}
+	return c.toRawKubeConfigLoader()
+}
+
+func (c *MemoryRESTClientGetter) toPersistentRawKubeConfigLoader() clientcmd.ClientConfig {
+	c.clientCfgMu.Lock()
+	defer c.clientCfgMu.Unlock()
+
+	if c.clientCfg == nil {
+		c.clientCfg = c.toRawKubeConfigLoader()
+	}
+	return c.clientCfg
+}
+
+func (c *MemoryRESTClientGetter) toRawKubeConfigLoader() clientcmd.ClientConfig {
+	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
+	// use the standard defaults for this client command
+	// DEPRECATED: remove and replace with something more accurate
+	loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
+
+	overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults}
+	overrides.Context.Namespace = c.namespace
+	overrides.AuthInfo.Impersonate = c.impersonate
+
+	return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
+}
diff --git a/internal/kube/wait.go b/internal/kube/wait.go
new file mode 100644
index 0000000..240ed3f
--- /dev/null
+++ b/internal/kube/wait.go
@@ -0,0 +1,382 @@
+/*
+Copyright 2023.
+
+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.
+*/
+
+package kube
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"math"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/go-logr/logr"
+	appsv1 "k8s.io/api/apps/v1"
+	batchv1 "k8s.io/api/batch/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/cache"
+	watchtools "k8s.io/client-go/tools/watch"
+)
+
+type StatusType string
+
+type Status struct {
+	StatusType
+	Msg string
+}
+
+type MinReady struct {
+	int32
+	Percent bool
+}
+
+const (
+	Ready   StatusType = "READY"
+	Skipped StatusType = "SKIPPED"
+	Unready StatusType = "UNREADY"
+	Error   StatusType = "ERROR"
+)
+
+// WaitOptions phase run command
+type WaitOptions struct {
+	RestConfig    rest.Config
+	Namespace     string
+	LabelSelector string
+	ResourceType  string
+	Timeout       time.Duration
+	Out           io.Writer
+	MinReady      string
+	Logger        logr.Logger
+}
+
+func getObjectStatus(obj interface{}, minReady *MinReady) Status {
+	switch v := obj.(type) {
+	case *corev1.Pod:
+		return isPodReady(v)
+	case *batchv1.Job:
+		return isJobReady(v)
+	case *appsv1.Deployment:
+		return isDeploymentReady(v, minReady)
+	case appsv1.Deployment:
+		return isDeploymentReady(&v, minReady)
+	case *appsv1.DaemonSet:
+		return isDaemonSetReady(v, minReady)
+	case *appsv1.StatefulSet:
+		return isStatefulSetReady(v, minReady)
+	default:
+		return Status{Error, fmt.Sprintf("Unable to cast an object to any type %s\n", obj)}
+	}
+}
+
+func allMatch(logger logr.Logger, store cache.Store, minReady *MinReady, obj runtime.Object) (bool, error) {
+	logger.Info(fmt.Sprintf("verifying ready status for %d number of objects", len(store.List())))
+	for _, item := range store.List() {
+		if obj != nil && item == obj {
+			logger.Info(fmt.Sprintf("Skipping the item as status is already computed"))
+			continue
+		}
+		status := getObjectStatus(item, minReady)
+		logger.Info(fmt.Sprintf("object %T status computed: %s %s\n", item, status.StatusType, status.Msg))
+		if status.StatusType != Ready && status.StatusType != Skipped {
+			return false, nil
+		}
+	}
+	logger.Info("all objects are ready\n")
+	return true, nil
+}
+
+func processEvent(logger logr.Logger, event watch.Event, minReady *MinReady) (StatusType, error) {
+	metaObj, err := meta.Accessor(event.Object)
+	if err != nil {
+		return Error, err
+	}
+	metaObj.GetResourceVersion()
+	logger.Info(fmt.Sprintf("watch event: type=%s, name=%s, namespace=%s, resource_ver %s",
+		event.Type, metaObj.GetName(), metaObj.GetNamespace(), metaObj.GetResourceVersion()))
+
+	if event.Type == "ERROR" {
+		return Error, errors.New(fmt.Sprintf("resource %s: got error event %s", metaObj.GetName(), event.Object))
+	}
+
+	status := getObjectStatus(event.Object, minReady)
+	logger.Info(fmt.Sprintf("object type: %T, status: %s", event.Object, status.Msg))
+	return status.StatusType, nil
+}
+
+func isPodReady(pod *corev1.Pod) Status {
+	if isTestPod(pod) || pod.Status.Phase == "Evicted" || hasOwner(&pod.ObjectMeta, "Job") {
+		return Status{Skipped,
+			fmt.Sprintf("Excluding Pod %s from wait: either test pod, owned by job or evicted", pod.GetName())}
+	}
+
+	phase := pod.Status.Phase
+	if phase == "Succeeded" {
+		return Status{Ready, fmt.Sprintf("Pod %s succeeded", pod.GetName())}
+	}
+
+	if phase == "Running" {
+		for _, cond := range pod.Status.Conditions {
+			if cond.Type == "Ready" && cond.Status == "True" {
+				return Status{Ready, fmt.Sprintf("Pod %s ready", pod.GetName())}
+			}
+		}
+	}
+	return Status{Unready, fmt.Sprintf("Waiting for pod %s to be ready", pod.GetName())}
+}
+
+func isJobReady(job *batchv1.Job) Status {
+	if hasOwner(&job.ObjectMeta, "CronJob") {
+		return Status{Skipped, fmt.Sprintf("Excluding Job %s from wait: owned by CronJob", job.GetName())}
+	}
+
+	expected := int32(0)
+	if job.Spec.Completions != nil {
+		expected = *job.Spec.Completions
+	}
+	completed := job.Status.Succeeded
+
+	if expected != completed {
+		return Status{Unready, fmt.Sprintf("Waiting for job %s to be successfully completed...", job.GetName())}
+	}
+
+	return Status{Ready, fmt.Sprintf("Job %s successfully completed", job.GetName())}
+}
+
+func isDeploymentReady(deployment *appsv1.Deployment, minReady *MinReady) Status {
+	name := deployment.GetName()
+	spec := deployment.Spec
+	status := deployment.Status
+	gen := deployment.GetGeneration()
+	observed := status.ObservedGeneration
+
+	if gen <= observed {
+		for _, cond := range status.Conditions {
+			if cond.Type == "Progressing" && cond.Reason == "ProgressDeadlineExceeded" {
+				return Status{Unready, fmt.Sprintf("Deployment %s exceeded its progress deadline\n", name)}
+			}
+		}
+		replicas := int32(0)
+		if spec.Replicas != nil {
+			replicas = *spec.Replicas
+		}
+		updated := status.UpdatedReplicas
+		available := status.AvailableReplicas
+		if updated < replicas {
+			return Status{Unready, fmt.Sprintf("Waiting for deployment %s rollout to finish: %d"+
+				" out of %d new replicas have been updated...\n", name, updated, replicas)}
+		}
+		if replicas > updated {
+			pending := replicas - updated
+			return Status{Unready, fmt.Sprintf("Waiting for deployment %s rollout to finish: %d old "+
+				"replicas are pending termination...\n", name, pending)}
+		}
+
+		if minReady.Percent {
+			minReady.int32 = int32(math.Ceil(float64((minReady.int32 / 100) * updated)))
+		}
+
+		if available < minReady.int32 {
+			return Status{Unready, fmt.Sprintf("Waiting for deployment %s rollout to finish: %d of %d "+
+				"updated replicas are available, with min_ready=%d\n", name, available, updated, minReady.int32)}
+		}
+
+		return Status{Ready, fmt.Sprintf("deployment %s successfully rolled out\n", name)}
+	}
+
+	return Status{Unready, fmt.Sprintf("Waiting for deployment %s spec update to be observed...\n", name)}
+}
+
+func isDaemonSetReady(daemonSet *appsv1.DaemonSet, minReady *MinReady) Status {
+	name := daemonSet.GetName()
+	status := daemonSet.Status
+	gen := daemonSet.GetGeneration()
+	observed := status.ObservedGeneration
+
+	if gen <= observed {
+		updated := status.UpdatedNumberScheduled
+		desired := status.DesiredNumberScheduled
+		available := status.NumberAvailable
+
+		if updated < desired {
+			return Status{Unready, fmt.Sprintf("Waiting for daemon set %s rollout to finish: %d out "+
+				"of %d new pods have been updated...", name, updated, desired)}
+		}
+
+		if minReady.Percent {
+			minReady.int32 = int32(math.Ceil(float64((minReady.int32 / 100) * desired)))
+		}
+		if available < minReady.int32 {
+			return Status{Unready, fmt.Sprintf("Waiting for daemon set %s rollout to finish: %d of %d "+
+				"updated pods are available, with min_ready=%d", name, available, desired, minReady.int32)}
+		}
+
+		return Status{Ready, fmt.Sprintf("daemon set %s successfully rolled out", name)}
+	}
+	return Status{Unready, fmt.Sprintf("Waiting for daemon set spec update to be observed...")}
+}
+
+func isStatefulSetReady(statefulSet *appsv1.StatefulSet, minReady *MinReady) Status {
+	name := statefulSet.GetName()
+	spec := statefulSet.Spec
+	status := statefulSet.Status
+	gen := statefulSet.GetGeneration()
+	observed := status.ObservedGeneration
+	replicas := int32(0)
+	if spec.Replicas != nil {
+		replicas = *spec.Replicas
+	}
+	ready := status.ReadyReplicas
+	updated := status.UpdatedReplicas
+	current := status.CurrentReplicas
+
+	if observed == 0 || gen > observed {
+		return Status{Unready, fmt.Sprintf("Waiting for statefulset spec update to be observed...")}
+	}
+
+	if minReady.Percent {
+		minReady.int32 = int32(math.Ceil(float64((minReady.int32 / 100) * replicas)))
+	}
+
+	if replicas > 0 && ready < minReady.int32 {
+		return Status{Unready, fmt.Sprintf("Waiting for statefulset %s rollout to finish: %d of %d "+
+			"pods are ready, with min_ready=%d", name, ready, replicas, minReady.int32)}
+	}
+
+	updateRev := status.UpdateRevision
+	currentRev := status.CurrentRevision
+	if updateRev != currentRev {
+		return Status{Unready, fmt.Sprintf("waiting for statefulset rolling update to complete %d "+
+			"pods at revision %s...", updated, updateRev)}
+	}
+
+	return Status{Ready, fmt.Sprintf("statefulset rolling update complete %d pods at revision %s...",
+		current, currentRev)}
+}
+
+func isTestPod(u *corev1.Pod) bool {
+	annotations := u.GetAnnotations()
+	var testHooks []string
+	if len(annotations) > 0 {
+		if val, ok := annotations["helm.sh/hook"]; ok {
+			hooks := strings.Split(val, ",")
+			for _, h := range hooks {
+				if h == "test" || h == "test-success" || h == "test-failure" {
+					testHooks = append(testHooks, h)
+				}
+			}
+		}
+	}
+	return len(testHooks) > 0
+}
+
+func hasOwner(ometa *metav1.ObjectMeta, kind string) bool {
+	ownerRefs := ometa.GetOwnerReferences()
+	for _, owner := range ownerRefs {
+		if kind == owner.Kind {
+			return true
+		}
+	}
+	return false
+}
+
+func getClient(resource string, config *rest.Config) (rest.Interface, error) {
+	cs, err := kubernetes.NewForConfig(config)
+	if err != nil {
+		return nil, err
+	}
+
+	switch resource {
+	case "jobs":
+		return cs.BatchV1().RESTClient(), nil
+	case "pods":
+		return cs.CoreV1().RESTClient(), nil
+	case "daemonsets", "deployments", "statefulsets":
+		return cs.AppsV1().RESTClient(), nil
+	}
+
+	return nil, errors.New(fmt.Sprintf("Unable to find a client for resource '%s'", resource))
+}
+
+func getMinReady(minReady string) (*MinReady, error) {
+	ret := &MinReady{0, false}
+	if minReady != "" {
+		if strings.HasSuffix(minReady, "%") {
+			ret.Percent = true
+		}
+		//var err error
+		val, err := strconv.Atoi(strings.ReplaceAll(minReady, "%", ""))
+		if err != nil {
+			return nil, err
+		}
+		ret.int32 = int32(val)
+
+	}
+	return ret, nil
+}
+
+func (c *WaitOptions) Wait(parent context.Context) error {
+	c.Logger.Info(fmt.Sprintf("armada-operator wait , namespace %s labels %s type %s timeout %s", c.Namespace, c.LabelSelector, c.ResourceType, c.Timeout))
+
+	clientSet, err := getClient(c.ResourceType, &c.RestConfig)
+	if err != nil {
+		return err
+	}
+
+	ctx, cancelFunc := watchtools.ContextWithOptionalTimeout(parent, c.Timeout)
+	defer cancelFunc()
+
+	lw := cache.NewFilteredListWatchFromClient(clientSet, c.ResourceType, c.Namespace, func(options *metav1.ListOptions) {
+		options.LabelSelector = c.LabelSelector
+		c.Logger.Info(fmt.Sprintf("Label selector applied %s", options))
+	})
+
+	minReady, err := getMinReady(c.MinReady)
+	if err != nil {
+		return err
+	}
+
+	var cacheStore cache.Store
+
+	cpu := func(store cache.Store) (bool, error) {
+		cacheStore = store
+		c.Logger.Info(fmt.Sprintf("number of objects to watch: %d", len(store.List())))
+		if len(store.List()) == 0 {
+			c.Logger.Info(fmt.Sprintf("Skipping non-required wait, no resources found.\n"))
+			return true, nil
+		}
+		return allMatch(c.Logger, cacheStore, minReady, nil)
+	}
+
+	cfu := func(event watch.Event) (bool, error) {
+		if ready, err := processEvent(c.Logger, event, minReady); ready != Ready || err != nil {
+			return false, err
+		}
+
+		return allMatch(c.Logger, cacheStore, minReady, event.Object)
+	}
+
+	_, err = watchtools.UntilWithSync(ctx, lw, nil, cpu, cfu)
+	return err
+}
diff --git a/internal/runner/log_buffer.go b/internal/runner/log_buffer.go
new file mode 100644
index 0000000..1b5fdd0
--- /dev/null
+++ b/internal/runner/log_buffer.go
@@ -0,0 +1,85 @@
+/*
+Copyright 2023.
+
+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.
+*/
+
+package runner
+
+import (
+	"container/ring"
+	"fmt"
+	"strings"
+	"sync"
+
+	"github.com/go-logr/logr"
+	"helm.sh/helm/v3/pkg/action"
+)
+
+const defaultBufferSize = 5
+
+func NewDebugLog(log logr.Logger) action.DebugLog {
+	return func(format string, v ...interface{}) {
+		log.Info(fmt.Sprintf(format, v...))
+	}
+}
+
+type LogBuffer struct {
+	mu     sync.Mutex
+	log    action.DebugLog
+	buffer *ring.Ring
+}
+
+func NewLogBuffer(log action.DebugLog, size int) *LogBuffer {
+	if size <= 0 {
+		size = defaultBufferSize
+	}
+	return &LogBuffer{
+		log:    log,
+		buffer: ring.New(size),
+	}
+}
+
+func (l *LogBuffer) Log(format string, v ...interface{}) {
+	l.mu.Lock()
+
+	// Filter out duplicate log lines, this happens for example when
+	// Helm is waiting on workloads to become ready.
+	msg := fmt.Sprintf(format, v...)
+	if prev := l.buffer.Prev(); prev.Value != msg {
+		l.buffer.Value = msg
+		l.buffer = l.buffer.Next()
+	}
+
+	l.mu.Unlock()
+	l.log(format, v...)
+}
+
+func (l *LogBuffer) Reset() {
+	l.mu.Lock()
+	l.buffer = ring.New(l.buffer.Len())
+	l.mu.Unlock()
+}
+
+func (l *LogBuffer) String() string {
+	var str string
+	l.mu.Lock()
+	l.buffer.Do(func(s interface{}) {
+		if s == nil {
+			return
+		}
+		str += s.(string) + "\n"
+	})
+	l.mu.Unlock()
+	return strings.TrimSpace(str)
+}
diff --git a/internal/runner/runner.go b/internal/runner/runner.go
new file mode 100644
index 0000000..1dadaf5
--- /dev/null
+++ b/internal/runner/runner.go
@@ -0,0 +1,169 @@
+/*
+Copyright 2023.
+
+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.
+*/
+
+package runner
+
+import (
+	"context"
+	"errors"
+	"sync"
+	"time"
+
+	"github.com/go-logr/logr"
+	"helm.sh/helm/v3/pkg/action"
+	"helm.sh/helm/v3/pkg/chart"
+	"helm.sh/helm/v3/pkg/chartutil"
+	"helm.sh/helm/v3/pkg/kube"
+	"helm.sh/helm/v3/pkg/release"
+	"helm.sh/helm/v3/pkg/storage/driver"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+
+	armadav1 "opendev.org/airship/armada-operator/api/v1"
+)
+
+// Runner represents a Helm action runner capable of performing Helm
+// operations for a ArmadaChart.
+type Runner struct {
+	mu        sync.Mutex
+	config    *action.Configuration
+	logBuffer *LogBuffer
+}
+
+type ActionError struct {
+	Err          error
+	CapturedLogs string
+}
+
+func (e ActionError) Error() string {
+	return e.Err.Error()
+}
+
+func (e ActionError) Unwrap() error {
+	return e.Err
+}
+
+// NewRunner constructs a new Runner configured to run Helm actions with the
+// given genericclioptions.RESTClientGetter, and the release and storage
+// namespace configured to the provided values.
+func NewRunner(getter genericclioptions.RESTClientGetter, storageNamespace string, logger logr.Logger) (*Runner, error) {
+	runner := &Runner{
+		logBuffer: NewLogBuffer(NewDebugLog(logger.V(2)), defaultBufferSize),
+	}
+
+	// Default to the trace level logger for the Helm action configuration,
+	// to ensure storage logs are captured.
+	cfg := new(action.Configuration)
+	if err := cfg.Init(getter, storageNamespace, "secret", NewDebugLog(logger)); err != nil {
+		return nil, err
+	}
+
+	// Override the logger used by the Helm actions and Kube client with the log buffer,
+	// which provides useful information in the event of an error.
+	cfg.Log = runner.logBuffer.Log
+	if kc, ok := cfg.KubeClient.(*kube.Client); ok {
+		kc.Log = runner.logBuffer.Log
+	}
+	runner.config = cfg
+
+	return runner, nil
+}
+
+// Install runs a Helm install action for the given ArmadaChart.
+func (r *Runner) Install(ctx context.Context, ac armadav1.ArmadaChart, chart *chart.Chart, values chartutil.Values) (*release.Release, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	defer r.logBuffer.Reset()
+
+	install := action.NewInstall(r.config)
+	install.ReleaseName = ac.Name
+	install.Namespace = ac.Namespace
+
+	if ac.Spec.Wait.Native != nil && ac.Spec.Wait.Native.Enabled && ac.Spec.Wait.Timeout > 0 {
+		install.Wait = true
+		install.Timeout = time.Duration(int64(time.Second) * int64(ac.Spec.Wait.Timeout))
+	}
+	install.DisableOpenAPIValidation = true
+	install.CreateNamespace = true
+
+	rel, err := install.RunWithContext(ctx, chart, values.AsMap())
+	return rel, wrapActionErr(r.logBuffer, err)
+}
+
+// Upgrade runs an Helm upgrade action for the given ArmadaChart.
+func (r *Runner) Upgrade(ctx context.Context, ac armadav1.ArmadaChart, chart *chart.Chart, values chartutil.Values) (*release.Release, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	defer r.logBuffer.Reset()
+
+	upgrade := action.NewUpgrade(r.config)
+	upgrade.Namespace = ac.Spec.Namespace
+	if ac.Spec.Wait.Native != nil && ac.Spec.Wait.Native.Enabled && ac.Spec.Wait.Timeout > 0 {
+		upgrade.Wait = true
+		upgrade.Timeout = time.Duration(int64(time.Second) * int64(ac.Spec.Wait.Timeout))
+	}
+	upgrade.DisableOpenAPIValidation = true
+
+	rel, err := upgrade.RunWithContext(ctx, ac.Name, chart, values.AsMap())
+	return rel, wrapActionErr(r.logBuffer, err)
+}
+
+// Test runs an Helm test action for the given ArmadaChart.
+func (r *Runner) Test(ac armadav1.ArmadaChart) (*release.Release, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	defer r.logBuffer.Reset()
+
+	r.logBuffer.Log("performing test")
+	test := action.NewReleaseTesting(r.config)
+	test.Namespace = ac.Spec.Namespace
+	test.Timeout = time.Duration(int64(time.Second) * int64(ac.Spec.Wait.Timeout))
+
+	rel, err := test.Run(ac.Name)
+	return rel, wrapActionErr(r.logBuffer, err)
+}
+
+// Uninstall runs an Helm uninstall action
+func (r *Runner) Uninstall(ac armadav1.ArmadaChart) error {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	defer r.logBuffer.Reset()
+
+	uninstall := action.NewUninstall(r.config)
+
+	_, err := uninstall.Run(ac.Name)
+	return wrapActionErr(r.logBuffer, err)
+}
+
+// ObserveLastRelease observes the last revision, if there is one,
+// for the actual Helm release associated with the given ArmadaChart.
+func (r *Runner) ObserveLastRelease(ac armadav1.ArmadaChart) (*release.Release, error) {
+	rel, err := r.config.Releases.Last(ac.Name)
+	if err != nil && errors.Is(err, driver.ErrReleaseNotFound) {
+		err = nil
+	}
+	return rel, err
+}
+
+func wrapActionErr(log *LogBuffer, err error) error {
+	if err == nil {
+		return err
+	}
+	err = &ActionError{
+		Err:          err,
+		CapturedLogs: log.String(),
+	}
+	return err
+}
diff --git a/tools/gate/playbooks/docker-image-build.yaml b/tools/gate/playbooks/docker-image-build.yaml
new file mode 100644
index 0000000..27fff85
--- /dev/null
+++ b/tools/gate/playbooks/docker-image-build.yaml
@@ -0,0 +1,126 @@
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
+#
+# 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.
+
+- hosts: primary
+  roles:
+    - bindep
+    - ensure-docker
+    - ensure-python
+    - ensure-pip
+
+  tasks:
+    - include_vars: vars.yaml
+
+    - name: Debug tag generation inputs
+      block:
+        - debug:
+            var: publish
+        - debug:
+            var: distro
+        - debug:
+            var: tags
+        - debug:
+            var: zuul
+        - debug:
+            msg: "{{ tags | to_json }}"
+
+    - name: Determine tags
+      shell: echo '{{ tags | to_json }}' | python3 {{ zuul.project.src_dir }}/tools/image_tags.py
+      environment:
+        BRANCH: "{{ zuul.branch | default('') }}"
+        CHANGE: "{{ zuul.change | default('') }}"
+        COMMIT: "{{ zuul.newrev | default('') }}"
+        PATCHSET: "{{ zuul.patchset | default('') }}"
+      register: image_tags
+
+    - name: Debug computed tags
+      debug:
+        var: image_tags
+
+    - name: Install Docker python module for ansible docker login
+      block:
+        - pip:
+            name: docker
+            version: 4.4.4
+            executable: pip3
+      become: True
+
+    - name: Install tox python module for ansible docker login
+      block:
+        - pip:
+            name: tox
+            version: 3.28.0
+            executable: pip3
+      become: True
+
+
+#    - name: Run images
+#      when: not publish
+#      shell: |
+#        set -ex
+#        sudo -E -H pip3 install tox==3.28.0
+#        make run_images
+#      args:
+#        chdir: "{{ zuul.project.src_dir }}"
+#        executable: /bin/bash
+#      become: True
+
+
+
+    - name: Make images
+      when: not publish
+      block:
+        - make:
+            chdir: "{{ zuul.project.src_dir }}"
+            target: images
+            params:
+              DISTRO: "{{ distro }}"
+              IMAGE_TAG: "{{ item }}"
+          with_items: "{{ image_tags.stdout_lines }}"
+
+        - shell: "docker images"
+          register: docker_images
+
+        - debug:
+            var: docker_images
+
+      become: True
+
+    - name: Publish images
+      block:
+        - docker_login:
+            username: "{{ airship_armada_operator_quay_creds.username }}"
+            password: "{{ airship_armada_operator_quay_creds.password }}"
+            registry_url: "https://quay.io/api/v1/"
+
+        - make:
+            chdir: "{{ zuul.project.src_dir }}"
+            target: images
+            params:
+              DOCKER_REGISTRY: "quay.io"
+              IMAGE_PREFIX: "airshipit"
+              DISTRO: "{{ distro }}"
+              IMAGE_TAG: "{{ item }}"
+              COMMIT: "{{ zuul.newrev | default('') }}"
+              PUSH_IMAGE: "true"
+          with_items: "{{ image_tags.stdout_lines }}"
+
+        - shell: "docker images"
+          register: docker_images
+
+        - debug:
+            var: docker_images
+
+      when: publish
+      become: True
diff --git a/tools/gate/playbooks/vars.yaml b/tools/gate/playbooks/vars.yaml
new file mode 100644
index 0000000..c89b798
--- /dev/null
+++ b/tools/gate/playbooks/vars.yaml
@@ -0,0 +1,19 @@
+# Copyright 2017 The Openstack-Helm Authors.
+#
+# 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.
+
+docker_daemon:
+  group: zuul
+  registry-mirrors:
+    - "http://{{ zuul_site_mirror_fqdn }}:8082/"
+  storage-driver: overlay2
diff --git a/tools/gate/roles/disable-systemd-resolved/tasks/disable-systemd-resolved.yaml b/tools/gate/roles/disable-systemd-resolved/tasks/disable-systemd-resolved.yaml
new file mode 100644
index 0000000..0eda1fb
--- /dev/null
+++ b/tools/gate/roles/disable-systemd-resolved/tasks/disable-systemd-resolved.yaml
@@ -0,0 +1,37 @@
+# Copyright 2020 AT&T Intellectual Property.  All other rights reserved.
+#
+# 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.
+
+- name: Disable systemd-resolved service
+  systemd:
+    state: stopped
+    enabled: no
+    masked: yes
+    daemon_reload: yes
+    name: systemd-resolved
+  become: yes
+
+- name: Remove local stub dns from resolv.conf, if it exists
+  lineinfile:
+    path: /etc/resolv.conf
+    state: absent
+    regexp: '^nameserver.*127.0.0.1'
+  become: yes
+
+- name: Add upstream nameservers in resolv.conf
+  blockinfile:
+    path: /etc/resolv.conf
+    block: |
+      nameserver 8.8.8.8
+      nameserver 8.8.4.4
+  become: yes
diff --git a/tools/gate/roles/disable-systemd-resolved/tasks/main.yaml b/tools/gate/roles/disable-systemd-resolved/tasks/main.yaml
new file mode 100644
index 0000000..bb381b4
--- /dev/null
+++ b/tools/gate/roles/disable-systemd-resolved/tasks/main.yaml
@@ -0,0 +1,15 @@
+# Copyright 2020 AT&T Intellectual Property.  All other rights reserved.
+#
+# 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.
+
+- include: disable-systemd-resolved.yaml
diff --git a/tools/image_tags.py b/tools/image_tags.py
new file mode 100644
index 0000000..be28669
--- /dev/null
+++ b/tools/image_tags.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python3
+# Copyright 2018 AT&T Intellectual Property.  All other rights reserved.
+#
+# 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.
+
+import json
+import logging
+import os
+import sys
+
+LOG = logging.getLogger(__name__)
+
+LOG_FORMAT = '%(asctime)s %(levelname)-8s %(name)s:%(filename)s:%(lineno)3d:%(funcName)s %(message)s'  # noqa
+
+
+class TagGenExeception(Exception):
+    pass
+
+
+def read_config(stream, env):
+    config = {}
+    try:
+        config['tags'] = json.load(stream)
+    except ValueError:
+        LOG.exception('Failed to decode JSON from input stream')
+        config['tags'] = {}
+
+    LOG.debug('Configuration after reading stream: %s', config)
+
+    config['context'] = {
+        'branch': env.get('BRANCH'),
+        'change': env.get('CHANGE'),
+        'commit': env.get('COMMIT'),
+        'ps': env.get('PATCHSET'),
+    }
+
+    LOG.info('Final configuration: %s', config)
+
+    return config
+
+
+def build_tags(config):
+    tags = config.get('tags', {}).get('static', [])
+    LOG.debug('Dynamic tags: %s', tags)
+    tags.extend(build_dynamic_tags(config))
+    LOG.info('All tags: %s', tags)
+    return tags
+
+
+def build_dynamic_tags(config):
+    dynamic_tags = []
+
+    dynamic_tags.extend(_build_branch_tag(config))
+    dynamic_tags.extend(_build_commit_tag(config))
+    dynamic_tags.extend(_build_ps_tag(config))
+
+    return dynamic_tags
+
+
+def _build_branch_tag(config):
+    if _valid_dg(config, 'branch'):
+        return [config['context']['branch']]
+    else:
+        return []
+
+
+def _build_commit_tag(config):
+    if _valid_dg(config, 'commit'):
+        return [config['context']['commit']]
+    else:
+        return []
+
+
+def _build_ps_tag(config):
+    if _valid_dg(config, 'patch_set', 'change') and _valid_dg(
+            config, 'patch_set', 'ps'):
+        return [
+            '%s-%s' % (config['context']['change'], config['context']['ps'])
+        ]
+    else:
+        return []
+
+
+def _valid_dg(config, dynamic_tag, context_name=None):
+    if context_name is None:
+        context_name = dynamic_tag
+
+    if config.get('tags', {}).get('dynamic', {}).get(dynamic_tag):
+        if config.get('context', {}).get(context_name):
+            return True
+        else:
+            raise TagGenExeception('Dynamic tag "%s" requested, but "%s"'
+                                   ' not found in context' % (dynamic_tag,
+                                                              context_name))
+    else:
+        return False
+
+
+def main():
+    config = read_config(sys.stdin, os.environ)
+    tags = build_tags(config)
+
+    for tag in tags:
+        print(tag)
+
+
+if __name__ == '__main__':
+    logging.basicConfig(format=LOG_FORMAT, level=logging.WARNING)
+    try:
+        main()
+    except TagGenExeception:
+        LOG.exception('Failed to generate tags')
+        sys.exit(1)
+    except Exception:
+        LOG.exception('Unexpected exception')
+        sys.exit(2)