Skip to content

Commit

Permalink
Support for Custom Endpoint Resolver, solves #107
Browse files Browse the repository at this point in the history
  • Loading branch information
mariotoffia committed Mar 25, 2021
1 parent 00e4661 commit 7fc8a21
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 28 deletions.
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"particulary",
"postgres",
"postgresql",
"prms",
"proto",
"somewordpress",
"sudoer",
"typeof",
Expand Down
50 changes: 50 additions & 0 deletions Ductus.FluentDocker.Tests/FluentApiTests/FluentNetworkTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Ductus.FluentDocker.Extensions;
using Ductus.FluentDocker.Services;
using Ductus.FluentDocker.Services.Extensions;
using Ductus.FluentDocker.Tests.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -81,5 +83,53 @@ public void InternalNetworkExposedToHostShallWork()
}
}
}

[TestMethod]
public void CustomResolverForContainerShallWork()
{
bool executedCustomResolver = false;

using (
var container =
Fd.UseContainer()
.UseImage("postgres:9.6-alpine")
.WithEnvironment("POSTGRES_PASSWORD=mysecretpassword")
.ExposePort(5432)
.UseCustomResolver((
ports, portAndProto, dockerUri) =>
{
executedCustomResolver = true;
if (null == ports || string.IsNullOrEmpty(portAndProto))
return null;
if (!ports.TryGetValue(portAndProto, out var endpoints))
return null;
if (null == endpoints || endpoints.Length == 0)
return null;
if (CommandExtensions.IsNative())
return endpoints[0];
if (CommandExtensions.IsEmulatedNative())
return CommandExtensions.IsDockerDnsAvailable()
? new IPEndPoint(CommandExtensions.EmulatedNativeAddress(), endpoints[0].Port)
: new IPEndPoint(IPAddress.Loopback, endpoints[0].Port);
if (Equals(endpoints[0].Address, IPAddress.Any) && null != dockerUri)
return new IPEndPoint(IPAddress.Parse(dockerUri.Host), endpoints[0].Port);
return endpoints[0];
})
.WaitForPort("5432/tcp", 30000 /*30s*/)
.Build()
.Start())
{
var state = container.GetConfiguration(true/*force*/).State.ToServiceState();
Assert.AreEqual(ServiceRunningState.Running, state);
Assert.IsTrue(executedCustomResolver);
}
}
}
}
18 changes: 16 additions & 2 deletions Ductus.FluentDocker/Builders/ContainerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public override IContainerService Build()
_config.DeleteOnDispose,
_config.DeleteVolumeOnDispose,
_config.DeleteNamedVolumeOnDispose,
_config.Command, _config.Arguments);
_config.Command, _config.Arguments,
_config.CustomResolver);

AddHooks(container);

Expand Down Expand Up @@ -130,10 +131,23 @@ public ContainerBuilder UseImage(string image, bool force = false)
return this;
}

/// <summary>
/// Sets a custom EndpointResolver. This resolver may override the default endpoint resolver if returns an endpoint, otherwise
/// the default resolve mechanism kicks in.
/// </summary>
/// <param name="customResolver">The custom resolver.§</param>
/// <returns>Itself for fluent access.</returns>
public ContainerBuilder UseCustomResolver(
Func<Dictionary<string, HostIpEndpoint[]>, string, Uri, IPEndPoint> customResolver)
{
_config.CustomResolver = customResolver;
return this;
}

/// <summary>
/// Uses credentials to login to a registry.
/// </summary>
/// <param name="server">The ip or dns to the server (with optioanl :port)</param>
/// <param name="server">The ip or dns to the server (with optional :port)</param>
/// <param name="user">An optional user to use when logging in.</param>
/// <param name="password">An optional password to user when logging in.</param>
/// <returns>Itself for fluent access.</returns>
Expand Down
2 changes: 1 addition & 1 deletion Ductus.FluentDocker/Extensions/CommandExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ public static bool IsNative()
return FdOs.IsLinux();
}

public static IPAddress EmulatedNativeAdress(bool useCache = true)
public static IPAddress EmulatedNativeAddress(bool useCache = true)
{
if (useCache && null != _cachedDockerIpAddress)
return _cachedDockerIpAddress;
Expand Down
4 changes: 3 additions & 1 deletion Ductus.FluentDocker/Model/Builders/ContainerBuilderConfig.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net;
using Ductus.FluentDocker.Model.Common;
using Ductus.FluentDocker.Model.Compose;
using Ductus.FluentDocker.Model.Containers;
Expand All @@ -20,6 +21,7 @@ public sealed class ContainerBuilderConfig
public bool DeleteOnDispose { get; set; } = true;
public bool DeleteVolumeOnDispose { get; set; } = false;
public bool DeleteNamedVolumeOnDispose { get; set; } = false;
public Func<Dictionary<string, HostIpEndpoint[]>, string, Uri, IPEndPoint> CustomResolver { get; set; }
public string Command { get; set; }
public string[] Arguments { get; set; }
public Tuple<string /*portAndProto*/, string /*address*/ , long /*waitTimeout*/> WaitForPort { get; set; }
Expand All @@ -40,7 +42,7 @@ public sealed class ContainerBuilderConfig
public List<NetworkWithAlias<string>> NetworkNamesWithAlias { get; set; }
public List<string> ExecuteOnRunningArguments { get; set; }
public List<string> ExecuteOnDisposingArguments { get; set; }

public NetworkWithAlias<string> FindFirstNetworkNameAndAlias()
{

Expand Down
24 changes: 13 additions & 11 deletions Ductus.FluentDocker/Services/Extensions/ContainerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static class ContainerExtensions
/// Read the logs from the container.
/// </summary>
/// <param name="service">The container to read the logs from.</param>
/// <param name="follow">If continious logs is wanted.</param>
/// <param name="follow">If continuous logs is wanted.</param>
/// <param name="token">The cancellation token for logs, especially needed when <paramref name="follow" /> is set to true.</param>
/// <returns>A console stream to consume.</returns>
public static ConsoleStream<string> Logs(this IContainerService service, bool follow = false,
Expand Down Expand Up @@ -51,11 +51,13 @@ public static CommandResponse<IList<string>> Execute(this IContainerService serv
/// <returns>A host based endpoint from a exposed port or null if none.</returns>
public static IPEndPoint ToHostExposedEndpoint(this IContainerService service, string portAndProto)
{
return service.GetConfiguration()?.NetworkSettings.Ports.ToHostPort(portAndProto, service.DockerHost);
return service.GetConfiguration()?.NetworkSettings.Ports.ToHostPortCustomResolver(
service.CustomEndpointResolver, portAndProto, service.DockerHost
);
}

/// <summary>
/// Diffs the container from it's orignal state to the current state.
/// Diffs the container from it's original state to the current state.
/// </summary>
/// <param name="service">The container to do the diff operation on.</param>
/// <returns>
Expand All @@ -68,7 +70,7 @@ public static IList<Diff> Diff(this IContainerService service)
}

/// <summary>
/// Diffs the container from it's orignal state to the current state.
/// Diffs the container from it's original state to the current state.
/// </summary>
/// <param name="service">The container to do the diff operation on.</param>
/// <param name="result">
Expand All @@ -94,7 +96,7 @@ public static IContainerService Diff(this IContainerService service, out IList<D
/// <param name="throwOnError">If a exception shall be thrown if any error occurs.</param>
/// <returns>The service itself.</returns>
/// <exception cref="FluentDockerException">
/// The exception thrown when an error occured and <paramref name="throwOnError" />
/// The exception thrown when an error occurred and <paramref name="throwOnError" />
/// is set to true.
/// </exception>
public static IContainerService Export(this IContainerService service, TemplateString fqPath, bool explode = false,
Expand Down Expand Up @@ -126,7 +128,7 @@ public static IContainerService Export(this IContainerService service, TemplateS
{
if (throwOnError)
{
throw new FluentDockerException("Exception while untaring archive", e);
throw new FluentDockerException("Exception while un-taring archive", e);
}
}
finally
Expand Down Expand Up @@ -155,7 +157,7 @@ public static Processes GetRunningProcesses(this IContainerService service)
/// <param name="hostPath">The host path to copy to.</param>
/// <param name="throwOnError">If it shall throw if any errors occur during copy.</param>
/// <returns>The path where the files where copied to if successful, otherwise null is returned.</returns>
/// <exception cref="FluentDockerException">If <paramref name="throwOnError" /> is true and error occured during copy.</exception>
/// <exception cref="FluentDockerException">If <paramref name="throwOnError" /> is true and error occurred during copy.</exception>
public static IContainerService CopyFrom(this IContainerService service, TemplateString containerPath,
TemplateString hostPath, bool throwOnError = false)
{
Expand Down Expand Up @@ -183,7 +185,7 @@ public static IContainerService CopyFrom(this IContainerService service, Templat
/// <param name="hostPath">The host path to copy from.</param>
/// <param name="throwOnError">If it shall throw if any errors occur during copy.</param>
/// <returns>The path where the files where copied from if successful, otherwise null is returned.</returns>
/// <exception cref="FluentDockerException">If <paramref name="throwOnError" /> is true and error occured during copy.</exception>
/// <exception cref="FluentDockerException">If <paramref name="throwOnError" /> is true and error occurred during copy.</exception>
public static IContainerService CopyTo(this IContainerService service, TemplateString containerPath,
TemplateString hostPath, bool throwOnError = false)
{
Expand All @@ -209,7 +211,7 @@ public static IContainerService CopyTo(this IContainerService service, TemplateS
/// <param name="service">The service to check processes within.</param>
/// <param name="process">The process to wait for.</param>
/// <param name="millisTimeout">Timeout giving up the wait.</param>
/// <returns>The inparam service.</returns>
/// <returns>The in param service.</returns>
public static IContainerService WaitForProcess(this IContainerService service, string process,
long millisTimeout = -1)
{
Expand Down Expand Up @@ -248,7 +250,7 @@ public static IContainerService WaitForProcess(this IContainerService service, s
/// </summary>
/// <param name="service">The service to check processes within.</param>
/// <param name="millisTimeout">Timeout giving up the wait.</param>
/// <returns>The inparam service.</returns>
/// <returns>The in param service.</returns>
public static IContainerService WaitForHealthy(this IContainerService service, long millisTimeout = -1)
{
if (service == null)
Expand Down Expand Up @@ -287,7 +289,7 @@ public static IContainerService WaitForHealthy(this IContainerService service, l
/// <param name="service">The service to check processes within.</param>
/// <param name="message">The message to wait for</param>
/// <param name="millisTimeout">Timeout giving up the wait.</param>
/// <returns>The inparam service.</returns>
/// <returns>The in param service.</returns>
public static IContainerService WaitForMessageInLogs(this IContainerService service, string message, long millisTimeout = -1)
{
if (service == null)
Expand Down
35 changes: 33 additions & 2 deletions Ductus.FluentDocker/Services/Extensions/NetworkExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public static void WaitForHttp(this IContainerService service, string url, long
/// <summary>Get extra long current timestamp</summary>
private static long Millis => (long)((DateTime.UtcNow - Jan1St1970).TotalMilliseconds);

/// <summary>
/// <summary>
/// Translates a docker exposed port and protocol (on format 'port/proto' e.g. '534/tcp') to a
/// host endpoint that can be contacted outside the container.
/// </summary>
Expand All @@ -116,8 +116,39 @@ public static void WaitForHttp(this IContainerService service, string url, long
/// <param name="dockerUri">Optional docker uri to use when the address is 0.0.0.0 in the endpoint.</param>
/// <returns>A endpoint of the host exposed ip and port into the container port. If none is found, null is returned.</returns>
public static IPEndPoint ToHostPort(this Dictionary<string, HostIpEndpoint[]> ports, string portAndProto,
Uri dockerUri = null)
{
return ToHostPortCustomResolver(ports, null, portAndProto, dockerUri);
}

/// <summary>
/// Translates a docker exposed port and protocol (on format 'port/proto' e.g. '534/tcp') to a
/// host endpoint that can be contacted outside the container.
/// </summary>
/// <param name="ports">The ports from the <see cref="ContainerNetworkSettings.Ports" /> property.</param>
/// <param name="portAndProto">The port and protocol string.</param>
/// <param name="customResolver">
/// An optional custom resolver that overrides the default behavior. If it returns null it will execute the default behavior.
/// </param>
/// <param name="dockerUri">Optional docker uri to use when the address is 0.0.0.0 in the endpoint.</param>
/// <returns>A endpoint of the host exposed ip and port into the container port. If none is found, null is returned.</returns>
public static IPEndPoint ToHostPortCustomResolver(
this Dictionary<string, HostIpEndpoint[]> ports,
Func<Dictionary<string, HostIpEndpoint[]>,string, Uri, IPEndPoint> customResolver,
string portAndProto,
Uri dockerUri = null)
{

if (customResolver != null)
{
var ep = customResolver.Invoke(ports, portAndProto, dockerUri);

if (ep != null) {
return ep;
}

}

if (null == ports || string.IsNullOrEmpty(portAndProto))
return null;

Expand All @@ -132,7 +163,7 @@ public static IPEndPoint ToHostPort(this Dictionary<string, HostIpEndpoint[]> po

if (CommandExtensions.IsEmulatedNative())
return CommandExtensions.IsDockerDnsAvailable()
? new IPEndPoint(CommandExtensions.EmulatedNativeAdress(), endpoints[0].Port)
? new IPEndPoint(CommandExtensions.EmulatedNativeAddress(), endpoints[0].Port)
: new IPEndPoint(IPAddress.Loopback, endpoints[0].Port);

if (Equals(endpoints[0].Address, IPAddress.Any) && null != dockerUri)
Expand Down
16 changes: 14 additions & 2 deletions Ductus.FluentDocker/Services/IContainerService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Net;
using Ductus.FluentDocker.Model.Common;
using Ductus.FluentDocker.Model.Containers;

Expand Down Expand Up @@ -33,12 +34,23 @@ public interface IContainerService : IService
bool StopOnDispose { get; set; }

/// <summary>
/// When set to true the container is removed automaticallyh on <see cref="IDisposable.Dispose()" />.
/// When set to true the container is removed automatically on <see cref="IDisposable.Dispose()" />.
/// </summary>
bool RemoveOnDispose { get; set; }

/// <summary>
/// Dettermines if this container is based on a windows image or linux image.
/// Optionally exposes a custom resolver. When this is set, it will override the default container endpoint IP resolver.
/// </summary>
/// <value>A custom resolver.</value>
/// <remarks>
/// If the resolver returns null it will use the built-in behavior. It is true, when no custom resolver has
/// been set on the service.
/// </remarks>
Func<Dictionary<string, HostIpEndpoint[]>, string, Uri, IPEndPoint> CustomEndpointResolver { get; }


/// <summary>
/// Determines if this container is based on a windows image or linux image.
/// </summary>
bool IsWindowsContainer { get; }

Expand Down
8 changes: 6 additions & 2 deletions Ductus.FluentDocker/Services/IHostService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Net;
using Ductus.FluentDocker.Common;
using Ductus.FluentDocker.Model.Common;
using Ductus.FluentDocker.Model.Containers;
Expand Down Expand Up @@ -57,12 +59,14 @@ public interface IHostService : IService
/// <param name="deleteNamedVolumeOnDispose">If associated named volumes should be deleted as well.</param>
/// <param name="command">Optionally a command to run when it is started.</param>
/// <param name="args">Optionally a set of parameters to go with the <see cref="command" /> when started.</param>
/// <param name="customEndpointResolver">Set this resolver when creating the container.</param>
/// <returns>A service reflecting the newly created container.</returns>
/// <exception cref="FluentDockerException">If error occurs.</exception>
IContainerService Create(string image, bool forcePull = false, ContainerCreateParams prms = null,
bool stopOnDispose = true, bool deleteOnDispose = true, bool deleteVolumeOnDispose = false,
bool deleteNamedVolumeOnDispose = false, string command = null,
string[] args = null);
string[] args = null,
Func<Dictionary<string, HostIpEndpoint[]>, string, Uri, IPEndPoint> customEndpointResolver = null);

/// <summary>
/// Gets all the docker networks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ public override void Start()

State = ServiceRunningState.Starting;

result = host.Host.ComposeUp(_config.AlternativeServiceName,
false/*forceRecreate*/,false/*noRecreate*/,false/*dontBuild*/, false/*buildBeforeCreate*/,
result = host.Host.ComposeUp(_config.AlternativeServiceName,
false/*forceRecreate*/, false/*noRecreate*/, false/*dontBuild*/, false/*buildBeforeCreate*/,
_config.TimeoutSeconds == TimeSpan.Zero ? (TimeSpan?)null : _config.TimeoutSeconds, _config.RemoveOrphans,
_config.UseColor,
false/*noStart*/,
Expand All @@ -175,7 +175,7 @@ public override void Start()
var name = ExtractNames(info.Data, out var project, out var instanceId);

list.Add(new DockerContainerService(name, cid, host.Host, info.Data.State.ToServiceState(),
host.Certificates, instanceId: instanceId, project: project));
host.Certificates, null/*noCustomResolver*/, instanceId: instanceId, project: project));
}

Containers = list;
Expand Down
Loading

0 comments on commit 7fc8a21

Please sign in to comment.