Skip to content

Commit 8b221ff

Browse files
KenHundleysstevenkang
authored andcommitted
Added support for SecretsManager keys
1 parent 0b83cd6 commit 8b221ff

15 files changed

+389
-121
lines changed

samples/Samples.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ VisualStudioVersion = 15.0.28010.2048
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Extensions.Configuration.SystemsManager", "..\src\Amazon.Extensions.Configuration.SystemsManager\Amazon.Extensions.Configuration.SystemsManager.csproj", "{CE965321-158B-47C8-BDA3-748F18745532}"
77
EndProject
8-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{74F8A828-62EB-4338-A106-F28D91FE4B3A}"
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples", "Samples\Samples.csproj", "{74F8A828-62EB-4338-A106-F28D91FE4B3A}"
99
EndProject
1010
Global
1111
GlobalSection(SolutionConfigurationPlatforms) = preSolution

src/Amazon.Extensions.Configuration.SystemsManager/Amazon.Extensions.Configuration.SystemsManager.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<AssemblyName>Amazon.Extensions.Configuration.SystemsManager</AssemblyName>
66
<RootNamespace>Amazon.Extensions.Configuration.SystemsManager</RootNamespace>
77
<OutputType>Library</OutputType>
8-
<VersionPrefix>1.0.2</VersionPrefix>
8+
<VersionPrefix>1.1.0</VersionPrefix>
99
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1010
<PackageId>Amazon.Extensions.Configuration.SystemsManager</PackageId>
1111
<Title>.NET Configuration Extensions for AWS Systems Manager</Title>
@@ -46,6 +46,7 @@
4646
<PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="3.3.0.*" />
4747
<PackageReference Include="AWSSDK.SimpleSystemsManagement" Version="3.3.10.*" />
4848
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.0.*" />
49+
<PackageReference Include="Newtonsoft.Json" Version="10.0.*" />
4950
</ItemGroup>
5051

5152
<ItemGroup>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using System;
17+
using Microsoft.Extensions.Configuration;
18+
19+
namespace Amazon.Extensions.Configuration.SystemsManager
20+
{
21+
/// <summary>
22+
/// This extension is an a different namespace to avoid misuse of this method which should only be called when being used from Lambda.
23+
/// </summary>
24+
public static class ConfigurationExtensions
25+
{
26+
/// <summary>
27+
/// This method blocks while any SystemsManagerConfigurationProvider added to IConfiguration are
28+
/// currently reloading the parameters from Parameter Store.
29+
///
30+
/// This is generally only needed when the provider is being called from a Lambda function. Without this call
31+
/// in a Lambda environment there is a potential of the background thread doing the refresh never running successfully.
32+
/// This can happen because the Lambda compute environment is frozen after the current Lambda event is complete.
33+
/// </summary>
34+
/// <param name="configuration"></param>
35+
/// <param name="timeSpan"></param>
36+
public static void WaitForSystemsManagerReloadToComplete(this IConfiguration configuration, TimeSpan timeSpan)
37+
{
38+
if(!(configuration is ConfigurationRoot configRoot)) return;
39+
40+
foreach(var provider in configRoot.Providers)
41+
{
42+
if(provider is SystemsManagerConfigurationProvider ssmProvider)
43+
{
44+
ssmProvider.WaitForReloadToComplete(timeSpan);
45+
}
46+
}
47+
}
48+
}
49+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
17+
using System;
18+
using System.Collections.Generic;
19+
using System.Globalization;
20+
using System.IO;
21+
using System.Linq;
22+
using Microsoft.Extensions.Configuration;
23+
using Newtonsoft.Json;
24+
using Newtonsoft.Json.Linq;
25+
26+
namespace Amazon.Extensions.Configuration.SystemsManager.Internal
27+
{
28+
public class JsonConfigurationParser : IDisposable
29+
{
30+
private JsonConfigurationParser() { }
31+
32+
private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
33+
private readonly Stack<string> _context = new Stack<string>();
34+
private string _currentPath;
35+
36+
private JsonTextReader _reader;
37+
38+
public static IDictionary<string, string> Parse(Stream input)
39+
{
40+
using (var reader = new StreamReader(input))
41+
using (var parser = new JsonConfigurationParser())
42+
{
43+
return parser.ParseInput(reader);
44+
}
45+
}
46+
47+
public static IDictionary<string, string> Parse(string input)
48+
{
49+
using (var reader = new StringReader(input))
50+
using (var parser = new JsonConfigurationParser())
51+
{
52+
return parser.ParseInput(reader);
53+
}
54+
}
55+
56+
private IDictionary<string, string> ParseInput(TextReader input)
57+
{
58+
_data.Clear();
59+
using (_reader = new JsonTextReader(input) {DateParseHandling = DateParseHandling.None})
60+
{
61+
var jsonConfig = JObject.Load(_reader);
62+
63+
VisitJObject(jsonConfig);
64+
65+
return _data;
66+
}
67+
}
68+
69+
private void VisitJObject(JObject jObject)
70+
{
71+
foreach (var property in jObject.Properties())
72+
{
73+
EnterContext(property.Name);
74+
VisitProperty(property);
75+
ExitContext();
76+
}
77+
}
78+
79+
private void VisitProperty(JProperty property)
80+
{
81+
VisitToken(property.Value);
82+
}
83+
84+
private void VisitToken(JToken token)
85+
{
86+
switch (token.Type)
87+
{
88+
case JTokenType.Object:
89+
VisitJObject(token.Value<JObject>());
90+
break;
91+
92+
case JTokenType.Array:
93+
VisitArray(token.Value<JArray>());
94+
break;
95+
96+
case JTokenType.Integer:
97+
case JTokenType.Float:
98+
case JTokenType.String:
99+
case JTokenType.Boolean:
100+
case JTokenType.Bytes:
101+
case JTokenType.Raw:
102+
case JTokenType.Null:
103+
VisitPrimitive(token.Value<JValue>());
104+
break;
105+
106+
default:
107+
throw new FormatException($"Unsupported JSON token '{_reader.TokenType}' was found. SecretId '{_reader.Path}', line {_reader.LineNumber} position {_reader.LinePosition}.");
108+
}
109+
}
110+
111+
private void VisitArray(JArray array)
112+
{
113+
for (var index = 0; index < array.Count; index++)
114+
{
115+
EnterContext(index.ToString(CultureInfo.InvariantCulture));
116+
VisitToken(array[index]);
117+
ExitContext();
118+
}
119+
}
120+
121+
private void VisitPrimitive(JValue data)
122+
{
123+
var key = _currentPath;
124+
125+
if (_data.ContainsKey(key))
126+
{
127+
throw new FormatException($"A duplicate key '{key}' was found.");
128+
}
129+
130+
_data[key] = data.Value == null ? null : data.ToString(CultureInfo.InvariantCulture);
131+
}
132+
133+
private void EnterContext(string context)
134+
{
135+
_context.Push(context);
136+
_currentPath = ConfigurationPath.Combine(_context.Reverse());
137+
}
138+
139+
private void ExitContext()
140+
{
141+
_context.Pop();
142+
_currentPath = ConfigurationPath.Combine(_context.Reverse());
143+
}
144+
145+
protected virtual void Dispose(bool disposing)
146+
{
147+
if (disposing)
148+
{
149+
((IDisposable) _reader)?.Dispose();
150+
}
151+
}
152+
153+
public void Dispose()
154+
{
155+
Dispose(true);
156+
GC.SuppressFinalize(this);
157+
}
158+
}
159+
}

src/Amazon.Extensions.Configuration.SystemsManager/Internal/SystemsManagerProcessor.cs

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,48 @@
1313
* permissions and limitations under the License.
1414
*/
1515

16-
using System.Collections.Generic;
17-
using System.Reflection;
18-
using System.Threading.Tasks;
19-
using Amazon.Extensions.NETCore.Setup;
2016
using Amazon.Runtime;
2117
using Amazon.SimpleSystemsManagement;
2218
using Amazon.SimpleSystemsManagement.Model;
19+
using System;
20+
using System.Collections.Generic;
21+
using System.Linq;
22+
using System.Reflection;
23+
using System.Threading.Tasks;
24+
using Microsoft.Extensions.Configuration;
2325

2426
namespace Amazon.Extensions.Configuration.SystemsManager.Internal
2527
{
2628
public interface ISystemsManagerProcessor
2729
{
28-
Task<IEnumerable<Parameter>> GetParametersByPathAsync(AWSOptions awsOptions, string path);
30+
Task<IDictionary<string, string>> GetDataAsync();
2931
}
3032

3133
public class SystemsManagerProcessor : ISystemsManagerProcessor
3234
{
33-
public async Task<IEnumerable<Parameter>> GetParametersByPathAsync(AWSOptions awsOptions, string path)
35+
private const string SecretsManagerPath = "/aws/reference/secretsmanager/";
36+
37+
private SystemsManagerConfigurationSource Source { get; }
38+
private IParameterProcessor ParameterProcessor { get; }
39+
40+
public SystemsManagerProcessor(SystemsManagerConfigurationSource source)
3441
{
35-
using (var client = awsOptions.CreateServiceClient<IAmazonSimpleSystemsManagement>())
42+
Source = source;
43+
ParameterProcessor = Source.ParameterProcessor ?? new DefaultParameterProcessor();
44+
}
45+
46+
public async Task<IDictionary<string, string>> GetDataAsync()
47+
{
48+
return IsSecretsManagerPath(Source.Path)
49+
? await GetParameterAsync().ConfigureAwait(false)
50+
: await GetParametersByPathAsync().ConfigureAwait(false);
51+
}
52+
53+
private async Task<IDictionary<string, string>> GetParametersByPathAsync()
54+
{
55+
using (var client = Source.AwsOptions.CreateServiceClient<IAmazonSimpleSystemsManagement>())
3656
{
37-
if(client is AmazonSimpleSystemsManagementClient impl)
57+
if (client is AmazonSimpleSystemsManagementClient impl)
3858
{
3959
impl.BeforeRequestEvent += ServiceClientBeforeRequestEvent;
4060
}
@@ -43,26 +63,64 @@ public async Task<IEnumerable<Parameter>> GetParametersByPathAsync(AWSOptions aw
4363
string nextToken = null;
4464
do
4565
{
46-
var response = await client.GetParametersByPathAsync(new GetParametersByPathRequest { Path = path, Recursive = true, WithDecryption = true, NextToken = nextToken }).ConfigureAwait(false);
66+
var response = await client.GetParametersByPathAsync(new GetParametersByPathRequest { Path = Source.Path, Recursive = true, WithDecryption = true, NextToken = nextToken }).ConfigureAwait(false);
4767
nextToken = response.NextToken;
4868
parameters.AddRange(response.Parameters);
4969
} while (!string.IsNullOrEmpty(nextToken));
5070

51-
return parameters;
71+
return AddPrefix(ProcessParameters(parameters, Source.Path, ParameterProcessor), Source.Prefix);
72+
}
73+
}
74+
75+
private async Task<IDictionary<string, string>> GetParameterAsync()
76+
{
77+
using (var client = Source.AwsOptions.CreateServiceClient<IAmazonSimpleSystemsManagement>())
78+
{
79+
if (client is AmazonSimpleSystemsManagementClient impl)
80+
{
81+
impl.BeforeRequestEvent += ServiceClientBeforeRequestEvent;
82+
}
83+
84+
var response = await client.GetParameterAsync(new GetParameterRequest { Name = Source.Path, WithDecryption = true }).ConfigureAwait(false);
85+
86+
if (!ParameterProcessor.IncludeParameter(response.Parameter, SecretsManagerPath)) return new Dictionary<string, string>();
87+
88+
var prefix = Source.Prefix ?? ParameterProcessor.GetKey(response.Parameter, SecretsManagerPath);
89+
return AddPrefix(JsonConfigurationParser.Parse(ParameterProcessor.GetValue(response.Parameter, SecretsManagerPath)), prefix);
90+
5291
}
5392
}
5493

55-
const string UserAgentHeader = "User-Agent";
56-
static readonly string _assemblyVersion = typeof(SystemsManagerProcessor).GetTypeInfo().Assembly.GetName().Version.ToString();
94+
public static bool IsSecretsManagerPath(string path) => path.StartsWith(SecretsManagerPath, StringComparison.OrdinalIgnoreCase);
5795

58-
void ServiceClientBeforeRequestEvent(object sender, RequestEventArgs e)
96+
public static IDictionary<string, string> AddPrefix(IDictionary<string, string> input, string prefix)
5997
{
60-
var args = e as Amazon.Runtime.WebServiceRequestEventArgs;
61-
if (args == null || !args.Headers.ContainsKey(UserAgentHeader))
62-
return;
98+
return string.IsNullOrEmpty(prefix)
99+
? input
100+
: input.ToDictionary(pair => $"{prefix}{ConfigurationPath.KeyDelimiter}{pair.Key}", pair => pair.Value, StringComparer.OrdinalIgnoreCase);
101+
}
102+
103+
104+
public static IDictionary<string, string> ProcessParameters(IEnumerable<Parameter> parameters, string path, IParameterProcessor parameterProcessor)
105+
{
106+
return parameters
107+
.Where(parameter => parameterProcessor.IncludeParameter(parameter, path))
108+
.Select(parameter => new
109+
{
110+
Key = parameterProcessor.GetKey(parameter, path),
111+
Value = parameterProcessor.GetValue(parameter, path)
112+
})
113+
.ToDictionary(parameter => parameter.Key, parameter => parameter.Value, StringComparer.OrdinalIgnoreCase);
114+
}
63115

116+
private const string UserAgentHeader = "User-Agent";
117+
private static readonly string AssemblyVersion = typeof(SystemsManagerProcessor).GetTypeInfo().Assembly.GetName().Version.ToString();
118+
119+
private static void ServiceClientBeforeRequestEvent(object sender, RequestEventArgs e)
120+
{
121+
if (!(e is WebServiceRequestEventArgs args) || !args.Headers.ContainsKey(UserAgentHeader)) return;
64122

65-
args.Headers[UserAgentHeader] = args.Headers[UserAgentHeader] + " SSMConfigProvider/" + _assemblyVersion;
123+
args.Headers[UserAgentHeader] = args.Headers[UserAgentHeader] + " SSMConfigProvider/" + AssemblyVersion;
66124
}
67125
}
68126
}

0 commit comments

Comments
 (0)