Skip to content

Commit

Permalink
Merge #11422
Browse files Browse the repository at this point in the history
11422: [sdk/python] Don't error on type mismatches when using input values for outputs r=justinvp a=justinvp

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. We do this across our SDKs. It can be problematic when the input prop's type isn't the same as the output prop's type. In the Python SDK, 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.

Note that this behavior of filling in input values for missing output values has mostly worked OK for custom resources where there's generally a 1:1 match between inputs/outputs, but obviously it does not work well when that's not the case (which is likely to be more common now with components). We likely need to re-think this overall for all SDKs, as a separate, future change (i.e. having some way to opt-in to disabling this behavior).

Fixes #11416

Co-authored-by: Justin Van Patten <jvp@justinvp.com>
  • Loading branch information
bors[bot] and justinvp committed Nov 21, 2022
2 parents f478b11 + bd85f08 commit 1ee98da
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 6 deletions.
@@ -0,0 +1,4 @@
changes:
- type: fix
scope: sdk/python
description: Don't error on type mismatches when using input values for outputs
28 changes: 22 additions & 6 deletions sdk/python/lib/pulumi/runtime/rpc.py
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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()
}
Expand All @@ -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(
(
Expand All @@ -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()
}
Expand All @@ -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)
]
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions 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.
81 changes: 81 additions & 0 deletions 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))
@@ -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
}

0 comments on commit 1ee98da

Please sign in to comment.