From 1c3a06783daf07133011e8bb899d2fb09d67d0fb Mon Sep 17 00:00:00 2001 From: ACoderLife <37108462+ACoderLife@users.noreply.github.com> Date: Fri, 14 Apr 2023 04:21:46 +1000 Subject: [PATCH] Add client support for Swarm Logs and Swarm Config (#589) * expose Swam Config as client.Configs * Expose Swam Service Logs * send SwarmConfigSpec to api * Update src/Docker.DotNet/Endpoints/ISwarmOperations.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> * Update src/Docker.DotNet/Endpoints/ISwarmOperations.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> * rename to standard add logging test * add logging test fix naming convention * Update src/Docker.DotNet/Endpoints/IConfigsOperations.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> * Update src/Docker.DotNet/Endpoints/IConfigsOperations.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> * Update src/Docker.DotNet/Endpoints/ConfigsOperations.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> * Add Config Tests, some Renaming fixes * updated test with retry and increased buffer. Improved cancelation handling. --------- Co-authored-by: Jasim Schluter Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- .gitattributes | 2 - src/Docker.DotNet/DockerClient.cs | 3 + .../Endpoints/ConfigsOperations.cs | 58 +++ .../Endpoints/IConfigsOperations.cs | 53 ++ .../Endpoints/ISwarmOperations.cs | 37 ++ .../Endpoints/SwarmOperations.cs | 490 ++++++++++-------- src/Docker.DotNet/IDockerClient.cs | 2 + .../IConfigOperationsTests.cs | 70 +++ .../ISwarmOperationsTests.cs | 411 +++++++++------ 9 files changed, 745 insertions(+), 381 deletions(-) create mode 100644 src/Docker.DotNet/Endpoints/ConfigsOperations.cs create mode 100644 src/Docker.DotNet/Endpoints/IConfigsOperations.cs create mode 100644 test/Docker.DotNet.Tests/IConfigOperationsTests.cs diff --git a/.gitattributes b/.gitattributes index 4eb3b48ef..fed459cac 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,3 @@ # Autodetect text files * text=auto -# Definitively text files -*.cs text diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 9846711cb..64a428f84 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -39,6 +39,7 @@ internal DockerClient(DockerClientConfiguration configuration, Version requested System = new SystemOperations(this); Networks = new NetworkOperations(this); Secrets = new SecretsOperations(this); + Configs = new ConfigOperations(this); Swarm = new SwarmOperations(this); Tasks = new TasksOperations(this); Volumes = new VolumeOperations(this); @@ -136,6 +137,8 @@ await sock.ConnectAsync(new Microsoft.Net.Http.Client.UnixDomainSocketEndPoint(p public ISecretsOperations Secrets { get; } + public IConfigOperations Configs { get; } + public ISwarmOperations Swarm { get; } public ITasksOperations Tasks { get; } diff --git a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs new file mode 100644 index 000000000..ec1182634 --- /dev/null +++ b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Docker.DotNet.Models; + +namespace Docker.DotNet +{ + internal class ConfigOperations : IConfigOperations + { + private readonly DockerClient _client; + + internal ConfigOperations(DockerClient client) + { + this._client = client; + } + + async Task> IConfigOperations.ListConfigsAsync(CancellationToken cancellationToken) + { + var response = await this._client.MakeRequestAsync(this._client.NoErrorHandlers, HttpMethod.Get, "configs", cancellationToken).ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject>(response.Body); + } + + async Task IConfigOperations.CreateConfigAsync(SwarmCreateConfigParameters body, CancellationToken cancellationToken) + { + if (body == null) + { + throw new ArgumentNullException(nameof(body)); + } + + var data = new JsonRequestContent(body.Config, this._client.JsonSerializer); + var response = await this._client.MakeRequestAsync(this._client.NoErrorHandlers, HttpMethod.Post, "configs/create", null, data, cancellationToken).ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject(response.Body); + } + + async Task IConfigOperations.InspectConfigAsync(string id, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException(nameof(id)); + } + + var response = await this._client.MakeRequestAsync(this._client.NoErrorHandlers, HttpMethod.Get, $"configs/{id}", cancellationToken).ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject(response.Body); + } + + Task IConfigOperations.RemoveConfigAsync(string id, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException(nameof(id)); + } + + return this._client.MakeRequestAsync(this._client.NoErrorHandlers, HttpMethod.Delete, $"configs/{id}", cancellationToken); + } + } +} diff --git a/src/Docker.DotNet/Endpoints/IConfigsOperations.cs b/src/Docker.DotNet/Endpoints/IConfigsOperations.cs new file mode 100644 index 000000000..59c701726 --- /dev/null +++ b/src/Docker.DotNet/Endpoints/IConfigsOperations.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Docker.DotNet.Models; + +namespace Docker.DotNet +{ + public interface IConfigOperations + { + /// + /// List configs + /// + /// + /// 200 - No error. + /// 500 - Server error. + /// + Task> ListConfigsAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Create a configs + /// + /// + /// 201 - No error. + /// 406 - Server error or node is not part of a swarm. + /// 409 - Name conflicts with an existing object. + /// 500 - Server error. + /// + Task CreateConfigAsync(SwarmCreateConfigParameters body, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Inspect a configs + /// + /// + /// 200 - No error. + /// 404 - Secret not found. + /// 406 - Node is not part of a swarm. + /// 500 - Server error. + /// + /// ID of the config. + Task InspectConfigAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Remove a configs + /// + /// + /// 204 - No error. + /// 404 - Secret not found. + /// 500 - Server error. + /// + /// ID of the config. + Task RemoveConfigAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet/Endpoints/ISwarmOperations.cs b/src/Docker.DotNet/Endpoints/ISwarmOperations.cs index 877e2f18d..9ee2cb71d 100644 --- a/src/Docker.DotNet/Endpoints/ISwarmOperations.cs +++ b/src/Docker.DotNet/Endpoints/ISwarmOperations.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Docker.DotNet.Models; using System.Threading; +using System.IO; namespace Docker.DotNet { @@ -154,6 +155,42 @@ public interface ISwarmOperations /// ID or name of service. Task RemoveServiceAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + /// + /// Gets stdout and stderr logs from services. + /// + /// The ID or name of the service. + /// Specifics of how to perform the operation. + /// When triggered, the operation will stop at the next available time, if possible. + /// A that will complete once all log lines have been read. + /// + /// This method is only suited for services with the json-file or journald logging driver. + /// + /// HTTP GET /services/(id)/logs + /// + /// 101 - Logs returned as a stream. + /// 200 - Logs returned as a string in response body. + /// 404 - No such service. + /// 500 - Server error. + /// 503 - Node is not part of a swarm. + /// + Task GetServiceLogsAsync(string id, ServiceLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Gets stdout and stderr logs from services. + /// + /// The ID or name of the service. + /// Indicates whether the service was created with a TTY. If , the returned stream is multiplexed. + /// Specifics of how to perform the operation. + /// When triggered, the operation will stop at the next available time, if possible. + /// + /// A that resolves to a , which provides the log information. + /// If the service wasn't created with a TTY, this stream is multiplexed. + /// + /// + /// This method is only suited for services with the json-file or journald logging driver. + /// + Task GetServiceLogsAsync(string id, bool tty, ServiceLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + #endregion Services #region Nodes diff --git a/src/Docker.DotNet/Endpoints/SwarmOperations.cs b/src/Docker.DotNet/Endpoints/SwarmOperations.cs index 1e3419a6a..7a482a85d 100644 --- a/src/Docker.DotNet/Endpoints/SwarmOperations.cs +++ b/src/Docker.DotNet/Endpoints/SwarmOperations.cs @@ -1,227 +1,263 @@ -namespace Docker.DotNet -{ - using System; - using System.Collections.Generic; - using System.Net; - using System.Net.Http; - using System.Text; - using System.Threading.Tasks; - using System.Threading; - using Models; - - internal class SwarmOperations : ISwarmOperations - { - internal static readonly ApiResponseErrorHandlingDelegate SwarmResponseHandler = (statusCode, responseBody) => - { - if (statusCode == HttpStatusCode.ServiceUnavailable) - { - // TODO: Make typed error. - throw new Exception("Node is not part of a swarm."); - } - }; - - private readonly DockerClient _client; - - internal SwarmOperations(DockerClient client) - { - this._client = client; - } - - async Task ISwarmOperations.CreateServiceAsync(ServiceCreateParameters parameters, CancellationToken cancellationToken) - { - if (parameters == null) throw new ArgumentNullException(nameof(parameters)); - - var data = new JsonRequestContent(parameters.Service ?? throw new ArgumentNullException(nameof(parameters.Service)), this._client.JsonSerializer); - var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Post, "services/create", null, data, RegistryAuthHeaders(parameters.RegistryAuth), cancellationToken).ConfigureAwait(false); - return this._client.JsonSerializer.DeserializeObject(response.Body); - } - - async Task ISwarmOperations.GetSwarmUnlockKeyAsync(CancellationToken cancellationToken) - { - var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, "swarm/unlockkey", cancellationToken).ConfigureAwait(false); - return this._client.JsonSerializer.DeserializeObject(response.Body); - } - - async Task ISwarmOperations.InitSwarmAsync(SwarmInitParameters parameters, CancellationToken cancellationToken) - { - var data = new JsonRequestContent(parameters ?? throw new ArgumentNullException(nameof(parameters)), this._client.JsonSerializer); - var response = await this._client.MakeRequestAsync( - new ApiResponseErrorHandlingDelegate[] - { - (statusCode, responseBody) => - { - if (statusCode == HttpStatusCode.NotAcceptable) - { - // TODO: Make typed error. - throw new Exception("Node is already part of a swarm."); - } - } - }, - HttpMethod.Post, - "swarm/init", - null, - data, - cancellationToken).ConfigureAwait(false); - - return response.Body; - } - - async Task ISwarmOperations.InspectServiceAsync(string id, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); - - var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"services/{id}", cancellationToken).ConfigureAwait(false); - return this._client.JsonSerializer.DeserializeObject(response.Body); - } - - async Task ISwarmOperations.InspectSwarmAsync(CancellationToken cancellationToken) - { - var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, "swarm", cancellationToken).ConfigureAwait(false); - return this._client.JsonSerializer.DeserializeObject(response.Body); - } - - async Task ISwarmOperations.JoinSwarmAsync(SwarmJoinParameters parameters, CancellationToken cancellationToken) - { - var data = new JsonRequestContent(parameters ?? throw new ArgumentNullException(nameof(parameters)), this._client.JsonSerializer); - await this._client.MakeRequestAsync( - new ApiResponseErrorHandlingDelegate[] - { - (statusCode, responseBody) => - { - if (statusCode == HttpStatusCode.ServiceUnavailable) - { - // TODO: Make typed error. - throw new Exception("Node is already part of a swarm."); - } - } - }, - HttpMethod.Post, - "swarm/join", - null, - data, - cancellationToken).ConfigureAwait(false); - } - - async Task ISwarmOperations.LeaveSwarmAsync(SwarmLeaveParameters parameters, CancellationToken cancellationToken) - { - var query = parameters == null ? null : new QueryString(parameters); - await this._client.MakeRequestAsync( - new ApiResponseErrorHandlingDelegate[] - { - (statusCode, responseBody) => - { - if (statusCode == HttpStatusCode.ServiceUnavailable) - { - // TODO: Make typed error. - throw new Exception("Node is not part of a swarm."); - } - } - }, - HttpMethod.Post, - "swarm/leave", - query, - cancellationToken).ConfigureAwait(false); - } - - async Task> ISwarmOperations.ListServicesAsync(ServicesListParameters parameters, CancellationToken cancellationToken) - { - var queryParameters = parameters != null ? new QueryString(parameters) : null; - var response = await this._client - .MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"services", queryParameters, cancellationToken) - .ConfigureAwait(false); - return this._client.JsonSerializer.DeserializeObject(response.Body); - } - - async Task ISwarmOperations.RemoveServiceAsync(string id, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); - - await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Delete, $"services/{id}", cancellationToken).ConfigureAwait(false); - } - - async Task ISwarmOperations.UnlockSwarmAsync(SwarmUnlockParameters parameters, CancellationToken cancellationToken) - { - var body = new JsonRequestContent(parameters ?? throw new ArgumentNullException(nameof(parameters)), this._client.JsonSerializer); - await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Post, "swarm/unlock", null, body, cancellationToken).ConfigureAwait(false); - } - - async Task ISwarmOperations.UpdateServiceAsync(string id, ServiceUpdateParameters parameters, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); - if (parameters == null) throw new ArgumentNullException(nameof(parameters)); - - var query = new QueryString(parameters); - var body = new JsonRequestContent(parameters.Service ?? throw new ArgumentNullException(nameof(parameters.Service)), this._client.JsonSerializer); - var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Post, $"services/{id}/update", query, body, RegistryAuthHeaders(parameters.RegistryAuth), cancellationToken).ConfigureAwait(false); - return this._client.JsonSerializer.DeserializeObject(response.Body); - } - - async Task ISwarmOperations.UpdateSwarmAsync(SwarmUpdateParameters parameters, CancellationToken cancellationToken) - { - var query = new QueryString(parameters ?? throw new ArgumentNullException(nameof(parameters))); - var body = new JsonRequestContent(parameters.Spec ?? throw new ArgumentNullException(nameof(parameters.Spec)), this._client.JsonSerializer); - await this._client.MakeRequestAsync( - new ApiResponseErrorHandlingDelegate[] - { - (statusCode, responseBody) => - { - if (statusCode == HttpStatusCode.ServiceUnavailable) - { - // TODO: Make typed error. - throw new Exception("Node is not part of a swarm."); - } - } - }, - HttpMethod.Post, - "swarm/update", - query, - body, - cancellationToken).ConfigureAwait(false); - } - - private IDictionary RegistryAuthHeaders(AuthConfig authConfig) - { - if (authConfig == null) - { - return new Dictionary(); - } - - return new Dictionary - { - { - "X-Registry-Auth", - Convert.ToBase64String(Encoding.UTF8.GetBytes(this._client.JsonSerializer.SerializeObject(authConfig))) - } - }; - } - - async Task> ISwarmOperations.ListNodesAsync(CancellationToken cancellationToken) - { - var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"nodes", cancellationToken).ConfigureAwait(false); - return this._client.JsonSerializer.DeserializeObject(response.Body); - } - - async Task ISwarmOperations.InspectNodeAsync(string id, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); - var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"nodes/{id}", cancellationToken).ConfigureAwait(false); - return this._client.JsonSerializer.DeserializeObject(response.Body); - } - - async Task ISwarmOperations.RemoveNodeAsync(string id, bool force, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); - var parameters = new NodeRemoveParameters {Force = force}; - var query = new QueryString(parameters); - await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Delete, $"nodes/{id}", query, cancellationToken).ConfigureAwait(false); - } - - async Task ISwarmOperations.UpdateNodeAsync(string id, ulong version, NodeUpdateParameters parameters, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); - var query = new EnumerableQueryString("version", new[] { version.ToString() }); - var body = new JsonRequestContent(parameters ?? throw new ArgumentNullException(nameof(parameters)), this._client.JsonSerializer); - await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Post, $"nodes/{id}/update", query, body, cancellationToken); - } - } -} +namespace Docker.DotNet +{ + using System; + using System.Collections.Generic; + using System.Net; + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + using System.Threading; + using Models; + using System.IO; + + internal class SwarmOperations : ISwarmOperations + { + internal static readonly ApiResponseErrorHandlingDelegate SwarmResponseHandler = (statusCode, responseBody) => + { + if (statusCode == HttpStatusCode.ServiceUnavailable) + { + // TODO: Make typed error. + throw new Exception("Node is not part of a swarm."); + } + }; + + private readonly DockerClient _client; + + internal SwarmOperations(DockerClient client) + { + this._client = client; + } + + async Task ISwarmOperations.CreateServiceAsync(ServiceCreateParameters parameters, CancellationToken cancellationToken) + { + if (parameters == null) throw new ArgumentNullException(nameof(parameters)); + + var data = new JsonRequestContent(parameters.Service ?? throw new ArgumentNullException(nameof(parameters.Service)), this._client.JsonSerializer); + var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Post, "services/create", null, data, RegistryAuthHeaders(parameters.RegistryAuth), cancellationToken).ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject(response.Body); + } + + async Task ISwarmOperations.GetSwarmUnlockKeyAsync(CancellationToken cancellationToken) + { + var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, "swarm/unlockkey", cancellationToken).ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject(response.Body); + } + + async Task ISwarmOperations.InitSwarmAsync(SwarmInitParameters parameters, CancellationToken cancellationToken) + { + var data = new JsonRequestContent(parameters ?? throw new ArgumentNullException(nameof(parameters)), this._client.JsonSerializer); + var response = await this._client.MakeRequestAsync( + new ApiResponseErrorHandlingDelegate[] + { + (statusCode, responseBody) => + { + if (statusCode == HttpStatusCode.NotAcceptable) + { + // TODO: Make typed error. + throw new Exception("Node is already part of a swarm."); + } + } + }, + HttpMethod.Post, + "swarm/init", + null, + data, + cancellationToken).ConfigureAwait(false); + + return response.Body; + } + + async Task ISwarmOperations.InspectServiceAsync(string id, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); + + var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"services/{id}", cancellationToken).ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject(response.Body); + } + + async Task ISwarmOperations.InspectSwarmAsync(CancellationToken cancellationToken) + { + var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, "swarm", cancellationToken).ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject(response.Body); + } + + async Task ISwarmOperations.JoinSwarmAsync(SwarmJoinParameters parameters, CancellationToken cancellationToken) + { + var data = new JsonRequestContent(parameters ?? throw new ArgumentNullException(nameof(parameters)), this._client.JsonSerializer); + await this._client.MakeRequestAsync( + new ApiResponseErrorHandlingDelegate[] + { + (statusCode, responseBody) => + { + if (statusCode == HttpStatusCode.ServiceUnavailable) + { + // TODO: Make typed error. + throw new Exception("Node is already part of a swarm."); + } + } + }, + HttpMethod.Post, + "swarm/join", + null, + data, + cancellationToken).ConfigureAwait(false); + } + + async Task ISwarmOperations.LeaveSwarmAsync(SwarmLeaveParameters parameters, CancellationToken cancellationToken) + { + var query = parameters == null ? null : new QueryString(parameters); + await this._client.MakeRequestAsync( + new ApiResponseErrorHandlingDelegate[] + { + (statusCode, responseBody) => + { + if (statusCode == HttpStatusCode.ServiceUnavailable) + { + // TODO: Make typed error. + throw new Exception("Node is not part of a swarm."); + } + } + }, + HttpMethod.Post, + "swarm/leave", + query, + cancellationToken).ConfigureAwait(false); + } + + async Task> ISwarmOperations.ListServicesAsync(ServicesListParameters parameters, CancellationToken cancellationToken) + { + var queryParameters = parameters != null ? new QueryString(parameters) : null; + var response = await this._client + .MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"services", queryParameters, cancellationToken) + .ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject(response.Body); + } + + async Task ISwarmOperations.RemoveServiceAsync(string id, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); + + await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Delete, $"services/{id}", cancellationToken).ConfigureAwait(false); + } + + async Task ISwarmOperations.UnlockSwarmAsync(SwarmUnlockParameters parameters, CancellationToken cancellationToken) + { + var body = new JsonRequestContent(parameters ?? throw new ArgumentNullException(nameof(parameters)), this._client.JsonSerializer); + await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Post, "swarm/unlock", null, body, cancellationToken).ConfigureAwait(false); + } + + async Task ISwarmOperations.UpdateServiceAsync(string id, ServiceUpdateParameters parameters, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); + if (parameters == null) throw new ArgumentNullException(nameof(parameters)); + + var query = new QueryString(parameters); + var body = new JsonRequestContent(parameters.Service ?? throw new ArgumentNullException(nameof(parameters.Service)), this._client.JsonSerializer); + var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Post, $"services/{id}/update", query, body, RegistryAuthHeaders(parameters.RegistryAuth), cancellationToken).ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject(response.Body); + } + + public Task GetServiceLogsAsync(string id, ServiceLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException(nameof(id)); + } + + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + IQueryString queryParameters = new QueryString(parameters); + return this._client.MakeRequestForStreamAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"services/{id}/logs", queryParameters, cancellationToken); + } + + public async Task GetServiceLogsAsync(string id, bool tty, ServiceLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException(nameof(id)); + } + + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + IQueryString queryParameters = new QueryString(parameters); + + Stream result = await this._client.MakeRequestForStreamAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"services/{id}/logs", queryParameters, cancellationToken).ConfigureAwait(false); + + return new MultiplexedStream(result, !tty); + } + + async Task ISwarmOperations.UpdateSwarmAsync(SwarmUpdateParameters parameters, CancellationToken cancellationToken) + { + var query = new QueryString(parameters ?? throw new ArgumentNullException(nameof(parameters))); + var body = new JsonRequestContent(parameters.Spec ?? throw new ArgumentNullException(nameof(parameters.Spec)), this._client.JsonSerializer); + await this._client.MakeRequestAsync( + new ApiResponseErrorHandlingDelegate[] + { + (statusCode, responseBody) => + { + if (statusCode == HttpStatusCode.ServiceUnavailable) + { + // TODO: Make typed error. + throw new Exception("Node is not part of a swarm."); + } + } + }, + HttpMethod.Post, + "swarm/update", + query, + body, + cancellationToken).ConfigureAwait(false); + } + + private IDictionary RegistryAuthHeaders(AuthConfig authConfig) + { + if (authConfig == null) + { + return new Dictionary(); + } + + return new Dictionary + { + { + "X-Registry-Auth", + Convert.ToBase64String(Encoding.UTF8.GetBytes(this._client.JsonSerializer.SerializeObject(authConfig))) + } + }; + } + + async Task> ISwarmOperations.ListNodesAsync(CancellationToken cancellationToken) + { + var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"nodes", cancellationToken).ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject(response.Body); + } + + async Task ISwarmOperations.InspectNodeAsync(string id, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); + var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"nodes/{id}", cancellationToken).ConfigureAwait(false); + return this._client.JsonSerializer.DeserializeObject(response.Body); + } + + async Task ISwarmOperations.RemoveNodeAsync(string id, bool force, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); + var parameters = new NodeRemoveParameters { Force = force }; + var query = new QueryString(parameters); + await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Delete, $"nodes/{id}", query, cancellationToken).ConfigureAwait(false); + } + + async Task ISwarmOperations.UpdateNodeAsync(string id, ulong version, NodeUpdateParameters parameters, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); + var query = new EnumerableQueryString("version", new[] { version.ToString() }); + var body = new JsonRequestContent(parameters ?? throw new ArgumentNullException(nameof(parameters)), this._client.JsonSerializer); + await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Post, $"nodes/{id}/update", query, body, cancellationToken); + } + } +} diff --git a/src/Docker.DotNet/IDockerClient.cs b/src/Docker.DotNet/IDockerClient.cs index 682ce3be5..c42afc048 100644 --- a/src/Docker.DotNet/IDockerClient.cs +++ b/src/Docker.DotNet/IDockerClient.cs @@ -20,6 +20,8 @@ public interface IDockerClient : IDisposable ISecretsOperations Secrets { get; } + IConfigOperations Configs { get; } + ISwarmOperations Swarm { get; } ITasksOperations Tasks { get; } diff --git a/test/Docker.DotNet.Tests/IConfigOperationsTests.cs b/test/Docker.DotNet.Tests/IConfigOperationsTests.cs new file mode 100644 index 000000000..cc9a6c65b --- /dev/null +++ b/test/Docker.DotNet.Tests/IConfigOperationsTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using Docker.DotNet.Models; +using Xunit; +using Xunit.Abstractions; + +namespace Docker.DotNet.Tests +{ + [Collection(nameof(TestCollection))] + public class IConfigOperationsTests + { + private readonly DockerClientConfiguration _dockerClientConfiguration; + private readonly DockerClient _dockerClient; + private readonly TestOutput _output; + public IConfigOperationsTests(TestFixture testFixture, ITestOutputHelper outputHelper) + { + _dockerClientConfiguration = testFixture.DockerClientConfiguration; + _dockerClient = _dockerClientConfiguration.CreateClient(); + _output = new TestOutput(outputHelper); + } + + [Fact] + public async void SwarmConfig_CanCreateAndRead() + { + var currentConfigs = await _dockerClient.Configs.ListConfigsAsync(); + + _output.WriteLine($"Current Configs: {currentConfigs.Count}"); + + var testConfigSpec = new SwarmConfigSpec + { + Name = $"Config-{Guid.NewGuid().ToString().Substring(1, 10)}", + Labels = new Dictionary { { "key", "value" } }, + Data = new List { 1, 2, 3, 4, 5 } + }; + + var configParameters = new SwarmCreateConfigParameters + { + Config = testConfigSpec + }; + + var createdConfig = await _dockerClient.Configs.CreateConfigAsync(configParameters); + Assert.NotNull(createdConfig.ID); + _output.WriteLine($"Config created: {createdConfig.ID}"); + + var configs = await _dockerClient.Configs.ListConfigsAsync(); + Assert.Contains(configs, c => c.ID == createdConfig.ID); + _output.WriteLine($"Current Configs: {configs.Count}"); + + var configResponse = await _dockerClient.Configs.InspectConfigAsync(createdConfig.ID); + + Assert.NotNull(configResponse); + + Assert.Equal(configResponse.Spec.Name, testConfigSpec.Name); + Assert.Equal(configResponse.Spec.Data, testConfigSpec.Data); + Assert.Equal(configResponse.Spec.Labels, testConfigSpec.Labels); + Assert.Equal(configResponse.Spec.Templating, testConfigSpec.Templating); + + + _output.WriteLine($"Config created is the same."); + + await _dockerClient.Configs.RemoveConfigAsync(createdConfig.ID); + + await Assert.ThrowsAsync(() => _dockerClient.Configs.InspectConfigAsync(createdConfig.ID)); + + + + } + } +} + diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs index 27cc67177..196284bbb 100644 --- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs +++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs @@ -1,152 +1,259 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Docker.DotNet.Models; -using Xunit; - -namespace Docker.DotNet.Tests -{ - [Collection(nameof(TestCollection))] - public class ISwarmOperationsTests - { - private readonly DockerClient _dockerClient; - private readonly string _imageId; - - public ISwarmOperationsTests(TestFixture testFixture) - { - _dockerClient = testFixture.DockerClient; - _imageId = testFixture.Image.ID; - } - - [Fact] - public async Task GetFilteredServicesByName_Succeeds() - { - var firstServiceName = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}"; - var firstServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters - { - Service = new ServiceSpec - { - Name = firstServiceName, - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } - } - }).Result.ID; - - var secondServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters - { - Service = new ServiceSpec - { - Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } - } - }).Result.ID; - - var thirdServiceid = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters - { - Service = new ServiceSpec - { - Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } - } - }).Result.ID; - - var services = await _dockerClient.Swarm.ListServicesAsync( - new ServicesListParameters - { - Filters = new ServiceFilter - { - Name = new string[] - { - firstServiceName - } - } - }, - CancellationToken.None); - - Assert.Single(services); - - await _dockerClient.Swarm.RemoveServiceAsync(firstServiceId, default); - await _dockerClient.Swarm.RemoveServiceAsync(secondServiceId, default); - await _dockerClient.Swarm.RemoveServiceAsync(thirdServiceid, default); - } - - [Fact] - public async Task GetFilteredServicesById_Succeeds() - { - var firstServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters - { - Service = new ServiceSpec - { - Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } - } - }).Result.ID; - - var secondServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters - { - Service = new ServiceSpec - { - Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } - } - }).Result.ID; - - var thirdServiceid = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters - { - Service = new ServiceSpec - { - Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } - } - }).Result.ID; - - var services = await _dockerClient.Swarm.ListServicesAsync(new ServicesListParameters { Filters = new ServiceFilter { Id = new string[] { firstServiceId } } }, CancellationToken.None); - Assert.Single(services); - - await _dockerClient.Swarm.RemoveServiceAsync(firstServiceId, default); - await _dockerClient.Swarm.RemoveServiceAsync(secondServiceId, default); - await _dockerClient.Swarm.RemoveServiceAsync(thirdServiceid, default); - } - - [Fact] - public async Task GetServices_Succeeds() - { - var initialServiceCount = _dockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None).Result.Count(); - - var firstServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters - { - Service = new ServiceSpec - { - Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } - } - }).Result.ID; - - var secondServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters - { - Service = new ServiceSpec - { - Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } - } - }).Result.ID; - - var thirdServiceid = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters - { - Service = new ServiceSpec - { - Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } - } - }).Result.ID; - - var services = await _dockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None); - - Assert.True(services.Count() > initialServiceCount); - - await _dockerClient.Swarm.RemoveServiceAsync(firstServiceId, default); - await _dockerClient.Swarm.RemoveServiceAsync(secondServiceId, default); - await _dockerClient.Swarm.RemoveServiceAsync(thirdServiceid, default); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Docker.DotNet.Models; +using Xunit; + +namespace Docker.DotNet.Tests +{ + [Collection(nameof(TestCollection))] + public class ISwarmOperationsTests + { + private readonly CancellationTokenSource _cts; + + private readonly DockerClient _dockerClient; + private readonly string _imageId; + + public ISwarmOperationsTests(TestFixture testFixture) + { + // 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; + } + + [Fact] + public async Task GetFilteredServicesByName_Succeeds() + { + var firstServiceName = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}"; + var firstServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + { + Service = new ServiceSpec + { + Name = firstServiceName, + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + } + }).Result.ID; + + var secondServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + { + Service = new ServiceSpec + { + Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + } + }).Result.ID; + + var thirdServiceid = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + { + Service = new ServiceSpec + { + Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + } + }).Result.ID; + + var services = await _dockerClient.Swarm.ListServicesAsync( + new ServicesListParameters + { + Filters = new ServiceFilter + { + Name = new string[] + { + firstServiceName + } + } + }, + CancellationToken.None); + + Assert.Single(services); + + await _dockerClient.Swarm.RemoveServiceAsync(firstServiceId, default); + await _dockerClient.Swarm.RemoveServiceAsync(secondServiceId, default); + await _dockerClient.Swarm.RemoveServiceAsync(thirdServiceid, default); + } + + [Fact] + public async Task GetFilteredServicesById_Succeeds() + { + var firstServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + { + Service = new ServiceSpec + { + Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + } + }).Result.ID; + + var secondServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + { + Service = new ServiceSpec + { + Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + } + }).Result.ID; + + var thirdServiceid = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + { + Service = new ServiceSpec + { + Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + } + }).Result.ID; + + var services = await _dockerClient.Swarm.ListServicesAsync(new ServicesListParameters { Filters = new ServiceFilter { Id = new string[] { firstServiceId } } }, CancellationToken.None); + Assert.Single(services); + + await _dockerClient.Swarm.RemoveServiceAsync(firstServiceId, default); + await _dockerClient.Swarm.RemoveServiceAsync(secondServiceId, default); + await _dockerClient.Swarm.RemoveServiceAsync(thirdServiceid, default); + } + + [Fact] + public async Task GetServices_Succeeds() + { + var initialServiceCount = _dockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None).Result.Count(); + + var firstServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + { + Service = new ServiceSpec + { + Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + } + }).Result.ID; + + var secondServiceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + { + Service = new ServiceSpec + { + Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + } + }).Result.ID; + + var thirdServiceid = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + { + Service = new ServiceSpec + { + Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + } + }).Result.ID; + + var services = await _dockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None); + + Assert.True(services.Count() > initialServiceCount); + + await _dockerClient.Swarm.RemoveServiceAsync(firstServiceId, default); + await _dockerClient.Swarm.RemoveServiceAsync(secondServiceId, default); + await _dockerClient.Swarm.RemoveServiceAsync(thirdServiceid, default); + } + + [Fact] + public async Task GetServiceLogs_Succeeds() + { + var cts = new CancellationTokenSource(); + var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, cts.Token); + + var serviceName = $"service-withLogs-{Guid.NewGuid().ToString().Substring(1, 10)}"; + var serviceId = _dockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + { + Service = new ServiceSpec + { + Name = serviceName, + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _imageId } } + } + }).Result.ID; + + var _stream = await _dockerClient.Swarm.GetServiceLogsAsync(serviceName, false, new ServiceLogsParameters + { + Follow = true, + ShowStdout = true, + ShowStderr = true + }); + + int maxRetries = 3; + int currentRetry = 0; + TimeSpan delayBetweenRetries = TimeSpan.FromSeconds(5); + List logLines = null; + + while (currentRetry < maxRetries && !linkedCts.IsCancellationRequested) + { + logLines = new List(); + TimeSpan delay = TimeSpan.FromSeconds(10); + cts.CancelAfter(delay); + + bool cancelRequested = false; // Add a flag to indicate cancellation + + while (!linkedCts.IsCancellationRequested && !cancelRequested) + { + var line = new List(); + var buffer = new byte[4096]; + + try + { + while (true) + { + var res = await _stream.ReadOutputAsync(buffer, 0, buffer.Length, linkedCts.Token); + + if (res.Count == 0) + { + continue; + } + + int newlineIndex = Array.IndexOf(buffer, (byte)'\n', 0, res.Count); + + if (newlineIndex != -1) + { + line.AddRange(buffer.Take(newlineIndex)); + break; + } + else + { + line.AddRange(buffer.Take(res.Count)); + } + } + + logLines.Add(Encoding.UTF8.GetString(line.ToArray())); + } + catch (OperationCanceledException) + { + cancelRequested = true; // Set the flag when cancellation is requested + + // Reset the CancellationTokenSource for the next attempt + cts = new CancellationTokenSource(); + linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, cts.Token); + cts.CancelAfter(delay); + } + } + + if (logLines.Any() && logLines.First().Contains("[INF]")) + { + break; + } + else + { + currentRetry++; + if (currentRetry < maxRetries) + { + await Task.Delay(delayBetweenRetries); + } + } + } + + Assert.True(logLines.Any()); + Assert.Contains("[INF]", logLines.First()); + + await _dockerClient.Swarm.RemoveServiceAsync(serviceId, default); + } + } +}