Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dotnet/auto] allow deserializing complex stack config values #11143

Merged
merged 1 commit into from Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,4 @@
changes:
- type: fix
scope: auto/dotnet
description: allow deserializing complex stack config values.
Expand Up @@ -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<NotSupportedException>(
() => _serializer.DeserializeJson<StackSettings>(json));
var settings = _serializer.DeserializeJson<StackSettings>(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<StackSettings>(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]
Expand Down
Expand Up @@ -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<YamlException>(
() => _serializer.DeserializeYaml<StackSettings>(yaml));
var settings = _serializer.DeserializeYaml<StackSettings>(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]
Expand Down
Expand Up @@ -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)
Expand Down
@@ -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;
Expand All @@ -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<MappingStart>(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<Scalar>(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<MappingStart>(out var _))
{
// secure string
if (!parser.TryConsume<Scalar>(out var securePropertyValue))
throw new YamlException($"Unable to deserialize [{type.FullName}] as a secure string. Expecting a string secret.");
var dictionaryFromYaml = new Dictionary<string, object?>();

// in which case, check whether it is a secure value
while (parser.TryConsume<Scalar>(out var firstPropertyName))
{
if (string.Equals("Secure", firstPropertyName.Value, StringComparison.OrdinalIgnoreCase))
{
// secure string
if (!parser.TryConsume<Scalar>(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<MappingEnd>();
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<object?>(parser));
}

// needs to be 1 mapping end and then return
parser.Require<MappingEnd>();
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<object?>(parser);
var serializedToJson = JsonSerializer.Serialize(deserializedFromYaml);
return new StackSettingsConfigValue(serializedToJson, false);
}

public void WriteYaml(IEmitter emitter, object? value, Type type)
Expand Down