193 lines
8.2 KiB
C#
193 lines
8.2 KiB
C#
// 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 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;
|
|
}
|
|
|
|
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);
|
|
|
|
// 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
|
|
// 20 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(20));
|
|
|
|
// 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 });
|
|
}
|
|
}
|
|
}
|
|
}
|