From bd85f088f2b0acecd4ae8cb01e73687b3283a56e Mon Sep 17 00:00:00 2001 From: Justin Van Patten Date: Mon, 21 Nov 2022 11:03:50 -0800 Subject: [PATCH] [sdk/python] Don't error on type mismatches when using input values for outputs When resolving a resource's outputs, if an output value is missing (and it's not a preview), the SDK will see if there was an input prop of the same name as the output prop and use that input value as the value for the output. This can be problematic when the input prop's type isn't the same as the output prop's type. If the input value is a `dict` and the type of the output cannot be converted from a `dict`, then the SDK currently raises an `AssertionError`, which causes `pulumi up` to fail. This is inconsistent with the other language SDKs, which don't error during `pulumi up` in such cases. This change changes the behavior of the Python SDK to not error when attempting to use an input value for the output value when there is a type mismatch. Instead of raising an error, `None` is returned, which is the same value that would be used if there had been no input value available to fill-in. --- ...s-when-using-input-values-for-outputs.yaml | 4 + sdk/python/lib/pulumi/runtime/rpc.py | 28 +++++-- .../input_values_for_outputs/__init__.py | 13 +++ .../input_values_for_outputs/__main__.py | 81 +++++++++++++++++++ .../test_input_values_for_outputs.py | 34 ++++++++ 5 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 changelog/pending/20221121--sdk-python--dont-error-on-type-mismatches-when-using-input-values-for-outputs.yaml create mode 100644 sdk/python/lib/test/langhost/input_values_for_outputs/__init__.py create mode 100644 sdk/python/lib/test/langhost/input_values_for_outputs/__main__.py create mode 100644 sdk/python/lib/test/langhost/input_values_for_outputs/test_input_values_for_outputs.py diff --git a/changelog/pending/20221121--sdk-python--dont-error-on-type-mismatches-when-using-input-values-for-outputs.yaml b/changelog/pending/20221121--sdk-python--dont-error-on-type-mismatches-when-using-input-values-for-outputs.yaml new file mode 100644 index 000000000000..d5c043894196 --- /dev/null +++ b/changelog/pending/20221121--sdk-python--dont-error-on-type-mismatches-when-using-input-values-for-outputs.yaml @@ -0,0 +1,4 @@ +changes: +- type: fix + scope: sdk/python + description: Don't error on type mismatches when using input values for outputs diff --git a/sdk/python/lib/pulumi/runtime/rpc.py b/sdk/python/lib/pulumi/runtime/rpc.py index 7f08f385077d..da9515e37007 100644 --- a/sdk/python/lib/pulumi/runtime/rpc.py +++ b/sdk/python/lib/pulumi/runtime/rpc.py @@ -789,6 +789,7 @@ def translate_output_properties( typ: Optional[type] = None, transform_using_type_metadata: bool = False, path: Optional["_Path"] = None, + return_none_on_dict_type_mismatch: bool = False, ) -> Any: """ Recursively rewrite keys of objects returned by the engine to conform with a naming @@ -827,7 +828,12 @@ def translate_output_properties( if is_rpc_secret(output): unwrapped = unwrap_rpc_secret(output) result = translate_output_properties( - unwrapped, output_transformer, typ, transform_using_type_metadata + unwrapped, + output_transformer, + typ, + transform_using_type_metadata, + path, + return_none_on_dict_type_mismatch, ) return wrap_rpc_secret(result) @@ -860,7 +866,8 @@ def translate_output_properties( output_transformer, get_type(k), transform_using_type_metadata, - path=_Path(k, parent=path), + _Path(k, parent=path), + return_none_on_dict_type_mismatch, ) for k, v in output.items() } @@ -878,6 +885,8 @@ def translate_output_properties( if transform_using_type_metadata: # pylint: disable=C3001 translate = lambda k: k + elif return_none_on_dict_type_mismatch: + return None else: raise AssertionError( ( @@ -893,7 +902,8 @@ def translate_output_properties( output_transformer, get_type(k), transform_using_type_metadata, - path=_Path(k, parent=path), + _Path(k, parent=path), + return_none_on_dict_type_mismatch, ) for k, v in output.items() } @@ -906,7 +916,8 @@ def translate_output_properties( output_transformer, element_type, transform_using_type_metadata, - path=_Path(str(i), parent=path), + _Path(str(i), parent=path), + return_none_on_dict_type_mismatch, ) for i, v in enumerate(output) ] @@ -1044,14 +1055,19 @@ def resolve_outputs( for key, value in list(serialized_props.items()): translated_key = translate(key) if translated_key not in all_properties: - # input prop the engine didn't give us a final value for.Just use the value passed into the resource by - # the user. + # input prop the engine didn't give us a final value for. + # Just use the value passed into the resource by the user. + # Set `return_none_on_dict_type_mismatch` to return `None` rather than raising an error when the value + # is a dict and the type doesn't match (which is what would happen if the value didn't exist as an + # input prop). This allows `pulumi up` to work without erroring when there is an input and output prop + # with the same name but different types. all_properties[translated_key] = translate_output_properties( deserialize_property(value), translate_to_pass, types.get(key), transform_using_type_metadata, path=_Path(translated_key, resource=f"{res._name}"), + return_none_on_dict_type_mismatch=True, ) resolve_properties(resolvers, all_properties, translated_deps) diff --git a/sdk/python/lib/test/langhost/input_values_for_outputs/__init__.py b/sdk/python/lib/test/langhost/input_values_for_outputs/__init__.py new file mode 100644 index 000000000000..91774a967494 --- /dev/null +++ b/sdk/python/lib/test/langhost/input_values_for_outputs/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2016-2022, Pulumi Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/sdk/python/lib/test/langhost/input_values_for_outputs/__main__.py b/sdk/python/lib/test/langhost/input_values_for_outputs/__main__.py new file mode 100644 index 000000000000..f6d8b81b3b98 --- /dev/null +++ b/sdk/python/lib/test/langhost/input_values_for_outputs/__main__.py @@ -0,0 +1,81 @@ +# Copyright 2016-2022, Pulumi Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional + +import pulumi + + +class Instance(pulumi.CustomResource): + public_ip: pulumi.Output[str] + def __init__(self, resource_name, name: pulumi.Input[str] = None, value: pulumi.Input[str] = None, opts = None): + if opts is None: + opts = pulumi.ResourceOptions() + if name is None and not opts.urn: + raise TypeError("Missing required property 'name'") + __props__: dict = dict() + __props__["public_ip"] = None + __props__["name"] = name + __props__["value"] = value + super(Instance, self).__init__("aws:ec2/instance:Instance", resource_name, __props__, opts) + + +@pulumi.input_type +class DefaultLogGroupArgs: + def __init__(self, *, skip: Optional[bool] = None): + if skip is not None: + pulumi.set(self, "skip", skip) + + @property + @pulumi.getter + def skip(self) -> Optional[bool]: + return pulumi.get(self, "skip") + + @skip.setter + def skip(self, value: Optional[bool]): + pulumi.set(self, "skip", value) + + +@pulumi.input_type +class FargateTaskDefinitionArgs: + def __init__(self, *, log_group: Optional[DefaultLogGroupArgs] = None): + if log_group is not None: + pulumi.set(self, "log_group", log_group) + + @property + @pulumi.getter(name="logGroup") + def log_group(self) -> Optional[DefaultLogGroupArgs]: + return pulumi.get(self, "log_group") + + @log_group.setter + def log_group(self, value: Optional[DefaultLogGroupArgs]): + pulumi.set(self, "log_group", value) + + +# This resource has an input named `logGroup` typed as `DefaultLogGroupArgs` and an output named `logGroup` typed +# as `Instance`. When the provider returns no value for `logGroup`, it should not try to set the output to the +# input value due to the type mismatch. +class FargateTaskDefinition(pulumi.ComponentResource): + def __init__(self, resource_name: str, log_group: Optional[pulumi.InputType[DefaultLogGroupArgs]] = None): + __props__ = FargateTaskDefinitionArgs.__new__(FargateTaskDefinitionArgs) + __props__.__dict__["log_group"] = log_group + super().__init__("awsx:ecs:FargateTaskDefinition", resource_name, __props__, None, remote=True) + + @property + @pulumi.getter(name="logGroup") + def log_group(self) -> pulumi.Output[Optional[Instance]]: + return pulumi.get(self, "log_group") + + +task_def = FargateTaskDefinition("task_def", log_group=DefaultLogGroupArgs(skip=True)) diff --git a/sdk/python/lib/test/langhost/input_values_for_outputs/test_input_values_for_outputs.py b/sdk/python/lib/test/langhost/input_values_for_outputs/test_input_values_for_outputs.py new file mode 100644 index 000000000000..71e89543b5c2 --- /dev/null +++ b/sdk/python/lib/test/langhost/input_values_for_outputs/test_input_values_for_outputs.py @@ -0,0 +1,34 @@ +# Copyright 2016-2022, Pulumi Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os import path +from ..util import LanghostTest + + +class InputValuesForOutputsTest(LanghostTest): + """ + """ + def test_input_values_for_outputs(self): + self.run_test( + program=path.join(self.base_path(), "input_values_for_outputs"), + expected_resource_count=1) + + def register_resource(self, _ctx, _dry_run, ty, name, _resource, _dependencies, _parent, _custom, protect, + _provider, _property_deps, _delete_before_replace, _ignore_changes, _version, _import, + _replace_on_changes): + return { + "urn": self.make_urn(ty, name), + "id": name, + "object": {} # return no outputs + }