chore: add more tests + add config
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..8675f7e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,17 @@
+# syntax=docker/dockerfile-upstream:master-labs
+
+FROM python:3.10-slim AS poetry
+RUN --mount=type=cache,target=/root/.cache <<EOF
+  pip install poetry
+EOF
+
+FROM poetry AS builder
+ADD . /app
+WORKDIR /app
+ENV POETRY_VIRTUALENVS_IN_PROJECT=true
+RUN poetry install --only main --no-interaction
+
+FROM python:3.10-slim AS runtime
+ENV PATH="/app/.venv/bin:$PATH"
+COPY --from=builder --link /app /app
+CMD ["atmosphere-operator"]
diff --git a/atmosphere/clients.py b/atmosphere/clients.py
new file mode 100644
index 0000000..ef36537
--- /dev/null
+++ b/atmosphere/clients.py
@@ -0,0 +1,5 @@
+import pykube
+
+
+def get_pykube_api(timeout=None) -> pykube.HTTPClient:
+    return pykube.HTTPClient(pykube.KubeConfig.from_env(), timeout=timeout)
diff --git a/atmosphere/cmd/__init__.py b/atmosphere/cmd/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/atmosphere/cmd/__init__.py
diff --git a/atmosphere/cmd/operator.py b/atmosphere/cmd/operator.py
new file mode 100644
index 0000000..c9b3ab0
--- /dev/null
+++ b/atmosphere/cmd/operator.py
@@ -0,0 +1,5 @@
+from atmosphere import deploy
+
+
+def main():
+    deploy.run()
diff --git a/atmosphere/config/__init__.py b/atmosphere/config/__init__.py
new file mode 100644
index 0000000..98bf87e
--- /dev/null
+++ b/atmosphere/config/__init__.py
@@ -0,0 +1,23 @@
+import os
+import sys
+
+import confspirator
+from confspirator import groups
+
+from atmosphere.config import images, memcached
+
+_root_config = groups.ConfigGroup("atmosphere")
+_root_config.register_child_config(images.config_group)
+_root_config.register_child_config(memcached.config_group)
+
+CONFIG_FILE = os.environ.get('ATMOSPHERE_CONFIG', '/etc/atmosphere/config.toml')
+
+
+def _load_config():
+    if "pytest" in sys.modules:
+        return confspirator.load_dict(_root_config, {}, test_mode=True)
+
+    return confspirator.load_file(_root_config, CONFIG_FILE)
+
+
+CONF = _load_config()
diff --git a/atmosphere/config/images.py b/atmosphere/config/images.py
new file mode 100644
index 0000000..2520187
--- /dev/null
+++ b/atmosphere/config/images.py
@@ -0,0 +1,20 @@
+from confspirator import fields, groups
+
+config_group = groups.ConfigGroup("images")
+
+config_group.register_child_config(
+    fields.StrConfig(
+        "memcached",
+        required=True,
+        default="docker.io/library/memcached:1.6.17",
+        help_text="Memcached image",
+    ),
+)
+config_group.register_child_config(
+    fields.StrConfig(
+        "memcached_exporter",
+        required=True,
+        default="quay.io/prometheus/memcached-exporter:v0.10.0",
+        help_text="Prometheus Memcached exporter image",
+    ),
+)
diff --git a/atmosphere/config/memcached.py b/atmosphere/config/memcached.py
new file mode 100644
index 0000000..44a466c
--- /dev/null
+++ b/atmosphere/config/memcached.py
@@ -0,0 +1,22 @@
+import uuid
+
+from confspirator import fields, groups
+
+config_group = groups.ConfigGroup("memcached")
+
+config_group.register_child_config(
+    fields.BoolConfig(
+        "enabled",
+        default=True,
+        required=True,
+        help_text="Enable Atmosphere-managed Memcached",
+    ),
+)
+config_group.register_child_config(
+    fields.StrConfig(
+        "secret_key",
+        required=True,
+        help_text="Encryption secret key",
+        test_default=uuid.uuid4().hex,
+    ),
+)
diff --git a/atmosphere/deploy.py b/atmosphere/deploy.py
index be76d36..2564976 100644
--- a/atmosphere/deploy.py
+++ b/atmosphere/deploy.py
@@ -1,6 +1,11 @@
+from atmosphere import clients
+from atmosphere.config import CONF
 from atmosphere.models.openstack_helm import values
 
 
-def run(api, config):
-    if config.memcached:
-        values.Values.for_chart("memcached", config).apply(api)
+def run(api=None):
+    if not api:
+        api = clients.get_pykube_api()
+
+    if CONF.memcached.enabled:
+        values.Values.for_chart("memcached").apply(api)
diff --git a/atmosphere/models/conf.py b/atmosphere/models/conf.py
deleted file mode 100644
index 73d4f6f..0000000
--- a/atmosphere/models/conf.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import tomli
-from schematics import types
-
-from atmosphere.models import base
-
-
-class ServiceConfig(base.Model):
-    enabled = types.BooleanType(default=True)
-    overrides = types.DictType(types.BaseType)
-
-
-class MemcachedImagesConfig(base.Model):
-    memcached = types.StringType(default="quay.io/vexxhost/memcached:1.6.9")
-    prometheus_memcached_exporter = types.StringType(
-        default="quay.io/vexxhost/memcached-exporter:v0.9.0-1"
-    )
-
-
-class MemcachedServiceConfig(ServiceConfig):
-    images = types.ModelType(MemcachedImagesConfig, default=MemcachedImagesConfig())
-    secret_key = types.StringType(required=True)
-
-
-class AtmosphereConfig(base.Model):
-    memcached = types.ModelType(
-        MemcachedServiceConfig, default=MemcachedServiceConfig()
-    )
-
-
-def from_toml(data):
-    cfg = AtmosphereConfig(tomli.loads(data), validate=True)
-    cfg.validate()
-    return cfg
-
-
-def from_file(path):
-    with open(path) as f:
-        return from_toml(f.read())
diff --git a/atmosphere/models/openstack_helm/endpoints.py b/atmosphere/models/openstack_helm/endpoints.py
index 82519a6..bfad161 100644
--- a/atmosphere/models/openstack_helm/endpoints.py
+++ b/atmosphere/models/openstack_helm/endpoints.py
@@ -1,5 +1,6 @@
 from schematics import types
 
+from atmosphere.config import CONF
 from atmosphere.models import base
 
 
@@ -19,12 +20,12 @@
     auth = types.ModelType(OsloCacheEndpointAuth)
 
     @classmethod
-    def for_chart(cls, chart, config):
+    def for_chart(cls, chart):
         return cls(
             {
                 "auth": OsloCacheEndpointAuth(
                     {
-                        "memcache_secret_key": config.memcached.secret_key,
+                        "memcache_secret_key": CONF.memcached.secret_key,
                     }
                 )
             }
@@ -49,7 +50,7 @@
     hosts = types.ModelType(OsloDbEndpointHosts, default=OsloDbEndpointHosts())
 
     @classmethod
-    def for_chart(cls, chart, config):
+    def for_chart(cls, chart):
         pass
 
 
@@ -67,13 +68,11 @@
     }
 
     @classmethod
-    def for_chart(cls, chart, config):
+    def for_chart(cls, chart):
         endpoint = cls()
 
         for endpoint_name in cls.ENDPOINTS[chart]:
-            endpoint[endpoint_name] = cls.MAPPINGS[endpoint_name].for_chart(
-                chart, config
-            )
+            endpoint[endpoint_name] = cls.MAPPINGS[endpoint_name].for_chart(chart)
         endpoint.validate()
 
         return endpoint
diff --git a/atmosphere/models/openstack_helm/images.py b/atmosphere/models/openstack_helm/images.py
index f807395..32b03dd 100644
--- a/atmosphere/models/openstack_helm/images.py
+++ b/atmosphere/models/openstack_helm/images.py
@@ -1,5 +1,6 @@
 from schematics import types
 
+from atmosphere.config import CONF
 from atmosphere.models import base
 
 
@@ -12,11 +13,11 @@
     prometheus_memcached_exporter = types.StringType(required=True)
 
     @classmethod
-    def for_chart(cls, chart, config):
+    def for_chart(cls, chart):
         return cls(
             {
-                "memcached": config.memcached.images.memcached,
-                "prometheus_memcached_exporter": config.memcached.images.prometheus_memcached_exporter,
+                "memcached": CONF.images.memcached,
+                "prometheus_memcached_exporter": CONF.images.memcached_exporter,
             }
         )
 
@@ -30,9 +31,9 @@
     }
 
     @classmethod
-    def for_chart(cls, chart, config):
+    def for_chart(cls, chart):
         return cls(
             {
-                "tags": cls.MAPPINGS[chart].for_chart(chart, config),
+                "tags": cls.MAPPINGS[chart].for_chart(chart),
             }
         )
diff --git a/atmosphere/models/openstack_helm/monitoring.py b/atmosphere/models/openstack_helm/monitoring.py
index c508f3c..e67b656 100644
--- a/atmosphere/models/openstack_helm/monitoring.py
+++ b/atmosphere/models/openstack_helm/monitoring.py
@@ -11,7 +11,7 @@
     prometheus = types.ModelType(PrometheusMonitoring)
 
     @classmethod
-    def for_chart(cls, chart, config):
+    def for_chart(cls, chart):
         if chart == "memcached":
             return Monitoring(
                 {
diff --git a/atmosphere/models/openstack_helm/values.py b/atmosphere/models/openstack_helm/values.py
index 7bd81da..ded2cfd 100644
--- a/atmosphere/models/openstack_helm/values.py
+++ b/atmosphere/models/openstack_helm/values.py
@@ -20,13 +20,13 @@
         roles = {"default": blacklist("chart")}
 
     @classmethod
-    def for_chart(cls, chart, config):
+    def for_chart(cls, chart):
         return cls(
             {
                 "chart": chart,
-                "endpoints": endpoints.Endpoints.for_chart(chart, config),
-                "images": images.Images.for_chart(chart, config),
-                "monitoring": monitoring.Monitoring.for_chart(chart, config),
+                "endpoints": endpoints.Endpoints.for_chart(chart),
+                "images": images.Images.for_chart(chart),
+                "monitoring": monitoring.Monitoring.for_chart(chart),
             }
         )
 
@@ -47,11 +47,13 @@
 
     def apply(self, api):
         resource = self.secret()
-        secret = pykube.Secret(api, resource)
+        secret = pykube.Secret(api, self.secret())
 
         if not secret.exists():
             secret.create()
 
+        secret.reload()
+
         if secret.obj["data"] != resource["data"]:
             secret.obj["data"] = resource["data"]
             secret.update()
diff --git a/atmosphere/operator.py b/atmosphere/operator.py
deleted file mode 100644
index 553d53f..0000000
--- a/atmosphere/operator.py
+++ /dev/null
@@ -1,6 +0,0 @@
-import kopf
-
-
-@kopf.on.event("secret", field="metadata.name", value="atmosphere-config")
-def secret_event_handler(body, **kwargs):
-    print(body)
diff --git a/atmosphere/shell.py b/atmosphere/shell.py
deleted file mode 100644
index 35ee45e..0000000
--- a/atmosphere/shell.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import click
-import pykube
-
-from atmosphere import deploy
-from atmosphere.models import conf
-
-
-@click.command()
-@click.option("--config", help="Path to Atmosphere config file", required=True)
-def run(config):
-    config = conf.from_file(config)
-
-    kube_config = pykube.KubeConfig.from_env()
-    api = pykube.HTTPClient(kube_config)
-
-    deploy.run(api, config)
-
-
-if __name__ == "__main__":
-    run()
diff --git a/atmosphere/tests/integration/test_kind.py b/atmosphere/tests/integration/test_kind.py
index 305af36..1181cc7 100644
--- a/atmosphere/tests/integration/test_kind.py
+++ b/atmosphere/tests/integration/test_kind.py
@@ -1,7 +1,8 @@
+import confspirator
 import pykube
 
 from atmosphere import deploy
-from atmosphere.models import conf
+from atmosphere.config import CONF
 from atmosphere.models.openstack_helm import values
 
 
@@ -12,35 +13,25 @@
 def test_deployment(kind_cluster, tmp_path):
     kind_cluster.kubectl("create", "namespace", "openstack")
 
-    initial_path = tmp_path / "config-initial.toml"
-    initial_path.write_text(
-        """
-    [memcached]
-    secret_key = "secret"
-    """
-    )
-
-    config = conf.from_file(initial_path)
-    deploy.run(kind_cluster.api, config)
+    deploy.run(api=kind_cluster.api)
 
     initial_memcache_secret = pykube.Secret(
-        kind_cluster.api, values.Values.for_chart("memcached", config).secret()
+        kind_cluster.api, values.Values.for_chart("memcached").secret()
     )
     assert initial_memcache_secret.exists()
 
-    updated_path = tmp_path / "config-updated.toml"
-    updated_path.write_text(
-        """
-    [memcached]
-    secret_key = "not-secret"
-    """
-    )
-
-    config = conf.from_file(updated_path)
-    deploy.run(kind_cluster.api, config)
+    with confspirator.modify_conf(
+        CONF,
+        {
+            "atmosphere.memcached.secret_key": [
+                {"operation": "override", "value": "not-secret"}
+            ],
+        },
+    ):
+        deploy.run(api=kind_cluster.api)
 
     updated_memcache_secret = pykube.Secret(
-        kind_cluster.api, values.Values.for_chart("memcached", config).secret()
+        kind_cluster.api, values.Values.for_chart("memcached").secret()
     )
     assert updated_memcache_secret.exists()
 
diff --git a/atmosphere/tests/unit/models/openstack_helm/test_endpoints.py b/atmosphere/tests/unit/models/openstack_helm/test_endpoints.py
index 488bc7e..17044b5 100644
--- a/atmosphere/tests/unit/models/openstack_helm/test_endpoints.py
+++ b/atmosphere/tests/unit/models/openstack_helm/test_endpoints.py
@@ -1,27 +1,14 @@
-import pytest
-from schematics import exceptions
-
-from atmosphere.models import conf
+from atmosphere.config import CONF
 from atmosphere.models.openstack_helm import endpoints as osh_endpoints
 
 
 def test_endpoint_for_chart_memcached():
-    data = conf.AtmosphereConfig.get_mock_object()
-    data.memcached.secret_key = "foobar"
-    endpoints = osh_endpoints.Endpoints.for_chart("memcached", data)
+    endpoints = osh_endpoints.Endpoints.for_chart("memcached")
 
     assert {
         "oslo_cache": {
             "auth": {
-                "memcache_secret_key": "foobar",
+                "memcache_secret_key": CONF.memcached.secret_key,
             }
         }
     } == endpoints.to_primitive()
-
-
-def test_endpoint_for_chart_memcached_with_no_secret_key():
-    data = conf.AtmosphereConfig.get_mock_object()
-    data.memcached.secret_key = None
-
-    with pytest.raises(exceptions.DataError):
-        osh_endpoints.Endpoints.for_chart("memcached", data)
diff --git a/atmosphere/tests/unit/models/openstack_helm/test_images.py b/atmosphere/tests/unit/models/openstack_helm/test_images.py
index 3f315d3..556c6aa 100644
--- a/atmosphere/tests/unit/models/openstack_helm/test_images.py
+++ b/atmosphere/tests/unit/models/openstack_helm/test_images.py
@@ -1,31 +1,31 @@
-from atmosphere.models import conf
+import confspirator
+
+from atmosphere.config import CONF
 from atmosphere.models.openstack_helm import images as osh_images
 
 
 def test_images_for_chart_memcached_with_defaults():
-    data = conf.AtmosphereConfig.get_mock_object()
-    images = osh_images.Images.for_chart("memcached", data)
-
     assert {
         "pull_policy": "Always",
         "tags": {
-            "memcached": data.memcached.images.memcached,
-            "prometheus_memcached_exporter": data.memcached.images.prometheus_memcached_exporter,
+            "memcached": CONF.images.memcached,
+            "prometheus_memcached_exporter": CONF.images.memcached_exporter,
         },
-    }, images.to_primitive()
+    } == osh_images.Images.for_chart("memcached").to_primitive()
 
 
+@confspirator.modify_conf(
+    CONF,
+    {
+        "atmosphere.images.memcached": [{"operation": "override", "value": "foo"}],
+        "atmosphere.images.memcached_exporter": [{"operation": "override", "value": "bar"}],
+    },
+)
 def test_images_for_chart_memcached_with_overrides():
-    data = conf.AtmosphereConfig.get_mock_object()
-    data.memcached.images.memcached = "foo"
-    data.memcached.images.prometheus_memcached_exporter = "bar"
-
-    images = osh_images.Images.for_chart("memcached", data)
-
     assert {
         "pull_policy": "Always",
         "tags": {
             "memcached": "foo",
             "prometheus_memcached_exporter": "bar",
         },
-    }, images.to_primitive()
+    } == osh_images.Images.for_chart("memcached").to_primitive()
diff --git a/atmosphere/tests/unit/models/openstack_helm/test_values.py b/atmosphere/tests/unit/models/openstack_helm/test_values.py
index c33615b..8f8f940 100644
--- a/atmosphere/tests/unit/models/openstack_helm/test_values.py
+++ b/atmosphere/tests/unit/models/openstack_helm/test_values.py
@@ -1,24 +1,26 @@
+import confspirator
 import pykube
 import pytest
 
-from atmosphere.models import conf
+from atmosphere.config import CONF
 from atmosphere.models.openstack_helm import values as osh_values
 
 
 class TestMemcachedValues:
     def test_values_for_chart(self):
-        data = conf.AtmosphereConfig.get_mock_object()
-        data.memcached.secret_key = "foobar"
-
-        values = osh_values.Values.for_chart("memcached", data)
+        values = osh_values.Values.for_chart("memcached")
 
         assert {
-            "endpoints": {"oslo_cache": {"auth": {"memcache_secret_key": "foobar"}}},
+            "endpoints": {
+                "oslo_cache": {
+                    "auth": {"memcache_secret_key": CONF.memcached.secret_key}
+                }
+            },
             "images": {
                 "pull_policy": "Always",
                 "tags": {
-                    "memcached": data.memcached.images.memcached,
-                    "prometheus_memcached_exporter": data.memcached.images.prometheus_memcached_exporter,
+                    "memcached": CONF.images.memcached,
+                    "prometheus_memcached_exporter": CONF.images.memcached_exporter,
                 },
             },
             "monitoring": {
@@ -29,10 +31,7 @@
         } == values.to_primitive()
 
     def test_apply_for_chart_with_no_existing_config(self, mocker):
-        data = conf.AtmosphereConfig.get_mock_object()
-        data.memcached.secret_key = "foobar"
-
-        values = osh_values.Values.for_chart("memcached", data)
+        values = osh_values.Values.for_chart("memcached")
 
         api = mocker.MagicMock()
 
@@ -48,13 +47,11 @@
         mocked_secret_class.assert_called_once_with(api, values.secret())
         mocked_secret.exists.assert_called_once()
         mocked_secret.create.assert_called_once()
+        mocked_secret.reload.assert_called_once()
         mocked_secret.update.assert_not_called()
 
     def test_apply_for_chart_with_no_config_change(self, mocker):
-        data = conf.AtmosphereConfig.get_mock_object()
-        data.memcached.secret_key = "foobar"
-
-        values = osh_values.Values.for_chart("memcached", data)
+        values = osh_values.Values.for_chart("memcached")
 
         api = mocker.MagicMock()
 
@@ -70,16 +67,21 @@
         mocked_secret_class.assert_called_once_with(api, values.secret())
         mocked_secret.exists.assert_called_once()
         mocked_secret.create.assert_not_called()
+        mocked_secret.reload.assert_called_once()
         mocked_secret.update.assert_not_called()
 
     def test_apply_for_chart_with_config_change(self, mocker):
-        data = conf.AtmosphereConfig.get_mock_object()
-        data.memcached.secret_key = "foobar"
+        old_values = osh_values.Values.for_chart("memcached")
 
-        old_values = osh_values.Values.for_chart("memcached", data)
-
-        data.memcached.secret_key = "barfoo"
-        new_values = osh_values.Values.for_chart("memcached", data)
+        with confspirator.modify_conf(
+            CONF,
+            {
+                "atmosphere.memcached.secret_key": [
+                    {"operation": "override", "value": "barfoo"}
+                ],
+            },
+        ):
+            new_values = osh_values.Values.for_chart("memcached")
 
         api = mocker.MagicMock()
 
@@ -95,13 +97,11 @@
         mocked_secret_class.assert_called_once_with(api, new_values.secret())
         mocked_secret.exists.assert_called_once()
         mocked_secret.create.assert_not_called()
+        mocked_secret.reload.assert_called_once()
         mocked_secret.update.assert_called_once()
 
     def test_apply_for_chart_with_unknown_failure(self, mocker):
-        data = conf.AtmosphereConfig.get_mock_object()
-        data.memcached.secret_key = "foobar"
-
-        values = osh_values.Values.for_chart("memcached", data)
+        values = osh_values.Values.for_chart("memcached")
 
         api = mocker.MagicMock()
         mocked_secret = mocker.MagicMock()
diff --git a/atmosphere/tests/unit/models/test_config.py b/atmosphere/tests/unit/models/test_config.py
deleted file mode 100644
index c1aa2cf..0000000
--- a/atmosphere/tests/unit/models/test_config.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import uuid
-
-import pytest
-from schematics import exceptions
-
-from atmosphere.models import conf
-
-MEMCACHE_SECRET_KEY = uuid.uuid4().hex
-
-VALID_CONFIG = f"""
-[memcached]
-secret_key = "{MEMCACHE_SECRET_KEY}"
-"""
-
-
-def test_from_toml_with_valid_configuration():
-    try:
-        data = conf.from_toml(VALID_CONFIG)
-    except exceptions.DataError:
-        pytest.fail("Failed to parse valid configuration")
-
-    assert data.memcached.secret_key == MEMCACHE_SECRET_KEY
-
-
-def test_from_toml_with_invalid_configuration():
-    with pytest.raises(exceptions.DataError):
-        conf.from_toml("")
-
-
-def test_from_file_with_valid_configuration(tmp_path):
-    path = tmp_path / "config.toml"
-    path.write_text(VALID_CONFIG)
-
-    try:
-        data = conf.from_file(path)
-    except exceptions.DataError:
-        pytest.fail("Failed to parse valid configuration")
-
-    assert data.memcached.secret_key == MEMCACHE_SECRET_KEY
-
-
-def test_from_file_with_invalid_configuration(tmp_path):
-    path = tmp_path / "config.toml"
-    path.write_text("")
-
-    with pytest.raises(exceptions.DataError):
-        conf.from_file(path)
-
-
-def test_from_file_with_missing_file(tmp_path):
-    path = tmp_path / "config.toml"
-
-    with pytest.raises(FileNotFoundError):
-        conf.from_file(path)
diff --git a/poetry.lock b/poetry.lock
index 128a29c..26d5b7c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -40,6 +40,23 @@
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
+name = "confspirator"
+version = "0.3.0"
+description = "A config library for handling nested incode config groups."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+netaddr = ">=0.7.18"
+pbr = ">=5.2.0"
+python-slugify = ">=3.0.2"
+PyYAML = ">=5.1"
+rfc3986 = ">=1.2.0"
+six = ">=1.12.0"
+toml = ">=0.10.2"
+
+[[package]]
 name = "coverage"
 version = "6.4.4"
 description = "Code coverage measurement for Python"
@@ -120,6 +137,14 @@
 python-versions = ">=3.6"
 
 [[package]]
+name = "netaddr"
+version = "0.8.0"
+description = "A network address manipulation library for Python"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
 name = "packaging"
 version = "21.3"
 description = "Core utilities for Python packages"
@@ -131,6 +156,14 @@
 pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
 
 [[package]]
+name = "pbr"
+version = "5.10.0"
+description = "Python Build Reasonableness"
+category = "main"
+optional = false
+python-versions = ">=2.6"
+
+[[package]]
 name = "pluggy"
 version = "1.0.0"
 description = "plugin and hook calling mechanisms for python"
@@ -255,6 +288,20 @@
 dev = ["pre-commit", "pytest-asyncio", "tox"]
 
 [[package]]
+name = "python-slugify"
+version = "6.1.2"
+description = "A Python slugify application that also handles Unicode"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[package.dependencies]
+text-unidecode = ">=1.3"
+
+[package.extras]
+unidecode = ["Unidecode (>=1.1.1)"]
+
+[[package]]
 name = "PyYAML"
 version = "6.0"
 description = "YAML parser and emitter for Python"
@@ -281,6 +328,17 @@
 use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
+name = "rfc3986"
+version = "2.0.0"
+description = "Validating URI References per RFC 3986"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+idna2008 = ["idna"]
+
+[[package]]
 name = "schematics"
 version = "2.1.1"
 description = "Python Data Structures for Humans"
@@ -289,6 +347,30 @@
 python-versions = "*"
 
 [[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "text-unidecode"
+version = "1.3"
+description = "The most basic Text::Unidecode port"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
 name = "tomli"
 version = "2.0.1"
 description = "A lil' TOML parser"
@@ -312,7 +394,7 @@
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.10"
-content-hash = "878e26cc2b4158be9e89c5f00f83895dc93e9fd180fb44c739afdaa87d0a3430"
+content-hash = "164328b30d2cabb80939277c14bed8bd9db881b6e2f80f95f03d4230a48f5cf1"
 
 [metadata.files]
 attrs = [
@@ -331,6 +413,9 @@
     {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
     {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
 ]
+confspirator = [
+    {file = "confspirator-0.3.0.tar.gz", hash = "sha256:065c22e8c317c623668fd71c6c40038829b934ec320785c7d21e05b6a5b2c711"},
+]
 coverage = [
     {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"},
     {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"},
@@ -407,10 +492,18 @@
     {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
     {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
 ]
+netaddr = [
+    {file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"},
+    {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"},
+]
 packaging = [
     {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
     {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
 ]
+pbr = [
+    {file = "pbr-5.10.0-py2.py3-none-any.whl", hash = "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf"},
+    {file = "pbr-5.10.0.tar.gz", hash = "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a"},
+]
 pluggy = [
     {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
     {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
@@ -451,6 +544,10 @@
     {file = "pytest-mock-3.8.2.tar.gz", hash = "sha256:77f03f4554392558700295e05aed0b1096a20d4a60a4f3ddcde58b0c31c8fca2"},
     {file = "pytest_mock-3.8.2-py3-none-any.whl", hash = "sha256:8a9e226d6c0ef09fcf20c94eb3405c388af438a90f3e39687f84166da82d5948"},
 ]
+python-slugify = [
+    {file = "python-slugify-6.1.2.tar.gz", hash = "sha256:272d106cb31ab99b3496ba085e3fea0e9e76dcde967b5e9992500d1f785ce4e1"},
+    {file = "python_slugify-6.1.2-py2.py3-none-any.whl", hash = "sha256:7b2c274c308b62f4269a9ba701aa69a797e9bca41aeee5b3a9e79e36b6656927"},
+]
 PyYAML = [
     {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
     {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
@@ -497,10 +594,26 @@
     {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
     {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
 ]
+rfc3986 = [
+    {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"},
+    {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"},
+]
 schematics = [
     {file = "schematics-2.1.1-py2.py3-none-any.whl", hash = "sha256:be2d451bfb86789975e5ec0864aec569b63cea9010f0d24cbbd992a4e564c647"},
     {file = "schematics-2.1.1.tar.gz", hash = "sha256:34c87f51a25063bb498ae1cc201891b134cfcb329baf9e9f4f3ae869b767560f"},
 ]
+six = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+text-unidecode = [
+    {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"},
diff --git a/pyproject.toml b/pyproject.toml
index 1c7be9a..d2055ba 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,10 +7,14 @@
 authors = ["Mohammed Naser <mnaser@vexxhost.com>"]
 readme = "README.md"
 
+[tool.poetry.scripts]
+atmosphere-operator = "atmosphere.cmd.operator:main"
+
 [tool.poetry.dependencies]
 python = "^3.10"
 schematics = "^2.1.1"
 pykube-ng = "^22.7.0"
+confspirator = "^0.3.0"
 
 [tool.poetry.group.dev.dependencies]
 pytest = "^7.1.3"