chore: Migrate ingress-nginx from operator to ansible role

In addition, update docker_image filter to support digest output
diff --git a/atmosphere/flows.py b/atmosphere/flows.py
index 42877cc..4bcf410 100644
--- a/atmosphere/flows.py
+++ b/atmosphere/flows.py
@@ -11,42 +11,6 @@
 def get_engine(config):
     api = clients.get_pykube_api()
 
-    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()
-
     objects.Namespace(
         api=api,
         metadata=types.ObjectMeta(
diff --git a/atmosphere/models/config.py b/atmosphere/models/config.py
index b861299..d4c5fab 100644
--- a/atmosphere/models/config.py
+++ b/atmosphere/models/config.py
@@ -112,9 +112,6 @@
     kube_prometheus_stack = types.ModelType(
         KubePrometheusStackChartConfig, default=KubePrometheusStackChartConfig()
     )
-    ingress_nginx = types.ModelType(
-        IngressNginxChartConfig, default=IngressNginxChartConfig()
-    )
     issuer = types.PolyModelType(
         [AcmeIssuerConfig, CaIssuerConfig, SelfSignedIssuerConfig],
         default=AcmeIssuerConfig(),
diff --git a/atmosphere/operator/constants.py b/atmosphere/operator/constants.py
index a77f655..d5c72c9 100644
--- a/atmosphere/operator/constants.py
+++ b/atmosphere/operator/constants.py
@@ -58,9 +58,6 @@
     "heat_purge_deleted": "us-docker.pkg.dev/vexxhost-infra/openstack/heat:wallaby",
     "horizon_db_sync": "us-docker.pkg.dev/vexxhost-infra/openstack/horizon:wallaby",
     "horizon": "us-docker.pkg.dev/vexxhost-infra/openstack/horizon:wallaby",
-    "ingress_nginx_controller": "k8s.gcr.io/ingress-nginx/controller:v1.1.1@sha256:0bc88eb15f9e7f84e8e56c14fa5735aaa488b840983f87bd79b1054190e660de",  # noqa
-    "ingress_nginx_default_backend": "k8s.gcr.io/defaultbackend-amd64:1.5",
-    "ingress_nginx_kube_webhook_certgen": "k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660",  # noqa
     "keepalived": "us-docker.pkg.dev/vexxhost-infra/openstack/keepalived:2.0.19",
     "keystone_api": "quay.io/vexxhost/keystone:wallaby",
     "keystone_credential_cleanup": "quay.io/vexxhost/heat:zed",
diff --git a/atmosphere/tasks/constants.py b/atmosphere/tasks/constants.py
index 34cf1c6..2604f86 100644
--- a/atmosphere/tasks/constants.py
+++ b/atmosphere/tasks/constants.py
@@ -13,9 +13,6 @@
 
 HELM_REPOSITORY_COREDNS = "coredns"
 
-HELM_REPOSITORY_INGRESS_NGINX = "ingress-nginx"
-HELM_REPOSITORY_INGRESS_NGINX_URL = "https://kubernetes.github.io/ingress-nginx"
-
 HELM_REPOSITORY_OPENSTACK_HELM = "openstack-helm"
 HELM_REPOSITORY_OPENSTACK_HELM_INFRA = "openstack-helm-infra"
 
@@ -393,71 +390,3 @@
         pkg_resources.resource_filename("atmosphere.jsonnet", "rules.jsonnet")
     ),
 }
-
-HELM_RELEASE_INGRESS_NGINX_NAME = "ingress-nginx"
-HELM_RELEASE_INGRESS_NGINX_VERSION = "4.0.17"
-HELM_RELEASE_INGRESS_NGINX_VALUES = {
-    "controller": {
-        "image": {
-            "registry": utils.get_image_ref_using_legacy_image_repository(
-                "ingress_nginx_controller"
-            ).repository["domain"],
-            "image": utils.get_image_ref_using_legacy_image_repository(
-                "ingress_nginx_controller"
-            ).repository["path"],
-            "tag": utils.get_image_ref_using_legacy_image_repository(
-                "ingress_nginx_controller"
-            )["tag"],
-            "digest": utils.get_image_ref_using_legacy_image_repository(
-                "ingress_nginx_controller"
-            )["digest"],
-        },
-        "config": {"proxy-buffer-size": "16k"},
-        "dnsPolicy": "ClusterFirstWithHostNet",
-        "hostNetwork": True,
-        "ingressClassResource": {"name": "openstack"},
-        "ingressClass": "openstack",
-        "kind": "DaemonSet",
-        "nodeSelector": NODE_SELECTOR_CONTROL_PLANE,
-        "service": {"type": "ClusterIP"},
-        "admissionWebhooks": {
-            "port": 7443,
-            "patch": {
-                "image": {
-                    "registry": utils.get_image_ref_using_legacy_image_repository(
-                        "ingress_nginx_kube_webhook_certgen"
-                    ).repository["domain"],
-                    "image": utils.get_image_ref_using_legacy_image_repository(
-                        "ingress_nginx_kube_webhook_certgen"
-                    ).repository["path"],
-                    "tag": utils.get_image_ref_using_legacy_image_repository(
-                        "ingress_nginx_kube_webhook_certgen"
-                    )["tag"],
-                    "digest": utils.get_image_ref_using_legacy_image_repository(
-                        "ingress_nginx_kube_webhook_certgen"
-                    )["digest"],
-                }
-            },
-        },
-    },
-    "defaultBackend": {
-        "enabled": True,
-        "image": {
-            "registry": utils.get_image_ref_using_legacy_image_repository(
-                "ingress_nginx_default_backend"
-            ).repository["domain"],
-            "image": utils.get_image_ref_using_legacy_image_repository(
-                "ingress_nginx_default_backend"
-            ).repository["path"],
-            "tag": utils.get_image_ref_using_legacy_image_repository(
-                "ingress_nginx_default_backend"
-            )["tag"],
-        },
-    },
-    "tcp": {
-        "5354": "openstack/minidns:5354",
-    },
-    "udp": {
-        "5354": "openstack/minidns:5354",
-    },
-}
diff --git a/docs/components/ingress.md b/docs/components/ingress.md
index 9600081..20d32c4 100644
--- a/docs/components/ingress.md
+++ b/docs/components/ingress.md
@@ -9,38 +9,14 @@
    If you make any changes to the ingress configuration, you may see a small
    outage as the ingress controller is restarted.
 
-## Customization
-
-You can customize the deployment of the ingress controller by editing the
-`ingress_nginx` section.
-
-!!! warning
-
-   This can result in unsupported behaviour, and is not recommended unless you
-   know what you're doing.
-
-### Disabling
-
-If you're looking to disable the ingress component, you can use the following
-configuration:
-
-```yaml
-atmosphere_config:
-  ingress_nginx:
-    enabled: false
-```
-
 ### Overriding chart values
 
 If you're looking to make changes to the Helm chart values used for the deployment
-of the Ingress, you can use the following configuration:
+of the Ingress, you can define `ingress_nginx_helm_values` ansible variable:
 
 ```yaml
-atmosphere_config:
-  ingress_nginx:
-    overrides:
-      foo: bar
+ingress_nginx_helm_values:
+  foo: bar
 ```
 
-This will be merged with the default values for the chart, and will override any
-values that Atmosphere includes out of the box.
+This will be merged with the default values for the chart.
diff --git a/playbooks/openstack.yml b/playbooks/openstack.yml
index e0ad7dd..95de1c9 100644
--- a/playbooks/openstack.yml
+++ b/playbooks/openstack.yml
@@ -40,6 +40,10 @@
       tags:
         - cert-manager
 
+    - role: ingress_nginx
+      tags:
+        - ingress-nginx
+
     - role: rabbitmq_cluster_operator
       tags:
         - rabbitmq-cluster-operator
diff --git a/plugins/filter/docker_image.py b/plugins/filter/docker_image.py
index ec5c184..0822197 100644
--- a/plugins/filter/docker_image.py
+++ b/plugins/filter/docker_image.py
@@ -91,6 +91,8 @@
             return ref.repository["domain"]
         if part == "path":
             return ref.repository["path"]
+        if part == "digest":
+            return ref["digest"]
 
     ref = reference.Reference.parse(value)
     if not registry:
diff --git a/roles/ingress_nginx/README.md b/roles/ingress_nginx/README.md
new file mode 100644
index 0000000..be5590a
--- /dev/null
+++ b/roles/ingress_nginx/README.md
@@ -0,0 +1 @@
+# `coredns`
diff --git a/roles/ingress_nginx/defaults/main.yml b/roles/ingress_nginx/defaults/main.yml
new file mode 100644
index 0000000..1d1d722
--- /dev/null
+++ b/roles/ingress_nginx/defaults/main.yml
@@ -0,0 +1,20 @@
+# Copyright (c) 2023 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.
+
+ingress_nginx_helm_release_name: ingress-nginx
+ingress_nginx_helm_chart_path: "{{ role_path }}/../../charts/ingress-nginx/"
+ingress_nginx_helm_chart_ref: /usr/local/src/ingress-nginx
+
+ingress_nginx_helm_release_namespace: ingress-nginx
+ingress_nginx_helm_values: {}
diff --git a/roles/ingress_nginx/meta/main.yml b/roles/ingress_nginx/meta/main.yml
new file mode 100644
index 0000000..cf899bd
--- /dev/null
+++ b/roles/ingress_nginx/meta/main.yml
@@ -0,0 +1,31 @@
+# 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.
+
+galaxy_info:
+  author: VEXXHOST, Inc.
+  description: Ansible role for Ingress Nginx Controller
+  license: Apache-2.0
+  min_ansible_version: 5.5.0
+  standalone: false
+  platforms:
+    - name: Ubuntu
+      versions:
+        - focal
+
+dependencies:
+  - role: defaults
+  - role: upload_helm_chart
+    vars:
+      upload_helm_chart_src: "{{ ingress_nginx_helm_chart_path }}"
+      upload_helm_chart_dest: "{{ ingress_nginx_helm_chart_ref }}"
diff --git a/roles/ingress_nginx/tasks/main.yml b/roles/ingress_nginx/tasks/main.yml
new file mode 100644
index 0000000..ce3f058
--- /dev/null
+++ b/roles/ingress_nginx/tasks/main.yml
@@ -0,0 +1,45 @@
+# 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.
+
+- name: Uninstall the legacy HelmRelease
+  run_once: true
+  block:
+    - name: Suspend the existing HelmRelease
+      kubernetes.core.k8s:
+        state: patched
+        api_version: helm.toolkit.fluxcd.io/v2beta1
+        kind: HelmRelease
+        name: "{{ ingress_nginx_helm_release_name }}"
+        namespace: "openstack"
+        definition:
+          spec:
+            suspend: true
+
+    - name: Remove the existing HelmRelease
+      kubernetes.core.k8s:
+        state: absent
+        api_version: helm.toolkit.fluxcd.io/v2beta1
+        kind: HelmRelease
+        name: "{{ ingress_nginx_helm_release_name }}"
+        namespace: "openstack"
+
+- name: Deploy Helm chart
+  run_once: true
+  kubernetes.core.helm:
+    name: "{{ ingress_nginx_helm_release_name }}"
+    chart_ref: "{{ ingress_nginx_helm_chart_ref }}"
+    release_namespace: "{{ ingress_nginx_helm_release_namespace }}"
+    create_namespace: true
+    kubeconfig: /etc/kubernetes/admin.conf
+    values: "{{ _ingress_nginx_helm_values | combine(ingress_nginx_helm_values, recursive=True) }}"
diff --git a/roles/ingress_nginx/vars/main.yml b/roles/ingress_nginx/vars/main.yml
new file mode 100644
index 0000000..8370da9
--- /dev/null
+++ b/roles/ingress_nginx/vars/main.yml
@@ -0,0 +1,51 @@
+# 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.
+
+_ingress_nginx_helm_values:
+  controller:
+    image:
+      registry: "{{ atmosphere_images['ingress_nginx_controller'] | vexxhost.atmosphere.docker_image('domain') }}"
+      image: "{{ atmosphere_images['ingress_nginx_controller'] | vexxhost.atmosphere.docker_image('path') }}"
+      tag: "{{ atmosphere_images['ingress_nginx_controller'] | vexxhost.atmosphere.docker_image('tag') }}"
+      digest: "{{ atmosphere_images['ingress_nginx_controller'] | vexxhost.atmosphere.docker_image('digest') }}"
+    config:
+      proxy-buffer-size: 16k
+    dnsPolicy: ClusterFirstWithHostNet
+    hostNetwork: true
+    ingressClassResource:
+      name: openstack
+    ingressClass: openstack
+    kind: DaemonSet
+    nodeSelector:
+      openstack-control-plane: enabled
+    service:
+      type: ClusterIP
+    admissionWebhooks:
+      port: 7443
+      patch:
+        image:
+          registry: "{{ atmosphere_images['ingress_nginx_kube_webhook_certgen'] | vexxhost.atmosphere.docker_image('domain') }}"
+          image: "{{ atmosphere_images['ingress_nginx_kube_webhook_certgen'] | vexxhost.atmosphere.docker_image('path') }}"
+          tag: "{{ atmosphere_images['ingress_nginx_kube_webhook_certgen'] | vexxhost.atmosphere.docker_image('tag') }}"
+          digest: "{{ atmosphere_images['ingress_nginx_kube_webhook_certgen'] | vexxhost.atmosphere.docker_image('digest') }}"
+  defaultBackend:
+    enabled: true
+    image:
+      registry: "{{ atmosphere_images['ingress_nginx_default_backend'] | vexxhost.atmosphere.docker_image('domain') }}"
+      image: "{{ atmosphere_images['ingress_nginx_default_backend'] | vexxhost.atmosphere.docker_image('path') }}"
+      tag: "{{ atmosphere_images['ingress_nginx_default_backend'] | vexxhost.atmosphere.docker_image('tag') }}"
+  tcp:
+    '5354': openstack/minidns:5354
+  udp:
+    '5354': openstack/minidns:5354