pulumi-extra 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ from .output import render_template
2
+ from .resource_ import get_resource_cls, resource_has_attribute
3
+ from .stack_reference import get_stack_outputs, get_stack_reference, re_export
4
+ from .transforms import (
5
+ override_default_provider,
6
+ override_invoke,
7
+ override_invoke_defaults,
8
+ override_invoke_options,
9
+ override_resource,
10
+ override_resource_defaults,
11
+ override_resource_options,
12
+ )
13
+
14
+ __all__ = (
15
+ "get_resource_cls",
16
+ "get_stack_outputs",
17
+ "get_stack_reference",
18
+ "override_default_provider",
19
+ "override_invoke",
20
+ "override_invoke_defaults",
21
+ "override_invoke_options",
22
+ "override_resource",
23
+ "override_resource_defaults",
24
+ "override_resource_options",
25
+ "re_export",
26
+ "render_template",
27
+ "resource_has_attribute",
28
+ )
File without changes
@@ -0,0 +1,8 @@
1
+ from .autotag import is_taggable, register_auto_tagging
2
+ from .common import is_aws_resource
3
+
4
+ __all__ = (
5
+ "is_aws_resource",
6
+ "is_taggable",
7
+ "register_auto_tagging",
8
+ )
@@ -0,0 +1,77 @@
1
+ # noqa: D100
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ import pulumi
7
+ import pulumi_aws # noqa: F401; Required to detect resources otherwise `is_taggable` will always return `False`
8
+
9
+ from pulumi_extra import resource_has_attribute
10
+
11
+ from .common import is_aws_resource
12
+
13
+ _NOT_TAGGABLE_RESOURCES: set[str] = {
14
+ "aws:autoscaling/group:Group",
15
+ "aws:devopsguru/resourceCollection:ResourceCollection",
16
+ }
17
+
18
+
19
+ def register_auto_tagging(
20
+ *,
21
+ exclude: set[str] | None = None,
22
+ extra: dict[str, str] | None = None,
23
+ ) -> None:
24
+ """Register a Pulumi stack transform that automatically tags resources.
25
+
26
+ Args:
27
+ exclude: Resources to exclude from tagging.
28
+ extra: Extra tags to add.
29
+ """
30
+ tags = {}
31
+ extra = extra or {}
32
+ exclude = exclude or set()
33
+
34
+ # Pulumi tags
35
+ org = pulumi.get_organization()
36
+ project = pulumi.get_project()
37
+ stack = pulumi.get_stack()
38
+ tags.update(
39
+ {
40
+ "pulumi:Organization": org,
41
+ "pulumi:Project": project,
42
+ "pulumi:Stack": stack,
43
+ "Managed-By": "Pulumi",
44
+ },
45
+ )
46
+ tags.update(extra)
47
+
48
+ def transform(
49
+ args: pulumi.ResourceTransformArgs,
50
+ ) -> pulumi.ResourceTransformResult | None:
51
+ if args.type_ not in exclude and is_taggable(args.type_):
52
+ if TYPE_CHECKING:
53
+ assert isinstance(args.props, dict)
54
+ args.props["tags"] = {
55
+ **tags,
56
+ **(args.props.get("tags", {})),
57
+ }
58
+ return pulumi.ResourceTransformResult(props=args.props, opts=args.opts)
59
+
60
+ return None
61
+
62
+ pulumi.runtime.register_resource_transform(transform)
63
+
64
+
65
+ def is_taggable(resource_type: str) -> bool:
66
+ """Determine if a given AWS resource type is taggable."""
67
+ if not is_aws_resource(resource_type):
68
+ pulumi.log.debug(f"Resource type {resource_type} is not a AWS resource")
69
+ return False
70
+
71
+ if resource_type in _NOT_TAGGABLE_RESOURCES:
72
+ pulumi.log.info(
73
+ f"Resource type {resource_type} is set not-taggable explicitly",
74
+ )
75
+ return False
76
+
77
+ return resource_has_attribute(resource_type, "tags")
@@ -0,0 +1,4 @@
1
+ # noqa: D100
2
+ def is_aws_resource(resource_type: str) -> bool:
3
+ """Determine if a given resource type is an AWS resource."""
4
+ return resource_type.startswith("aws:")
@@ -0,0 +1,4 @@
1
+ from .require_description import require_description
2
+ from .require_tags import require_tags
3
+
4
+ __all__ = ("require_description", "require_tags")
@@ -0,0 +1,60 @@
1
+ # noqa: D100
2
+ import pulumi_policy as policy
3
+
4
+ from pulumi_extra import resource_has_attribute
5
+ from pulumi_extra.contrib.aws import is_aws_resource, is_taggable
6
+
7
+
8
+ class RequireDescription:
9
+ """Policy validator to require description (or tag if unsupported) on resources."""
10
+
11
+ def __init__(
12
+ self,
13
+ *,
14
+ require_tag_if_description_unsupported: bool = False,
15
+ description_tag_key: str = "Description",
16
+ ) -> None:
17
+ """Initialize the policy validator.
18
+
19
+ Args:
20
+ require_tag_if_description_unsupported: Require a tag if description is unsupported.
21
+ Because AWS tags support human-friendly descriptions,
22
+ in most cases, using a tag for description is recommended.
23
+ description_tag_key: The tag key to use for description.
24
+
25
+ """
26
+ self.require_tag_if_description_unsupported = require_tag_if_description_unsupported
27
+ self.description_tag_key = description_tag_key
28
+
29
+ def __call__( # noqa: D102
30
+ self,
31
+ args: policy.ResourceValidationArgs,
32
+ report_violation: policy.ReportViolation,
33
+ ) -> None:
34
+ if not is_aws_resource(args.resource_type):
35
+ return
36
+
37
+ if resource_has_attribute(args.resource_type, "description") and args.props.get("description") is None:
38
+ report_violation(
39
+ f"Resource '{args.urn}' is missing required description",
40
+ None,
41
+ )
42
+
43
+ if self.require_tag_if_description_unsupported: # noqa: SIM102
44
+ if is_taggable(args.resource_type):
45
+ tags = args.props.get("tags", {})
46
+ if self.description_tag_key not in tags:
47
+ report_violation(
48
+ f"Resource '{args.urn}' is missing required tag '{self.description_tag_key}'",
49
+ None,
50
+ )
51
+
52
+
53
+ require_description = policy.ResourceValidationPolicy(
54
+ name="aws:require-description",
55
+ description="Require description (or tag if unsupported) on resources",
56
+ config_schema=policy.PolicyConfigSchema(
57
+ properties={},
58
+ ),
59
+ validate=RequireDescription(),
60
+ )
@@ -0,0 +1,42 @@
1
+ # noqa: D100
2
+ import pulumi_policy as policy
3
+
4
+ from pulumi_extra.contrib.aws import is_aws_resource, is_taggable
5
+
6
+
7
+ class RequireTags:
8
+ """Policy validator to require specific tags on resources."""
9
+
10
+ def __call__( # noqa: D102
11
+ self,
12
+ args: policy.ResourceValidationArgs,
13
+ report_violation: policy.ReportViolation,
14
+ ) -> None:
15
+ config = args.get_config()
16
+ required_tags = config["required-tags"]
17
+ if not required_tags or not is_aws_resource(args.resource_type):
18
+ return
19
+
20
+ if is_taggable(args.resource_type):
21
+ tags = args.props.get("tags", {})
22
+ for rt in required_tags:
23
+ if not tags or rt not in tags:
24
+ report_violation(
25
+ f"Resource '{args.urn}' is missing required tag '{rt}'",
26
+ None,
27
+ )
28
+
29
+
30
+ require_tags = policy.ResourceValidationPolicy(
31
+ name="aws:require-tags",
32
+ description="Require specific tags on resources",
33
+ config_schema=policy.PolicyConfigSchema(
34
+ properties={
35
+ "required-tags": {
36
+ "type": "array",
37
+ "items": {"type": "string"},
38
+ },
39
+ },
40
+ ),
41
+ validate=RequireTags(),
42
+ )
@@ -0,0 +1,8 @@
1
+ from .autolabel import is_labelable, register_auto_labeling
2
+ from .common import is_gcp_resource
3
+
4
+ __all__ = (
5
+ "is_gcp_resource",
6
+ "is_labelable",
7
+ "register_auto_labeling",
8
+ )
@@ -0,0 +1,75 @@
1
+ # noqa: D100
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ import pulumi
7
+ import pulumi_gcp # noqa: F401; Required to detect resources otherwise `is_labelable` will always return `False`
8
+
9
+ from pulumi_extra import resource_has_attribute
10
+
11
+ from .common import is_gcp_resource
12
+
13
+ _NOT_LABELABLE_RESOURCES: set[str] = set()
14
+
15
+
16
+ def register_auto_labeling(
17
+ *,
18
+ exclude: set[str] | None = None,
19
+ extra: dict[str, str] | None = None,
20
+ ) -> None:
21
+ """Register a Pulumi stack transform that automatically labels resources.
22
+
23
+ Args:
24
+ exclude: Resources to exclude from labeling.
25
+ extra: Extra labels to add.
26
+ """
27
+ labels = {}
28
+ extra = extra or {}
29
+ exclude = exclude or set()
30
+
31
+ # Pulumi labels
32
+ # NOTE: Labels transformed because of strict restrictions GCP enforces
33
+ org = pulumi.get_organization()
34
+ project = pulumi.get_project()
35
+ stack = pulumi.get_stack()
36
+ labels.update(
37
+ {
38
+ "pulumi-organization": org,
39
+ "pulumi-project": project.replace(".", "-"),
40
+ "pulumi-stack": stack,
41
+ "managed-by": "pulumi",
42
+ },
43
+ )
44
+ labels.update(extra)
45
+
46
+ def transform(
47
+ args: pulumi.ResourceTransformArgs,
48
+ ) -> pulumi.ResourceTransformResult | None:
49
+ if args.type_ not in exclude and is_labelable(args.type_):
50
+ if TYPE_CHECKING:
51
+ assert isinstance(args.props, dict)
52
+ args.props["labels"] = {
53
+ **labels,
54
+ **(args.props.get("labels", {})),
55
+ }
56
+ return pulumi.ResourceTransformResult(props=args.props, opts=args.opts)
57
+
58
+ return None
59
+
60
+ pulumi.runtime.register_resource_transform(transform)
61
+
62
+
63
+ def is_labelable(resource_type: str) -> bool:
64
+ """Determine if a given GCP resource type is labelable."""
65
+ if not is_gcp_resource(resource_type):
66
+ pulumi.log.debug(f"Resource type {resource_type} is not a GCP resource")
67
+ return False
68
+
69
+ if resource_type in _NOT_LABELABLE_RESOURCES:
70
+ pulumi.log.info(
71
+ f"Resource type {resource_type} is set not-labelable explicitly",
72
+ )
73
+ return False
74
+
75
+ return resource_has_attribute(resource_type, "labels")
@@ -0,0 +1,4 @@
1
+ # noqa: D100
2
+ def is_gcp_resource(resource_type: str) -> bool:
3
+ """Determine if a given resource type is an AWS resource."""
4
+ return resource_type.startswith("gcp:")
@@ -0,0 +1,4 @@
1
+ from .require_description import require_description
2
+ from .require_labels import require_labels
3
+
4
+ __all__ = ("require_description", "require_labels")
@@ -0,0 +1,60 @@
1
+ # noqa: D100
2
+ import pulumi_policy as policy
3
+
4
+ from pulumi_extra import resource_has_attribute
5
+ from pulumi_extra.contrib.gcp import is_gcp_resource, is_labelable
6
+
7
+
8
+ class RequireDescription:
9
+ """Policy validator to require description (or label if unsupported) on resources."""
10
+
11
+ def __init__(
12
+ self,
13
+ *,
14
+ require_label_if_description_unsupported: bool = False,
15
+ description_label_key: str = "description",
16
+ ) -> None:
17
+ """Initialize the policy validator.
18
+
19
+ Args:
20
+ require_label_if_description_unsupported: Require a label if description is unsupported.
21
+ Because GCP labels does not support human-friendly descriptions,
22
+ in most cases, using a label for description is not recommended.
23
+ description_label_key: The label key to use for description.
24
+
25
+ """
26
+ self.require_label_if_description_unsupported = require_label_if_description_unsupported
27
+ self.description_label_key = description_label_key
28
+
29
+ def __call__( # noqa: D102
30
+ self,
31
+ args: policy.ResourceValidationArgs,
32
+ report_violation: policy.ReportViolation,
33
+ ) -> None:
34
+ if not is_gcp_resource(args.resource_type):
35
+ return
36
+
37
+ if resource_has_attribute(args.resource_type, "description") and args.props.get("description") is None:
38
+ report_violation(
39
+ f"Resource '{args.urn}' is missing required description",
40
+ None,
41
+ )
42
+
43
+ if self.require_label_if_description_unsupported: # noqa: SIM102
44
+ if is_labelable(args.resource_type):
45
+ labels = args.props.get("labels", {})
46
+ if self.description_label_key not in labels:
47
+ report_violation(
48
+ f"Resource '{args.urn}' is missing required label '{self.description_label_key}'",
49
+ None,
50
+ )
51
+
52
+
53
+ require_description = policy.ResourceValidationPolicy(
54
+ name="gcp:require-description",
55
+ description="Require description (or label if unsupported) on resources",
56
+ config_schema=policy.PolicyConfigSchema(
57
+ properties={},
58
+ ),
59
+ validate=RequireDescription(),
60
+ )
@@ -0,0 +1,42 @@
1
+ # noqa: D100
2
+ import pulumi_policy as policy
3
+
4
+ from pulumi_extra.contrib.gcp import is_gcp_resource, is_labelable
5
+
6
+
7
+ class RequireLabels:
8
+ """Policy validator to require specific labels on resources."""
9
+
10
+ def __call__( # noqa: D102
11
+ self,
12
+ args: policy.ResourceValidationArgs,
13
+ report_violation: policy.ReportViolation,
14
+ ) -> None:
15
+ config = args.get_config()
16
+ required_labels = config["required-labels"]
17
+ if not required_labels or not is_gcp_resource(args.resource_type):
18
+ return
19
+
20
+ if is_labelable(args.resource_type):
21
+ labels = args.props.get("labels", {})
22
+ for rl in required_labels:
23
+ if not labels or rl not in labels:
24
+ report_violation(
25
+ f"Resource '{args.urn}' is missing required label '{rl}'",
26
+ None,
27
+ )
28
+
29
+
30
+ require_labels = policy.ResourceValidationPolicy(
31
+ name="gcp:require-labels",
32
+ description="Require specific labels on resources",
33
+ config_schema=policy.PolicyConfigSchema(
34
+ properties={
35
+ "required-labels": {
36
+ "type": "array",
37
+ "items": {"type": "string"},
38
+ },
39
+ },
40
+ ),
41
+ validate=RequireLabels(),
42
+ )
pulumi_extra/errors.py ADDED
@@ -0,0 +1,3 @@
1
+ # noqa: D100
2
+ class UnknownResourceTypeError(Exception):
3
+ """Raised when an unknown resource type is encountered."""
pulumi_extra/output.py ADDED
@@ -0,0 +1,60 @@
1
+ """Utils for outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, overload
7
+
8
+ import pulumi
9
+ from jinja2 import StrictUndefined, Template
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Mapping
13
+ from typing import Any
14
+
15
+
16
+ @overload
17
+ def render_template(
18
+ template: Path | str,
19
+ *,
20
+ context: Mapping[str, Any],
21
+ ) -> str: ... # pragma: no cover
22
+
23
+
24
+ @overload
25
+ def render_template(
26
+ template: Path | str,
27
+ *,
28
+ inputs: Mapping[str, pulumi.Input[Any]],
29
+ ) -> pulumi.Output[str]: ... # pragma: no cover
30
+
31
+
32
+ def render_template(
33
+ template: Path | str,
34
+ *,
35
+ context: Mapping[str, Any] | None = None,
36
+ inputs: Mapping[str, pulumi.Input[Any]] | None = None,
37
+ ) -> str | pulumi.Output[str]:
38
+ """Render a template file with the given context.
39
+
40
+ Args:
41
+ template: The template file or inline template string.
42
+ context: The context to render the template with. Conflicts with inputs.
43
+ inputs: The inputs to render the template with. Conflicts with context.
44
+ """
45
+ if isinstance(template, Path):
46
+ template = template.read_text()
47
+
48
+ jinja_tpl = Template(template, undefined=StrictUndefined)
49
+
50
+ # Render with Python values.
51
+ if context is not None and inputs is None:
52
+ return jinja_tpl.render(context)
53
+
54
+ # Render with Pulumi inputs.
55
+ if context is None and inputs is not None:
56
+ return pulumi.Output.all(inputs).apply(lambda args: jinja_tpl.render(args[0]))
57
+
58
+ # Only one of context or inputs must be provided.
59
+ msg = "Either context or input must be provided."
60
+ raise ValueError(msg)
pulumi_extra/py.typed ADDED
File without changes
@@ -0,0 +1,73 @@
1
+ """Utility functions for working with Pulumi resources.
2
+
3
+ References:
4
+ - https://github.com/tlinhart/pulumi-aws-tags
5
+
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from functools import cache
11
+ from importlib import import_module
12
+ from inspect import signature
13
+ from typing import TYPE_CHECKING
14
+
15
+ import pulumi
16
+ from pulumi.runtime.rpc import _RESOURCE_MODULES
17
+
18
+ from .errors import UnknownResourceTypeError
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Iterator
22
+ from typing import Any
23
+
24
+
25
+ @cache
26
+ def resource_has_attribute(resource_type: str, attribute: str) -> bool:
27
+ """Determine if a given GCP resource type is labelable."""
28
+ cls = get_resource_cls(resource_type)
29
+ if cls is None:
30
+ msg = f"Unable to resolve resource type {resource_type!r}"
31
+ raise UnknownResourceTypeError(msg)
32
+
33
+ sig = signature(cls._internal_init)
34
+ return attribute in sig.parameters
35
+
36
+
37
+ @cache
38
+ def get_resource_cls(resource_type: str) -> Any | None:
39
+ """Get the Pulumi resource class for a given resource type.
40
+
41
+ Args:
42
+ resource_type: Resource type to get the class for.
43
+
44
+ Returns:
45
+ Pulumi resource class if found, otherwise `None`.
46
+
47
+ """
48
+ try:
49
+ _, resource = next(filter(lambda k: k[0] == resource_type, _get_resources()))
50
+ except StopIteration:
51
+ pulumi.log.debug(f"Resource type {resource_type} not found")
52
+ return None
53
+
54
+ module_name, class_name = resource
55
+ module = import_module(module_name)
56
+ return getattr(module, class_name)
57
+
58
+
59
+ def _get_resources() -> Iterator[tuple[str, tuple[str, str]]]:
60
+ """Return Pulumi resource registry.
61
+
62
+ Returns:
63
+ Iterator of tuple containing resource type and resource class.
64
+
65
+ """
66
+ # NOTE: This cannot be cached as the underlying registry (`_RESOURCE_MODULES`) gradually populates
67
+ for modules in _RESOURCE_MODULES.values():
68
+ for module in modules:
69
+ mod_info = module.mod_info # type: ignore[attr-defined]
70
+ fqn, classes = mod_info["fqn"], mod_info["classes"]
71
+ for type_, name in classes.items():
72
+ # e.g. ("gcp:activedirectory/domain:Domain", ("pulumi_gcp.activedirectory", "Domain"))
73
+ yield (type_, (fqn, name))
@@ -0,0 +1,119 @@
1
+ """Utils for stack references."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import cache
6
+ from itertools import chain
7
+ from typing import Any, overload
8
+
9
+ import pulumi
10
+ from braceexpand import braceexpand
11
+
12
+
13
+ @cache
14
+ def get_stack_reference(ref: str) -> pulumi.StackReference:
15
+ """Resolve given stack reference shorthand to fully qualified stack reference.
16
+
17
+ The shorthand can be one of the following:
18
+
19
+ - `"{stack}"`
20
+
21
+ Returns the stack reference for the current project and organization.
22
+
23
+ - `"{project}/{stack}"`
24
+
25
+ Returns the stack reference for the current organization.
26
+
27
+ - `"{organization}/{project}/{stack}"`
28
+
29
+ No change is made to the stack reference.
30
+
31
+ """
32
+ fqr = _resolve_stack_ref(ref)
33
+ return pulumi.StackReference(fqr)
34
+
35
+
36
+ def _resolve_stack_ref(ref: str) -> str:
37
+ components = ref.split("/")
38
+ num_components = len(components)
39
+ if num_components == 1:
40
+ org = pulumi.get_organization()
41
+ project = pulumi.get_project()
42
+ fqr = f"{org}/{project}/{ref}"
43
+ elif num_components == 2: # noqa: PLR2004
44
+ org = pulumi.get_organization()
45
+ fqr = f"{org}/{ref}"
46
+ elif num_components == 3: # noqa: PLR2004
47
+ fqr = ref
48
+ else:
49
+ msg = f"Invalid stack reference: {ref!r}"
50
+ raise ValueError(msg)
51
+
52
+ return fqr
53
+
54
+
55
+ @overload
56
+ def get_stack_outputs(ref: str) -> pulumi.Output[Any]: ... # pragma: no cover
57
+
58
+
59
+ @overload
60
+ def get_stack_outputs(*refs: str) -> list[pulumi.Output[Any]]: ... # pragma: no cover
61
+
62
+
63
+ def get_stack_outputs( # type: ignore[misc]
64
+ *refs: str,
65
+ ) -> pulumi.Output[Any] | list[pulumi.Output[Any]]:
66
+ """Get outputs from a output reference shorthands. Supports brace expansion.
67
+
68
+ - Single output reference: (`"<stack_ref>:<output_key>"`).
69
+ - Multiple outputs using brace expansion: (`"<stack_ref>:{<output_key_1>,<output_key_2>}"`).
70
+
71
+ Args:
72
+ *refs: Output references.
73
+
74
+ """
75
+ outputs = _get_stack_outputs(*refs)
76
+ output_values = list(outputs.values())
77
+ if len(output_values) == 1:
78
+ return output_values[0]
79
+
80
+ return output_values
81
+
82
+
83
+ def re_export(*refs: str) -> None:
84
+ """Re-export outputs from a output reference shorthands.
85
+
86
+ Args:
87
+ *refs: Output references.
88
+
89
+ """
90
+ outputs = _get_stack_outputs(*refs)
91
+ for (_, output_key), output in outputs.items():
92
+ pulumi.export(output_key, output)
93
+
94
+
95
+ def _get_stack_outputs(*refs: str) -> dict[tuple[str, str], pulumi.Output[Any]]:
96
+ expand_refs = list(chain.from_iterable(map(braceexpand, refs)))
97
+ pulumi.log.debug(f"Expanded output references ({refs!r}): {expand_refs!r}")
98
+
99
+ fqr: list[tuple] = []
100
+ for ref in expand_refs:
101
+ stack_ref, output_key = _resolve_output_ref(ref)
102
+ fqr.append((stack_ref, output_key))
103
+
104
+ outputs: dict[tuple[str, str], pulumi.Output[Any]] = {}
105
+ for stack_ref, output_key in fqr:
106
+ sr = get_stack_reference(stack_ref)
107
+ outputs[(stack_ref, output_key)] = sr.require_output(output_key)
108
+
109
+ return outputs
110
+
111
+
112
+ def _resolve_output_ref(ref: str) -> tuple[str, str]:
113
+ components = ref.split(":")
114
+ stack_ref, output_key = components
115
+ if not stack_ref or not output_key:
116
+ msg = f"Invalid output reference: {ref!r}"
117
+ raise ValueError(msg)
118
+
119
+ return stack_ref, output_key
@@ -0,0 +1,13 @@
1
+ from .invoke import override_invoke, override_invoke_defaults, override_invoke_options
2
+ from .resource_ import override_resource, override_resource_defaults, override_resource_options
3
+ from .runtime import override_default_provider
4
+
5
+ __all__ = (
6
+ "override_default_provider",
7
+ "override_invoke",
8
+ "override_invoke_defaults",
9
+ "override_invoke_options",
10
+ "override_resource",
11
+ "override_resource_defaults",
12
+ "override_resource_options",
13
+ )
@@ -0,0 +1,86 @@
1
+ """Pulumi invoke transforms.
2
+
3
+ Invoke transforms should be register at the runtime level because Pulumi doesn't support
4
+ registering invoke transforms per-invoke basis.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from fnmatch import fnmatch
10
+ from itertools import chain
11
+ from typing import TYPE_CHECKING, Any, Callable
12
+
13
+ import pulumi
14
+ from braceexpand import braceexpand
15
+
16
+ _Args = dict[str, pulumi.Input[Any]]
17
+
18
+
19
+ def override_invoke(
20
+ *invoke_tokens: str,
21
+ args: _Args | Callable[[_Args], _Args] | None = None,
22
+ opts: pulumi.InvokeOptions | Callable[[pulumi.InvokeOptions], pulumi.InvokeOptions] | None = None,
23
+ ) -> pulumi.InvokeTransform:
24
+ """Pulumi transform factory for invoke tokens (`get_*`).
25
+
26
+ Args:
27
+ *invoke_tokens: Invoke tokens to match. Supports glob patterns and brace expand.
28
+ args: Invoke arguments to override, or a callable that returns the new arguments from given `args.args` input.
29
+ opts: Invoke options to override, or a callable that returns the new options given `args.opts` input.
30
+
31
+ """
32
+ args_ = args
33
+
34
+ def transform(args: pulumi.InvokeTransformArgs) -> pulumi.InvokeTransformResult | None:
35
+ nonlocal args_, opts
36
+
37
+ for it in chain.from_iterable(map(braceexpand, invoke_tokens)):
38
+ if not fnmatch(args.token, it):
39
+ continue
40
+
41
+ # Transform invoke arguments
42
+ if TYPE_CHECKING:
43
+ assert isinstance(args.args, dict)
44
+
45
+ if callable(args_): # noqa: SIM108
46
+ new_args = args_(args.args)
47
+ else:
48
+ new_args = args.args | args_ if args_ is not None else args.args
49
+
50
+ # Transform invoke options
51
+ new_opts = opts(args.opts) if callable(opts) else (opts or pulumi.InvokeOptions())
52
+ new_opts = pulumi.InvokeOptions.merge(args.opts, new_opts)
53
+
54
+ return pulumi.InvokeTransformResult(args=new_args, opts=new_opts)
55
+
56
+ return None
57
+
58
+ return transform
59
+
60
+
61
+ def override_invoke_defaults(*invoke_tokens: str, defaults: dict[str, Any]) -> pulumi.InvokeTransform:
62
+ """Pulumi transform factory that provides default arguments to matching invoke tokens.
63
+
64
+ Args:
65
+ *invoke_tokens: Invoke tokens to match.
66
+ defaults: Default arguments.
67
+
68
+ """
69
+ return override_invoke(
70
+ *invoke_tokens,
71
+ args=lambda args: defaults | args,
72
+ )
73
+
74
+
75
+ def override_invoke_options(*invoke_tokens: str, **options: Any) -> pulumi.InvokeTransform:
76
+ """Pulumi transform factory that overrides the invoke options for matching invoke tokens.
77
+
78
+ Args:
79
+ *invoke_tokens: Invoke tokens to match.
80
+ options: Arguments of `pulumi.InvokeOptions`.
81
+
82
+ """
83
+ return override_invoke(
84
+ *invoke_tokens,
85
+ opts=pulumi.InvokeOptions(**options),
86
+ )
@@ -0,0 +1,84 @@
1
+ """Pulumi resource transforms."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fnmatch import fnmatch
6
+ from itertools import chain
7
+ from typing import TYPE_CHECKING, Any, Callable
8
+
9
+ import pulumi
10
+ from braceexpand import braceexpand
11
+
12
+ _Props = dict[str, pulumi.Input[Any]]
13
+
14
+
15
+ def override_resource(
16
+ *resource_types: str,
17
+ props: _Props | Callable[[_Props], _Props] | None = None,
18
+ opts: pulumi.ResourceOptions | Callable[[pulumi.ResourceOptions], pulumi.ResourceOptions] | None = None,
19
+ ) -> pulumi.ResourceTransform:
20
+ """Pulumi transform factory for resources.
21
+
22
+ Args:
23
+ *resource_types: Resource types to match. Supports glob patterns and brace expand.
24
+ props: Resource properties to override, or a callable that returns the new properties from given `args.props` input.
25
+ opts: Resource options to override, or a callable that returns the new options given `args.opts` input.
26
+
27
+ """ # noqa: E501
28
+
29
+ def transform(args: pulumi.ResourceTransformArgs) -> pulumi.ResourceTransformResult | None:
30
+ nonlocal props, opts
31
+
32
+ for rt in chain.from_iterable(map(braceexpand, resource_types)):
33
+ if not fnmatch(args.type_, rt):
34
+ continue
35
+
36
+ # Transform resource properties
37
+ if TYPE_CHECKING:
38
+ assert isinstance(args.props, dict)
39
+
40
+ if callable(props):
41
+ new_props = props(args.props)
42
+ else:
43
+ new_props = args.props | props if props is not None else args.props
44
+
45
+ # Transform resource options
46
+ new_opts = opts(args.opts) if callable(opts) else (opts or pulumi.ResourceOptions())
47
+ new_opts = pulumi.ResourceOptions.merge(args.opts, new_opts)
48
+
49
+ return pulumi.ResourceTransformResult(props=new_props, opts=new_opts)
50
+
51
+ return None
52
+
53
+ return transform
54
+
55
+
56
+ def override_resource_defaults(
57
+ *resource_types: str,
58
+ defaults: dict[str, pulumi.Input[Any]],
59
+ ) -> pulumi.ResourceTransform:
60
+ """Pulumi transform factory that provides default properties to matching resource types.
61
+
62
+ Args:
63
+ *resource_types: Resource type to match.
64
+ defaults: Default properties.
65
+
66
+ """
67
+ return override_resource(
68
+ *resource_types,
69
+ props=lambda props: defaults | props,
70
+ )
71
+
72
+
73
+ def override_resource_options(*resource_types: str, **options: Any) -> pulumi.ResourceTransform:
74
+ """Pulumi transform factory that overrides the resource options for resources of given types.
75
+
76
+ Args:
77
+ *resource_types: Resource types to match.
78
+ options: Arguments of `pulumi.ResourceOptions`.
79
+
80
+ """
81
+ return override_resource(
82
+ *resource_types,
83
+ opts=pulumi.ResourceOptions(**options),
84
+ )
@@ -0,0 +1,23 @@
1
+ """Runtime-level transforms for Pulumi programs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pulumi
6
+
7
+ from .invoke import override_invoke_options
8
+ from .resource_ import override_resource_options
9
+
10
+
11
+ def override_default_provider(
12
+ *rt_or_it: str,
13
+ provider: pulumi.ProviderResource,
14
+ ) -> None:
15
+ """Override the default provider for resources and invokes of given types.
16
+
17
+ Args:
18
+ *rt_or_it: Resource types or invoke tokens to match.
19
+ provider: Provider to override.
20
+
21
+ """
22
+ pulumi.runtime.register_resource_transform(override_resource_options(*rt_or_it, provider=provider))
23
+ pulumi.runtime.register_invoke_transform(override_invoke_options(*rt_or_it, provider=provider))
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: pulumi-extra
3
+ Version: 0.1.0
4
+ Summary: Extra Pulumi utils and resources.
5
+ Author-email: Yuchan Lee <lasuillard@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: <4.0,>=3.9
9
+ Requires-Dist: braceexpand<1,>=0.1.7
10
+ Requires-Dist: jinja2<4,>=3
11
+ Requires-Dist: pulumi<4,>=3
12
+ Provides-Extra: aws
13
+ Requires-Dist: pulumi-aws>=6; extra == 'aws'
14
+ Provides-Extra: dev
15
+ Requires-Dist: mypy~=1.11; extra == 'dev'
16
+ Requires-Dist: ruff~=0.6; extra == 'dev'
17
+ Provides-Extra: gcp
18
+ Requires-Dist: pulumi-gcp>=8; extra == 'gcp'
19
+ Provides-Extra: policy
20
+ Requires-Dist: pulumi-policy<2,>=1; extra == 'policy'
21
+ Provides-Extra: test
22
+ Requires-Dist: coverage~=7.3; extra == 'test'
23
+ Requires-Dist: nox~=2024.10.9; extra == 'test'
24
+ Requires-Dist: pulumi-random>=4.18.0; extra == 'test'
25
+ Requires-Dist: pytest-cov<7,>=5; extra == 'test'
26
+ Requires-Dist: pytest-order>=1.3.0; extra == 'test'
27
+ Requires-Dist: pytest-sugar~=1.0; extra == 'test'
28
+ Requires-Dist: pytest~=8.0; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # pulumi-extra
32
+
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
34
+ [![CI](https://github.com/lasuillard/pulumi-extra/actions/workflows/ci.yaml/badge.svg)](https://github.com/lasuillard/pulumi-extra/actions/workflows/ci.yaml)
35
+ [![codecov](https://codecov.io/gh/lasuillard/pulumi-extra/graph/badge.svg?token=uuckU93NAu)](https://codecov.io/gh/lasuillard/pulumi-extra)
36
+ ![GitHub Release](https://img.shields.io/github/v/release/lasuillard/pulumi-extra)
37
+
38
+ Extra Pulumi utils and resources.
@@ -0,0 +1,27 @@
1
+ pulumi_extra/__init__.py,sha256=GOzVO3J_Xbe_-FDIT6LjFvdSExsTziqm8-efyaXwVJI,776
2
+ pulumi_extra/errors.py,sha256=S6F6CMnj8BrQZqCYZl4mMMWN-VqC7KRNHG-5BldAs_A,119
3
+ pulumi_extra/output.py,sha256=jJBWi98AIWL87y1J5MMUHjXXK-_Q1gspmveVCi9IQl0,1620
4
+ pulumi_extra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ pulumi_extra/resource_.py,sha256=Nexd9h5dEeJKJqb0w_3lH7NmYSmx7Twl9_ta03j8UDI,2194
6
+ pulumi_extra/stack_reference.py,sha256=66S4aIQyx5HpISaHqT_jsNKNNLAggT5Y5Y2B4n4Smc4,3291
7
+ pulumi_extra/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ pulumi_extra/contrib/aws/__init__.py,sha256=atpnUqZUL6C8t20LHK3EgKhPGSU8GlcuRPBpxddloAw,178
9
+ pulumi_extra/contrib/aws/autotag.py,sha256=Y5cwrzRGGrgRHELKXJPvxthRgOnrEeHQEhCCXRi5Ebw,2185
10
+ pulumi_extra/contrib/aws/common.py,sha256=93wyc8KNvgJdmj_rRG2zUa3OTFFDCcu_S-wLosggUs0,171
11
+ pulumi_extra/contrib/aws/policies/__init__.py,sha256=UgZuL2S8sHlK7ULm_OZpgnzOodPejgV50AYCs7GqPPM,143
12
+ pulumi_extra/contrib/aws/policies/require_description.py,sha256=uqqTbYZeOeQNy8j4GjcQqD_nzKFfocAO3l1BRUClhQ8,2160
13
+ pulumi_extra/contrib/aws/policies/require_tags.py,sha256=JZXFvgkkxcNzNGcY4qDMJ6eH-pJAQUjbm5EHFULrx5c,1247
14
+ pulumi_extra/contrib/gcp/__init__.py,sha256=mQghe26s_H0ni0m85wi6LCgk7c8F95bR1ixphP90III,184
15
+ pulumi_extra/contrib/gcp/autolabel.py,sha256=LhS3yHku8P8TRhR9cWWnMTTwvU_JXgOL9PQhmtF7N3U,2214
16
+ pulumi_extra/contrib/gcp/common.py,sha256=hsWt6f9y7GHrEMFNZeqXCT7Gp_deNHJrtNMLrQ7fZfo,171
17
+ pulumi_extra/contrib/gcp/policies/__init__.py,sha256=1iXvLVzQCuOtXl1VMPnq33XlWxo2WTIcJsbaJl6qQuI,149
18
+ pulumi_extra/contrib/gcp/policies/require_description.py,sha256=0vfC0ZPFXel1Pp0JIhBon_lteuZCodIoZsIDfbTaOZ8,2217
19
+ pulumi_extra/contrib/gcp/policies/require_labels.py,sha256=fTjjOGH2YcoLJOYxle3dH7vq9lva8hIrJyvpG12qtm0,1281
20
+ pulumi_extra/transforms/__init__.py,sha256=24I3Uta0nTSGPEuO7udHHu1pZdNdcbwnwV0sn1JhCxE,456
21
+ pulumi_extra/transforms/invoke.py,sha256=IcH3TUFEtGjsRCoF_3rtrvrACGqr8FIWqIlTPlMtS0Q,2778
22
+ pulumi_extra/transforms/resource_.py,sha256=K6HtYgs8swmV2FpYT42H-u9ctpHOwR8ls8hehneuHe8,2716
23
+ pulumi_extra/transforms/runtime.py,sha256=VUFwIcq3BVTKq04v9_54F5xSwmhW_yhXaXmYAS9_b_I,704
24
+ pulumi_extra-0.1.0.dist-info/METADATA,sha256=6BKKQnh67D5iUFGDF1IdI0z_uY_cE0muzMvhPRYK8RE,1568
25
+ pulumi_extra-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
+ pulumi_extra-0.1.0.dist-info/licenses/LICENSE,sha256=Q5GkvYijQ2KTQ-QWhv43ilzCno4ZrzrEuATEQZd9rYo,1067
27
+ pulumi_extra-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yuchan Lee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.