diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 301c59f0..9f613c4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,4 +24,4 @@ jobs: - name: Build run: dotnet build -c Release --framework ${{ matrix.framework }} - name: Test - run: dotnet test -c Release --framework ${{ matrix.framework }} --no-build + run: dotnet test -c Release --framework ${{ matrix.framework }} --no-build --logger console diff --git a/src/Docker.DotNet.BasicAuth/BasicAuthCredentials.cs b/src/Docker.DotNet.BasicAuth/BasicAuthCredentials.cs index f1ba77f4..b51bfc85 100644 --- a/src/Docker.DotNet.BasicAuth/BasicAuthCredentials.cs +++ b/src/Docker.DotNet.BasicAuth/BasicAuthCredentials.cs @@ -5,12 +5,10 @@ public class BasicAuthCredentials : Credentials private readonly bool _isTls; private readonly MaybeSecureString _username; + private readonly MaybeSecureString _password; - public override HttpMessageHandler GetHandler(HttpMessageHandler innerHandler) - { - return new BasicAuthHandler(_username, _password, innerHandler); - } + private bool _disposed; public BasicAuthCredentials(SecureString username, SecureString password, bool isTls = false) : this(new MaybeSecureString(username), new MaybeSecureString(password), isTls) @@ -29,14 +27,35 @@ private BasicAuthCredentials(MaybeSecureString username, MaybeSecureString passw _password = password; } + public override void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + public override bool IsTlsCredentials() { return _isTls; } - public override void Dispose() + public override HttpMessageHandler GetHandler(HttpMessageHandler handler) + { + return new BasicAuthHandler(_username, _password, handler); + } + + protected virtual void Dispose(bool disposing) { - _username.Dispose(); - _password.Dispose(); + if (_disposed) + { + return; + } + + if (disposing) + { + _username.Dispose(); + _password.Dispose(); + } + + _disposed = true; } } \ No newline at end of file diff --git a/src/Docker.DotNet.X509/CertificateCredentials.cs b/src/Docker.DotNet.X509/CertificateCredentials.cs index e7e56ce0..3404616b 100644 --- a/src/Docker.DotNet.X509/CertificateCredentials.cs +++ b/src/Docker.DotNet.X509/CertificateCredentials.cs @@ -4,37 +4,47 @@ public class CertificateCredentials : Credentials { private readonly X509Certificate2 _certificate; - public CertificateCredentials(X509Certificate2 clientCertificate) + private bool _disposed; + + public CertificateCredentials(X509Certificate2 certificate) { - _certificate = clientCertificate; + _certificate = certificate; } - public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } - - public override HttpMessageHandler GetHandler(HttpMessageHandler innerHandler) + public override void Dispose() { - var handler = (ManagedHandler)innerHandler; - handler.ClientCertificates = new X509CertificateCollection - { - _certificate - }; + Dispose(true); + GC.SuppressFinalize(this); + } - handler.ServerCertificateValidationCallback = ServerCertificateValidationCallback; + public override bool IsTlsCredentials() + { + return true; + } - if (handler.ServerCertificateValidationCallback == null) + public override HttpMessageHandler GetHandler(HttpMessageHandler handler) + { + if (handler is HttpClientHandler httpClientHandler) { - handler.ServerCertificateValidationCallback = ServicePointManager.ServerCertificateValidationCallback; + httpClientHandler.ClientCertificates.Add(_certificate); + httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; } return handler; } - public override bool IsTlsCredentials() + protected virtual void Dispose(bool disposing) { - return true; - } + if (_disposed) + { + return; + } - public override void Dispose() - { + if (disposing) + { + _certificate.Dispose(); + } + + _disposed = true; } } \ No newline at end of file diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj index 017ee021..8c2dc7c0 100644 --- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj +++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj @@ -4,6 +4,9 @@ <PackageId>Docker.DotNet.Enhanced.X509</PackageId> <Description>Docker.DotNet.X509 is a library that allows you to use certificate authentication with a remote Docker engine programmatically in your .NET applications.</Description> </PropertyGroup> + <ItemGroup Condition="$(TargetFrameworkIdentifier) == '.NETStandard'"> + <PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" /> + </ItemGroup> <ItemGroup> <ProjectReference Include="..\Docker.DotNet\Docker.DotNet.csproj" /> </ItemGroup> diff --git a/src/Docker.DotNet.X509/RSAUtil.cs b/src/Docker.DotNet.X509/RSAUtil.cs index 274e3ef7..9f9ceda9 100644 --- a/src/Docker.DotNet.X509/RSAUtil.cs +++ b/src/Docker.DotNet.X509/RSAUtil.cs @@ -2,179 +2,25 @@ namespace Docker.DotNet.X509; public static class RSAUtil { - private const byte Padding = 0x00; - public static X509Certificate2 GetCertFromPFX(string pfxFilePath, string password) { +#if NET9_0_OR_GREATER + return X509CertificateLoader.LoadPkcs12FromFile(pfxFilePath, password); +#else return new X509Certificate2(pfxFilePath, password); +#endif } - public static X509Certificate2 GetCertFromPFXSecure(string pfxFilePath, SecureString password) - { - return new X509Certificate2(pfxFilePath, password); - } - - public static X509Certificate2 GetCertFromPEMFiles(string certFilePath, string keyFilePath) - { - var cert = new X509Certificate2(certFilePath); - cert.PrivateKey = ReadFromPemFile(keyFilePath); - return cert; - } - - private static RSACryptoServiceProvider ReadFromPemFile(string pemFilePath) - { - var allBytes = File.ReadAllBytes(pemFilePath); - var mem = new MemoryStream(allBytes); - var startIndex = 0; - var endIndex = 0; - - using (var rdr = new BinaryReader(mem)) - { - if (!TryReadUntil(rdr, "-----BEGIN RSA PRIVATE KEY-----")) - { - throw new Exception("Invalid file format expected. No begin tag."); - } - - startIndex = (int)(mem.Position); - - const string endTag = "-----END RSA PRIVATE KEY-----"; - if (!TryReadUntil(rdr, endTag)) - { - throw new Exception("Invalid file format expected. No end tag."); - } - - endIndex = (int)(mem.Position - endTag.Length - 2); - } - - // Convert the bytes from base64; - var convertedBytes = Convert.FromBase64String(Encoding.UTF8.GetString(allBytes, startIndex, endIndex - startIndex)); - mem = new MemoryStream(convertedBytes); - using (var rdr = new BinaryReader(mem)) - { - var val = rdr.ReadUInt16(); - if (val != 0x8230) - { - throw new Exception("Invalid byte ordering."); - } - - // Discard the next bits of the version. - rdr.ReadUInt32(); - if (rdr.ReadByte() != Padding) - { - throw new InvalidDataException("Invalid ASN.1 format."); - } - - var rsa = new RSAParameters() - { - Modulus = rdr.ReadBytes(ReadIntegerCount(rdr)), - Exponent = rdr.ReadBytes(ReadIntegerCount(rdr)), - D = rdr.ReadBytes(ReadIntegerCount(rdr)), - P = rdr.ReadBytes(ReadIntegerCount(rdr)), - Q = rdr.ReadBytes(ReadIntegerCount(rdr)), - DP = rdr.ReadBytes(ReadIntegerCount(rdr)), - DQ = rdr.ReadBytes(ReadIntegerCount(rdr)), - InverseQ = rdr.ReadBytes(ReadIntegerCount(rdr)) - }; - - // Use "1" to indicate RSA. - var csp = new CspParameters(1) - { - - // Set the KeyContainerName so that native code that looks up the private key - // can find it. This produces a keyset file on disk as a side effect. - KeyContainerName = pemFilePath - }; - var rsaProvider = new RSACryptoServiceProvider(csp) - { - - // Setting to false makes sure the keystore file will be cleaned up - // when the current process exits. - PersistKeyInCsp = false - }; - - // Import the private key into the keyset. - rsaProvider.ImportParameters(rsa); - - return rsaProvider; - } - } - - /// <summary> - /// Reads an integer count encoding in DER ASN.1 format. - /// <summary> - private static int ReadIntegerCount(BinaryReader rdr) + public static X509Certificate2 GetCertFromPEM(string certFilePath, string keyFilePath) { - const byte highBitOctet = 0x80; - const byte ASN1_INTEGER = 0x02; - - if (rdr.ReadByte() != ASN1_INTEGER) - { - throw new Exception("Integer tag expected."); - } - - int count = 0; - var val = rdr.ReadByte(); - if ((val & highBitOctet) == highBitOctet) - { - byte numOfOctets = (byte)(val - highBitOctet); - if (numOfOctets > 4) - { - throw new InvalidDataException("Too many octets."); - } - - for (var i = 0; i < numOfOctets; i++) - { - count <<= 8; - count += rdr.ReadByte(); - } - } - else - { - count = val; - } - - while (rdr.ReadByte() == Padding) - { - count--; - } - - // The last read was a valid byte. Go back here. - rdr.BaseStream.Seek(-1, SeekOrigin.Current); - - return count; - } - - /// <summary> - /// Reads until the matching PEM tag is found. - /// <summary> - private static bool TryReadUntil(BinaryReader rdr, string tag) - { - char delim = '\n'; - char c; - char[] line = new char[64]; - int index; - - try - { - do - { - index = 0; - while ((c = rdr.ReadChar()) != delim) - { - if(c == '\r') - { - continue; - } - line[index] = c; - index++; - } - } while (new string(line, 0, index) != tag); - - return true; - } - catch (EndOfStreamException) - { - return false; - } +#if NETSTANDARD + return Polyfills.X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); +#elif NET9_0_OR_GREATER + var certificate = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); + return OperatingSystem.IsWindows() ? X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx), null) : certificate; +#elif NET6_0_OR_GREATER + var certificate = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); + return OperatingSystem.IsWindows() ? new X509Certificate2(certificate.Export(X509ContentType.Pfx)) : certificate; +#endif } } \ No newline at end of file diff --git a/src/Docker.DotNet.X509/X509Certificate2.cs b/src/Docker.DotNet.X509/X509Certificate2.cs new file mode 100644 index 00000000..f3a9ff91 --- /dev/null +++ b/src/Docker.DotNet.X509/X509Certificate2.cs @@ -0,0 +1,64 @@ +#if NETSTANDARD +namespace Docker.DotNet.X509.Polyfills; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.X509; + +public static class X509Certificate2 +{ + private static readonly X509CertificateParser CertificateParser = new X509CertificateParser(); + + public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) + { + if (!File.Exists(certPemFilePath)) + { + throw new FileNotFoundException(certPemFilePath); + } + + if (!File.Exists(keyPemFilePath)) + { + throw new FileNotFoundException(keyPemFilePath); + } + + using var keyPairStream = new StreamReader(keyPemFilePath); + + using var certificateStream = new MemoryStream(); + + var store = new Pkcs12StoreBuilder().Build(); + + var certificate = CertificateParser.ReadCertificate(File.ReadAllBytes(certPemFilePath)); + + var password = Guid.NewGuid().ToString("D"); + + var keyObject = new PemReader(keyPairStream).ReadObject(); + + var certificateEntry = new X509CertificateEntry(certificate); + + var keyParameter = ResolveKeyParameter(keyObject); + + var keyEntry = new AsymmetricKeyEntry(keyParameter); + + store.SetKeyEntry(certificate.SubjectDN + "_key", keyEntry, new[] { certificateEntry }); + store.Save(certificateStream, password.ToCharArray(), new SecureRandom()); + + return new System.Security.Cryptography.X509Certificates.X509Certificate2(Pkcs12Utilities.ConvertToDefiniteLength(certificateStream.ToArray()), password); + } + + private static AsymmetricKeyParameter ResolveKeyParameter(object keyObject) + { + switch (keyObject) + { + case AsymmetricCipherKeyPair ackp: + return ackp.Private; + case RsaPrivateCrtKeyParameters rpckp: + return rpckp; + default: + throw new ArgumentOutOfRangeException(nameof(keyObject), $"Unsupported asymmetric key entry encountered while trying to resolve key from input object '{keyObject.GetType()}'."); + } + } +} +#endif \ No newline at end of file diff --git a/src/Docker.DotNet/AnonymousCredentials.cs b/src/Docker.DotNet/AnonymousCredentials.cs index 6844c5f4..0375ca01 100644 --- a/src/Docker.DotNet/AnonymousCredentials.cs +++ b/src/Docker.DotNet/AnonymousCredentials.cs @@ -2,17 +2,17 @@ namespace Docker.DotNet; public class AnonymousCredentials : Credentials { - public override bool IsTlsCredentials() + public override void Dispose() { - return false; } - public override void Dispose() + public override bool IsTlsCredentials() { + return false; } - public override HttpMessageHandler GetHandler(HttpMessageHandler innerHandler) + public override HttpMessageHandler GetHandler(HttpMessageHandler handler) { - return innerHandler; + return handler; } } \ No newline at end of file diff --git a/src/Docker.DotNet/Credentials.cs b/src/Docker.DotNet/Credentials.cs index d971a21b..a20428a6 100644 --- a/src/Docker.DotNet/Credentials.cs +++ b/src/Docker.DotNet/Credentials.cs @@ -2,11 +2,9 @@ namespace Docker.DotNet; public abstract class Credentials : IDisposable { - public abstract bool IsTlsCredentials(); + public abstract void Dispose(); - public abstract HttpMessageHandler GetHandler(HttpMessageHandler innerHandler); + public abstract bool IsTlsCredentials(); - public virtual void Dispose() - { - } + public abstract HttpMessageHandler GetHandler(HttpMessageHandler handler); } \ No newline at end of file diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index ea1aa6ec..e58096c1 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -4,6 +4,9 @@ <PackageId>Docker.DotNet.Enhanced</PackageId> <Description>Docker.DotNet is a library that allows you to interact with the Docker Remote API programmatically with fully asynchronous, non-blocking and object-oriented code in your .NET applications.</Description> </PropertyGroup> + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" /> + </ItemGroup> <ItemGroup Condition="$(TargetFramework) == 'net8.0'"> <PackageReference Include="System.IO.Pipelines" Version="8.0.0" /> </ItemGroup> @@ -44,6 +47,8 @@ <Using Include="System.Threading.Tasks" /> <Using Include="Docker.DotNet" /> <Using Include="Docker.DotNet.Models" /> + <Using Include="Microsoft.Extensions.Logging" /> + <Using Include="Microsoft.Extensions.Logging.Abstractions" /> <Using Include="Microsoft.Net.Http.Client" /> </ItemGroup> </Project> \ No newline at end of file diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 821b1369..f0e67b6c 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -17,9 +17,12 @@ public sealed class DockerClient : IDockerClient private readonly Version _requestedApiVersion; - internal DockerClient(DockerClientConfiguration configuration, Version requestedApiVersion) + private readonly ILogger _logger; + + internal DockerClient(DockerClientConfiguration configuration, Version requestedApiVersion, ILogger logger = null) { _requestedApiVersion = requestedApiVersion; + _logger = logger ?? NullLogger.Instance; Configuration = configuration; DefaultTimeout = configuration.DefaultTimeout; @@ -72,7 +75,7 @@ await stream.ConnectAsync(timeout, cancellationToken) .ConfigureAwait(false); return dockerStream; - }); + }, _logger); break; case "tcp": @@ -82,11 +85,11 @@ await stream.ConnectAsync(timeout, cancellationToken) Scheme = configuration.Credentials.IsTlsCredentials() ? "https" : "http" }; uri = builder.Uri; - handler = new ManagedHandler(); + handler = new ManagedHandler(_logger); break; case "https": - handler = new ManagedHandler(); + handler = new ManagedHandler(_logger); break; case "unix": @@ -99,7 +102,7 @@ await sock.ConnectAsync(new Microsoft.Net.Http.Client.UnixDomainSocketEndPoint(p .ConfigureAwait(false); return sock; - }); + }, _logger); uri = new UriBuilder("http", uri.Segments.Last()).Uri; break; @@ -388,10 +391,7 @@ await HandleIfErrorResponseAsync(response.StatusCode, response, errorHandlers) throw new NotSupportedException("message handler does not support hijacked streams"); } - var stream = await content.ReadAsStreamAsync() - .ConfigureAwait(false); - - return (WriteClosableStream)stream; + return content.HijackStream(); } private async Task<HttpResponseMessage> PrivateMakeRequestAsync( diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index d92e4ef7..046359a0 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -50,9 +50,9 @@ public DockerClientConfiguration( public TimeSpan NamedPipeConnectTimeout { get; } - public DockerClient CreateClient(Version requestedApiVersion = null) + public DockerClient CreateClient(Version requestedApiVersion = null, ILogger logger = null) { - return new DockerClient(this, requestedApiVersion); + return new DockerClient(this, requestedApiVersion, logger); } public void Dispose() diff --git a/src/Docker.DotNet/Endpoints/ContainerOperations.cs b/src/Docker.DotNet/Endpoints/ContainerOperations.cs index 9fad6cad..226f2c76 100644 --- a/src/Docker.DotNet/Endpoints/ContainerOperations.cs +++ b/src/Docker.DotNet/Endpoints/ContainerOperations.cs @@ -297,14 +297,8 @@ public Task RenameContainerAsync(string id, ContainerRenameParameters parameters } var queryParameters = new QueryString<ContainerAttachParameters>(parameters); - var stream = await _client.MakeRequestForHijackedStreamAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/attach", queryParameters, null, null, cancellationToken).ConfigureAwait(false); - if (!stream.CanCloseWrite) - { - stream.Dispose(); - throw new NotSupportedException("Cannot shutdown write on this transport"); - } - - return new MultiplexedStream(stream, !tty); + var result = await _client.MakeRequestForStreamAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"containers/{id}/attach", queryParameters, null, null, cancellationToken).ConfigureAwait(false); + return new MultiplexedStream(result, !tty); } public async Task<ContainerWaitResponse> WaitContainerAsync(string id, CancellationToken cancellationToken = default(CancellationToken)) diff --git a/src/Docker.DotNet/Endpoints/ExecOperations.cs b/src/Docker.DotNet/Endpoints/ExecOperations.cs index 8b6e87b6..884be06b 100644 --- a/src/Docker.DotNet/Endpoints/ExecOperations.cs +++ b/src/Docker.DotNet/Endpoints/ExecOperations.cs @@ -83,21 +83,15 @@ public async Task<MultiplexedStream> StartAndAttachContainerExecAsync(string id, return await StartWithConfigContainerExecAsync(id, new ContainerExecStartParameters() { AttachStdin = true, AttachStderr = true, AttachStdout = true, Tty = tty }, cancellationToken); } - public async Task<MultiplexedStream> StartWithConfigContainerExecAsync(string id, ContainerExecStartParameters eConfig, CancellationToken cancellationToken) + public async Task<MultiplexedStream> StartWithConfigContainerExecAsync(string id, ContainerExecStartParameters parameters, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException(nameof(id)); } - var data = new JsonRequestContent<ContainerExecStartParameters>(eConfig, DockerClient.JsonSerializer); - var stream = await _client.MakeRequestForHijackedStreamAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"exec/{id}/start", null, data, null, cancellationToken).ConfigureAwait(false); - if (!stream.CanCloseWrite) - { - stream.Dispose(); - throw new NotSupportedException("Cannot shutdown write on this transport"); - } - - return new MultiplexedStream(stream, !eConfig.Tty); + var data = new JsonRequestContent<ContainerExecStartParameters>(parameters, DockerClient.JsonSerializer); + var result = await _client.MakeRequestForStreamAsync(new[] { NoSuchContainerHandler }, HttpMethod.Post, $"exec/{id}/start", null, data, null, cancellationToken).ConfigureAwait(false); + return new MultiplexedStream(result, !parameters.Tty); } } \ No newline at end of file diff --git a/src/Docker.DotNet/JsonNullableDateTimeConverter.cs b/src/Docker.DotNet/JsonNullableDateTimeConverter.cs index 52b1fffe..fa92a113 100644 --- a/src/Docker.DotNet/JsonNullableDateTimeConverter.cs +++ b/src/Docker.DotNet/JsonNullableDateTimeConverter.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet; +namespace Docker.DotNet; internal sealed class JsonNullableDateTimeConverter : JsonConverter<DateTime?> { diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs index 94ba4246..2a216647 100644 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ b/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -1,39 +1,27 @@ -#if !NET45 -#endif - namespace Microsoft.Net.Http.Client; -internal class BufferedReadStream : WriteClosableStream, IPeekableStream +internal sealed class BufferedReadStream : WriteClosableStream, IPeekableStream { - private const char CR = '\r'; - private const char LF = '\n'; - private readonly Stream _inner; private readonly Socket _socket; private readonly byte[] _buffer; - private volatile int _bufferRefCount; - private int _bufferOffset = 0; - private int _bufferCount = 0; - private bool _disposed; + private readonly ILogger _logger; + private int _bufferRefCount; + private int _bufferOffset; + private int _bufferCount; - public BufferedReadStream(Stream inner, Socket socket) - : this(inner, socket, 1024) - { } + public BufferedReadStream(Stream inner, Socket socket, ILogger logger) + : this(inner, socket, 8192, logger) + { + } - public BufferedReadStream(Stream inner, Socket socket, int bufferLength) + public BufferedReadStream(Stream inner, Socket socket, int bufferLength, ILogger logger) { - if (inner == null) - { - throw new ArgumentNullException(nameof(inner)); - } - _inner = inner; + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); _socket = socket; -#if !NET45 - _bufferRefCount = 1; _buffer = ArrayPool<byte>.Shared.Rent(bufferLength); -#else - _buffer = new byte[bufferLength]; -#endif + _logger = logger; + _bufferRefCount = 1; } public override bool CanRead @@ -67,32 +55,32 @@ public override long Position set { throw new NotSupportedException(); } } - public override long Seek(long offset, SeekOrigin origin) + public override bool CanCloseWrite => _socket != null || _inner is WriteClosableStream; + + protected override void Dispose(bool disposing) { - throw new NotSupportedException(); + // TODO: Why does disposing break the implementation, see the other chunked streams too. + // base.Dispose(disposing); + + if (disposing) + { + _inner.Dispose(); + + if (Interlocked.Decrement(ref _bufferRefCount) == 0) + { + ArrayPool<byte>.Shared.Return(_buffer); + } + } } - public override void SetLength(long value) + public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } - protected override void Dispose(bool disposing) + public override void SetLength(long value) { - if (!_disposed) - { - _disposed = true; - if (disposing) - { - _inner.Dispose(); -#if !NET45 - if (Interlocked.Decrement(ref _bufferRefCount) == 0) - { - ArrayPool<byte>.Shared.Return(_buffer); - } -#endif - } - } + throw new NotSupportedException(); } public override void Flush() @@ -137,6 +125,23 @@ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, Cancel return _inner.ReadAsync(buffer, offset, count, cancellationToken); } + public override void CloseWrite() + { + if (_socket != null) + { + _socket.Shutdown(SocketShutdown.Send); + return; + } + + if (_inner is WriteClosableStream writeClosableStream) + { + writeClosableStream.CloseWrite(); + return; + } + + throw new NotSupportedException("Cannot shutdown write on this transport"); + } + public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) { int read = PeekBuffer(buffer, toPeek, out peeked, out available, out remaining); @@ -153,132 +158,124 @@ public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available throw new NotSupportedException("_inner stream isn't a peekable stream"); } - private int ReadBuffer(byte[] buffer, int offset, int count) + public async Task<string> ReadLineAsync(CancellationToken cancellationToken) { - if (_bufferCount > 0) - { - int toCopy = Math.Min(_bufferCount, count); - Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); - _bufferOffset += toCopy; - _bufferCount -= toCopy; - return toCopy; - } + const char nullChar = '\0'; - return 0; - } + const char cr = '\r'; - private int PeekBuffer(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) - { - if (_bufferCount > 0) - { - int toCopy = Math.Min(_bufferCount, (int)toPeek); - Buffer.BlockCopy(_buffer, _bufferOffset, buffer, 0, toCopy); - peeked = (uint) toCopy; - available = (uint)_bufferCount; - remaining = available - peeked; - return toCopy; - } + const char lf = '\n'; - peeked = 0; - available = 0; - remaining = 0; - return 0; - } - - private async Task EnsureBufferedAsync(CancellationToken cancel) - { if (_bufferCount == 0) { - _bufferOffset = 0; -#if !NET45 - bool validBuffer = Interlocked.Increment(ref _bufferRefCount) > 1; + var bufferInUse = Interlocked.Increment(ref _bufferRefCount) > 1; + try { - if (validBuffer) + if (bufferInUse) { - _bufferCount = await _inner.ReadAsync(_buffer, _bufferOffset, _buffer.Length, cancel).ConfigureAwait(false); + _bufferOffset = 0; + + _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) + .ConfigureAwait(false); } } + catch (Exception e) + { + _logger.LogCritical(e, "Failed to read from buffer."); + throw; + } finally { - if ((Interlocked.Decrement(ref _bufferRefCount) == 0) && validBuffer) + var bufferReleased = Interlocked.Decrement(ref _bufferRefCount) == 0; + + if (bufferInUse && bufferReleased) { ArrayPool<byte>.Shared.Return(_buffer); } } -#else - _bufferCount = await _inner.ReadAsync(_buffer, _bufferOffset, _buffer.Length, cancel).ConfigureAwait(false); -#endif - if (_bufferCount == 0) - { - throw new IOException("Unexpected end of stream"); - } } - } - // TODO: Line length limits? - public async Task<string> ReadLineAsync(CancellationToken cancel) - { - ThrowIfDisposed(); - StringBuilder builder = new StringBuilder(); - bool foundCR = false, foundCRLF = false; - do + if (_logger.IsEnabled(LogLevel.Debug)) { - if (_bufferCount == 0) - { - await EnsureBufferedAsync(cancel).ConfigureAwait(false); - } + var content = Encoding.ASCII.GetString(_buffer.TakeWhile(value => value != nullChar).ToArray()); + content = content.Replace("\r", "<CR>"); + content = content.Replace("\n", "<LF>"); + _logger.LogDebug("Raw buffer content: '{Content}'.", content); + } + + var start = _bufferOffset; - char ch = (char)_buffer[_bufferOffset]; // TODO: Encoding enforcement - builder.Append(ch); - _bufferOffset++; - _bufferCount--; - if (ch == CR) + var end = -1; + + for (var i = _bufferOffset; i < _buffer.Length; i++) + { + // If a null terminator is found, skip the rest of the buffer. + if (_buffer[i] == nullChar) { - foundCR = true; + _logger.LogDebug("Null terminator found at position: {Position}.", i); + end = i; + break; } - else if (ch == LF) + + // Check if current byte is CR and the next byte is LF. + if (_buffer[i] == cr && i + 1 < _buffer.Length && _buffer[i + 1] == lf) { - if (foundCR) - { - foundCRLF = true; - } - else - { - foundCR = false; - } + _logger.LogDebug("CRLF found at positions {CR} and {LF}.", i, i + 1); + end = i; + break; } } - while (!foundCRLF); - - return builder.ToString(0, builder.Length - 2); // Drop the CRLF - } - private void ThrowIfDisposed() - { - if (_disposed) + // No CRLF found, process the entire remaining buffer. + if (end == -1) { - throw new ObjectDisposedException(nameof(BufferedReadStream)); + end = _buffer.Length; + _logger.LogDebug("No CRLF found. Setting end position to buffer length: {End}.", end); + } + else + { + _bufferCount -= end - start + 2; + _bufferOffset = end + 2; + _logger.LogDebug("CRLF found. Consumed {Consumed} bytes. New offset: {Offset}, Remaining count: {RemainingBytes}.", end - start + 2, _bufferOffset, _bufferCount); } - } - public override bool CanCloseWrite => _socket != null || _inner is WriteClosableStream; + var length = end - start; + var line = Encoding.ASCII.GetString(_buffer, start, length); - public override void CloseWrite() + _logger.LogDebug("String from positions {Start} to {End} (length {Length}): '{Line}'.", start, end, length, line); + return line; + } + + private int ReadBuffer(byte[] buffer, int offset, int count) { - if (_socket != null) + if (_bufferCount > 0) { - _socket.Shutdown(SocketShutdown.Send); - return; + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; } - var s = _inner as WriteClosableStream; - if (s != null) + return 0; + } + + private int PeekBuffer(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) + { + if (_bufferCount > 0) { - s.CloseWrite(); - return; + int toCopy = Math.Min(_bufferCount, (int)toPeek); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, 0, toCopy); + peeked = (uint) toCopy; + available = (uint)_bufferCount; + remaining = available - peeked; + return toCopy; } - throw new NotSupportedException("Cannot shutdown write on this transport"); + peeked = 0; + available = 0; + remaining = 0; + return 0; } } \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs index 99496512..3bcdc582 100644 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs +++ b/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs @@ -1,20 +1,19 @@ namespace Microsoft.Net.Http.Client; -internal class ChunkedReadStream : WriteClosableStream +internal sealed class ChunkedReadStream : Stream { private readonly BufferedReadStream _inner; - private long _chunkBytesRemaining; - private bool _disposed; + private int _chunkBytesRemaining; private bool _done; - public ChunkedReadStream(BufferedReadStream inner) + public ChunkedReadStream(BufferedReadStream stream) { - _inner = inner; + _inner = stream ?? throw new ArgumentNullException(nameof(stream)); } public override bool CanRead { - get { return !_disposed; } + get { return _inner.CanRead; } } public override bool CanSeek @@ -32,11 +31,6 @@ public override bool CanWrite get { return false; } } - public override bool CanCloseWrite - { - get { return _inner.CanCloseWrite; } - } - public override long Length { get { throw new NotSupportedException(); } @@ -52,12 +46,10 @@ public override int ReadTimeout { get { - ThrowIfDisposed(); return _inner.ReadTimeout; } set { - ThrowIfDisposed(); _inner.ReadTimeout = value; } } @@ -66,89 +58,78 @@ public override int WriteTimeout { get { - ThrowIfDisposed(); return _inner.WriteTimeout; } set { - ThrowIfDisposed(); _inner.WriteTimeout = value; } } - public override int Read(byte[] buffer, int offset, int count) + protected override void Dispose(bool disposing) { - return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + // base.Dispose(disposing); + + if (disposing) + { + // _inner.Dispose(); + } } - public async override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override int Read(byte[] buffer, int offset, int count) { - // TODO: Validate buffer - ThrowIfDisposed(); + throw new NotSupportedException(); + } + public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { if (_done) { return 0; } - cancellationToken.ThrowIfCancellationRequested(); - if (_chunkBytesRemaining == 0) { - string headerLine = await _inner.ReadLineAsync(cancellationToken).ConfigureAwait(false); - if (!long.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) + var headerLine = await _inner.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) { - throw new IOException("Invalid chunk header: " + headerLine); + throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); } } - int read = 0; + var readBytesCount = 0; + if (_chunkBytesRemaining > 0) { - int toRead = (int)Math.Min(count, _chunkBytesRemaining); - read = await _inner.ReadAsync(buffer, offset, toRead, cancellationToken).ConfigureAwait(false); - if (read == 0) + var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); + + readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) + .ConfigureAwait(false); + + if (readBytesCount == 0) { throw new EndOfStreamException(); } - _chunkBytesRemaining -= read; + _chunkBytesRemaining -= readBytesCount; } if (_chunkBytesRemaining == 0) { - // End of chunk, read the terminator CRLF - var trailer = await _inner.ReadLineAsync(cancellationToken).ConfigureAwait(false); - if (trailer.Length > 0) - { - throw new IOException("Invalid chunk trailer"); - } + var emptyLine = await _inner.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); - if (read == 0) + if (!string.IsNullOrEmpty(emptyLine)) { - _done = true; + throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); } - } - return read; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - // TODO: Sync drain with timeout if small number of bytes remaining? This will let us re-use the connection. - _inner.Dispose(); + _done = readBytesCount == 0; } - _disposed = true; - } - private void ThrowIfDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(typeof(ContentLengthReadStream).FullName); - } + return readBytesCount; } public override void Write(byte[] buffer, int offset, int count) @@ -175,9 +156,4 @@ public override void Flush() { _inner.Flush(); } - - public override void CloseWrite() - { - _inner.CloseWrite(); - } } \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs index 65e74185..e66174a3 100644 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs +++ b/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs @@ -1,17 +1,14 @@ namespace Microsoft.Net.Http.Client; -internal class ChunkedWriteStream : Stream +internal sealed class ChunkedWriteStream : Stream { private static readonly byte[] s_EndContentBytes = Encoding.ASCII.GetBytes("0\r\n\r\n"); - private Stream _innerStream; + private readonly Stream _inner; public ChunkedWriteStream(Stream stream) { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - - _innerStream = stream; + _inner = stream ?? throw new ArgumentNullException(nameof(stream)); } public override bool CanRead => false; @@ -31,14 +28,24 @@ public override long Position set { throw new NotImplementedException(); } } + protected override void Dispose(bool disposing) + { + // base.Dispose(disposing); + + if (disposing) + { + // _inner.Dispose(); + } + } + public override void Flush() { - _innerStream.Flush(); + _inner.Flush(); } public override Task FlushAsync(CancellationToken cancellationToken) { - return _innerStream.FlushAsync(cancellationToken); + return _inner.FlushAsync(cancellationToken); } public override int Read(byte[] buffer, int offset, int count) @@ -58,7 +65,7 @@ public override void SetLength(long value) public override void Write(byte[] buffer, int offset, int count) { - WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); + throw new NotSupportedException(); } public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) @@ -69,13 +76,13 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc } var chunkSize = Encoding.ASCII.GetBytes(count.ToString("x") + "\r\n"); - await _innerStream.WriteAsync(chunkSize, 0, chunkSize.Length, cancellationToken).ConfigureAwait(false); - await _innerStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - await _innerStream.WriteAsync(chunkSize, chunkSize.Length - 2, 2, cancellationToken).ConfigureAwait(false); + await _inner.WriteAsync(chunkSize, 0, chunkSize.Length, cancellationToken).ConfigureAwait(false); + await _inner.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + await _inner.WriteAsync(chunkSize, chunkSize.Length - 2, 2, cancellationToken).ConfigureAwait(false); } public Task EndContentAsync(CancellationToken cancellationToken) { - return _innerStream.WriteAsync(s_EndContentBytes, 0, s_EndContentBytes.Length, cancellationToken); + return _inner.WriteAsync(s_EndContentBytes, 0, s_EndContentBytes.Length, cancellationToken); } } \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ContentLengthReadStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/ContentLengthReadStream.cs index a46a736f..2d38d85c 100644 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ContentLengthReadStream.cs +++ b/src/Docker.DotNet/Microsoft.Net.Http.Client/ContentLengthReadStream.cs @@ -100,7 +100,7 @@ public override int Read(byte[] buffer, int offset, int count) return read; } - public async override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { // TODO: Validate args if (_disposed) diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs index 666cc8f2..aba3d8fc 100644 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs @@ -1,13 +1,15 @@ namespace Microsoft.Net.Http.Client; -internal class HttpConnection : IDisposable +internal sealed class HttpConnection : IDisposable { + // private static readonly ISet<string> DockerStreamHeaders = new HashSet<string>{ "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; + public HttpConnection(BufferedReadStream transport) { Transport = transport; } - public BufferedReadStream Transport { get; private set; } + public BufferedReadStream Transport { get; } public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { @@ -37,8 +39,8 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Can // Receive headers List<string> responseLines = await ReadResponseLinesAsync(cancellationToken); - // Determine response type (Chunked, Content-Length, opaque, none...) - // Receive body + + // Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque) return CreateResponseMessage(responseLines); } catch (Exception ex) @@ -83,13 +85,22 @@ private string SerializeRequest(HttpRequestMessage request) private async Task<List<string>> ReadResponseLinesAsync(CancellationToken cancellationToken) { - List<string> lines = new List<string>(); - string line = await Transport.ReadLineAsync(cancellationToken); - while (line.Length > 0) + var lines = new List<string>(12); + + do { + var line = await Transport.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (string.IsNullOrEmpty(line)) + { + break; + } + lines.Add(line); - line = await Transport.ReadLineAsync(cancellationToken); } + while (true); + return lines; } @@ -103,8 +114,8 @@ private HttpResponseMessage CreateResponseMessage(List<string> responseLines) { throw new HttpRequestException("Invalid response line: " + responseLine); } - int statusCode = 0; - if (int.TryParse(responseLineParts[1], NumberStyles.None, CultureInfo.InvariantCulture, out statusCode)) + + if (int.TryParse(responseLineParts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode)) { // TODO: Validate range } @@ -135,22 +146,16 @@ private HttpResponseMessage CreateResponseMessage(List<string> responseLines) System.Diagnostics.Debug.Assert(success, "Failed to add response header: " + rawHeader); } } - // After headers have been set - content.ResolveResponseStream(chunked: response.Headers.TransferEncodingChunked.HasValue && response.Headers.TransferEncodingChunked.Value); + // var isStream = content.Headers.TryGetValues("Content-Type", out var headerValues) + // && headerValues.Any(header => DockerStreamHeaders.Contains(header)); + + content.ResolveResponseStream(chunked: response.Headers.TransferEncodingChunked.HasValue && response.Headers.TransferEncodingChunked.Value); return response; } public void Dispose() { - Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - Transport.Dispose(); - } + Transport.Dispose(); } } \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs index f9fcef56..a34d0ac7 100644 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs +++ b/src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs @@ -4,21 +4,27 @@ namespace Microsoft.Net.Http.Client; public class ManagedHandler : HttpMessageHandler { + private readonly ILogger _logger; + public delegate Task<Stream> StreamOpener(string host, int port, CancellationToken cancellationToken); + public delegate Task<Socket> SocketOpener(string host, int port, CancellationToken cancellationToken); - public ManagedHandler() + public ManagedHandler(ILogger logger) { + _logger = logger; _socketOpener = TCPSocketOpenerAsync; } - public ManagedHandler(StreamOpener opener) + public ManagedHandler(StreamOpener opener, ILogger logger) { + _logger = logger; _streamOpener = opener ?? throw new ArgumentNullException(nameof(opener)); } - public ManagedHandler(SocketOpener opener) + public ManagedHandler(SocketOpener opener, ILogger logger) { + _logger = logger; _socketOpener = opener ?? throw new ArgumentNullException(nameof(opener)); } @@ -52,7 +58,7 @@ public IWebProxy Proxy private SocketOpener _socketOpener; private IWebProxy _proxy; - protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request == null) { @@ -172,7 +178,7 @@ private async Task<HttpResponseMessage> ProcessRequestAsync(HttpRequestMessage r transport = sslStream; } - var bufferedReadStream = new BufferedReadStream(transport, socket); + var bufferedReadStream = new BufferedReadStream(transport, socket, _logger); var connection = new HttpConnection(bufferedReadStream); return await connection.SendAsync(request, cancellationToken); } @@ -308,36 +314,35 @@ private ProxyMode DetermineProxyModeAndAddressLine(HttpRequestMessage request) private static async Task<Socket> TCPSocketOpenerAsync(string host, int port, CancellationToken cancellationToken) { - var addresses = await Dns.GetHostAddressesAsync(host).ConfigureAwait(false); + var addresses = await Dns.GetHostAddressesAsync(host) + .ConfigureAwait(false); + if (addresses.Length == 0) { - throw new Exception($"could not resolve address for {host}"); + throw new Exception($"Unable to resolve any IP addresses for the host '{host}'."); } - Socket connectedSocket = null; - Exception lastException = null; + var exceptions = new List<Exception>(); + foreach (var address in addresses) { - var s = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + try { - await s.ConnectAsync(address, port).ConfigureAwait(false); - connectedSocket = s; - break; + await socket.ConnectAsync(address, port) + .ConfigureAwait(false); + + return socket; } catch (Exception e) { - s.Dispose(); - lastException = e; + socket.Dispose(); + exceptions.Add(e); } } - if (connectedSocket == null) - { - throw lastException; - } - - return connectedSocket; + throw new AggregateException(exceptions); } private async Task TunnelThroughProxyAsync(HttpRequestMessage request, Stream transport, CancellationToken cancellationToken) @@ -355,7 +360,7 @@ private async Task TunnelThroughProxyAsync(HttpRequestMessage request, Stream tr connectRequest.SetAddressLineProperty(authority); connectRequest.Headers.Host = authority; - HttpConnection connection = new HttpConnection(new BufferedReadStream(transport, null)); + HttpConnection connection = new HttpConnection(new BufferedReadStream(transport, null, _logger)); HttpResponseMessage connectResponse; try { diff --git a/src/Docker.DotNet/MultiplexedStream.cs b/src/Docker.DotNet/MultiplexedStream.cs index 48d5727e..e35db4d1 100644 --- a/src/Docker.DotNet/MultiplexedStream.cs +++ b/src/Docker.DotNet/MultiplexedStream.cs @@ -1,51 +1,55 @@ -#if !NET45 -#endif - namespace Docker.DotNet; -public class MultiplexedStream : IDisposable, IPeekableStream +public sealed class MultiplexedStream : IDisposable, IPeekableStream { + private const int BufferSize = 16384; private readonly Stream _stream; private TargetStream _target; private int _remaining; private readonly byte[] _header = new byte[8]; private readonly bool _multiplexed; - const int BufferSize = 81920; - public MultiplexedStream(Stream stream, bool multiplexed) { _stream = stream; _multiplexed = multiplexed; } - public enum TargetStream + public void Dispose() + { + _stream.Dispose(); + } + + public enum TargetStream : byte { StandardIn = 0, StandardOut = 1, StandardError = 2 } - public struct ReadResult + public readonly struct ReadResult { - public int Count { get; set; } - public TargetStream Target { get; set; } + public ReadResult(TargetStream target, int count) + { + Target = target; + Count = count; + } + + public TargetStream Target { get; } + + public int Count { get; } + public bool EOF => Count == 0; } public void CloseWrite() { - if (_stream is WriteClosableStream closable) + if (_stream is WriteClosableStream writeClosableStream) { - closable.CloseWrite(); + writeClosableStream.CloseWrite(); } } - public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _stream.WriteAsync(buffer, offset, count, cancellationToken); - } - public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) { if (_stream is IPeekableStream peekableStream) @@ -56,46 +60,50 @@ public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available throw new NotSupportedException("_stream isn't a peekable stream"); } + public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _stream.WriteAsync(buffer, offset, count, cancellationToken); + } + public async Task<ReadResult> ReadOutputAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { + int readBytesCount; + if (!_multiplexed) { - return new ReadResult - { - Count = await _stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false), - Target = TargetStream.StandardOut - }; + readBytesCount = await _stream.ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + + return new ReadResult(TargetStream.StandardOut, readBytesCount); } while (_remaining == 0) { for (var i = 0; i < _header.Length;) { - var n = await _stream.ReadAsync(_header, i, _header.Length - i, cancellationToken).ConfigureAwait(false); - if (n == 0) + readBytesCount = await _stream.ReadAsync(_header, i, _header.Length - i, cancellationToken) + .ConfigureAwait(false); + + if (readBytesCount == 0) { if (i == 0) { - // End of the stream. return new ReadResult(); } throw new EndOfStreamException(); } - i += n; + i += readBytesCount; } - switch ((TargetStream)_header[0]) + if (Enum.IsDefined(typeof(TargetStream), _header[0])) { - case TargetStream.StandardIn: - case TargetStream.StandardOut: - case TargetStream.StandardError: - _target = (TargetStream)_header[0]; - break; - - default: - throw new IOException("unknown stream type"); + _target = (TargetStream)_header[0]; + } + else + { + throw new IOException($"Unknown stream type: '{_header[0]}'."); } _remaining = (_header[4] << 24) | @@ -104,87 +112,84 @@ public async Task<ReadResult> ReadOutputAsync(byte[] buffer, int offset, int cou _header[7]; } - var toRead = Math.Min(count, _remaining); - var read = await _stream.ReadAsync(buffer, offset, toRead, cancellationToken).ConfigureAwait(false); - if (read == 0) + var remainingBytesCount = Math.Min(count, _remaining); + + readBytesCount = await _stream.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) + .ConfigureAwait(false); + + if (readBytesCount == 0) { throw new EndOfStreamException(); } - _remaining -= read; - return new ReadResult - { - Count = read, - Target = _target - }; + _remaining -= readBytesCount; + return new ReadResult(_target, readBytesCount); } public async Task<(string stdout, string stderr)> ReadOutputToEndAsync(CancellationToken cancellationToken) { - using (MemoryStream outMem = new MemoryStream(), outErr = new MemoryStream()) - { - await CopyOutputToAsync(Stream.Null, outMem, outErr, cancellationToken); + using MemoryStream stdoutMemoryStream = new MemoryStream(), stderrMemoryStream = new MemoryStream(); - outMem.Seek(0, SeekOrigin.Begin); - outErr.Seek(0, SeekOrigin.Begin); + using StreamReader stdoutStreamReader = new StreamReader(stdoutMemoryStream), stderrStreamReader = new StreamReader(stderrMemoryStream); - using (StreamReader outRdr = new StreamReader(outMem), errRdr = new StreamReader(outErr)) - { - var stdout = outRdr.ReadToEnd(); - var stderr = errRdr.ReadToEnd(); - return (stdout, stderr); - } - } + await CopyOutputToAsync(Stream.Null, stdoutMemoryStream, stderrMemoryStream, cancellationToken) + .ConfigureAwait(false); + + stdoutMemoryStream.Seek(0, SeekOrigin.Begin); + stderrMemoryStream.Seek(0, SeekOrigin.Begin); + + var stdoutReadTask = stdoutStreamReader.ReadToEndAsync(); + var stderrReadTask = stderrStreamReader.ReadToEndAsync(); + await Task.WhenAll(stdoutReadTask, stderrReadTask) + .ConfigureAwait(false); + + return (stdoutReadTask.Result, stderrReadTask.Result); } public async Task CopyFromAsync(Stream input, CancellationToken cancellationToken) { -#if !NET45 var buffer = ArrayPool<byte>.Shared.Rent(BufferSize); -#else - var buffer = new byte[BufferSize]; -#endif try { for (;;) { - var count = await input.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); - if (count == 0) + var readBytesCount = await input.ReadAsync(buffer, 0, buffer.Length, cancellationToken) + .ConfigureAwait(false); + + if (readBytesCount == 0) { break; } - await WriteAsync(buffer, 0, count, cancellationToken).ConfigureAwait(false); + await WriteAsync(buffer, 0, readBytesCount, cancellationToken) + .ConfigureAwait(false); } } finally { -#if !NET45 ArrayPool<byte>.Shared.Return(buffer); -#endif } } public async Task CopyOutputToAsync(Stream stdin, Stream stdout, Stream stderr, CancellationToken cancellationToken) { -#if !NET45 var buffer = ArrayPool<byte>.Shared.Rent(BufferSize); -#else - var buffer = new byte[BufferSize]; -#endif try { for (;;) { - var result = await ReadOutputAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + var result = await ReadOutputAsync(buffer, 0, buffer.Length, cancellationToken) + .ConfigureAwait(false); + if (result.EOF) { return; } Stream stream; + switch (result.Target) { case TargetStream.StandardIn: @@ -197,22 +202,16 @@ public async Task CopyOutputToAsync(Stream stdin, Stream stdout, Stream stderr, stream = stderr; break; default: - throw new InvalidOperationException($"Unknown TargetStream: '{result.Target}'."); + throw new IOException($"Unknown stream type: '{result.Target}'."); } - await stream.WriteAsync(buffer, 0, result.Count, cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(buffer, 0, result.Count, cancellationToken) + .ConfigureAwait(false); } } finally { -#if !NET45 ArrayPool<byte>.Shared.Return(buffer); -#endif } } - - public void Dispose() - { - ((IDisposable)_stream).Dispose(); - } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/CommonCommands.cs b/test/Docker.DotNet.Tests/CommonCommands.cs new file mode 100644 index 00000000..484f28db --- /dev/null +++ b/test/Docker.DotNet.Tests/CommonCommands.cs @@ -0,0 +1,8 @@ +namespace Docker.DotNet.Tests; + +public static class CommonCommands +{ + public static readonly string[] SleepInfinity = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; sleep infinity"]; + + public static readonly string[] EchoToStdoutAndStderr = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; while true; do echo \"stdout message\"; echo \"stderr message\" >&2; sleep 1; done"]; +} \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj index 342e804d..470c3e68 100644 --- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj +++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj @@ -14,6 +14,9 @@ <ProjectReference Include="..\..\src\Docker.DotNet.X509\Docker.DotNet.X509.csproj" /> <ProjectReference Include="..\..\src\Docker.DotNet\Docker.DotNet.csproj" /> </ItemGroup> + <ItemGroup> + <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> + </ItemGroup> <ItemGroup> <Using Include="System" /> <Using Include="System.Collections.Generic" /> @@ -24,7 +27,10 @@ <Using Include="System.Threading" /> <Using Include="System.Threading.Tasks" /> <Using Include="Docker.DotNet.Models" /> + <Using Include="Microsoft.Extensions.Logging" /> + <Using Include="Microsoft.Extensions.Logging.Abstractions" /> <Using Include="Xunit" /> <Using Include="Xunit.Abstractions" /> + <Using Include="Xunit.Sdk" /> </ItemGroup> </Project> \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/IConfigOperationsTests.cs b/test/Docker.DotNet.Tests/IConfigOperationsTests.cs index e3c9b78c..533d5d25 100644 --- a/test/Docker.DotNet.Tests/IConfigOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IConfigOperationsTests.cs @@ -3,21 +3,21 @@ namespace Docker.DotNet.Tests; [Collection(nameof(TestCollection))] public class IConfigOperationsTests { - private readonly DockerClient _dockerClient; - private readonly TestOutput _output; + private readonly TestFixture _testFixture; + private readonly ITestOutputHelper _testOutputHelper; - public IConfigOperationsTests(TestFixture testFixture, ITestOutputHelper outputHelper) + public IConfigOperationsTests(TestFixture testFixture, ITestOutputHelper testOutputHelper) { - _dockerClient = testFixture.DockerClient; - _output = new TestOutput(outputHelper); + _testFixture = testFixture; + _testOutputHelper = testOutputHelper; } [Fact] public async Task SwarmConfig_CanCreateAndRead() { - var currentConfigs = await _dockerClient.Configs.ListConfigsAsync(); + var currentConfigs = await _testFixture.DockerClient.Configs.ListConfigsAsync(); - _output.WriteLine($"Current Configs: {currentConfigs.Count}"); + _testOutputHelper.WriteLine($"Current Configs: {currentConfigs.Count}"); var testConfigSpec = new SwarmConfigSpec { @@ -31,15 +31,15 @@ public async Task SwarmConfig_CanCreateAndRead() Config = testConfigSpec }; - var createdConfig = await _dockerClient.Configs.CreateConfigAsync(configParameters); + var createdConfig = await _testFixture.DockerClient.Configs.CreateConfigAsync(configParameters); Assert.NotNull(createdConfig.ID); - _output.WriteLine($"Config created: {createdConfig.ID}"); + _testOutputHelper.WriteLine($"Config created: {createdConfig.ID}"); - var configs = await _dockerClient.Configs.ListConfigsAsync(); + var configs = await _testFixture.DockerClient.Configs.ListConfigsAsync(); Assert.Contains(configs, c => c.ID == createdConfig.ID); - _output.WriteLine($"Current Configs: {configs.Count}"); + _testOutputHelper.WriteLine($"Current Configs: {configs.Count}"); - var configResponse = await _dockerClient.Configs.InspectConfigAsync(createdConfig.ID); + var configResponse = await _testFixture.DockerClient.Configs.InspectConfigAsync(createdConfig.ID); Assert.NotNull(configResponse); @@ -49,10 +49,10 @@ public async Task SwarmConfig_CanCreateAndRead() Assert.Equal(configResponse.Spec.Templating, testConfigSpec.Templating); - _output.WriteLine("Config created is the same."); + _testOutputHelper.WriteLine("Config created is the same."); - await _dockerClient.Configs.RemoveConfigAsync(createdConfig.ID); + await _testFixture.DockerClient.Configs.RemoveConfigAsync(createdConfig.ID); - await Assert.ThrowsAsync<DockerApiException>(() => _dockerClient.Configs.InspectConfigAsync(createdConfig.ID)); + await Assert.ThrowsAsync<DockerApiException>(() => _testFixture.DockerClient.Configs.InspectConfigAsync(createdConfig.ID)); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs index d8d2d10d..31e1b747 100644 --- a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs @@ -3,101 +3,55 @@ namespace Docker.DotNet.Tests; [Collection(nameof(TestCollection))] public class IContainerOperationsTests { - private readonly CancellationTokenSource _cts; + private readonly TestFixture _testFixture; + private readonly ITestOutputHelper _testOutputHelper; - private readonly TestOutput _output; - private readonly string _imageId; - private readonly DockerClientConfiguration _dockerClientConfiguration; - private readonly DockerClient _dockerClient; - - public IContainerOperationsTests(TestFixture testFixture, ITestOutputHelper outputHelper) + public IContainerOperationsTests(TestFixture testFixture, ITestOutputHelper testOutputHelper) { - _output = new TestOutput(outputHelper); - - _dockerClientConfiguration = testFixture.DockerClientConfiguration; - _dockerClient = _dockerClientConfiguration.CreateClient(); - - // Do not wait forever in case it gets stuck - _cts = CancellationTokenSource.CreateLinkedTokenSource(testFixture.Cts.Token); - _cts.CancelAfter(TimeSpan.FromMinutes(5)); - _cts.Token.Register(() => throw new TimeoutException("ContainerOperationsTests timeout")); - - _imageId = testFixture.Image.ID; + _testFixture = testFixture; + _testOutputHelper = testOutputHelper; } [Fact] public async Task CreateContainerAsync_CreatesContainer() { - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr }, - _cts.Token + _testFixture.Cts.Token ); Assert.NotNull(createContainerResponse); Assert.NotEmpty(createContainerResponse.ID); } - // Timeout causing task to be cancelled - [Theory(Skip = "There is nothing we can do to delay CreateContainerAsync (aka HttpClient.SendAsync) deterministic. We cannot control if it responses successful before the timeout.")] - [InlineData(1)] - [InlineData(5)] - [InlineData(10)] - public async Task CreateContainerAsync_TimeoutExpires_Fails(int millisecondsTimeout) - { - using var dockerClientWithTimeout = _dockerClientConfiguration.CreateClient(); - - dockerClientWithTimeout.DefaultTimeout = TimeSpan.FromMilliseconds(millisecondsTimeout); - - _output.WriteLine($"Time available for CreateContainer operation: {millisecondsTimeout} ms'"); - - var timer = new Stopwatch(); - timer.Start(); - - var createContainerTask = dockerClientWithTimeout.Containers.CreateContainerAsync( - new CreateContainerParameters - { - Image = _imageId, - Name = Guid.NewGuid().ToString(), - }, - _cts.Token); - - _ = await Assert.ThrowsAsync<OperationCanceledException>(() => createContainerTask); - - timer.Stop(); - _output.WriteLine($"CreateContainerOperation finished after {timer.ElapsedMilliseconds} ms"); - - Assert.True(createContainerTask.IsCanceled); - Assert.True(createContainerTask.IsCompleted); - } - [Fact] public async Task GetContainerLogs_Tty_False_Follow_True_TaskIsCompleted() { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, - _cts.Token + _testFixture.Cts.Token ); - await _dockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - var containerLogsTask = _dockerClient.Containers.GetContainerLogsAsync( + var containerLogsTask = _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -107,13 +61,13 @@ await _dockerClient.Containers.StartContainerAsync( Follow = true }, containerLogsCts.Token, - new Progress<string>(m => _output.WriteLine(m)) + new Progress<string>(m => _testOutputHelper.WriteLine(m)) ); - await _dockerClient.Containers.StopContainerAsync( + await _testFixture.DockerClient.Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), - _cts.Token + _testFixture.Cts.Token ); await containerLogsTask; @@ -125,25 +79,25 @@ public async Task GetContainerLogs_Tty_False_Follow_False_ReadsLogs() { var logList = new List<string>(); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, - _cts.Token + _testFixture.Cts.Token ); - await _dockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); await Task.Delay(TimeSpan.FromSeconds(5)); - await _dockerClient.Containers.GetContainerLogsAsync( + await _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -152,17 +106,17 @@ await _dockerClient.Containers.GetContainerLogsAsync( Timestamps = true, Follow = false }, - default, - new Progress<string>(m => { logList.Add(m); _output.WriteLine(m); }) + _testFixture.Cts.Token, + new Progress<string>(m => { _testOutputHelper.WriteLine(m); logList.Add(m); }) ); - await _dockerClient.Containers.StopContainerAsync( + await _testFixture.DockerClient.Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), - _cts.Token + _testFixture.Cts.Token ); - _output.WriteLine($"Line count: {logList.Count}"); + _testOutputHelper.WriteLine($"Line count: {logList.Count}"); Assert.NotEmpty(logList); } @@ -172,25 +126,25 @@ public async Task GetContainerLogs_Tty_True_Follow_False_ReadsLogs() { var logList = new List<string>(); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, - _cts.Token + _testFixture.Cts.Token ); - await _dockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); await Task.Delay(TimeSpan.FromSeconds(5)); - await _dockerClient.Containers.GetContainerLogsAsync( + await _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -199,17 +153,17 @@ await _dockerClient.Containers.GetContainerLogsAsync( Timestamps = true, Follow = false }, - default, - new Progress<string>(m => { _output.WriteLine(m); logList.Add(m); }) + _testFixture.Cts.Token, + new Progress<string>(m => { _testOutputHelper.WriteLine(m); logList.Add(m); }) ); - await _dockerClient.Containers.StopContainerAsync( + await _testFixture.DockerClient.Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), - _cts.Token + _testFixture.Cts.Token ); - _output.WriteLine($"Line count: {logList.Count}"); + _testOutputHelper.WriteLine($"Line count: {logList.Count}"); Assert.NotEmpty(logList); } @@ -219,25 +173,25 @@ public async Task GetContainerLogs_Tty_False_Follow_True_Requires_Task_To_Be_Can { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, - _cts.Token + _testFixture.Cts.Token ); - await _dockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - await Assert.ThrowsAsync<TaskCanceledException>(() => _dockerClient.Containers.GetContainerLogsAsync( + await Assert.ThrowsAsync<TaskCanceledException>(() => _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -247,7 +201,7 @@ await Assert.ThrowsAsync<TaskCanceledException>(() => _dockerClient.Containers.G Follow = true }, containerLogsCts.Token, - new Progress<string>(m => _output.WriteLine(m)) + new Progress<string>(m => _testOutputHelper.WriteLine(m)) )); } @@ -256,25 +210,25 @@ public async Task GetContainerLogs_Tty_True_Follow_True_Requires_Task_To_Be_Canc { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, - _cts.Token + _testFixture.Cts.Token ); - await _dockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - var containerLogsTask = _dockerClient.Containers.GetContainerLogsAsync( + var containerLogsTask = _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -284,7 +238,7 @@ await _dockerClient.Containers.StartContainerAsync( Follow = true }, containerLogsCts.Token, - new Progress<string>(m => _output.WriteLine(m)) + new Progress<string>(m => _testOutputHelper.WriteLine(m)) ); await Assert.ThrowsAsync<TaskCanceledException>(() => containerLogsTask); @@ -296,25 +250,25 @@ public async Task GetContainerLogs_Tty_True_Follow_True_ReadsLogs_TaskIsCancelle using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var logList = new List<string>(); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, - _cts.Token + _testFixture.Cts.Token ); - await _dockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - var containerLogsTask = _dockerClient.Containers.GetContainerLogsAsync( + var containerLogsTask = _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -324,20 +278,19 @@ await _dockerClient.Containers.StartContainerAsync( Follow = true }, containerLogsCts.Token, - new Progress<string>(m => { _output.WriteLine(m); logList.Add(m); }) + new Progress<string>(m => { _testOutputHelper.WriteLine(m); logList.Add(m); }) ); await Task.Delay(TimeSpan.FromSeconds(5)); - await _dockerClient.Containers.StopContainerAsync( + await _testFixture.DockerClient.Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), - _cts.Token + _testFixture.Cts.Token ); - await Assert.ThrowsAsync<TaskCanceledException>(() => containerLogsTask); - _output.WriteLine($"Line count: {logList.Count}"); + _testOutputHelper.WriteLine($"Line count: {logList.Count}"); Assert.NotEmpty(logList); } @@ -345,34 +298,34 @@ await _dockerClient.Containers.StopContainerAsync( [Fact] public async Task GetContainerStatsAsync_Tty_False_Stream_False_ReadsStats() { - using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); + using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); var containerStatsList = new List<ContainerStatsResponse>(); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, - _cts.Token + _testFixture.Cts.Token ); - _ = await _dockerClient.Containers.StartContainerAsync( + _ = await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); tcs.CancelAfter(TimeSpan.FromSeconds(10)); - await _dockerClient.Containers.GetContainerStatsAsync( + await _testFixture.DockerClient.Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { Stream = false }, - new Progress<ContainerStatsResponse>(m => { _output.WriteLine(m.ID); containerStatsList.Add(m); }), + new Progress<ContainerStatsResponse>(m => { _testOutputHelper.WriteLine(m.ID); containerStatsList.Add(m); }), tcs.Token ); @@ -380,31 +333,33 @@ await _dockerClient.Containers.GetContainerStatsAsync( Assert.NotEmpty(containerStatsList); Assert.Single(containerStatsList); - _output.WriteLine($"ConntainerStats count: {containerStatsList.Count}"); + _testOutputHelper.WriteLine($"ConntainerStats count: {containerStatsList.Count}"); } [Fact] public async Task GetContainerStatsAsync_Tty_False_StreamStats() { - using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); + using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); using (tcs.Token.Register(() => throw new TimeoutException("GetContainerStatsAsync_Tty_False_StreamStats"))) { - _output.WriteLine($"Running test {MethodBase.GetCurrentMethod().Module}->{MethodBase.GetCurrentMethod().Name}"); + var method = MethodBase.GetCurrentMethod(); + + _testOutputHelper.WriteLine($"Running test '{method!.Module}' -> '{method!.Name}'"); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, - _cts.Token + _testFixture.Cts.Token ); - _ = await _dockerClient.Containers.StartContainerAsync( + _ = await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); List<ContainerStatsResponse> containerStatsList = new List<ContainerStatsResponse>(); @@ -413,13 +368,13 @@ public async Task GetContainerStatsAsync_Tty_False_StreamStats() linkedCts.CancelAfter(TimeSpan.FromSeconds(5)); try { - await _dockerClient.Containers.GetContainerStatsAsync( + await _testFixture.DockerClient.Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { Stream = true }, - new Progress<ContainerStatsResponse>(m => { containerStatsList.Add(m); _output.WriteLine(JsonSerializer.Instance.Serialize(m)); }), + new Progress<ContainerStatsResponse>(m => { containerStatsList.Add(m); _testOutputHelper.WriteLine(JsonSerializer.Instance.Serialize(m)); }), linkedCts.Token ); } @@ -428,7 +383,7 @@ await _dockerClient.Containers.GetContainerStatsAsync( // this is expected to happen on task cancelaltion } - _output.WriteLine($"Container stats count: {containerStatsList.Count}"); + _testOutputHelper.WriteLine($"Container stats count: {containerStatsList.Count}"); Assert.NotEmpty(containerStatsList); } } @@ -436,34 +391,34 @@ await _dockerClient.Containers.GetContainerStatsAsync( [Fact] public async Task GetContainerStatsAsync_Tty_True_Stream_False_ReadsStats() { - using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); + using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); var containerStatsList = new List<ContainerStatsResponse>(); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, - _cts.Token + _testFixture.Cts.Token ); - _ = await _dockerClient.Containers.StartContainerAsync( + _ = await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); tcs.CancelAfter(TimeSpan.FromSeconds(10)); - await _dockerClient.Containers.GetContainerStatsAsync( + await _testFixture.DockerClient.Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { Stream = false }, - new Progress<ContainerStatsResponse>(m => { _output.WriteLine(m.ID); containerStatsList.Add(m); }), + new Progress<ContainerStatsResponse>(m => { _testOutputHelper.WriteLine(m.ID); containerStatsList.Add(m); }), tcs.Token ); @@ -471,32 +426,32 @@ await _dockerClient.Containers.GetContainerStatsAsync( Assert.NotEmpty(containerStatsList); Assert.Single(containerStatsList); - _output.WriteLine($"ConntainerStats count: {containerStatsList.Count}"); + _testOutputHelper.WriteLine($"ConntainerStats count: {containerStatsList.Count}"); } [Fact] public async Task GetContainerStatsAsync_Tty_True_StreamStats() { - using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); + using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); using (tcs.Token.Register(() => throw new TimeoutException("GetContainerStatsAsync_Tty_True_StreamStats"))) { - _output.WriteLine("Running test GetContainerStatsAsync_Tty_True_StreamStats"); + _testOutputHelper.WriteLine("Running test GetContainerStatsAsync_Tty_True_StreamStats"); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, - _cts.Token + _testFixture.Cts.Token ); - _ = await _dockerClient.Containers.StartContainerAsync( + _ = await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); List<ContainerStatsResponse> containerStatsList = new List<ContainerStatsResponse>(); @@ -506,23 +461,23 @@ public async Task GetContainerStatsAsync_Tty_True_StreamStats() try { - await _dockerClient.Containers.GetContainerStatsAsync( + await _testFixture.DockerClient.Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { Stream = true }, - new Progress<ContainerStatsResponse>(m => { containerStatsList.Add(m); _output.WriteLine(JsonSerializer.Instance.Serialize(m)); }), + new Progress<ContainerStatsResponse>(m => { containerStatsList.Add(m); _testOutputHelper.WriteLine(JsonSerializer.Instance.Serialize(m)); }), linkedTcs.Token ); } catch (OperationCanceledException) { - // this is expected to happen on task cancelaltion + // This is expected to happen on task cancellation. } await Task.Delay(TimeSpan.FromSeconds(1)); - _output.WriteLine($"Container stats count: {containerStatsList.Count}"); + _testOutputHelper.WriteLine($"Container stats count: {containerStatsList.Count}"); Assert.NotEmpty(containerStatsList); } } @@ -530,63 +485,65 @@ await _dockerClient.Containers.GetContainerStatsAsync( [Fact] public async Task KillContainerAsync_ContainerRunning_Succeeds() { - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _imageId + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr }, - _cts.Token); + _testFixture.Cts.Token); - await _dockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); - var inspectRunningContainerResponse = await _dockerClient.Containers.InspectContainerAsync( + var inspectRunningContainerResponse = await _testFixture.DockerClient.Containers.InspectContainerAsync( createContainerResponse.ID, - _cts.Token); + _testFixture.Cts.Token); - await _dockerClient.Containers.KillContainerAsync( + await _testFixture.DockerClient.Containers.KillContainerAsync( createContainerResponse.ID, new ContainerKillParameters(), - _cts.Token); + _testFixture.Cts.Token); - var inspectKilledContainerResponse = await _dockerClient.Containers.InspectContainerAsync( + var inspectKilledContainerResponse = await _testFixture.DockerClient.Containers.InspectContainerAsync( createContainerResponse.ID, - _cts.Token); + _testFixture.Cts.Token); Assert.True(inspectRunningContainerResponse.State.Running); Assert.False(inspectKilledContainerResponse.State.Running); Assert.Equal("exited", inspectKilledContainerResponse.State.Status); - _output.WriteLine("Killed"); - _output.WriteLine(JsonSerializer.Instance.Serialize(inspectKilledContainerResponse)); + _testOutputHelper.WriteLine("Killed"); + _testOutputHelper.WriteLine(JsonSerializer.Instance.Serialize(inspectKilledContainerResponse)); } [Fact] public async Task ListContainersAsync_ContainerExists_Succeeds() { - await _dockerClient.Containers.CreateContainerAsync(new CreateContainerParameters() + await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString() + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, }, - _cts.Token); + _testFixture.Cts.Token); - IList<ContainerListResponse> containerList = await _dockerClient.Containers.ListContainersAsync( + IList<ContainerListResponse> containerList = await _testFixture.DockerClient.Containers.ListContainersAsync( new ContainersListParameters { Filters = new Dictionary<string, IDictionary<string, bool>> { ["ancestor"] = new Dictionary<string, bool> { - [_imageId] = true + [_testFixture.Image.ID] = true } }, All = true }, - _cts.Token + _testFixture.Cts.Token ); Assert.NotNull(containerList); @@ -596,32 +553,32 @@ await _dockerClient.Containers.CreateContainerAsync(new CreateContainerParameter [Fact] public async Task ListProcessesAsync_RunningContainer_Succeeds() { - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString() + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr }, - _cts.Token + _testFixture.Cts.Token ); - await _dockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); - var containerProcessesResponse = await _dockerClient.Containers.ListProcessesAsync( + var containerProcessesResponse = await _testFixture.DockerClient.Containers.ListProcessesAsync( createContainerResponse.ID, new ContainerListProcessesParameters(), - _cts.Token + _testFixture.Cts.Token ); - _output.WriteLine($"Title '{containerProcessesResponse.Titles[0]}' - '{containerProcessesResponse.Titles[1]}' - '{containerProcessesResponse.Titles[2]}' - '{containerProcessesResponse.Titles[3]}'"); + _testOutputHelper.WriteLine($"Title '{containerProcessesResponse.Titles[0]}' - '{containerProcessesResponse.Titles[1]}' - '{containerProcessesResponse.Titles[2]}' - '{containerProcessesResponse.Titles[3]}'"); foreach (var processes in containerProcessesResponse.Processes) { - _output.WriteLine($"Process '{processes[0]}' - ''{processes[1]}' - '{processes[2]}' - '{processes[3]}'"); + _testOutputHelper.WriteLine($"Process '{processes[0]}' - ''{processes[1]}' - '{processes[2]}' - '{processes[3]}'"); } Assert.NotNull(containerProcessesResponse); @@ -631,32 +588,32 @@ await _dockerClient.Containers.StartContainerAsync( [Fact] public async Task RemoveContainerAsync_ContainerExists_Succeedes() { - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString() + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, }, - _cts.Token + _testFixture.Cts.Token ); - ContainerInspectResponse inspectCreatedContainer = await _dockerClient.Containers.InspectContainerAsync( + ContainerInspectResponse inspectCreatedContainer = await _testFixture.DockerClient.Containers.InspectContainerAsync( createContainerResponse.ID, - _cts.Token + _testFixture.Cts.Token ); - await _dockerClient.Containers.RemoveContainerAsync( + await _testFixture.DockerClient.Containers.RemoveContainerAsync( createContainerResponse.ID, new ContainerRemoveParameters { Force = true }, - _cts.Token + _testFixture.Cts.Token ); - Task inspectRemovedContainerTask = _dockerClient.Containers.InspectContainerAsync( + Task inspectRemovedContainerTask = _testFixture.DockerClient.Containers.InspectContainerAsync( createContainerResponse.ID, - _cts.Token + _testFixture.Cts.Token ); Assert.NotNull(inspectCreatedContainer.State); @@ -666,19 +623,19 @@ await _dockerClient.Containers.RemoveContainerAsync( [Fact] public async Task StartContainerAsync_ContainerExists_Succeeds() { - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters() + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString() + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, }, - _cts.Token + _testFixture.Cts.Token ); - var startContainerResult = await _dockerClient.Containers.StartContainerAsync( + var startContainerResult = await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); Assert.True(startContainerResult); @@ -687,10 +644,10 @@ public async Task StartContainerAsync_ContainerExists_Succeeds() [Fact] public async Task StartContainerAsync_ContainerNotExists_ThrowsException() { - Task startContainerTask = _dockerClient.Containers.StartContainerAsync( + Task startContainerTask = _testFixture.DockerClient.Containers.StartContainerAsync( Guid.NewGuid().ToString(), new ContainerStartParameters(), - _cts.Token + _testFixture.Cts.Token ); await Assert.ThrowsAsync<DockerContainerNotFoundException>(() => startContainerTask); @@ -703,20 +660,20 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio using var waitContainerCts = new CancellationTokenSource(delay: TimeSpan.FromMinutes(5)); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _imageId, - Name = Guid.NewGuid().ToString(), + Image = _testFixture.Image.ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr }, waitContainerCts.Token ); - _output.WriteLine($"CreateContainerResponse: '{JsonSerializer.Instance.Serialize(createContainerResponse)}'"); + _testOutputHelper.WriteLine($"CreateContainerResponse: '{JsonSerializer.Instance.Serialize(createContainerResponse)}'"); - _ = await _dockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters(), waitContainerCts.Token); + _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters(), waitContainerCts.Token); - _output.WriteLine("Starting timeout to cancel WaitContainer operation."); + _testOutputHelper.WriteLine("Starting timeout to cancel WaitContainer operation."); TimeSpan delay = TimeSpan.FromSeconds(5); @@ -724,14 +681,14 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio stopWatch.Start(); // Will wait forever here if cancelation fails. - var waitContainerTask = _dockerClient.Containers.WaitContainerAsync(createContainerResponse.ID, waitContainerCts.Token); + var waitContainerTask = _testFixture.DockerClient.Containers.WaitContainerAsync(createContainerResponse.ID, waitContainerCts.Token); _ = await Assert.ThrowsAsync<TaskCanceledException>(() => waitContainerTask); stopWatch.Stop(); - _output.WriteLine($"WaitContainerTask was cancelled after {stopWatch.ElapsedMilliseconds} ms"); - _output.WriteLine($"WaitContainerAsync: {stopWatch.Elapsed} elapsed"); + _testOutputHelper.WriteLine($"WaitContainerTask was cancelled after {stopWatch.ElapsedMilliseconds} ms"); + _testOutputHelper.WriteLine($"WaitContainerAsync: {stopWatch.Elapsed} elapsed"); // Task should be cancelled when CancelAfter timespan expires TimeSpan tolerance = TimeSpan.FromMilliseconds(500); @@ -741,46 +698,41 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio } [Fact] - public async Task CreateImageAsync_NonexistantImage_ThrowsDockerImageNotFoundException() + public async Task CreateImageAsync_NonExistingImage_ThrowsDockerImageNotFoundException() { - var parameters = new CreateContainerParameters - { - Image = "no-such-image-ytfghbkufhresdhtrjygvb", - }; - Func<Task> op = async () => await _dockerClient.Containers.CreateContainerAsync(parameters); + var createContainerParameters = new CreateContainerParameters(); + createContainerParameters.Image = Guid.NewGuid().ToString("D"); + + Func<Task> op = () => _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); await Assert.ThrowsAsync<DockerImageNotFoundException>(op); } - [Fact] + [Fact(Skip = "Refactor IExecOperations operations and writing/reading to/from stdin and stdout. It does not work reliably.")] public async Task MultiplexedStreamWriteAsync_DoesNotThrowAnException() { // Given - Exception exception; + var createContainerParameters = new CreateContainerParameters(); + createContainerParameters.Image = _testFixture.Image.ID; + createContainerParameters.Entrypoint = CommonCommands.SleepInfinity; - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync( - new CreateContainerParameters - { - Image = _imageId - }); + var containerExecCreateParameters = new ContainerExecCreateParameters(); + containerExecCreateParameters.AttachStdout = true; + containerExecCreateParameters.AttachStderr = true; + containerExecCreateParameters.AttachStdin = true; + containerExecCreateParameters.Cmd = new[] { "/bin/sh", "-c", "read line; echo Done" }; - _ = await _dockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); + var containerExecStartParameters = new ContainerExecStartParameters(); - var containerExecCreateResponse = await _dockerClient.Exec.ExecCreateContainerAsync(createContainerResponse.ID, - new ContainerExecCreateParameters - { - AttachStdout = true, - AttachStderr = true, - AttachStdin = true, - Cmd = new [] { string.Empty } - }); + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); + _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); // When - using (var stream = await _dockerClient.Exec.StartAndAttachContainerExecAsync(containerExecCreateResponse.ID, false)) - { - var buffer = new byte[] { 10 }; - exception = await Record.ExceptionAsync(() => stream.WriteAsync(buffer, 0, buffer.Length, default)); - } + var containerExecCreateResponse = await _testFixture.DockerClient.Exec.ExecCreateContainerAsync(createContainerResponse.ID, containerExecCreateParameters); + using var stream = await _testFixture.DockerClient.Exec.StartWithConfigContainerExecAsync(containerExecCreateResponse.ID, containerExecStartParameters); + + var buffer = new byte[] { 10 }; + var exception = await Record.ExceptionAsync(() => stream.WriteAsync(buffer, 0, buffer.Length, _testFixture.Cts.Token)); // Then Assert.Null(exception); diff --git a/test/Docker.DotNet.Tests/IImageOperationsTests.cs b/test/Docker.DotNet.Tests/IImageOperationsTests.cs index e74c80b5..50d522f7 100644 --- a/test/Docker.DotNet.Tests/IImageOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IImageOperationsTests.cs @@ -3,38 +3,25 @@ namespace Docker.DotNet.Tests; [Collection(nameof(TestCollection))] public class IImageOperationsTests { - private readonly CancellationTokenSource _cts; + private readonly TestFixture _testFixture; + private readonly ITestOutputHelper _testOutputHelper; - private readonly TestOutput _output; - private readonly string _repositoryName; - private readonly string _tag; - private readonly DockerClient _dockerClient; - - public IImageOperationsTests(TestFixture testFixture, ITestOutputHelper outputHelper) + public IImageOperationsTests(TestFixture testFixture, ITestOutputHelper testOutputHelper) { - _output = new TestOutput(outputHelper); - - _dockerClient = testFixture.DockerClient; - - // Do not wait forever in case it gets stuck - _cts = CancellationTokenSource.CreateLinkedTokenSource(testFixture.Cts.Token); - _cts.CancelAfter(TimeSpan.FromMinutes(5)); - _cts.Token.Register(() => throw new TimeoutException("ImageOperationTests timeout")); - - _repositoryName = testFixture.Repository; - _tag = testFixture.Tag; + _testFixture = testFixture; + _testOutputHelper = testOutputHelper; } [Fact] - public async Task CreateImageAsync_TaskCancelled_ThowsTaskCanceledException() + public async Task CreateImageAsync_TaskCancelled_ThrowsTaskCanceledException() { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); var newTag = Guid.NewGuid().ToString(); var newRepositoryName = Guid.NewGuid().ToString(); - await _dockerClient.Images.TagImageAsync( - $"{_repositoryName}:{_tag}", + await _testFixture.DockerClient.Images.TagImageAsync( + $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = newRepositoryName, @@ -43,13 +30,13 @@ await _dockerClient.Images.TagImageAsync( cts.Token ); - var createImageTask = _dockerClient.Images.CreateImageAsync( + var createImageTask = _testFixture.DockerClient.Images.CreateImageAsync( new ImagesCreateParameters { FromImage = $"{newRepositoryName}:{newTag}" }, null, - new Progress<JSONMessage>((message) => _output.WriteLine(JsonSerializer.Instance.Serialize(message))), + new Progress<JSONMessage>(message => _testOutputHelper.WriteLine(JsonSerializer.Instance.Serialize(message))), cts.Token); TimeSpan delay = TimeSpan.FromMilliseconds(5); @@ -63,8 +50,8 @@ await _dockerClient.Images.TagImageAsync( [Fact] public Task CreateImageAsync_ErrorResponse_ThrowsDockerApiException() { - return Assert.ThrowsAsync<DockerApiException>(() => _dockerClient.Images.CreateImageAsync( - new ImagesCreateParameters() + return Assert.ThrowsAsync<DockerApiException>(() => _testFixture.DockerClient.Images.CreateImageAsync( + new ImagesCreateParameters { FromImage = "1.2.3.Apparently&this$is+not-a_valid%repository//name", Tag = "ancient-one" @@ -76,30 +63,30 @@ public async Task DeleteImageAsync_RemovesImage() { var newImageTag = Guid.NewGuid().ToString(); - await _dockerClient.Images.TagImageAsync( - $"{_repositoryName}:{_tag}", + await _testFixture.DockerClient.Images.TagImageAsync( + $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { - RepositoryName = _repositoryName, + RepositoryName = _testFixture.Repository, Tag = newImageTag }, - _cts.Token + _testFixture.Cts.Token ); - var inspectExistingImageResponse = await _dockerClient.Images.InspectImageAsync( - $"{_repositoryName}:{newImageTag}", - _cts.Token + var inspectExistingImageResponse = await _testFixture.DockerClient.Images.InspectImageAsync( + $"{_testFixture.Repository}:{newImageTag}", + _testFixture.Cts.Token ); - await _dockerClient.Images.DeleteImageAsync( - $"{_repositoryName}:{newImageTag}", + await _testFixture.DockerClient.Images.DeleteImageAsync( + $"{_testFixture.Repository}:{newImageTag}", new ImageDeleteParameters(), - _cts.Token + _testFixture.Cts.Token ); - Task inspectDeletedImageTask = _dockerClient.Images.InspectImageAsync( - $"{_repositoryName}:{newImageTag}", - _cts.Token + Task inspectDeletedImageTask = _testFixture.DockerClient.Images.InspectImageAsync( + $"{_testFixture.Repository}:{newImageTag}", + _testFixture.Cts.Token ); Assert.NotNull(inspectExistingImageResponse); diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs index ce25fb04..e620446c 100644 --- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs +++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs @@ -3,20 +3,13 @@ namespace Docker.DotNet.Tests; [Collection(nameof(TestCollection))] public class ISwarmOperationsTests { - private readonly CancellationTokenSource _cts; + private readonly TestFixture _testFixture; + private readonly ITestOutputHelper _testOutputHelper; - private readonly DockerClient _dockerClient; - private readonly string _imageId; - - public ISwarmOperationsTests(TestFixture testFixture) + public ISwarmOperationsTests(TestFixture testFixture, ITestOutputHelper testOutputHelper) { - // Do not wait forever in case it gets stuck - _cts = CancellationTokenSource.CreateLinkedTokenSource(testFixture.Cts.Token); - _cts.CancelAfter(TimeSpan.FromMinutes(5)); - _cts.Token.Register(() => throw new TimeoutException("SwarmOperationTests timeout")); - - _dockerClient = testFixture.DockerClient; - _imageId = testFixture.Image.ID; + _testFixture = testFixture; + _testOutputHelper = testOutputHelper; } [Fact] @@ -24,34 +17,34 @@ public async Task GetFilteredServicesByName_Succeeds() { var serviceName = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}"; - var firstServiceId = (await _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var firstServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = serviceName, - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var secondServiceId = (await _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var thirdServiceId = (await _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var services = await _dockerClient.Swarm.ListServicesAsync(new ServiceListParameters + var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(new ServiceListParameters { Filters = new Dictionary<string, IDictionary<string, bool>> { @@ -64,42 +57,42 @@ public async Task GetFilteredServicesByName_Succeeds() Assert.Single(services); - await _dockerClient.Swarm.RemoveServiceAsync(firstServiceId); - await _dockerClient.Swarm.RemoveServiceAsync(secondServiceId); - await _dockerClient.Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); } [Fact] public async Task GetFilteredServicesById_Succeeds() { - var firstServiceId = (await _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var firstServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var secondServiceId = (await _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var thirdServiceId = (await _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var services = await _dockerClient.Swarm.ListServicesAsync(new ServiceListParameters + var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(new ServiceListParameters { Filters = new Dictionary<string, IDictionary<string, bool>> { @@ -112,69 +105,69 @@ public async Task GetFilteredServicesById_Succeeds() Assert.Single(services); - await _dockerClient.Swarm.RemoveServiceAsync(firstServiceId); - await _dockerClient.Swarm.RemoveServiceAsync(secondServiceId); - await _dockerClient.Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); } [Fact] public async Task GetServices_Succeeds() { - var initialServiceCount = (await _dockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None)).Count(); + var initialServiceCount = (await _testFixture.DockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None)).Count(); - var firstServiceId = (await _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var firstServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var secondServiceId = (await _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var thirdServiceId = (await _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var services = await _dockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None); + var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None); Assert.True(services.Count() > initialServiceCount); - await _dockerClient.Swarm.RemoveServiceAsync(firstServiceId); - await _dockerClient.Swarm.RemoveServiceAsync(secondServiceId); - await _dockerClient.Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); } [Fact] public async Task GetServiceLogs_Succeeds() { var cts = new CancellationTokenSource(); - var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, cts.Token); + var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token, cts.Token); var serviceName = $"service-withLogs-{Guid.NewGuid().ToString().Substring(1, 10)}"; - var serviceId = (await _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var serviceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = serviceName, - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID, Command = CommonCommands.EchoToStdoutAndStderr } } } })).ID; - var stream = await _dockerClient.Swarm.GetServiceLogsAsync(serviceName, false, new ServiceLogsParameters + using var stream = await _testFixture.DockerClient.Swarm.GetServiceLogsAsync(serviceName, false, new ServiceLogsParameters { Follow = true, ShowStdout = true, @@ -231,12 +224,12 @@ public async Task GetServiceLogs_Succeeds() // Reset the CancellationTokenSource for the next attempt cts = new CancellationTokenSource(); - linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, cts.Token); + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token, cts.Token); cts.CancelAfter(delay); } } - if (logLines.Any() && logLines.First().Contains("[INF]")) + if (logLines.Any()) { break; } @@ -252,8 +245,7 @@ public async Task GetServiceLogs_Succeeds() Assert.NotNull(logLines); Assert.NotEmpty(logLines); - Assert.Contains("[INF]", logLines.First()); - await _dockerClient.Swarm.RemoveServiceAsync(serviceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(serviceId); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs index 5e1f814b..2857647d 100644 --- a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs +++ b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs @@ -3,46 +3,33 @@ namespace Docker.DotNet.Tests; [Collection(nameof(TestCollection))] public class ISystemOperationsTests { - private readonly CancellationTokenSource _cts; + private readonly TestFixture _testFixture; + private readonly ITestOutputHelper _testOutputHelper; - private readonly TestOutput _output; - private readonly string _repositoryName; - private readonly string _tag; - private readonly DockerClient _dockerClient; - - public ISystemOperationsTests(TestFixture testFixture, ITestOutputHelper outputHelper) + public ISystemOperationsTests(TestFixture testFixture, ITestOutputHelper testOutputHelper) { - _output = new TestOutput(outputHelper); - - _dockerClient = testFixture.DockerClient; - - // Do not wait forever in case it gets stuck - _cts = CancellationTokenSource.CreateLinkedTokenSource(testFixture.Cts.Token); - _cts.CancelAfter(TimeSpan.FromMinutes(5)); - _cts.Token.Register(() => throw new TimeoutException("SystemOperationsTests timeout")); - - _repositoryName = testFixture.Repository; - _tag = testFixture.Tag; + _testFixture = testFixture; + _testOutputHelper = testOutputHelper; } [Fact] public void Docker_IsRunning() { var dockerProcess = Process.GetProcesses().FirstOrDefault(process => process.ProcessName.Equals("docker", StringComparison.InvariantCultureIgnoreCase) || process.ProcessName.Equals("dockerd", StringComparison.InvariantCultureIgnoreCase)); - Assert.NotNull(dockerProcess); // docker is not running + Assert.NotNull(dockerProcess); } [Fact] public async Task GetSystemInfoAsync_Succeeds() { - var info = await _dockerClient.System.GetSystemInfoAsync(); + var info = await _testFixture.DockerClient.System.GetSystemInfoAsync(); Assert.NotNull(info.Architecture); } [Fact] public async Task GetVersionAsync_Succeeds() { - var version = await _dockerClient.System.GetVersionAsync(); + var version = await _testFixture.DockerClient.System.GetVersionAsync(); Assert.NotNull(version.APIVersion); } @@ -55,20 +42,20 @@ public async Task MonitorEventsAsync_EmptyContainersList_CanBeCancelled() await cts.CancelAsync(); await Task.Delay(1); - await Assert.ThrowsAsync<TaskCanceledException>(() => _dockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), progress, cts.Token)); + await Assert.ThrowsAsync<TaskCanceledException>(() => _testFixture.DockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), progress, cts.Token)); } [Fact] public async Task MonitorEventsAsync_NullParameters_Throws() { - await Assert.ThrowsAsync<ArgumentNullException>(() => _dockerClient.System.MonitorEventsAsync(null, null)); + await Assert.ThrowsAsync<ArgumentNullException>(() => _testFixture.DockerClient.System.MonitorEventsAsync(null, null)); } [Fact] public async Task MonitorEventsAsync_NullProgress_Throws() { - await Assert.ThrowsAsync<ArgumentNullException>(() => _dockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), null)); + await Assert.ThrowsAsync<ArgumentNullException>(() => _testFixture.DockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), null)); } [Fact] @@ -78,29 +65,29 @@ public async Task MonitorEventsAsync_Succeeds() var wasProgressCalled = false; - var progressMessage = new Progress<Message>((m) => + var progressMessage = new Progress<Message>(m => { - _output.WriteLine($"MonitorEventsAsync_Succeeds: Message - {m.Action} - {m.Status} {m.From} - {m.Type}"); + _testOutputHelper.WriteLine($"MonitorEventsAsync_Succeeds: Message - {m.Action} - {m.Status} {m.From} - {m.Type}"); wasProgressCalled = true; Assert.NotNull(m); }); - using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); - var task = _dockerClient.System.MonitorEventsAsync( + var task = _testFixture.DockerClient.System.MonitorEventsAsync( new ContainerEventsParameters(), progressMessage, cts.Token); - await _dockerClient.Images.TagImageAsync($"{_repositoryName}:{_tag}", new ImageTagParameters { RepositoryName = _repositoryName, Tag = newTag }, _cts.Token); + await _testFixture.DockerClient.Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }, _testFixture.Cts.Token); - await _dockerClient.Images.DeleteImageAsync( - name: $"{_repositoryName}:{newTag}", + await _testFixture.DockerClient.Images.DeleteImageAsync( + name: $"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters { Force = true }, - _cts.Token); + _testFixture.Cts.Token); // Give it some time for output operation to complete before cancelling task await Task.Delay(TimeSpan.FromSeconds(1)); @@ -123,24 +110,24 @@ public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption() try { // (1) Create monitor task - using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); string newImageTag = Guid.NewGuid().ToString(); - var monitorTask = _dockerClient.System.MonitorEventsAsync( + var monitorTask = _testFixture.DockerClient.System.MonitorEventsAsync( new ContainerEventsParameters(), - new Progress<Message>((value) => _output.WriteLine($"DockerSystemEvent: {JsonSerializer.Instance.Serialize(value)}")), + new Progress<Message>(value => _testOutputHelper.WriteLine($"DockerSystemEvent: {JsonSerializer.Instance.Serialize(value)}")), cts.Token); // (2) Wait for some time to make sure we get into blocking IO call await Task.Delay(100, CancellationToken.None); // (3) Invoke another request that will attempt to grab the same buffer - var listImagesTask1 = _dockerClient.Images.TagImageAsync( - $"{_repositoryName}:{_tag}", + var listImagesTask1 = _testFixture.DockerClient.Images.TagImageAsync( + $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { - RepositoryName = _repositoryName, + RepositoryName = _testFixture.Repository, Tag = newImageTag, }, CancellationToken.None); @@ -153,17 +140,17 @@ public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption() // noop } - _output.WriteLine($"Waited for {sw.Elapsed.TotalMilliseconds} ms"); + _testOutputHelper.WriteLine($"Waited for {sw.Elapsed.TotalMilliseconds} ms"); await cts.CancelAsync(); await listImagesTask1; - await _dockerClient.Images.TagImageAsync( - $"{_repositoryName}:{_tag}", + await _testFixture.DockerClient.Images.TagImageAsync( + $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { - RepositoryName = _repositoryName, + RepositoryName = _testFixture.Repository, Tag = newImageTag, }, CancellationToken.None); @@ -180,31 +167,31 @@ await _dockerClient.Images.TagImageAsync( public async Task MonitorEventsFiltered_Succeeds() { string newTag = $"MonitorTests-{Guid.NewGuid().ToString().Substring(1, 10)}"; - string newImageRespositoryName = Guid.NewGuid().ToString(); + string newImageRepositoryName = Guid.NewGuid().ToString(); - await _dockerClient.Images.TagImageAsync( - $"{_repositoryName}:{_tag}", + await _testFixture.DockerClient.Images.TagImageAsync( + $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { - RepositoryName = newImageRespositoryName, + RepositoryName = newImageRepositoryName, Tag = newTag }, - _cts.Token + _testFixture.Cts.Token ); - ImageInspectResponse image = await _dockerClient.Images.InspectImageAsync( - $"{newImageRespositoryName}:{newTag}", - _cts.Token + ImageInspectResponse image = await _testFixture.DockerClient.Images.InspectImageAsync( + $"{newImageRepositoryName}:{newTag}", + _testFixture.Cts.Token ); var progressCalledCounter = 0; - var eventsParams = new ContainerEventsParameters() + var eventsParams = new ContainerEventsParameters { - Filters = new Dictionary<string, IDictionary<string, bool>>() + Filters = new Dictionary<string, IDictionary<string, bool>> { { - "event", new Dictionary<string, bool>() + "event", new Dictionary<string, bool> { { "tag", true @@ -215,7 +202,7 @@ await _dockerClient.Images.TagImageAsync( } }, { - "type", new Dictionary<string, bool>() + "type", new Dictionary<string, bool> { { "image", true @@ -223,7 +210,7 @@ await _dockerClient.Images.TagImageAsync( } }, { - "image", new Dictionary<string, bool>() + "image", new Dictionary<string, bool> { { image.ID, true @@ -233,21 +220,21 @@ await _dockerClient.Images.TagImageAsync( } }; - var progress = new Progress<Message>((m) => + var progress = new Progress<Message>(m => { Interlocked.Increment(ref progressCalledCounter); Assert.True(m.Status == "tag" || m.Status == "untag"); - _output.WriteLine($"MonitorEventsFiltered_Succeeds: Message received: {m.Action} - {m.Status} {m.From} - {m.Type}"); + _testOutputHelper.WriteLine($"MonitorEventsFiltered_Succeeds: Message received: {m.Action} - {m.Status} {m.From} - {m.Type}"); }); - using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); - var task = Task.Run(() => _dockerClient.System.MonitorEventsAsync(eventsParams, progress, cts.Token)); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); + var task = Task.Run(() => _testFixture.DockerClient.System.MonitorEventsAsync(eventsParams, progress, cts.Token)); - await _dockerClient.Images.TagImageAsync($"{_repositoryName}:{_tag}", new ImageTagParameters { RepositoryName = _repositoryName, Tag = newTag }); - await _dockerClient.Images.DeleteImageAsync($"{_repositoryName}:{newTag}", new ImageDeleteParameters()); + await _testFixture.DockerClient.Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }); + await _testFixture.DockerClient.Images.DeleteImageAsync($"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters()); - var createContainerResponse = await _dockerClient.Containers.CreateContainerAsync(new CreateContainerParameters { Image = $"{_repositoryName}:{_tag}" }); - await _dockerClient.Containers.RemoveContainerAsync(createContainerResponse.ID, new ContainerRemoveParameters(), cts.Token); + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(new CreateContainerParameters { Image = $"{_testFixture.Repository}:{_testFixture.Tag}", Entrypoint = CommonCommands.SleepInfinity }); + await _testFixture.DockerClient.Containers.RemoveContainerAsync(createContainerResponse.ID, new ContainerRemoveParameters(), cts.Token); await Task.Delay(TimeSpan.FromSeconds(1)); await cts.CancelAsync(); @@ -261,6 +248,6 @@ await _dockerClient.Images.TagImageAsync( [Fact] public async Task PingAsync_Succeeds() { - await _dockerClient.System.PingAsync(); + await _testFixture.DockerClient.System.PingAsync(); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs b/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs index f1ed7d4c..e77ad7d1 100644 --- a/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs @@ -3,46 +3,39 @@ namespace Docker.DotNet.Tests; [Collection(nameof(TestCollection))] public class IVolumeOperationsTests { - private readonly CancellationTokenSource _cts; - - private readonly DockerClient _dockerClient; - - public IVolumeOperationsTests(TestFixture testFixture) - { - _dockerClient = testFixture.DockerClient; - - // Do not wait forever in case it gets stuck - _cts = CancellationTokenSource.CreateLinkedTokenSource(testFixture.Cts.Token); - _cts.CancelAfter(TimeSpan.FromMinutes(5)); - _cts.Token.Register(() => throw new TimeoutException("VolumeOperationsTests timeout")); - } - - [Fact] - public async Task ListAsync_VolumeExists_Succeeds() - { - const string volumeName = "docker-dotnet-test-volume"; - - await _dockerClient.Volumes.CreateAsync(new VolumesCreateParameters - { - Name = volumeName, - }, - _cts.Token); - - try - { - - var response = await _dockerClient.Volumes.ListAsync(new VolumesListParameters() - { - Filters = new Dictionary<string, IDictionary<string, bool>>(), - }, - _cts.Token); - - Assert.Contains(volumeName, response.Volumes.Select(volume => volume.Name)); - - } - finally - { - await _dockerClient.Volumes.RemoveAsync(volumeName, force: true, _cts.Token); - } - } + private readonly TestFixture _testFixture; + private readonly ITestOutputHelper _testOutputHelper; + + public IVolumeOperationsTests(TestFixture testFixture, ITestOutputHelper testOutputHelper) + { + _testFixture = testFixture; + _testOutputHelper = testOutputHelper; + } + + [Fact] + public async Task ListAsync_VolumeExists_Succeeds() + { + const string volumeName = "docker-dotnet-test-volume"; + + await _testFixture.DockerClient.Volumes.CreateAsync(new VolumesCreateParameters + { + Name = volumeName, + }, + _testFixture.Cts.Token); + + try + { + var response = await _testFixture.DockerClient.Volumes.ListAsync(new VolumesListParameters + { + Filters = new Dictionary<string, IDictionary<string, bool>>(), + }, + _testFixture.Cts.Token); + + Assert.Contains(volumeName, response.Volumes.Select(volume => volume.Name)); + } + finally + { + await _testFixture.DockerClient.Volumes.RemoveAsync(volumeName, force: true, _testFixture.Cts.Token); + } + } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index 9ae79c6b..8ae4634b 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -1,36 +1,30 @@ namespace Docker.DotNet.Tests; -public sealed class TestFixture : IAsyncLifetime, IDisposable +[CollectionDefinition(nameof(TestCollection))] +public sealed class TestCollection : ICollectionFixture<TestFixture>; + +public sealed class TestFixture : Progress<JSONMessage>, IAsyncLifetime, IDisposable, ILogger { - /// <summary> - /// The Docker image name. - /// </summary> - private const string Name = "nats"; + private const LogLevel MinLogLevel = LogLevel.Debug; - private static readonly Progress<JSONMessage> WriteProgressOutput; + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); - private bool _hasInitializedSwarm; + private readonly IMessageSink _messageSink; - static TestFixture() - { - WriteProgressOutput = new Progress<JSONMessage>(jsonMessage => - { - var message = JsonSerializer.Instance.Serialize(jsonMessage); - Console.WriteLine(message); - Debug.WriteLine(message); - }); - } + private bool _hasInitializedSwarm; /// <summary> /// Initializes a new instance of the <see cref="TestFixture" /> class. /// </summary> - /// <exception cref="TimeoutException">Thrown when tests are not finished within 5 minutes.</exception> - public TestFixture() + /// <param name="messageSink">The message sink.</param> + /// <exception cref="TimeoutException">Thrown when tests are not completed within 5 minutes.</exception> + public TestFixture(IMessageSink messageSink) { + _messageSink = messageSink; DockerClientConfiguration = new DockerClientConfiguration(); - DockerClient = DockerClientConfiguration.CreateClient(); + DockerClient = DockerClientConfiguration.CreateClient(logger: this); Cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); - Cts.Token.Register(() => throw new TimeoutException("Docker.DotNet test timeout exception")); + Cts.Token.Register(() => throw new TimeoutException("Docker.DotNet tests timed out.")); } /// <summary> @@ -46,14 +40,14 @@ public TestFixture() = Guid.NewGuid().ToString("N"); /// <summary> - /// Gets the Docker client. + /// Gets the Docker client configuration. /// </summary> - public DockerClient DockerClient { get; } + public DockerClientConfiguration DockerClientConfiguration { get; } /// <summary> - /// Gets the Docker client configuration. + /// Gets the Docker client. /// </summary> - public DockerClientConfiguration DockerClientConfiguration { get; } + public DockerClient DockerClient { get; } /// <summary> /// Gets the cancellation token source. @@ -68,8 +62,12 @@ public TestFixture() /// <inheritdoc /> public async Task InitializeAsync() { + const string repository = "alpine"; + + const string tag = "3.20"; + // Create image - await DockerClient.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = Name, Tag = "latest" }, null, WriteProgressOutput, Cts.Token) + await DockerClient.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = repository, Tag = tag }, null, this, Cts.Token) .ConfigureAwait(false); // Get images @@ -80,7 +78,7 @@ await DockerClient.Images.CreateImageAsync(new ImagesCreateParameters { FromImag { ["reference"] = new Dictionary<string, bool> { - [Name] = true + [repository + ":" + tag] = true } } }, Cts.Token) @@ -103,9 +101,7 @@ await DockerClient.Images.CreateImageAsync(new ImagesCreateParameters { FromImag } catch { - const string message = "Couldn't init a new swarm, the node should take part of an existing one."; - Console.WriteLine(message); - Debug.WriteLine(message); + this.LogDebug("Couldn't init a new swarm, the node should take part of an existing one."); _hasInitializedSwarm = false; } @@ -141,7 +137,7 @@ await DockerClient.Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true { ["reference"] = new Dictionary<string, bool> { - [Image.RepoDigests.Single()] = true + [Image.ID] = true } }, All = true @@ -164,13 +160,44 @@ await DockerClient.Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true /// <inheritdoc /> public void Dispose() { + Cts.Dispose(); DockerClient.Dispose(); DockerClientConfiguration.Dispose(); - Cts.Dispose(); } -} -[CollectionDefinition(nameof(TestCollection))] -public sealed class TestCollection : ICollectionFixture<TestFixture> -{ + /// <inheritdoc /> + public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) + { + if (IsEnabled(logLevel)) + { + var message = exception == null ? formatter.Invoke(state, null) : string.Join(Environment.NewLine, formatter.Invoke(state, exception), exception); + _messageSink.OnMessage(new DiagnosticMessage(string.Format("[Docker.DotNet {0:hh\\:mm\\:ss\\.ff}] {1}", _stopwatch.Elapsed, message))); + } + } + + /// <inheritdoc /> + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= MinLogLevel; + } + + /// <inheritdoc /> + public IDisposable BeginScope<TState>(TState state) where TState : notnull + { + return new Disposable(); + } + + /// <inheritdoc /> + protected override void OnReport(JSONMessage value) + { + var message = JsonSerializer.Instance.Serialize(value); + this.LogDebug("Progress: '{Progress}'.", message); + } + + private sealed class Disposable : IDisposable + { + public void Dispose() + { + } + } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/TestOutput.cs b/test/Docker.DotNet.Tests/TestOutput.cs deleted file mode 100644 index 57e3f244..00000000 --- a/test/Docker.DotNet.Tests/TestOutput.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Docker.DotNet.Tests; - -public class TestOutput -{ - private readonly ITestOutputHelper _outputHelper; - - public TestOutput(ITestOutputHelper outputHelper) - { - _outputHelper = outputHelper; - } - - public void WriteLine(string line) - { - Console.WriteLine(line); - _outputHelper.WriteLine(line); - System.Diagnostics.Debug.WriteLine(line); - } -} \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/xunit.runner.json b/test/Docker.DotNet.Tests/xunit.runner.json new file mode 100644 index 00000000..0e2fef59 --- /dev/null +++ b/test/Docker.DotNet.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "diagnosticMessages" : true +} \ No newline at end of file