Skip to content
11 changes: 11 additions & 0 deletions .autover/changes/44bf0928-edf9-4888-aebb-196b6364ffa8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Amazon.Extensions.Configuration.SystemsManager",
"Type": "Minor",
"ChangelogMessages": [
"Add opt-in JsonOrStringParameterProcessor processor supporting parameters being either strings or JSON"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
*
* http://aws.amazon.com/apache2.0
*
*
* or in the "license" file accompanying this file. This file 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
Expand All @@ -19,6 +19,7 @@
using Amazon.SimpleSystemsManagement;
using Amazon.SimpleSystemsManagement.Model;
using Microsoft.Extensions.Configuration;
using static Amazon.Extensions.Configuration.SystemsManager.Utils.ParameterProcessorUtil;

namespace Amazon.Extensions.Configuration.SystemsManager
{
Expand All @@ -33,10 +34,11 @@ public class DefaultParameterProcessor : IParameterProcessor

public virtual bool IncludeParameter(Parameter parameter, string path) => true;

/// Get the extra prefix if the path is subset of parameter name.
public virtual string GetKey(Parameter parameter, string path)
{
var name = parameter.Name.StartsWith(path, StringComparison.OrdinalIgnoreCase)
? parameter.Name.Substring(path.Length)
var name = parameter.Name.StartsWith(path, StringComparison.OrdinalIgnoreCase)
? parameter.Name.Substring(path.Length)
: parameter.Name;
#if NETCOREAPP3_1_OR_GREATER
return name.TrimStart('/').Replace("/", KeyDelimiter, StringComparison.InvariantCulture);
Expand All @@ -47,51 +49,25 @@ public virtual string GetKey(Parameter parameter, string path)

public virtual string GetValue(Parameter parameter, string path) => parameter.Value;

private IEnumerable<KeyValuePair<string, string>> ParseStringList(Parameter parameter, string path)
{
// Items in a StringList must be separated by a comma (,).
// You can't use other punctuation or special characters to escape items in the list.
// If you have a parameter value that requires a comma, then use the String type.
// https://docs.aws.amazon.com/systems-manager/latest/userguide/param-create-cli.html#param-create-cli-stringlist
return parameter.Value.Split(',').Select((value, idx) =>
new KeyValuePair<string, string>($"{GetKey(parameter, path)}:{idx}", value));
}

public virtual IDictionary<string, string> ProcessParameters(IEnumerable<Parameter> parameters, string path)
{
var result = new List<KeyValuePair<string, string>>();
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

foreach (var parameter in parameters.Where(parameter => IncludeParameter(parameter, path)))
{
var keyPrefix = GetKey(parameter, path);
var value = GetValue(parameter, path);

if (parameter.Type == ParameterType.StringList)
{
var parameterList = ParseStringList(parameter, path);

// Check for duplicate parameter keys.
var stringListKeys = parameterList.Select(p => p.Key);
var duplicateKeys = result.Where(r => stringListKeys.Contains(r.Key, StringComparer.OrdinalIgnoreCase)).Select(r => r.Key);
if (duplicateKeys.Count() > 0)
{
throw new DuplicateParameterException($"Duplicate parameters '{string.Join(";", duplicateKeys)}' found. Parameter keys are case-insensitive.");
}

result.AddRange(parameterList);
ParseStringListParameter(keyPrefix, value, result);
continue;
}
else
{
string parameterKey = GetKey(parameter, path);

// Check for duplicate parameter key.
if (result.Any(r => string.Equals(r.Key, parameterKey, StringComparison.OrdinalIgnoreCase)))
{
throw new DuplicateParameterException($"Duplicate parameter '{parameterKey}' found. Parameter keys are case-insensitive.");
}

result.Add(new KeyValuePair<string, string>(parameterKey, GetValue(parameter, path)));
}
ParseStringParameter(keyPrefix, value, result);
}

return result.ToDictionary(parameter => parameter.Key, parameter => parameter.Value,
StringComparer.OrdinalIgnoreCase);
return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Amazon.SimpleSystemsManagement;
using Amazon.SimpleSystemsManagement.Model;
using static Amazon.Extensions.Configuration.SystemsManager.Utils.ParameterProcessorUtil;

namespace Amazon.Extensions.Configuration.SystemsManager
{
/// <inheritdoc />
/// <summary>
/// A processor that prioritizes JSON parameters but falls back to string parameters,
/// in accordance with Systems Manager's suggested naming conventions
/// </summary>
public class JsonOrStringParameterProcessor : DefaultParameterProcessor
{
public override IDictionary<string, string> ProcessParameters(IEnumerable<Parameter> parameters, string path)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

foreach (var parameter in parameters.Where(parameter => IncludeParameter(parameter, path)))
{
var keyPrefix = GetKey(parameter, path);
var value = GetValue(parameter, path);

if (parameter.Type == ParameterType.StringList)
{
ParseStringListParameter(keyPrefix, value, result);
continue;
}

try
{
ParseJsonParameter(keyPrefix, value, result);
}
catch (JsonException)
{
ParseStringParameter(keyPrefix, value, result);
}
}

return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
*
* http://aws.amazon.com/apache2.0
*
*
* or in the "license" file accompanying this file. This file 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
Expand All @@ -16,42 +16,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Amazon.Extensions.Configuration.SystemsManager.Internal;
using Amazon.SimpleSystemsManagement.Model;
using Microsoft.Extensions.Configuration;
using static Amazon.Extensions.Configuration.SystemsManager.Utils.ParameterProcessorUtil;

namespace Amazon.Extensions.Configuration.SystemsManager
{
/// <inheritdoc />
/// <summary>
/// Default parameter processor based on Systems Manager's suggested naming convention
/// A processor specifically designed for handling JSON parameters,
/// following Systems Manager's recommended naming conventions
/// </summary>
public class JsonParameterProcessor : DefaultParameterProcessor
{
public override IDictionary<string, string> ProcessParameters(IEnumerable<Parameter> parameters, string path)
{
var outputDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (Parameter parameter in parameters.Where(parameter => IncludeParameter(parameter, path)))
{
// Get the extra prefix if the path is subset of paramater name.
string prefix = GetKey(parameter, path);

var parameterDictionary = JsonConfigurationParser.Parse(parameter.Value);
foreach (var keyValue in parameterDictionary)
{
string key = (!string.IsNullOrEmpty(prefix) ? ConfigurationPath.Combine(prefix, keyValue.Key) : keyValue.Key);
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

// Check for duplicate parameter key.
if (outputDictionary.ContainsKey(key))
{
throw new DuplicateParameterException($"Duplicate parameter '{key}' found. Parameter keys are case-insensitive.");
}
foreach (var parameter in parameters.Where(parameter => IncludeParameter(parameter, path)))
{
var keyPrefix = GetKey(parameter, path);
var value = GetValue(parameter, path);

outputDictionary.Add(key, keyValue.Value);
}
ParseJsonParameter(keyPrefix, value, result);
}

return outputDictionary;
return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Linq;
using Amazon.Extensions.Configuration.SystemsManager.Internal;
using Microsoft.Extensions.Configuration;
using System.Text.Json;

namespace Amazon.Extensions.Configuration.SystemsManager.Utils
{
public static class ParameterProcessorUtil
{
/// <summary>
/// Parses the SSM parameter as JSON
/// </summary>
/// <param name="keyPrefix">prefix to add in configution key</param>
/// <param name="value">SSM parameter value</param>
/// <param name="result">append the parsed JSON value into</param>
/// <exception cref="DuplicateParameterException">SSM parameter key is already present in <paramref name="result"/></exception>
/// <exception cref="JsonException"><paramref name="value" /> does not represent a valid single JSON value.</exception>
public static void ParseJsonParameter(string keyPrefix, string value, IDictionary<string, string> result)
{
foreach (var kv in JsonConfigurationParser.Parse(value))
{
var key = !string.IsNullOrEmpty(keyPrefix) ? ConfigurationPath.Combine(keyPrefix, kv.Key) : kv.Key;
if (result.ContainsKey(key))
{
throw new DuplicateParameterException($"Duplicate parameter '{key}' found. Parameter keys are case-insensitive.");
}

result.Add(key, kv.Value);
}
}

/// <summary>
/// Parses the StringList SSM parameter as List of String
/// <br/><br/>
/// Items in a StringList must be separated by a comma (,).
/// You can't use other punctuation or special characters to escape items in the list.
/// If you have a parameter value that requires a comma, then use the String type.
/// https://docs.aws.amazon.com/systems-manager/latest/userguide/param-create-cli.html#param-create-cli-stringlist
/// </summary>
/// <param name="keyPrefix">prefix to add in configution key</param>
/// <param name="value">SSM parameter</param>
/// <param name="result">append the parsed string list value into</param>
/// <exception cref="DuplicateParameterException">SSM parameter key is already present in <paramref name="result"/></exception>
public static void ParseStringListParameter(string keyPrefix, string value, IDictionary<string, string> result)
{
var configKeyValuePairs = value
.Split(',')
.Select((eachValue, idx) => new KeyValuePair<string, string>($"{keyPrefix}{ConfigurationPath.KeyDelimiter}{idx}", eachValue));

foreach (var kv in configKeyValuePairs)
{
if (result.ContainsKey(kv.Key))
{
throw new DuplicateParameterException($"Duplicate parameter '{kv.Key}' found. Parameter keys are case-insensitive.");
}

result.Add(kv.Key, kv.Value);
}
}

/// <summary>
/// Parses the SSM parameter as String
/// </summary>
/// <param name="key">key to be used for configution key</param>
/// <param name="value">SSM parameter</param>
/// <param name="result">append the parsed string value into</param>
/// <exception cref="DuplicateParameterException">SSM parameter key is already present in <paramref name="result"/></exception>
public static void ParseStringParameter(string key, string value, IDictionary<string, string> result)
{
if (result.ContainsKey(key))
{
throw new DuplicateParameterException($"Duplicate parameter '{key}' found. Parameter keys are case-insensitive.");
}

result.Add(key, value);
}
}
}
Loading
Loading