Support arbitrary stack export values in python. (#3015)
This commit is contained in:
parent
aac25eabc4
commit
d1376db975
|
@ -9,6 +9,10 @@ CHANGELOG
|
|||
|
||||
- Add `requireOutput` to `StackReference` [#3007](https://github.com/pulumi/pulumi/pull/3007)
|
||||
|
||||
- Arbitrary values can now be exported from a Python app. This includes dictionaries, lists, class
|
||||
instances, and the like. Values are treated as "plain old python data" and generally kept as
|
||||
simple values (like strings, numbers, etc.) or the simple collections supported by the Pulumi data model (specifically, dictionaries and lists).
|
||||
|
||||
### Compatibility
|
||||
|
||||
- Deprecated functions in `@pulumi/pulumi` will now issue warnings if you call them. Please migrate
|
||||
|
|
|
@ -152,6 +152,7 @@ disable=print-statement,
|
|||
too-many-arguments,
|
||||
too-many-branches,
|
||||
too-many-locals,
|
||||
too-many-return-statements,
|
||||
missing-docstring,
|
||||
fixme,
|
||||
broad-except,
|
||||
|
|
|
@ -16,19 +16,21 @@
|
|||
Support for automatic stack components.
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Callable, Any, Dict
|
||||
import collections
|
||||
from typing import Callable, Any, Dict, List
|
||||
|
||||
from ..resource import ComponentResource
|
||||
from ..resource import ComponentResource, Resource
|
||||
from .settings import get_project, get_stack, get_root_resource, set_root_resource
|
||||
from .rpc_manager import RPC_MANAGER
|
||||
from .. import log
|
||||
|
||||
from ..output import Output
|
||||
|
||||
async def run_in_stack(func: Callable):
|
||||
"""
|
||||
Run the given function inside of a new stack resource. This ensures that any stack export calls will end
|
||||
up as output properties on the resulting stack component in the checkpoint file. This is meant for internal
|
||||
runtime use only and is used by the Python SDK entrypoint program.
|
||||
Run the given function inside of a new stack resource. This ensures that any stack export calls
|
||||
will end up as output properties on the resulting stack component in the checkpoint file. This
|
||||
is meant for internal runtime use only and is used by the Python SDK entrypoint program.
|
||||
"""
|
||||
try:
|
||||
Stack(func)
|
||||
|
@ -49,12 +51,12 @@ async def run_in_stack(func: Callable):
|
|||
# Wait for all outstanding RPCs to retire.
|
||||
await RPC_MANAGER.wait_for_outstanding_rpcs()
|
||||
|
||||
# Asyncio event loops require that all outstanding tasks be completed by the time that the event
|
||||
# loop closes. If we're at this point and there are no outstanding RPCs, we should just cancel
|
||||
# all outstanding tasks.
|
||||
# Asyncio event loops require that all outstanding tasks be completed by the time that the
|
||||
# event loop closes. If we're at this point and there are no outstanding RPCs, we should
|
||||
# just cancel all outstanding tasks.
|
||||
#
|
||||
# We will occasionally start tasks deliberately that we know will never complete. We must cancel
|
||||
# them before shutting down the event loop.
|
||||
# We will occasionally start tasks deliberately that we know will never complete. We must
|
||||
# cancel them before shutting down the event loop.
|
||||
log.debug("Canceling all outstanding tasks")
|
||||
for task in asyncio.Task.all_tasks():
|
||||
# Don't kill ourselves, that would be silly.
|
||||
|
@ -92,7 +94,7 @@ class Stack(ComponentResource):
|
|||
try:
|
||||
func()
|
||||
finally:
|
||||
self.register_outputs(self.outputs)
|
||||
self.register_outputs(massage(self.outputs, []))
|
||||
# Intentionally leave this resource installed in case subsequent async work uses it.
|
||||
|
||||
def output(self, name: str, value: Any):
|
||||
|
@ -100,3 +102,84 @@ class Stack(ComponentResource):
|
|||
Export a stack output with a given name and value.
|
||||
"""
|
||||
self.outputs[name] = value
|
||||
|
||||
# Note: we use a List here instead of a set as many objects are unhashable. This is inefficient,
|
||||
# but python seems to offer no alternative.
|
||||
def massage(attr: Any, seen: List[Any]):
|
||||
"""
|
||||
massage takes an arbitrary python value and attempts to *deeply* convert it into
|
||||
plain-old-python-value that can registered as an output. In general, this means leaving alone
|
||||
things like strings, ints, bools. However, it does mean trying to make other values into either
|
||||
lists or dictionaries as appropriate. In general, iterable things are turned into lists, and
|
||||
dictionary-like things are turned into dictionaries.
|
||||
"""
|
||||
|
||||
# Basic primitive types (numbers, booleans, strings, etc.) don't need any special handling.
|
||||
|
||||
if is_primitive(attr):
|
||||
return attr
|
||||
|
||||
# from this point on, we have complex objects. If we see them again, we don't want to emit them
|
||||
# again fully or else we'd loop infinitely.
|
||||
if reference_contains(attr, seen):
|
||||
# Note: for Resources we hit again, emit their urn so cycles can be easily understood in
|
||||
# the popo objects.
|
||||
if isinstance(attr, Resource):
|
||||
return attr.urn
|
||||
|
||||
# otherwise just emit as nothing to stop the looping.
|
||||
return None
|
||||
|
||||
seen.append(attr)
|
||||
|
||||
# first check if the value is an actual dictionary. If so, massage the values of it to deeply
|
||||
# make sure this is a popo.
|
||||
if isinstance(attr, dict):
|
||||
result = {}
|
||||
for key, value in attr.items():
|
||||
# ignore private keys
|
||||
if not key.startswith("_"):
|
||||
result[key] = massage(value, seen)
|
||||
|
||||
return result
|
||||
|
||||
if isinstance(attr, Output):
|
||||
return attr.apply(lambda v: massage(v, seen))
|
||||
|
||||
if hasattr(attr, "__dict__"):
|
||||
# recurse on the dictionary itself. It will be handled above.
|
||||
return massage(attr.__dict__, seen)
|
||||
|
||||
# finally, recurse through iterables, converting into a list of massaged values.
|
||||
return [massage(a, seen) for a in attr]
|
||||
|
||||
|
||||
def reference_contains(val1: Any, seen: List[Any]) -> bool:
|
||||
for val2 in seen:
|
||||
if val1 is val2:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_primitive(attr: Any) -> bool:
|
||||
if attr is None:
|
||||
return True
|
||||
|
||||
if isinstance(attr, str):
|
||||
return True
|
||||
|
||||
# dictionaries, lists and dictionary-like things are not primitive.
|
||||
if isinstance(attr, dict):
|
||||
return False
|
||||
|
||||
if hasattr(attr, "__dict__"):
|
||||
return False
|
||||
|
||||
try:
|
||||
iter(attr)
|
||||
return False
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
|
|
@ -13,4 +13,24 @@
|
|||
# limitations under the License.
|
||||
import pulumi
|
||||
|
||||
pulumi.export("the-coolest", "pulumi")
|
||||
class TestClass:
|
||||
def __init__(self):
|
||||
self.num = 1
|
||||
self._private = 2
|
||||
|
||||
recursive = {"a": 1}
|
||||
recursive["b"] = 2
|
||||
recursive["c"] = recursive
|
||||
|
||||
pulumi.export("string", "pulumi")
|
||||
pulumi.export("number", 1)
|
||||
pulumi.export("boolean", True)
|
||||
pulumi.export("list", [])
|
||||
pulumi.export("list_with_none", [None])
|
||||
pulumi.export("list_of_lists", [[], []])
|
||||
pulumi.export("list_of_outputs", [[pulumi.Output.from_input(1)], pulumi.Output.from_input([2])])
|
||||
pulumi.export("set", set(["val"]))
|
||||
pulumi.export("dict", {"a": 1})
|
||||
pulumi.export("output", pulumi.Output.from_input(1))
|
||||
pulumi.export("class", TestClass())
|
||||
pulumi.export("recursive", recursive)
|
||||
|
|
|
@ -27,5 +27,16 @@ class StackOutputTest(LanghostTest):
|
|||
def register_resource_outputs(self, _ctx, _dry_run, _urn, ty, _name, _resource, outputs):
|
||||
self.assertEqual(ty, "pulumi:pulumi:Stack")
|
||||
self.assertDictEqual({
|
||||
"the-coolest": "pulumi"
|
||||
"string": "pulumi",
|
||||
"number": 1.0,
|
||||
"boolean": True,
|
||||
"list": [],
|
||||
"list_with_none": [None],
|
||||
"list_of_lists": [[], []],
|
||||
"list_of_outputs": [[1], [2]],
|
||||
"set": ["val"],
|
||||
"dict": {"a": 1.0},
|
||||
"output": 1.0,
|
||||
"class": {"num": 1.0},
|
||||
"recursive": {"a": 1.0, "b": 2.0},
|
||||
}, outputs)
|
||||
|
|
Loading…
Reference in a new issue