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

Implement configuration loader #3

Merged
merged 5 commits into from
Mar 5, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
using Google.Cloud.SecretManager.V1;
using Xunit;

namespace Gcp.SecretManager.Provider.Tests
{
public class DefaultSecretManagerConfigurationLoaderTests
{

private readonly DefaultSecretManagerConfigurationLoader _target;
public DefaultSecretManagerConfigurationLoaderTests()
{
_target = new DefaultSecretManagerConfigurationLoader();
}

[Fact]
public void Load_ReturnsAlwaysTrue()
{
var result = _target.Load(null);

Assert.True(result);
}

[Theory]
[InlineData("This__Is__MultiLevel", "This:Is:MultiLevel")]
[InlineData("SingleLevel", "SingleLevel")]
[InlineData("MultiLevel_With__One_UnderScore", "MultiLevel_With:One_UnderScore")]
public void GetKey_ReplacesUnderscoresWithColon(string secret, string expected)
{
var result = _target.GetKey(new Secret
{
SecretName = new SecretName("SomeProjectId", secret)
});

Assert.Equal(expected, result);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using Moq;
using System.Collections.Generic;
using System.Linq;
using Gcp.SecretManager.Provider.Contracts;
using Newtonsoft.Json.Bson;
using Xunit;

namespace Gcp.SecretManager.Provider.Tests
Expand All @@ -16,6 +18,8 @@ public class SecretManagerConfigurationProviderTests
private const string _projectName = "TestProjectName";
private const string _secretProjectName = "TestSecretProjectName";

private readonly SecretManagerConfigurationProvider _target;

public SecretManagerConfigurationProviderTests()
{
_testSecrets = new List<Secret>
Expand Down Expand Up @@ -59,17 +63,18 @@ public SecretManagerConfigurationProviderTests()
svn.SecretVersionId == "latest"), null))
.ReturnsAsync(response);
}

_target = new SecretManagerConfigurationProvider(_mockClient.Object, new ProjectName(_projectName), new DefaultSecretManagerConfigurationLoader());
}

[Fact]
public void Should_FetchSecrets_When_LoadIsCalled()
{
var configurationProvider = new SecretManagerConfigurationProvider(_mockClient.Object, new ProjectName(_projectName));
configurationProvider.Load();
_target.Load();

foreach (var secret in _testSecrets)
{
configurationProvider.TryGet(secret.SecretName.SecretId, out string value);
_target.TryGet(secret.SecretName.SecretId, out string value);
Assert.Equal($"{secret.SecretName.SecretId}-Value", value);
}
}
Expand All @@ -90,12 +95,11 @@ public void Should_FetchSecrets_When_LoadIsCalled_And_ExceptionIsThrown()
svn.SecretVersionId == "latest"), null))
.ThrowsAsync(new Grpc.Core.RpcException(Grpc.Core.Status.DefaultCancelled));

var configurationProvider = new SecretManagerConfigurationProvider(_mockClient.Object, new ProjectName(_projectName));
configurationProvider.Load();
_target.Load();

foreach (var secret in _testSecrets.Where(x => x.SecretName.SecretId != errorSecretId))
{
configurationProvider.TryGet(secret.SecretName.SecretId, out string value);
_target.TryGet(secret.SecretName.SecretId, out string value);
Assert.Equal($"{secret.SecretName.SecretId}-Value", value);
}
}
Expand Down Expand Up @@ -129,11 +133,45 @@ public void Should_FetcHierarchicalSecrets_When_LoadIsCalled()
_mockClient.Setup(x => x.ListSecrets(It.Is<ProjectName>(pn => pn.ProjectId == _projectName), null, null, null))
.Returns(pagedResponse);

var configurationProvider = new SecretManagerConfigurationProvider(_mockClient.Object, new ProjectName(_projectName));
configurationProvider.Load();

Assert.True(configurationProvider.TryGet(dotNetName, out var configValue));
_target.Load();

Assert.True(_target.TryGet(dotNetName, out var configValue));
Assert.Equal(value, configValue);
}

[Fact]
public void Should_CallLoaderGetKey()
{
var mockLoader = new Mock<ISecretManagerConfigurationLoader>();
mockLoader.Setup(m => m.GetKey(It.IsAny<Secret>()))
.Returns((Secret secret) => secret.SecretName.SecretId);
mockLoader.Setup(m => m.Load(It.IsAny<Secret>())).Returns(true);

var target = new SecretManagerConfigurationProvider(_mockClient.Object, new ProjectName(_projectName), mockLoader.Object);

target.Load();

mockLoader.Verify(m => m.GetKey(It.IsAny<Secret>()), Times.Exactly(_testSecrets.Count()));
}

[Fact]
public void Should_NotLoadAllSecrets()
{
var mockLoader = new Mock<ISecretManagerConfigurationLoader>();
mockLoader.Setup(m => m.GetKey(It.IsAny<Secret>()))
.Returns((Secret secret) => secret.SecretName.SecretId);
mockLoader.Setup(m => m.Load(It.IsAny<Secret>())).Returns((Secret secret) =>
secret.SecretName.SecretId != _testSecrets.First().SecretName.SecretId);

var target = new SecretManagerConfigurationProvider(_mockClient.Object, new ProjectName(_projectName), mockLoader.Object);

target.Load();

Assert.False(target.TryGet("SecretId1", out var configValue1));
Assert.True(target.TryGet("SecretId2", out var configValue2));
Assert.True(target.TryGet("SecretId3", out var configValue3));
Assert.True(target.TryGet("SecretId4", out var configValue4));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Google.Cloud.SecretManager.V1;

namespace Gcp.SecretManager.Provider.Contracts
{
public interface ISecretManagerConfigurationLoader
{
string GetKey(Secret secret);
bool Load(Secret secret);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Gcp.SecretManager.Provider.Contracts;
using Google.Cloud.SecretManager.V1;
using Microsoft.Extensions.Configuration;

namespace Gcp.SecretManager.Provider
{
public class DefaultSecretManagerConfigurationLoader : ISecretManagerConfigurationLoader
{
public virtual string GetKey(Secret secret)
=> secret.SecretName.SecretId.Replace("__", ConfigurationPath.KeyDelimiter);

public virtual bool Load(Secret secret)
=> true;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
namespace Gcp.SecretManager.Provider
using Gcp.SecretManager.Provider.Contracts;

namespace Gcp.SecretManager.Provider
{
public class SecretManagerConfigurationOptions
{
public string CredentialsPath { get; set; }
public string ProjectId { get; set; }
public ISecretManagerConfigurationLoader Loader { get; set; }
}
}
29 changes: 18 additions & 11 deletions Gcp.SecretManager.Provider/SecretManagerConfigurationProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@
using Google.Cloud.SecretManager.V1;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
using Gcp.SecretManager.Provider.Contracts;

namespace Gcp.SecretManager.Provider
{
public class SecretManagerConfigurationProvider : ConfigurationProvider
{
private readonly SecretManagerServiceClient _client;
private ProjectName _projectName;
private readonly ProjectName _projectName;
private readonly ISecretManagerConfigurationLoader _loader;

public SecretManagerConfigurationProvider(SecretManagerServiceClient client, ProjectName projectName)
public SecretManagerConfigurationProvider(
SecretManagerServiceClient client,
ProjectName projectName,
ISecretManagerConfigurationLoader loader)
{
_client = client;
_projectName = projectName;
_loader = loader;
}

public override void Load()
Expand All @@ -29,21 +35,22 @@ public async Task LoadAsync()
{
try
{
var secretVersionName = new SecretVersionName(secret.SecretName.ProjectId, secret.SecretName.SecretId, "latest");
if (!_loader.Load(secret))
{
continue;
}

var secretVersionName = new SecretVersionName(secret.SecretName.ProjectId,
secret.SecretName.SecretId, "latest");
var secretVersion = await _client.AccessSecretVersionAsync(secretVersionName);
Set(ConvertDelimiter(secret.SecretName.SecretId), secretVersion.Payload.Data.ToStringUtf8());
} catch (Grpc.Core.RpcException)
Set(_loader.GetKey(secret), secretVersion.Payload.Data.ToStringUtf8());
}
catch (Grpc.Core.RpcException)
{
// This might happen if secret is created but it has no versions available
// For now just ignore. Maybe in future we should log that something went wrong?
}
}

}

private static string ConvertDelimiter(string key)
{
return key.Replace("__", ConfigurationPath.KeyDelimiter);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)

var projectName = new ProjectName(_options.ProjectId);
var client = CreateClient();
var loader = _options.Loader ?? new DefaultSecretManagerConfigurationLoader();

return new SecretManagerConfigurationProvider(client, projectName);
return new SecretManagerConfigurationProvider(client, projectName, loader);
}

private SecretManagerServiceClient CreateClient()
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@ dotnet add package Gcp.SecretManager.Provider
config.AddGcpSecretManager(options => {
options.ProjectId = "ProjectId"; // Required
options.CredentialsPath = "/path/to/credentials"; // Optional
options.Loader = new DefaultSecretManagerConfigurationLoader() // Optional, see more info below
});
```
*You can also provide CredentialsPath with GOOGLE_APPLICATION_CREDENTIALS environment variable*

3. Ready to go!
3. Ready to go!

## Loaders
Loaders handles if secret should be loaded and mapping from Secret Manager keys to application configuration values by implementing contract `ISecretManagerConfigurationLoader`. This can be passed as an option during setup.

Contract exposes two method: `Load` and `GetKey`. `Load` method determines if the key should be loaded or not and `GetKey` handles mapping from secret to application configuration. You may access secret ID from `secret.SecretName.SecretId`

If no loader is specified then `DefaultSecretManagerConfigurationLoader` will be used. It loads all keys and hierarcy is added by adding two underscores in the secret name. Eg. `MultiLevel__Secret` maps to `MultiLevel:Secret` key in application configuration.