Add osh ingress (#249)

* feat: add OpenstackHelmIngress

* chore: drop certbuilder deps

* chore: shave more deps

* feat: added openstackhelmrabbitmqclusters

* fix: install cert-manager first

* test: fix integration tests

* test: fix e2e tests

* tests: describe and get all resources

* fix: change default image repo to be none

* fix: solve when no override_registry

* fix: add annotation + labels

* fix: move more reesources to helm

* fix: add more dependencies

* chore: move services out of flows

* chore: build dependencies

* fix: drops deps from ApplyPerconaXtraDBClusterTask

* fix: add wait_for_pxc role to avoid race conditions

* fix: solve rabbitmq for magnum

* fix: clean-up filter_annotations

* chore: increase wait_timeout for secret waiting
diff --git a/.gitignore b/.gitignore
index 548e34a..d40a05a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@
 .pytest*
 .coverage
 .kube
+.hypothesis
diff --git a/atmosphere/cmd/operator.py b/atmosphere/cmd/operator.py
index e8c672f..f1e931b 100644
--- a/atmosphere/cmd/operator.py
+++ b/atmosphere/cmd/operator.py
@@ -1,20 +1,76 @@
-import os
-
 import kopf
 
-from atmosphere import flows
-from atmosphere.models import config
-from atmosphere.operator import controllers  # noqa: F401
+from atmosphere import clients
+from atmosphere.operator import constants, controllers, utils  # noqa: F401
+from atmosphere.operator.api import objects, types
 
 
-@kopf.on.startup()
-def configure(settings: kopf.OperatorSettings, **_):
-    settings.admission.server = kopf.WebhookServer(host=os.environ["POD_IP"])
-    settings.admission.managed = "auto.atmosphere.vexxhost.com"
+@kopf.on.create(
+    constants.API_VERSION_ATMOSPHERE,
+    constants.KIND_OPENSTACK_HELM_RABBITMQ_CLUSTER,
+)
+@kopf.on.resume(
+    constants.API_VERSION_ATMOSPHERE,
+    constants.KIND_OPENSTACK_HELM_RABBITMQ_CLUSTER,
+)
+def create_openstack_helm_rabbitmq_cluster(
+    namespace: str, name: str, annotations: dict, labels: dict, spec: dict, **_
+):
+    api = clients.get_pykube_api()
+    objects.OpenstackHelmRabbitmqCluster(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=name,
+            namespace=namespace,
+            annotations=utils.filter_annotations(annotations),
+            labels=labels,
+        ),
+        spec=types.OpenstackHelmRabbitmqClusterSpec(**spec),
+    ).apply_rabbitmq_cluster()
 
 
-@kopf.on.startup()
-def startup(**_):
-    cfg = config.Config.from_file()
-    engine = flows.get_engine(cfg)
-    engine.run()
+@kopf.on.delete(
+    constants.API_VERSION_ATMOSPHERE,
+    constants.KIND_OPENSTACK_HELM_RABBITMQ_CLUSTER,
+)
+def delete_openstack_helm_rabbitmq_cluster(namespace: str, name: str, spec: dict, **_):
+    api = clients.get_pykube_api()
+    objects.OpenstackHelmRabbitmqCluster(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=name,
+            namespace=namespace,
+        ),
+        spec=types.OpenstackHelmRabbitmqClusterSpec(**spec),
+    ).delete_rabbitmq_cluster()
+
+
+@kopf.on.create(constants.API_VERSION_ATMOSPHERE, constants.KIND_OPENSTACK_HELM_INGRESS)
+@kopf.on.resume(constants.API_VERSION_ATMOSPHERE, constants.KIND_OPENSTACK_HELM_INGRESS)
+def create_openstack_helm_ingress(
+    namespace: str, name: str, annotations: dict, labels: dict, spec: dict, **_
+):
+    api = clients.get_pykube_api()
+    objects.OpenstackHelmIngress(
+        api=api,
+        metadata=types.OpenstackHelmIngressObjectMeta(
+            name=name,
+            namespace=namespace,
+            annotations=utils.filter_annotations(annotations),
+            labels=labels,
+        ),
+        spec=types.OpenstackHelmIngressSpec(**spec),
+    ).apply_ingress()
+
+
+@kopf.on.delete(constants.API_VERSION_ATMOSPHERE, constants.KIND_OPENSTACK_HELM_INGRESS)
+def delete_openstack_helm_ingress(namespace: str, name: str, spec: dict, **_):
+    api = clients.get_pykube_api()
+    objects.OpenstackHelmIngress(
+        api=api,
+        metadata=types.OpenstackHelmIngressObjectMeta(
+            name=name,
+            namespace=namespace,
+        ),
+        spec=types.OpenstackHelmIngressSpec(**spec),
+    ).delete_ingress()
diff --git a/atmosphere/flows.py b/atmosphere/flows.py
index a1be369..6fd57ab 100644
--- a/atmosphere/flows.py
+++ b/atmosphere/flows.py
@@ -1,12 +1,308 @@
 from taskflow import engines
 from taskflow.patterns import graph_flow
 
+from atmosphere import clients
+from atmosphere.operator.api import objects, types
 from atmosphere.tasks import constants
 from atmosphere.tasks.composite import openstack_helm
-from atmosphere.tasks.kubernetes import cert_manager, flux, v1
+from atmosphere.tasks.kubernetes import cert_manager, v1
 
 
 def get_engine(config):
+    api = clients.get_pykube_api()
+
+    objects.HelmRepository(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_REPOSITORY_CEPH,
+            namespace=constants.NAMESPACE_KUBE_SYSTEM,
+        ),
+        spec=types.HelmRepositorySpec(
+            url="https://ceph.github.io/csi-charts",
+        ),
+    ).apply()
+
+    if config.ingress_nginx.enabled:
+        objects.HelmRepository(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name=constants.HELM_REPOSITORY_INGRESS_NGINX,
+                namespace=config.ingress_nginx.namespace,
+            ),
+            spec=types.HelmRepositorySpec(
+                url=constants.HELM_REPOSITORY_INGRESS_NGINX_URL,
+            ),
+        ).apply()
+        objects.HelmRelease(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name=constants.HELM_RELEASE_INGRESS_NGINX_NAME,
+                namespace=config.ingress_nginx.namespace,
+            ),
+            spec=types.HelmReleaseSpec(
+                chart=types.HelmChartTemplate(
+                    spec=types.HelmChartTemplateSpec(
+                        chart=constants.HELM_RELEASE_INGRESS_NGINX_NAME,
+                        version=constants.HELM_RELEASE_INGRESS_NGINX_VERSION,
+                        source_ref=types.CrossNamespaceObjectReference(
+                            kind="HelmRepository",
+                            name=constants.HELM_REPOSITORY_INGRESS_NGINX,
+                            namespace=config.ingress_nginx.namespace,
+                        ),
+                    )
+                ),
+                values={
+                    **constants.HELM_RELEASE_INGRESS_NGINX_VALUES,
+                    **config.ingress_nginx.overrides,
+                },
+            ),
+        ).apply()
+
+    # NOTE(mnaser): We're running this first since we do get often timeouts
+    #               when waiting for the self-signed certificate authority to
+    #               be ready.
+    objects.Namespace(
+        api=api,
+        metadata=types.ObjectMeta(
+            name=constants.NAMESPACE_CERT_MANAGER,
+        ),
+    ).apply()
+    objects.HelmRepository(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_REPOSITORY_JETSTACK,
+            namespace=constants.NAMESPACE_CERT_MANAGER,
+        ),
+        spec=types.HelmRepositorySpec(
+            url="https://charts.jetstack.io",
+        ),
+    ).apply()
+    objects.HelmRelease(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_RELEASE_CERT_MANAGER_NAME,
+            namespace=constants.NAMESPACE_CERT_MANAGER,
+        ),
+        spec=types.HelmReleaseSpec(
+            chart=types.HelmChartTemplate(
+                spec=types.HelmChartTemplateSpec(
+                    chart=constants.HELM_RELEASE_CERT_MANAGER_NAME,
+                    version=constants.HELM_RELEASE_CERT_MANAGER_VERSION,
+                    source_ref=types.CrossNamespaceObjectReference(
+                        kind="HelmRepository",
+                        name=constants.HELM_REPOSITORY_JETSTACK,
+                        namespace=constants.NAMESPACE_CERT_MANAGER,
+                    ),
+                )
+            ),
+            depends_on=[
+                types.NamespacedObjectReference(
+                    name=constants.HELM_RELEASE_INGRESS_NGINX_NAME,
+                    namespace=config.ingress_nginx.namespace,
+                )
+            ],
+            values=constants.HELM_RELEASE_CERT_MANAGER_VALUES,
+        ),
+    ).apply()
+
+    objects.Namespace(
+        api=api,
+        metadata=types.ObjectMeta(
+            name=constants.NAMESPACE_MONITORING,
+        ),
+    ).apply()
+    objects.HelmRepository(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_REPOSITORY_NODE_FEATURE_DISCOVERY,
+            namespace=constants.NAMESPACE_MONITORING,
+        ),
+        spec=types.HelmRepositorySpec(
+            url="https://kubernetes-sigs.github.io/node-feature-discovery/charts",
+        ),
+    ).apply()
+    objects.HelmRelease(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name="node-feature-discovery",
+            namespace=constants.NAMESPACE_MONITORING,
+        ),
+        spec=types.HelmReleaseSpec(
+            chart=types.HelmChartTemplate(
+                spec=types.HelmChartTemplateSpec(
+                    chart="node-feature-discovery",
+                    version="0.11.2",
+                    source_ref=types.CrossNamespaceObjectReference(
+                        kind="HelmRepository",
+                        name=constants.HELM_REPOSITORY_NODE_FEATURE_DISCOVERY,
+                        namespace=constants.NAMESPACE_MONITORING,
+                    ),
+                )
+            ),
+            values=constants.HELM_RELEASE_NODE_FEATURE_DISCOVERY_VALUES,
+        ),
+    ).apply()
+
+    objects.HelmRepository(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_REPOSITORY_BITNAMI,
+            namespace=constants.NAMESPACE_OPENSTACK,
+        ),
+        spec=types.HelmRepositorySpec(
+            url="https://charts.bitnami.com/bitnami",
+        ),
+    ).apply()
+    objects.HelmRelease(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_RELEASE_RABBITMQ_OPERATOR_NAME,
+            namespace=constants.NAMESPACE_OPENSTACK,
+        ),
+        spec=types.HelmReleaseSpec(
+            chart=types.HelmChartTemplate(
+                spec=types.HelmChartTemplateSpec(
+                    chart=constants.HELM_RELEASE_RABBITMQ_OPERATOR_NAME,
+                    version=constants.HELM_RELEASE_RABBITMQ_OPERATOR_VERSION,
+                    source_ref=types.CrossNamespaceObjectReference(
+                        kind="HelmRepository",
+                        name=constants.HELM_REPOSITORY_BITNAMI,
+                        namespace=constants.NAMESPACE_OPENSTACK,
+                    ),
+                )
+            ),
+            depends_on=[
+                types.NamespacedObjectReference(
+                    name=constants.HELM_RELEASE_CERT_MANAGER_NAME,
+                    namespace=constants.NAMESPACE_CERT_MANAGER,
+                )
+            ],
+            values=constants.HELM_RELEASE_RABBITMQ_OPERATOR_VALUES,
+        ),
+    ).apply()
+    objects.HelmRepository(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_REPOSITORY_PERCONA,
+            namespace=constants.NAMESPACE_OPENSTACK,
+        ),
+        spec=types.HelmRepositorySpec(
+            url="https://percona.github.io/percona-helm-charts/",
+        ),
+    ).apply()
+    objects.HelmRelease(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_RELEASE_PXC_OPERATOR_NAME,
+            namespace=constants.NAMESPACE_OPENSTACK,
+        ),
+        spec=types.HelmReleaseSpec(
+            chart=types.HelmChartTemplate(
+                spec=types.HelmChartTemplateSpec(
+                    chart=constants.HELM_RELEASE_PXC_OPERATOR_NAME,
+                    version=constants.HELM_RELEASE_PXC_OPERATOR_VERSION,
+                    source_ref=types.CrossNamespaceObjectReference(
+                        kind="HelmRepository",
+                        name=constants.HELM_REPOSITORY_PERCONA,
+                        namespace=constants.NAMESPACE_OPENSTACK,
+                    ),
+                )
+            ),
+            depends_on=[
+                types.NamespacedObjectReference(
+                    name=constants.HELM_RELEASE_CERT_MANAGER_NAME,
+                    namespace=constants.NAMESPACE_CERT_MANAGER,
+                )
+            ],
+            values=constants.HELM_RELEASE_PXC_OPERATOR_VALUES,
+        ),
+    ).apply()
+    objects.HelmRepository(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_REPOSITORY_OPENSTACK_HELM_INFRA,
+            namespace=constants.NAMESPACE_OPENSTACK,
+        ),
+        spec=types.HelmRepositorySpec(
+            url="https://tarballs.opendev.org/openstack/openstack-helm-infra/",
+        ),
+    ).apply()
+    objects.HelmRepository(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_REPOSITORY_COREDNS,
+            namespace=constants.NAMESPACE_OPENSTACK,
+        ),
+        spec=types.HelmRepositorySpec(url="https://coredns.github.io/helm"),
+    ).apply()
+    objects.HelmRepository(
+        api=api,
+        metadata=types.NamespacedObjectMeta(
+            name=constants.HELM_REPOSITORY_OPENSTACK_HELM,
+            namespace=constants.NAMESPACE_OPENSTACK,
+        ),
+        spec=types.HelmRepositorySpec(
+            url="https://tarballs.opendev.org/openstack/openstack-helm/",
+        ),
+    ).apply()
+
+    if config.kube_prometheus_stack.enabled:
+        objects.HelmRepository(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name=constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY,
+                namespace=config.kube_prometheus_stack.namespace,
+            ),
+            spec=types.HelmRepositorySpec(
+                url=constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY_URL,
+            ),
+        ).apply()
+        objects.HelmRelease(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name=constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_NAME,
+                namespace=config.kube_prometheus_stack.namespace,
+            ),
+            spec=types.HelmReleaseSpec(
+                chart=types.HelmChartTemplate(
+                    spec=types.HelmChartTemplateSpec(
+                        chart=constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_NAME,
+                        version=constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_VERSION,
+                        source_ref=types.CrossNamespaceObjectReference(
+                            kind="HelmRepository",
+                            name=constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY,
+                            namespace=config.kube_prometheus_stack.namespace,
+                        ),
+                    )
+                ),
+                depends_on=[
+                    types.NamespacedObjectReference(
+                        name=constants.HELM_RELEASE_RABBITMQ_OPERATOR_NAME,
+                        namespace=constants.NAMESPACE_OPENSTACK,
+                    ),
+                    types.NamespacedObjectReference(
+                        name=constants.HELM_RELEASE_PXC_OPERATOR_NAME,
+                        namespace=constants.NAMESPACE_OPENSTACK,
+                    ),
+                    types.NamespacedObjectReference(
+                        name="node-feature-discovery",
+                        namespace=constants.NAMESPACE_MONITORING,
+                    ),
+                ],
+                values={
+                    **constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_VALUES,
+                    **config.kube_prometheus_stack.overrides,
+                    **{
+                        "alertmanager": {
+                            "config": openstack_helm.generate_alertmanager_config_for_opsgenie(
+                                config.opsgenie
+                            )
+                        }
+                    },
+                },
+            ),
+        ).apply()
+
     return engines.load(
         get_deployment_flow(config),
         executor="greenthreaded",
@@ -15,126 +311,12 @@
     )
 
 
+# TODO(mnaser): Move this into the Cloud CRD
 def get_deployment_flow(config):
     flow = graph_flow.Flow("deploy").add(
-        # kube-system
-        v1.ApplyNamespaceTask(name=constants.NAMESPACE_KUBE_SYSTEM),
-        flux.ApplyHelmRepositoryTask(
-            namespace=constants.NAMESPACE_KUBE_SYSTEM,
-            name=constants.HELM_REPOSITORY_CEPH,
-            url="https://ceph.github.io/csi-charts",
-        ),
         # cert-manager
-        v1.ApplyNamespaceTask(name=constants.NAMESPACE_CERT_MANAGER),
-        flux.ApplyHelmRepositoryTask(
-            namespace=constants.NAMESPACE_CERT_MANAGER,
-            name=constants.HELM_REPOSITORY_JETSTACK,
-            url="https://charts.jetstack.io",
-        ),
-        flux.ApplyHelmReleaseTask(
-            namespace=constants.NAMESPACE_CERT_MANAGER,
-            name=constants.HELM_RELEASE_CERT_MANAGER_NAME,
-            repository=constants.HELM_REPOSITORY_JETSTACK,
-            chart=constants.HELM_RELEASE_CERT_MANAGER_NAME,
-            version=constants.HELM_RELEASE_CERT_MANAGER_VERSION,
-            values=constants.HELM_RELEASE_CERT_MANAGER_VALUES,
-        ),
         *cert_manager.issuer_tasks_from_config(config.issuer),
-        # monitoring
-        v1.ApplyNamespaceTask(name=constants.NAMESPACE_MONITORING),
-        *openstack_helm.kube_prometheus_stack_tasks_from_config(
-            config.kube_prometheus_stack,
-            opsgenie=config.opsgenie,
-        ),
-        flux.ApplyHelmRepositoryTask(
-            namespace=constants.NAMESPACE_MONITORING,
-            name=constants.HELM_REPOSITORY_NODE_FEATURE_DISCOVERY,
-            url="https://kubernetes-sigs.github.io/node-feature-discovery/charts",
-        ),
-        flux.ApplyHelmReleaseTask(
-            namespace=constants.NAMESPACE_MONITORING,
-            name="node-feature-discovery",
-            repository=constants.HELM_REPOSITORY_NODE_FEATURE_DISCOVERY,
-            chart="node-feature-discovery",
-            version="0.11.2",
-            values=constants.HELM_RELEASE_NODE_FEATURE_DISCOVERY_VALUES,
-        ),
-        # openstack
-        v1.ApplyNamespaceTask(name=constants.NAMESPACE_OPENSTACK),
-        flux.ApplyHelmRepositoryTask(
-            namespace=constants.NAMESPACE_OPENSTACK,
-            name=constants.HELM_REPOSITORY_BITNAMI,
-            url="https://charts.bitnami.com/bitnami",
-        ),
-        flux.ApplyHelmReleaseTask(
-            namespace=constants.NAMESPACE_OPENSTACK,
-            name=constants.HELM_RELEASE_RABBITMQ_OPERATOR_NAME,
-            repository=constants.HELM_REPOSITORY_BITNAMI,
-            chart=constants.HELM_RELEASE_RABBITMQ_OPERATOR_NAME,
-            version=constants.HELM_RELEASE_RABBITMQ_OPERATOR_VERSION,
-            values=constants.HELM_RELEASE_RABBITMQ_OPERATOR_VALUES,
-            requires=constants.HELM_RELEASE_RABBITMQ_OPERATOR_REQUIRES,
-        ),
-        flux.ApplyHelmRepositoryTask(
-            namespace=constants.NAMESPACE_OPENSTACK,
-            name=constants.HELM_REPOSITORY_PERCONA,
-            url="https://percona.github.io/percona-helm-charts/",
-        ),
-        flux.ApplyHelmReleaseTask(
-            namespace=constants.NAMESPACE_OPENSTACK,
-            name=constants.HELM_RELEASE_PXC_OPERATOR_NAME,
-            repository=constants.HELM_REPOSITORY_PERCONA,
-            chart=constants.HELM_RELEASE_PXC_OPERATOR_NAME,
-            version=constants.HELM_RELEASE_PXC_OPERATOR_VERSION,
-            values=constants.HELM_RELEASE_PXC_OPERATOR_VALUES,
-        ),
         openstack_helm.ApplyPerconaXtraDBClusterTask(),
-        *openstack_helm.ingress_nginx_tasks_from_config(config.ingress_nginx),
-        flux.ApplyHelmRepositoryTask(
-            namespace=constants.NAMESPACE_OPENSTACK,
-            name=constants.HELM_REPOSITORY_OPENSTACK_HELM_INFRA,
-            url="https://tarballs.opendev.org/openstack/openstack-helm-infra/",
-        ),
-        flux.ApplyHelmRepositoryTask(
-            namespace=constants.NAMESPACE_OPENSTACK,
-            name=constants.HELM_REPOSITORY_COREDNS,
-            url="https://coredns.github.io/helm",
-        ),
-        flux.ApplyHelmRepositoryTask(
-            namespace=constants.NAMESPACE_OPENSTACK,
-            name=constants.HELM_REPOSITORY_OPENSTACK_HELM,
-            url="https://tarballs.opendev.org/openstack/openstack-helm/",
-        ),
-        openstack_helm.ApplyRabbitmqClusterTask(
-            name=constants.HELM_RELEASE_KEYSTONE_NAME,
-        ),
-        openstack_helm.ApplyRabbitmqClusterTask(
-            name=constants.HELM_RELEASE_BARBICAN_NAME,
-        ),
-        openstack_helm.ApplyRabbitmqClusterTask(
-            name=constants.HELM_RELEASE_GLANCE_NAME,
-        ),
-        openstack_helm.ApplyRabbitmqClusterTask(
-            name=constants.HELM_RELEASE_CINDER_NAME,
-        ),
-        openstack_helm.ApplyRabbitmqClusterTask(
-            name=constants.HELM_RELEASE_NEUTRON_NAME,
-        ),
-        openstack_helm.ApplyRabbitmqClusterTask(
-            name=constants.HELM_RELEASE_NOVA_NAME,
-        ),
-        openstack_helm.ApplyRabbitmqClusterTask(
-            name=constants.HELM_RELEASE_OCTAVIA_NAME,
-        ),
-        openstack_helm.ApplyRabbitmqClusterTask(
-            name=constants.HELM_RELEASE_SENLIN_NAME,
-        ),
-        openstack_helm.ApplyRabbitmqClusterTask(
-            name=constants.HELM_RELEASE_DESIGNATE_NAME,
-        ),
-        openstack_helm.ApplyRabbitmqClusterTask(
-            name=constants.HELM_RELEASE_HEAT_NAME,
-        ),
     )
 
     if config.memcached.enabled:
diff --git a/atmosphere/operator/api/mixins.py b/atmosphere/operator/api/mixins.py
new file mode 100644
index 0000000..dad93f7
--- /dev/null
+++ b/atmosphere/operator/api/mixins.py
@@ -0,0 +1,30 @@
+import json
+
+import kopf
+import requests
+
+
+class ServerSideApplyMixin:
+    def apply(self):
+        resp = self.api.patch(
+            **self.api_kwargs(
+                headers={
+                    "Content-Type": "application/apply-patch+yaml",
+                },
+                params={
+                    "fieldManager": "atmosphere-operator",
+                    "force": True,
+                },
+                data=json.dumps(self.obj),
+            )
+        )
+
+        try:
+            self.api.raise_for_status(resp)
+        except requests.exceptions.HTTPError:
+            if resp.status_code == 404:
+                raise kopf.TemporaryError("CRD is not yet installed", delay=1)
+            raise
+
+        self.set_obj(resp.json())
+        return self
diff --git a/atmosphere/operator/api/objects.py b/atmosphere/operator/api/objects.py
new file mode 100644
index 0000000..d668b00
--- /dev/null
+++ b/atmosphere/operator/api/objects.py
@@ -0,0 +1,233 @@
+from typing import ClassVar
+
+import pykube
+from pydantic import Field
+
+from atmosphere.operator import constants
+from atmosphere.operator.api import mixins, types
+
+# Kubernetes API
+
+
+class Namespace(types.KubernetesObject):
+    endpoint: ClassVar[str] = "namespaces"
+
+    version: str = Field("v1", alias="apiVersion", const=True)
+    kind: str = Field("Namespace", const=True)
+
+
+class Ingress(pykube.objects.Ingress, mixins.ServerSideApplyMixin):
+    pass
+
+
+class HelmRepository(types.NamespacedKubernetesObject):
+    endpoint: ClassVar[str] = "helmrepositories"
+
+    version: str = Field(
+        "source.toolkit.fluxcd.io/v1beta2", alias="apiVersion", const=True
+    )
+    kind: str = Field("HelmRepository", const=True)
+    spec: types.HelmRepositorySpec
+
+
+class HelmRelease(types.NamespacedKubernetesObject):
+    endpoint: ClassVar[str] = "helmreleases"
+
+    version: str = Field(
+        "helm.toolkit.fluxcd.io/v2beta1", alias="apiVersion", const=True
+    )
+    kind: str = Field("HelmRelease", const=True)
+    spec: types.HelmReleaseSpec
+
+
+class RabbitmqCluster(pykube.objects.NamespacedAPIObject, mixins.ServerSideApplyMixin):
+    version = "rabbitmq.com/v1beta1"
+    endpoint = "rabbitmqclusters"
+    kind = "RabbitmqCluster"
+
+
+# Atmosphere
+
+
+class OpenstackHelmRabbitmqCluster(types.NamespacedKubernetesObject):
+    endpoint: ClassVar[str] = "openstackhelmrabbitmqclusters"
+
+    kind: str = Field(constants.KIND_OPENSTACK_HELM_RABBITMQ_CLUSTER, const=True)
+    spec: types.OpenstackHelmRabbitmqClusterSpec
+
+    def apply_rabbitmq_cluster(self):
+        return RabbitmqCluster(
+            self.api,
+            {
+                "apiVersion": RabbitmqCluster.version,
+                "kind": RabbitmqCluster.kind,
+                "metadata": {
+                    "name": f"rabbitmq-{self.metadata.name}",
+                    "namespace": self.metadata.namespace,
+                    "annotations": self.metadata.annotations,
+                    "labels": self.metadata.labels,
+                },
+                "spec": {
+                    "image": self.spec.image,
+                    "affinity": {
+                        "nodeAffinity": {
+                            "requiredDuringSchedulingIgnoredDuringExecution": {
+                                "nodeSelectorTerms": [
+                                    {
+                                        "matchExpressions": [
+                                            {
+                                                "key": "openstack-control-plane",
+                                                "operator": "In",
+                                                "values": ["enabled"],
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        }
+                    },
+                    "rabbitmq": {
+                        "additionalConfig": "vm_memory_high_watermark.relative = 0.9\n"
+                    },
+                    "resources": {
+                        "requests": {"cpu": "500m", "memory": "1Gi"},
+                        "limits": {"cpu": "1", "memory": "2Gi"},
+                    },
+                    "terminationGracePeriodSeconds": 15,
+                },
+            },
+        ).apply()
+
+    def delete_rabbitmq_cluster(self):
+        rabbitmq_cluster = RabbitmqCluster.objects(
+            self.api, namespace=self.metadata.namespace
+        ).get_or_none(name=f"rabbitmq-{self.metadata.name}")
+        if rabbitmq_cluster:
+            rabbitmq_cluster.delete()
+
+
+class OpenstackHelmIngress(types.NamespacedKubernetesObject):
+    endpoint: ClassVar[str] = "openstackhelmingresses"
+
+    kind: str = Field(constants.KIND_OPENSTACK_HELM_INGRESS, const=True)
+    metadata: types.OpenstackHelmIngressObjectMeta
+    spec: types.OpenstackHelmIngressSpec
+
+    ENDPOINT_TO_SERVICE_MAPPING: ClassVar[dict] = {
+        types.OpenstackHelmIngressObjectMetaName.cloudformation: types.IngressServiceBackend(
+            name="heat-cfn",
+            port=types.ServiceBackendPort(number=8000),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.clustering: types.IngressServiceBackend(
+            name="senlin-api",
+            port=types.ServiceBackendPort(number=8778),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.compute: types.IngressServiceBackend(
+            name="nova-api",
+            port=types.ServiceBackendPort(number=8774),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.compute_novnc_proxy: types.IngressServiceBackend(
+            name="nova-novncproxy",
+            port=types.ServiceBackendPort(number=6080),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.container_infra: types.IngressServiceBackend(
+            name="magnum-api",
+            port=types.ServiceBackendPort(number=9511),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.container_infra_registry: types.IngressServiceBackend(
+            name="magnum-registry",
+            port=types.ServiceBackendPort(number=5000),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.dashboard: types.IngressServiceBackend(
+            name="horizon-int",
+            port=types.ServiceBackendPort(number=80),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.identity: types.IngressServiceBackend(
+            name="keystone-api",
+            port=types.ServiceBackendPort(number=5000),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.image: types.IngressServiceBackend(
+            name="glance-api",
+            port=types.ServiceBackendPort(number=9292),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.key_manager: types.IngressServiceBackend(
+            name="barbican-api",
+            port=types.ServiceBackendPort(number=9311),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.load_balancer: types.IngressServiceBackend(
+            name="octavia-api",
+            port=types.ServiceBackendPort(number=9876),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.network: types.IngressServiceBackend(
+            name="neutron-server",
+            port=types.ServiceBackendPort(number=9696),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.orchestration: types.IngressServiceBackend(
+            name="heat-api",
+            port=types.ServiceBackendPort(number=8004),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.placement: types.IngressServiceBackend(
+            name="placement-api",
+            port=types.ServiceBackendPort(number=8778),
+        ),
+        types.OpenstackHelmIngressObjectMetaName.volumev3: types.IngressServiceBackend(
+            name="cinder-api",
+            port=types.ServiceBackendPort(number=8776),
+        ),
+    }
+
+    @property
+    def service(self):
+        return self.ENDPOINT_TO_SERVICE_MAPPING[self.metadata.name]
+
+    def apply_ingress(self) -> Ingress:
+        return Ingress(
+            self.api,
+            {
+                "apiVersion": Ingress.version,
+                "kind": Ingress.kind,
+                "metadata": {
+                    "name": self.metadata.name,
+                    "namespace": self.metadata.namespace,
+                    "labels": self.metadata.labels,
+                    "annotations": {
+                        **{
+                            "cert-manager.io/cluster-issuer": self.spec.clusterIssuer,
+                        },
+                        **self.metadata.annotations,
+                    },
+                },
+                "spec": {
+                    "ingressClassName": self.spec.ingressClassName,
+                    "rules": [
+                        {
+                            "host": self.spec.host,
+                            "http": {
+                                "paths": [
+                                    {
+                                        "path": "/",
+                                        "pathType": "Prefix",
+                                        "backend": {
+                                            "service": self.service.dict(),
+                                        },
+                                    },
+                                ],
+                            },
+                        },
+                    ],
+                    "tls": [
+                        {
+                            "secretName": f"{self.service.name}-certs",
+                            "hosts": [self.spec.host],
+                        }
+                    ],
+                },
+            },
+        ).apply()
+
+    def delete_ingress(self) -> None:
+        ingress = Ingress.objects(
+            self.api, namespace=self.metadata.namespace
+        ).get_or_none(name=self.metadata.name)
+        if ingress:
+            ingress.delete()
diff --git a/atmosphere/operator/api/types.py b/atmosphere/operator/api/types.py
new file mode 100644
index 0000000..8e99769
--- /dev/null
+++ b/atmosphere/operator/api/types.py
@@ -0,0 +1,202 @@
+from enum import Enum
+from typing import Any
+
+import pydantic
+import pykube
+import validators.domain
+
+from atmosphere.operator import constants
+from atmosphere.operator.api import mixins
+
+# Generic
+
+
+class Hostname(str):
+    @classmethod
+    def __get_validators__(cls):
+        yield cls.validate
+
+    @classmethod
+    def __modify_schema__(cls, field_schema: dict[str, Any]) -> None:
+        field_schema.update(
+            examples=["example.com"],
+        )
+
+    @classmethod
+    def validate(cls, v):
+        if validators.domain(v):
+            return cls(v)
+
+    def __repr__(self):
+        return f"Hostname({super().__repr__()})"
+
+
+# Kubernetes API
+
+
+class ObjectMeta(pydantic.BaseModel):
+    name: pydantic.constr(min_length=1)
+    annotations: dict[str, str] = {}
+    labels: dict[str, str] = {}
+
+
+class NamespacedObjectMeta(ObjectMeta):
+    namespace: pydantic.constr(min_length=1)
+
+
+class KubernetesObject(pydantic.BaseModel, mixins.ServerSideApplyMixin):
+    api: pykube.http.HTTPClient = None
+
+    version: str = pydantic.Field(
+        constants.API_VERSION_ATMOSPHERE, alias="apiVersion", const=True
+    )
+    metadata: ObjectMeta
+
+    class Config:
+        allow_population_by_field_name = True
+        arbitrary_types_allowed = True
+        fields = {"api": {"exclude": True}}
+
+    @property
+    def obj(self) -> dict:
+        return self.dict(by_alias=True)
+
+    def set_obj(self, *_):
+        pass
+
+    @property
+    def namespace(self) -> str:
+        return None
+
+    @property
+    def name(self) -> str:
+        return self.metadata.name
+
+    @property
+    def base(self) -> None:
+        return None
+
+    def api_kwargs(self, **kwargs):
+        return pykube.objects.APIObject.api_kwargs(self, **kwargs)
+
+
+class NamespacedKubernetesObject(KubernetesObject):
+    metadata: NamespacedObjectMeta
+
+    @property
+    def namespace(self) -> str:
+        return self.metadata.namespace
+
+
+class ServiceBackendPort(pydantic.BaseModel):
+    number: pydantic.conint(ge=1, le=65535)
+
+
+class IngressServiceBackend(pydantic.BaseModel):
+    name: pydantic.constr(min_length=1)
+    port: ServiceBackendPort
+
+
+class NamespacedObjectReference(pydantic.BaseModel):
+    name: pydantic.constr(min_length=1)
+    namespace: pydantic.constr(min_length=1) = None
+
+
+class CrossNamespaceObjectReference(NamespacedObjectReference):
+    kind: pydantic.constr(min_length=1)
+
+
+class HelmRepositorySpec(pydantic.BaseModel):
+    url: pydantic.HttpUrl
+    interval: str = "60s"
+
+
+class HelmChartTemplateSpec(pydantic.BaseModel):
+    chart: pydantic.constr(min_length=1)
+    version: pydantic.constr(min_length=1) = None
+    source_ref: CrossNamespaceObjectReference = pydantic.Field(alias="sourceRef")
+
+    class Config:
+        allow_population_by_field_name = True
+
+
+class HelmChartTemplate(pydantic.BaseModel):
+    spec: HelmChartTemplateSpec
+
+
+class HelmReleaseActionSpecCRDsPolicy(str, Enum):
+    SKIP = "Skip"
+    CREATE = "Create"
+    CREATE_REPLACE = "CreateReplace"
+
+
+class HelmReleaseActionSpec(pydantic.BaseModel):
+    crds: HelmReleaseActionSpecCRDsPolicy = (
+        HelmReleaseActionSpecCRDsPolicy.CREATE_REPLACE
+    )
+    disable_wait: bool = pydantic.Field(default=True, alias="disableWait")
+
+    class Config:
+        allow_population_by_field_name = True
+
+
+class HelmReleaseValuesReference(pydantic.BaseModel):
+    kind: pydantic.constr(min_length=1)
+    name: pydantic.constr(min_length=1)
+    values_key: str = pydantic.Field(default=None, alias="valuesKey")
+    target_path: str = pydantic.Field(default=None, alias="targetPath")
+
+    class Config:
+        allow_population_by_field_name = True
+
+
+class HelmReleaseSpec(pydantic.BaseModel):
+    chart: HelmChartTemplate
+    interval: str = "60s"
+    depends_on: list[NamespacedObjectReference] = pydantic.Field(
+        default=[], alias="dependsOn"
+    )
+    install: HelmReleaseActionSpec = HelmReleaseActionSpec()
+    upgrade: HelmReleaseActionSpec = HelmReleaseActionSpec()
+    values: dict = {}
+    values_from: list[HelmReleaseValuesReference] = pydantic.Field(
+        default=[], alias="valuesFrom"
+    )
+
+    class Config:
+        allow_population_by_field_name = True
+
+
+# Atmosphere
+
+
+class OpenstackHelmRabbitmqClusterSpec(pydantic.BaseModel):
+    image: pydantic.constr(min_length=1)
+
+
+class OpenstackHelmIngressObjectMetaName(str, Enum):
+    cloudformation = "cloudformation"
+    clustering = "clustering"
+    compute = "compute"
+    compute_novnc_proxy = "compute-novnc-proxy"
+    container_infra = "container-infra"
+    container_infra_registry = "container-infra-registry"
+    dashboard = "dashboard"
+    identity = "identity"
+    image = "image"
+    key_manager = "key-manager"
+    load_balancer = "load-balancer"
+    network = "network"
+    orchestration = "orchestration"
+    placement = "placement"
+    volumev3 = "volumev3"
+
+
+class OpenstackHelmIngressObjectMeta(NamespacedObjectMeta):
+    name: OpenstackHelmIngressObjectMetaName
+
+
+class OpenstackHelmIngressSpec(pydantic.BaseModel):
+    clusterIssuer: pydantic.constr(min_length=1) = "atmosphere"
+    ingressClassName: pydantic.constr(min_length=1) = "atmosphere"
+    host: Hostname
diff --git a/atmosphere/operator/constants.py b/atmosphere/operator/constants.py
index f487040..3fd102d 100644
--- a/atmosphere/operator/constants.py
+++ b/atmosphere/operator/constants.py
@@ -1,3 +1,8 @@
+API_VERSION_ATMOSPHERE = "atmosphere.vexxhost.com/v1alpha1"
+
+KIND_OPENSTACK_HELM_RABBITMQ_CLUSTER = "OpenstackHelmRabbitmqCluster"
+KIND_OPENSTACK_HELM_INGRESS = "OpenstackHelmIngress"
+
 IMAGE_LIST = {
     "alertmanager": "quay.io/prometheus/alertmanager:v0.24.0",
     "atmosphere": "quay.io/vexxhost/atmosphere:0.13.0",  # x-release-please-version
diff --git a/atmosphere/operator/controllers/cloud.py b/atmosphere/operator/controllers/cloud.py
index 39c836f..0ea5dc2 100644
--- a/atmosphere/operator/controllers/cloud.py
+++ b/atmosphere/operator/controllers/cloud.py
@@ -5,25 +5,42 @@
 from taskflow.listeners import logging as logging_listener
 from taskflow.patterns import graph_flow
 
-from atmosphere.operator import tasks
-from atmosphere.operator.api import Cloud
+from atmosphere import clients, flows
+from atmosphere.models import config
+from atmosphere.operator import tasks, utils
+from atmosphere.operator.api import Cloud, objects, types
 
 
 @kopf.on.resume(Cloud.version, Cloud.kind)
 @kopf.on.create(Cloud.version, Cloud.kind)
 def create_fn(namespace: str, name: str, spec: dict, **_):
+    api = clients.get_pykube_api()
+
+    # TODO(mnaser): Get rid of this flow.
+    cfg = config.Config.from_file()
+    engine = flows.get_engine(cfg)
+    engine.run()
+
     flow = graph_flow.Flow("deploy").add(
-        tasks.BuildApiClient(),
         tasks.GenerateImageTagsConfigMap(provides="image_tags"),
         tasks.GenerateSecrets(provides="secrets"),
     )
 
     if spec["magnum"].get("enabled", True):
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="magnum",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
         flow.add(
             tasks.InstallClusterApiTask(),
-            tasks.ApplyRabbitmqClusterTask(
-                inject={"chart_name": "magnum"}, provides="magnum_rabbitmq"
-            ),
             tasks.GetChartValues(
                 inject={
                     "helm_repository": "openstack-helm",
@@ -35,11 +52,10 @@
             ),
             tasks.GenerateReleaseValues(
                 inject={"chart_name": "magnum"},
-                rebind={"rabbitmq": "magnum_rabbitmq"},
                 provides="magnum_release_values",
             ),
             tasks.GenerateMagnumChartValuesFrom(
-                rebind={"rabbitmq": "magnum_rabbitmq"},
+                inject={"chart_name": "magnum"},
                 provides="magnum_values_from",
             ),
             tasks.ApplyHelmReleaseTask(
@@ -54,18 +70,145 @@
                     "values_from": "magnum_values_from",
                 },
             ),
-            tasks.ApplyIngressTask(
-                inject={"endpoint": "container_infra"},
-                rebind={
-                    "chart_values": "magnum_chart_values",
-                    "release_values": "magnum_release_values",
-                },
-            ),
         )
+        objects.OpenstackHelmIngress(
+            api=api,
+            metadata=types.OpenstackHelmIngressObjectMeta(
+                name="container-infra",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmIngressSpec(
+                clusterIssuer=spec["certManagerClusterIssuer"],
+                ingressClassName=spec["ingressClassName"],
+                host=spec["magnum"]["endpoint"],
+            ),
+        ).apply()
+
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="keystone",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="barbican",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="glance",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="cinder",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="neutron",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="nova",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="octavia",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="senlin",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="designate",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
+        objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="heat",
+                namespace=namespace,
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image=utils.get_image_ref(
+                    "rabbitmq_server", override_registry=spec["imageRepository"]
+                ).string()
+            ),
+        ).apply()
 
     engine = engines.load(
         flow,
         store={
+            "api": api,
             "namespace": namespace,
             "name": name,
             "spec": spec,
diff --git a/atmosphere/operator/tasks.py b/atmosphere/operator/tasks.py
index 01710b3..68fcc50 100644
--- a/atmosphere/operator/tasks.py
+++ b/atmosphere/operator/tasks.py
@@ -12,24 +12,12 @@
 from taskflow import task
 from tenacity import retry, retry_if_result, stop_after_delay, wait_fixed
 
-from atmosphere import clients
 from atmosphere.operator import constants, utils
 
 LOG = logging.getLogger(__name__)
 
 
-class BuildApiClient(task.Task):
-    default_provides = "api"
-
-    def execute(self) -> pykube.HTTPClient:
-        return clients.get_pykube_api()
-
-
 class ApplyKubernetesObjectTask(task.Task):
-    @property
-    def api(self):
-        return clients.get_pykube_api()
-
     def generate_object(self, *args, **kwargs) -> pykube.objects.APIObject:
         raise NotImplementedError
 
@@ -90,61 +78,6 @@
             )
 
 
-class RabbitmqCluster(pykube.objects.NamespacedAPIObject):
-    version = "rabbitmq.com/v1beta1"
-    endpoint = "rabbitmqclusters"
-    kind = "RabbitmqCluster"
-
-
-class ApplyRabbitmqClusterTask(ApplyKubernetesObjectTask):
-    def execute(
-        self, api: pykube.HTTPClient, namespace: str, chart_name: str, spec: dict
-    ) -> dict:
-        resource = RabbitmqCluster(
-            api,
-            {
-                "apiVersion": RabbitmqCluster.version,
-                "kind": RabbitmqCluster.kind,
-                "metadata": {
-                    "name": f"rabbitmq-{chart_name}",
-                    "namespace": namespace,
-                },
-                "spec": {
-                    "image": utils.get_image_ref(
-                        "rabbitmq_server", override_registry=spec["imageRepository"]
-                    ).string(),
-                    "affinity": {
-                        "nodeAffinity": {
-                            "requiredDuringSchedulingIgnoredDuringExecution": {
-                                "nodeSelectorTerms": [
-                                    {
-                                        "matchExpressions": [
-                                            {
-                                                "key": "openstack-control-plane",
-                                                "operator": "In",
-                                                "values": ["enabled"],
-                                            }
-                                        ]
-                                    }
-                                ]
-                            }
-                        }
-                    },
-                    "rabbitmq": {
-                        "additionalConfig": "vm_memory_high_watermark.relative = 0.9\n"
-                    },
-                    "resources": {
-                        "requests": {"cpu": "500m", "memory": "1Gi"},
-                        "limits": {"cpu": "1", "memory": "2Gi"},
-                    },
-                    "terminationGracePeriodSeconds": 15,
-                },
-            },
-        )
-
-        return self._apply(resource)
-
-
 class HelmRelease(pykube.objects.NamespacedAPIObject):
     version = "helm.toolkit.fluxcd.io/v2beta1"
     endpoint = "helmreleases"
@@ -293,7 +226,7 @@
 
 
 class GenerateReleaseValues(task.Task):
-    def _generate_base(self, rabbitmq: RabbitmqCluster, spec: dict) -> dict:
+    def _generate_base(self, rabbitmq: str, spec: dict) -> dict:
         return {
             "endpoints": {
                 "identity": {
@@ -314,7 +247,7 @@
                     "statefulset": None,
                     "hosts": {
                         # TODO(mnaser): handle scenario when those don't exist
-                        "default": rabbitmq.name,
+                        "default": rabbitmq,
                     },
                 },
             },
@@ -410,10 +343,10 @@
             },
         }
 
-    def execute(self, chart_name: str, rabbitmq: RabbitmqCluster, spec: dict) -> dict:
+    def execute(self, chart_name: str, spec: dict) -> dict:
         return mergedeep.merge(
             {},
-            self._generate_base(rabbitmq, spec),
+            self._generate_base(f"rabbitmq-{chart_name}", spec),
             getattr(self, f"_generate_{chart_name}")(spec),
             spec[chart_name].get("overrides", {}),
         )
@@ -422,9 +355,9 @@
 class GenerateMagnumChartValuesFrom(task.Task):
     def execute(
         self,
+        chart_name: str,
         image_tags: pykube.ConfigMap,
         secrets: pykube.Secret,
-        rabbitmq: RabbitmqCluster,
     ) -> dict:
         return [
             {
@@ -475,13 +408,13 @@
             },
             {
                 "kind": pykube.Secret.kind,
-                "name": f"{rabbitmq.name}-default-user",
+                "name": f"rabbitmq-{chart_name}-default-user",
                 "targetPath": "endpoints.oslo_messaging.auth.admin.username",
                 "valuesKey": "username",
             },
             {
                 "kind": pykube.Secret.kind,
-                "name": f"{rabbitmq.name}-default-user",
+                "name": f"rabbitmq-{chart_name}-default-user",
                 "targetPath": "endpoints.oslo_messaging.auth.admin.password",
                 "valuesKey": "password",
             },
@@ -494,67 +427,6 @@
         ]
 
 
-class ApplyIngressTask(ApplyKubernetesObjectTask):
-    def execute(
-        self,
-        api: pykube.HTTPClient,
-        namespace: str,
-        endpoint: str,
-        spec: dict,
-        chart_values: dict,
-        release_values: dict,
-    ) -> pykube.Ingress:
-        host = release_values["endpoints"][endpoint]["host_fqdn_override"]["public"][
-            "host"
-        ]
-        service_name = chart_values["endpoints"][endpoint]["hosts"]["default"]
-        service_port = chart_values["endpoints"][endpoint]["port"]["api"]["default"]
-
-        resource = pykube.Ingress(
-            api,
-            {
-                "apiVersion": pykube.Ingress.version,
-                "kind": pykube.Ingress.kind,
-                "metadata": {
-                    "name": endpoint.replace("_", "-"),
-                    "namespace": namespace,
-                    "annotations": {
-                        "cert-manager.io/cluster-issuer": spec[
-                            "certManagerClusterIssuer"
-                        ],
-                    },
-                },
-                "spec": {
-                    "ingressClassName": spec["ingressClassName"],
-                    "rules": [
-                        {
-                            "host": host,
-                            "http": {
-                                "paths": [
-                                    {
-                                        "path": "/",
-                                        "pathType": "Prefix",
-                                        "backend": {
-                                            "service": {
-                                                "name": service_name,
-                                                "port": {
-                                                    "number": service_port,
-                                                },
-                                            },
-                                        },
-                                    },
-                                ],
-                            },
-                        },
-                    ],
-                    "tls": [{"secretName": f"{service_name}-certs", "hosts": [host]}],
-                },
-            },
-        )
-
-        return self._apply(resource)
-
-
 class GenerateOpenStackHelmEndpoints(task.Task):
     SKIPPED_ENDPOINTS = (
         "cluster_domain_suffix",
diff --git a/atmosphere/operator/utils.py b/atmosphere/operator/utils.py
index fa8bf63..57cfecc 100644
--- a/atmosphere/operator/utils.py
+++ b/atmosphere/operator/utils.py
@@ -7,7 +7,7 @@
     image_name: str, override_registry: str = None
 ) -> reference.Reference:
     ref = reference.Reference.parse(constants.IMAGE_LIST[image_name])
-    if override_registry is None:
+    if not override_registry:
         return ref
 
     # NOTE(mnaser): We re-write the name of a few images to make sense of them
@@ -23,3 +23,11 @@
     ref = reference.Reference.parse(ref.string())
 
     return ref
+
+
+def filter_annotations(annotations: dict) -> dict:
+    return {
+        key: value
+        for key, value in annotations.items()
+        if key != "kopf.zalando.org/last-handled-configuration"
+    }
diff --git a/atmosphere/tasks/composite/openstack_helm.py b/atmosphere/tasks/composite/openstack_helm.py
index 37d40dc..acd9b89 100644
--- a/atmosphere/tasks/composite/openstack_helm.py
+++ b/atmosphere/tasks/composite/openstack_helm.py
@@ -133,67 +133,6 @@
     }
 
 
-def kube_prometheus_stack_tasks_from_config(
-    config: config.KubePrometheusStackChartConfig, opsgenie: config.OpsGenieConfig
-):
-    if not config.enabled:
-        return []
-
-    values = mergedeep.merge(
-        {},
-        constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_VALUES,
-        config.overrides,
-    )
-
-    if opsgenie.enabled:
-        values["alertmanager"]["config"] = generate_alertmanager_config_for_opsgenie(
-            opsgenie
-        )
-
-    return [
-        flux.ApplyHelmRepositoryTask(
-            namespace=constants.NAMESPACE_MONITORING,
-            name=constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY,
-            url=constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY_URL,
-        ),
-        flux.ApplyHelmReleaseTask(
-            namespace=config.namespace,
-            name=constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_NAME,
-            repository=constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY,
-            chart=constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_NAME,
-            version=constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_VERSION,
-            values=values,
-        ),
-    ]
-
-
-def ingress_nginx_tasks_from_config(config: config.IngressNginxChartConfig):
-    if not config.enabled:
-        return []
-
-    values = mergedeep.merge(
-        {},
-        constants.HELM_RELEASE_INGRESS_NGINX_VALUES,
-        config.overrides,
-    )
-
-    return [
-        flux.ApplyHelmRepositoryTask(
-            namespace=config.namespace,
-            name=constants.HELM_REPOSITORY_INGRESS_NGINX,
-            url=constants.HELM_REPOSITORY_INGRESS_NGINX_URL,
-        ),
-        flux.ApplyHelmReleaseTask(
-            namespace=config.namespace,
-            name=constants.HELM_RELEASE_INGRESS_NGINX_NAME,
-            repository=constants.HELM_REPOSITORY_INGRESS_NGINX,
-            chart=constants.HELM_RELEASE_INGRESS_NGINX_NAME,
-            version=constants.HELM_RELEASE_INGRESS_NGINX_VERSION,
-            values=values,
-        ),
-    ]
-
-
 class PerconaXtraDBCluster(pykube.objects.NamespacedAPIObject):
     version = "pxc.percona.com/v1-10-0"
     endpoint = "perconaxtradbclusters"
@@ -206,9 +145,6 @@
             kind=PerconaXtraDBCluster,
             namespace=constants.NAMESPACE_OPENSTACK,
             name="percona-xtradb",
-            requires=[
-                f"helm-release-{constants.NAMESPACE_OPENSTACK}-{constants.HELM_RELEASE_PXC_OPERATOR_NAME}",
-            ],
         )
 
     def generate_object(self) -> PerconaXtraDBCluster:
@@ -276,64 +212,3 @@
                 },
             },
         )
-
-
-class RabbitmqCluster(pykube.objects.NamespacedAPIObject):
-    version = "rabbitmq.com/v1beta1"
-    endpoint = "rabbitmqclusters"
-    kind = "RabbitmqCluster"
-
-
-class ApplyRabbitmqClusterTask(base.ApplyKubernetesObjectTask):
-    def __init__(self, name: str):
-        super().__init__(
-            kind=RabbitmqCluster,
-            namespace=constants.NAMESPACE_OPENSTACK,
-            name=name,
-            requires=[
-                f"helm-release-{constants.NAMESPACE_OPENSTACK}-{constants.HELM_RELEASE_RABBITMQ_OPERATOR_NAME}",
-            ],
-        )
-
-    def generate_object(self) -> RabbitmqCluster:
-        return RabbitmqCluster(
-            self.api,
-            {
-                "apiVersion": self._obj_kind.version,
-                "kind": self._obj_kind.kind,
-                "metadata": {
-                    "name": f"rabbitmq-{self._obj_name}",
-                    "namespace": self._obj_namespace,
-                },
-                "spec": {
-                    "image": utils.get_image_ref_using_legacy_image_repository(
-                        "rabbitmq_server"
-                    ).string(),
-                    "affinity": {
-                        "nodeAffinity": {
-                            "requiredDuringSchedulingIgnoredDuringExecution": {
-                                "nodeSelectorTerms": [
-                                    {
-                                        "matchExpressions": [
-                                            {
-                                                "key": "openstack-control-plane",
-                                                "operator": "In",
-                                                "values": ["enabled"],
-                                            }
-                                        ]
-                                    }
-                                ]
-                            }
-                        }
-                    },
-                    "rabbitmq": {
-                        "additionalConfig": "vm_memory_high_watermark.relative = 0.9\n"
-                    },
-                    "resources": {
-                        "requests": {"cpu": "500m", "memory": "1Gi"},
-                        "limits": {"cpu": "1", "memory": "2Gi"},
-                    },
-                    "terminationGracePeriodSeconds": 15,
-                },
-            },
-        )
diff --git a/atmosphere/tasks/constants.py b/atmosphere/tasks/constants.py
index 00b1931..5e99700 100644
--- a/atmosphere/tasks/constants.py
+++ b/atmosphere/tasks/constants.py
@@ -593,11 +593,6 @@
     },
     "useCertManager": True,
 }
-HELM_RELEASE_RABBITMQ_OPERATOR_REQUIRES = set(
-    [
-        f"helm-release-{NAMESPACE_CERT_MANAGER}-{HELM_RELEASE_CERT_MANAGER_NAME}",
-    ]
-)
 
 HELM_RELEASE_PXC_OPERATOR_NAME = "pxc-operator"
 HELM_RELEASE_PXC_OPERATOR_VERSION = "1.10.0"
diff --git a/atmosphere/tasks/kubernetes/base.py b/atmosphere/tasks/kubernetes/base.py
index 7159310..b8f5bae 100644
--- a/atmosphere/tasks/kubernetes/base.py
+++ b/atmosphere/tasks/kubernetes/base.py
@@ -3,6 +3,7 @@
 
 import pykube
 from taskflow import task
+from tenacity import retry, stop_after_delay, wait_fixed
 
 from atmosphere import clients, logger
 
@@ -23,12 +24,6 @@
             kwargs["name"] += f"-{namespace}"
         kwargs["name"] += f"-{name}"
 
-        if namespace:
-            # kwargs.setdefault("requires", [])
-            # kwargs["requires"] += [f"namespace-{namespace}"]
-            kwargs.setdefault("rebind", {})
-            kwargs["rebind"]["namespace"] = f"namespace-{namespace}"
-
         kwargs.setdefault("provides", set())
         kwargs["provides"] = kwargs["provides"].union(set([kwargs["name"]]))
 
@@ -56,6 +51,7 @@
     def wait_for_resource(self, resource: pykube.objects.APIObject):
         pass
 
+    @retry(wait=wait_fixed(2), stop=stop_after_delay(120))
     def execute(self, *args, **kwargs):
         self.logger.debug("Ensuring resource")
 
diff --git a/atmosphere/tasks/kubernetes/cert_manager.py b/atmosphere/tasks/kubernetes/cert_manager.py
index 61a741d..2c420a3 100644
--- a/atmosphere/tasks/kubernetes/cert_manager.py
+++ b/atmosphere/tasks/kubernetes/cert_manager.py
@@ -19,11 +19,6 @@
             kind=Certificate,
             namespace=namespace,
             name=name,
-            requires=set(
-                [
-                    f"helm-release-{constants.NAMESPACE_CERT_MANAGER}-{constants.HELM_RELEASE_CERT_MANAGER_NAME}",
-                ]
-            ),
         )
 
     def generate_object(self) -> Certificate:
@@ -55,11 +50,6 @@
             kind=ClusterIssuer,
             namespace=None,
             name=name,
-            requires=set(
-                [
-                    f"helm-release-{constants.NAMESPACE_CERT_MANAGER}-{constants.HELM_RELEASE_CERT_MANAGER_NAME}",
-                ]
-            ),
         )
 
     def generate_object(self) -> ClusterIssuer:
diff --git a/atmosphere/tasks/kubernetes/flux.py b/atmosphere/tasks/kubernetes/flux.py
index 77d6f22..3294897 100644
--- a/atmosphere/tasks/kubernetes/flux.py
+++ b/atmosphere/tasks/kubernetes/flux.py
@@ -8,41 +8,6 @@
 LOG = logger.get_logger()
 
 
-class HelmRepository(pykube.objects.NamespacedAPIObject):
-    version = "source.toolkit.fluxcd.io/v1beta2"
-    endpoint = "helmrepositories"
-    kind = "HelmRepository"
-
-
-class ApplyHelmRepositoryTask(base.ApplyKubernetesObjectTask):
-    def __init__(self, namespace: str, name: str, url: str):
-        self._url = url
-
-        super().__init__(
-            kind=HelmRepository,
-            namespace=namespace,
-            name=name,
-            requires=set(["namespace"]),
-        )
-
-    def generate_object(self) -> HelmRepository:
-        return HelmRepository(
-            self.api,
-            {
-                "apiVersion": self._obj_kind.version,
-                "kind": self._obj_kind.kind,
-                "metadata": {
-                    "name": self._obj_name,
-                    "namespace": self._obj_namespace,
-                },
-                "spec": {
-                    "interval": "1m",
-                    "url": self._url,
-                },
-            },
-        )
-
-
 class HelmRelease(pykube.objects.NamespacedAPIObject):
     version = "helm.toolkit.fluxcd.io/v2beta1"
     endpoint = "helmreleases"
@@ -68,14 +33,10 @@
         self._values = values
         self._values_from = values_from
 
-        kwargs.setdefault("requires", set())
-        kwargs["requires"] = kwargs["requires"].union(set(["repository"]))
-
         super().__init__(
             kind=HelmRelease,
             namespace=namespace,
             name=name,
-            rebind={"repository": f"helm-repository-{namespace}-{repository}"},
             *args,
             **kwargs,
         )
diff --git a/atmosphere/tasks/kubernetes/v1.py b/atmosphere/tasks/kubernetes/v1.py
index 771bbe9..fc1a881 100644
--- a/atmosphere/tasks/kubernetes/v1.py
+++ b/atmosphere/tasks/kubernetes/v1.py
@@ -6,47 +6,6 @@
 LOG = logger.get_logger()
 
 
-class ApplyNamespaceTask(base.ApplyKubernetesObjectTask):
-    def __init__(self, name: str):
-        super().__init__(kind=pykube.Namespace, namespace=None, name=name)
-
-    def generate_object(self) -> pykube.Namespace:
-        return pykube.Namespace(
-            self.api,
-            {
-                "apiVersion": self._obj_kind.version,
-                "kind": self._obj_kind.kind,
-                "metadata": {"name": self._obj_name},
-            },
-        )
-
-
-class ApplyConfigMapTask(base.ApplyKubernetesObjectTask):
-    def __init__(self, namespace: str, name: str, data: str):
-        self._data = data
-
-        super().__init__(
-            kind=pykube.ConfigMap,
-            namespace=namespace,
-            name=name,
-            requires=set(["namespace"]),
-        )
-
-    def generate_object(self) -> pykube.ConfigMap:
-        return pykube.ConfigMap(
-            self.api,
-            {
-                "apiVersion": self._obj_kind.version,
-                "kind": self._obj_kind.kind,
-                "metadata": {
-                    "name": self._obj_name,
-                    "namespace": self._obj_namespace,
-                },
-                "data": self._data,
-            },
-        )
-
-
 class ApplyServiceTask(base.ApplyKubernetesObjectTask):
     def __init__(self, namespace: str, name: str, labels: dict, spec: dict):
         self._labels = labels
@@ -56,7 +15,6 @@
             kind=pykube.Service,
             namespace=namespace,
             name=name,
-            requires=set(["namespace"]),
         )
 
     def generate_object(self) -> pykube.Service:
@@ -83,7 +41,6 @@
             kind=pykube.Secret,
             namespace=namespace,
             name=name,
-            requires=set(["namespace"]),
         )
 
     def generate_object(self) -> pykube.Secret:
@@ -99,40 +56,3 @@
                 "stringData": self._data,
             },
         )
-
-
-class ApplyIngressTask(base.ApplyKubernetesObjectTask):
-    def __init__(
-        self,
-        namespace: str,
-        name: str,
-        spec: dict,
-        annotations: dict = {},
-        labels: dict = {},
-    ):
-        self._annotations = annotations
-        self._labels = labels
-        self._spec = spec
-
-        super().__init__(
-            kind=pykube.Ingress,
-            namespace=namespace,
-            name=name,
-            requires=set(["namespace"]),
-        )
-
-    def generate_object(self) -> pykube.Ingress:
-        return pykube.Ingress(
-            self.api,
-            {
-                "apiVersion": self._obj_kind.version,
-                "kind": self._obj_kind.kind,
-                "metadata": {
-                    "name": self._obj_name,
-                    "namespace": self._obj_namespace,
-                    "annotations": self._annotations,
-                    "labels": self._labels,
-                },
-                "spec": self._spec,
-            },
-        )
diff --git a/atmosphere/tests/conftest.py b/atmosphere/tests/conftest.py
index c830701..7092894 100644
--- a/atmosphere/tests/conftest.py
+++ b/atmosphere/tests/conftest.py
@@ -3,6 +3,7 @@
 
 import pytest
 import requests
+import responses
 
 from atmosphere.models import config
 
@@ -19,7 +20,45 @@
 
 
 @pytest.fixture
+def kubeconfig(tmpdir):
+    kubeconfig = tmpdir.join("kubeconfig")
+    kubeconfig.write(
+        """
+apiVersion: v1
+clusters:
+- cluster: {server: 'https://localhost:9443'}
+  name: test
+contexts:
+- context: {cluster: test, user: test}
+  name: test
+current-context: test
+kind: Config
+preferences: {}
+users:
+- name: test
+  user: {token: testtoken}
+    """
+    )
+    return kubeconfig
+
+
+@pytest.fixture
+def requests_mock():
+    return responses.RequestsMock(target="pykube.http.KubernetesHTTPAdapter._do_send")
+
+
+@pytest.fixture
+def api(kubeconfig):
+    import pykube
+
+    config = pykube.KubeConfig.from_file(str(kubeconfig))
+    return pykube.HTTPClient(config)
+
+
+@pytest.fixture
 def pykube(mocker):
+    # TODO(mnaser): We should get rid of this fixture and rename the other one
+    #               to pykube.
     mocked_api = mocker.MagicMock()
     mocker.patch("atmosphere.clients.get_pykube_api", return_value=mocked_api)
     return mocked_api
diff --git a/atmosphere/tests/e2e/test_operator.py b/atmosphere/tests/e2e/test_operator.py
index cf1bd48..922ecad 100644
--- a/atmosphere/tests/e2e/test_operator.py
+++ b/atmosphere/tests/e2e/test_operator.py
@@ -67,7 +67,7 @@
             wait=wait_fixed(1),
         ):
             with attempt:
-                assert "Initial authentication has finished." in pod.logs()
+                assert "kind=Secret name=atmosphere-memcached" in pod.logs()
 
     for secret_name in ["atmosphere-config", "atmosphere-memcached"]:
         secret = pykube.Secret.objects(
diff --git a/atmosphere/tests/unit/operator/test_objects.py b/atmosphere/tests/unit/operator/test_objects.py
new file mode 100644
index 0000000..017166e
--- /dev/null
+++ b/atmosphere/tests/unit/operator/test_objects.py
@@ -0,0 +1,608 @@
+import json
+
+import pytest
+import responses
+from hypothesis import given
+from hypothesis import provisional as prov
+from hypothesis import strategies as st
+
+from atmosphere.operator.api import objects, types
+
+
+class TestNamespace:
+    @given(st.builds(objects.Namespace))
+    def test_property(self, instance):
+        assert isinstance(instance, objects.Namespace)
+        assert isinstance(instance.metadata, types.ObjectMeta)
+
+    def test_apply(self, api, requests_mock):
+        instance = objects.Namespace(
+            api=api,
+            metadata=types.ObjectMeta(
+                name="openstack",
+                annotations={
+                    "annotate": "this",
+                },
+                labels={
+                    "foo": "bar",
+                },
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.PATCH,
+                "https://localhost:9443/api/v1/namespaces/openstack?fieldManager=atmosphere-operator&force=True",
+                json={},
+            )
+
+            instance.apply()
+
+            assert len(rsps.calls) == 1
+            assert json.loads(rsps.calls[0].request.body) == {
+                "apiVersion": "v1",
+                "kind": "Namespace",
+                "metadata": {
+                    "name": "openstack",
+                    "annotations": {
+                        "annotate": "this",
+                    },
+                    "labels": {
+                        "foo": "bar",
+                    },
+                },
+            }
+
+
+class TestHelmRepository:
+    @given(
+        st.builds(
+            objects.HelmRepository,
+            spec=st.builds(types.HelmRepositorySpec, url=prov.urls()),
+        )
+    )
+    def test_property(self, instance):
+        assert isinstance(instance, objects.HelmRepository)
+        assert isinstance(instance.spec, types.HelmRepositorySpec)
+
+    def test_apply(self, api, requests_mock):
+        instance = objects.HelmRepository(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="openstack-helm",
+                namespace="openstack",
+                annotations={
+                    "annotate": "this",
+                },
+                labels={
+                    "foo": "bar",
+                },
+            ),
+            spec=types.HelmRepositorySpec(
+                url="https://tarballs.opendev.org/openstack/openstack-helm/",
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.PATCH,
+                "https://localhost:9443/apis/source.toolkit.fluxcd.io/v1beta2/namespaces/openstack/helmrepositories/openstack-helm?fieldManager=atmosphere-operator&force=True",  # noqa E501
+                json={},
+            )
+
+            instance.apply()
+
+            assert len(rsps.calls) == 1
+            assert json.loads(rsps.calls[0].request.body) == {
+                "apiVersion": "source.toolkit.fluxcd.io/v1beta2",
+                "kind": "HelmRepository",
+                "metadata": {
+                    "name": "openstack-helm",
+                    "namespace": "openstack",
+                    "annotations": {
+                        "annotate": "this",
+                    },
+                    "labels": {
+                        "foo": "bar",
+                    },
+                },
+                "spec": {
+                    "interval": "60s",
+                    "url": "https://tarballs.opendev.org/openstack/openstack-helm/",
+                },
+            }
+
+
+class TestHelmRelease:
+    @given(st.builds(objects.HelmRelease))
+    def test_property(self, instance):
+        assert isinstance(instance, objects.HelmRelease)
+        assert isinstance(instance.spec, types.HelmReleaseSpec)
+
+    def test_apply(self, api, requests_mock):
+        instance = objects.HelmRelease(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="neutron",
+                namespace="openstack",
+                annotations={
+                    "annotate": "this",
+                },
+                labels={
+                    "foo": "bar",
+                },
+            ),
+            spec=types.HelmReleaseSpec(
+                chart=types.HelmChartTemplate(
+                    spec=types.HelmChartTemplateSpec(
+                        chart="neutron",
+                        version="0.1.0",
+                        source_ref=types.CrossNamespaceObjectReference(
+                            kind="HelmRepository",
+                            name="openstack-helm",
+                            namespace="openstack",
+                        ),
+                    )
+                ),
+                values={
+                    "foo": "bar",
+                },
+                values_from=[
+                    types.HelmReleaseValuesReference(
+                        kind="Secret",
+                        name="rabbitmq-neutron-default-user",
+                        values_key="username",
+                        target_path="rabbitmq.username",
+                    )
+                ],
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.PATCH,
+                "https://localhost:9443/apis/helm.toolkit.fluxcd.io/v2beta1/namespaces/openstack/helmreleases/neutron?fieldManager=atmosphere-operator&force=True",  # noqa E501
+                json={},
+            )
+
+            instance.apply()
+
+            assert len(rsps.calls) == 1
+            assert json.loads(rsps.calls[0].request.body) == {
+                "apiVersion": "helm.toolkit.fluxcd.io/v2beta1",
+                "kind": "HelmRelease",
+                "metadata": {
+                    "name": instance.metadata.name,
+                    "namespace": instance.metadata.namespace,
+                    "labels": instance.metadata.labels,
+                    "annotations": instance.metadata.annotations,
+                },
+                "spec": {
+                    "chart": {
+                        "spec": {
+                            "chart": "neutron",
+                            "version": "0.1.0",
+                            "sourceRef": {
+                                "kind": "HelmRepository",
+                                "name": "openstack-helm",
+                                "namespace": "openstack",
+                            },
+                        },
+                    },
+                    "dependsOn": [],
+                    "install": {
+                        "crds": "CreateReplace",
+                        "disableWait": True,
+                    },
+                    "interval": "60s",
+                    "upgrade": {
+                        "crds": "CreateReplace",
+                        "disableWait": True,
+                    },
+                    "values": {
+                        "foo": "bar",
+                    },
+                    "valuesFrom": [
+                        {
+                            "kind": "Secret",
+                            "name": "rabbitmq-neutron-default-user",
+                            "valuesKey": "username",
+                            "targetPath": "rabbitmq.username",
+                        },
+                    ],
+                },
+            }
+
+
+class TestOpenstackHelmRabbitmqCluster:
+    @given(st.builds(objects.OpenstackHelmRabbitmqCluster))
+    def test_property(self, instance):
+        assert isinstance(instance, objects.OpenstackHelmRabbitmqCluster)
+        assert isinstance(instance.metadata, types.NamespacedObjectMeta)
+        assert isinstance(instance.spec, types.OpenstackHelmRabbitmqClusterSpec)
+
+    def test_apply(self, api, requests_mock):
+        instance = objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="neutron",
+                namespace="default",
+                annotations={
+                    "annotate": "this",
+                },
+                labels={
+                    "foo": "bar",
+                },
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image="rabbitmq:3.8.9",
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.PATCH,
+                "https://localhost:9443/apis/atmosphere.vexxhost.com/v1alpha1/namespaces/default/openstackhelmrabbitmqclusters/neutron?fieldManager=atmosphere-operator&force=True",  # noqa E501
+                json={},
+            )
+
+            instance.apply()
+
+            assert len(rsps.calls) == 1
+            assert json.loads(rsps.calls[0].request.body) == {
+                "apiVersion": "atmosphere.vexxhost.com/v1alpha1",
+                "kind": "OpenstackHelmRabbitmqCluster",
+                "metadata": {
+                    "name": "neutron",
+                    "namespace": "default",
+                    "labels": {
+                        "foo": "bar",
+                    },
+                    "annotations": {
+                        "annotate": "this",
+                    },
+                },
+                "spec": {
+                    "image": "rabbitmq:3.8.9",
+                },
+            }
+
+    def test_apply_rabbitmq_cluster(self, api, requests_mock):
+        instance = objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="neutron",
+                namespace="default",
+                annotations={
+                    "annotate": "this",
+                },
+                labels={
+                    "foo": "bar",
+                },
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image="rabbitmq:3.8.9",
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.PATCH,
+                "https://localhost:9443/apis/rabbitmq.com/v1beta1/namespaces/default/rabbitmqclusters/rabbitmq-neutron?fieldManager=atmosphere-operator&force=True",  # noqa E501
+                json={},
+            )
+
+            instance.apply_rabbitmq_cluster()
+
+            assert len(rsps.calls) == 1
+            assert json.loads(rsps.calls[0].request.body) == {
+                "apiVersion": "rabbitmq.com/v1beta1",
+                "kind": "RabbitmqCluster",
+                "metadata": {
+                    "name": "rabbitmq-neutron",
+                    "namespace": "default",
+                    "annotations": {
+                        "annotate": "this",
+                    },
+                    "labels": {
+                        "foo": "bar",
+                    },
+                },
+                "spec": {
+                    "image": "rabbitmq:3.8.9",
+                    "affinity": {
+                        "nodeAffinity": {
+                            "requiredDuringSchedulingIgnoredDuringExecution": {
+                                "nodeSelectorTerms": [
+                                    {
+                                        "matchExpressions": [
+                                            {
+                                                "key": "openstack-control-plane",
+                                                "operator": "In",
+                                                "values": ["enabled"],
+                                            }
+                                        ]
+                                    }
+                                ]
+                            }
+                        }
+                    },
+                    "rabbitmq": {
+                        "additionalConfig": "vm_memory_high_watermark.relative = 0.9\n"
+                    },
+                    "resources": {
+                        "requests": {"cpu": "500m", "memory": "1Gi"},
+                        "limits": {"cpu": "1", "memory": "2Gi"},
+                    },
+                    "terminationGracePeriodSeconds": 15,
+                },
+            }
+
+    def test_delete_rabbitmq_cluster(self, api, requests_mock):
+        instance = objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="neutron",
+                namespace="default",
+                annotations={
+                    "annotate": "this",
+                },
+                labels={
+                    "foo": "bar",
+                },
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image="rabbitmq:3.8.9",
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.GET,
+                "https://localhost:9443/apis/rabbitmq.com/v1beta1/namespaces/default/rabbitmqclusters/rabbitmq-neutron",
+                json={
+                    "metadata": {
+                        "name": "rabbitmq-neutron",
+                    },
+                },
+            )
+            rsps.add(
+                responses.DELETE,
+                "https://localhost:9443/apis/rabbitmq.com/v1beta1/namespaces/default/rabbitmqclusters/rabbitmq-neutron",
+            )
+
+            instance.delete_rabbitmq_cluster()
+            assert len(rsps.calls) == 2
+
+    def test_delete_missing_rabbitmq_cluster(self, api, requests_mock):
+        instance = objects.OpenstackHelmRabbitmqCluster(
+            api=api,
+            metadata=types.NamespacedObjectMeta(
+                name="neutron",
+                namespace="default",
+                annotations={
+                    "annotate": "this",
+                },
+                labels={
+                    "foo": "bar",
+                },
+            ),
+            spec=types.OpenstackHelmRabbitmqClusterSpec(
+                image="rabbitmq:3.8.9",
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.GET,
+                "https://localhost:9443/apis/rabbitmq.com/v1beta1/namespaces/default/rabbitmqclusters/rabbitmq-neutron",
+                status=404,
+            )
+
+            instance.delete_rabbitmq_cluster()
+            assert len(rsps.calls) == 1
+
+
+class TestOpenstackHelmIngress:
+    @given(st.builds(objects.OpenstackHelmIngress))
+    def test_property(self, instance):
+        assert isinstance(instance, objects.OpenstackHelmIngress)
+        assert isinstance(instance.metadata, types.OpenstackHelmIngressObjectMeta)
+        assert isinstance(instance.spec, types.OpenstackHelmIngressSpec)
+
+    def test_endpont_to_service_mapping_order(self):
+        assert [*objects.OpenstackHelmIngress.ENDPOINT_TO_SERVICE_MAPPING] == sorted(
+            [*objects.OpenstackHelmIngress.ENDPOINT_TO_SERVICE_MAPPING]
+        )
+
+    @pytest.mark.parametrize("name", types.OpenstackHelmIngressObjectMetaName)
+    def test_service(self, name):
+        instance = objects.OpenstackHelmIngress(
+            metadata=types.OpenstackHelmIngressObjectMeta(
+                name=name,
+                namespace="default",
+            ),
+            spec=types.OpenstackHelmIngressSpec(
+                host=f"{name}.example.com",
+            ),
+        )
+
+        assert instance.service is not None
+
+    @pytest.mark.parametrize("name", types.OpenstackHelmIngressObjectMetaName)
+    def test_apply(self, api, requests_mock, name):
+        instance = objects.OpenstackHelmIngress(
+            api=api,
+            metadata=types.OpenstackHelmIngressObjectMeta(
+                name=name,
+                namespace="default",
+                annotations={
+                    "annotate": "this",
+                },
+                labels={
+                    "foo": "bar",
+                },
+            ),
+            spec=types.OpenstackHelmIngressSpec(
+                host=f"{name}.example.com",
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.PATCH,
+                f"https://localhost:9443/apis/atmosphere.vexxhost.com/v1alpha1/namespaces/default/openstackhelmingresses/{name}?fieldManager=atmosphere-operator&force=True",  # noqa
+                json={},
+            )
+
+            instance.apply()
+
+            assert len(rsps.calls) == 1
+            assert json.loads(rsps.calls[0].request.body) == {
+                "apiVersion": "atmosphere.vexxhost.com/v1alpha1",
+                "kind": "OpenstackHelmIngress",
+                "metadata": {
+                    "name": name,
+                    "namespace": "default",
+                    "annotations": {
+                        "annotate": "this",
+                    },
+                    "labels": {
+                        "foo": "bar",
+                    },
+                },
+                "spec": {
+                    "host": f"{name}.example.com",
+                    "clusterIssuer": "atmosphere",
+                    "ingressClassName": "atmosphere",
+                },
+            }
+
+    @pytest.mark.parametrize("name", types.OpenstackHelmIngressObjectMetaName)
+    def test_apply_ingress(self, api, requests_mock, name):
+        instance = objects.OpenstackHelmIngress(
+            api=api,
+            metadata=types.OpenstackHelmIngressObjectMeta(
+                name=name,
+                namespace="default",
+                annotations={
+                    "annotate": "this",
+                },
+                labels={
+                    "foo": "bar",
+                },
+            ),
+            spec=types.OpenstackHelmIngressSpec(
+                host=f"{name}.example.com",
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.PATCH,
+                f"https://localhost:9443/apis/networking.k8s.io/v1/namespaces/default/ingresses/{name}?fieldManager=atmosphere-operator&force=True",  # noqa
+                json={},
+            )
+
+            instance.apply_ingress()
+
+            assert len(rsps.calls) == 1
+            assert json.loads(rsps.calls[0].request.body) == {
+                "apiVersion": "networking.k8s.io/v1",
+                "kind": "Ingress",
+                "metadata": {
+                    "name": name,
+                    "namespace": "default",
+                    "labels": {
+                        "foo": "bar",
+                    },
+                    "annotations": {
+                        "annotate": "this",
+                        "cert-manager.io/cluster-issuer": "atmosphere",
+                    },
+                },
+                "spec": {
+                    "ingressClassName": "atmosphere",
+                    "rules": [
+                        {
+                            "host": f"{name}.example.com",
+                            "http": {
+                                "paths": [
+                                    {
+                                        "path": "/",
+                                        "pathType": "Prefix",
+                                        "backend": {
+                                            "service": {
+                                                "name": instance.service.name,
+                                                "port": {
+                                                    "number": instance.service.port.number,
+                                                },
+                                            },
+                                        },
+                                    },
+                                ],
+                            },
+                        },
+                    ],
+                    "tls": [
+                        {
+                            "secretName": f"{instance.service.name}-certs",
+                            "hosts": [f"{name}.example.com"],
+                        }
+                    ],
+                },
+            }
+
+    @pytest.mark.parametrize("name", types.OpenstackHelmIngressObjectMetaName)
+    def test_delete_ingress(self, api, requests_mock, name):
+        instance = objects.OpenstackHelmIngress(
+            api=api,
+            metadata=types.OpenstackHelmIngressObjectMeta(
+                name=name,
+                namespace="default",
+            ),
+            spec=types.OpenstackHelmIngressSpec(
+                host=f"{name}.example.com",
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.GET,
+                f"https://localhost:9443/apis/networking.k8s.io/v1/namespaces/default/ingresses/{name}",  # noqa
+                json={
+                    "metadata": {
+                        "name": instance.metadata.name,
+                    },
+                },
+            )
+            rsps.add(
+                responses.DELETE,
+                f"https://localhost:9443/apis/networking.k8s.io/v1/namespaces/default/ingresses/{name}",  # noqa
+            )
+
+            instance.delete_ingress()
+            assert len(rsps.calls) == 2
+
+    @pytest.mark.parametrize("name", types.OpenstackHelmIngressObjectMetaName)
+    def test_delete_missing_ingress(self, api, requests_mock, name):
+        instance = objects.OpenstackHelmIngress(
+            api=api,
+            metadata=types.OpenstackHelmIngressObjectMeta(
+                name=name,
+                namespace="default",
+            ),
+            spec=types.OpenstackHelmIngressSpec(
+                host=f"{name}.example.com",
+            ),
+        )
+
+        with requests_mock as rsps:
+            rsps.add(
+                responses.GET,
+                f"https://localhost:9443/apis/networking.k8s.io/v1/namespaces/default/ingresses/{name}",  # noqa
+                status=404,
+            )
+
+            instance.delete_ingress()
+            assert len(rsps.calls) == 1
diff --git a/atmosphere/tests/unit/operator/test_types.py b/atmosphere/tests/unit/operator/test_types.py
new file mode 100644
index 0000000..446b31a
--- /dev/null
+++ b/atmosphere/tests/unit/operator/test_types.py
@@ -0,0 +1,174 @@
+import pydantic
+from hypothesis import given
+from hypothesis import provisional as prov
+from hypothesis import strategies as st
+
+from atmosphere.operator.api import types
+
+
+class TestHostname:
+    def test_modify_schema(self):
+        class FakeObj(pydantic.BaseModel):
+            hostname: types.Hostname
+
+        assert FakeObj.schema().get("properties").get("hostname").get("examples") == [
+            "example.com"
+        ]
+
+    def test_repr(self):
+        assert repr(types.Hostname("example.com")) == "Hostname('example.com')"
+
+
+class TestObjectMeta:
+    @given(st.builds(types.ObjectMeta))
+    def test_property(self, instance):
+        assert isinstance(instance, types.ObjectMeta)
+        assert isinstance(instance.name, str)
+        assert instance.name != ""
+
+
+class TestNamespacedObjectMeta:
+    @given(st.builds(types.NamespacedObjectMeta))
+    def test_property(self, instance):
+        assert isinstance(instance, types.NamespacedObjectMeta)
+        assert isinstance(instance.name, str)
+        assert instance.name != ""
+        assert isinstance(instance.namespace, str)
+        assert instance.namespace != ""
+
+
+class TestKubernetesObject:
+    @given(st.builds(types.KubernetesObject))
+    def test_property(self, instance):
+        assert isinstance(instance, types.KubernetesObject)
+        assert isinstance(instance.metadata, types.ObjectMeta)
+
+
+class TestNamespacedKubernetesObject:
+    @given(st.builds(types.NamespacedKubernetesObject))
+    def test_property(self, instance):
+        assert isinstance(instance, types.NamespacedKubernetesObject)
+        assert isinstance(instance.metadata, types.NamespacedObjectMeta)
+
+
+class TestServiceBackendPort:
+    @given(st.builds(types.ServiceBackendPort))
+    def test_property(self, instance):
+        assert isinstance(instance, types.ServiceBackendPort)
+        assert isinstance(instance.number, int)
+        assert 1 <= instance.number <= 65535
+
+
+class TestIngressServiceBackend:
+    @given(st.builds(types.IngressServiceBackend))
+    def test_property(self, instance):
+        assert isinstance(instance, types.IngressServiceBackend)
+        assert isinstance(instance.name, str)
+        assert instance.name != ""
+        assert isinstance(instance.port, types.ServiceBackendPort)
+
+
+class TestCrossNamespaceObjectReference:
+    @given(st.builds(types.CrossNamespaceObjectReference))
+    def test_property(self, instance):
+        assert isinstance(instance, types.CrossNamespaceObjectReference)
+        assert isinstance(instance.kind, str)
+        assert instance.kind != ""
+        assert isinstance(instance.name, str)
+        assert instance.name != ""
+        assert isinstance(instance.namespace, str) or instance.namespace is None
+        assert instance.namespace != ""
+
+
+class TestHelmRepositorySpec:
+    @given(st.builds(types.HelmRepositorySpec, url=prov.urls()))
+    def test_property(self, instance):
+        assert isinstance(instance, types.HelmRepositorySpec)
+        assert isinstance(instance.url, str)
+        assert instance.url != ""
+        assert isinstance(instance.interval, str)
+        assert instance.interval != ""
+
+
+class TestHelmChartTemplateSpec:
+    @given(st.builds(types.HelmChartTemplateSpec))
+    def test_property(self, instance):
+        assert isinstance(instance, types.HelmChartTemplateSpec)
+        assert isinstance(instance.chart, str)
+        assert instance.chart != ""
+        assert isinstance(instance.version, str) or instance.version is None
+        assert instance.version != ""
+
+
+class TestHelmChartTemplate:
+    @given(st.builds(types.HelmChartTemplate))
+    def test_property(self, instance):
+        assert isinstance(instance, types.HelmChartTemplate)
+        assert isinstance(instance.spec, types.HelmChartTemplateSpec)
+
+
+class TestHelmReleaseActionSpec:
+    @given(st.builds(types.HelmReleaseActionSpec))
+    def test_property(self, instance):
+        assert isinstance(instance, types.HelmReleaseActionSpec)
+        assert isinstance(instance.crds, types.HelmReleaseActionSpecCRDsPolicy)
+        assert isinstance(instance.disable_wait, bool)
+        assert instance.disable_wait in [True, False]
+
+
+class TestHelmReleaseValuesReference:
+    @given(st.builds(types.HelmReleaseValuesReference))
+    def test_property(self, instance):
+        assert isinstance(instance, types.HelmReleaseValuesReference)
+        assert isinstance(instance.kind, str)
+        assert instance.kind != ""
+        assert isinstance(instance.name, str)
+        assert instance.name != ""
+        assert isinstance(instance.values_key, str) or instance.values_key is None
+        assert instance.values_key != ""
+        assert isinstance(instance.target_path, str) or instance.target_path is None
+        assert instance.target_path != ""
+
+
+class TestHelmReleaseSpec:
+    @given(st.builds(types.HelmReleaseSpec))
+    def test_property(self, instance):
+        assert isinstance(instance, types.HelmReleaseSpec)
+        assert isinstance(instance.interval, str)
+        assert instance.interval != ""
+        assert isinstance(instance.chart, types.HelmChartTemplate)
+        assert isinstance(instance.install, types.HelmReleaseActionSpec)
+        assert isinstance(instance.upgrade, types.HelmReleaseActionSpec)
+        assert isinstance(instance.values, dict)
+        assert isinstance(instance.values_from, list)
+
+
+class TestOpenstackHelmRabbitmqClusterSpec:
+    @given(st.builds(types.OpenstackHelmRabbitmqClusterSpec))
+    def test_property(self, instance):
+        assert isinstance(instance, types.OpenstackHelmRabbitmqClusterSpec)
+        assert isinstance(instance.image, str)
+        assert instance.image != ""
+
+
+class TestOpenstackHelmIngressObjectMetaName:
+    def test_name_order(self):
+        assert [*types.OpenstackHelmIngressObjectMetaName] == sorted(
+            [*types.OpenstackHelmIngressObjectMetaName]
+        )
+
+
+class TestOpenstackHelmIngressObjectMeta:
+    @given(st.builds(types.OpenstackHelmIngressObjectMeta))
+    def test_property(self, instance):
+        assert isinstance(instance, types.OpenstackHelmIngressObjectMeta)
+        assert isinstance(instance.name, types.OpenstackHelmIngressObjectMetaName)
+
+
+class TestOpenstackHelmIngressSpec:
+    @given(st.builds(types.OpenstackHelmIngressSpec))
+    def test_property(self, instance):
+        assert isinstance(instance, types.OpenstackHelmIngressSpec)
+        assert instance.clusterIssuer != ""
+        assert instance.ingressClassName != ""
+        assert instance.host != ""
diff --git a/atmosphere/tests/unit/tasks/composite/test_openstack_helm.py b/atmosphere/tests/unit/tasks/composite/test_openstack_helm.py
deleted file mode 100644
index f94d970..0000000
--- a/atmosphere/tests/unit/tasks/composite/test_openstack_helm.py
+++ /dev/null
@@ -1,317 +0,0 @@
-import textwrap
-
-import mergedeep
-import pytest
-
-from atmosphere.models import config
-from atmosphere.tasks import constants
-from atmosphere.tasks.composite import openstack_helm
-
-
-@pytest.mark.parametrize(
-    "cfg_data,expected",
-    [
-        pytest.param(
-            textwrap.dedent(
-                """\
-                """
-            ),
-            [
-                {
-                    "apiVersion": "source.toolkit.fluxcd.io/v1beta2",
-                    "kind": "HelmRepository",
-                    "metadata": {
-                        "name": constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY,
-                        "namespace": constants.NAMESPACE_MONITORING,
-                    },
-                    "spec": {
-                        "interval": "1m",
-                        "url": constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY_URL,
-                    },
-                },
-                {
-                    "apiVersion": "helm.toolkit.fluxcd.io/v2beta1",
-                    "kind": "HelmRelease",
-                    "metadata": {
-                        "name": constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_NAME,
-                        "namespace": constants.NAMESPACE_MONITORING,
-                    },
-                    "spec": {
-                        "chart": {
-                            "spec": {
-                                "chart": constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_NAME,
-                                "sourceRef": {
-                                    "kind": "HelmRepository",
-                                    "name": constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY,
-                                },
-                                "version": constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_VERSION,
-                            }
-                        },
-                        "install": {"crds": "CreateReplace", "disableWait": True},
-                        "interval": "60s",
-                        "upgrade": {"crds": "CreateReplace", "disableWait": True},
-                        "values": constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_VALUES,
-                        "valuesFrom": [],
-                    },
-                },
-            ],
-            id="default",
-        ),
-        pytest.param(
-            textwrap.dedent(
-                """\
-                [kube_prometheus_stack.overrides]
-                foo = "bar"
-                """
-            ),
-            [
-                {
-                    "apiVersion": "source.toolkit.fluxcd.io/v1beta2",
-                    "kind": "HelmRepository",
-                    "metadata": {
-                        "name": constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY,
-                        "namespace": constants.NAMESPACE_MONITORING,
-                    },
-                    "spec": {
-                        "interval": "1m",
-                        "url": constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY_URL,
-                    },
-                },
-                {
-                    "apiVersion": "helm.toolkit.fluxcd.io/v2beta1",
-                    "kind": "HelmRelease",
-                    "metadata": {
-                        "name": constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_NAME,
-                        "namespace": constants.NAMESPACE_MONITORING,
-                    },
-                    "spec": {
-                        "chart": {
-                            "spec": {
-                                "chart": constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_NAME,
-                                "sourceRef": {
-                                    "kind": "HelmRepository",
-                                    "name": constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY,
-                                },
-                                "version": constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_VERSION,
-                            }
-                        },
-                        "install": {"crds": "CreateReplace", "disableWait": True},
-                        "interval": "60s",
-                        "upgrade": {"crds": "CreateReplace", "disableWait": True},
-                        "values": mergedeep.merge(
-                            {},
-                            constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_VALUES,
-                            {"foo": "bar"},
-                        ),
-                        "valuesFrom": [],
-                    },
-                },
-            ],
-            id="overrides",
-        ),
-        pytest.param(
-            textwrap.dedent(
-                """\
-                [opsgenie]
-                enabled = true
-                api_key = "foobar"
-                heartbeat = "prod"
-                """
-            ),
-            [
-                {
-                    "apiVersion": "source.toolkit.fluxcd.io/v1beta2",
-                    "kind": "HelmRepository",
-                    "metadata": {
-                        "name": constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY,
-                        "namespace": constants.NAMESPACE_MONITORING,
-                    },
-                    "spec": {
-                        "interval": "1m",
-                        "url": constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY_URL,
-                    },
-                },
-                {
-                    "apiVersion": "helm.toolkit.fluxcd.io/v2beta1",
-                    "kind": "HelmRelease",
-                    "metadata": {
-                        "name": constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_NAME,
-                        "namespace": constants.NAMESPACE_MONITORING,
-                    },
-                    "spec": {
-                        "chart": {
-                            "spec": {
-                                "chart": constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_NAME,
-                                "sourceRef": {
-                                    "kind": "HelmRepository",
-                                    "name": constants.HELM_REPOSITORY_PROMETHEUS_COMMUINTY,
-                                },
-                                "version": constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_VERSION,
-                            }
-                        },
-                        "install": {"crds": "CreateReplace", "disableWait": True},
-                        "interval": "60s",
-                        "upgrade": {"crds": "CreateReplace", "disableWait": True},
-                        "values": mergedeep.merge(
-                            {},
-                            constants.HELM_RELEASE_KUBE_PROMETHEUS_STACK_VALUES,
-                            {
-                                "alertmanager": {
-                                    "config": openstack_helm.generate_alertmanager_config_for_opsgenie(
-                                        config.OpsGenieConfig(
-                                            {"api_key": "foobar", "heartbeat": "prod"}
-                                        )
-                                    )
-                                }
-                            },
-                        ),
-                        "valuesFrom": [],
-                    },
-                },
-            ],
-            id="opsgenie",
-        ),
-        pytest.param(
-            textwrap.dedent(
-                """\
-                [kube_prometheus_stack]
-                enabled = false
-                """
-            ),
-            [],
-            id="disabled",
-        ),
-    ],
-)
-def test_kube_prometheus_stack_tasks_from_config(pykube, cfg_data, expected):
-    cfg = config.Config.from_string(cfg_data, validate=False)
-    cfg.kube_prometheus_stack.validate()
-
-    assert [
-        t.generate_object().obj
-        for t in openstack_helm.kube_prometheus_stack_tasks_from_config(
-            cfg.kube_prometheus_stack, opsgenie=cfg.opsgenie
-        )
-    ] == expected
-
-
-@pytest.mark.parametrize(
-    "cfg_data,expected",
-    [
-        pytest.param(
-            textwrap.dedent(
-                """\
-                """
-            ),
-            [
-                {
-                    "apiVersion": "source.toolkit.fluxcd.io/v1beta2",
-                    "kind": "HelmRepository",
-                    "metadata": {
-                        "name": constants.HELM_RELEASE_INGRESS_NGINX_NAME,
-                        "namespace": constants.NAMESPACE_OPENSTACK,
-                    },
-                    "spec": {
-                        "interval": "1m",
-                        "url": constants.HELM_REPOSITORY_INGRESS_NGINX_URL,
-                    },
-                },
-                {
-                    "apiVersion": "helm.toolkit.fluxcd.io/v2beta1",
-                    "kind": "HelmRelease",
-                    "metadata": {
-                        "name": constants.HELM_RELEASE_INGRESS_NGINX_NAME,
-                        "namespace": constants.NAMESPACE_OPENSTACK,
-                    },
-                    "spec": {
-                        "chart": {
-                            "spec": {
-                                "chart": constants.HELM_RELEASE_INGRESS_NGINX_NAME,
-                                "sourceRef": {
-                                    "kind": "HelmRepository",
-                                    "name": constants.HELM_REPOSITORY_INGRESS_NGINX,
-                                },
-                                "version": constants.HELM_RELEASE_INGRESS_NGINX_VERSION,
-                            }
-                        },
-                        "install": {"crds": "CreateReplace", "disableWait": True},
-                        "interval": "60s",
-                        "upgrade": {"crds": "CreateReplace", "disableWait": True},
-                        "values": constants.HELM_RELEASE_INGRESS_NGINX_VALUES,
-                        "valuesFrom": [],
-                    },
-                },
-            ],
-            id="default",
-        ),
-        pytest.param(
-            textwrap.dedent(
-                """\
-                [ingress_nginx.overrides]
-                foo = "bar"
-                """
-            ),
-            [
-                {
-                    "apiVersion": "source.toolkit.fluxcd.io/v1beta2",
-                    "kind": "HelmRepository",
-                    "metadata": {
-                        "name": constants.HELM_RELEASE_INGRESS_NGINX_NAME,
-                        "namespace": constants.NAMESPACE_OPENSTACK,
-                    },
-                    "spec": {
-                        "interval": "1m",
-                        "url": constants.HELM_REPOSITORY_INGRESS_NGINX_URL,
-                    },
-                },
-                {
-                    "apiVersion": "helm.toolkit.fluxcd.io/v2beta1",
-                    "kind": "HelmRelease",
-                    "metadata": {
-                        "name": constants.HELM_RELEASE_INGRESS_NGINX_NAME,
-                        "namespace": constants.NAMESPACE_OPENSTACK,
-                    },
-                    "spec": {
-                        "chart": {
-                            "spec": {
-                                "chart": constants.HELM_RELEASE_INGRESS_NGINX_NAME,
-                                "sourceRef": {
-                                    "kind": "HelmRepository",
-                                    "name": constants.HELM_REPOSITORY_INGRESS_NGINX,
-                                },
-                                "version": constants.HELM_RELEASE_INGRESS_NGINX_VERSION,
-                            }
-                        },
-                        "install": {"crds": "CreateReplace", "disableWait": True},
-                        "interval": "60s",
-                        "upgrade": {"crds": "CreateReplace", "disableWait": True},
-                        "values": {
-                            **constants.HELM_RELEASE_INGRESS_NGINX_VALUES,
-                            "foo": "bar",
-                        },
-                        "valuesFrom": [],
-                    },
-                },
-            ],
-            id="overrides",
-        ),
-        pytest.param(
-            textwrap.dedent(
-                """\
-                [ingress_nginx]
-                enabled = false
-                """
-            ),
-            [],
-            id="disabled",
-        ),
-    ],
-)
-def test_ingress_nginx_tasks_from_config(pykube, cfg_data, expected):
-    cfg = config.Config.from_string(cfg_data, validate=False)
-    cfg.ingress_nginx.validate()
-
-    assert [
-        t.generate_object().obj
-        for t in openstack_helm.ingress_nginx_tasks_from_config(cfg.ingress_nginx)
-    ] == expected
diff --git a/molecule/default/destroy.yml b/molecule/default/destroy.yml
index 71c277d..b1c3516 100644
--- a/molecule/default/destroy.yml
+++ b/molecule/default/destroy.yml
@@ -25,6 +25,80 @@
       when:
         - ansible_host is not defined
 
+    - name: Describe all cluster-scoped objects
+      become: true
+      shell: |-
+        set -e
+        export OBJECT_TYPE=node,clusterrole,clusterrolebinding,storageclass,namespace
+        export PARALLELISM_FACTOR=4
+        function list_objects () {
+          printf ${OBJECT_TYPE} | xargs -d ',' -I {} -P1 -n1 bash -c 'echo "$@"' _ {}
+        }
+        export -f list_objects
+        function name_objects () {
+          export OBJECT=$1
+          kubectl get ${OBJECT} -o name | xargs -L1 -I {} -P1 -n1 bash -c 'echo "${OBJECT} ${1#*/}"' _ {}
+        }
+        export -f name_objects
+        function get_objects () {
+          input=($1)
+          export OBJECT=${input[0]}
+          export NAME=${input[1]#*/}
+          echo "${OBJECT}/${NAME}"
+          DIR="{{ logs_dir }}/objects/cluster/${OBJECT}"
+          mkdir -p ${DIR}
+          kubectl get ${OBJECT} ${NAME} -o yaml > "${DIR}/${NAME}.yaml"
+          kubectl describe ${OBJECT} ${NAME} > "${DIR}/${NAME}.txt"
+        }
+        export -f get_objects
+        list_objects | \
+          xargs -r -n 1 -P ${PARALLELISM_FACTOR} -I {} bash -c 'name_objects "$@"' _ {} | \
+          xargs -r -n 1 -P ${PARALLELISM_FACTOR} -I {} bash -c 'get_objects "$@"' _ {}
+      args:
+        executable: /bin/bash
+      ignore_errors: True
+
+    - name: Describe all namespace-scoped objects
+      become: true
+      shell: |-
+        set -e
+        export OBJECT_TYPE=configmaps,cronjobs,daemonsets,deployment,endpoints,ingresses,jobs,networkpolicies,pods,podsecuritypolicies,persistentvolumeclaims,rolebindings,roles,secrets,serviceaccounts,services,statefulsets
+        export PARALLELISM_FACTOR=4
+        function get_namespaces () {
+          kubectl get namespaces -o name | awk -F '/' '{ print $NF }'
+        }
+        function list_namespaced_objects () {
+          export NAMESPACE=$1
+          printf ${OBJECT_TYPE} | xargs -d ',' -I {} -P1 -n1 bash -c 'echo "${NAMESPACE} $@"' _ {}
+        }
+        export -f list_namespaced_objects
+        function name_objects () {
+          input=($1)
+          export NAMESPACE=${input[0]}
+          export OBJECT=${input[1]}
+          kubectl get -n ${NAMESPACE} ${OBJECT} -o name | xargs -L1 -I {} -P1 -n1 bash -c 'echo "${NAMESPACE} ${OBJECT} $@"' _ {}
+        }
+        export -f name_objects
+        function get_objects () {
+          input=($1)
+          export NAMESPACE=${input[0]}
+          export OBJECT=${input[1]}
+          export NAME=${input[2]#*/}
+          echo "${NAMESPACE}/${OBJECT}/${NAME}"
+          DIR="{{ logs_dir }}/objects/namespaced/${NAMESPACE}/${OBJECT}"
+          mkdir -p ${DIR}
+          kubectl get -n ${NAMESPACE} ${OBJECT} ${NAME} -o yaml > "${DIR}/${NAME}.yaml"
+          kubectl describe -n ${NAMESPACE} ${OBJECT} ${NAME} > "${DIR}/${NAME}.txt"
+        }
+        export -f get_objects
+        get_namespaces | \
+          xargs -r -n 1 -P ${PARALLELISM_FACTOR} -I {} bash -c 'list_namespaced_objects "$@"' _ {} | \
+          xargs -r -n 1 -P ${PARALLELISM_FACTOR} -I {} bash -c 'name_objects "$@"' _ {} | \
+          xargs -r -n 1 -P ${PARALLELISM_FACTOR} -I {} bash -c 'get_objects "$@"' _ {}
+      args:
+        executable: /bin/bash
+      ignore_errors: True
+
     - name: Retrieve all container logs, current and previous (if they exist)
       become: true
       shell: |-
diff --git a/poetry.lock b/poetry.lock
index 77caa2b..4d982f6 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -82,14 +82,6 @@
 python-dateutil = ">=2.7.0"
 
 [[package]]
-name = "asn1crypto"
-version = "1.5.1"
-description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP"
-category = "main"
-optional = true
-python-versions = "*"
-
-[[package]]
 name = "async-timeout"
 version = "4.0.2"
 description = "Timeout context manager for asyncio programs"
@@ -145,18 +137,6 @@
 python-versions = "~=3.7"
 
 [[package]]
-name = "certbuilder"
-version = "0.14.2"
-description = "Creates and signs X.509 certificates"
-category = "main"
-optional = true
-python-versions = "*"
-
-[package.dependencies]
-asn1crypto = ">=0.18.1"
-oscrypto = ">=0.16.1"
-
-[[package]]
 name = "certifi"
 version = "2022.12.7"
 description = "Python package for providing Mozilla's CA Bundle."
@@ -271,7 +251,7 @@
 
 [[package]]
 name = "cryptography"
-version = "38.0.4"
+version = "39.0.0"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 category = "main"
 optional = false
@@ -281,9 +261,9 @@
 cffi = ">=1.12"
 
 [package.extras]
-docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
+docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"]
 docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
-pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
+pep8test = ["black", "ruff"]
 sdist = ["setuptools-rust (>=0.11.4)"]
 ssh = ["bcrypt (>=3.1.5)"]
 test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
@@ -463,6 +443,35 @@
 test = ["faulthandler", "objgraph", "psutil"]
 
 [[package]]
+name = "hypothesis"
+version = "6.61.0"
+description = "A library for property-based testing"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+attrs = ">=19.2.0"
+exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+sortedcontainers = ">=2.1.0,<3.0.0"
+
+[package.extras]
+all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "django (>=3.2)", "dpcontracts (>=0.4)", "importlib-metadata (>=3.6)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.9.0)", "pandas (>=1.0)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2022.7)"]
+cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"]
+codemods = ["libcst (>=0.3.16)"]
+dateutil = ["python-dateutil (>=1.4)"]
+django = ["django (>=3.2)"]
+dpcontracts = ["dpcontracts (>=0.4)"]
+ghostwriter = ["black (>=19.10b0)"]
+lark = ["lark (>=0.10.1)"]
+numpy = ["numpy (>=1.9.0)"]
+pandas = ["pandas (>=1.0)"]
+pytest = ["pytest (>=4.6)"]
+pytz = ["pytz (>=2014.1)"]
+redis = ["redis (>=3.0.0)"]
+zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2022.7)"]
+
+[[package]]
 name = "idna"
 version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
@@ -848,17 +857,6 @@
 pbr = ">=2.0.0,<2.1.0 || >2.1.0"
 
 [[package]]
-name = "oscrypto"
-version = "1.3.0"
-description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD."
-category = "main"
-optional = true
-python-versions = "*"
-
-[package.dependencies]
-asn1crypto = ">=1.5.1"
-
-[[package]]
 name = "oslo.concurrency"
 version = "5.0.1"
 description = "Oslo Concurrency library"
@@ -1033,14 +1031,14 @@
 
 [[package]]
 name = "pydantic"
-version = "1.10.2"
+version = "1.10.4"
 description = "Data validation and settings management using python type hints"
-category = "dev"
+category = "main"
 optional = false
 python-versions = ">=3.7"
 
 [package.dependencies]
-typing-extensions = ">=4.1.0"
+typing-extensions = ">=4.2.0"
 
 [package.extras]
 dotenv = ["python-dotenv (>=0.10.4)"]
@@ -1067,7 +1065,7 @@
 
 [[package]]
 name = "Pygments"
-version = "2.13.0"
+version = "2.14.0"
 description = "Pygments is a syntax highlighting package written in Python."
 category = "dev"
 optional = false
@@ -1125,7 +1123,7 @@
 
 [[package]]
 name = "pyrsistent"
-version = "0.19.2"
+version = "0.19.3"
 description = "Persistent/Functional/Immutable data structures"
 category = "main"
 optional = false
@@ -1315,6 +1313,23 @@
 test = ["commentjson", "packaging", "pytest"]
 
 [[package]]
+name = "responses"
+version = "0.22.0"
+description = "A utility library for mocking out the `requests` Python library."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+requests = ">=2.22.0,<3.0"
+toml = "*"
+types-toml = "*"
+urllib3 = ">=1.25.10"
+
+[package.extras]
+tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "types-requests"]
+
+[[package]]
 name = "rfc3986"
 version = "2.0.0"
 description = "Validating URI References per RFC 3986"
@@ -1327,11 +1342,11 @@
 
 [[package]]
 name = "rich"
-version = "12.6.0"
+version = "13.0.0"
 description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
 category = "dev"
 optional = false
-python-versions = ">=3.6.3,<4.0.0"
+python-versions = ">=3.7.0"
 
 [package.dependencies]
 commonmark = ">=0.9.0,<0.10.0"
@@ -1357,6 +1372,14 @@
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
+name = "sortedcontainers"
+version = "2.4.0"
+description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
 name = "stevedore"
 version = "4.1.1"
 description = "Manage dynamic plugins for Python applications"
@@ -1442,6 +1465,14 @@
 python-versions = "*"
 
 [[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
 name = "tomli"
 version = "2.0.1"
 description = "A lil' TOML parser"
@@ -1492,6 +1523,14 @@
 test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"]
 
 [[package]]
+name = "types-toml"
+version = "0.10.8.1"
+description = "Typing stubs for toml"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
 name = "typing-extensions"
 version = "4.4.0"
 description = "Backported and Experimental Type Hints for Python 3.7+"
@@ -1526,8 +1565,22 @@
 test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"]
 
 [[package]]
+name = "validators"
+version = "0.20.0"
+description = "Python Data Validation for Humans™."
+category = "main"
+optional = true
+python-versions = ">=3.4"
+
+[package.dependencies]
+decorator = ">=3.4.0"
+
+[package.extras]
+test = ["flake8 (>=2.4.0)", "isort (>=4.2.2)", "pytest (>=2.2.3)"]
+
+[[package]]
 name = "watchdog"
-version = "2.2.0"
+version = "2.2.1"
 description = "Filesystem events monitoring"
 category = "dev"
 optional = false
@@ -1565,12 +1618,12 @@
 multidict = ">=4.0"
 
 [extras]
-operator = ["schematics", "pykube-ng", "structlog", "mergedeep", "taskflow", "eventlet", "tomli", "jsonnet", "kopf", "openstacksdk", "certbuilder"]
+operator = ["schematics", "pydantic", "pykube-ng", "structlog", "mergedeep", "taskflow", "eventlet", "tomli", "jsonnet", "kopf", "openstacksdk", "validators"]
 
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.10"
-content-hash = "566e542d5ec662462d50aa2cf5317f0ee1cfe447d87330a756aac85c18069505"
+content-hash = "505316358da7d5be5cb22c3f8e87eb1fd96183738c8baad35d615eda1be7594c"
 
 [metadata.files]
 aiohttp = [
@@ -1682,10 +1735,6 @@
     {file = "arrow-1.2.3-py3-none-any.whl", hash = "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"},
     {file = "arrow-1.2.3.tar.gz", hash = "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1"},
 ]
-asn1crypto = [
-    {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"},
-    {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"},
-]
 async-timeout = [
     {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
     {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
@@ -1706,10 +1755,6 @@
     {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"},
     {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"},
 ]
-certbuilder = [
-    {file = "certbuilder-0.14.2-py2.py3-none-any.whl", hash = "sha256:feed83d15b20c149debc1b73a7eb74b8e8041c78d3a8deb989e21905b4394a30"},
-    {file = "certbuilder-0.14.2.tar.gz", hash = "sha256:56a8aee8ed31a211678647797dfdcdc85ec25d5d1bb1515e44ebae45cce363f9"},
-]
 certifi = [
     {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
     {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
@@ -1862,32 +1907,29 @@
     {file = "coverage-7.0.1.tar.gz", hash = "sha256:a4a574a19eeb67575a5328a5760bbbb737faa685616586a9f9da4281f940109c"},
 ]
 cryptography = [
-    {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70"},
-    {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb"},
-    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d"},
-    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1"},
-    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8"},
-    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db"},
-    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b"},
-    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c"},
-    {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00"},
-    {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0"},
-    {file = "cryptography-38.0.4-cp36-abi3-win32.whl", hash = "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744"},
-    {file = "cryptography-38.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d"},
-    {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353"},
-    {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7"},
-    {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd"},
-    {file = "cryptography-38.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2"},
-    {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b"},
-    {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6"},
-    {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876"},
-    {file = "cryptography-38.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b"},
-    {file = "cryptography-38.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285"},
-    {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b"},
-    {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083"},
-    {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee"},
-    {file = "cryptography-38.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9"},
-    {file = "cryptography-38.0.4.tar.gz", hash = "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290"},
+    {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"},
+    {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72"},
+    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9"},
+    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f"},
+    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b"},
+    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c"},
+    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96"},
+    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717"},
+    {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df"},
+    {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1"},
+    {file = "cryptography-39.0.0-cp36-abi3-win32.whl", hash = "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de"},
+    {file = "cryptography-39.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e"},
+    {file = "cryptography-39.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb"},
+    {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f"},
+    {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"},
+    {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190"},
+    {file = "cryptography-39.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773"},
+    {file = "cryptography-39.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856"},
+    {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f"},
+    {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce"},
+    {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8"},
+    {file = "cryptography-39.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39"},
+    {file = "cryptography-39.0.0.tar.gz", hash = "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf"},
 ]
 debtcollector = [
     {file = "debtcollector-2.5.0-py3-none-any.whl", hash = "sha256:1393a527d2c72f143ffa6a629e9c33face6642634eece475b48cab7b04ba61f3"},
@@ -2055,7 +2097,6 @@
     {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce"},
     {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000"},
     {file = "greenlet-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2"},
-    {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9"},
     {file = "greenlet-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1"},
     {file = "greenlet-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1"},
     {file = "greenlet-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23"},
@@ -2064,7 +2105,6 @@
     {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e"},
     {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48"},
     {file = "greenlet-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764"},
-    {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0"},
     {file = "greenlet-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9"},
     {file = "greenlet-2.0.1-cp38-cp38-win32.whl", hash = "sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608"},
     {file = "greenlet-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6"},
@@ -2073,12 +2113,15 @@
     {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5"},
     {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7"},
     {file = "greenlet-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d"},
-    {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726"},
     {file = "greenlet-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e"},
     {file = "greenlet-2.0.1-cp39-cp39-win32.whl", hash = "sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a"},
     {file = "greenlet-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6"},
     {file = "greenlet-2.0.1.tar.gz", hash = "sha256:42e602564460da0e8ee67cb6d7236363ee5e131aa15943b6670e44e5c2ed0f67"},
 ]
+hypothesis = [
+    {file = "hypothesis-6.61.0-py3-none-any.whl", hash = "sha256:7bb22d22e35db99d5724bbf5bdc686b46add94a0f228bf1be249c47ec46b9c7f"},
+    {file = "hypothesis-6.61.0.tar.gz", hash = "sha256:fbf7da30aea839d88898f74bcc027f0f997060498a8a7605880688c8a2166215"},
+]
 idna = [
     {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
     {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
@@ -2386,10 +2429,6 @@
     {file = "os-service-types-1.7.0.tar.gz", hash = "sha256:31800299a82239363995b91f1ebf9106ac7758542a1e4ef6dc737a5932878c6c"},
     {file = "os_service_types-1.7.0-py2.py3-none-any.whl", hash = "sha256:0505c72205690910077fb72b88f2a1f07533c8d39f2fe75b29583481764965d6"},
 ]
-oscrypto = [
-    {file = "oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085"},
-    {file = "oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4"},
-]
 "oslo.concurrency" = [
     {file = "oslo.concurrency-5.0.1-py3-none-any.whl", hash = "sha256:7a6b6a1aa28e65a7cbfc9e04ecc9cdcaf7f7a2ad6cdf37014b3694b627280f5f"},
     {file = "oslo.concurrency-5.0.1.tar.gz", hash = "sha256:0dfbf36095f4637ffbb65e5c241f4c25851abd3b728dddad9f07396302a72544"},
@@ -2443,42 +2482,42 @@
     {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
 ]
 pydantic = [
-    {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"},
-    {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"},
-    {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"},
-    {file = "pydantic-1.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559"},
-    {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236"},
-    {file = "pydantic-1.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c"},
-    {file = "pydantic-1.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644"},
-    {file = "pydantic-1.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f"},
-    {file = "pydantic-1.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a"},
-    {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525"},
-    {file = "pydantic-1.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283"},
-    {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42"},
-    {file = "pydantic-1.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52"},
-    {file = "pydantic-1.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c"},
-    {file = "pydantic-1.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5"},
-    {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c"},
-    {file = "pydantic-1.10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254"},
-    {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5"},
-    {file = "pydantic-1.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d"},
-    {file = "pydantic-1.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2"},
-    {file = "pydantic-1.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13"},
-    {file = "pydantic-1.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116"},
-    {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624"},
-    {file = "pydantic-1.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1"},
-    {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9"},
-    {file = "pydantic-1.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965"},
-    {file = "pydantic-1.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e"},
-    {file = "pydantic-1.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488"},
-    {file = "pydantic-1.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41"},
-    {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b"},
-    {file = "pydantic-1.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe"},
-    {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d"},
-    {file = "pydantic-1.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda"},
-    {file = "pydantic-1.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6"},
-    {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
-    {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
+    {file = "pydantic-1.10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854"},
+    {file = "pydantic-1.10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817"},
+    {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c"},
+    {file = "pydantic-1.10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e"},
+    {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85"},
+    {file = "pydantic-1.10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6"},
+    {file = "pydantic-1.10.4-cp310-cp310-win_amd64.whl", hash = "sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d"},
+    {file = "pydantic-1.10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53"},
+    {file = "pydantic-1.10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3"},
+    {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a"},
+    {file = "pydantic-1.10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d"},
+    {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72"},
+    {file = "pydantic-1.10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857"},
+    {file = "pydantic-1.10.4-cp311-cp311-win_amd64.whl", hash = "sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3"},
+    {file = "pydantic-1.10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00"},
+    {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978"},
+    {file = "pydantic-1.10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e"},
+    {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa"},
+    {file = "pydantic-1.10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8"},
+    {file = "pydantic-1.10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903"},
+    {file = "pydantic-1.10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423"},
+    {file = "pydantic-1.10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f"},
+    {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06"},
+    {file = "pydantic-1.10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15"},
+    {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c"},
+    {file = "pydantic-1.10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416"},
+    {file = "pydantic-1.10.4-cp38-cp38-win_amd64.whl", hash = "sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6"},
+    {file = "pydantic-1.10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d"},
+    {file = "pydantic-1.10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f"},
+    {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024"},
+    {file = "pydantic-1.10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c"},
+    {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28"},
+    {file = "pydantic-1.10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f"},
+    {file = "pydantic-1.10.4-cp39-cp39-win_amd64.whl", hash = "sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4"},
+    {file = "pydantic-1.10.4-py3-none-any.whl", hash = "sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774"},
+    {file = "pydantic-1.10.4.tar.gz", hash = "sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648"},
 ]
 pydot = [
     {file = "pydot-1.4.2-py2.py3-none-any.whl", hash = "sha256:66c98190c65b8d2e2382a441b4c0edfdb4f4c025ef9cb9874de478fb0793a451"},
@@ -2489,8 +2528,8 @@
     {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
 ]
 Pygments = [
-    {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"},
-    {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"},
+    {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"},
+    {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"},
 ]
 pyinotify = [
     {file = "pyinotify-0.9.6.tar.gz", hash = "sha256:9c998a5d7606ca835065cdabc013ae6c66eb9ea76a00a1e3bc6e0cfe2b4f71f4"},
@@ -2508,28 +2547,33 @@
     {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
 ]
 pyrsistent = [
-    {file = "pyrsistent-0.19.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d6982b5a0237e1b7d876b60265564648a69b14017f3b5f908c5be2de3f9abb7a"},
-    {file = "pyrsistent-0.19.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:187d5730b0507d9285a96fca9716310d572e5464cadd19f22b63a6976254d77a"},
-    {file = "pyrsistent-0.19.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:055ab45d5911d7cae397dc418808d8802fb95262751872c841c170b0dbf51eed"},
-    {file = "pyrsistent-0.19.2-cp310-cp310-win32.whl", hash = "sha256:456cb30ca8bff00596519f2c53e42c245c09e1a4543945703acd4312949bfd41"},
-    {file = "pyrsistent-0.19.2-cp310-cp310-win_amd64.whl", hash = "sha256:b39725209e06759217d1ac5fcdb510e98670af9e37223985f330b611f62e7425"},
-    {file = "pyrsistent-0.19.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aede922a488861de0ad00c7630a6e2d57e8023e4be72d9d7147a9fcd2d30712"},
-    {file = "pyrsistent-0.19.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879b4c2f4d41585c42df4d7654ddffff1239dc4065bc88b745f0341828b83e78"},
-    {file = "pyrsistent-0.19.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c43bec251bbd10e3cb58ced80609c5c1eb238da9ca78b964aea410fb820d00d6"},
-    {file = "pyrsistent-0.19.2-cp37-cp37m-win32.whl", hash = "sha256:d690b18ac4b3e3cab73b0b7aa7dbe65978a172ff94970ff98d82f2031f8971c2"},
-    {file = "pyrsistent-0.19.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3ba4134a3ff0fc7ad225b6b457d1309f4698108fb6b35532d015dca8f5abed73"},
-    {file = "pyrsistent-0.19.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a178209e2df710e3f142cbd05313ba0c5ebed0a55d78d9945ac7a4e09d923308"},
-    {file = "pyrsistent-0.19.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e371b844cec09d8dc424d940e54bba8f67a03ebea20ff7b7b0d56f526c71d584"},
-    {file = "pyrsistent-0.19.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111156137b2e71f3a9936baf27cb322e8024dac3dc54ec7fb9f0bcf3249e68bb"},
-    {file = "pyrsistent-0.19.2-cp38-cp38-win32.whl", hash = "sha256:e5d8f84d81e3729c3b506657dddfe46e8ba9c330bf1858ee33108f8bb2adb38a"},
-    {file = "pyrsistent-0.19.2-cp38-cp38-win_amd64.whl", hash = "sha256:9cd3e9978d12b5d99cbdc727a3022da0430ad007dacf33d0bf554b96427f33ab"},
-    {file = "pyrsistent-0.19.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f1258f4e6c42ad0b20f9cfcc3ada5bd6b83374516cd01c0960e3cb75fdca6770"},
-    {file = "pyrsistent-0.19.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21455e2b16000440e896ab99e8304617151981ed40c29e9507ef1c2e4314ee95"},
-    {file = "pyrsistent-0.19.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd880614c6237243ff53a0539f1cb26987a6dc8ac6e66e0c5a40617296a045e"},
-    {file = "pyrsistent-0.19.2-cp39-cp39-win32.whl", hash = "sha256:71d332b0320642b3261e9fee47ab9e65872c2bd90260e5d225dabeed93cbd42b"},
-    {file = "pyrsistent-0.19.2-cp39-cp39-win_amd64.whl", hash = "sha256:dec3eac7549869365fe263831f576c8457f6c833937c68542d08fde73457d291"},
-    {file = "pyrsistent-0.19.2-py3-none-any.whl", hash = "sha256:ea6b79a02a28550c98b6ca9c35b9f492beaa54d7c5c9e9949555893c8a9234d0"},
-    {file = "pyrsistent-0.19.2.tar.gz", hash = "sha256:bfa0351be89c9fcbcb8c9879b826f4353be10f58f8a677efab0c017bf7137ec2"},
+    {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"},
+    {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"},
+    {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"},
+    {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"},
+    {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"},
+    {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"},
+    {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"},
+    {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"},
+    {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"},
+    {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"},
+    {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"},
+    {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"},
+    {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"},
+    {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"},
+    {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"},
+    {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"},
+    {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"},
+    {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"},
+    {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"},
+    {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"},
+    {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"},
+    {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"},
+    {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"},
+    {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"},
+    {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"},
+    {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"},
+    {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"},
 ]
 pytest = [
     {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
@@ -2715,13 +2759,17 @@
     {file = "resolvelib-0.8.1-py2.py3-none-any.whl", hash = "sha256:d9b7907f055c3b3a2cfc56c914ffd940122915826ff5fb5b1de0c99778f4de98"},
     {file = "resolvelib-0.8.1.tar.gz", hash = "sha256:c6ea56732e9fb6fca1b2acc2ccc68a0b6b8c566d8f3e78e0443310ede61dbd37"},
 ]
+responses = [
+    {file = "responses-0.22.0-py3-none-any.whl", hash = "sha256:dcf294d204d14c436fddcc74caefdbc5764795a40ff4e6a7740ed8ddbf3294be"},
+    {file = "responses-0.22.0.tar.gz", hash = "sha256:396acb2a13d25297789a5866b4881cf4e46ffd49cc26c43ab1117f40b973102e"},
+]
 rfc3986 = [
     {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"},
     {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"},
 ]
 rich = [
-    {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"},
-    {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"},
+    {file = "rich-13.0.0-py3-none-any.whl", hash = "sha256:12b1d77ee7edf251b741531323f0d990f5f570a4e7c054d0bfb59fb7981ad977"},
+    {file = "rich-13.0.0.tar.gz", hash = "sha256:3aa9eba7219b8c575c6494446a59f702552efe1aa261e7eeb95548fa586e1950"},
 ]
 schematics = [
     {file = "schematics-2.1.1-py2.py3-none-any.whl", hash = "sha256:be2d451bfb86789975e5ec0864aec569b63cea9010f0d24cbbd992a4e564c647"},
@@ -2731,6 +2779,10 @@
     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
     {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
 ]
+sortedcontainers = [
+    {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
+    {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
+]
 stevedore = [
     {file = "stevedore-4.1.1-py3-none-any.whl", hash = "sha256:aa6436565c069b2946fe4ebff07f5041e0c8bf18c7376dd29edf80cf7d524e4e"},
     {file = "stevedore-4.1.1.tar.gz", hash = "sha256:7f8aeb6e3f90f96832c301bff21a7eb5eefbe894c88c506483d355565d88cc1a"},
@@ -2755,6 +2807,10 @@
     {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
     {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
 ]
+toml = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
 tomli = [
     {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
     {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
@@ -2771,6 +2827,10 @@
     {file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"},
     {file = "typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"},
 ]
+types-toml = [
+    {file = "types-toml-0.10.8.1.tar.gz", hash = "sha256:171bdb3163d79a520560f24ba916a9fc9bff81659c5448a9fea89240923722be"},
+    {file = "types_toml-0.10.8.1-py3-none-any.whl", hash = "sha256:b7b5c4977f96ab7b5ac06d8a6590d17c0bf252a96efc03b109c2711fb3e0eafd"},
+]
 typing-extensions = [
     {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
     {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
@@ -2811,35 +2871,38 @@
     {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded"},
     {file = "uvloop-0.17.0.tar.gz", hash = "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1"},
 ]
+validators = [
+    {file = "validators-0.20.0.tar.gz", hash = "sha256:24148ce4e64100a2d5e267233e23e7afeb55316b47d30faae7eb6e7292bc226a"},
+]
 watchdog = [
-    {file = "watchdog-2.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ed91c3ccfc23398e7aa9715abf679d5c163394b8cad994f34f156d57a7c163dc"},
-    {file = "watchdog-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:76a2743402b794629a955d96ea2e240bd0e903aa26e02e93cd2d57b33900962b"},
-    {file = "watchdog-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:920a4bda7daa47545c3201a3292e99300ba81ca26b7569575bd086c865889090"},
-    {file = "watchdog-2.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ceaa9268d81205876bedb1069f9feab3eccddd4b90d9a45d06a0df592a04cae9"},
-    {file = "watchdog-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1893d425ef4fb4f129ee8ef72226836619c2950dd0559bba022b0818c63a7b60"},
-    {file = "watchdog-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e99c1713e4436d2563f5828c8910e5ff25abd6ce999e75f15c15d81d41980b6"},
-    {file = "watchdog-2.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a5bd9e8656d07cae89ac464ee4bcb6f1b9cecbedc3bf1334683bed3d5afd39ba"},
-    {file = "watchdog-2.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a048865c828389cb06c0bebf8a883cec3ae58ad3e366bcc38c61d8455a3138f"},
-    {file = "watchdog-2.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e722755d995035dd32177a9c633d158f2ec604f2a358b545bba5bed53ab25bca"},
-    {file = "watchdog-2.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:af4b5c7ba60206759a1d99811b5938ca666ea9562a1052b410637bb96ff97512"},
-    {file = "watchdog-2.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:619d63fa5be69f89ff3a93e165e602c08ed8da402ca42b99cd59a8ec115673e1"},
-    {file = "watchdog-2.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f2b0665c57358ce9786f06f5475bc083fea9d81ecc0efa4733fd0c320940a37"},
-    {file = "watchdog-2.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:441024df19253bb108d3a8a5de7a186003d68564084576fecf7333a441271ef7"},
-    {file = "watchdog-2.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a410dd4d0adcc86b4c71d1317ba2ea2c92babaf5b83321e4bde2514525544d5"},
-    {file = "watchdog-2.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28704c71afdb79c3f215c90231e41c52b056ea880b6be6cee035c6149d658ed1"},
-    {file = "watchdog-2.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ac0bd7c206bb6df78ef9e8ad27cc1346f2b41b1fef610395607319cdab89bc1"},
-    {file = "watchdog-2.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:27e49268735b3c27310883012ab3bd86ea0a96dcab90fe3feb682472e30c90f3"},
-    {file = "watchdog-2.2.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:2af1a29fd14fc0a87fb6ed762d3e1ae5694dcde22372eebba50e9e5be47af03c"},
-    {file = "watchdog-2.2.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:c7bd98813d34bfa9b464cf8122e7d4bec0a5a427399094d2c17dd5f70d59bc61"},
-    {file = "watchdog-2.2.0-py3-none-manylinux2014_i686.whl", hash = "sha256:56fb3f40fc3deecf6e518303c7533f5e2a722e377b12507f6de891583f1b48aa"},
-    {file = "watchdog-2.2.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:74535e955359d79d126885e642d3683616e6d9ab3aae0e7dcccd043bd5a3ff4f"},
-    {file = "watchdog-2.2.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cf05e6ff677b9655c6e9511d02e9cc55e730c4e430b7a54af9c28912294605a4"},
-    {file = "watchdog-2.2.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:d6ae890798a3560688b441ef086bb66e87af6b400a92749a18b856a134fc0318"},
-    {file = "watchdog-2.2.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5aed2a700a18c194c39c266900d41f3db0c1ebe6b8a0834b9995c835d2ca66e"},
-    {file = "watchdog-2.2.0-py3-none-win32.whl", hash = "sha256:d0fb5f2b513556c2abb578c1066f5f467d729f2eb689bc2db0739daf81c6bb7e"},
-    {file = "watchdog-2.2.0-py3-none-win_amd64.whl", hash = "sha256:1f8eca9d294a4f194ce9df0d97d19b5598f310950d3ac3dd6e8d25ae456d4c8a"},
-    {file = "watchdog-2.2.0-py3-none-win_ia64.whl", hash = "sha256:ad0150536469fa4b693531e497ffe220d5b6cd76ad2eda474a5e641ee204bbb6"},
-    {file = "watchdog-2.2.0.tar.gz", hash = "sha256:83cf8bc60d9c613b66a4c018051873d6273d9e45d040eed06d6a96241bd8ec01"},
+    {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"},
+    {file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5100eae58133355d3ca6c1083a33b81355c4f452afa474c2633bd2fbbba398b3"},
+    {file = "watchdog-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e618a4863726bc7a3c64f95c218437f3349fb9d909eb9ea3a1ed3b567417c661"},
+    {file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:102a60093090fc3ff76c983367b19849b7cc24ec414a43c0333680106e62aae1"},
+    {file = "watchdog-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:748ca797ff59962e83cc8e4b233f87113f3cf247c23e6be58b8a2885c7337aa3"},
+    {file = "watchdog-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ccd8d84b9490a82b51b230740468116b8205822ea5fdc700a553d92661253a3"},
+    {file = "watchdog-2.2.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e01d699cd260d59b84da6bda019dce0a3353e3fcc774408ae767fe88ee096b7"},
+    {file = "watchdog-2.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8586d98c494690482c963ffb24c49bf9c8c2fe0589cec4dc2f753b78d1ec301d"},
+    {file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:adaf2ece15f3afa33a6b45f76b333a7da9256e1360003032524d61bdb4c422ae"},
+    {file = "watchdog-2.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83a7cead445008e880dbde833cb9e5cc7b9a0958edb697a96b936621975f15b9"},
+    {file = "watchdog-2.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8ac23ff2c2df4471a61af6490f847633024e5aa120567e08d07af5718c9d092"},
+    {file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d0f29fd9f3f149a5277929de33b4f121a04cf84bb494634707cfa8ea8ae106a8"},
+    {file = "watchdog-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:967636031fa4c4955f0f3f22da3c5c418aa65d50908d31b73b3b3ffd66d60640"},
+    {file = "watchdog-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96cbeb494e6cbe3ae6aacc430e678ce4b4dd3ae5125035f72b6eb4e5e9eb4f4e"},
+    {file = "watchdog-2.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61fdb8e9c57baf625e27e1420e7ca17f7d2023929cd0065eb79c83da1dfbeacd"},
+    {file = "watchdog-2.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4cb5ecc332112017fbdb19ede78d92e29a8165c46b68a0b8ccbd0a154f196d5e"},
+    {file = "watchdog-2.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a480d122740debf0afac4ddd583c6c0bb519c24f817b42ed6f850e2f6f9d64a8"},
+    {file = "watchdog-2.2.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:978a1aed55de0b807913b7482d09943b23a2d634040b112bdf31811a422f6344"},
+    {file = "watchdog-2.2.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:8c28c23972ec9c524967895ccb1954bc6f6d4a557d36e681a36e84368660c4ce"},
+    {file = "watchdog-2.2.1-py3-none-manylinux2014_i686.whl", hash = "sha256:c27d8c1535fd4474e40a4b5e01f4ba6720bac58e6751c667895cbc5c8a7af33c"},
+    {file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d6b87477752bd86ac5392ecb9eeed92b416898c30bd40c7e2dd03c3146105646"},
+    {file = "watchdog-2.2.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:cece1aa596027ff56369f0b50a9de209920e1df9ac6d02c7f9e5d8162eb4f02b"},
+    {file = "watchdog-2.2.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:8b5cde14e5c72b2df5d074774bdff69e9b55da77e102a91f36ef26ca35f9819c"},
+    {file = "watchdog-2.2.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e038be858425c4f621900b8ff1a3a1330d9edcfeaa1c0468aeb7e330fb87693e"},
+    {file = "watchdog-2.2.1-py3-none-win32.whl", hash = "sha256:bc43c1b24d2f86b6e1cc15f68635a959388219426109233e606517ff7d0a5a73"},
+    {file = "watchdog-2.2.1-py3-none-win_amd64.whl", hash = "sha256:17f1708f7410af92ddf591e94ae71a27a13974559e72f7e9fde3ec174b26ba2e"},
+    {file = "watchdog-2.2.1-py3-none-win_ia64.whl", hash = "sha256:195ab1d9d611a4c1e5311cbf42273bc541e18ea8c32712f2fb703cfc6ff006f9"},
+    {file = "watchdog-2.2.1.tar.gz", hash = "sha256:cdcc23c9528601a8a293eb4369cbd14f6b4f34f07ae8769421252e9c22718b6f"},
 ]
 wcwidth = [
     {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
diff --git a/pyproject.toml b/pyproject.toml
index dbec531..71fa6b6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,6 +12,7 @@
 [tool.poetry.dependencies]
 python = "^3.10"
 click = "^8.1.3"
+pydantic = { version = "^1.10.4", optional = true }
 schematics = { version = "^2.1.1", optional = true }
 pykube-ng = { version = "^22.7.0", optional = true }
 structlog = { version = "^22.1.0", optional = true }
@@ -22,7 +23,7 @@
 jsonnet = { version = "^0.18.0", optional = true }
 kopf = { version = "^1.36.0", optional = true, extras = ["uvloop"] }
 openstacksdk = { version = "<0.99.0", optional = true }
-certbuilder = { version = "^0.14.2", optional = true }
+validators = { version = "^0.20.0", optional = true }
 "oslo.log" = "^5.0.2"
 "oslo.config" = "^9.0.0"
 "oslo.concurrency" = "^5.0.1"
@@ -31,10 +32,9 @@
 [tool.poetry.extras]
 operator = [
   "schematics",
+  "pydantic",
   "pykube-ng",
   "structlog",
-  "rich",
-  "better-exceptions",
   "mergedeep",
   "taskflow",
   "eventlet",
@@ -42,23 +42,25 @@
   "jsonnet",
   "kopf",
   "openstacksdk",
-  "certbuilder",
+  "validators",
 ]
 
 [tool.poetry.group.dev.dependencies]
 ansible-core = "^2.13.4"
 flake8 = "^5.0.4"
 flake8-isort = "^4.2.0"
+hypothesis = "6.61.0"
 Jinja2 = "^3.1.2"
-molecule = "^4.0.1"
+jinja2-ansible-filters = "^1.3.2"
 jmespath = "^1.0.1"
+molecule = "^4.0.1"
 pytest = "^7.1.3"
 pytest-cov = "^3.0.0"
 pytest-kind = "^22.9.0"
 pytest-mock = "^3.8.2"
 python-on-whales = "^0.52.0"
 tomli-w = "^1.0.0"
-jinja2-ansible-filters = "^1.3.2"
+responses = "^0.22.0"
 
 [tool.poetry.group.docs.dependencies]
 mkdocs-material = "^8.5.7"
diff --git a/roles/atmosphere/templates/crds.yml b/roles/atmosphere/templates/crds.yml
index 2a31263..db56695 100644
--- a/roles/atmosphere/templates/crds.yml
+++ b/roles/atmosphere/templates/crds.yml
@@ -2,6 +2,58 @@
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
+  name: openstackhelmrabbitmqclusters.atmosphere.vexxhost.com
+spec:
+  scope: Namespaced
+  group: atmosphere.vexxhost.com
+  names:
+    kind: OpenstackHelmRabbitmqCluster
+    plural: openstackhelmrabbitmqclusters
+    singular: openstackhelmrabbitmqcluster
+  versions:
+    - name: v1alpha1
+      served: true
+      storage: true
+      schema:
+        openAPIV3Schema:
+          type: object
+          properties:
+            spec:
+              type: object
+              x-kubernetes-preserve-unknown-fields: true
+            status:
+              type: object
+              x-kubernetes-preserve-unknown-fields: true
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: openstackhelmingresses.atmosphere.vexxhost.com
+spec:
+  scope: Namespaced
+  group: atmosphere.vexxhost.com
+  names:
+    kind: OpenstackHelmIngress
+    plural: openstackhelmingresses
+    singular: openstackhelmingress
+  versions:
+    - name: v1alpha1
+      served: true
+      storage: true
+      schema:
+        openAPIV3Schema:
+          type: object
+          properties:
+            spec:
+              type: object
+              x-kubernetes-preserve-unknown-fields: true
+            status:
+              type: object
+              x-kubernetes-preserve-unknown-fields: true
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
   name: clouds.atmosphere.vexxhost.com
 spec:
   scope: Namespaced
diff --git a/roles/atmosphere/templates/deployment.yml b/roles/atmosphere/templates/deployment.yml
index 64066dc..3242dd0 100644
--- a/roles/atmosphere/templates/deployment.yml
+++ b/roles/atmosphere/templates/deployment.yml
@@ -19,11 +19,6 @@
       containers:
         - name: operator
           image: "{{ atmosphere_image }}"
-          env:
-            - name: POD_IP
-              valueFrom:
-                fieldRef:
-                  fieldPath: status.podIP
           volumeMounts:
             - name: config
               mountPath: /etc/atmosphere
diff --git a/roles/atmosphere/vars/main.yml b/roles/atmosphere/vars/main.yml
index 8801f39..31b9832 100644
--- a/roles/atmosphere/vars/main.yml
+++ b/roles/atmosphere/vars/main.yml
@@ -1,5 +1,5 @@
 _atmosphere_cloud_spec:
-  imageRepository: "{{ atmosphere_image_repository | default('quay.io/vexxhost') }}"
+  imageRepository: "{{ atmosphere_image_repository | default(None) }}"
   ingressClassName: "{{ openstack_helm_ingress_class_name | default('openstack') }}"
   certManagerClusterIssuer: "{{ openstack_helm_ingress_cluster_issuer | default('atmosphere') }}"
   regionName: "{{ openstack_helm_endpoints_region_name }}"
diff --git a/roles/certificates/tasks/main.yml b/roles/certificates/tasks/main.yml
index 4cdb916..e47b897 100644
--- a/roles/certificates/tasks/main.yml
+++ b/roles/certificates/tasks/main.yml
@@ -25,7 +25,7 @@
         namespace: cert-manager
         wait: true
         wait_sleep: 1
-        wait_timeout: 300
+        wait_timeout: 600
       register: _openstack_helm_root_secret
 
     - name: Copy CA certificate on host
diff --git a/roles/openstack_helm_barbican/meta/main.yml b/roles/openstack_helm_barbican/meta/main.yml
index f479c17..3f8b4a4 100644
--- a/roles/openstack_helm_barbican/meta/main.yml
+++ b/roles/openstack_helm_barbican/meta/main.yml
@@ -25,3 +25,4 @@
 
 dependencies:
   - role: atmosphere
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_cinder/meta/main.yml b/roles/openstack_helm_cinder/meta/main.yml
index e44991a..83f86ce 100644
--- a/roles/openstack_helm_cinder/meta/main.yml
+++ b/roles/openstack_helm_cinder/meta/main.yml
@@ -25,3 +25,4 @@
 
 dependencies:
   - role: atmosphere
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_designate/meta/main.yml b/roles/openstack_helm_designate/meta/main.yml
index c0d03fc..469958a 100644
--- a/roles/openstack_helm_designate/meta/main.yml
+++ b/roles/openstack_helm_designate/meta/main.yml
@@ -24,3 +24,4 @@
 
 dependencies:
   - role: atmosphere
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_glance/meta/main.yml b/roles/openstack_helm_glance/meta/main.yml
index d286883..baf7a6d 100644
--- a/roles/openstack_helm_glance/meta/main.yml
+++ b/roles/openstack_helm_glance/meta/main.yml
@@ -25,3 +25,4 @@
 
 dependencies:
   - role: openstacksdk
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_heat/meta/main.yml b/roles/openstack_helm_heat/meta/main.yml
index 3a7140b..57e2e1c 100644
--- a/roles/openstack_helm_heat/meta/main.yml
+++ b/roles/openstack_helm_heat/meta/main.yml
@@ -25,3 +25,4 @@
 
 dependencies:
   - role: atmosphere
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_horizon/meta/main.yml b/roles/openstack_helm_horizon/meta/main.yml
index f101c63..f0271e3 100644
--- a/roles/openstack_helm_horizon/meta/main.yml
+++ b/roles/openstack_helm_horizon/meta/main.yml
@@ -25,3 +25,4 @@
 
 dependencies:
   - role: atmosphere
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_ingress/tasks/main.yml b/roles/openstack_helm_ingress/tasks/main.yml
index 8ba0a52..cf2b287 100644
--- a/roles/openstack_helm_ingress/tasks/main.yml
+++ b/roles/openstack_helm_ingress/tasks/main.yml
@@ -16,26 +16,13 @@
   kubernetes.core.k8s:
     state: present
     definition:
-      apiVersion: v1
-      kind: Ingress
+      apiVersion: atmosphere.vexxhost.com/v1alpha1
+      kind: OpenstackHelmIngress
       metadata:
         name: "{{ openstack_helm_ingress_endpoint | replace('_', '-') }}"
         namespace: openstack
-        annotations: "{{ _openstack_helm_ingress_annotations | combine(openstack_helm_ingress_annotations, recursive=True) }}"
+        annotations: "{{ openstack_helm_ingress_annotations }}"
       spec:
+        clusterIssuer: "{{ openstack_helm_ingress_cluster_issuer | default('atmosphere') }}"
         ingressClassName: "{{ openstack_helm_ingress_class_name | default('openstack') }}"
-        rules:
-          - host: "{{ openstack_helm_endpoints[openstack_helm_ingress_endpoint]['host_fqdn_override']['public']['host'] }}"
-            http:
-              paths: "{{ _openstack_helm_ingress_paths }}"
-        tls:
-          - secretName: "{{ openstack_helm_ingress_secret_name | default(openstack_helm_ingress_service_name + '-certs') }}"
-            hosts:
-              - "{{ openstack_helm_endpoints[openstack_helm_ingress_endpoint]['host_fqdn_override']['public']['host'] }}"
-  # NOTE(mnaser): The Atmosphere operator is so fast that the Ingress webhook
-  #               is not up yet by the time we run this for the first time, so
-  #               we retry until we let the operator handle creating the ingress.
-  retries: 60
-  delay: 5
-  register: _result
-  until: _result is not failed
+        host: "{{ openstack_helm_endpoints[openstack_helm_ingress_endpoint]['host_fqdn_override']['public']['host'] }}"
diff --git a/roles/openstack_helm_ingress/vars/main.yml b/roles/openstack_helm_ingress/vars/main.yml
deleted file mode 100644
index 0af5c42..0000000
--- a/roles/openstack_helm_ingress/vars/main.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-# Copyright (c) 2022 VEXXHOST, Inc.
-#
-# 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.
-
-_openstack_helm_ingress_annotations:
-  cert-manager.io/cluster-issuer: "{{ openstack_helm_ingress_cluster_issuer | default('atmosphere') }}"
-
-_openstack_helm_ingress_paths: "{{ openstack_helm_ingress_paths + __openstack_helm_ingress_paths }}"
-__openstack_helm_ingress_paths:
-  - path: /
-    pathType: Prefix
-    backend:
-      service:
-        name: "{{ openstack_helm_ingress_service_name }}"
-        port:
-          number: "{{ openstack_helm_ingress_service_port }}"
diff --git a/roles/openstack_helm_keystone/meta/main.yml b/roles/openstack_helm_keystone/meta/main.yml
index 4929cc5..0fcab44 100644
--- a/roles/openstack_helm_keystone/meta/main.yml
+++ b/roles/openstack_helm_keystone/meta/main.yml
@@ -25,3 +25,4 @@
 
 dependencies:
   - role: atmosphere
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_magnum/meta/main.yml b/roles/openstack_helm_magnum/meta/main.yml
index f2e80be..9b7fe68 100644
--- a/roles/openstack_helm_magnum/meta/main.yml
+++ b/roles/openstack_helm_magnum/meta/main.yml
@@ -25,5 +25,6 @@
 dependencies:
   - role: openstacksdk
   - role: openstack_cli
+  - role: wait_for_pxc
   - role: openstack_helm_barbican
   - role: openstack_helm_octavia
diff --git a/roles/openstack_helm_neutron/meta/main.yml b/roles/openstack_helm_neutron/meta/main.yml
index ba41c29..b56ee77 100644
--- a/roles/openstack_helm_neutron/meta/main.yml
+++ b/roles/openstack_helm_neutron/meta/main.yml
@@ -26,3 +26,4 @@
 dependencies:
   - role: atmosphere
   - role: openstacksdk
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_nova/meta/main.yml b/roles/openstack_helm_nova/meta/main.yml
index ad12692..2812822 100644
--- a/roles/openstack_helm_nova/meta/main.yml
+++ b/roles/openstack_helm_nova/meta/main.yml
@@ -26,3 +26,4 @@
 dependencies:
   - role: atmosphere
   - role: openstacksdk
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_octavia/meta/main.yml b/roles/openstack_helm_octavia/meta/main.yml
index 181e9ad..96b62d3 100644
--- a/roles/openstack_helm_octavia/meta/main.yml
+++ b/roles/openstack_helm_octavia/meta/main.yml
@@ -25,3 +25,4 @@
 dependencies:
   - role: openstacksdk
   - role: openstack_cli
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_placement/meta/main.yml b/roles/openstack_helm_placement/meta/main.yml
index c83011a..37a83d9 100644
--- a/roles/openstack_helm_placement/meta/main.yml
+++ b/roles/openstack_helm_placement/meta/main.yml
@@ -25,3 +25,4 @@
 
 dependencies:
   - role: atmosphere
+  - role: wait_for_pxc
diff --git a/roles/openstack_helm_senlin/meta/main.yml b/roles/openstack_helm_senlin/meta/main.yml
index 4c25fbe..73308ad 100644
--- a/roles/openstack_helm_senlin/meta/main.yml
+++ b/roles/openstack_helm_senlin/meta/main.yml
@@ -25,3 +25,4 @@
 
 dependencies:
   - role: atmosphere
+  - role: wait_for_pxc
diff --git a/roles/wait_for_pxc/README.md b/roles/wait_for_pxc/README.md
new file mode 100644
index 0000000..918f313
--- /dev/null
+++ b/roles/wait_for_pxc/README.md
@@ -0,0 +1,4 @@
+# wait_for_pxc
+
+This is a meta-role which should be used as a dependency for now to allow the
+Ansible roles to wait for the PXC cluster to be ready before proceeding.
diff --git a/roles/wait_for_pxc/tasks/main.yml b/roles/wait_for_pxc/tasks/main.yml
new file mode 100644
index 0000000..5fa3637
--- /dev/null
+++ b/roles/wait_for_pxc/tasks/main.yml
@@ -0,0 +1,12 @@
+- name: Wait until Percona XtraDB Cluster is ready
+  kubernetes.core.k8s_info:
+    api_version: pxc.percona.com/v1
+    kind: PerconaXtraDBCluster
+    name: percona-xtradb
+    namespace: openstack
+    wait_sleep: 1
+    wait_timeout: 600
+    wait: true
+    wait_condition:
+      type: ready
+      status: true