Skip to content

Commit c7ab41b

Browse files
Adds 'requiredPorts' functionality
I find people often have processes running locally which have port usages similar to those that we wish to spin-up via this library. E.g. a local proxy to a cloud instance of the DB which their app uses. This leads to annoyance and hunting through sometimes cryptic docker compose log lines to discover a simple problem which could have been proactively checked for (fast-fail) prior to docker compose up being invoked. This PR introduces an optional property 'RequiredPorts' which will be checked for before running docker compose up. If any ports are found to be in use, a custom exception is raised and the clashing ports are surfaced on the 'Ports' property of this exception instance.
1 parent eea71d7 commit c7ab41b

8 files changed

Lines changed: 112 additions & 31 deletions

File tree

DockerComposeFixture.Tests/DockerFixtureTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public void Init_Throws_IfTestIsNeverTrue()
125125
[Fact]
126126
public void Init_MonitorsServices_WhenTheyStartSlowly()
127127
{
128-
Stopwatch stopwatch = new Stopwatch();
128+
var stopwatch = new Stopwatch();
129129
var compose = new Mock<IDockerCompose>();
130130
compose.Setup(c => c.PauseMs).Returns(100);
131131
compose.Setup(c => c.Up())
@@ -158,7 +158,7 @@ public void Init_Throws_WhenServicesFailToStart()
158158
{
159159
var compose = new Mock<IDockerCompose>();
160160
compose.Setup(c => c.PauseMs).Returns(NumberOfMsInOneSec);
161-
bool firstTime = true;
161+
var firstTime = true;
162162
compose.Setup(c => c.PsWithJsonFormat())
163163
.Returns(() =>
164164
{
@@ -191,7 +191,7 @@ public void Init_Throws_WhenYmlFileDoesntExist()
191191
compose.Setup(c => c.PsWithJsonFormat())
192192
.Returns(new[] { "non-json-message", "{ \"Status\": \"Up 3 seconds\" }", "{ \"Status\": \"Up 15 seconds\" }" });
193193

194-
string fileDoesntExist = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
194+
var fileDoesntExist = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
195195
Assert.Throws<ArgumentException>(() =>
196196
new DockerFixture(null).Init(new[] { fileDoesntExist }, "up", "down", 120, null, compose.Object));
197197
}

DockerComposeFixture.Tests/Utils/ObservableCounter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public IDisposable Subscribe(IObserver<string> observer)
1616

1717
public void Count(int min = 1, int max = 10, int delay = 10)
1818
{
19-
for (int i = min; i <= max; i++)
19+
for (var i = min; i <= max; i++)
2020
{
2121
this.observalbes.ForEach(o => o.OnNext(i.ToString()));
2222
Thread.Sleep(delay);

DockerComposeFixture/DockerFixture.cs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class DockerFixture : IDisposable
2121
private ILogger[] loggers;
2222
private int startupTimeoutSecs;
2323
private readonly IMessageSink output;
24+
private ushort[] requiredPorts;
2425

2526
public DockerFixture(IMessageSink output)
2627
{
@@ -31,7 +32,7 @@ public DockerFixture(IMessageSink output)
3132
/// Initialize docker compose services from file(s) but only once.
3233
/// If you call this multiple times on the same DockerFixture then it will be ignored.
3334
/// </summary>
34-
/// <param name="setupOptions">Options that control how docker-compose is executed.</param>
35+
/// <param name="setupOptions">Options that control how docker compose is executed.</param>
3536
public void InitOnce(Func<IDockerFixtureOptions> setupOptions)
3637
{
3738
InitOnce(setupOptions, null);
@@ -41,7 +42,7 @@ public void InitOnce(Func<IDockerFixtureOptions> setupOptions)
4142
/// Initialize docker compose services from file(s) but only once.
4243
/// If you call this multiple times on the same DockerFixture then it will be ignored.
4344
/// </summary>
44-
/// <param name="setupOptions">Options that control how docker-compose is executed.</param>
45+
/// <param name="setupOptions">Options that control how docker compose is executed.</param>
4546
/// <param name="dockerCompose"></param>
4647
public void InitOnce(Func<IDockerFixtureOptions> setupOptions, IDockerCompose dockerCompose)
4748
{
@@ -56,7 +57,7 @@ public void InitOnce(Func<IDockerFixtureOptions> setupOptions, IDockerCompose do
5657
/// <summary>
5758
/// Initialize docker compose services from file(s).
5859
/// </summary>
59-
/// <param name="setupOptions">Options that control how docker-compose is executed</param>
60+
/// <param name="setupOptions">Options that control how docker compose is executed</param>
6061
public void Init(Func<IDockerFixtureOptions> setupOptions)
6162
{
6263
Init(setupOptions, null);
@@ -65,18 +66,25 @@ public void Init(Func<IDockerFixtureOptions> setupOptions)
6566
/// <summary>
6667
/// Initialize docker compose services from file(s).
6768
/// </summary>
68-
/// <param name="setupOptions">Options that control how docker-compose is executed</param>
69+
/// <param name="setupOptions">Options that control how docker compose is executed</param>
6970
/// <param name="compose"></param>
7071
public void Init(Func<IDockerFixtureOptions> setupOptions, IDockerCompose compose)
7172
{
7273
var options = setupOptions();
7374
options.Validate();
74-
string logFile = options.DebugLog
75+
var logFile = options.DebugLog
7576
? Path.Combine(Path.GetTempPath(), $"docker-compose-{DateTime.Now.Ticks}.log")
7677
: null;
7778

78-
this.Init(options.DockerComposeFiles, options.DockerComposeUpArgs, options.DockerComposeDownArgs,
79-
options.StartupTimeoutSecs, options.CustomUpTest, compose, this.GetLoggers(logFile).ToArray());
79+
this.Init(
80+
options.DockerComposeFiles,
81+
options.DockerComposeUpArgs,
82+
options.DockerComposeDownArgs,
83+
options.StartupTimeoutSecs,
84+
options.CustomUpTest,
85+
compose,
86+
this.GetLoggers(logFile).ToArray(),
87+
options.RequiredPorts);
8088
}
8189

8290
private IEnumerable<ILogger> GetLoggers(string file)
@@ -97,22 +105,24 @@ private IEnumerable<ILogger> GetLoggers(string file)
97105
/// Initialize docker compose services from file(s).
98106
/// </summary>
99107
/// <param name="dockerComposeFiles">Array of docker compose files</param>
100-
/// <param name="dockerComposeUpArgs">Arguments to append after 'docker-compose -f file.yml up'</param>
101-
/// <param name="dockerComposeDownArgs">Arguments to append after 'docker-compose -f file.yml down'</param>
108+
/// <param name="dockerComposeUpArgs">Arguments to append after 'docker compose -f file.yml up'</param>
109+
/// <param name="dockerComposeDownArgs">Arguments to append after 'docker compose -f file.yml down'</param>
102110
/// <param name="startupTimeoutSecs">How long to wait for the application to start before giving up</param>
103-
/// <param name="customUpTest">Checks whether the docker-compose services have come up correctly based upon the output of docker-compose</param>
111+
/// <param name="customUpTest">Checks whether the docker compose services have come up correctly based upon the output of docker compose</param>
104112
/// <param name="dockerCompose"></param>
105113
/// <param name="logger"></param>
114+
/// <param name="requiredPorts">Checks that these ports are available on the host network (not in use by other processes)</param>
106115
public void Init(string[] dockerComposeFiles, string dockerComposeUpArgs, string dockerComposeDownArgs,
107116
int startupTimeoutSecs, Func<string[], bool> customUpTest = null,
108-
IDockerCompose dockerCompose = null, ILogger[] logger = null)
117+
IDockerCompose dockerCompose = null, ILogger[] logger = null, ushort[] requiredPorts = null)
109118
{
110119
this.loggers = logger ?? GetLoggers(null).ToArray();
111120

112121
var dockerComposeFilePaths = dockerComposeFiles.Select(this.GetComposeFilePath);
113122
this.dockerCompose = dockerCompose ?? new DockerCompose(this.loggers);
114123
this.customUpTest = customUpTest;
115124
this.startupTimeoutSecs = startupTimeoutSecs;
125+
this.requiredPorts = requiredPorts;
116126

117127
this.dockerCompose.Init(
118128
string.Join(" ",
@@ -129,7 +139,7 @@ private string GetComposeFilePath(string file)
129139
return file;
130140
}
131141

132-
DirectoryInfo curDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
142+
var curDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
133143
if (File.Exists(Path.Combine(curDir.FullName, file)))
134144
{
135145
return Path.Combine(curDir.FullName, file);
@@ -139,7 +149,7 @@ private string GetComposeFilePath(string file)
139149
{
140150
while (curDir != null)
141151
{
142-
string curFile = Path.Combine(curDir.FullName, file);
152+
var curFile = Path.Combine(curDir.FullName, file);
143153
if (File.Exists(curFile))
144154
{
145155
return curFile;
@@ -169,7 +179,7 @@ public static async Task Kill(string applicationName, bool killEverything = fals
169179
/// <returns></returns>
170180
public static async Task Kill(Regex filterRx, bool killEverything = false)
171181
{
172-
Process ps = Process.Start(new ProcessStartInfo("docker", "ps")
182+
var ps = Process.Start(new ProcessStartInfo("docker", "ps")
173183
{
174184
UseShellExecute = false,
175185
RedirectStandardOutput = true
@@ -187,7 +197,6 @@ public static async Task Kill(Regex filterRx, bool killEverything = false)
187197
{
188198
Process.Start("docker", $"kill {id}").WaitForExit();
189199
}
190-
191200
}
192201

193202
public virtual void Dispose()
@@ -203,14 +212,21 @@ private void Start()
203212
this.Stop();
204213
}
205214

215+
if (requiredPorts != null && requiredPorts.Length > 0)
216+
{
217+
this.loggers.Log("---- checking for port availability ----");
218+
this.CheckForRequiredPorts();
219+
this.loggers.Log("---- all required host ports are available ----");
220+
}
221+
206222
this.loggers.Log("---- starting docker services ----");
207223
var upTask = this.dockerCompose.Up();
208224

209-
for (int i = 0; i < this.startupTimeoutSecs; i++)
225+
for (var i = 0; i < this.startupTimeoutSecs; i++)
210226
{
211227
if (upTask.IsCompleted)
212228
{
213-
this.loggers.Log("docker-compose exited prematurely");
229+
this.loggers.Log("docker compose exited prematurely");
214230
break;
215231
}
216232
this.loggers.Log($"---- checking docker services ({i + 1}/{this.startupTimeoutSecs}) ----");
@@ -235,7 +251,16 @@ private void Start()
235251
}
236252
throw new DockerComposeException(this.loggers.GetLoggedLines());
237253
}
238-
254+
255+
private void CheckForRequiredPorts()
256+
{
257+
var usedPorts = Ports.DetermineUtilizedPorts(requiredPorts);
258+
if (usedPorts.Any())
259+
{
260+
throw new PortsUnavailableException(this.loggers.GetLoggedLines(), usedPorts);
261+
}
262+
}
263+
239264
private (bool hasContainers, bool containersAreUp) CheckIfRunning()
240265
{
241266
var lines = dockerCompose.PsWithJsonFormat()

DockerComposeFixture/DockerFixtureOptions.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,21 @@
33
namespace DockerComposeFixture
44
{
55
/// <summary>
6-
/// Options that control how docker-compose is executed
6+
/// Options that control how docker compose is executed
77
/// </summary>
88
public class DockerFixtureOptions : IDockerFixtureOptions
99
{
1010
/// <summary>
11-
/// Checks whether the docker-compose services have come up correctly based upon the output of docker-compose
11+
/// An array of ports required to be available on the host in order for the docker compose services
12+
/// to start. Provides a fail-fast check along with aiding developers to easier debug issues related
13+
/// to running other programs locally with clashing ports.
14+
/// If null or empty - no ports are checked.
15+
/// If any required ports are reserved by other processes - throws an 'PortsUnavailableException'.
16+
/// </summary>
17+
public ushort[] RequiredPorts { get; set; }
18+
19+
/// <summary>
20+
/// Checks whether the docker compose services have come up correctly based upon the output of docker compose
1221
/// </summary>
1322
public Func<string[], bool> CustomUpTest { get; set; }
1423

@@ -19,17 +28,17 @@ public class DockerFixtureOptions : IDockerFixtureOptions
1928
/// </summary>
2029
public string[] DockerComposeFiles { get; set; } = new[] { "docker-compose.yml" };
2130
/// <summary>
22-
/// When true this logs docker-compose output to %temp%\docker-compose-*.log
31+
/// When true this logs docker compose output to %temp%\docker-compose-*.log
2332
/// </summary>
2433
public bool DebugLog { get; set; }
2534
/// <summary>
26-
/// Arguments to append after 'docker-compose -f file.yml up'
27-
/// Default is 'docker-compose -f file.yml up' you can append '--build' if you want it to always build
35+
/// Arguments to append after 'docker compose -f file.yml up'
36+
/// Default is 'docker compose -f file.yml up' you can append '--build' if you want it to always build
2837
/// </summary>
2938
public string DockerComposeUpArgs { get; set; } = "";
3039
/// <summary>
31-
/// Arguments to append after 'docker-compose -f file.yml down'
32-
/// Default is 'docker-compose -f file.yml down --remove-orphans' you can add '--rmi all' if you want to guarantee a fresh build on each test
40+
/// Arguments to append after 'docker compose -f file.yml down'
41+
/// Default is 'docker compose -f file.yml down --remove-orphans' you can add '--rmi all' if you want to guarantee a fresh build on each test
3342
/// </summary>
3443
public string DockerComposeDownArgs { get; set; } = "--remove-orphans";
3544

DockerComposeFixture/Exceptions/DockerComposeException.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
namespace DockerComposeFixture.Exceptions
44
{
5-
public class DockerComposeException:Exception
5+
public class DockerComposeException : Exception
66
{
7-
public DockerComposeException(string[] loggedLines):base($"docker-compose failed - see {nameof(DockerComposeOutput)} property")
7+
public DockerComposeException(string[] loggedLines)
8+
: base($"docker compose failed - see {nameof(DockerComposeOutput)} property")
89
{
910
this.DockerComposeOutput = loggedLines;
1011
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
3+
namespace DockerComposeFixture.Exceptions
4+
{
5+
public class PortsUnavailableException : DockerComposeException
6+
{
7+
public PortsUnavailableException(string[] loggedLines, ushort[] ports) : base(loggedLines)
8+
{
9+
Ports = ports;
10+
}
11+
12+
public ushort[] Ports { get; set; }
13+
}
14+
}

DockerComposeFixture/IDockerFixtureOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace DockerComposeFixture
44
{
55
public interface IDockerFixtureOptions
66
{
7+
UInt16[] RequiredPorts { get; set; }
78
Func<string[], bool> CustomUpTest { get; set; }
89
string[] DockerComposeFiles { get; set; }
910
bool DebugLog { get; set; }

DockerComposeFixture/Ports.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Linq;
3+
using System.Net.NetworkInformation;
4+
5+
namespace DockerComposeFixture
6+
{
7+
public static class Ports
8+
{
9+
// check if network port is open
10+
public static ushort[] DetermineUtilizedPorts(ushort[] ports)
11+
{
12+
return ports.Where(p => !IsPortAvailable(p)).ToArray();
13+
}
14+
15+
private static bool IsPortAvailable(UInt16 port)
16+
{
17+
var isAvailable = true;
18+
var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
19+
var tcpConnInfoArray = ipGlobalProperties.GetActiveTcpListeners();
20+
21+
foreach (var endpoint in tcpConnInfoArray) {
22+
if (endpoint.Port == port) {
23+
isAvailable = false;
24+
break;
25+
}
26+
}
27+
28+
return isAvailable;
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)