diff --git a/Consul.Test/AgentTest.cs b/Consul.Test/AgentTest.cs index e31e25a87..441d8146e 100644 --- a/Consul.Test/AgentTest.cs +++ b/Consul.Test/AgentTest.cs @@ -709,5 +709,89 @@ public async Task Agent_Register_UseAliasCheck() Assert.Equal(HealthStatus.Passing, checks.Response[check2Id].Status); Assert.Equal("All checks passing.", checks.Response[check2Id].Output); } + + [Fact] + public async Task Agent_Service_Register_With_Connect() + { + // Arrange + var destinationServiceID = KVTest.GenerateTestKeyName(); + var destinationServiceRegistrationParameters = new AgentServiceRegistration + { + ID = destinationServiceID, + Name = destinationServiceID, + Port = 8000, + Check = new AgentServiceCheck + { + TTL = TimeSpan.FromSeconds(15) + }, + Connect = new AgentServiceConnect + { + SidecarService = new AgentServiceRegistration + { + Port = 8001 + } + } + }; + + var sourceServiceID = KVTest.GenerateTestKeyName(); + var sourceServiceRegistrationParameters = new AgentServiceRegistration + { + ID = sourceServiceID, + Name = sourceServiceID, + Port = 9000, + Check = new AgentServiceCheck + { + TTL = TimeSpan.FromSeconds(15) + }, + Tags = new string[] { "tag1", "tag2" }, + Connect = new AgentServiceConnect + { + SidecarService = new AgentServiceRegistration + { + Port = 9001, + Proxy = new AgentServiceProxy + { + Upstreams = new AgentServiceProxyUpstream[] { new AgentServiceProxyUpstream { DestinationName = destinationServiceID, LocalBindPort = 9002 } } + } + } + } + }; + + // Act + await _client.Agent.ServiceRegister(destinationServiceRegistrationParameters); + await _client.Agent.ServiceRegister(sourceServiceRegistrationParameters); + + // Assert + var services = await _client.Agent.Services(); + + // Assert SourceService + var sourceProxyServiceID = $"{sourceServiceID}-sidecar-proxy"; + Assert.Contains(sourceServiceID, services.Response.Keys); + Assert.Contains(sourceProxyServiceID, services.Response.Keys); + AgentService sourceProxyService = services.Response[sourceProxyServiceID]; + Assert.Equal(sourceServiceRegistrationParameters.Tags, sourceProxyService.Tags); + Assert.Equal(sourceServiceRegistrationParameters.Connect.SidecarService.Port, sourceProxyService.Port); + Assert.Equal(sourceServiceID, sourceProxyService.Proxy.DestinationServiceName); + Assert.Equal(sourceServiceID, sourceProxyService.Proxy.DestinationServiceID); + Assert.Equal("127.0.0.1", sourceProxyService.Proxy.LocalServiceAddress); + Assert.Equal(sourceServiceRegistrationParameters.Port, sourceProxyService.Proxy.LocalServicePort); + Assert.Equal(ServiceKind.ConnectProxy, sourceProxyService.Kind); + Assert.Single(sourceProxyService.Proxy.Upstreams); + Assert.Equal(sourceServiceRegistrationParameters.Connect.SidecarService.Proxy.Upstreams[0].DestinationName, sourceProxyService.Proxy.Upstreams[0].DestinationName); + Assert.Equal(sourceServiceRegistrationParameters.Connect.SidecarService.Proxy.Upstreams[0].LocalBindPort, sourceProxyService.Proxy.Upstreams[0].LocalBindPort); + + // Assert DestinationService + var destinationProxyServiceID = $"{destinationServiceID}-sidecar-proxy"; + Assert.Contains(destinationServiceID, services.Response.Keys); + Assert.Contains(destinationProxyServiceID, services.Response.Keys); + AgentService destinationProxyService = services.Response[destinationProxyServiceID]; + Assert.Equal(destinationServiceRegistrationParameters.Connect.SidecarService.Port, destinationProxyService.Port); + Assert.Equal(destinationServiceID, destinationProxyService.Proxy.DestinationServiceName); + Assert.Equal(destinationServiceID, destinationProxyService.Proxy.DestinationServiceID); + Assert.Equal("127.0.0.1", destinationProxyService.Proxy.LocalServiceAddress); + Assert.Equal(destinationServiceRegistrationParameters.Port, destinationProxyService.Proxy.LocalServicePort); + Assert.Null(destinationProxyService.Proxy.Upstreams); + Assert.Equal(ServiceKind.ConnectProxy, destinationProxyService.Kind); + } } } diff --git a/Consul.Test/ServiceKindUnitTests.cs b/Consul.Test/ServiceKindUnitTests.cs new file mode 100644 index 000000000..d8ef63cbe --- /dev/null +++ b/Consul.Test/ServiceKindUnitTests.cs @@ -0,0 +1,62 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2020 G-Research Limited +// +// 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 System.Collections.Generic; +using Xunit; + +namespace Consul.Test +{ + public class ServiceKindUnitTests + { + public static IList TryParseTestCases => new List + { + new object[] { null, true, null }, + new object[] { "", true, null }, + new object[] { " ", true, null }, + new object[] { " ", true, null }, + new object[] { "invalidvalud", false, null }, + new object[] { "connect-proxy", true, ServiceKind.ConnectProxy }, + new object[] { "Connect-proxy", true, ServiceKind.ConnectProxy }, + new object[] { "mesh-gateway", true, ServiceKind.MeshGateway }, + new object[] { "Mesh-gateway", true, ServiceKind.MeshGateway }, + new object[] { "terminating-gateway", true, ServiceKind.TerminatingGateway }, + new object[] { "Terminating-gateway", true, ServiceKind.TerminatingGateway }, + new object[] { "ingress-gateway", true, ServiceKind.IngressGateway }, + new object[] { "Ingress-gateway", true, ServiceKind.IngressGateway }, + }; + + [Fact] + public void Test_Equals() + { + Assert.Equal(ServiceKind.IngressGateway, ServiceKind.IngressGateway); + Assert.Equal(ServiceKind.IngressGateway, (object)"ingress-gateway"); + Assert.NotEqual(ServiceKind.IngressGateway, ServiceKind.ConnectProxy); + Assert.NotEqual(ServiceKind.IngressGateway, (object)"connect-proxy"); + } + + [Theory] + [MemberData(nameof(TryParseTestCases))] + public void TryParse(string value, bool expectedPredicateResult, ServiceKind expected) + { + bool actualPredicateResult = ServiceKind.TryParse(value, out ServiceKind actual); + + Assert.Equal(expectedPredicateResult, actualPredicateResult); + Assert.Equal(expected, actual); + } + } +} diff --git a/Consul/Agent.cs b/Consul/Agent.cs index fc8a58e31..7877d6e22 100644 --- a/Consul/Agent.cs +++ b/Consul/Agent.cs @@ -20,6 +20,8 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -139,6 +141,64 @@ public class AgentService public IDictionary TaggedAddresses { get; set; } public bool EnableTagOverride { get; set; } public IDictionary Meta { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] // If the Proxy property is serialized to have null value, a protocol error occurs when registering the service through the catalog (catalog/register) during an http request. + public AgentServiceProxy Proxy { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public ServiceKind Kind { get; set; } + } + + /// + /// ServiceKind specifies the type of service. + /// + [TypeConverter(typeof(ServiceKindTypeConverter))] + public class ServiceKind : IEquatable + { + static IReadOnlyDictionary Map { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "connect-proxy", new ServiceKind("connect-proxy") }, + { "mesh-gateway", new ServiceKind("mesh-gateway") }, + { "terminating-gateway", new ServiceKind("terminating-gateway") }, + { "ingress-gateway", new ServiceKind("ingress-gateway") }, + }; + + public static ServiceKind ConnectProxy => Map["connect-proxy"]; + public static ServiceKind MeshGateway => Map["mesh-gateway"]; + public static ServiceKind TerminatingGateway => Map["terminating-gateway"]; + public static ServiceKind IngressGateway => Map["ingress-gateway"]; + + string Value { get; } + + ServiceKind(string value) => Value = value; + + public override bool Equals(object obj) => obj is ServiceKind typedObject ? Equals(typedObject) : Value.Equals(obj.ToString(), StringComparison.OrdinalIgnoreCase); + + public bool Equals(ServiceKind other) => ReferenceEquals(this, other); + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(); + + public static bool TryParse(string value, out ServiceKind result) + { + result = null; + + if (string.IsNullOrWhiteSpace(value)) + { + return true; + } + + return Map.TryGetValue(value, out result); + } + } + + class ServiceKindTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) => sourceType == typeof(string); + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) => destinationType == typeof(string); + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) => ServiceKind.TryParse(value?.ToString(), out ServiceKind result) ? result : throw new NotSupportedException(); + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) => value is null ? string.Empty : value.ToString(); } /// @@ -198,6 +258,12 @@ public class AgentServiceRegistration [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public IDictionary TaggedAddresses { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public AgentServiceConnect Connect { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public AgentServiceProxy Proxy { get; set; } } /// @@ -209,6 +275,48 @@ public class AgentCheckRegistration : AgentServiceCheck public string ServiceID { get; set; } } + /// + /// AgentServiceConnect specifies the configuration for Connect + /// + public class AgentServiceConnect + { + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public AgentServiceRegistration SidecarService { get; set; } + } + + /// + /// AgentServiceProxy specifies the configuration for a Connect service proxy instance. This is only valid if Kind defines a proxy or gateway. + /// + public class AgentServiceProxy + { + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string DestinationServiceID { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public int LocalServicePort { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string LocalServiceAddress { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string DestinationServiceName { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public AgentServiceProxyUpstream[] Upstreams { get; set; } + } + + /// + /// AgentServiceProxyUpstream specifies the upstream service for which the proxy should create a listener. + /// + public class AgentServiceProxyUpstream + { + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string DestinationName { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public int LocalBindPort { get; set; } + } + /// /// AgentServiceCheck is used to create an associated check for a service ///