From a7afbd0e4e4b4e84a0ec53ec73c82970ac61d311 Mon Sep 17 00:00:00 2001 From: Zaid Ajaj Date: Tue, 25 Oct 2022 13:59:20 +0200 Subject: [PATCH] [dotnet/auto] allow deserializing complex stack config values --- ...rializing-complex-stack-config-values.yaml | 4 ++ ...ckSettingsConfigValueJsonConverterTests.cs | 40 +++++++++++---- ...ckSettingsConfigValueYamlConverterTests.cs | 18 ++++--- .../StackSettingsConfigValueJsonConverter.cs | 32 +++++++----- .../StackSettingsConfigValueYamlConverter.cs | 49 +++++++++++++------ 5 files changed, 98 insertions(+), 45 deletions(-) create mode 100644 changelog/pending/20221025--auto-dotnet--allow-deserializing-complex-stack-config-values.yaml diff --git a/changelog/pending/20221025--auto-dotnet--allow-deserializing-complex-stack-config-values.yaml b/changelog/pending/20221025--auto-dotnet--allow-deserializing-complex-stack-config-values.yaml new file mode 100644 index 000000000000..7b30b0a574d5 --- /dev/null +++ b/changelog/pending/20221025--auto-dotnet--allow-deserializing-complex-stack-config-values.yaml @@ -0,0 +1,4 @@ +changes: +- type: fix + scope: auto/dotnet + description: allow deserializing complex stack config values. diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueJsonConverterTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueJsonConverterTests.cs index c18e58ae9703..7f16dc7f379d 100644 --- a/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueJsonConverterTests.cs +++ b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueJsonConverterTests.cs @@ -56,25 +56,47 @@ public void CanDeserializeSecureString() } [Fact] - public void CannotDeserializeObject() + public void DeserializeObjectWorks() { const string json = @" { ""config"": { ""value"": { - ""test"": ""test"", - ""nested"": { - ""one"": 1, - ""two"": true, - ""three"": ""three"" - } + ""hello"": ""world"" } } } "; - Assert.Throws( - () => _serializer.DeserializeJson(json)); + var settings = _serializer.DeserializeJson(json); + Assert.NotNull(settings.Config); + Assert.True(settings!.Config!.ContainsKey("value")); + + var value = settings.Config["value"]; + Assert.NotNull(value); + Assert.Equal("{\"hello\":\"world\"}", value.Value); + Assert.False(value.IsSecure); + } + + [Fact] + public void DeserializeArrayWorks() + { + const string json = @" +{ + ""config"": { + ""value"": [1,2,3,4,5] + } +} +"; + + var settings = _serializer.DeserializeJson(json); + Assert.NotNull(settings.Config); + Assert.True(settings!.Config!.ContainsKey("value")); + + var value = settings.Config["value"]; + Assert.NotNull(value); + Assert.Equal("[1,2,3,4,5]", value.Value); + Assert.False(value.IsSecure); } [Fact] diff --git a/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueYamlConverterTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueYamlConverterTests.cs index 29304ee25ab2..3fe19fc118ef 100644 --- a/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueYamlConverterTests.cs +++ b/sdk/dotnet/Pulumi.Automation.Tests/Serialization/StackSettingsConfigValueYamlConverterTests.cs @@ -49,20 +49,22 @@ public void CanDeserializeSecureString() } [Fact] - public void CannotDeserializeObject() + public void CanDeserializeObject() { const string yaml = @" config: value: - test: test - nested: - one: 1 - two: true - three: three + hello: world "; - Assert.Throws( - () => _serializer.DeserializeYaml(yaml)); + var settings = _serializer.DeserializeYaml(yaml); + Assert.NotNull(settings.Config); + Assert.True(settings!.Config!.ContainsKey("value")); + + var value = settings.Config["value"]; + Assert.NotNull(value); + Assert.Equal("{\"hello\":\"world\"}", value.Value); + Assert.False(value.IsSecure); } [Fact] diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Json/StackSettingsConfigValueJsonConverter.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Json/StackSettingsConfigValueJsonConverter.cs index 30e179719f7b..2f4501a9b9b4 100644 --- a/sdk/dotnet/Pulumi.Automation/Serialization/Json/StackSettingsConfigValueJsonConverter.cs +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Json/StackSettingsConfigValueJsonConverter.cs @@ -19,22 +19,28 @@ public override StackSettingsConfigValue Read(ref Utf8JsonReader reader, Type ty return new StackSettingsConfigValue(value, false); } - // confirm object - if (element.ValueKind != JsonValueKind.Object) - throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}]. Expecting object if not plain string."); - - // check if secure string - var securePropertyName = options.PropertyNamingPolicy?.ConvertName("Secure") ?? "Secure"; - if (element.TryGetProperty(securePropertyName, out var secureProperty)) + // check if the element is an object, + // if it has a single property called "secure" then it is a secret value + // otherwise, serialize the whole object as JSON into the stack settings value + if (element.ValueKind == JsonValueKind.Object) { - if (secureProperty.ValueKind != JsonValueKind.String) - throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}] as a secure string. Expecting a string secret."); - - var secret = secureProperty.GetString(); - return new StackSettingsConfigValue(secret, true); + foreach(var property in element.EnumerateObject()) + { + if (string.Equals("Secure", property.Name, StringComparison.OrdinalIgnoreCase)) + { + var secureValue = property.Value; + if (secureValue.ValueKind != JsonValueKind.String) + { + throw new JsonException($"Unable to deserialize [{typeToConvert.FullName}] as a secure string. Expecting a string secret."); + } + + return new StackSettingsConfigValue(secureValue.GetString(), true); + } + } } - throw new NotSupportedException("Automation API does not currently support deserializing complex objects from stack settings."); + var serializedElement = JsonSerializer.Serialize(element); + return new StackSettingsConfigValue(serializedElement, false); } public override void Write(Utf8JsonWriter writer, StackSettingsConfigValue value, JsonSerializerOptions options) diff --git a/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/StackSettingsConfigValueYamlConverter.cs b/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/StackSettingsConfigValueYamlConverter.cs index 603f81278edb..f3690d4cc540 100644 --- a/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/StackSettingsConfigValueYamlConverter.cs +++ b/sdk/dotnet/Pulumi.Automation/Serialization/Yaml/StackSettingsConfigValueYamlConverter.cs @@ -1,6 +1,8 @@ // Copyright 2016-2021, Pulumi Corporation using System; +using System.Collections.Generic; +using System.Text.Json; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; @@ -22,27 +24,44 @@ public object ReadYaml(IParser parser, Type type) return new StackSettingsConfigValue(stringValue.Value, false); } - // confirm it is an object - if (!parser.TryConsume(out _)) - throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting object if not plain string."); + var deserializer = new Deserializer(); - // get first property name - if (!parser.TryConsume(out var firstPropertyName)) - throw new YamlException($"Unable to deserialize [{type.FullName}]. Expecting first property name inside object."); - - // check if secure string - if (string.Equals("Secure", firstPropertyName.Value, StringComparison.OrdinalIgnoreCase)) + // check whether it is an object with a single property called "secure" + // this means we have a secret value serialized into the value + if (parser.TryConsume(out var _)) { - // secure string - if (!parser.TryConsume(out var securePropertyValue)) - throw new YamlException($"Unable to deserialize [{type.FullName}] as a secure string. Expecting a string secret."); + var dictionaryFromYaml = new Dictionary(); + + // in which case, check whether it is a secure value + while (parser.TryConsume(out var firstPropertyName)) + { + if (string.Equals("Secure", firstPropertyName.Value, StringComparison.OrdinalIgnoreCase)) + { + // secure string + if (!parser.TryConsume(out var securePropertyValue)) + throw new YamlException($"Unable to deserialize [{type.FullName}] as a secure string. Expecting a string secret."); + + // needs to be 1 mapping end and then return + parser.Require(); + parser.MoveNext(); + return new StackSettingsConfigValue(securePropertyValue.Value, true); + } + + // not a secure string, so we need to add first value to the dictionary + dictionaryFromYaml.Add(firstPropertyName.Value, deserializer.Deserialize(parser)); + } - // needs to be 1 mapping end and then return parser.Require(); parser.MoveNext(); - return new StackSettingsConfigValue(securePropertyValue.Value, true); + // serialize the dictionary back into the value as JSON + var serializedDictionary = JsonSerializer.Serialize(dictionaryFromYaml); + return new StackSettingsConfigValue(serializedDictionary, false); } - throw new NotSupportedException("Automation API does not currently support deserializing complex objects from stack settings."); + + // for anything else, i.e. arrays, parse the contents as is and serialize it a JSON string + var deserializedFromYaml = deserializer.Deserialize(parser); + var serializedToJson = JsonSerializer.Serialize(deserializedFromYaml); + return new StackSettingsConfigValue(serializedToJson, false); } public void WriteYaml(IEmitter emitter, object? value, Type type)