From 9ab39760aa49fdea2b2194a377b8d30a5a073e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Lindstr=C3=B6m?= Date: Sun, 18 Nov 2018 12:07:45 +0100 Subject: [PATCH 1/4] Services filter fixed. And service operations now throws dockerapiexception on error --- .../Endpoints/SwarmOperations.cs | 107 +++++++----------- src/Docker.DotNet/Models/ServiceFilter.cs | 29 +++-- .../ISwarmOperationsTests.cs | 51 ++++++++- .../ISystemOperations.Tests.cs | 2 +- test/Docker.DotNet.Tests/QueryStringTests.cs | 8 +- 5 files changed, 119 insertions(+), 78 deletions(-) diff --git a/src/Docker.DotNet/Endpoints/SwarmOperations.cs b/src/Docker.DotNet/Endpoints/SwarmOperations.cs index 13d1c819e..924fbc830 100644 --- a/src/Docker.DotNet/Endpoints/SwarmOperations.cs +++ b/src/Docker.DotNet/Endpoints/SwarmOperations.cs @@ -13,10 +13,9 @@ internal class SwarmOperations : ISwarmOperations { internal static readonly ApiResponseErrorHandlingDelegate SwarmResponseHandler = (statusCode, responseBody) => { - if (statusCode == HttpStatusCode.ServiceUnavailable) + if (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.BadRequest) { - // TODO: Make typed error. - throw new Exception("Node is not part of a swarm."); + throw new DockerApiException(statusCode, responseBody); } }; @@ -46,21 +45,11 @@ async Task ISwarmOperations.InitSwarmAsync(SwarmInitParameters parameter { 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."); - } - } - }, + new[] { SwarmResponseHandler }, HttpMethod.Post, "swarm/init", - null, - data, + null, + data, cancellationToken).ConfigureAwait(false); return response.Body; @@ -70,13 +59,21 @@ async Task ISwarmOperations.InspectServiceAsync(string id, Cancell { if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); - var response = await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Get, $"services/{id}", cancellationToken).ConfigureAwait(false); + 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); + var response = await this._client.MakeRequestAsync( + new[] { SwarmResponseHandler }, + HttpMethod.Get, + "swarm", + cancellationToken).ConfigureAwait(false); return this._client.JsonSerializer.DeserializeObject(response.Body); } @@ -84,21 +81,11 @@ async Task ISwarmOperations.JoinSwarmAsync(SwarmJoinParameters parameters, Cance { 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, + new[] { SwarmResponseHandler }, + HttpMethod.Post, + "swarm/join", + null, + data, cancellationToken).ConfigureAwait(false); } @@ -106,29 +93,22 @@ async Task ISwarmOperations.LeaveSwarmAsync(SwarmLeaveParameters parameters, Can { 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, + new[] { SwarmResponseHandler }, + 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); + var response = await this._client.MakeRequestAsync( + new[] { SwarmResponseHandler }, + HttpMethod.Get, + $"services", + queryParameters, + cancellationToken).ConfigureAwait(false); return this._client.JsonSerializer.DeserializeObject(response.Body); } @@ -136,7 +116,11 @@ async Task ISwarmOperations.RemoveServiceAsync(string id, CancellationToken canc { if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); - await this._client.MakeRequestAsync(new[] { SwarmResponseHandler }, HttpMethod.Delete, $"services/{id}", cancellationToken).ConfigureAwait(false); + await this._client.MakeRequestAsync( + new[] { SwarmResponseHandler }, + HttpMethod.Delete, + $"services/{id}", + cancellationToken).ConfigureAwait(false); } async Task ISwarmOperations.UnlockSwarmAsync(SwarmUnlockParameters parameters, CancellationToken cancellationToken) @@ -152,7 +136,14 @@ async Task ISwarmOperations.UpdateServiceAsync(string id, 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); + 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); } @@ -161,17 +152,7 @@ async Task ISwarmOperations.UpdateSwarmAsync(SwarmUpdateParameters parameters, C 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."); - } - } - }, + new[] { SwarmResponseHandler }, HttpMethod.Post, "swarm/update", query, diff --git a/src/Docker.DotNet/Models/ServiceFilter.cs b/src/Docker.DotNet/Models/ServiceFilter.cs index 58b374048..846a7b151 100644 --- a/src/Docker.DotNet/Models/ServiceFilter.cs +++ b/src/Docker.DotNet/Models/ServiceFilter.cs @@ -10,24 +10,39 @@ public class ServicesListParameters public ServiceFilter Filters { get; set; } } - public class ServiceFilter : Dictionary + public class ServiceFilter : Dictionary { - public string Id + public string[] Id { get => this["id"]; set => this["id"] = value; } - public string Label + public string[] Label { get => this["label"]; set => this["label"] = value; } - public ServiceCreationMode Mode + public ServiceCreationMode[] Mode { - get => !Enum.TryParse(this["mode"], out ServiceCreationMode mode) ? ServiceCreationMode.Replicated : mode; - set => this["mode"] = value.ToString(); + get + { + if (this["mode"].Length == 0) + { + return new ServiceCreationMode[] { ServiceCreationMode.Replicated }; + } + else + { + var lst = new ServiceCreationMode[this["mode"].Length]; + for(int i = 0; i< this["mode"].Length; i++) + { + lst[i] = (ServiceCreationMode)Enum.Parse(typeof(ServiceCreationMode), this["mode"][i]); + } + return lst; + } + } + set => this["mode"] = value.Select(m => m.ToString()).ToArray(); } - public string Name + public string[] Name { get => this["name"]; set => this["name"] = value; diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs index 6bfabe567..f43c0381a 100644 --- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs +++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs @@ -2,30 +2,75 @@ using System.Threading; using System.Threading.Tasks; using Docker.DotNet.Models; +using System.Collections.Generic; using Xunit; +using System; namespace Docker.DotNet.Tests { - public class ISwarmOperationsTests + public class ISwarmOperationsTests :IDisposable { private readonly DockerClient _client; + private readonly string _testServiceId; + private static string _testServiceName = "docker-dotnet-test-service"; public ISwarmOperationsTests() { _client = new DockerClientConfiguration().CreateClient(); + + _testServiceId = _client.Swarm.CreateServiceAsync(new ServiceCreateParameters() + { + + Service = new ServiceSpec + { + Name = _testServiceName, + TaskTemplate = new TaskSpec() + { + ContainerSpec = new ContainerSpec() + { + Image = "nginx:latest" + } + } + } + }).Result.ID; } [Fact] public async Task GetServicesAsync_Succeeds() { var services = await _client.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None); - Assert.Equal(2, services.Count()); + Assert.Contains(_testServiceId, services.Select(s => s.ID)); } [Fact] public async Task GetFilteredServicesAsync_Succeeds() { - var services = await _client.Swarm.ListServicesAsync(new ServicesListParameters { Filters = new ServiceFilter { Id = "pr6264hhb836" } }, CancellationToken.None); + var services = await _client.Swarm.ListServicesAsync(new ServicesListParameters { Filters = new ServiceFilter { Name = new string[] { _testServiceName } } }, CancellationToken.None); Assert.Single(services); } + [Fact] + public async Task CreateServiceAsync_FaultyNetwork_Throws() + { + await Assert.ThrowsAsync(() => _client.Swarm.CreateServiceAsync(new ServiceCreateParameters() + { + + Service = new ServiceSpec + { + Name = $"{_testServiceName}2", + TaskTemplate = new TaskSpec() + { + ContainerSpec = new ContainerSpec() + { + Image = "nginx:latest" + } + }, + Networks = new List() { new NetworkAttachmentConfig() { Target = "non-existing-network" } } + } + })); + } + + public void Dispose() + { + _client.Swarm.RemoveServiceAsync(_testServiceId, CancellationToken.None); + } } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs index a36061167..b0993be2d 100644 --- a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs +++ b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs @@ -21,7 +21,7 @@ public ISystemOperationsTests() public void DockerService_IsRunning() { var services = ServiceController.GetServices(); - using (var dockerService = services.SingleOrDefault(service => service.ServiceName == "docker")) + using (var dockerService = services.SingleOrDefault(service => service.ServiceName == "docker" || service.ServiceName == "com.docker.service")) { Assert.NotNull(dockerService); // docker is not running Assert.Equal(ServiceControllerStatus.Running, dockerService.Status); diff --git a/test/Docker.DotNet.Tests/QueryStringTests.cs b/test/Docker.DotNet.Tests/QueryStringTests.cs index 0b067d332..cb6074e17 100644 --- a/test/Docker.DotNet.Tests/QueryStringTests.cs +++ b/test/Docker.DotNet.Tests/QueryStringTests.cs @@ -9,18 +9,18 @@ public class QueryStringTests [Fact] public void ServicesListParameters_GenerateIdFilters() { - var p = new ServicesListParameters { Filters = new ServiceFilter { Id = "service-id" } }; + var p = new ServicesListParameters { Filters = new ServiceFilter { Id = new string[]{ "service-id" } } }; var qs = new QueryString(p); - Assert.Equal("filters={\"id\":\"service-id\"}", Uri.UnescapeDataString(qs.GetQueryString())); + Assert.Equal("filters={\"id\":[\"service-id\"]}", Uri.UnescapeDataString(qs.GetQueryString())); } [Fact] public void ServicesListParameters_GenerateCompositeFilters() { - var p = new ServicesListParameters { Filters = new ServiceFilter { Id = "service-id", Label = "label" } }; + var p = new ServicesListParameters { Filters = new ServiceFilter { Id = new string[] { "service-id" }, Label = new string[] { "label" } } }; var qs = new QueryString(p); - Assert.Equal("filters={\"id\":\"service-id\",\"label\":\"label\"}", Uri.UnescapeDataString(qs.GetQueryString())); + Assert.Equal("filters={\"id\":[\"service-id\"],\"label\":[\"label\"]}", Uri.UnescapeDataString(qs.GetQueryString())); } } } \ No newline at end of file From aa3fb081d6841cf3537f923844ecf4d1f970bca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Lindstr=C3=B6m?= Date: Mon, 19 Nov 2018 23:12:09 +0100 Subject: [PATCH 2/4] Null servicecreateparameters test. Newlines removed. Servicefilter mode null as default. New dockerswarmexception added --- .../Endpoints/DockerSwarmException.cs | 14 ++++++++++++++ src/Docker.DotNet/Endpoints/SwarmOperations.cs | 3 ++- src/Docker.DotNet/Models/ServiceFilter.cs | 15 ++++----------- .../ISwarmOperationsTests.cs | 4 +--- test/Docker.DotNet.Tests/QueryStringTests.cs | 18 ++++++++++++++++++ 5 files changed, 39 insertions(+), 15 deletions(-) create mode 100644 src/Docker.DotNet/Endpoints/DockerSwarmException.cs diff --git a/src/Docker.DotNet/Endpoints/DockerSwarmException.cs b/src/Docker.DotNet/Endpoints/DockerSwarmException.cs new file mode 100644 index 000000000..41bb5625c --- /dev/null +++ b/src/Docker.DotNet/Endpoints/DockerSwarmException.cs @@ -0,0 +1,14 @@ +using System.Net; + +namespace Docker.DotNet +{ + public class DockerSwarmException : DockerApiException + { + public string DeserializedMessage { get; private set; } + + public DockerSwarmException(HttpStatusCode statusCode, string responseBody, string deserializedMessage) : base(statusCode, responseBody) + { + DeserializedMessage = deserializedMessage; + } + } +} diff --git a/src/Docker.DotNet/Endpoints/SwarmOperations.cs b/src/Docker.DotNet/Endpoints/SwarmOperations.cs index 924fbc830..68a813e4a 100644 --- a/src/Docker.DotNet/Endpoints/SwarmOperations.cs +++ b/src/Docker.DotNet/Endpoints/SwarmOperations.cs @@ -15,7 +15,8 @@ internal class SwarmOperations : ISwarmOperations { if (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.BadRequest) { - throw new DockerApiException(statusCode, responseBody); + var deserializedBody = new JsonSerializer().DeserializeObject(responseBody); + throw new DockerSwarmException(statusCode, responseBody, deserializedBody.Message); } }; diff --git a/src/Docker.DotNet/Models/ServiceFilter.cs b/src/Docker.DotNet/Models/ServiceFilter.cs index 846a7b151..13f64f1f2 100644 --- a/src/Docker.DotNet/Models/ServiceFilter.cs +++ b/src/Docker.DotNet/Models/ServiceFilter.cs @@ -26,19 +26,12 @@ public ServiceCreationMode[] Mode { get { - if (this["mode"].Length == 0) + var lst = new ServiceCreationMode[this["mode"].Length]; + for(int i = 0; i< this["mode"].Length; i++) { - return new ServiceCreationMode[] { ServiceCreationMode.Replicated }; - } - else - { - var lst = new ServiceCreationMode[this["mode"].Length]; - for(int i = 0; i< this["mode"].Length; i++) - { - lst[i] = (ServiceCreationMode)Enum.Parse(typeof(ServiceCreationMode), this["mode"][i]); - } - return lst; + lst[i] = (ServiceCreationMode)Enum.Parse(typeof(ServiceCreationMode), this["mode"][i]); } + return lst; } set => this["mode"] = value.Select(m => m.ToString()).ToArray(); } diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs index f43c0381a..d9adf22bf 100644 --- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs +++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs @@ -20,7 +20,6 @@ public ISwarmOperationsTests() _testServiceId = _client.Swarm.CreateServiceAsync(new ServiceCreateParameters() { - Service = new ServiceSpec { Name = _testServiceName, @@ -50,9 +49,8 @@ public async Task GetFilteredServicesAsync_Succeeds() [Fact] public async Task CreateServiceAsync_FaultyNetwork_Throws() { - await Assert.ThrowsAsync(() => _client.Swarm.CreateServiceAsync(new ServiceCreateParameters() + await Assert.ThrowsAsync(() => _client.Swarm.CreateServiceAsync(new ServiceCreateParameters() { - Service = new ServiceSpec { Name = $"{_testServiceName}2", diff --git a/test/Docker.DotNet.Tests/QueryStringTests.cs b/test/Docker.DotNet.Tests/QueryStringTests.cs index cb6074e17..917a1916c 100644 --- a/test/Docker.DotNet.Tests/QueryStringTests.cs +++ b/test/Docker.DotNet.Tests/QueryStringTests.cs @@ -22,5 +22,23 @@ public void ServicesListParameters_GenerateCompositeFilters() Assert.Equal("filters={\"id\":[\"service-id\"],\"label\":[\"label\"]}", Uri.UnescapeDataString(qs.GetQueryString())); } + + [Fact] + public void ServicesListParameters_GenerateNullFilters() + { + var p = new ServicesListParameters { Filters = new ServiceFilter() }; + var qs = new QueryString(p); + Assert.Equal("filters={}", Uri.UnescapeDataString(qs.GetQueryString())); + } + + [Fact] + public void ServicesListParameters_GenerateNullModeFilters() + { + var p = new ServicesListParameters { Filters = new ServiceFilter() { Mode = new ServiceCreationMode[] { } } }; + var qs = new QueryString(p); + var tmp = qs.GetQueryString(); + var tmp2 = Uri.UnescapeDataString(tmp); + Assert.Equal("filters={\"mode\":[]}", tmp2); + } } } \ No newline at end of file From 626ff9a475802989f0faa3c404170b444b459047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Lindstr=C3=B6m?= Date: Thu, 22 Nov 2018 22:08:36 +0100 Subject: [PATCH 3/4] null check, and simplifications --- src/Docker.DotNet/Models/ServiceFilter.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Docker.DotNet/Models/ServiceFilter.cs b/src/Docker.DotNet/Models/ServiceFilter.cs index 13f64f1f2..1f7ebcba9 100644 --- a/src/Docker.DotNet/Models/ServiceFilter.cs +++ b/src/Docker.DotNet/Models/ServiceFilter.cs @@ -24,23 +24,14 @@ public string[] Label } public ServiceCreationMode[] Mode { - get - { - var lst = new ServiceCreationMode[this["mode"].Length]; - for(int i = 0; i< this["mode"].Length; i++) - { - lst[i] = (ServiceCreationMode)Enum.Parse(typeof(ServiceCreationMode), this["mode"][i]); - } - return lst; - } - set => this["mode"] = value.Select(m => m.ToString()).ToArray(); + get => this["mode"]?.ToList().Select(m => (ServiceCreationMode)Enum.Parse(typeof(ServiceCreationMode), m)).ToArray(); + set => this["mode"] = value?.Select(m => m.ToString()).ToArray(); } public string[] Name { get => this["name"]; set => this["name"] = value; } - } public enum ServiceCreationMode From ead04272d140510571b2837bff6140cdbb0e8ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Lindstr=C3=B6m?= Date: Sat, 24 Nov 2018 16:34:05 +0100 Subject: [PATCH 4/4] Now using jsonserializer of instance for custom error handler in SwarmOperations. --- src/Docker.DotNet/Endpoints/SwarmOperations.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Docker.DotNet/Endpoints/SwarmOperations.cs b/src/Docker.DotNet/Endpoints/SwarmOperations.cs index 68a813e4a..320bced26 100644 --- a/src/Docker.DotNet/Endpoints/SwarmOperations.cs +++ b/src/Docker.DotNet/Endpoints/SwarmOperations.cs @@ -11,20 +11,23 @@ namespace Docker.DotNet internal class SwarmOperations : ISwarmOperations { - internal static readonly ApiResponseErrorHandlingDelegate SwarmResponseHandler = (statusCode, responseBody) => + internal readonly ApiResponseErrorHandlingDelegate SwarmResponseHandler; + + private readonly DockerClient _client; + + internal void ErrorHandler(HttpStatusCode statusCode, string responseBody) { if (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.BadRequest) { - var deserializedBody = new JsonSerializer().DeserializeObject(responseBody); + var deserializedBody = this._client.JsonSerializer.DeserializeObject(responseBody); throw new DockerSwarmException(statusCode, responseBody, deserializedBody.Message); } - }; - - private readonly DockerClient _client; + } internal SwarmOperations(DockerClient client) { this._client = client; + SwarmResponseHandler = new ApiResponseErrorHandlingDelegate(ErrorHandler); } async Task ISwarmOperations.CreateServiceAsync(ServiceCreateParameters parameters, CancellationToken cancellationToken)