Skip to content

Commit

Permalink
Handle null runtime values in InputMap/List cast (#459)
Browse files Browse the repository at this point in the history
Fixes #456

This is a bit of a sad fix but `InputMap` used to tolerate the implicit
cast from dictionary being called with null by just passing that null
down. If you tried to call Add or enumerate it would NRE, but if it just
got assigned to another resource property the serialiser could handle
it.

#449 broke this because it
always tried to enumerate the incoming immutable dictionary to build the
new internal nested immutable dictionary. This fixes it to behave as
before were null flows through as null.
  • Loading branch information
Frassle authored Feb 19, 2025
1 parent 654f993 commit 8f50081
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 6 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/bug-fixes-459.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
component: sdk
kind: bug-fixes
body: Handle null in InputMap/List implicit conversions
time: 2025-02-12T16:47:02.408546Z
custom:
PR: "459"
40 changes: 40 additions & 0 deletions sdk/Pulumi.Tests/Core/InputTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,46 @@ public Task InputMapAdd()
await Assert.ThrowsAsync<ArgumentException>(() => map.ToOutput().DataTask).ConfigureAwait(false);
});

// Regression test for https://github.com/pulumi/pulumi-dotnet/issues/456
[Fact]
public Task InputMapNull()
=> RunInPreview(async () =>
{
ImmutableDictionary<string, string>? nullDict = null;
var map = (InputMap<string>)nullDict!;

var data = await map.ToOutput().DataTask.ConfigureAwait(false);
Assert.True(data.IsKnown);
Assert.Null(data.Value);

var nullDictOutput = Output.Create(nullDict);
map = (InputMap<string>)nullDictOutput!;

data = await map.ToOutput().DataTask.ConfigureAwait(false);
Assert.True(data.IsKnown);
Assert.Null(data.Value);
});

// Regression test for https://github.com/pulumi/pulumi-dotnet/issues/456
[Fact]
public Task InputListNull()
=> RunInPreview(async () =>
{
ImmutableArray<string> nullList = default;
var list = (InputList<string>)nullList;

var data = await list.ToOutput().DataTask.ConfigureAwait(false);
Assert.True(data.IsKnown);
Assert.True(data.Value.IsDefault);

var nullListOutput = Output.Create(nullList);
list = (InputList<string>)nullListOutput!;

data = await list.ToOutput().DataTask.ConfigureAwait(false);
Assert.True(data.IsKnown);
Assert.True(data.Value.IsDefault);
});

private class SampleArgs
{
public readonly InputList<Union<string, int>> List = new InputList<Union<string, int>>();
Expand Down
15 changes: 10 additions & 5 deletions sdk/Pulumi/Core/InputList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,15 @@ public sealed class InputList<T> : Input<ImmutableArray<T>>, IEnumerable, IAsync
/// inner elements is an unknown value.
///
/// To do that we keep a separate value of the form <c>Input{ImmutableArray{Input{T}}}</c>/> which each
/// time we set syncs the flattened value to the base <c>Input{ImmutableArray{T}}</c>.
/// time we set syncs the flattened value to the base <c>Input{ImmutableArray{T}}</c>.
/// </summary>
Input<ImmutableArray<Input<T>>> Value
{
get => _inputValue;
set
{
_inputValue = value;
_outputValue = _inputValue.Apply(inputs => Output.All(inputs));
_outputValue = _inputValue.Apply(inputs => inputs.IsDefault ? Output.Create((ImmutableArray<T>)default) : Output.All(inputs));
}
}

Expand All @@ -70,7 +70,7 @@ public InputList() : this(ImmutableArray<Input<T>>.Empty)
}

private InputList(Input<ImmutableArray<Input<T>>> values)
: base(values.Apply(values => Output.All(values)))
: base(values.Apply(values => values.IsDefault ? Output.Create((ImmutableArray<T>)default) : Output.All(values)))
{
_inputValue = values;
}
Expand Down Expand Up @@ -153,10 +153,10 @@ public static implicit operator InputList<T>(List<Input<T>> values)
#region construct from immutable array

public static implicit operator InputList<T>(ImmutableArray<T> values)
=> values.SelectAsArray(v => (Input<T>)v);
=> values.IsDefault ? default : values.SelectAsArray(v => (Input<T>)v);

public static implicit operator InputList<T>(ImmutableArray<Output<T>> values)
=> values.SelectAsArray(v => (Input<T>)v);
=> values.IsDefault ? default : values.SelectAsArray(v => (Input<T>)v);

public static implicit operator InputList<T>(ImmutableArray<Input<T>> values)
=> new InputList<T>(values);
Expand All @@ -177,6 +177,11 @@ public static implicit operator InputList<T>(Output<IEnumerable<T>> values)
public static implicit operator InputList<T>(Output<ImmutableArray<T>> values)
=> new InputList<T>(values.Apply(values =>
{
if (values.IsDefault)
{
return default;
}

var builder = ImmutableArray.CreateBuilder<Input<T>>(values.Length);
foreach (var value in values)
{
Expand Down
14 changes: 14 additions & 0 deletions sdk/Pulumi/Core/InputMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ private static Input<ImmutableDictionary<string, V>> Flatten(Input<ImmutableDict
{
return inputs.Apply(inputs =>
{
// Backcompat: See the comment in the implicit conversion from Output<ImmutableDictionary<string, V>>.
if (inputs == null)
{
return null!;
}

var list = inputs.Select(kv => kv.Value.Apply(value => KeyValuePair.Create(kv.Key, value)));
return Output.All(list).Apply(kvs =>
{
Expand Down Expand Up @@ -185,6 +191,14 @@ public static implicit operator InputMap<V>(Output<IDictionary<string, V>> value
public static implicit operator InputMap<V>(Output<ImmutableDictionary<string, V>> values)
=> new InputMap<V>(values.Apply(values =>
{
// Backwards compatibility: if the immutable dictionary is null just flow through the nullness. This is
// against the nullability annotations but it used to "work" before
// https://github.com/pulumi/pulumi-dotnet/pull/449.
if (values == null)
{
return null!;
}

var builder = ImmutableDictionary.CreateBuilder<string, Input<V>>();
foreach (var value in values)
{
Expand Down
2 changes: 1 addition & 1 deletion sdk/Pulumi/Pulumi.xml
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@
inner elements is an unknown value.

To do that we keep a separate value of the form <c>Input{ImmutableArray{Input{T}}}</c>/> which each
time we set syncs the flattened value to the base <c>Input{ImmutableArray{T}}</c>.
time we set syncs the flattened value to the base <c>Input{ImmutableArray{T}}</c>.
</summary>
</member>
<member name="M:Pulumi.InputList`1.Add(Pulumi.InputList{`0})">
Expand Down

0 comments on commit 8f50081

Please sign in to comment.