-
Notifications
You must be signed in to change notification settings - Fork 9.8k
/
Docker.cs
212 lines (173 loc) · 8.64 KB
/
Docker.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.AspNetCore.SignalR.Redis.Tests
{
public class Docker
{
private static readonly string _exeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
private static readonly string _dockerContainerName = "redisTestContainer-1x";
private static readonly string _dockerMonitorContainerName = _dockerContainerName + "Monitor-1x";
private static readonly Lazy<Docker> _instance = new Lazy<Docker>(Create);
public static Docker Default => _instance.Value;
private readonly string _path;
public Docker(string path)
{
_path = path;
}
private static Docker Create()
{
var location = GetDockerLocation();
if (location == null)
{
return null;
}
var docker = new Docker(location);
docker.RunCommand("info --format '{{.OSType}}'", "docker info", out var output);
if (!string.Equals(output.Trim('\'', '"', '\r', '\n', ' '), "linux"))
{
Console.WriteLine($"'docker info' output: {output}");
return null;
}
return docker;
}
private static string GetDockerLocation()
{
// OSX + Docker + Redis don't play well together for some reason. We already have these tests covered on Linux and Windows
// So we are happy ignoring them on OSX
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return null;
}
foreach (var dir in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator))
{
var candidate = Path.Combine(dir, "docker" + _exeSuffix);
if (File.Exists(candidate))
{
return candidate;
}
}
return null;
}
private void StartRedis(ILogger logger)
{
try
{
Run();
}
catch (Exception ex)
{
logger.LogError(ex, "Error starting redis docker container, retrying.");
Thread.Sleep(1000);
Run();
}
void Run()
{
// create and run docker container, remove automatically when stopped, map 6379 from the container to 6379 localhost
// use static name 'redisTestContainer' so if the container doesn't get removed we don't keep adding more
// use redis base docker image
// 30 second timeout to allow redis image to be downloaded, should be a rare occurrence, only happening when a new version is released
RunProcessAndThrowIfFailed(_path, $"run --rm -p 6380:6379 --name {_dockerContainerName} -d redis", "redis", logger, TimeSpan.FromSeconds(30));
}
}
public void Start(ILogger logger)
{
logger.LogInformation("Starting docker container");
// stop container if there is one, could be from a previous test run, ignore failures
RunProcessAndWait(_path, $"stop {_dockerMonitorContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _);
RunProcessAndWait(_path, $"stop {_dockerContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var output);
StartRedis(logger);
// inspect the redis docker image and extract the IPAddress. Necessary when running tests from inside a docker container, spinning up a new docker container for redis
// outside the current container requires linking the networks (difficult to automate) or using the IP:Port combo
RunProcessAndWait(_path, "inspect --format=\"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\" " + _dockerContainerName, "docker ipaddress", logger, TimeSpan.FromSeconds(5), out output);
output = output.Trim().Replace(Environment.NewLine, "");
// variable used by Startup.cs
Environment.SetEnvironmentVariable("REDIS_CONNECTION-PREV", $"{output}:6379");
var (monitorProcess, monitorOutput) = RunProcess(_path, $"run -i --name {_dockerMonitorContainerName} --link {_dockerContainerName}:redis --rm redis redis-cli -h redis -p 6379", "redis monitor", logger);
monitorProcess.StandardInput.WriteLine("MONITOR");
monitorProcess.StandardInput.Flush();
}
public void Stop(ILogger logger)
{
// Get logs from Redis container before stopping the container
RunProcessAndThrowIfFailed(_path, $"logs {_dockerContainerName}", "docker logs", logger, TimeSpan.FromSeconds(5));
logger.LogInformation("Stopping docker container");
RunProcessAndWait(_path, $"stop {_dockerMonitorContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _);
RunProcessAndWait(_path, $"stop {_dockerContainerName}", "docker stop", logger, TimeSpan.FromSeconds(15), out var _);
}
public int RunCommand(string commandAndArguments, string prefix, out string output) =>
RunCommand(commandAndArguments, prefix, NullLogger.Instance, out output);
public int RunCommand(string commandAndArguments, string prefix, ILogger logger, out string output)
{
return RunProcessAndWait(_path, commandAndArguments, prefix, logger, TimeSpan.FromSeconds(5), out output);
}
private static void RunProcessAndThrowIfFailed(string fileName, string arguments, string prefix, ILogger logger, TimeSpan timeout)
{
var exitCode = RunProcessAndWait(fileName, arguments, prefix, logger, timeout, out var output);
if (exitCode != 0)
{
throw new Exception($"Command '{fileName} {arguments}' failed with exit code '{exitCode}'. Output:{Environment.NewLine}{output}");
}
}
private static int RunProcessAndWait(string fileName, string arguments, string prefix, ILogger logger, TimeSpan timeout, out string output)
{
var (process, lines) = RunProcess(fileName, arguments, prefix, logger);
if (!process.WaitForExit((int)timeout.TotalMilliseconds))
{
process.Close();
logger.LogError("Closing process '{processName}' because it is running longer than the configured timeout.", fileName);
}
// Need to WaitForExit without a timeout to guarantee the output stream has written everything
process.WaitForExit();
output = string.Join(Environment.NewLine, lines);
return process.ExitCode;
}
private static (Process, ConcurrentQueue<string>) RunProcess(string fileName, string arguments, string prefix, ILogger logger)
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true
},
EnableRaisingEvents = true
};
var exitCode = 0;
var lines = new ConcurrentQueue<string>();
process.Exited += (_, __) => exitCode = process.ExitCode;
process.OutputDataReceived += (_, a) =>
{
LogIfNotNull(logger.LogInformation, $"'{prefix}' stdout: {{0}}", a.Data);
lines.Enqueue(a.Data);
};
process.ErrorDataReceived += (_, a) =>
{
LogIfNotNull(logger.LogError, $"'{prefix}' stderr: {{0}}", a.Data);
lines.Enqueue(a.Data);
};
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
return (process, lines);
}
private static void LogIfNotNull(Action<string, object[]> logger, string message, string data)
{
if (!string.IsNullOrEmpty(data))
{
logger(message, new[] { data });
}
}
}
}