Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions DockerSdk.Tests/VolumeAccessTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Threading.Tasks;
using DockerSdk.Volumes;
using FluentAssertions;
using Xunit;

namespace DockerSdk.Tests
{
[Collection("Common")]
public class VolumeAccessTests
{
[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 GetDetailsAsync_NoSuchVolume_ThrowsVolumeNotFoundException()
{
using var client = await DockerClient.StartAsync();

await Assert.ThrowsAnyAsync<VolumeNotFoundException>(
() => client.Volumes.GetDetailsAsync("ddnt-no-such-volume-366ad733152b70e53ddd7fd59defe9fa2e055ed2090f5f3a8839b2797388d0b4"));
}

[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");
}
}
}
6 changes: 6 additions & 0 deletions DockerSdk.Tests/scripts/clean.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions DockerSdk.Tests/scripts/definitions.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 = @(
Expand Down
28 changes: 28 additions & 0 deletions DockerSdk.Tests/scripts/up.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ function VerifySetup()
}
}

foreach ($name in $volumeDefinitions.Name)
{
if (!(VerifyVolume $name))
{
return $false
}
}

foreach ($name in $imageDefinitions.Name)
{
if (!(VerifyImage $name))
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
{
Expand Down
7 changes: 7 additions & 0 deletions DockerSdk/DockerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -58,6 +60,11 @@ private DockerClient(Comm core, ClientOptions options, Version negotiatedApiVers
/// </summary>
public RegistryAccess Registries { get; }

/// <summary>
/// Provides access to functionality related to Docker volumes.
/// </summary>
public VolumeAccess Volumes { get; }

/// <summary>
/// Gets the core client, which is what does all the heavy lifting for communicating with the Docker daemon.
/// </summary>
Expand Down
32 changes: 32 additions & 0 deletions DockerSdk/JsonConverters/VolumeScopeConverter.cs
Original file line number Diff line number Diff line change
@@ -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<VolumeScope>
{
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);
}
}
}
5 changes: 5 additions & 0 deletions DockerSdk/RequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using DockerSdk.Networks;
using DockerSdk.Registries;
using DockerSdk.Registries.Dto;
using DockerSdk.Volumes;

namespace DockerSdk
{
Expand Down Expand Up @@ -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);
}

Expand Down
15 changes: 15 additions & 0 deletions DockerSdk/Volumes/Dto/ListResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace DockerSdk.Volumes.Dto
{
internal class ListResponse
{
/// <summary>
/// List of volumes.
/// </summary>
public VolumeResponse[] Volumes { get; set; } = null!;

/// <summary>
/// Warnings that occurred when fetching the list of volumes.
/// </summary>
public string[] Warnings { get; set; } = null!;
}
}
22 changes: 22 additions & 0 deletions DockerSdk/Volumes/Dto/UsageData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace DockerSdk.Volumes.Dto
{
/// <summary>
/// Usage details about the volume. This information is used by the GET /system/df endpoint, and omitted in other
/// endpoints.
/// </summary>
internal class UsageData
{
/// <summary>
/// The number of containers referencing this volume. This field is set to -1 if the reference-count is not
/// available.
/// </summary>
public int RefCount { get; set; } = -1;

/// <summary>
/// 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")
/// </summary>
public long Size { get; set; } = -1;
}
}
54 changes: 54 additions & 0 deletions DockerSdk/Volumes/Dto/VolumeResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;

namespace DockerSdk.Volumes.Dto
{
internal class VolumeResponse
{
/// <summary>
/// Date and time of when the volume was created.
/// </summary>
public DateTimeOffset CreatedAt { get; set; }

/// <summary>
/// Name of the volume driver used by the volume.
/// </summary>
public string Driver { get; set; } = null!;

/// <summary>
/// User-defined key/value metadata.
/// </summary>
public Dictionary<string, string> Labels { get; set; } = null!;

/// <summary>
/// Mount path of the volume on the host.
/// </summary>
public string Mountpoint { get; set; } = null!;

/// <summary>
/// Name of the volume.
/// </summary>
public string Name { get; set; } = null!;

/// <summary>
/// The driver specific options used when creating the volume.
/// </summary>
public Dictionary<string, string> Options { get; set; } = null!;

/// <summary>
/// The level at which the volume exists. Either global for cluster-wide, or local for machine level.
/// </summary>
public VolumeScope Scope { get; set; }

/// <summary>
/// Low-level details about the volume, provided by the volume driver. This value is omitted if the volume
/// driver does not support this feature.
/// </summary>
public Dictionary<string, object>? Status { get; set; }

/// <summary>
/// Usage details about the volume.
/// </summary>
public UsageData? UsageData { get; set; }
}
}
30 changes: 30 additions & 0 deletions DockerSdk/Volumes/IVolume.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Threading;
using System.Threading.Tasks;

namespace DockerSdk.Volumes
{
/// <summary>
/// Represents a Docker volume, which is a persistent filesystem mount used by containers.
/// </summary>
/// <seealso cref="IVolumeInfo"/>
public interface IVolume
{
/// <summary>
/// Gets the volume's name.
/// </summary>
VolumeName Name { get; }

/// <summary>
/// Gets detailed information about the volume.
/// </summary>
/// <param name="ct">A <see cref="CancellationToken"/> used to cancel the operation.</param>
/// <returns>A <see cref="Task"/> that completes when the result is available.</returns>
/// <exception cref="VolumeNotFoundException">The volume no longer exists.</exception>
/// <exception cref="System.Net.Http.HttpRequestException">
/// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate
/// validation, or timeout.
/// </exception>
Task<IVolumeInfo> GetDetailsAsync(CancellationToken ct = default);

}
}
39 changes: 39 additions & 0 deletions DockerSdk/Volumes/IVolumeInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;

namespace DockerSdk.Volumes
{
/// <summary>
/// Provides detailed information about a volume.
/// </summary>
/// <remarks>This class holds a snapshot in time. Its information is immutable once created.</remarks>
public interface IVolumeInfo : IVolume
{
/// <summary>
/// Gets the time at which the volume was created.
/// </summary>
DateTimeOffset CreationTime { get; }

/// <summary>
/// Gets the name of the storage driver that operates the volume.
/// </summary>
string Driver { get; }

/// <summary>
/// Gets the labels that have been applied to the volume.
/// </summary>
IReadOnlyDictionary<string, string> Labels { get; }

/// <summary>
/// Gets the absolute path of where the volume's data is stored on the host.
/// </summary>
string Mountpoint { get; }

/// <summary>
/// Gets the level at which the volume exists: either Global for cluster-wide or Local for machine-level.
/// </summary>
VolumeScope Scope { get; }

// TODO: Options
}
}
Loading