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 13d1c819e..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.ServiceUnavailable) + if (statusCode < HttpStatusCode.OK || statusCode >= HttpStatusCode.BadRequest) { - // TODO: Make typed error. - throw new Exception("Node is not part of a swarm."); + 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) @@ -46,21 +49,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 +63,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 +85,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 +97,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 +120,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 +140,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 +156,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..1f7ebcba9 100644 --- a/src/Docker.DotNet/Models/ServiceFilter.cs +++ b/src/Docker.DotNet/Models/ServiceFilter.cs @@ -10,29 +10,28 @@ 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 => this["mode"]?.ToList().Select(m => (ServiceCreationMode)Enum.Parse(typeof(ServiceCreationMode), m)).ToArray(); + set => this["mode"] = value?.Select(m => m.ToString()).ToArray(); } - public string Name + public string[] Name { get => this["name"]; set => this["name"] = value; } - } public enum ServiceCreationMode diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs index 6bfabe567..d9adf22bf 100644 --- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs +++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs @@ -2,30 +2,73 @@ 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..917a1916c 100644 --- a/test/Docker.DotNet.Tests/QueryStringTests.cs +++ b/test/Docker.DotNet.Tests/QueryStringTests.cs @@ -9,18 +9,36 @@ 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())); + } + + [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