Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit 0713e38

Browse files
CloudFormation: Store resource scaffolding in 'generated' directory to separate auto-generated code from human-written code.
1 parent 532d755 commit 0713e38

12 files changed

Lines changed: 329 additions & 144 deletions

File tree

localstack-core/localstack/__init__.py

Whitespace-only changes.

localstack-core/localstack/services/cloudformation/provider_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ def resource_tags_to_remove_or_update(
297297

298298
# LocalStack specific utilities
299299
def get_schema_path(file_path: Path) -> dict:
300-
file_name_base = file_path.name.removesuffix(".py").removesuffix(".py.enc")
300+
file_name_base = (
301+
file_path.name.removesuffix("_base.py").removesuffix(".py").removesuffix(".py.enc")
302+
)
301303
with Path(file_path).parent.joinpath(f"{file_name_base}.schema.json").open() as fd:
302304
return json.load(fd)

localstack-core/localstack/services/cloudformation/scaffolding/__main__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ class FileType(Enum):
280280
# service code
281281
plugin = auto()
282282
provider = auto()
283+
provider_base = auto()
283284

284285
# test files
285286
integration_test = auto()
@@ -325,6 +326,7 @@ def render(
325326
template_mapping = {
326327
FileType.plugin: "plugin_template.py.j2",
327328
FileType.provider: "provider_template.py.j2",
329+
FileType.provider_base: "provider_base_template.py.j2",
328330
FileType.getatt_test: "test_getatt_template.py.j2",
329331
FileType.integration_test: "test_integration_template.py.j2",
330332
# FileType.cloudcontrol_test: "test_cloudcontrol_template.py.j2",
@@ -352,6 +354,11 @@ def render(
352354
)
353355
)
354356
case FileType.provider:
357+
kwargs["service"] = resource_name.python_compatible_service_name.lower()
358+
kwargs["lower_resource"] = resource_name.resource.lower()
359+
kwargs["pro"] = self.pro
360+
pass
361+
case FileType.provider_base:
355362
property_ir = generate_ir_for_type(
356363
[self.schema],
357364
resource_name.full_name,
@@ -588,18 +595,28 @@ def __init__(
588595
"resource_providers",
589596
f"{self.resource_name.namespace.lower()}_{self.resource_name.service.lower()}_{self.resource_name.resource.lower()}.py",
590597
),
598+
FileType.provider_base: root_dir(self.pro).joinpath(
599+
*base_path,
600+
"services",
601+
self.resource_name.python_compatible_service_name.lower(),
602+
"resource_providers",
603+
"generated",
604+
f"{self.resource_name.namespace.lower()}_{self.resource_name.service.lower()}_{self.resource_name.resource.lower()}_base.py",
605+
),
591606
FileType.plugin: root_dir(self.pro).joinpath(
592607
*base_path,
593608
"services",
594609
self.resource_name.python_compatible_service_name.lower(),
595610
"resource_providers",
611+
"generated",
596612
f"{self.resource_name.namespace.lower()}_{self.resource_name.service.lower()}_{self.resource_name.resource.lower()}_plugin.py",
597613
),
598614
FileType.schema: root_dir(self.pro).joinpath(
599615
*base_path,
600616
"services",
601617
self.resource_name.python_compatible_service_name.lower(),
602618
"resource_providers",
619+
"generated",
603620
f"aws_{self.resource_name.service.lower()}_{self.resource_name.resource.lower()}.schema.json",
604621
),
605622
FileType.integration_test: tests_root_dir(self.pro).joinpath(
@@ -656,6 +673,10 @@ def write(self, file_type: FileType, contents: str):
656673
self.ensure_python_init_files(destination_path)
657674
self.write_text(contents, file_destination)
658675
self.console.print(f"Written provider to {file_destination}")
676+
case FileType.provider_base:
677+
self.ensure_python_init_files(destination_path)
678+
self.write_text(contents, file_destination)
679+
self.console.print(f"Written provider base to {file_destination}")
659680
case FileType.plugin:
660681
self.ensure_python_init_files(destination_path)
661682
self.write_text(contents, file_destination)
@@ -777,6 +798,9 @@ def print(self):
777798
case FileType.provider:
778799
self.printer.print("\n[underline]Provider template[/underline]\n")
779800
self.printer.print(Syntax(self.contents, "python"))
801+
case FileType.provider_base:
802+
self.printer.print("\n[underline]Provider base template[/underline]\n")
803+
self.printer.print(Syntax(self.contents, "python"))
780804
case FileType.plugin:
781805
self.printer.print("\n[underline]Plugin[/underline]\n")
782806
self.printer.print(Syntax(self.contents, "python"))

localstack-core/localstack/services/cloudformation/scaffolding/templates/plugin_template.py.j2

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#
2+
# AUTOGENERATED FILE - DO NOT EDIT
3+
#
14
from typing import Optional, Type
25

36
from localstack.services.cloudformation.resource_provider import ResourceProvider
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# LocalStack Resource Provider Base Class Scaffolding {{ scaffolding_version }}
2+
#
3+
# AUTOGENERATED FILE - DO NOT EDIT
4+
#
5+
6+
from __future__ import annotations
7+
8+
from abc import ABC, abstractmethod
9+
from pathlib import Path
10+
from typing import Optional, TypedDict
11+
12+
import localstack.services.cloudformation.provider_utils as util
13+
from localstack.services.cloudformation.resource_provider import (
14+
ProgressEvent,
15+
ResourceProvider,
16+
ResourceRequest,
17+
)
18+
19+
{{ provider_properties }}
20+
21+
REPEATED_INVOCATION = "repeated_invocation"
22+
23+
class {{ resource }}ProviderBase(ResourceProvider[{{ resource }}Properties], ABC):
24+
25+
TYPE = "{{ name }}" # Autogenerated. Don't change
26+
SCHEMA = util.get_schema_path(Path(__file__)) # Autogenerated. Don't change
27+
28+
@abstractmethod
29+
def create(
30+
self,
31+
request: ResourceRequest[{{ resource }}Properties],
32+
) -> ProgressEvent[{{ resource }}Properties]:
33+
"""
34+
Create a new resource.
35+
36+
{% if primary_identifier -%}
37+
Primary identifier fields:
38+
{%- for property in primary_identifier %}
39+
- {{ property }}
40+
{%- endfor %}
41+
{%- endif %}
42+
43+
{% if required_properties -%}
44+
Required properties:
45+
{%- for property in required_properties %}
46+
- {{ property }}
47+
{%- endfor %}
48+
{%- endif %}
49+
50+
{% if create_only_properties -%}
51+
Create-only properties:
52+
{%- for property in create_only_properties %}
53+
- {{ property }}
54+
{%- endfor %}
55+
{%- endif %}
56+
57+
{% if read_only_properties -%}
58+
Read-only properties:
59+
{%- for property in read_only_properties %}
60+
- {{ property }}
61+
{%- endfor %}
62+
{%- endif %}
63+
64+
{% if create_permissions -%}
65+
IAM permissions required:
66+
{%- for permission in create_permissions %}
67+
- {{ permission }}
68+
{%- endfor -%}
69+
{%- endif %}
70+
71+
"""
72+
raise NotImplementedError
73+
74+
@abstractmethod
75+
def read(
76+
self,
77+
request: ResourceRequest[{{ resource }}Properties],
78+
) -> ProgressEvent[{{ resource }}Properties]:
79+
"""
80+
Fetch resource information
81+
82+
{% if read_permissions -%}
83+
IAM permissions required:
84+
{%- for permission in read_permissions %}
85+
- {{ permission }}
86+
{%- endfor %}
87+
{%- endif %}
88+
"""
89+
raise NotImplementedError
90+
91+
@abstractmethod
92+
def delete(
93+
self,
94+
request: ResourceRequest[{{ resource }}Properties],
95+
) -> ProgressEvent[{{ resource }}Properties]:
96+
"""
97+
Delete a resource
98+
99+
{% if delete_permissions -%}
100+
IAM permissions required:
101+
{%- for permission in delete_permissions %}
102+
- {{ permission }}
103+
{%- endfor %}
104+
{%- endif %}
105+
"""
106+
raise NotImplementedError
107+
108+
@abstractmethod
109+
def update(
110+
self,
111+
request: ResourceRequest[{{ resource }}Properties],
112+
) -> ProgressEvent[{{ resource }}Properties]:
113+
"""
114+
Update a resource
115+
116+
{% if update_permissions -%}
117+
IAM permissions required:
118+
{%- for permission in update_permissions %}
119+
- {{ permission }}
120+
{%- endfor %}
121+
{%- endif %}
122+
"""
123+
raise NotImplementedError
Lines changed: 11 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,27 @@
11
# LocalStack Resource Provider Scaffolding {{ scaffolding_version }}
2-
from __future__ import annotations
32

4-
from pathlib import Path
5-
from typing import Optional, TypedDict
6-
7-
import localstack.services.cloudformation.provider_utils as util
83
from localstack.services.cloudformation.resource_provider import (
94
OperationStatus,
105
ProgressEvent,
11-
ResourceProvider,
126
ResourceRequest,
137
)
8+
{%- if pro %}
9+
{%- set root_module = "localstack.pro.core" %}
10+
{%- else %}
11+
{%- set root_module = "localstack" %}
12+
{%- endif %}
13+
from {{ root_module }}.services.{{ service }}.resource_providers.generated.aws_{{ service }}_{{ lower_resource }}_base import (
14+
{{ resource }}ProviderBase,
15+
{{ resource }}Properties,
16+
REPEATED_INVOCATION
17+
)
1418

15-
{{ provider_properties }}
16-
17-
18-
REPEATED_INVOCATION = "repeated_invocation"
19-
20-
class {{ resource }}Provider(ResourceProvider[{{ resource }}Properties]):
21-
22-
TYPE = "{{ name }}" # Autogenerated. Don't change
23-
SCHEMA = util.get_schema_path(Path(__file__)) # Autogenerated. Don't change
19+
class {{ resource }}Provider({{ resource }}ProviderBase):
2420

2521
def create(
2622
self,
2723
request: ResourceRequest[{{ resource }}Properties],
2824
) -> ProgressEvent[{{ resource }}Properties]:
29-
"""
30-
Create a new resource.
31-
32-
{% if primary_identifier -%}
33-
Primary identifier fields:
34-
{%- for property in primary_identifier %}
35-
- {{ property }}
36-
{%- endfor %}
37-
{%- endif %}
38-
39-
{% if required_properties -%}
40-
Required properties:
41-
{%- for property in required_properties %}
42-
- {{ property }}
43-
{%- endfor %}
44-
{%- endif %}
45-
46-
{% if create_only_properties -%}
47-
Create-only properties:
48-
{%- for property in create_only_properties %}
49-
- {{ property }}
50-
{%- endfor %}
51-
{%- endif %}
52-
53-
{% if read_only_properties -%}
54-
Read-only properties:
55-
{%- for property in read_only_properties %}
56-
- {{ property }}
57-
{%- endfor %}
58-
{%- endif %}
59-
60-
{% if create_permissions -%}
61-
IAM permissions required:
62-
{%- for permission in create_permissions %}
63-
- {{ permission }}
64-
{%- endfor -%}
65-
{%- endif %}
66-
67-
"""
6825
model = request.desired_state
6926

7027
# TODO: validations
@@ -93,46 +50,16 @@ class {{ resource }}Provider(ResourceProvider[{{ resource }}Properties]):
9350
self,
9451
request: ResourceRequest[{{ resource }}Properties],
9552
) -> ProgressEvent[{{ resource }}Properties]:
96-
"""
97-
Fetch resource information
98-
99-
{% if read_permissions -%}
100-
IAM permissions required:
101-
{%- for permission in read_permissions %}
102-
- {{ permission }}
103-
{%- endfor %}
104-
{%- endif %}
105-
"""
10653
raise NotImplementedError
10754

10855
def delete(
10956
self,
11057
request: ResourceRequest[{{ resource }}Properties],
11158
) -> ProgressEvent[{{ resource }}Properties]:
112-
"""
113-
Delete a resource
114-
115-
{% if delete_permissions -%}
116-
IAM permissions required:
117-
{%- for permission in delete_permissions %}
118-
- {{ permission }}
119-
{%- endfor %}
120-
{%- endif %}
121-
"""
12259
raise NotImplementedError
12360

12461
def update(
12562
self,
12663
request: ResourceRequest[{{ resource }}Properties],
12764
) -> ProgressEvent[{{ resource }}Properties]:
128-
"""
129-
Update a resource
130-
131-
{% if update_permissions -%}
132-
IAM permissions required:
133-
{%- for permission in update_permissions %}
134-
- {{ permission }}
135-
{%- endfor %}
136-
{%- endif %}
137-
"""
13865
raise NotImplementedError

0 commit comments

Comments
 (0)