From 34865832f3f97e8014faa6e98604cbe89e1fcc1c Mon Sep 17 00:00:00 2001 From: Leon Wright Date: Wed, 19 Jun 2024 17:09:42 +0800 Subject: [PATCH 1/3] feat: Initial environment loader concept --- src/cally/cli/commands/tf.py | 10 ++-- src/cally/cli/config/__init__.py | 24 +++++++-- src/cally/cli/config/loaders/__init__.py | 27 ++++++++++ src/cally/cli/config/loaders/environment.py | 53 +++++++++++++++++++ .../config/{loader.py => loaders/service.py} | 25 ++------- src/cally/cli/config/types.py | 5 ++ tests/__init__.py | 2 +- tests/cli/config/__init__.py | 6 +-- 8 files changed, 119 insertions(+), 33 deletions(-) create mode 100644 src/cally/cli/config/loaders/__init__.py create mode 100644 src/cally/cli/config/loaders/environment.py rename src/cally/cli/config/{loader.py => loaders/service.py} (62%) diff --git a/src/cally/cli/commands/tf.py b/src/cally/cli/commands/tf.py index 0308b7f..1968b63 100644 --- a/src/cally/cli/commands/tf.py +++ b/src/cally/cli/commands/tf.py @@ -3,7 +3,7 @@ import click -from ..config import CallyConfig, service_options +from ..config import CallyConfig, service_options, pass_stack_obj from ..tools import terraform @@ -14,9 +14,9 @@ def tf() -> None: @click.command(name='print') @service_options -@click.pass_obj +@pass_stack_obj def print_template(config: CallyConfig): - with terraform.Action(service=config.as_dataclass('CallyStackService')) as action: + with terraform.Action(service=config.as_dataclass()) as action: click.secho(action.print()) @@ -28,9 +28,9 @@ def print_template(config: CallyConfig): help='Output path for the terraform json', ) @service_options -@click.pass_obj +@pass_stack_obj def write_template(config: CallyConfig, output: Path): - with terraform.Action(service=config.as_dataclass('CallyStackService')) as action: + with terraform.Action(service=config.as_dataclass()) as action: output.write_text(action.print()) click.secho(f'Template written to {output}') diff --git a/src/cally/cli/config/__init__.py b/src/cally/cli/config/__init__.py index 706518e..8937302 100644 --- a/src/cally/cli/config/__init__.py +++ b/src/cally/cli/config/__init__.py @@ -1,4 +1,5 @@ from dataclasses import fields +from functools import update_wrapper from pathlib import Path from typing import Optional, Union @@ -11,12 +12,16 @@ class CallyConfig: config_file: Path + loader: str + cally_type: str _environment: Optional[str] = None _service: Optional[str] = None _settings: Dynaconf def __init__(self, config_file: Path) -> None: self.config_file = config_file + self.loader = 'cally.cli.config.loaders.service' + self.cally_type = 'CallyService' @property def environment(self) -> Optional[str]: @@ -45,7 +50,7 @@ def settings(self): merge_enabled=True, core_loaders=[], loaders=[ - 'cally.cli.config.loader', + self.loader, ], validators=BASE_CALLY_CONFIG, cally_env=self.environment, @@ -53,11 +58,13 @@ def settings(self): ) return self._settings - def as_dataclass(self, cally_type='CallyService') -> cally_types.CallyService: - cls = getattr(cally_types, cally_type) + def as_dataclass( + self, + ) -> Union[cally_types.CallyService, cally_types.CallyEnvironment]: + cls = getattr(cally_types, self.cally_type) items = { x.name: getattr(self.settings, x.name) - for x in fields(cally_types.CallyStackService) + for x in fields(cls) if x.name in self.settings } return cls(**items) @@ -95,3 +102,12 @@ def service_options(func): for option in reversed(options): func = option(func) return func + + +def pass_stack_obj(f): + @click.pass_obj + def new_func(obj: CallyConfig, *args, **kwargs): + obj.cally_type = 'CallyStackService' + return f(obj, *args, **kwargs) + + return update_wrapper(new_func, f) diff --git a/src/cally/cli/config/loaders/__init__.py b/src/cally/cli/config/loaders/__init__.py new file mode 100644 index 0000000..19282c8 --- /dev/null +++ b/src/cally/cli/config/loaders/__init__.py @@ -0,0 +1,27 @@ +from dynaconf import LazySettings + + +def mixin_helper(obj: LazySettings, loaded: dict, service: dict) -> None: + mixins = service.pop('mixins', []) + # Resolution Order: Global Mixins, Env Mixins, Service + # The last to be loaded wins. + for mixin in [x.strip() for x in mixins if len(x.strip()) > 0]: + obj.update(loaded.get('mixins', {}).get(mixin, {})) + obj.update(loaded.get(obj.cally_env, {}).get('mixins', {}).get(mixin, {})) + obj.update(service) + + +def envvar_helper(obj: LazySettings) -> None: + # Environment Variables + prefix: str = obj.envvar_prefix_for_dynaconf + remove = len(prefix) + 1 # PREFIX_ lenth + for key, val in obj.environ.items(): + if not key.startswith(prefix): + continue + if key in {f'{prefix}_SERVICE', f'{prefix}_ENVIRONMENT'}: + continue + obj.update({key[remove:].lower(): val}) + + # Clear out Service/Environment + obj.unset(f'{prefix}_ENV') + obj.unset(f'{prefix}_SERVICE') diff --git a/src/cally/cli/config/loaders/environment.py b/src/cally/cli/config/loaders/environment.py new file mode 100644 index 0000000..07193b2 --- /dev/null +++ b/src/cally/cli/config/loaders/environment.py @@ -0,0 +1,53 @@ +from pathlib import Path + +import yaml +from dynaconf import LazySettings + +from . import envvar_helper, mixin_helper + +try: + from cally.idp.defaults import DEFAULTS as IDP_DEFAULTS # type: ignore +except ModuleNotFoundError: + IDP_DEFAULTS: dict = {} # type: ignore[no-redef] + + +def load(obj: LazySettings, *args, **kwargs) -> None: # noqa: ARG001 + """ + Load a cally yaml file, with a resolution order of defaults, environment + defaults, service, then environment variables. + + cally.yml + ```yaml + defaults: + providers: + example: + foo: bar + dev: + defaults: + providers: + random: + alias: cats + services: + example-service: + stack_vars: + foo: bar + """ + config_file = Path(obj.settings_file_for_dynaconf) + loaded = {} + if config_file.exists(): + loaded = yaml.safe_load(config_file.read_text()) + + # Defaults + obj.update(IDP_DEFAULTS) + obj.update(loaded.get('defaults', {})) + if obj.cally_env is not None: + obj.update(environment=obj.cally_env) + obj.update(loaded.get(obj.cally_env, {}).get('defaults', {})) + + if obj.cally_env: + services = loaded.get(obj.cally_env, {}).get('services', {}) + for service in services.values(): + mixin_helper(obj, loaded, service) + + # Process Env Vars + envvar_helper(obj) diff --git a/src/cally/cli/config/loader.py b/src/cally/cli/config/loaders/service.py similarity index 62% rename from src/cally/cli/config/loader.py rename to src/cally/cli/config/loaders/service.py index 89d0a4b..254cc47 100644 --- a/src/cally/cli/config/loader.py +++ b/src/cally/cli/config/loaders/service.py @@ -3,6 +3,8 @@ import yaml from dynaconf import LazySettings +from . import envvar_helper, mixin_helper + try: from cally.idp.defaults import DEFAULTS as IDP_DEFAULTS # type: ignore except ModuleNotFoundError: @@ -51,24 +53,7 @@ def load(obj: LazySettings, *args, **kwargs) -> None: # noqa: ARG001 ) if service is None: service = {} - mixins = service.pop('mixins', []) - # Resolution Order: Global Mixins, Env Mixins, Service - # The last to be loaded wins. - for mixin in [x.strip() for x in mixins if len(x.strip()) > 0]: - obj.update(loaded.get('mixins', {}).get(mixin, {})) - obj.update(loaded.get(obj.cally_env, {}).get('mixins', {}).get(mixin, {})) - obj.update(service) - - # Environment Variables - prefix: str = obj.envvar_prefix_for_dynaconf - remove = len(prefix) + 1 # PREFIX_ lenth - for key, val in obj.environ.items(): - if not key.startswith(prefix): - continue - if key in {f'{prefix}_SERVICE', f'{prefix}_ENVIRONMENT'}: - continue - obj.update({key[remove:].lower(): val}) + mixin_helper(obj, loaded, service) - # Clear out Service/Environment - obj.unset(f'{prefix}_ENV') - obj.unset(f'{prefix}_SERVICE') + # Process Env Vars + envvar_helper(obj) diff --git a/src/cally/cli/config/types.py b/src/cally/cli/config/types.py index b09b5eb..d751c65 100644 --- a/src/cally/cli/config/types.py +++ b/src/cally/cli/config/types.py @@ -56,3 +56,8 @@ def get_stack_var(self, var: str, default: Optional[Any] = None) -> Any: def to_dict(self) -> dict: return asdict(self) + + +@dataclass +class CallyEnvironment: + environment: str diff --git a/tests/__init__.py b/tests/__init__.py index 22c972e..98b12ad 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -42,7 +42,7 @@ def reload_cally_modules(): modules = [ 'cally.cli', 'cally.cdk.stacks', - 'cally.cli.config.loader', + 'cally.cli.config.loaders.service', 'cally.cli.config', ] for module in modules: diff --git a/tests/cli/config/__init__.py b/tests/cli/config/__init__.py index 1fdf10d..13e3447 100644 --- a/tests/cli/config/__init__.py +++ b/tests/cli/config/__init__.py @@ -153,14 +153,14 @@ class CallyConfigTypeTests(CallyTestHarness): def test_as_cally_service(self): config = CallyConfig(config_file='blah.yml') config.environment = 'test' + config.cally_type = 'CallyService' config.service = 'test' self.assertIsInstance(config.as_dataclass(), cally_types.CallyService) def test_as_cally_stack_service(self): config = CallyConfig(config_file='blah.yml') config.environment = 'test' + config.cally_type = 'CallyStackService' config.service = 'test' config.settings.stack_type = 'CallyStack' - self.assertIsInstance( - config.as_dataclass('CallyStackService'), cally_types.CallyStackService - ) + self.assertIsInstance(config.as_dataclass(), cally_types.CallyStackService) From 26beb129bf263be338475656336d942181cc4339 Mon Sep 17 00:00:00 2001 From: Leon Wright Date: Sun, 14 Jul 2024 16:56:16 +0800 Subject: [PATCH 2/3] feat: more environment work This is more work --- src/cally/cli/commands/config.py | 4 +- src/cally/cli/commands/tf.py | 7 +-- src/cally/cli/config/__init__.py | 63 +-------------------- src/cally/cli/config/environment.py | 86 +++++++++++++++++++++++++++++ src/cally/cli/config/service.py | 71 ++++++++++++++++++++++++ src/cally/testing/__init__.py | 8 +-- tests/cli/config/__init__.py | 47 ++++++++-------- tests/cli/config/test_idp.py | 4 +- tests/cli/test_config.py | 7 +-- tests/cli/test_idp.py | 7 ++- tests/cli/test_tf.py | 7 +-- 11 files changed, 205 insertions(+), 106 deletions(-) create mode 100644 src/cally/cli/config/environment.py create mode 100644 src/cally/cli/config/service.py diff --git a/src/cally/cli/commands/config.py b/src/cally/cli/commands/config.py index 54a4c8a..3b87e96 100644 --- a/src/cally/cli/commands/config.py +++ b/src/cally/cli/commands/config.py @@ -1,7 +1,7 @@ import click import yaml -from ..config import CallyConfig, service_options +from ..config.service import CallyServiceConfig, service_options @click.group() @@ -12,7 +12,7 @@ def config() -> None: @click.command() @service_options @click.pass_obj -def print_service(config: CallyConfig): +def print_service(config: CallyServiceConfig): """Prints the service config as YAML""" click.secho(yaml.dump(config.settings.to_dict())) diff --git a/src/cally/cli/commands/tf.py b/src/cally/cli/commands/tf.py index 1968b63..46d5099 100644 --- a/src/cally/cli/commands/tf.py +++ b/src/cally/cli/commands/tf.py @@ -1,9 +1,8 @@ from pathlib import Path - import click -from ..config import CallyConfig, service_options, pass_stack_obj +from ..config.service import CallyServiceConfig, pass_stack_obj, service_options from ..tools import terraform @@ -15,7 +14,7 @@ def tf() -> None: @click.command(name='print') @service_options @pass_stack_obj -def print_template(config: CallyConfig): +def print_template(config: CallyServiceConfig): with terraform.Action(service=config.as_dataclass()) as action: click.secho(action.print()) @@ -29,7 +28,7 @@ def print_template(config: CallyConfig): ) @service_options @pass_stack_obj -def write_template(config: CallyConfig, output: Path): +def write_template(config: CallyServiceConfig, output: Path): with terraform.Action(service=config.as_dataclass()) as action: output.write_text(action.print()) click.secho(f'Template written to {output}') diff --git a/src/cally/cli/config/__init__.py b/src/cally/cli/config/__init__.py index 8937302..e7eed0f 100644 --- a/src/cally/cli/config/__init__.py +++ b/src/cally/cli/config/__init__.py @@ -1,7 +1,7 @@ +from abc import ABC, abstractmethod from dataclasses import fields -from functools import update_wrapper from pathlib import Path -from typing import Optional, Union +from typing import Union import click from dynaconf import Dynaconf @@ -10,34 +10,13 @@ from .validators import BASE_CALLY_CONFIG -class CallyConfig: +class CallyConfig(ABC): config_file: Path - loader: str cally_type: str - _environment: Optional[str] = None - _service: Optional[str] = None _settings: Dynaconf def __init__(self, config_file: Path) -> None: self.config_file = config_file - self.loader = 'cally.cli.config.loaders.service' - self.cally_type = 'CallyService' - - @property - def environment(self) -> Optional[str]: - return self._environment - - @environment.setter - def environment(self, value: str): - self._environment = value - - @property - def service(self) -> Optional[str]: - return self._service - - @service.setter - def service(self, value: str): - self._service = value @property def settings(self): @@ -75,39 +54,3 @@ def ctx_callback( ) -> Union[str, int]: setattr(ctx.obj, str(param.name), value) return value - - -def service_options(func): - """This decorator, can be used on any custom commands where you expect - a service and environment to be set. - """ - options = [ - click.option( - '--environment', - envvar='CALLY_ENVIRONMENT', - expose_value=False, - required=True, - help='Environment to operate within', - callback=ctx_callback, - ), - click.option( - '--service', - envvar='CALLY_SERVICE', - expose_value=False, - required=True, - help='Service name to retrieve config details', - callback=ctx_callback, - ), - ] - for option in reversed(options): - func = option(func) - return func - - -def pass_stack_obj(f): - @click.pass_obj - def new_func(obj: CallyConfig, *args, **kwargs): - obj.cally_type = 'CallyStackService' - return f(obj, *args, **kwargs) - - return update_wrapper(new_func, f) diff --git a/src/cally/cli/config/environment.py b/src/cally/cli/config/environment.py new file mode 100644 index 0000000..0c39d37 --- /dev/null +++ b/src/cally/cli/config/environment.py @@ -0,0 +1,86 @@ +from dataclasses import fields +from functools import update_wrapper +from pathlib import Path +from typing import Optional, Union + +import click +from dynaconf import Dynaconf + +from . import CallyConfig, ctx_callback +from . import types as cally_types +from .validators import BASE_CALLY_CONFIG + + +class CallyEnvironmentConfig(CallyConfig): + _environment: Optional[str] = None + _service: Optional[str] = None + _settings: Dynaconf + loader = 'cally.cli.config.loaders.environment' + cally_type = 'CallyEnvironment' + + def __init__(self, config_file: Path) -> None: + self.config_file = config_file + + @property + def environment(self) -> Optional[str]: + return self._environment + + @environment.setter + def environment(self, value: str): + self._environment = value + + @property + def settings(self): + if getattr(self, '_settings', None) is None: + self._settings = Dynaconf( + environments=False, + envvar_prefix='CALLY', + root_path=Path().cwd(), + settings_file=self.config_file, + merge_enabled=True, + core_loaders=[], + loaders=[ + self.loader, + ], + validators=BASE_CALLY_CONFIG, + cally_env=self.environment, + ) + return self._settings + + def as_dataclass( + self, + ) -> Union[cally_types.CallyService, cally_types.CallyEnvironment]: + cls = getattr(cally_types, self.cally_type) + items = { + x.name: getattr(self.settings, x.name) + for x in fields(cls) + if x.name in self.settings + } + return cls(**items) + + +def environment_options(func): + """This decorator, can be used on any custom commands where you expect + an environment to be set. + """ + options = [ + click.option( + '--environment', + envvar='CALLY_ENVIRONMENT', + expose_value=False, + required=True, + help='Environment to operate within', + callback=ctx_callback, + ), + ] + for option in reversed(options): + func = option(func) + return func + + +def pass_stack_obj(f): + @click.pass_obj + def new_func(obj: CallyEnvironmentConfig, *args, **kwargs): + return f(obj, *args, **kwargs) + + return update_wrapper(new_func, f) diff --git a/src/cally/cli/config/service.py b/src/cally/cli/config/service.py new file mode 100644 index 0000000..a9d38b7 --- /dev/null +++ b/src/cally/cli/config/service.py @@ -0,0 +1,71 @@ +from functools import update_wrapper +from pathlib import Path +from typing import Optional + +import click +from dynaconf import Dynaconf + +from . import CallyConfig, ctx_callback + + +class CallyServiceConfig(CallyConfig): + _environment: Optional[str] = None + _service: Optional[str] = None + _settings: Dynaconf + loader = 'cally.cli.config.loaders.service' + cally_type = 'CallyService' + + def __init__(self, config_file: Path) -> None: + self.config_file = config_file + + @property + def environment(self) -> Optional[str]: + return self._environment + + @environment.setter + def environment(self, value: str): + self._environment = value + + @property + def service(self) -> Optional[str]: + return self._service + + @service.setter + def service(self, value: str): + self._service = value + + +def service_options(func): + """This decorator, can be used on any custom commands where you expect + a service and environment to be set. + """ + options = [ + click.option( + '--environment', + envvar='CALLY_ENVIRONMENT', + expose_value=False, + required=True, + help='Environment to operate within', + callback=ctx_callback, + ), + click.option( + '--service', + envvar='CALLY_SERVICE', + expose_value=False, + required=True, + help='Service name to retrieve config details', + callback=ctx_callback, + ), + ] + for option in reversed(options): + func = option(func) + return func + + +def pass_stack_obj(f): + @click.pass_obj + def new_func(obj: CallyConfig, *args, **kwargs): + obj.cally_type = 'CallyStackService' + return f(obj, *args, **kwargs) + + return update_wrapper(new_func, f) diff --git a/src/cally/testing/__init__.py b/src/cally/testing/__init__.py index 14c4ee3..c63fe9b 100644 --- a/src/cally/testing/__init__.py +++ b/src/cally/testing/__init__.py @@ -6,7 +6,7 @@ from unittest import TestCase, mock from cally.cdk import CallyStack -from cally.cli.config import CallyConfig +from cally.cli.config.service import CallyServiceConfig from .constants import HOME_ENVS from .exceptions import CallyTestingTestdataError @@ -59,10 +59,10 @@ def testdata(self) -> Path: return self._testdata @staticmethod - def get_cally_config(service='test', environment='cally') -> CallyConfig: - """Returns a CallyConfig object, with a service and environment pre-configured as + def get_cally_config(service='test', environment='cally') -> CallyServiceConfig: + """Returns a CallyServiceConfig object, with a service and environment pre-configured as 'test' and 'cally'""" - config = CallyConfig(config_file=Path('not-required.yaml')) + config = CallyServiceConfig(config_file=Path('not-required.yaml')) config.service = service config.environment = environment return config diff --git a/tests/cli/config/__init__.py b/tests/cli/config/__init__.py index 13e3447..ed87189 100644 --- a/tests/cli/config/__init__.py +++ b/tests/cli/config/__init__.py @@ -1,56 +1,57 @@ import os from unittest import mock +from cally.cli.config import types as cally_types +from cally.cli.config.service import CallyServiceConfig from dynaconf import ValidationError -from cally.cli.config import CallyConfig -from cally.cli.config import types as cally_types + from ... import CallyTestHarness -class CallyConfigTests(CallyTestHarness): +class CallyServiceConfigTests(CallyTestHarness): def test_service_name(self): - config = CallyConfig(config_file='blah.yml') + config = CallyServiceConfig(config_file='blah.yml') config.service = 'test' self.assertEqual(config.settings.name, 'test') def test_empty_service(self): - config = CallyConfig(config_file='blah.yml') + config = CallyServiceConfig(config_file='blah.yml') config.environment = 'empty' config.service = 'empty' data = {'ENVIRONMENT': 'empty', 'NAME': 'empty'} self.assertDictEqual(config.settings.to_dict(), data) def test_environment(self): - config = CallyConfig(config_file='blah.yml') + config = CallyServiceConfig(config_file='blah.yml') config.environment = 'test' self.assertEqual(config.settings.environment, 'test') @mock.patch.dict(os.environ, {"CALLY_SERVICE": "ignored"}) def test_service_name_envvar_ignored(self): - config = CallyConfig(config_file='blah.yml') + config = CallyServiceConfig(config_file='blah.yml') config.service = 'test' self.assertEqual(config.settings.name, 'test') @mock.patch.dict(os.environ, {"CALLY_ENVIRONMENT": "ignored"}) def test_environment_envvar_ignored(self): - config = CallyConfig(config_file='blah.yml') + config = CallyServiceConfig(config_file='blah.yml') config.environment = 'test' self.assertEqual(config.settings.environment, 'test') def test_defaults(self): - config = CallyConfig(self.get_test_file('config/defaults.yaml')) + config = CallyServiceConfig(self.get_test_file('config/defaults.yaml')) data = {'PROVIDERS': {'test': {'foo': 'bar'}}} self.assertDictEqual(config.settings.to_dict(), data) def test_defaults_with_env(self): - config = CallyConfig(self.get_test_file('config/defaults.yaml')) + config = CallyServiceConfig(self.get_test_file('config/defaults.yaml')) config.environment = 'harness' data = {'ENVIRONMENT': 'harness', 'PROVIDERS': {'test': {'foo': 'not-bar'}}} self.assertDictEqual(config.settings.to_dict(), data) def test_defaults_with_service(self): - config = CallyConfig(self.get_test_file('config/defaults.yaml')) + config = CallyServiceConfig(self.get_test_file('config/defaults.yaml')) config.environment = 'harness' config.service = 'defaults' data = { @@ -62,7 +63,7 @@ def test_defaults_with_service(self): @mock.patch.dict(os.environ, {"CALLY_PROVIDERS__TEST__FOO": "totally-not-bar"}) def test_defaults_with_envvar_override(self): - config = CallyConfig(self.get_test_file('config/defaults.yaml')) + config = CallyServiceConfig(self.get_test_file('config/defaults.yaml')) config.environment = 'harness' config.service = 'defaults' data = { @@ -73,7 +74,7 @@ def test_defaults_with_envvar_override(self): self.assertDictEqual(config.settings.to_dict(), data) def test_mixin_service_win(self): - config = CallyConfig(self.get_test_file('config/mixins.yaml')) + config = CallyServiceConfig(self.get_test_file('config/mixins.yaml')) config.environment = 'harness' config.service = 'mixy-m-toasus-service' data = { @@ -84,7 +85,7 @@ def test_mixin_service_win(self): self.assertDictEqual(config.settings.to_dict(), data) def test_mixin_env_win(self): - config = CallyConfig(self.get_test_file('config/mixins.yaml')) + config = CallyServiceConfig(self.get_test_file('config/mixins.yaml')) config.environment = 'harness' config.service = 'mixy-m-toasus-env' data = { @@ -95,7 +96,7 @@ def test_mixin_env_win(self): self.assertDictEqual(config.settings.to_dict(), data) def test_mixin_combined(self): - config = CallyConfig(self.get_test_file('config/mixins.yaml')) + config = CallyServiceConfig(self.get_test_file('config/mixins.yaml')) config.environment = 'harness' config.service = 'mixy-m-toasus-combine' data = { @@ -106,9 +107,9 @@ def test_mixin_combined(self): self.assertDictEqual(config.settings.to_dict(), data) -class CallyConfigValidationTests(CallyTestHarness): +class CallyServiceConfigValidationTests(CallyTestHarness): def test_name_upper_raises(self): - config = CallyConfig(self.get_test_file('config/validation.yaml')) + config = CallyServiceConfig(self.get_test_file('config/validation.yaml')) config.environment = 'harness' config.service = 'INVALID-name' with self.assertRaises(ValidationError) as context: @@ -116,7 +117,7 @@ def test_name_upper_raises(self): self.assertEqual('Name must be lowercase', str(context.exception)) def test_environment_raises(self): - config = CallyConfig(config_file='blah.yaml') + config = CallyServiceConfig(config_file='blah.yaml') config.environment = 10 config.service = 'name' with self.assertRaises(ValidationError) as context: @@ -127,7 +128,7 @@ def test_environment_raises(self): ) def test_stack_vars_not_dict_raises(self): - config = CallyConfig(self.get_test_file('config/validation.yaml')) + config = CallyServiceConfig(self.get_test_file('config/validation.yaml')) config.environment = 'harness' config.service = 'invalid-stack-vars' with self.assertRaises(ValidationError) as context: @@ -138,7 +139,7 @@ def test_stack_vars_not_dict_raises(self): ) def test_providers_not_dict_raises(self): - config = CallyConfig(self.get_test_file('config/validation.yaml')) + config = CallyServiceConfig(self.get_test_file('config/validation.yaml')) config.environment = 'harness' config.service = 'invalid-providers' with self.assertRaises(ValidationError) as context: @@ -149,16 +150,16 @@ def test_providers_not_dict_raises(self): ) -class CallyConfigTypeTests(CallyTestHarness): +class CallyServiceConfigTypeTests(CallyTestHarness): def test_as_cally_service(self): - config = CallyConfig(config_file='blah.yml') + config = CallyServiceConfig(config_file='blah.yml') config.environment = 'test' config.cally_type = 'CallyService' config.service = 'test' self.assertIsInstance(config.as_dataclass(), cally_types.CallyService) def test_as_cally_stack_service(self): - config = CallyConfig(config_file='blah.yml') + config = CallyServiceConfig(config_file='blah.yml') config.environment = 'test' config.cally_type = 'CallyStackService' config.service = 'test' diff --git a/tests/cli/config/test_idp.py b/tests/cli/config/test_idp.py index d72406c..bd85f0d 100644 --- a/tests/cli/config/test_idp.py +++ b/tests/cli/config/test_idp.py @@ -1,4 +1,4 @@ -from cally.cli.config import CallyConfig +from cally.cli.config.service import CallyServiceConfig from ... import CallyIdpTestHarness @@ -6,7 +6,7 @@ class CallyIdpConfigTests(CallyIdpTestHarness): def test_idp_defaults(self): - config = CallyConfig(config_file='blah.yaml') + config = CallyServiceConfig(config_file='blah.yaml') config.environment = 'harness' config.service = 'idp-defaults-ya' data = { diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index cb88bff..9aa38b5 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -1,10 +1,9 @@ import os from unittest import mock -from click.testing import CliRunner - from cally.cli.commands.config import print_service -from cally.cli.config import CallyConfig +from cally.cli.config.service import CallyServiceConfig +from click.testing import CliRunner from .. import CallyTestHarness @@ -15,7 +14,7 @@ def test_print_service_basic(self): result = CliRunner().invoke( print_service, ['--service', 'test-cli', '--environment', 'test'], - obj=CallyConfig(config_file='blah.yaml'), + obj=CallyServiceConfig(config_file='blah.yaml'), ) self.assertEqual(result.exit_code, 0) self.assertEqual( diff --git a/tests/cli/test_idp.py b/tests/cli/test_idp.py index 08849f7..f8a283b 100644 --- a/tests/cli/test_idp.py +++ b/tests/cli/test_idp.py @@ -1,7 +1,8 @@ -from cally import cli -from cally.cli.config import CallyConfig from click.testing import CliRunner +from cally import cli +from cally.cli.config.service import CallyServiceConfig + from .. import CallyIdpTestHarness @@ -11,7 +12,7 @@ def test_example(self): result = CliRunner().invoke( cli.cally, ['example', 'hello', 'World'], - obj=CallyConfig(config_file='blah.yaml'), + obj=CallyServiceConfig(config_file='blah.yaml'), ) self.assertEqual(result.exit_code, 0) self.assertEqual(result.output, 'Hello World\n') diff --git a/tests/cli/test_tf.py b/tests/cli/test_tf.py index edcaa8b..6895d2a 100644 --- a/tests/cli/test_tf.py +++ b/tests/cli/test_tf.py @@ -2,10 +2,9 @@ import os from unittest import mock -from click.testing import CliRunner - from cally.cli.commands.tf import tf -from cally.cli.config import CallyConfig +from cally.cli.config.service import CallyServiceConfig +from click.testing import CliRunner from .. import CallyTestHarness @@ -17,7 +16,7 @@ def test_empty_print(self): result = CliRunner().invoke( tf, ['print', '--service', 'test', '--environment', 'test'], - obj=CallyConfig(config_file='blah.yaml'), + obj=CallyServiceConfig(config_file='blah.yaml'), ) self.assertEqual(result.exit_code, 0) testdata = {"backend": {"local": {"path": "state/test/test"}}} From 2348fb042e30bc695f381ffb1518ab9fbbd91bbc Mon Sep 17 00:00:00 2001 From: Leon Wright Date: Sat, 28 Sep 2024 13:03:38 +0930 Subject: [PATCH 3/3] feat: iterate environments --- cally.code-workspace | 1 + src/cally/cdk/__init__.py | 2 +- src/cally/cli/__init__.py | 11 +-- src/cally/cli/commands/config.py | 14 ++- src/cally/cli/commands/tf.py | 15 ++- src/cally/cli/config/__init__.py | 85 +++++++++++------ .../cli/config/{types.py => config_types.py} | 19 +++- src/cally/cli/config/environment.py | 94 ++++--------------- src/cally/cli/config/loaders/environment.py | 6 +- src/cally/cli/config/mixins.py | 25 +++++ src/cally/cli/config/options.py | 48 ++++++++++ src/cally/cli/config/service.py | 80 ++++------------ src/cally/cli/config/terraform_service.py | 43 +++++++++ src/cally/cli/exceptions.py | 2 + src/cally/cli/tools/terraform.py | 2 +- src/cally/testing/__init__.py | 24 ++++- tests/__init__.py | 2 +- tests/cli/config/__init__.py | 8 +- tests/cli/config/test_idp.py | 3 +- tests/cli/config/test_types.py | 2 +- tests/cli/test_config.py | 5 +- tests/cli/test_idp.py | 2 +- tests/cli/test_tf.py | 5 +- 23 files changed, 291 insertions(+), 207 deletions(-) rename src/cally/cli/config/{types.py => config_types.py} (78%) create mode 100644 src/cally/cli/config/mixins.py create mode 100644 src/cally/cli/config/options.py create mode 100644 src/cally/cli/config/terraform_service.py create mode 100644 src/cally/cli/exceptions.py diff --git a/cally.code-workspace b/cally.code-workspace index a4f60f5..4569e1a 100644 --- a/cally.code-workspace +++ b/cally.code-workspace @@ -12,6 +12,7 @@ "**/.mypy_cache": true, "**/.ruff_cache": true, "**/*egg*": true, + "**/venv": true }, "files.watcherExclude": { "**/.pytest_cache": true, diff --git a/src/cally/cdk/__init__.py b/src/cally/cdk/__init__.py index 7120470..d5de86c 100644 --- a/src/cally/cdk/__init__.py +++ b/src/cally/cdk/__init__.py @@ -16,7 +16,7 @@ from constructs import Construct if TYPE_CHECKING: - from cally.cli.config.types import CallyStackService + from cally.cli.config.config_types import CallyStackService @dataclass diff --git a/src/cally/cli/__init__.py b/src/cally/cli/__init__.py index 987c666..6e97f25 100644 --- a/src/cally/cli/__init__.py +++ b/src/cally/cli/__init__.py @@ -31,20 +31,11 @@ def get_commands(package_name: str) -> List: @click.group() -@click.option( - '--config', - type=click.Path(path_type=Path), - default=Path(Path.cwd(), 'cally.yaml'), - envvar='CALLY_CONFIG', - help='Path to the project config file', -) @click.version_option(__version__) -@click.pass_context -def cally(ctx: click.Context, config: Path) -> None: +def cally() -> None: """ Top level click command group for Cally """ - ctx.obj = CallyConfig(config_file=config) commands = get_commands('cally.cli.commands') diff --git a/src/cally/cli/commands/config.py b/src/cally/cli/commands/config.py index 3b87e96..7815550 100644 --- a/src/cally/cli/commands/config.py +++ b/src/cally/cli/commands/config.py @@ -1,7 +1,8 @@ import click import yaml -from ..config.service import CallyServiceConfig, service_options +from ..config.environment import CallyEnvironmentCommand, CallyEnvironmentConfig +from ..config.service import CallyServiceCommand, CallyServiceConfig @click.group() @@ -9,12 +10,19 @@ def config() -> None: pass -@click.command() -@service_options +@click.command(cls=CallyServiceCommand()) @click.pass_obj def print_service(config: CallyServiceConfig): """Prints the service config as YAML""" click.secho(yaml.dump(config.settings.to_dict())) +@click.command(cls=CallyEnvironmentCommand()) +@click.pass_obj +def print_services(config: CallyEnvironmentConfig): + """Prints all the services in an environment config as YAML""" + click.secho(yaml.dump(config.settings.to_dict())) + + config.add_command(print_service) +config.add_command(print_services) diff --git a/src/cally/cli/commands/tf.py b/src/cally/cli/commands/tf.py index 46d5099..a0bbab6 100644 --- a/src/cally/cli/commands/tf.py +++ b/src/cally/cli/commands/tf.py @@ -2,7 +2,8 @@ import click -from ..config.service import CallyServiceConfig, pass_stack_obj, service_options +from ..config.config_types import CallyStackService +from ..config.terraform_service import CallyStackServiceCommand, pass_stack_obj from ..tools import terraform @@ -11,11 +12,10 @@ def tf() -> None: pass -@click.command(name='print') -@service_options +@click.command(name='print', cls=CallyStackServiceCommand()) @pass_stack_obj -def print_template(config: CallyServiceConfig): - with terraform.Action(service=config.as_dataclass()) as action: +def print_template(config: CallyStackService): + with terraform.Action(service=config) as action: click.secho(action.print()) @@ -26,10 +26,9 @@ def print_template(config: CallyServiceConfig): default=Path(Path.cwd(), 'cdk.tf.json'), help='Output path for the terraform json', ) -@service_options @pass_stack_obj -def write_template(config: CallyServiceConfig, output: Path): - with terraform.Action(service=config.as_dataclass()) as action: +def write_template(config: CallyStackService, output: Path): + with terraform.Action(service=config) as action: output.write_text(action.print()) click.secho(f'Template written to {output}') diff --git a/src/cally/cli/config/__init__.py b/src/cally/cli/config/__init__.py index e7eed0f..93c1914 100644 --- a/src/cally/cli/config/__init__.py +++ b/src/cally/cli/config/__init__.py @@ -1,22 +1,39 @@ -from abc import ABC, abstractmethod -from dataclasses import fields from pathlib import Path -from typing import Union +from typing import Callable, ClassVar, Generic, List, Optional, TypeVar import click from dynaconf import Dynaconf -from . import types as cally_types +from . import config_types +from .options import CALLY_CONFIG_OPTIONS from .validators import BASE_CALLY_CONFIG +T = TypeVar( + "T", + config_types.CallyEnvironment, + config_types.CallyProject, + config_types.CallyService, + config_types.CallyStackService, +) -class CallyConfig(ABC): - config_file: Path - cally_type: str + +class CallyConfig(Generic[T]): + CALLY_TYPE: ClassVar[Callable] + _config_file: Path + _config: T _settings: Dynaconf - def __init__(self, config_file: Path) -> None: - self.config_file = config_file + @property + def config_file(self) -> Optional[Path]: + return self._config_file + + @config_file.setter + def config_file(self, value: Path): + self._config_file = value + + @property + def _settings_kwargs(self) -> dict: + return {'loaders': []} @property def settings(self): @@ -28,29 +45,37 @@ def settings(self): settings_file=self.config_file, merge_enabled=True, core_loaders=[], - loaders=[ - self.loader, - ], validators=BASE_CALLY_CONFIG, - cally_env=self.environment, - cally_service=self.service, + **self._settings_kwargs, ) return self._settings - def as_dataclass( + @property + def config( self, - ) -> Union[cally_types.CallyService, cally_types.CallyEnvironment]: - cls = getattr(cally_types, self.cally_type) - items = { - x.name: getattr(self.settings, x.name) - for x in fields(cls) - if x.name in self.settings - } - return cls(**items) - - -def ctx_callback( - ctx: click.Context, param: click.Parameter, value: Union[str, int] -) -> Union[str, int]: - setattr(ctx.obj, str(param.name), value) - return value + ) -> T: + if getattr(self, '_config', None) is None: + self._config = self.CALLY_TYPE( + **config_types.filter_dataclass_props(self.settings, self.CALLY_TYPE) + ) + return self._config + + +class CallyConfigContext(click.Context): + def __init__(self, *args, **kwargs) -> None: + if kwargs.get('obj', None) is None: + kwargs.update(obj=CallyConfig()) + super().__init__(*args, **kwargs) + + +class CallyCommandClass(click.Command): + context_class = CallyConfigContext + default_cally_options: List[click.Option] = CALLY_CONFIG_OPTIONS + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.params.extend(self.default_cally_options) + + +def CallyCommand(): # noqa: N802 + return CallyCommandClass diff --git a/src/cally/cli/config/types.py b/src/cally/cli/config/config_types.py similarity index 78% rename from src/cally/cli/config/types.py rename to src/cally/cli/config/config_types.py index d751c65..d1e485c 100644 --- a/src/cally/cli/config/types.py +++ b/src/cally/cli/config/config_types.py @@ -1,9 +1,22 @@ -from dataclasses import asdict, dataclass, field +from dataclasses import asdict, dataclass, field, fields, is_dataclass from typing import Any, Optional +from ..exceptions import ObjectNotDataclassError + + +def filter_dataclass_props(data: dict, cls: Any) -> dict: + if not is_dataclass(cls): + raise ObjectNotDataclassError(f'{cls} is not of type dataclass') + return {x.name: data.get(x.name) for x in fields(cls) if x.name in data} + + +@dataclass +class CallyProject: + pass + @dataclass -class CallyService: +class CallyService(CallyProject): name: str environment: str @@ -59,5 +72,5 @@ def to_dict(self) -> dict: @dataclass -class CallyEnvironment: +class CallyEnvironment(CallyProject): environment: str diff --git a/src/cally/cli/config/environment.py b/src/cally/cli/config/environment.py index 0c39d37..97bff98 100644 --- a/src/cally/cli/config/environment.py +++ b/src/cally/cli/config/environment.py @@ -1,86 +1,32 @@ -from dataclasses import fields -from functools import update_wrapper -from pathlib import Path -from typing import Optional, Union - import click -from dynaconf import Dynaconf - -from . import CallyConfig, ctx_callback -from . import types as cally_types -from .validators import BASE_CALLY_CONFIG - -class CallyEnvironmentConfig(CallyConfig): - _environment: Optional[str] = None - _service: Optional[str] = None - _settings: Dynaconf - loader = 'cally.cli.config.loaders.environment' - cally_type = 'CallyEnvironment' +from . import CallyCommandClass, CallyConfig, config_types, mixins +from .options import CALLY_ENVIRONMENT_OPTIONS - def __init__(self, config_file: Path) -> None: - self.config_file = config_file - @property - def environment(self) -> Optional[str]: - return self._environment - - @environment.setter - def environment(self, value: str): - self._environment = value +class CallyEnvironmentConfig( + CallyConfig[config_types.CallyEnvironment], mixins.CallyEnvironment +): + CALLY_TYPE = config_types.CallyEnvironment @property - def settings(self): - if getattr(self, '_settings', None) is None: - self._settings = Dynaconf( - environments=False, - envvar_prefix='CALLY', - root_path=Path().cwd(), - settings_file=self.config_file, - merge_enabled=True, - core_loaders=[], - loaders=[ - self.loader, - ], - validators=BASE_CALLY_CONFIG, - cally_env=self.environment, - ) - return self._settings - - def as_dataclass( - self, - ) -> Union[cally_types.CallyService, cally_types.CallyEnvironment]: - cls = getattr(cally_types, self.cally_type) - items = { - x.name: getattr(self.settings, x.name) - for x in fields(cls) - if x.name in self.settings + def _settings_kwargs(self) -> dict: + return { + 'loaders': ['cally.cli.config.loaders.environment'], + 'cally_env': self.environment, } - return cls(**items) -def environment_options(func): - """This decorator, can be used on any custom commands where you expect - an environment to be set. - """ - options = [ - click.option( - '--environment', - envvar='CALLY_ENVIRONMENT', - expose_value=False, - required=True, - help='Environment to operate within', - callback=ctx_callback, - ), - ] - for option in reversed(options): - func = option(func) - return func +class CallyEnvironmentConfigContext(click.Context): + def __init__(self, *args, **kwargs) -> None: + if kwargs.get('obj', None) is None: + kwargs.update(obj=CallyEnvironmentConfig()) + super().__init__(*args, **kwargs) -def pass_stack_obj(f): - @click.pass_obj - def new_func(obj: CallyEnvironmentConfig, *args, **kwargs): - return f(obj, *args, **kwargs) +def CallyEnvironmentCommand(): # noqa: N802 + class CallyEnvironmentCommandClass(CallyCommandClass): + context_class = CallyEnvironmentConfigContext + default_cally_options = CALLY_ENVIRONMENT_OPTIONS - return update_wrapper(new_func, f) + return CallyEnvironmentCommandClass diff --git a/src/cally/cli/config/loaders/environment.py b/src/cally/cli/config/loaders/environment.py index 07193b2..9bb96ed 100644 --- a/src/cally/cli/config/loaders/environment.py +++ b/src/cally/cli/config/loaders/environment.py @@ -46,8 +46,10 @@ def load(obj: LazySettings, *args, **kwargs) -> None: # noqa: ARG001 if obj.cally_env: services = loaded.get(obj.cally_env, {}).get('services', {}) - for service in services.values(): - mixin_helper(obj, loaded, service) + for service, config in services.items(): + obj.update({'services': {service: config}}) + service_obj = obj.get('services', {}).get(service, {}) + mixin_helper(service_obj, loaded, config) # Process Env Vars envvar_helper(obj) diff --git a/src/cally/cli/config/mixins.py b/src/cally/cli/config/mixins.py new file mode 100644 index 0000000..50e271a --- /dev/null +++ b/src/cally/cli/config/mixins.py @@ -0,0 +1,25 @@ +from typing import Optional + + +class CallyEnvironment: + _environment: Optional[str] = None + + @property + def environment(self) -> Optional[str]: + return self._environment + + @environment.setter + def environment(self, value: str): + self._environment = value + + +class CallyService(CallyEnvironment): + _service: Optional[str] + + @property + def service(self) -> Optional[str]: + return self._service + + @service.setter + def service(self, value: str): + self._service = value diff --git a/src/cally/cli/config/options.py b/src/cally/cli/config/options.py new file mode 100644 index 0000000..99c7b51 --- /dev/null +++ b/src/cally/cli/config/options.py @@ -0,0 +1,48 @@ +from pathlib import Path +from typing import Union + +import click + + +def ctx_callback( + ctx: click.Context, param: click.Parameter, value: Union[str, int] +) -> Union[str, int]: + setattr(ctx.obj, str(param.name), value) + return value + + +CALLY_CONFIG_OPTIONS = [ + click.Option( + ['--config-file'], + type=click.Path(path_type=Path), + default=Path(Path.cwd(), 'cally.yaml'), + envvar='CALLY_CONFIG', + help='Path to the project config file', + expose_value=False, + callback=ctx_callback, + ) +] + +CALLY_ENVIRONMENT_OPTIONS = [ + *CALLY_CONFIG_OPTIONS, + click.Option( + ['--environment'], + envvar='CALLY_ENVIRONMENT', + expose_value=False, + required=True, + help='Environment to operate within', + callback=ctx_callback, + ), +] + +CALLY_SERVICE_OPTIONS = [ + *CALLY_ENVIRONMENT_OPTIONS, + click.Option( + ['--service'], + envvar='CALLY_SERVICE', + expose_value=False, + required=True, + help='Service name to retrieve config details', + callback=ctx_callback, + ), +] diff --git a/src/cally/cli/config/service.py b/src/cally/cli/config/service.py index a9d38b7..62ce55e 100644 --- a/src/cally/cli/config/service.py +++ b/src/cally/cli/config/service.py @@ -1,71 +1,31 @@ -from functools import update_wrapper -from pathlib import Path -from typing import Optional - import click -from dynaconf import Dynaconf - -from . import CallyConfig, ctx_callback - -class CallyServiceConfig(CallyConfig): - _environment: Optional[str] = None - _service: Optional[str] = None - _settings: Dynaconf - loader = 'cally.cli.config.loaders.service' - cally_type = 'CallyService' +from . import CallyCommandClass, CallyConfig, config_types, mixins +from .options import CALLY_SERVICE_OPTIONS - def __init__(self, config_file: Path) -> None: - self.config_file = config_file - @property - def environment(self) -> Optional[str]: - return self._environment - - @environment.setter - def environment(self, value: str): - self._environment = value +class CallyServiceConfig(CallyConfig[config_types.CallyService], mixins.CallyService): + CALLY_TYPE = config_types.CallyService @property - def service(self) -> Optional[str]: - return self._service - - @service.setter - def service(self, value: str): - self._service = value + def _settings_kwargs(self) -> dict: + return { + 'loaders': ['cally.cli.config.loaders.service'], + 'cally_env': self.environment, + 'cally_service': self.service, + } -def service_options(func): - """This decorator, can be used on any custom commands where you expect - a service and environment to be set. - """ - options = [ - click.option( - '--environment', - envvar='CALLY_ENVIRONMENT', - expose_value=False, - required=True, - help='Environment to operate within', - callback=ctx_callback, - ), - click.option( - '--service', - envvar='CALLY_SERVICE', - expose_value=False, - required=True, - help='Service name to retrieve config details', - callback=ctx_callback, - ), - ] - for option in reversed(options): - func = option(func) - return func +class CallyServiceConfigContext(click.Context): + def __init__(self, *args, **kwargs) -> None: + if kwargs.get('obj', None) is None: + kwargs.update(obj=CallyServiceConfig()) + super().__init__(*args, **kwargs) -def pass_stack_obj(f): - @click.pass_obj - def new_func(obj: CallyConfig, *args, **kwargs): - obj.cally_type = 'CallyStackService' - return f(obj, *args, **kwargs) +def CallyServiceCommand(): # noqa: N802 + class CallyServiceCommandClass(CallyCommandClass): + context_class = CallyServiceConfigContext + default_cally_options = CALLY_SERVICE_OPTIONS - return update_wrapper(new_func, f) + return CallyServiceCommandClass diff --git a/src/cally/cli/config/terraform_service.py b/src/cally/cli/config/terraform_service.py new file mode 100644 index 0000000..e87989e --- /dev/null +++ b/src/cally/cli/config/terraform_service.py @@ -0,0 +1,43 @@ +from functools import update_wrapper + +import click + +from . import CallyCommandClass, CallyConfig, config_types, mixins +from .options import CALLY_SERVICE_OPTIONS + + +class CallyStackServiceConfig( + CallyConfig[config_types.CallyStackService], mixins.CallyService +): + CALLY_TYPE = config_types.CallyStackService + + @property + def _settings_kwargs(self) -> dict: + return { + 'loaders': ['cally.cli.config.loaders.service'], + 'cally_env': self.environment, + 'cally_service': self.service, + } + + +class CallyStackServiceConfigContext(click.Context): + def __init__(self, *args, **kwargs) -> None: + if kwargs.get('obj', None) is None: + kwargs.update(obj=CallyStackServiceConfig()) + super().__init__(*args, **kwargs) + + +def CallyStackServiceCommand(): # noqa: N802 + class CallyStackServiceCommandClass(CallyCommandClass): + context_class = CallyStackServiceConfigContext + default_cally_options = CALLY_SERVICE_OPTIONS + + return CallyStackServiceCommandClass + + +def pass_stack_obj(f): + @click.pass_obj + def new_func(obj: CallyStackServiceConfig, *args, **kwargs): + return f(obj.config, *args, **kwargs) + + return update_wrapper(new_func, f) diff --git a/src/cally/cli/exceptions.py b/src/cally/cli/exceptions.py new file mode 100644 index 0000000..86fe8e2 --- /dev/null +++ b/src/cally/cli/exceptions.py @@ -0,0 +1,2 @@ +class ObjectNotDataclassError(Exception): + pass diff --git a/src/cally/cli/tools/terraform.py b/src/cally/cli/tools/terraform.py index 535e9b5..3a7874d 100644 --- a/src/cally/cli/tools/terraform.py +++ b/src/cally/cli/tools/terraform.py @@ -6,7 +6,7 @@ from cally.cdk import stacks -from ..config.types import CallyStackService +from ..config.config_types import CallyStackService class Action: diff --git a/src/cally/testing/__init__.py b/src/cally/testing/__init__.py index c63fe9b..c20eef7 100644 --- a/src/cally/testing/__init__.py +++ b/src/cally/testing/__init__.py @@ -7,6 +7,7 @@ from cally.cdk import CallyStack from cally.cli.config.service import CallyServiceConfig +from cally.cli.config.terraform_service import CallyStackServiceConfig from .constants import HOME_ENVS from .exceptions import CallyTestingTestdataError @@ -60,9 +61,26 @@ def testdata(self) -> Path: @staticmethod def get_cally_config(service='test', environment='cally') -> CallyServiceConfig: - """Returns a CallyServiceConfig object, with a service and environment pre-configured as - 'test' and 'cally'""" - config = CallyServiceConfig(config_file=Path('not-required.yaml')) + """ + Returns a CallyServiceConfig object, with a service and environment + pre-configured as 'test' and 'cally' + """ + config = CallyServiceConfig() + config.config_file = Path('not-required.yaml') + config.service = service + config.environment = environment + return config + + @staticmethod + def get_cally_stack_config( + service='test', environment='cally' + ) -> CallyStackServiceConfig: + """ + Returns a CallyStackServiceConfig object, with a service and environment + pre-configured as 'test' and 'cally' + """ + config = CallyStackServiceConfig() + config.config_file = Path('not-required.yaml') config.service = service config.environment = environment return config diff --git a/tests/__init__.py b/tests/__init__.py index 98b12ad..6a8bd9c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,7 +3,7 @@ from pathlib import Path from cally import testing -from cally.cli.config.types import CallyStackService +from cally.cli.config.config_types import CallyStackService class CallyTestHarness(testing.CallyTestHarness): diff --git a/tests/cli/config/__init__.py b/tests/cli/config/__init__.py index ed87189..8eacf58 100644 --- a/tests/cli/config/__init__.py +++ b/tests/cli/config/__init__.py @@ -1,10 +1,10 @@ import os from unittest import mock -from cally.cli.config import types as cally_types -from cally.cli.config.service import CallyServiceConfig from dynaconf import ValidationError +from cally.cli.config import config_types +from cally.cli.config.service import CallyServiceConfig from ... import CallyTestHarness @@ -156,7 +156,7 @@ def test_as_cally_service(self): config.environment = 'test' config.cally_type = 'CallyService' config.service = 'test' - self.assertIsInstance(config.as_dataclass(), cally_types.CallyService) + self.assertIsInstance(config.as_dataclass(), config_types.CallyService) def test_as_cally_stack_service(self): config = CallyServiceConfig(config_file='blah.yml') @@ -164,4 +164,4 @@ def test_as_cally_stack_service(self): config.cally_type = 'CallyStackService' config.service = 'test' config.settings.stack_type = 'CallyStack' - self.assertIsInstance(config.as_dataclass(), cally_types.CallyStackService) + self.assertIsInstance(config.as_dataclass(), config_types.CallyStackService) diff --git a/tests/cli/config/test_idp.py b/tests/cli/config/test_idp.py index bd85f0d..359e809 100644 --- a/tests/cli/config/test_idp.py +++ b/tests/cli/config/test_idp.py @@ -6,7 +6,8 @@ class CallyIdpConfigTests(CallyIdpTestHarness): def test_idp_defaults(self): - config = CallyServiceConfig(config_file='blah.yaml') + config = CallyServiceConfig() + config.config_file = 'blah.yaml' config.environment = 'harness' config.service = 'idp-defaults-ya' data = { diff --git a/tests/cli/config/test_types.py b/tests/cli/config/test_types.py index f407c69..ff6615d 100644 --- a/tests/cli/config/test_types.py +++ b/tests/cli/config/test_types.py @@ -1,4 +1,4 @@ -from cally.cli.config.types import CallyService, CallyStackService +from cally.cli.config.config_types import CallyService, CallyStackService from .. import CallyTestHarness diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index 9aa38b5..f5eeb83 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -1,9 +1,10 @@ import os from unittest import mock +from click.testing import CliRunner + from cally.cli.commands.config import print_service from cally.cli.config.service import CallyServiceConfig -from click.testing import CliRunner from .. import CallyTestHarness @@ -14,7 +15,7 @@ def test_print_service_basic(self): result = CliRunner().invoke( print_service, ['--service', 'test-cli', '--environment', 'test'], - obj=CallyServiceConfig(config_file='blah.yaml'), + obj=CallyServiceConfig(), ) self.assertEqual(result.exit_code, 0) self.assertEqual( diff --git a/tests/cli/test_idp.py b/tests/cli/test_idp.py index f8a283b..bda3920 100644 --- a/tests/cli/test_idp.py +++ b/tests/cli/test_idp.py @@ -12,7 +12,7 @@ def test_example(self): result = CliRunner().invoke( cli.cally, ['example', 'hello', 'World'], - obj=CallyServiceConfig(config_file='blah.yaml'), + obj=CallyServiceConfig(), ) self.assertEqual(result.exit_code, 0) self.assertEqual(result.output, 'Hello World\n') diff --git a/tests/cli/test_tf.py b/tests/cli/test_tf.py index 6895d2a..da2be6a 100644 --- a/tests/cli/test_tf.py +++ b/tests/cli/test_tf.py @@ -2,9 +2,10 @@ import os from unittest import mock +from click.testing import CliRunner + from cally.cli.commands.tf import tf from cally.cli.config.service import CallyServiceConfig -from click.testing import CliRunner from .. import CallyTestHarness @@ -16,7 +17,7 @@ def test_empty_print(self): result = CliRunner().invoke( tf, ['print', '--service', 'test', '--environment', 'test'], - obj=CallyServiceConfig(config_file='blah.yaml'), + obj=CallyServiceConfig(), ) self.assertEqual(result.exit_code, 0) testdata = {"backend": {"local": {"path": "state/test/test"}}}