Skip to content

Commit

Permalink
feat: Provider Builder
Browse files Browse the repository at this point in the history
Automatically instantiate providers into the stack as required,
including the ability to inject config from the service settings.
  • Loading branch information
techman83 committed Mar 18, 2024
1 parent 5d377fa commit 4c7295a
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 5 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/coverage-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ jobs:
cache: pip
- name: Install Cally test dependencies
run: pip install .[test]
- name: Restore Provider Packages
id: cache-providers
uses: actions/cache@v4
with:
path: build/random/dist/CallyProvidersRandom-3.6.0.tar.gz
key: cally-provider-random-3.6.0
- uses: actions/setup-node@v4
if: steps.cache-providers.outputs.cache-hit != 'true'
with:
node-version: "20"
- name: Install cdktf-cli and build
if: steps.cache-providers.outputs.cache-hit != 'true'
run: |
npm install cdktf-cli
echo "$(pwd)/node_modules/.bin/" >> $GITHUB_PATH
cally provider build --provider random --version 3.6.0
(cd build/random && python -m build)
- name: Install Provider Pacakge
run: pip install build/random/dist/CallyProvidersRandom-3.6.0.tar.gz
- name: Run Coverage
run: |
coverage run -m pytest
Expand Down
44 changes: 39 additions & 5 deletions src/cally/cdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from copy import deepcopy
from dataclasses import dataclass, make_dataclass
from importlib import import_module
from typing import TYPE_CHECKING, Any, List
from typing import TYPE_CHECKING, Any, Dict, List, Optional

from cdktf import (
App,
Expand Down Expand Up @@ -39,9 +39,12 @@ def __init__(self, identifier: str, **kwargs) -> None:
def __str__(self) -> str:
return f'${{{self.resource}.{self.attributes.id}.id}}'

def __getattr__(self, item: str) -> str:
def __getattr__(self, item: str) -> Optional[str]:
# TODO: This likely could use some improvement
if item.startswith('__jsii'):
return getattr(self._instantiated_resource, item)
if item == 'attributes':
return None
return f'${{{self.resource}.{self.attributes.id}.{item}}}'

def _get_attribute_default(self, name: str) -> Any:
Expand All @@ -59,7 +62,11 @@ def _build_attributes(self, identifier: str, **kwargs) -> CallyResourceAttribute
]
name = f'{self.__class__.__name__}CallyAttributes'
cls = make_dataclass(name, fields, bases=(CallyResourceAttributes,))
return cls(**{'id_': identifier, **kwargs})
# Some newer provider releases appear to use 'id_'
id_field = 'id'
if 'id_' in parameters:
id_field = 'id_'
return cls(**{id_field: identifier, **kwargs})

def construct_resource(self, scope: Construct, provider: TerraformProvider) -> None:
self.attributes.provider = provider
Expand All @@ -69,6 +76,7 @@ def construct_resource(self, scope: Construct, provider: TerraformProvider) -> N


class CallyStack:
_providers: Dict[str, TerraformProvider]
_resources: List[CallyResource]
service: 'CallyStackService'

Expand All @@ -81,6 +89,24 @@ def add_resource(self, resource: CallyResource) -> None:
def add_resources(self, resources: List[CallyResource]) -> None:
self.resources.extend(resources)

def get_provider(self, scope: Construct, provider: str) -> TerraformProvider:
if provider not in self.providers:
# google_beta -> GoogleBetaProvider
resource = f"{provider.capitalize().replace('_', '')}Provider"
module = import_module(f'cally.providers.{provider}.provider')
cls = getattr(module, resource)
self.providers.update(
{
provider: cls(
scope, id=provider, **self.service.providers.get(provider, {})
)
}
)
prov = self.providers.get(provider)
if prov is None:
raise ValueError("Provider instantion failed")
return prov

@property
def name(self) -> str:
return self.service.name
Expand All @@ -95,6 +121,12 @@ def resources(self) -> List[CallyResource]:
self._resources = []
return self._resources

@property
def providers(self) -> Dict[str, TerraformProvider]:
if getattr(self, '_providers', None) is None:
self._providers = {}
return self._providers

def synth_stack(self, outdir='cdktf.out'):
stack = self

Expand All @@ -104,8 +136,10 @@ def __init__(self, scope: Construct) -> None:
super().__init__(scope, stack.name)
# TODO: Build provider loader
for resource in stack.resources:
provider = TerraformProvider(self, 'test') # type: ignore
resource.construct_resource(self, provider=provider)
resource.construct_resource(
self,
provider=stack.get_provider(self, resource.provider),
)

LocalBackend(
self, path=f'state/{stack.name}.tfstate'
Expand Down
42 changes: 42 additions & 0 deletions tests/stacks/test_providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from contextlib import suppress
from unittest import skipUnless

from cally.cdk import CallyResource, CallyStack

from .. import CallyTfTestHarness

skip_tests = False
with suppress(ModuleNotFoundError):
from cally.providers.random import pet # noqa: F401

skip_tests = True


@skipUnless(skip_tests, "Random provider must be installed")
class CallyProviderTests(CallyTfTestHarness):
def test_provider_load(self):
stack = CallyStack(service=self.empty_service)

class Pet(CallyResource):
provider = 'random'
resource = 'pet'

stack.add_resource(Pet('random-pet-name'))
result = self.synth_stack(stack)
testdata = self.load_json_file('cdk/provider_load.json')
self.assertDictEqual(result.get('provider'), testdata.get('provider'))
self.assertDictEqual(result.get('resource'), testdata.get('resource'))

def test_provider_config(self):
self.empty_service.providers.update(random={'alias': 'foo'})
stack = CallyStack(service=self.empty_service)

class Pet(CallyResource):
provider = 'random'
resource = 'pet'

stack.add_resource(Pet('random-pet-name'))
result = self.synth_stack(stack)
self.assertEqual(
result.get('provider', {}).get('random', {})[0].get('alias', ''), 'foo'
)
20 changes: 20 additions & 0 deletions tests/testdata/cdk/provider_load.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"provider": {
"random": [
{}
]
},
"resource": {
"random_pet": {
"random-pet-name": {
"//": {
"metadata": {
"path": "test/random-pet-name",
"uniqueId": "random-pet-name"
}
},
"provider": "random"
}
}
}
}

0 comments on commit 4c7295a

Please sign in to comment.