feat(ingress): enable overriding/disabling
diff --git a/atmosphere/flows.py b/atmosphere/flows.py
index 55d490b..c5f5d0d 100644
--- a/atmosphere/flows.py
+++ b/atmosphere/flows.py
@@ -39,6 +39,7 @@
             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),
         flux.ApplyHelmRepositoryTask(
@@ -89,19 +90,7 @@
             values=constants.HELM_RELEASE_PXC_OPERATOR_VALUES,
         ),
         openstack_helm.ApplyPerconaXtraDBClusterTask(),
-        flux.ApplyHelmRepositoryTask(
-            namespace=constants.NAMESPACE_OPENSTACK,
-            name=constants.HELM_REPOSITORY_INGRESS_NGINX,
-            url="https://kubernetes.github.io/ingress-nginx",
-        ),
-        flux.ApplyHelmReleaseTask(
-            namespace=constants.NAMESPACE_OPENSTACK,
-            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=constants.HELM_RELEASE_INGRESS_NGINX_VALUES,
-        ),
+        *openstack_helm.ingress_nginx_tasks_from_config(config.ingress_nginx),
         flux.ApplyHelmRepositoryTask(
             namespace=constants.NAMESPACE_OPENSTACK,
             name=constants.HELM_REPOSITORY_OPENSTACK_HELM_INFRA,
@@ -143,9 +132,6 @@
         ),
     )
 
-    for t in cert_manager.ApplyIssuerTask.from_config(config.issuer):
-        flow.add(t)
-
     if config.memcached.enabled:
         flow.add(
             openstack_helm.ApplyReleaseSecretTask(
diff --git a/atmosphere/models/config.py b/atmosphere/models/config.py
index f5a7fe9..be20536 100644
--- a/atmosphere/models/config.py
+++ b/atmosphere/models/config.py
@@ -77,21 +77,29 @@
     TYPE = "self-signed"
 
 
+class ChartConfig(base.Model):
+    enabled = types.BooleanType(default=True, required=True)
+    overrides = types.DictType(types.BaseType(), default={})
+
+
 class MemcachedImagesConfig(base.Model):
     memcached = types.StringType(default="docker.io/library/memcached:1.6.17")
     exporter = types.StringType(default="quay.io/prometheus/memcached-exporter:v0.10.0")
 
 
-class MemcachedConfig(base.Model):
-    enabled = types.BooleanType(default=True)
+class MemcachedChartConfig(ChartConfig):
     secret_key = types.StringType(required=True)
     images = types.ModelType(MemcachedImagesConfig, default=MemcachedImagesConfig())
-    overrides = types.DictType(types.BaseType(), default={})
+
+
+class IngressNginxConfig(ChartConfig):
+    pass
 
 
 class Config(base.Model):
+    ingress_nginx = types.ModelType(IngressNginxConfig, default=IngressNginxConfig())
     memcached = types.ModelType(
-        MemcachedConfig, default=MemcachedConfig(), required=True
+        MemcachedChartConfig, default=MemcachedChartConfig(), required=True
     )
     issuer = types.PolyModelType(
         [AcmeIssuerConfig, CaIssuerConfig, SelfSignedIssuerConfig],
diff --git a/atmosphere/tasks/composite/openstack_helm.py b/atmosphere/tasks/composite/openstack_helm.py
index 22d221d..96d0446 100644
--- a/atmosphere/tasks/composite/openstack_helm.py
+++ b/atmosphere/tasks/composite/openstack_helm.py
@@ -49,6 +49,33 @@
         )
 
 
+def ingress_nginx_tasks_from_config(config: config.IngressNginxConfig):
+    if not config.enabled:
+        return []
+
+    values = mergedeep.merge(
+        {},
+        constants.HELM_RELEASE_INGRESS_NGINX_VALUES,
+        config.overrides,
+    )
+
+    return [
+        flux.ApplyHelmRepositoryTask(
+            namespace=constants.NAMESPACE_OPENSTACK,
+            name=constants.HELM_REPOSITORY_INGRESS_NGINX,
+            url=constants.HELM_REPOSITORY_INGRESS_NGINX_URL,
+        ),
+        flux.ApplyHelmReleaseTask(
+            namespace=constants.NAMESPACE_OPENSTACK,
+            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"
diff --git a/atmosphere/tasks/constants.py b/atmosphere/tasks/constants.py
index d816bf8..3073379 100644
--- a/atmosphere/tasks/constants.py
+++ b/atmosphere/tasks/constants.py
@@ -10,7 +10,10 @@
 HELM_REPOSITORY_BITNAMI = "bitnami"
 HELM_REPOSITORY_CEPH = "ceph"
 HELM_REPOSITORY_COREDNS = "coredns"
+
 HELM_REPOSITORY_INGRESS_NGINX = "ingress-nginx"
+HELM_REPOSITORY_INGRESS_NGINX_URL = "https://kubernetes.github.io/ingress-nginx"
+
 HELM_REPOSITORY_JETSTACK = "jetstack"
 HELM_REPOSITORY_NODE_FEATURE_DISCOVERY = "node-feature-discovery"
 HELM_REPOSITORY_OPENSTACK_HELM = "openstack-helm"
diff --git a/atmosphere/tasks/kubernetes/cert_manager.py b/atmosphere/tasks/kubernetes/cert_manager.py
index 95f1431..2d2caca 100644
--- a/atmosphere/tasks/kubernetes/cert_manager.py
+++ b/atmosphere/tasks/kubernetes/cert_manager.py
@@ -77,146 +77,146 @@
             },
         )
 
-    @classmethod
-    def from_config(cls, config: config.Issuer) -> list:
-        objects = []
 
-        if config.type == "acme":
-            spec = {
-                "acme": {
-                    "email": config.email,
-                    "server": config.server,
-                    "privateKeySecretRef": {
-                        "name": "cert-manager-issuer-account-key",
+def issuer_tasks_from_config(config: config.Issuer) -> list:
+    objects = []
+
+    if config.type == "acme":
+        spec = {
+            "acme": {
+                "email": config.email,
+                "server": config.server,
+                "privateKeySecretRef": {
+                    "name": "cert-manager-issuer-account-key",
+                },
+            },
+        }
+
+        if config.solver.type == "http":
+            spec["acme"]["solvers"] = [
+                {
+                    "http01": {
+                        "ingress": {
+                            "class": "openstack",
+                        },
                     },
                 },
-            }
-
-            if config.solver.type == "http":
-                spec["acme"]["solvers"] = [
-                    {
-                        "http01": {
-                            "ingress": {
-                                "class": "openstack",
-                            },
-                        },
-                    },
-                ]
-            elif config.solver.type == "rfc2136":
-                # NOTE(mnaser): We have to create a secret containing the AWS
-                #               credentials in this case.
-                objects.append(
-                    v1.ApplySecretTask(
-                        constants.NAMESPACE_OPENSTACK,
-                        "cert-manager-issuer-tsig-secret-key",
-                        data={
-                            "tsig-secret-key": base64.encode_as_text(
-                                config.solver.tsig_secret
-                            ),
-                        },
-                    )
-                )
-
-                spec["acme"]["solvers"] = [
-                    {
-                        "dns01": {
-                            "rfc2136": {
-                                "nameserver": config.solver.nameserver,
-                                "tsigAlgorithm": config.solver.tsig_algorithm,
-                                "tsigKeyName": config.solver.tsig_key_name,
-                                "tsigSecretSecretRef": {
-                                    "name": "cert-manager-issuer-tsig-secret-key",
-                                    "key": "tsig-secret-key",
-                                },
-                            },
-                        },
-                    },
-                ]
-            elif config.solver.type == "route53":
-                # NOTE(mnaser): We have to create a secret containing the AWS
-                #               credentials in this case.
-                objects.append(
-                    v1.ApplySecretTask(
-                        constants.NAMESPACE_OPENSTACK,
-                        "cert-manager-issuer-route53-credentials",
-                        data={
-                            "secret-access-key": base64.encode_as_text(
-                                config.solver.secret_access_key
-                            ),
-                        },
-                    )
-                )
-
-                spec["acme"]["solvers"] = [
-                    {
-                        "dns01": {
-                            "route53": {
-                                "region": config.solver.region,
-                                "hostedZoneID": config.solver.hosted_zone_id,
-                                "accessKeyID": config.solver.access_key_id,
-                                "secretAccessKeySecretRef": {
-                                    "name": "cert-manager-issuer-route53-credentials",
-                                    "key": "secret-access-key",
-                                },
-                            },
-                        },
-                    },
-                ]
-        elif config.type == "ca":
-            # NOTE(mnaser): We have to create a secret containing the CA
-            #               certificate and key in this case.
+            ]
+        elif config.solver.type == "rfc2136":
+            # NOTE(mnaser): We have to create a secret containing the AWS
+            #               credentials in this case.
             objects.append(
                 v1.ApplySecretTask(
                     constants.NAMESPACE_OPENSTACK,
-                    "cert-manager-issuer-ca",
+                    "cert-manager-issuer-tsig-secret-key",
                     data={
-                        "tls.crt": base64.encode_as_text(config.certificate),
-                        "tls.key": base64.encode_as_text(config.private_key),
+                        "tsig-secret-key": base64.encode_as_text(
+                            config.solver.tsig_secret
+                        ),
                     },
                 )
             )
 
-            spec = {
-                "ca": {
-                    "secretName": "cert-manager-issuer-ca",
-                }
-            }
-        elif config.type == "self-signed":
-            # NOTE(mnaser): We have to setup the self-signed CA in this case
-            objects += [
-                ApplyIssuerTask(
-                    namespace=constants.NAMESPACE_OPENSTACK,
-                    name="self-signed",
-                    spec={
-                        "selfSigned": {},
-                    },
-                ),
-                ApplyCertificateTask(
-                    namespace=constants.NAMESPACE_OPENSTACK,
-                    name="self-signed-ca",
-                    spec={
-                        "isCA": True,
-                        "commonName": "selfsigned-ca",
-                        "secretName": "cert-manager-selfsigned-ca",
-                        "duration": "86400h",
-                        "renewBefore": "360h",
-                        "privateKey": {"algorithm": "ECDSA", "size": 256},
-                        "issuerRef": {
-                            "kind": "Issuer",
-                            "name": "self-signed",
+            spec["acme"]["solvers"] = [
+                {
+                    "dns01": {
+                        "rfc2136": {
+                            "nameserver": config.solver.nameserver,
+                            "tsigAlgorithm": config.solver.tsig_algorithm,
+                            "tsigKeyName": config.solver.tsig_key_name,
+                            "tsigSecretSecretRef": {
+                                "name": "cert-manager-issuer-tsig-secret-key",
+                                "key": "tsig-secret-key",
+                            },
                         },
                     },
-                ),
+                },
             ]
-
-            spec = {
-                "ca": {
-                    "secretName": "cert-manager-selfsigned-ca",
-                }
-            }
-
-        return objects + [
-            ApplyIssuerTask(
-                namespace=constants.NAMESPACE_OPENSTACK, name="openstack", spec=spec
+        elif config.solver.type == "route53":
+            # NOTE(mnaser): We have to create a secret containing the AWS
+            #               credentials in this case.
+            objects.append(
+                v1.ApplySecretTask(
+                    constants.NAMESPACE_OPENSTACK,
+                    "cert-manager-issuer-route53-credentials",
+                    data={
+                        "secret-access-key": base64.encode_as_text(
+                            config.solver.secret_access_key
+                        ),
+                    },
+                )
             )
+
+            spec["acme"]["solvers"] = [
+                {
+                    "dns01": {
+                        "route53": {
+                            "region": config.solver.region,
+                            "hostedZoneID": config.solver.hosted_zone_id,
+                            "accessKeyID": config.solver.access_key_id,
+                            "secretAccessKeySecretRef": {
+                                "name": "cert-manager-issuer-route53-credentials",
+                                "key": "secret-access-key",
+                            },
+                        },
+                    },
+                },
+            ]
+    elif config.type == "ca":
+        # NOTE(mnaser): We have to create a secret containing the CA
+        #               certificate and key in this case.
+        objects.append(
+            v1.ApplySecretTask(
+                constants.NAMESPACE_OPENSTACK,
+                "cert-manager-issuer-ca",
+                data={
+                    "tls.crt": base64.encode_as_text(config.certificate),
+                    "tls.key": base64.encode_as_text(config.private_key),
+                },
+            )
+        )
+
+        spec = {
+            "ca": {
+                "secretName": "cert-manager-issuer-ca",
+            }
+        }
+    elif config.type == "self-signed":
+        # NOTE(mnaser): We have to setup the self-signed CA in this case
+        objects += [
+            ApplyIssuerTask(
+                namespace=constants.NAMESPACE_OPENSTACK,
+                name="self-signed",
+                spec={
+                    "selfSigned": {},
+                },
+            ),
+            ApplyCertificateTask(
+                namespace=constants.NAMESPACE_OPENSTACK,
+                name="self-signed-ca",
+                spec={
+                    "isCA": True,
+                    "commonName": "selfsigned-ca",
+                    "secretName": "cert-manager-selfsigned-ca",
+                    "duration": "86400h",
+                    "renewBefore": "360h",
+                    "privateKey": {"algorithm": "ECDSA", "size": 256},
+                    "issuerRef": {
+                        "kind": "Issuer",
+                        "name": "self-signed",
+                    },
+                },
+            ),
         ]
+
+        spec = {
+            "ca": {
+                "secretName": "cert-manager-selfsigned-ca",
+            }
+        }
+
+    return objects + [
+        ApplyIssuerTask(
+            namespace=constants.NAMESPACE_OPENSTACK, name="openstack", spec=spec
+        )
+    ]
diff --git a/atmosphere/tests/unit/tasks/composite/test_openstack_helm.py b/atmosphere/tests/unit/tasks/composite/test_openstack_helm.py
new file mode 100644
index 0000000..efb161b
--- /dev/null
+++ b/atmosphere/tests/unit/tasks/composite/test_openstack_helm.py
@@ -0,0 +1,129 @@
+import textwrap
+
+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_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": {"disableWait": True},
+                        "interval": "60s",
+                        "upgrade": {"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": {"disableWait": True},
+                        "interval": "60s",
+                        "upgrade": {"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/atmosphere/tests/unit/tasks/kubernetes/test_cert_manager.py b/atmosphere/tests/unit/tasks/kubernetes/test_cert_manager.py
index aecd126..4a24f18 100644
--- a/atmosphere/tests/unit/tasks/kubernetes/test_cert_manager.py
+++ b/atmosphere/tests/unit/tasks/kubernetes/test_cert_manager.py
@@ -304,5 +304,5 @@
     cfg.issuer.validate()
     assert [
         t.generate_object().obj
-        for t in cert_manager.ApplyIssuerTask.from_config(cfg.issuer)
+        for t in cert_manager.issuer_tasks_from_config(cfg.issuer)
     ] == expected