From 296a2885b4478b14611697866d304f38f5d8c571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=20Ts=C5=8Dnto?= Date: Thu, 26 Aug 2021 18:38:03 -0500 Subject: [PATCH 1/3] WIP --- DockerSdk/DockerClient.cs | 7 ++ DockerSdk/Volumes/IVolume.cs | 32 ++++++ DockerSdk/Volumes/ListVolumesOptions.cs | 6 ++ DockerSdk/Volumes/Volume.cs | 21 ++++ DockerSdk/Volumes/VolumeAccess.cs | 104 +++++++++++++++++++ DockerSdk/Volumes/VolumeFactory.cs | 50 +++++++++ DockerSdk/Volumes/VolumeInfo.cs | 19 ++++ DockerSdk/Volumes/VolumeName.cs | 60 +++++++++++ DockerSdk/Volumes/VolumeNotFoundException.cs | 52 ++++++++++ 9 files changed, 351 insertions(+) create mode 100644 DockerSdk/Volumes/IVolume.cs create mode 100644 DockerSdk/Volumes/ListVolumesOptions.cs create mode 100644 DockerSdk/Volumes/Volume.cs create mode 100644 DockerSdk/Volumes/VolumeAccess.cs create mode 100644 DockerSdk/Volumes/VolumeFactory.cs create mode 100644 DockerSdk/Volumes/VolumeInfo.cs create mode 100644 DockerSdk/Volumes/VolumeName.cs create mode 100644 DockerSdk/Volumes/VolumeNotFoundException.cs diff --git a/DockerSdk/DockerClient.cs b/DockerSdk/DockerClient.cs index 40ae15a..b923593 100644 --- a/DockerSdk/DockerClient.cs +++ b/DockerSdk/DockerClient.cs @@ -9,6 +9,7 @@ using DockerSdk.Events; using DockerSdk.Images; using DockerSdk.Registries; +using DockerSdk.Volumes; using NetworkAccess = DockerSdk.Networks.NetworkAccess; using Version = System.Version; @@ -30,6 +31,7 @@ private DockerClient(Comm core, ClientOptions options, Version negotiatedApiVers Images = new ImageAccess(this); Networks = new NetworkAccess(this); Registries = new RegistryAccess(this); + Volumes = new VolumeAccess(this); } /// @@ -58,6 +60,11 @@ private DockerClient(Comm core, ClientOptions options, Version negotiatedApiVers /// public RegistryAccess Registries { get; } + /// + /// Provides access to functionality related to Docker volumes. + /// + public VolumeAccess Volumes { get; } + /// /// Gets the core client, which is what does all the heavy lifting for communicating with the Docker daemon. /// diff --git a/DockerSdk/Volumes/IVolume.cs b/DockerSdk/Volumes/IVolume.cs new file mode 100644 index 0000000..6babd08 --- /dev/null +++ b/DockerSdk/Volumes/IVolume.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace DockerSdk.Volumes +{ + public interface IVolume + { + VolumeName Name { get; } + + Task GetDetailsAsync(CancellationToken ct = default); + } + + public interface IVolumeInfo : IVolume + { + DateTimeOffset CreationTime { get; } + + string Driver { get; } + + /// + /// Gets the labels that have been applied to the volume. + /// + IReadOnlyDictionary Labels { get; } + + string Mountpoint { get; } + + string Scope { get; } + + // TODO: Options + } +} diff --git a/DockerSdk/Volumes/ListVolumesOptions.cs b/DockerSdk/Volumes/ListVolumesOptions.cs new file mode 100644 index 0000000..6a5a5b9 --- /dev/null +++ b/DockerSdk/Volumes/ListVolumesOptions.cs @@ -0,0 +1,6 @@ +namespace DockerSdk.Volumes +{ + public class ListVolumesOptions + { + } +} \ No newline at end of file diff --git a/DockerSdk/Volumes/Volume.cs b/DockerSdk/Volumes/Volume.cs new file mode 100644 index 0000000..3481e69 --- /dev/null +++ b/DockerSdk/Volumes/Volume.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace DockerSdk.Volumes +{ + internal class Volume : IVolume + { + protected DockerClient client; + + public Volume(DockerClient client, VolumeName name) + { + this.client = client; + Name = name; + } + + public VolumeName Name { get; } + + public Task GetDetailsAsync(CancellationToken ct = default) + => VolumeFactory.LoadInfoAsync(client, Name, ct); + } +} diff --git a/DockerSdk/Volumes/VolumeAccess.cs b/DockerSdk/Volumes/VolumeAccess.cs new file mode 100644 index 0000000..f4d6ff8 --- /dev/null +++ b/DockerSdk/Volumes/VolumeAccess.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace DockerSdk.Volumes +{ + /// + /// Provides methods for interacting with Docker networks. + /// + public class VolumeAccess + { + internal VolumeAccess(DockerClient dockerClient) + { + client = dockerClient; + } + + private readonly DockerClient client; + + /// + /// Loads an object that can be used to interact with the indicated volume. + /// + /// A volume name. + /// A token used to cancel the operation. + /// A that resolves to the volume object. + /// No such network exists. + /// + /// The request failed due to an underlying issue such as loss of network connectivity. + /// + /// The volume reference is improperly formatted. + /// The input is null. + public Task GetAsync(string volume, CancellationToken ct = default) + => GetAsync(VolumeName.Parse(volume), ct); + + /// + /// Loads an object that can be used to interact with the indicated volume. + /// + /// A volume name. + /// A token used to cancel the operation. + /// A that resolves to the volume object. + /// No such network exists. + /// + /// The request failed due to an underlying issue such as loss of network connectivity. + /// + /// The input is null. + public Task GetAsync(VolumeName volume, CancellationToken ct = default) + { + if (volume is null) + throw new ArgumentNullException(nameof(volume)); + + return VolumeFactory.LoadAsync(client, volume, ct); + } + + /// + /// Loads detailed information about a volume. + /// + /// A volume name. + /// A token used to cancel the operation. + /// A that resolves to the details. + /// No such volume exists. + /// + /// The request failed due to an underlying issue such as network connectivity. + /// + /// The volume reference is improperly formatted. + public Task GetDetailsAsync(string volume, CancellationToken ct = default) + => GetDetailsAsync(VolumeName.Parse(volume), ct); + + /// + /// Loads detailed information about a volume. + /// + /// A volume name. + /// A token used to cancel the operation. + /// A that resolves to the details. + /// No such volume exists. + /// + /// The request failed due to an underlying issue such as network connectivity. + /// + public Task GetDetailsAsync(VolumeName volume, CancellationToken ct = default) + => VolumeFactory.LoadInfoAsync(client, volume, ct); + + /// + /// Gets a list of Docker volumes known to the daemon. + /// + /// A token used to cancel the operation. + /// A that resolves to the list of volumes. + /// The sequence of the results is undefined. + public async Task> ListAsync(CancellationToken ct = default) + { + CoreModels.VolumesListResponse response; + try + { + response = await client.Core.Volumes.ListAsync(ct).ConfigureAwait(false); + } + catch (Core.DockerApiException ex) + { + throw DockerException.Wrap(ex); + } + + return response.Volumes.Select(raw => new Volume(client, new VolumeName(raw.Name))).ToArray(); + } + } +} diff --git a/DockerSdk/Volumes/VolumeFactory.cs b/DockerSdk/Volumes/VolumeFactory.cs new file mode 100644 index 0000000..9460999 --- /dev/null +++ b/DockerSdk/Volumes/VolumeFactory.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; + +namespace DockerSdk.Volumes +{ + internal static class VolumeFactory + { + internal static Task LoadAsync(DockerClient client, VolumeName name, CancellationToken ct) + { + if (name is null) + throw new ArgumentNullException(nameof(name)); + + IVolume volume = new Volume(client, name); + return Task.FromResult(volume); + } + + internal static async Task LoadInfoAsync(DockerClient client, VolumeName name, CancellationToken ct) + { + if (name is null) + throw new ArgumentNullException(nameof(name)); + + var raw = await LoadCoreAsync(client, name, ct).ConfigureAwait(false); + + return new VolumeInfo(client, name) + { + CreationTime = DateTimeOffset.Parse(raw.CreatedAt), + Driver = raw.Driver, + Labels = raw.Labels?.ToImmutableDictionary() ?? ImmutableDictionary.Create(), + Mountpoint = raw.Mountpoint, + Scope = raw.Scope, + }; + } + + private static async Task LoadCoreAsync(DockerClient client, VolumeName name, CancellationToken ct) + { + try + { + return await client.Core.Volumes.InspectAsync(name, ct).ConfigureAwait(false); + } + catch (Core.DockerApiException ex) + { + if (VolumeNotFoundException.TryWrap(ex, name, out var wrapped)) + throw wrapped; + throw DockerException.Wrap(ex); + } + } + } +} diff --git a/DockerSdk/Volumes/VolumeInfo.cs b/DockerSdk/Volumes/VolumeInfo.cs new file mode 100644 index 0000000..f18bbfe --- /dev/null +++ b/DockerSdk/Volumes/VolumeInfo.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace DockerSdk.Volumes +{ + internal class VolumeInfo : Volume, IVolumeInfo + { + public VolumeInfo(DockerClient client, VolumeName name) + : base(client, name) + { } + + public DateTimeOffset CreationTime { get; init; } + public string Driver { get; init; } = null!; + public IReadOnlyDictionary Labels { get; init; } = ImmutableDictionary.Empty; + public string Mountpoint { get; init; } = null!; + public string Scope { get; init; } = null!; + } +} diff --git a/DockerSdk/Volumes/VolumeName.cs b/DockerSdk/Volumes/VolumeName.cs new file mode 100644 index 0000000..5bbcee6 --- /dev/null +++ b/DockerSdk/Volumes/VolumeName.cs @@ -0,0 +1,60 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace DockerSdk.Volumes +{ + /// + /// Represents the name of a Docker volume. + /// + public class VolumeName + { + /// + /// The full text of the reference. + /// + protected readonly string value; + + internal VolumeName(string value) + => this.value = value; + + /// + /// Lets the reference be implicitly cast to a string. + /// + /// + public static implicit operator string(VolumeName reference) + => reference.ToString(); + + /// + public override string ToString() => value; + + /// + /// Parses the input as a Docker volume name. + /// + /// The text to parse. + /// The reference. + /// + /// The input is not a validly-formatted volume reference. + /// + internal static VolumeName Parse(string input) + { + if (input is null) + throw new ArgumentNullException(nameof(input)); + if (TryParse(input, out var reference)) + return reference; + throw new MalformedReferenceException($"\"{input}\" is not a valid Docker volume name."); + } + + /// + /// Tries to parse the input as a Docker volume name. + /// + /// The text to parse. + /// The reference, or null if parsing failed. + /// True if parsing succeeded; false otherwise. + /// is null or empty. + public static bool TryParse(string input, [NotNullWhen(returnValue: true)] out VolumeName? reference) + { + // TODO: are there any limits here? + reference = new VolumeName(input); + return true; + } + } +} diff --git a/DockerSdk/Volumes/VolumeNotFoundException.cs b/DockerSdk/Volumes/VolumeNotFoundException.cs new file mode 100644 index 0000000..9538778 --- /dev/null +++ b/DockerSdk/Volumes/VolumeNotFoundException.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Docker.DotNet; +using Core = Docker.DotNet; + +namespace DockerSdk.Volumes +{ + /// + /// Indicates that the Docker daemon could not find the indicated volume where it was looking. + /// + [Serializable] + public abstract class VolumeNotFoundException : ResourceNotFoundException + { + /// + /// Creates an instance of the class. + /// + public VolumeNotFoundException() + { + } + + /// + /// Creates an instance of the class. + /// + /// + public VolumeNotFoundException(string message) : base(message) + { + } + + /// + /// Creates an instance of the class. + /// + /// + /// + public VolumeNotFoundException(string message, Exception inner) : base(message, inner) + { + } + + internal static bool TryWrap(DockerApiException ex, VolumeName name, out VolumeNotFoundException wrapped) + { + throw new NotImplementedException(); + } + + /// + /// Creates an instance of the class. + /// + /// + /// + protected VolumeNotFoundException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} From 5cdac06ce0d235146013b2b57fcba4c249abf363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=20Ts=C5=8Dnto?= Date: Fri, 3 Sep 2021 21:30:59 -0500 Subject: [PATCH 2/3] WIP --- DockerSdk.Tests/VolumeAccessTests.cs | 56 +++++++++++++++++ DockerSdk.Tests/scripts/clean.ps1 | 6 ++ DockerSdk.Tests/scripts/definitions.ps1 | 8 +++ DockerSdk.Tests/scripts/up.ps1 | 28 +++++++++ .../JsonConverters/VolumeScopeConverter.cs | 32 ++++++++++ DockerSdk/RequestBuilder.cs | 5 ++ DockerSdk/Volumes/Dto/ListResponse.cs | 15 +++++ DockerSdk/Volumes/Dto/UsageData.cs | 22 +++++++ DockerSdk/Volumes/Dto/VolumeResponse.cs | 54 ++++++++++++++++ DockerSdk/Volumes/IVolume.cs | 2 +- DockerSdk/Volumes/ListVolumesOptions.cs | 63 ++++++++++++++++++- DockerSdk/Volumes/VolumeAccess.cs | 27 +++++--- DockerSdk/Volumes/VolumeFactory.cs | 22 +++---- DockerSdk/Volumes/VolumeInfo.cs | 2 +- DockerSdk/Volumes/VolumeNotFoundException.cs | 10 +-- DockerSdk/Volumes/VolumeScope.cs | 12 ++++ 16 files changed, 328 insertions(+), 36 deletions(-) create mode 100644 DockerSdk.Tests/VolumeAccessTests.cs create mode 100644 DockerSdk/JsonConverters/VolumeScopeConverter.cs create mode 100644 DockerSdk/Volumes/Dto/ListResponse.cs create mode 100644 DockerSdk/Volumes/Dto/UsageData.cs create mode 100644 DockerSdk/Volumes/Dto/VolumeResponse.cs create mode 100644 DockerSdk/Volumes/VolumeScope.cs diff --git a/DockerSdk.Tests/VolumeAccessTests.cs b/DockerSdk.Tests/VolumeAccessTests.cs new file mode 100644 index 0000000..64edc80 --- /dev/null +++ b/DockerSdk.Tests/VolumeAccessTests.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using DockerSdk.Volumes; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace DockerSdk.Tests +{ + [Collection("Common")] + public class VolumeAccessTests + { + public VolumeAccessTests(ITestOutputHelper toh) + { + this.toh = toh; + } + + private readonly ITestOutputHelper toh; + + [Fact] + public async Task GetDetailsAsync_NoSuchVolume_ThrowsVolumeNotFoundException() + { + using var client = await DockerClient.StartAsync(); + + await Assert.ThrowsAnyAsync( + () => client.Volumes.GetDetailsAsync("ddnt-no-such-volume-366ad733152b70e53ddd7fd59defe9fa2e055ed2090f5f3a8839b2797388d0b4")); + } + + [Fact] + public async Task GetDetailsAsync_LocalVolumeExists_GetsExpectedDetails() + { + using var client = await DockerClient.StartAsync(); + + var volume = await client.Volumes.GetDetailsAsync("ddnt-general"); + + volume.Should().NotBeNull(); + volume.CreationTime.Should().BeAfter(DateTimeOffset.Parse("2020-1-1")); + volume.Driver.Should().Be("local"); + volume.Labels["ddnt1"].Should().Be("alef"); + volume.Labels["ddnt2"].Should().Be("beth"); + volume.Mountpoint.Should().NotBeNullOrWhiteSpace(); + volume.Name.ToString().Should().Be("ddnt-general"); + volume.Scope.Should().Be(VolumeScope.Local); + } + + [Fact] + public async Task ListAsync_NoFilters_FindsPremadeVolumes() + { + using var client = await DockerClient.StartAsync(); + + var volumes = await client.Volumes.ListAsync(); + + volumes.Should().ContainSingle(v => v.Name.ToString() == "ddnt-general"); + } + } +} diff --git a/DockerSdk.Tests/scripts/clean.ps1 b/DockerSdk.Tests/scripts/clean.ps1 index 89b294a..c50f775 100644 --- a/DockerSdk.Tests/scripts/clean.ps1 +++ b/DockerSdk.Tests/scripts/clean.ps1 @@ -25,6 +25,12 @@ foreach ($id in docker network ls --filter="name=ddnt" --quiet --no-trunc) docker network rm $id } +# Remove volumes. +foreach ($id in docker volume ls --filter="name=ddnt" --quiet) +{ + docker volume rm $id +} + # Remove images in reverse order of how they were created. $tags = $imageDefinitions.Name $tags = [System.Linq.Enumerable]::Reverse([string[]] $tags) diff --git a/DockerSdk.Tests/scripts/definitions.ps1 b/DockerSdk.Tests/scripts/definitions.ps1 index 279a2c8..255033c 100644 --- a/DockerSdk.Tests/scripts/definitions.ps1 +++ b/DockerSdk.Tests/scripts/definitions.ps1 @@ -39,6 +39,14 @@ $networkDefinitions = @( } ) +# Defines the volumes to build. +$volumeDefinitions = @( + @{ + name = 'general' + args = '--label ddnt1=alef --label ddnt2=beth' + } +) + # Defines the containers to start. If the image is not specified, it defaults to the name. # It's intentional that error-out immediately fails and that some other containers immediately run to completion. $containerDefinitions = @( diff --git a/DockerSdk.Tests/scripts/up.ps1 b/DockerSdk.Tests/scripts/up.ps1 index b7746ee..f37b223 100644 --- a/DockerSdk.Tests/scripts/up.ps1 +++ b/DockerSdk.Tests/scripts/up.ps1 @@ -25,6 +25,14 @@ function VerifySetup() } } + foreach ($name in $volumeDefinitions.Name) + { + if (!(VerifyVolume $name)) + { + return $false + } + } + foreach ($name in $imageDefinitions.Name) { if (!(VerifyImage $name)) @@ -88,6 +96,18 @@ function VerifyNetwork($name) return $true } +function VerifyVolume($name) +{ + $dockerId = docker volume ls --filter=name=ddnt-$name --quiet + if (!$dockerId) + { + # Setup hasn't been run, or someone has deleted the volume. Clean and rebuild. + return $false + } + + return $true +} + function VerifyImage($name) { $path = "$name/image.id" @@ -135,6 +155,14 @@ foreach ($entry in $imageDefinitions) Pop-Location } +# Create the volumes. +foreach ($entry in $volumeDefinitions) +{ + $name = $entry.Name + $args = $entry['Args'] ?? '' + Invoke-Expression "docker volume create $args ddnt-$name" +} + # Create the networks. foreach ($entry in $networkDefinitions) { diff --git a/DockerSdk/JsonConverters/VolumeScopeConverter.cs b/DockerSdk/JsonConverters/VolumeScopeConverter.cs new file mode 100644 index 0000000..8d0cb8c --- /dev/null +++ b/DockerSdk/JsonConverters/VolumeScopeConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using DockerSdk.Volumes; + +namespace DockerSdk.JsonConverters +{ + internal class VolumeScopeConverter : JsonConverter + { + public override VolumeScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var name = reader.GetString(); + return name switch + { + "local" => VolumeScope.Local, + "global" => VolumeScope.Global, + _ => throw new JsonException($"Unexpected volume scope \"{name}\".") + }; + } + + public override void Write(Utf8JsonWriter writer, VolumeScope value, JsonSerializerOptions options) + { + var name = value switch + { + VolumeScope.Global => "global", + VolumeScope.Local => "local", + _ => throw new JsonException($"Unexpected volume scope \"{value}\".") + }; + writer.WriteStringValue(name); + } + } +} diff --git a/DockerSdk/RequestBuilder.cs b/DockerSdk/RequestBuilder.cs index d6580f9..c199917 100644 --- a/DockerSdk/RequestBuilder.cs +++ b/DockerSdk/RequestBuilder.cs @@ -17,6 +17,7 @@ using DockerSdk.Networks; using DockerSdk.Registries; using DockerSdk.Registries.Dto; +using DockerSdk.Volumes; namespace DockerSdk { @@ -391,6 +392,10 @@ private async Task ThrowAsync(HttpResponseMessage response) if (match.Success) throw new NetworkNotFoundException($"No network with name or ID \"{match.Groups[1].Value}\" exists."); + match = Regex.Match(error, "^get (.*): no such volume$"); + if (match.Success) + throw new VolumeNotFoundException($"No volume with name \"{match.Groups[1].Value}\" exists."); + throw CreateResourceNotFoundException(error); } diff --git a/DockerSdk/Volumes/Dto/ListResponse.cs b/DockerSdk/Volumes/Dto/ListResponse.cs new file mode 100644 index 0000000..e401f68 --- /dev/null +++ b/DockerSdk/Volumes/Dto/ListResponse.cs @@ -0,0 +1,15 @@ +namespace DockerSdk.Volumes.Dto +{ + internal class ListResponse + { + /// + /// List of volumes. + /// + public VolumeResponse[] Volumes { get; set; } = null!; + + /// + /// Warnings that occurred when fetching the list of volumes. + /// + public string[] Warnings { get; set; } = null!; + } +} diff --git a/DockerSdk/Volumes/Dto/UsageData.cs b/DockerSdk/Volumes/Dto/UsageData.cs new file mode 100644 index 0000000..14ff6f8 --- /dev/null +++ b/DockerSdk/Volumes/Dto/UsageData.cs @@ -0,0 +1,22 @@ +namespace DockerSdk.Volumes.Dto +{ + /// + /// Usage details about the volume. This information is used by the GET /system/df endpoint, and omitted in other + /// endpoints. + /// + internal class UsageData + { + /// + /// The number of containers referencing this volume. This field is set to -1 if the reference-count is not + /// available. + /// + public int RefCount { get; set; } = -1; + + /// + /// Amount of disk space used by the volume (in bytes). This information is only available for volumes created + /// with the "local" volume driver. For volumes created with other volume drivers, this field is set to -1 ("not + /// available") + /// + public long Size { get; set; } = -1; + } +} diff --git a/DockerSdk/Volumes/Dto/VolumeResponse.cs b/DockerSdk/Volumes/Dto/VolumeResponse.cs new file mode 100644 index 0000000..c877854 --- /dev/null +++ b/DockerSdk/Volumes/Dto/VolumeResponse.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +namespace DockerSdk.Volumes.Dto +{ + internal class VolumeResponse + { + /// + /// Date and time of when the volume was created. + /// + public DateTimeOffset CreatedAt { get; set; } + + /// + /// Name of the volume driver used by the volume. + /// + public string Driver { get; set; } = null!; + + /// + /// User-defined key/value metadata. + /// + public Dictionary Labels { get; set; } = null!; + + /// + /// Mount path of the volume on the host. + /// + public string Mountpoint { get; set; } = null!; + + /// + /// Name of the volume. + /// + public string Name { get; set; } = null!; + + /// + /// The driver specific options used when creating the volume. + /// + public Dictionary Options { get; set; } = null!; + + /// + /// The level at which the volume exists. Either global for cluster-wide, or local for machine level. + /// + public VolumeScope Scope { get; set; } + + /// + /// Low-level details about the volume, provided by the volume driver. This value is omitted if the volume + /// driver does not support this feature. + /// + public Dictionary? Status { get; set; } + + /// + /// Usage details about the volume. + /// + public UsageData? UsageData { get; set; } + } +} diff --git a/DockerSdk/Volumes/IVolume.cs b/DockerSdk/Volumes/IVolume.cs index 6babd08..c919571 100644 --- a/DockerSdk/Volumes/IVolume.cs +++ b/DockerSdk/Volumes/IVolume.cs @@ -25,7 +25,7 @@ public interface IVolumeInfo : IVolume string Mountpoint { get; } - string Scope { get; } + VolumeScope Scope { get; } // TODO: Options } diff --git a/DockerSdk/Volumes/ListVolumesOptions.cs b/DockerSdk/Volumes/ListVolumesOptions.cs index 6a5a5b9..458c642 100644 --- a/DockerSdk/Volumes/ListVolumesOptions.cs +++ b/DockerSdk/Volumes/ListVolumesOptions.cs @@ -1,6 +1,65 @@ -namespace DockerSdk.Volumes +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DockerSdk.Volumes { public class ListVolumesOptions { + /// + /// Gets or sets a filter for dangling volumes. False means to return only volumes in use by one or more + /// containers; true means to only return volumes that are not in use by containers; and null means not to + /// filter by whether the volume is dangling. + /// + public bool? DanglingVolumesFilter { get; set; } + + /// + /// If set, the query will only return volumes that use the specified driver. + /// + public string? DriverFilter { get; set; } + + /// + /// Gets a list of labels to filter by. Only volumes that have all of the given labels will be returned. + /// + /// + /// If both this setting and are set, they are combined with "or" logic. + /// + public List LabelExistsFilters { get; } = new(); + + /// + /// Gets a set of label-value pairs to filter by. Only volumes that have all of the given labels set to the given + /// values will be returned. + /// + /// + /// If both this setting and are set, they are combined with "or" logic. + /// + public Dictionary LabelValueFilters { get; } = new(); + + /// + /// If set, the query will only return volumes whose names contain the given text. + /// + public string? NameContainsFilter { get; set; } + + internal string ToQueryString() + { + var dangling = DanglingVolumesFilter switch + { + true => "true", + false => "false", + null => null + }; + + var labels = LabelValueFilters.Select(kvp => $"{kvp.Key}={kvp.Value}").Concat(LabelExistsFilters); + + var filters = new QueryStringBuilder.StringStringBool(); + filters.Set("dangling", dangling); + filters.Set("driver", DriverFilter); + filters.Set("label", labels); + filters.Set("name", NameContainsFilter); + + var builder = new QueryStringBuilder(); + builder.Set("filters", filters); + return builder.Build(); + } } -} \ No newline at end of file +} diff --git a/DockerSdk/Volumes/VolumeAccess.cs b/DockerSdk/Volumes/VolumeAccess.cs index f4d6ff8..7539906 100644 --- a/DockerSdk/Volumes/VolumeAccess.cs +++ b/DockerSdk/Volumes/VolumeAccess.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; +using DockerSdk.Volumes.Dto; namespace DockerSdk.Volumes { @@ -86,17 +88,22 @@ public Task GetDetailsAsync(VolumeName volume, CancellationToken ct /// A token used to cancel the operation. /// A that resolves to the list of volumes. /// The sequence of the results is undefined. - public async Task> ListAsync(CancellationToken ct = default) + public Task> ListAsync(CancellationToken ct = default) + => ListAsync(new(), ct); + + /// + /// Gets a list of Docker volumes known to the daemon. + /// + /// Filters for which volumes to return. + /// A token used to cancel the operation. + /// A that resolves to the list of volumes. + /// The sequence of the results is undefined. + public async Task> ListAsync(ListVolumesOptions options, CancellationToken ct = default) { - CoreModels.VolumesListResponse response; - try - { - response = await client.Core.Volumes.ListAsync(ct).ConfigureAwait(false); - } - catch (Core.DockerApiException ex) - { - throw DockerException.Wrap(ex); - } + ListResponse response = await client.BuildRequest(HttpMethod.Get, "volumes") + .WithParameters(options.ToQueryString()) + .SendAsync(ct) + .ConfigureAwait(false); return response.Volumes.Select(raw => new Volume(client, new VolumeName(raw.Name))).ToArray(); } diff --git a/DockerSdk/Volumes/VolumeFactory.cs b/DockerSdk/Volumes/VolumeFactory.cs index 9460999..682a014 100644 --- a/DockerSdk/Volumes/VolumeFactory.cs +++ b/DockerSdk/Volumes/VolumeFactory.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Immutable; +using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using DockerSdk.Volumes.Dto; namespace DockerSdk.Volumes { internal static class VolumeFactory { - internal static Task LoadAsync(DockerClient client, VolumeName name, CancellationToken ct) + internal static Task LoadAsync(DockerClient client, VolumeName name, CancellationToken _) { if (name is null) throw new ArgumentNullException(nameof(name)); @@ -25,7 +28,7 @@ internal static async Task LoadInfoAsync(DockerClient client, Volum return new VolumeInfo(client, name) { - CreationTime = DateTimeOffset.Parse(raw.CreatedAt), + CreationTime = raw.CreatedAt, Driver = raw.Driver, Labels = raw.Labels?.ToImmutableDictionary() ?? ImmutableDictionary.Create(), Mountpoint = raw.Mountpoint, @@ -33,18 +36,11 @@ internal static async Task LoadInfoAsync(DockerClient client, Volum }; } - private static async Task LoadCoreAsync(DockerClient client, VolumeName name, CancellationToken ct) + private static Task LoadCoreAsync(DockerClient client, VolumeName name, CancellationToken ct) { - try - { - return await client.Core.Volumes.InspectAsync(name, ct).ConfigureAwait(false); - } - catch (Core.DockerApiException ex) - { - if (VolumeNotFoundException.TryWrap(ex, name, out var wrapped)) - throw wrapped; - throw DockerException.Wrap(ex); - } + return client.BuildRequest(HttpMethod.Get, $"volumes/{name}") + .RejectStatus(HttpStatusCode.NotFound, _ => new VolumeNotFoundException($"No volume with name \"{name}\" exists.")) + .SendAsync(ct); } } } diff --git a/DockerSdk/Volumes/VolumeInfo.cs b/DockerSdk/Volumes/VolumeInfo.cs index f18bbfe..fb6085e 100644 --- a/DockerSdk/Volumes/VolumeInfo.cs +++ b/DockerSdk/Volumes/VolumeInfo.cs @@ -14,6 +14,6 @@ public VolumeInfo(DockerClient client, VolumeName name) public string Driver { get; init; } = null!; public IReadOnlyDictionary Labels { get; init; } = ImmutableDictionary.Empty; public string Mountpoint { get; init; } = null!; - public string Scope { get; init; } = null!; + public VolumeScope Scope { get; init; } } } diff --git a/DockerSdk/Volumes/VolumeNotFoundException.cs b/DockerSdk/Volumes/VolumeNotFoundException.cs index 9538778..a804bfd 100644 --- a/DockerSdk/Volumes/VolumeNotFoundException.cs +++ b/DockerSdk/Volumes/VolumeNotFoundException.cs @@ -1,7 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; -using Docker.DotNet; -using Core = Docker.DotNet; namespace DockerSdk.Volumes { @@ -9,7 +6,7 @@ namespace DockerSdk.Volumes /// Indicates that the Docker daemon could not find the indicated volume where it was looking. /// [Serializable] - public abstract class VolumeNotFoundException : ResourceNotFoundException + public class VolumeNotFoundException : ResourceNotFoundException { /// /// Creates an instance of the class. @@ -35,11 +32,6 @@ public VolumeNotFoundException(string message, Exception inner) : base(message, { } - internal static bool TryWrap(DockerApiException ex, VolumeName name, out VolumeNotFoundException wrapped) - { - throw new NotImplementedException(); - } - /// /// Creates an instance of the class. /// diff --git a/DockerSdk/Volumes/VolumeScope.cs b/DockerSdk/Volumes/VolumeScope.cs new file mode 100644 index 0000000..dcb612f --- /dev/null +++ b/DockerSdk/Volumes/VolumeScope.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using DockerSdk.JsonConverters; + +namespace DockerSdk.Volumes +{ + [JsonConverter(typeof(VolumeScopeConverter))] + public enum VolumeScope + { + Local, + Global, + } +} From 2f5c67c118144e81b446c1708c8b6cc176a01977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=20Ts=C5=8Dnto?= Date: Fri, 3 Sep 2021 21:47:03 -0500 Subject: [PATCH 3/3] finished basic list/get/inspect for volumes --- DockerSdk.Tests/VolumeAccessTests.cs | 26 ++++++----------- DockerSdk/Volumes/IVolume.cs | 36 +++++++++++------------ DockerSdk/Volumes/IVolumeInfo.cs | 39 +++++++++++++++++++++++++ DockerSdk/Volumes/ListVolumesOptions.cs | 3 ++ DockerSdk/Volumes/Volume.cs | 10 ++++++- DockerSdk/Volumes/VolumeInfo.cs | 15 ++++++++++ DockerSdk/Volumes/VolumeScope.cs | 10 +++++++ 7 files changed, 102 insertions(+), 37 deletions(-) create mode 100644 DockerSdk/Volumes/IVolumeInfo.cs diff --git a/DockerSdk.Tests/VolumeAccessTests.cs b/DockerSdk.Tests/VolumeAccessTests.cs index 64edc80..93b414b 100644 --- a/DockerSdk.Tests/VolumeAccessTests.cs +++ b/DockerSdk.Tests/VolumeAccessTests.cs @@ -3,29 +3,12 @@ using DockerSdk.Volumes; using FluentAssertions; using Xunit; -using Xunit.Abstractions; namespace DockerSdk.Tests { [Collection("Common")] public class VolumeAccessTests { - public VolumeAccessTests(ITestOutputHelper toh) - { - this.toh = toh; - } - - private readonly ITestOutputHelper toh; - - [Fact] - public async Task GetDetailsAsync_NoSuchVolume_ThrowsVolumeNotFoundException() - { - using var client = await DockerClient.StartAsync(); - - await Assert.ThrowsAnyAsync( - () => client.Volumes.GetDetailsAsync("ddnt-no-such-volume-366ad733152b70e53ddd7fd59defe9fa2e055ed2090f5f3a8839b2797388d0b4")); - } - [Fact] public async Task GetDetailsAsync_LocalVolumeExists_GetsExpectedDetails() { @@ -43,6 +26,15 @@ public async Task GetDetailsAsync_LocalVolumeExists_GetsExpectedDetails() volume.Scope.Should().Be(VolumeScope.Local); } + [Fact] + public async Task GetDetailsAsync_NoSuchVolume_ThrowsVolumeNotFoundException() + { + using var client = await DockerClient.StartAsync(); + + await Assert.ThrowsAnyAsync( + () => client.Volumes.GetDetailsAsync("ddnt-no-such-volume-366ad733152b70e53ddd7fd59defe9fa2e055ed2090f5f3a8839b2797388d0b4")); + } + [Fact] public async Task ListAsync_NoFilters_FindsPremadeVolumes() { diff --git a/DockerSdk/Volumes/IVolume.cs b/DockerSdk/Volumes/IVolume.cs index c919571..b2a3817 100644 --- a/DockerSdk/Volumes/IVolume.cs +++ b/DockerSdk/Volumes/IVolume.cs @@ -1,32 +1,30 @@ -using System; -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace DockerSdk.Volumes { + /// + /// Represents a Docker volume, which is a persistent filesystem mount used by containers. + /// + /// public interface IVolume { + /// + /// Gets the volume's name. + /// VolumeName Name { get; } - Task GetDetailsAsync(CancellationToken ct = default); - } - - public interface IVolumeInfo : IVolume - { - DateTimeOffset CreationTime { get; } - - string Driver { get; } - /// - /// Gets the labels that have been applied to the volume. + /// Gets detailed information about the volume. /// - IReadOnlyDictionary Labels { get; } - - string Mountpoint { get; } - - VolumeScope Scope { get; } + /// A used to cancel the operation. + /// A that completes when the result is available. + /// The volume no longer exists. + /// + /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate + /// validation, or timeout. + /// + Task GetDetailsAsync(CancellationToken ct = default); - // TODO: Options } } diff --git a/DockerSdk/Volumes/IVolumeInfo.cs b/DockerSdk/Volumes/IVolumeInfo.cs new file mode 100644 index 0000000..d933bed --- /dev/null +++ b/DockerSdk/Volumes/IVolumeInfo.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace DockerSdk.Volumes +{ + /// + /// Provides detailed information about a volume. + /// + /// This class holds a snapshot in time. Its information is immutable once created. + public interface IVolumeInfo : IVolume + { + /// + /// Gets the time at which the volume was created. + /// + DateTimeOffset CreationTime { get; } + + /// + /// Gets the name of the storage driver that operates the volume. + /// + string Driver { get; } + + /// + /// Gets the labels that have been applied to the volume. + /// + IReadOnlyDictionary Labels { get; } + + /// + /// Gets the absolute path of where the volume's data is stored on the host. + /// + string Mountpoint { get; } + + /// + /// Gets the level at which the volume exists: either Global for cluster-wide or Local for machine-level. + /// + VolumeScope Scope { get; } + + // TODO: Options + } +} diff --git a/DockerSdk/Volumes/ListVolumesOptions.cs b/DockerSdk/Volumes/ListVolumesOptions.cs index 458c642..63492a4 100644 --- a/DockerSdk/Volumes/ListVolumesOptions.cs +++ b/DockerSdk/Volumes/ListVolumesOptions.cs @@ -4,6 +4,9 @@ namespace DockerSdk.Volumes { + /// + /// Specifies how to list volumes. + /// public class ListVolumesOptions { /// diff --git a/DockerSdk/Volumes/Volume.cs b/DockerSdk/Volumes/Volume.cs index 3481e69..03e3680 100644 --- a/DockerSdk/Volumes/Volume.cs +++ b/DockerSdk/Volumes/Volume.cs @@ -3,18 +3,26 @@ namespace DockerSdk.Volumes { + /// internal class Volume : IVolume { protected DockerClient client; - public Volume(DockerClient client, VolumeName name) + /// + /// Initializes a new instance of the type. + /// + /// The instance to use. + /// The volume's name. + internal Volume(DockerClient client, VolumeName name) { this.client = client; Name = name; } + /// public VolumeName Name { get; } + /// public Task GetDetailsAsync(CancellationToken ct = default) => VolumeFactory.LoadInfoAsync(client, Name, ct); } diff --git a/DockerSdk/Volumes/VolumeInfo.cs b/DockerSdk/Volumes/VolumeInfo.cs index fb6085e..ca310ea 100644 --- a/DockerSdk/Volumes/VolumeInfo.cs +++ b/DockerSdk/Volumes/VolumeInfo.cs @@ -4,16 +4,31 @@ namespace DockerSdk.Volumes { + /// internal class VolumeInfo : Volume, IVolumeInfo { + /// + /// Initializes a new instance of the type. + /// + /// The instance to use. + /// The volume's name. public VolumeInfo(DockerClient client, VolumeName name) : base(client, name) { } + /// public DateTimeOffset CreationTime { get; init; } + + /// public string Driver { get; init; } = null!; + + /// public IReadOnlyDictionary Labels { get; init; } = ImmutableDictionary.Empty; + + /// public string Mountpoint { get; init; } = null!; + + /// public VolumeScope Scope { get; init; } } } diff --git a/DockerSdk/Volumes/VolumeScope.cs b/DockerSdk/Volumes/VolumeScope.cs index dcb612f..e207fa6 100644 --- a/DockerSdk/Volumes/VolumeScope.cs +++ b/DockerSdk/Volumes/VolumeScope.cs @@ -3,10 +3,20 @@ namespace DockerSdk.Volumes { + /// + /// Indicates the level at which a volume exists. + /// [JsonConverter(typeof(VolumeScopeConverter))] public enum VolumeScope { + /// + /// The volume exists on the local machine only. + /// Local, + + /// + /// The volume exists at the cluster level. + /// Global, } }