Add a ability to configure user option classes to VCAP_SERVICES bindings [#40]

Add a simplier way to bind VCAP_SERVICES service bindings to a user provided class using the ANC Options features
This commit is contained in:
Dave Tillman 2019-01-13 10:37:22 -07:00
Родитель 449d5e1cc0
Коммит e16edc6b46
9 изменённых файлов: 602 добавлений и 2 удалений

Просмотреть файл

@ -0,0 +1,79 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 permissions and
// limitations under the License.
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
namespace Steeltoe.Extensions.Configuration.CloudFoundry
{
public abstract class AbstractServiceOptions
{
public const string CONFIGURATION_PREFIX = "vcap:services";
public string Name { get; set; }
public string Label { get; set; }
public List<string> Tags { get; set; }
public string Plan { get; set; }
public void Bind(IConfiguration configuration, string serviceName)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
if (string.IsNullOrEmpty(serviceName))
{
throw new ArgumentException(nameof(serviceName));
}
var services = configuration.GetSection(CONFIGURATION_PREFIX);
var section = FindServiceSection(services, serviceName);
if (section != null)
{
section.Bind(this);
}
}
internal IConfigurationSection FindServiceSection(IConfigurationSection section, string serviceName)
{
var children = section.GetChildren();
foreach (var child in children)
{
string name = child.GetValue<string>("name");
if (serviceName == name)
{
return child;
}
}
foreach (var child in children)
{
var result = FindServiceSection(child, serviceName);
if (result != null)
{
return result;
}
}
return null;
}
}
}

Просмотреть файл

@ -15,3 +15,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Steeltoe.Extensions.Configuration.CloudFoundryBase.Test")]
[assembly: InternalsVisibleTo("Steeltoe.Extensions.Configuration.CloudFoundryCore.Test")]

Просмотреть файл

@ -23,6 +23,14 @@ namespace Steeltoe.Extensions.Configuration.CloudFoundry
/// </summary>
public static class CloudFoundryServiceCollectionExtensions
{
/// <summary>
/// Bind configuration data into <see cref="CloudFoundryApplicationOptions"/> and <see cref="CloudFoundryServicesOptions"/>
/// and add both to the provided service container as configured TOptions. You can then inject both options using the normal
/// Options pattern.
/// </summary>
/// <param name="services">the service container</param>
/// <param name="config">the applications configuration</param>
/// <returns>service container</returns>
public static IServiceCollection ConfigureCloudFoundryOptions(this IServiceCollection services, IConfiguration config)
{
if (services == null)
@ -45,5 +53,94 @@ namespace Steeltoe.Extensions.Configuration.CloudFoundry
return services;
}
/// <summary>
/// Find the Cloud Foundry service with the <paramref name="serviceName"/> in VCAP_SERVICES and bind the configuration data from
/// the provided <paramref name="config"/> into the options type and add it to the provided service container as a configured named TOption.
/// The name of the TOption will be the <paramref name="serviceName"/>. You can then inject the option using the normal Options pattern.
/// </summary>
/// <typeparam name="TOption">the options type</typeparam>
/// <param name="services">the service container</param>
/// <param name="config">the applications configuration</param>
/// <param name="serviceName">the Cloud Foundry service name to bind to the options type</param>
/// <returns>service container</returns>
public static IServiceCollection ConfigureCloudFoundryService<TOption>(this IServiceCollection services, IConfiguration config, string serviceName)
where TOption : AbstractServiceOptions
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
if (string.IsNullOrEmpty(serviceName))
{
throw new ArgumentException(nameof(serviceName));
}
services.Configure<TOption>(serviceName, (option) =>
{
option.Bind(config, serviceName);
});
return services;
}
/// <summary>
/// Find all of the Cloud Foundry services with the <paramref name="serviceLabel"/> in VCAP_SERVICES and bind the configuration data from
/// the provided <paramref name="config"/> into the options type and add them all to the provided service container as a configured named TOptions.
/// The name of each TOption will be the the name of the Cloud Foundry service binding. You can then inject all the options using the normal Options pattern.
/// </summary>
/// <typeparam name="TOption">the options type</typeparam>
/// <param name="services">the service container</param>
/// <param name="config">the applications configuration</param>
/// <param name="serviceLabel">the Cloud Foundry service label to use to bind to the options type</param>
/// <returns>serice container</returns>
public static IServiceCollection ConfigureCloudFoundryServices<TOption>(this IServiceCollection services, IConfiguration config, string serviceLabel)
where TOption : AbstractServiceOptions
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
if (string.IsNullOrEmpty(serviceLabel))
{
throw new ArgumentException(nameof(serviceLabel));
}
var servicesOptions = GetServiceOptionsFromConfiguration(config);
servicesOptions.Services.TryGetValue(serviceLabel, out Service[] cfServices);
if (cfServices != null)
{
foreach (Service s in cfServices)
{
services.ConfigureCloudFoundryService<TOption>(config, s.Name);
}
}
return services;
}
private static CloudFoundryServicesOptions GetServiceOptionsFromConfiguration(IConfiguration config)
{
if (config is IConfigurationRoot asRoot)
{
return new CloudFoundryServicesOptions(asRoot);
}
else
{
return new CloudFoundryServicesOptions(config);
}
}
}
}

Просмотреть файл

@ -0,0 +1,119 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 permissions and
// limitations under the License.
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;
namespace Steeltoe.Extensions.Configuration.CloudFoundry.Test
{
public class AbstractServiceOptionsTest
{
[Fact]
public void Bind_ThrowsWithBadArguments()
{
var opt = new MySqlServiceOption();
Assert.Throws<ArgumentNullException>(() => opt.Bind(null, "foobar"));
Assert.Throws<ArgumentException>(() => opt.Bind(new ConfigurationBuilder().Build(), null));
Assert.Throws<ArgumentException>(() => opt.Bind(new ConfigurationBuilder().Build(), string.Empty));
}
[Fact]
public void Bind_BindsConfiguration()
{
// Arrange
var configJson = @"
{ 'vcap': {
'services' : {
'p-mysql': [
{
'name': 'mySql1',
'label': 'p-mysql',
'tags': [
'mysql',
'relational'
],
'plan': '100mb-dev',
'credentials': {
'hostname': '192.168.0.97',
'port': 3306,
'name': 'cf_0f5dda44_e678_4727_993f_30e6d455cc31',
'username': '9vD0Mtk3wFFuaaaY',
'password': 'Cjn4HsAiKV8sImst',
'uri': 'mysql://9vD0Mtk3wFFuaaaY:Cjn4HsAiKV8sImst@192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?reconnect=true',
'jdbcUrl': 'jdbc:mysql://192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?user=9vD0Mtk3wFFuaaaY&password=Cjn4HsAiKV8sImst'
}
},
{
'name': 'mySql2',
'label': 'p-mysql',
'tags': [
'mysql',
'relational'
],
'plan': '100mb-dev',
'credentials': {
'hostname': '192.168.0.97',
'port': 3306,
'name': 'cf_0f5dda44_e678_4727_993f_30e6d455cc31',
'username': '9vD0Mtk3wFFuaaaY',
'password': 'Cjn4HsAiKV8sImst',
'uri': 'mysql://9vD0Mtk3wFFuaaaY:Cjn4HsAiKV8sImst@192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?reconnect=true',
'jdbcUrl': 'jdbc:mysql://192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?user=9vD0Mtk3wFFuaaaY&password=Cjn4HsAiKV8sImst'
}
}
]
}
}
}";
var memStream = CloudFoundryConfigurationProvider.GetMemoryStream(configJson);
var jsonSource = new JsonStreamConfigurationSource(memStream);
var builder = new ConfigurationBuilder().Add(jsonSource);
var config = builder.Build();
var opt = new MySqlServiceOption();
opt.Bind(config, "mySql2");
Assert.Equal("mySql2", opt.Name);
Assert.Equal("p-mysql", opt.Label);
var opt2 = new MySqlServiceOption();
opt2.Bind(config, "mySql1");
Assert.Equal("mySql1", opt2.Name);
Assert.Equal("p-mysql", opt2.Label);
}
[Fact]
public void Bind_DoesNotBindsConfiguration()
{
// Arrange
var configJson = @"
{ 'foo': {
'bar' : {
}
}
}";
var memStream = CloudFoundryConfigurationProvider.GetMemoryStream(configJson);
var jsonSource = new JsonStreamConfigurationSource(memStream);
var builder = new ConfigurationBuilder().Add(jsonSource);
var config = builder.Build();
var opt = new MySqlServiceOption();
opt.Bind(config, "mySql2");
Assert.NotEqual("mySql2", opt.Name);
Assert.NotEqual("p-mysql", opt.Label);
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 permissions and
// limitations under the License.
namespace Steeltoe.Extensions.Configuration.CloudFoundry.Test
{
public class MySqlCredentials
{
public string Hostname { get; set; }
public int Port { get; set; }
public string Name { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Uri { get; set; }
public string JdbcUrl { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,25 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 permissions and
// limitations under the License.
namespace Steeltoe.Extensions.Configuration.CloudFoundry.Test
{
public class MySqlServiceOption : AbstractServiceOptions
{
public MySqlServiceOption()
{
}
public MySqlCredentials Credentials { get; set; }
}
}

Просмотреть файл

@ -60,9 +60,198 @@ namespace Steeltoe.Extensions.Configuration.CloudFoundry.Test
var serviceProvider = services.BuildServiceProvider();
var app = serviceProvider.GetService<IOptions<CloudFoundryApplicationOptions>>();
Assert.NotNull(app);
Assert.NotNull(app.Value);
var service = serviceProvider.GetService<IOptions<CloudFoundryServicesOptions>>();
Assert.NotNull(service);
Assert.NotNull(service.Value);
}
[Fact]
public void ConfigureCloudFoundryService_ThrowsIfServiceCollectionNull()
{
// Arrange
IServiceCollection services = null;
IConfigurationRoot config = null;
// Act and Assert
Assert.Throws<ArgumentNullException>(() => CloudFoundryServiceCollectionExtensions.ConfigureCloudFoundryService<MySqlServiceOption>(services, config, "foobar"));
}
[Fact]
public void ConfigureCloudFoundryService_ThrowsIfConfigurtionNull()
{
// Arrange
IServiceCollection services = new ServiceCollection();
IConfigurationRoot config = null;
// Act and Assert
Assert.Throws<ArgumentNullException>(() => CloudFoundryServiceCollectionExtensions.ConfigureCloudFoundryService<MySqlServiceOption>(services, config, "foobar"));
}
[Fact]
public void ConfigureCloudFoundryService_BadServiceName()
{
// Arrange
IServiceCollection services = new ServiceCollection();
IConfigurationRoot config = new ConfigurationBuilder().Build();
// Act and Assert
Assert.Throws<ArgumentException>(() => CloudFoundryServiceCollectionExtensions.ConfigureCloudFoundryService<MySqlServiceOption>(services, config, null));
Assert.Throws<ArgumentException>(() => CloudFoundryServiceCollectionExtensions.ConfigureCloudFoundryService<MySqlServiceOption>(services, config, string.Empty));
}
[Fact]
public void ConfigureCloudFoundryService_ConfiguresService()
{
// Arrange
var configJson = @"
{ 'vcap': {
'services' : {
'p-mysql': [
{
'name': 'mySql1',
'label': 'p-mysql',
'tags': [
'mysql',
'relational'
],
'plan': '100mb-dev',
'credentials': {
'hostname': '192.168.0.97',
'port': 3306,
'name': 'cf_0f5dda44_e678_4727_993f_30e6d455cc31',
'username': '9vD0Mtk3wFFuaaaY',
'password': 'Cjn4HsAiKV8sImst',
'uri': 'mysql://9vD0Mtk3wFFuaaaY:Cjn4HsAiKV8sImst@192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?reconnect=true',
'jdbcUrl': 'jdbc:mysql://192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?user=9vD0Mtk3wFFuaaaY&password=Cjn4HsAiKV8sImst'
}
},
{
'name': 'mySql2',
'label': 'p-mysql',
'tags': [
'mysql',
'relational'
],
'plan': '100mb-dev',
'credentials': {
'hostname': '192.168.0.97',
'port': 3306,
'name': 'cf_0f5dda44_e678_4727_993f_30e6d455cc31',
'username': '9vD0Mtk3wFFuaaaY',
'password': 'Cjn4HsAiKV8sImst',
'uri': 'mysql://9vD0Mtk3wFFuaaaY:Cjn4HsAiKV8sImst@192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?reconnect=true',
'jdbcUrl': 'jdbc:mysql://192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?user=9vD0Mtk3wFFuaaaY&password=Cjn4HsAiKV8sImst'
}
}
]
}
}
}";
var memStream = CloudFoundryConfigurationProvider.GetMemoryStream(configJson);
var jsonSource = new JsonStreamConfigurationSource(memStream);
var builder = new ConfigurationBuilder().Add(jsonSource);
var config = builder.Build();
var services = new ServiceCollection();
services.AddOptions();
// Act and Assert
CloudFoundryServiceCollectionExtensions.ConfigureCloudFoundryService<MySqlServiceOption>(services, config, "mySql2");
var serviceProvider = services.BuildServiceProvider();
var snapShot = serviceProvider.GetRequiredService<IOptionsSnapshot<MySqlServiceOption>>();
var monitor = serviceProvider.GetRequiredService<IOptionsMonitor<MySqlServiceOption>>();
var snapOpt = snapShot.Get("mySql2");
var monOpt = monitor.Get("mySql2");
Assert.NotNull(snapOpt);
Assert.NotNull(monOpt);
Assert.Equal("mySql2", snapOpt.Name);
Assert.Equal("p-mysql", snapOpt.Label);
Assert.Equal("mySql2", monOpt.Name);
Assert.Equal("p-mysql", monOpt.Label);
}
[Fact]
public void ConfigureCloudFoundryServices_ConfiguresServices()
{
// Arrange
var configJson = @"
{ 'vcap': {
'services' : {
'p-mysql': [
{
'name': 'mySql1',
'label': 'p-mysql',
'tags': [
'mysql',
'relational'
],
'plan': '100mb-dev',
'credentials': {
'hostname': '192.168.0.97',
'port': 3306,
'name': 'cf_0f5dda44_e678_4727_993f_30e6d455cc31',
'username': '9vD0Mtk3wFFuaaaY',
'password': 'Cjn4HsAiKV8sImst',
'uri': 'mysql://9vD0Mtk3wFFuaaaY:Cjn4HsAiKV8sImst@192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?reconnect=true',
'jdbcUrl': 'jdbc:mysql://192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?user=9vD0Mtk3wFFuaaaY&password=Cjn4HsAiKV8sImst'
}
},
{
'name': 'mySql2',
'label': 'p-mysql',
'tags': [
'mysql',
'relational'
],
'plan': '100mb-dev',
'credentials': {
'hostname': '192.168.0.97',
'port': 3306,
'name': 'cf_0f5dda44_e678_4727_993f_30e6d455cc31',
'username': '9vD0Mtk3wFFuaaaY',
'password': 'Cjn4HsAiKV8sImst',
'uri': 'mysql://9vD0Mtk3wFFuaaaY:Cjn4HsAiKV8sImst@192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?reconnect=true',
'jdbcUrl': 'jdbc:mysql://192.168.0.97:3306/cf_0f5dda44_e678_4727_993f_30e6d455cc31?user=9vD0Mtk3wFFuaaaY&password=Cjn4HsAiKV8sImst'
}
}
]
}
}
}";
var memStream = CloudFoundryConfigurationProvider.GetMemoryStream(configJson);
var jsonSource = new JsonStreamConfigurationSource(memStream);
var builder = new ConfigurationBuilder().Add(jsonSource);
var config = builder.Build();
var services = new ServiceCollection();
services.AddOptions();
// Act and Assert
CloudFoundryServiceCollectionExtensions.ConfigureCloudFoundryServices<MySqlServiceOption>(services, config, "p-mysql");
var serviceProvider = services.BuildServiceProvider();
var snapShot = serviceProvider.GetRequiredService<IOptionsSnapshot<MySqlServiceOption>>();
var monitor = serviceProvider.GetRequiredService<IOptionsMonitor<MySqlServiceOption>>();
var snapOpt1 = snapShot.Get("mySql1");
var monOpt1 = monitor.Get("mySql1");
Assert.NotNull(snapOpt1);
Assert.NotNull(monOpt1);
Assert.Equal("mySql1", snapOpt1.Name);
Assert.Equal("p-mysql", snapOpt1.Label);
Assert.Equal("mySql1", monOpt1.Name);
Assert.Equal("p-mysql", monOpt1.Label);
var snapOpt2 = snapShot.Get("mySql2");
var monOpt2 = monitor.Get("mySql2");
Assert.NotNull(snapOpt2);
Assert.NotNull(monOpt2);
Assert.Equal("mySql2", snapOpt2.Name);
Assert.Equal("p-mysql", snapOpt2.Label);
Assert.Equal("mySql2", monOpt2.Name);
Assert.Equal("p-mysql", monOpt2.Label);
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 permissions and
// limitations under the License.
namespace Steeltoe.Extensions.Configuration.CloudFoundry.Test
{
public class MySqlCredentials
{
public string Hostname { get; set; }
public int Port { get; set; }
public string Name { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Uri { get; set; }
public string JdbcUrl { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,24 @@
// Copyright 2017 the original author or authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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 permissions and
// limitations under the License.
namespace Steeltoe.Extensions.Configuration.CloudFoundry.Test
{
public class MySqlServiceOption : AbstractServiceOptions
{
public MySqlServiceOption()
{
}
public MySqlCredentials Credentials { get; set; }
}
}