From 90d94a1055c6988a2702a3a149924d218aba3b35 Mon Sep 17 00:00:00 2001 From: ACoderLife Date: Mon, 19 Sep 2022 13:07:42 +1000 Subject: [PATCH 01/12] expose Swam Config as client.Configs --- src/Docker.DotNet/DockerClient.cs | 3 + .../Endpoints/ConfigsOperations.cs | 58 +++++++++++++++++++ .../Endpoints/IConfigsOperations.cs | 53 +++++++++++++++++ src/Docker.DotNet/IDockerClient.cs | 2 + 4 files changed, 116 insertions(+) create mode 100644 src/Docker.DotNet/Endpoints/ConfigsOperations.cs create mode 100644 src/Docker.DotNet/Endpoints/IConfigsOperations.cs diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 852b384cc..aeea33696 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 ConfigsOperations(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 IConfigsOperations 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..35edc9d72 --- /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 ConfigsOperations : IConfigsOperations + { + private readonly DockerClient _client; + + internal ConfigsOperations(DockerClient client) + { + this._client = client; + } + + async Task> IConfigsOperations.ListAsync(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 IConfigsOperations.CreateAsync(SwarmCreateConfigParameters body, CancellationToken cancellationToken) + { + if (body == null) + { + throw new ArgumentNullException(nameof(body)); + } + + var data = new JsonRequestContent(body, 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 IConfigsOperations.InspectAsync(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 IConfigsOperations.DeleteAsync(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); + } + } +} \ No newline at end of file diff --git a/src/Docker.DotNet/Endpoints/IConfigsOperations.cs b/src/Docker.DotNet/Endpoints/IConfigsOperations.cs new file mode 100644 index 000000000..69da499dc --- /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 IConfigsOperations + { + /// + /// List secrets + /// + /// + /// 200 - No error. + /// 500 - Server error. + /// + Task> ListAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Create a secret + /// + /// + /// 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 CreateAsync(SwarmCreateConfigParameters body, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Inspect a secret + /// + /// + /// 200 - No error. + /// 404 - Secret not found. + /// 406 - Node is not part of a swarm. + /// 500 - Server error. + /// + /// ID of the config. + Task InspectAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Delete a secret + /// + /// + /// 204 - No error. + /// 404 - Secret not found. + /// 500 - Server error. + /// + /// ID of the config. + Task DeleteAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet/IDockerClient.cs b/src/Docker.DotNet/IDockerClient.cs index 682ce3be5..cd03959a5 100644 --- a/src/Docker.DotNet/IDockerClient.cs +++ b/src/Docker.DotNet/IDockerClient.cs @@ -20,6 +20,8 @@ public interface IDockerClient : IDisposable ISecretsOperations Secrets { get; } + IConfigsOperations Configs { get; } + ISwarmOperations Swarm { get; } ITasksOperations Tasks { get; } From 6fc666c5c236d598a6866f8dae2613da7b024cc2 Mon Sep 17 00:00:00 2001 From: ACoderLife Date: Mon, 19 Sep 2022 13:35:31 +1000 Subject: [PATCH 02/12] Expose Swam Service Logs --- .../Endpoints/ISwarmOperations.cs | 32 ++ .../Endpoints/SwarmOperations.cs | 490 ++++++++++-------- 2 files changed, 295 insertions(+), 227 deletions(-) diff --git a/src/Docker.DotNet/Endpoints/ISwarmOperations.cs b/src/Docker.DotNet/Endpoints/ISwarmOperations.cs index 877e2f18d..d0278032b 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,37 @@ public interface ISwarmOperations /// ID or name of service. Task RemoveServiceAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + /// + /// Get service logs. + /// + /// Get {stdout} and {stderr} logs from all service tasks. + /// Note: This endpoint works only for services with the {json-file} or {journald} logging driver. + /// + /// + /// docker service logs + /// + /// 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. + /// + /// ID or name of the service. + Task GetServiceLogsAsync(string id, ServiceLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Gets the stdout and stderr logs from all service tasks. + /// This endpoint works only for services with the json-file or journald logging driver. + /// + /// ID or name of the service. + /// If the service was created with a TTY or not. If , the returned stream is multiplexed. + /// The parameters used to retrieve the logs. + /// A token used to cancel this operation. + /// A stream with the retrieved logs. If the service wasn't created with a TTY, this stream is multiplexed. + 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..23f7eec6e 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); + } + } +} From bcf12b1d42ca003cba1ddb00560e0895158deebb Mon Sep 17 00:00:00 2001 From: Jasim Schluter Date: Mon, 19 Sep 2022 16:45:07 +1000 Subject: [PATCH 03/12] send SwarmConfigSpec to api --- src/Docker.DotNet/Endpoints/ConfigsOperations.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs index 35edc9d72..47398fda4 100644 --- a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs +++ b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net.Http; using System.Threading; @@ -29,7 +29,7 @@ async Task IConfigsOperations.CreateAsync(SwarmCreate throw new ArgumentNullException(nameof(body)); } - var data = new JsonRequestContent(body, this._client.JsonSerializer); + 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); } @@ -55,4 +55,4 @@ Task IConfigsOperations.DeleteAsync(string id, CancellationToken cancellationTok return this._client.MakeRequestAsync(this._client.NoErrorHandlers, HttpMethod.Delete, $"configs/{id}", cancellationToken); } } -} \ No newline at end of file +} From 12aec2fe7305646887f0eb8fcae942f01520969f Mon Sep 17 00:00:00 2001 From: ACoderLife <37108462+ACoderLife@users.noreply.github.com> Date: Mon, 20 Mar 2023 16:18:25 +1100 Subject: [PATCH 04/12] Update src/Docker.DotNet/Endpoints/ISwarmOperations.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- src/Docker.DotNet/Endpoints/ISwarmOperations.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Docker.DotNet/Endpoints/ISwarmOperations.cs b/src/Docker.DotNet/Endpoints/ISwarmOperations.cs index d0278032b..4189f7aa1 100644 --- a/src/Docker.DotNet/Endpoints/ISwarmOperations.cs +++ b/src/Docker.DotNet/Endpoints/ISwarmOperations.cs @@ -156,13 +156,14 @@ public interface ISwarmOperations Task RemoveServiceAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); /// - /// Get service logs. - /// - /// Get {stdout} and {stderr} logs from all service tasks. - /// Note: This endpoint works only for services with the {json-file} or {journald} logging driver. + /// 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. /// - /// docker service logs + /// This method is only suited for services with the json-file or journald logging driver. /// /// HTTP GET /services/(id)/logs /// @@ -172,7 +173,6 @@ public interface ISwarmOperations /// 500 - Server error. /// 503 - Node is not part of a swarm. /// - /// ID or name of the service. Task GetServiceLogsAsync(string id, ServiceLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); /// From 6dd30fca936e59ed745f2e0a21aacbf963daaae8 Mon Sep 17 00:00:00 2001 From: ACoderLife <37108462+ACoderLife@users.noreply.github.com> Date: Mon, 20 Mar 2023 16:18:36 +1100 Subject: [PATCH 05/12] Update src/Docker.DotNet/Endpoints/ISwarmOperations.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- .../Endpoints/ISwarmOperations.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Docker.DotNet/Endpoints/ISwarmOperations.cs b/src/Docker.DotNet/Endpoints/ISwarmOperations.cs index 4189f7aa1..9ee2cb71d 100644 --- a/src/Docker.DotNet/Endpoints/ISwarmOperations.cs +++ b/src/Docker.DotNet/Endpoints/ISwarmOperations.cs @@ -176,14 +176,19 @@ public interface ISwarmOperations Task GetServiceLogsAsync(string id, ServiceLogsParameters parameters, CancellationToken cancellationToken = default(CancellationToken)); /// - /// Gets the stdout and stderr logs from all service tasks. - /// This endpoint works only for services with the json-file or journald logging driver. - /// - /// ID or name of the service. - /// If the service was created with a TTY or not. If , the returned stream is multiplexed. - /// The parameters used to retrieve the logs. - /// A token used to cancel this operation. - /// A stream with the retrieved logs. If the service wasn't created with a TTY, this stream is multiplexed. + /// 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 From b04f266c4e9888763275ebbe5f804f4835438175 Mon Sep 17 00:00:00 2001 From: ACoderLife Date: Mon, 20 Mar 2023 18:35:36 +1100 Subject: [PATCH 06/12] rename to standard add logging test --- .gitattributes | 2 -- 1 file changed, 2 deletions(-) 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 From 7c0e19e485becd86bf436991d626af4f77cf11ae Mon Sep 17 00:00:00 2001 From: ACoderLife Date: Mon, 20 Mar 2023 18:35:36 +1100 Subject: [PATCH 07/12] add logging test fix naming convention --- .../Endpoints/ConfigsOperations.cs | 8 +- .../Endpoints/IConfigsOperations.cs | 16 +- .../Endpoints/SwarmOperations.cs | 4 +- .../ISwarmOperationsTests.cs | 373 +++++++++++------- 4 files changed, 235 insertions(+), 166 deletions(-) diff --git a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs index 47398fda4..355a86792 100644 --- a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs +++ b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs @@ -16,13 +16,13 @@ internal ConfigsOperations(DockerClient client) this._client = client; } - async Task> IConfigsOperations.ListAsync(CancellationToken cancellationToken) + async Task> IConfigsOperations.ListConfigAsync(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 IConfigsOperations.CreateAsync(SwarmCreateConfigParameters body, CancellationToken cancellationToken) + async Task IConfigsOperations.CreateConfigAsync(SwarmCreateConfigParameters body, CancellationToken cancellationToken) { if (body == null) { @@ -34,7 +34,7 @@ async Task IConfigsOperations.CreateAsync(SwarmCreate return this._client.JsonSerializer.DeserializeObject(response.Body); } - async Task IConfigsOperations.InspectAsync(string id, CancellationToken cancellationToken) + async Task IConfigsOperations.InspectConfigAsync(string id, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(id)) { @@ -45,7 +45,7 @@ async Task IConfigsOperations.InspectAsync(string id, CancellationT return this._client.JsonSerializer.DeserializeObject(response.Body); } - Task IConfigsOperations.DeleteAsync(string id, CancellationToken cancellationToken) + Task IConfigsOperations.RemoveConfigAsync(string id, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(id)) { diff --git a/src/Docker.DotNet/Endpoints/IConfigsOperations.cs b/src/Docker.DotNet/Endpoints/IConfigsOperations.cs index 69da499dc..24c7a7178 100644 --- a/src/Docker.DotNet/Endpoints/IConfigsOperations.cs +++ b/src/Docker.DotNet/Endpoints/IConfigsOperations.cs @@ -8,16 +8,16 @@ namespace Docker.DotNet public interface IConfigsOperations { /// - /// List secrets + /// List configs /// /// /// 200 - No error. /// 500 - Server error. /// - Task> ListAsync(CancellationToken cancellationToken = default(CancellationToken)); + Task> ListConfigAsync(CancellationToken cancellationToken = default(CancellationToken)); /// - /// Create a secret + /// Create a configs /// /// /// 201 - No error. @@ -25,10 +25,10 @@ public interface IConfigsOperations /// 409 - Name conflicts with an existing object. /// 500 - Server error. /// - Task CreateAsync(SwarmCreateConfigParameters body, CancellationToken cancellationToken = default(CancellationToken)); + Task CreateConfigAsync(SwarmCreateConfigParameters body, CancellationToken cancellationToken = default(CancellationToken)); /// - /// Inspect a secret + /// Inspect a configs /// /// /// 200 - No error. @@ -37,10 +37,10 @@ public interface IConfigsOperations /// 500 - Server error. /// /// ID of the config. - Task InspectAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + Task InspectConfigAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); /// - /// Delete a secret + /// Remove a configs /// /// /// 204 - No error. @@ -48,6 +48,6 @@ public interface IConfigsOperations /// 500 - Server error. /// /// ID of the config. - Task DeleteAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); + Task RemoveConfigAsync(string id, CancellationToken cancellationToken = default(CancellationToken)); } } \ No newline at end of file diff --git a/src/Docker.DotNet/Endpoints/SwarmOperations.cs b/src/Docker.DotNet/Endpoints/SwarmOperations.cs index 23f7eec6e..7a482a85d 100644 --- a/src/Docker.DotNet/Endpoints/SwarmOperations.cs +++ b/src/Docker.DotNet/Endpoints/SwarmOperations.cs @@ -192,7 +192,7 @@ async Task ISwarmOperations.UpdateServiceAsync(string id, return new MultiplexedStream(result, !tty); } - async Task ISwarmOperations.UpdateSwarmAsync(SwarmUpdateParameters parameters, CancellationToken cancellationToken) + 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); @@ -247,7 +247,7 @@ async Task ISwarmOperations.InspectNodeAsync(string id, Cancel 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 parameters = new NodeRemoveParameters { Force = force }; var query = new QueryString(parameters); await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Delete, $"nodes/{id}", query, cancellationToken).ConfigureAwait(false); } diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs index 27cc67177..f0a109946 100644 --- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs +++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs @@ -1,152 +1,221 @@ -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() + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(_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 + }); + + TimeSpan delay = TimeSpan.FromSeconds(10); + cts.CancelAfter(delay); + + var logLines = new List(); + while (!cts.IsCancellationRequested) + { + var line = new List(); + var buffer = new byte[1]; + + while (!cts.IsCancellationRequested) + { + var res = await _stream.ReadOutputAsync(buffer, 0, 1, default); + + if (res.Count == 0) + { + continue; + } + + else if (buffer[0] == '\n') + { + break; + } + + else + { + line.Add(buffer[0]); + } + } + + logLines.Add(Encoding.UTF8.GetString(line.ToArray())); + } + + Assert.True(logLines.Any()); + Assert.Contains("[INF]", logLines.First()); + + await _dockerClient.Swarm.RemoveServiceAsync(serviceId, default); + } + } +} From 91af57f9640053671862a0bd2fa21a72e163e771 Mon Sep 17 00:00:00 2001 From: ACoderLife <37108462+ACoderLife@users.noreply.github.com> Date: Thu, 23 Mar 2023 22:14:51 +1100 Subject: [PATCH 08/12] Update src/Docker.DotNet/Endpoints/IConfigsOperations.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- src/Docker.DotNet/Endpoints/IConfigsOperations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Docker.DotNet/Endpoints/IConfigsOperations.cs b/src/Docker.DotNet/Endpoints/IConfigsOperations.cs index 24c7a7178..fd3f670ef 100644 --- a/src/Docker.DotNet/Endpoints/IConfigsOperations.cs +++ b/src/Docker.DotNet/Endpoints/IConfigsOperations.cs @@ -5,7 +5,7 @@ namespace Docker.DotNet { - public interface IConfigsOperations + public interface IConfigOperations { /// /// List configs From 532f4e6c1e0e41999714b7d1d7850ebee1f4efed Mon Sep 17 00:00:00 2001 From: ACoderLife <37108462+ACoderLife@users.noreply.github.com> Date: Thu, 23 Mar 2023 22:15:17 +1100 Subject: [PATCH 09/12] Update src/Docker.DotNet/Endpoints/IConfigsOperations.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- src/Docker.DotNet/Endpoints/IConfigsOperations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Docker.DotNet/Endpoints/IConfigsOperations.cs b/src/Docker.DotNet/Endpoints/IConfigsOperations.cs index fd3f670ef..59c701726 100644 --- a/src/Docker.DotNet/Endpoints/IConfigsOperations.cs +++ b/src/Docker.DotNet/Endpoints/IConfigsOperations.cs @@ -14,7 +14,7 @@ public interface IConfigOperations /// 200 - No error. /// 500 - Server error. /// - Task> ListConfigAsync(CancellationToken cancellationToken = default(CancellationToken)); + Task> ListConfigsAsync(CancellationToken cancellationToken = default(CancellationToken)); /// /// Create a configs From e6b39934767730e1fab9af229657986f55c69388 Mon Sep 17 00:00:00 2001 From: ACoderLife <37108462+ACoderLife@users.noreply.github.com> Date: Thu, 23 Mar 2023 22:15:28 +1100 Subject: [PATCH 10/12] Update src/Docker.DotNet/Endpoints/ConfigsOperations.cs Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- src/Docker.DotNet/Endpoints/ConfigsOperations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs index 355a86792..be9bc47ae 100644 --- a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs +++ b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs @@ -7,7 +7,7 @@ namespace Docker.DotNet { - internal class ConfigsOperations : IConfigsOperations + internal class ConfigOperations : IConfigOperations { private readonly DockerClient _client; From 4f75b712c182e0adcd78e93587d98737bf83115c Mon Sep 17 00:00:00 2001 From: ACoderLife Date: Fri, 24 Mar 2023 11:09:59 +1100 Subject: [PATCH 11/12] Add Config Tests, some Renaming fixes --- src/Docker.DotNet/DockerClient.cs | 4 +- .../Endpoints/ConfigsOperations.cs | 10 +-- src/Docker.DotNet/IDockerClient.cs | 2 +- .../IConfigOperationsTests.cs | 70 +++++++++++++++++++ 4 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 test/Docker.DotNet.Tests/IConfigOperationsTests.cs diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 77eaf08d2..64a428f84 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -39,7 +39,7 @@ internal DockerClient(DockerClientConfiguration configuration, Version requested System = new SystemOperations(this); Networks = new NetworkOperations(this); Secrets = new SecretsOperations(this); - Configs = new ConfigsOperations(this); + Configs = new ConfigOperations(this); Swarm = new SwarmOperations(this); Tasks = new TasksOperations(this); Volumes = new VolumeOperations(this); @@ -137,7 +137,7 @@ await sock.ConnectAsync(new Microsoft.Net.Http.Client.UnixDomainSocketEndPoint(p public ISecretsOperations Secrets { get; } - public IConfigsOperations Configs { get; } + public IConfigOperations Configs { get; } public ISwarmOperations Swarm { get; } diff --git a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs index be9bc47ae..ec1182634 100644 --- a/src/Docker.DotNet/Endpoints/ConfigsOperations.cs +++ b/src/Docker.DotNet/Endpoints/ConfigsOperations.cs @@ -11,18 +11,18 @@ internal class ConfigOperations : IConfigOperations { private readonly DockerClient _client; - internal ConfigsOperations(DockerClient client) + internal ConfigOperations(DockerClient client) { this._client = client; } - async Task> IConfigsOperations.ListConfigAsync(CancellationToken cancellationToken) + 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 IConfigsOperations.CreateConfigAsync(SwarmCreateConfigParameters body, CancellationToken cancellationToken) + async Task IConfigOperations.CreateConfigAsync(SwarmCreateConfigParameters body, CancellationToken cancellationToken) { if (body == null) { @@ -34,7 +34,7 @@ async Task IConfigsOperations.CreateConfigAsync(Swarm return this._client.JsonSerializer.DeserializeObject(response.Body); } - async Task IConfigsOperations.InspectConfigAsync(string id, CancellationToken cancellationToken) + async Task IConfigOperations.InspectConfigAsync(string id, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(id)) { @@ -45,7 +45,7 @@ async Task IConfigsOperations.InspectConfigAsync(string id, Cancell return this._client.JsonSerializer.DeserializeObject(response.Body); } - Task IConfigsOperations.RemoveConfigAsync(string id, CancellationToken cancellationToken) + Task IConfigOperations.RemoveConfigAsync(string id, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(id)) { diff --git a/src/Docker.DotNet/IDockerClient.cs b/src/Docker.DotNet/IDockerClient.cs index cd03959a5..c42afc048 100644 --- a/src/Docker.DotNet/IDockerClient.cs +++ b/src/Docker.DotNet/IDockerClient.cs @@ -20,7 +20,7 @@ public interface IDockerClient : IDisposable ISecretsOperations Secrets { get; } - IConfigsOperations Configs { get; } + IConfigOperations Configs { get; } ISwarmOperations Swarm { 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)); + + + + } + } +} + From 4a7cdf717d8fa277259f722ccdf561207bb947e6 Mon Sep 17 00:00:00 2001 From: ACoderLife Date: Thu, 13 Apr 2023 14:30:07 +1000 Subject: [PATCH 12/12] updated test with retry and increased buffer. Improved cancelation handling. --- .../ISwarmOperationsTests.cs | 74 ++++++++++++++----- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs index f0a109946..196284bbb 100644 --- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs +++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs @@ -161,7 +161,8 @@ public async Task GetServices_Succeeds() [Fact] public async Task GetServiceLogs_Succeeds() { - using var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token); + 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 @@ -180,36 +181,73 @@ public async Task GetServiceLogs_Succeeds() ShowStderr = true }); - TimeSpan delay = TimeSpan.FromSeconds(10); - cts.CancelAfter(delay); + int maxRetries = 3; + int currentRetry = 0; + TimeSpan delayBetweenRetries = TimeSpan.FromSeconds(5); + List logLines = null; - var logLines = new List(); - while (!cts.IsCancellationRequested) + while (currentRetry < maxRetries && !linkedCts.IsCancellationRequested) { - var line = new List(); - var buffer = new byte[1]; + logLines = new List(); + TimeSpan delay = TimeSpan.FromSeconds(10); + cts.CancelAfter(delay); - while (!cts.IsCancellationRequested) + bool cancelRequested = false; // Add a flag to indicate cancellation + + while (!linkedCts.IsCancellationRequested && !cancelRequested) { - var res = await _stream.ReadOutputAsync(buffer, 0, 1, default); + var line = new List(); + var buffer = new byte[4096]; - if (res.Count == 0) + try { - continue; - } + 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)); + } + } - else if (buffer[0] == '\n') + logLines.Add(Encoding.UTF8.GetString(line.ToArray())); + } + catch (OperationCanceledException) { - break; + 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); } + } - else + if (logLines.Any() && logLines.First().Contains("[INF]")) + { + break; + } + else + { + currentRetry++; + if (currentRetry < maxRetries) { - line.Add(buffer[0]); + await Task.Delay(delayBetweenRetries); } } - - logLines.Add(Encoding.UTF8.GetString(line.ToArray())); } Assert.True(logLines.Any());