Skip to content
Merged
3 changes: 3 additions & 0 deletions src/Docker.DotNet/DockerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal DockerClient(
Volumes = new VolumeOperations(this);
Secrets = new SecretsOperations(this);
Configs = new ConfigOperations(this);
Distribution = new DistributionOperations(this);
Swarm = new SwarmOperations(this);
Tasks = new TasksOperations(this);
Plugin = new PluginOperations(this);
Expand All @@ -57,6 +58,8 @@ internal DockerClient(

public IConfigOperations Configs { get; }

public IDistributionOperations Distribution { get; }

public ISwarmOperations Swarm { get; }

public ITasksOperations Tasks { get; }
Expand Down
30 changes: 30 additions & 0 deletions src/Docker.DotNet/Endpoints/DistributionOperations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Docker.DotNet;

internal class DistributionOperations : IDistributionOperations
{
private static readonly ApiResponseErrorHandlingDelegate NoSuchImageHandler = (statusCode, responseBody) =>
{
if (statusCode == HttpStatusCode.NotFound)
{
throw new DockerImageNotFoundException(statusCode, responseBody);
}
};

private readonly DockerClient _client;

internal DistributionOperations(DockerClient client)
{
_client = client;
}

public async Task<DistributionInspectResponse> InspectAsync(string name, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException(nameof(name));
}

return await _client.MakeRequestAsync<DistributionInspectResponse>([NoSuchImageHandler], HttpMethod.Get, $"distribution/{name}/json", cancellationToken)
.ConfigureAwait(false);
}
}
18 changes: 18 additions & 0 deletions src/Docker.DotNet/Endpoints/IDistributionOperations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Docker.DotNet;

public interface IDistributionOperations
{
/// <summary>
/// Retrieves low-level information about an image from a registry.
/// </summary>
/// <param name="name">An image name or reference.</param>
/// <param name="cancellationToken">When triggered, the operation will stop at the next available time, if possible.</param>
/// <remarks>
/// The equivalent command in the Docker CLI is <c>docker manifest inspect</c>.
/// </remarks>
/// <exception cref="ArgumentNullException">One or more of the inputs was <see langword="null"/>.</exception>
/// <exception cref="DockerImageNotFoundException">No such image was found.</exception>
/// <exception cref="DockerApiException">The input is invalid or the daemon experienced an error.</exception>
/// <exception cref="HttpRequestException">The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout.</exception>
Task<DistributionInspectResponse> InspectAsync(string name, CancellationToken cancellationToken = default);
}
2 changes: 2 additions & 0 deletions src/Docker.DotNet/IDockerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public interface IDockerClient : IDisposable

IConfigOperations Configs { get; }

IDistributionOperations Distribution { get; }

ISwarmOperations Swarm { get; }

ITasksOperations Tasks { get; }
Expand Down
24 changes: 24 additions & 0 deletions src/Docker.DotNet/Models/DistributionInspectResponse.Generated.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#nullable enable
namespace Docker.DotNet.Models
{
/// <summary>
/// DistributionInspect describes the result obtained from contacting the
/// registry to retrieve image metadata
/// </summary>
public class DistributionInspectResponse // (registry.DistributionInspect)
{
/// <summary>
/// Descriptor contains information about the manifest, including
/// the content addressable digest
/// </summary>
[JsonPropertyName("Descriptor")]
public Descriptor Descriptor { get; set; } = default!;

/// <summary>
/// Platforms contains the list of platforms supported by the image,
/// obtained by parsing the manifest
/// </summary>
[JsonPropertyName("Platforms")]
public IList<Platform> Platforms { get; set; } = default!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ namespace Docker.DotNet.Models
[JsonSerializable(typeof(DeviceRequest))]
[JsonSerializable(typeof(DiscreteGenericResource))]
[JsonSerializable(typeof(DispatcherConfig))]
[JsonSerializable(typeof(DistributionInspectResponse))]
[JsonSerializable(typeof(DockerOCIImageConfig))]
[JsonSerializable(typeof(DockerOCIImageConfigExt))]
[JsonSerializable(typeof(Driver))]
Expand Down
48 changes: 48 additions & 0 deletions test/Docker.DotNet.Tests/IDistributionOperationsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
namespace Docker.DotNet.Tests;

[Collection(nameof(TestCollection))]
public class IDistributionOperationsTests
{
private readonly TestFixture _testFixture;

public IDistributionOperationsTests(TestFixture testFixture)
{
_testFixture = testFixture;
}

[Fact]
public async Task InspectAsync_ReturnsDescriptorAndPlatforms()
{
var response = await _testFixture.DockerClient.Distribution.InspectAsync(
"alpine:3.20",
_testFixture.Cts.Token);

Assert.NotNull(response);
Assert.NotNull(response.Descriptor);
Assert.StartsWith("sha256:", response.Descriptor.Digest);
Assert.NotEmpty(response.Platforms);
}

[Fact]
public Task InspectAsync_UnknownImage_ThrowsDockerImageNotFoundException()
{
return Assert.ThrowsAsync<DockerImageNotFoundException>(
() => _testFixture.DockerClient.Distribution.InspectAsync(
"alpine:does-not-exist",
_testFixture.Cts.Token));
}

[Fact]
public async Task InspectAsync_NullOrEmptyName_ThrowsArgumentNullException()
{
await Assert.ThrowsAsync<ArgumentNullException>(
() => _testFixture.DockerClient.Distribution.InspectAsync(
null!,
_testFixture.Cts.Token));

await Assert.ThrowsAsync<ArgumentNullException>(
() => _testFixture.DockerClient.Distribution.InspectAsync(
string.Empty,
_testFixture.Cts.Token));
}
}
6 changes: 6 additions & 0 deletions tools/specgen/specgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ var typesToDisambiguate = map[string]*CSModelType{
typeToKey(reflect.TypeOf(volume.Secret{})): {Name: "VolumeSecret"},
typeToKey(reflect.TypeOf(volume.Topology{})): {Name: "VolumeTopology"},
typeToKey(reflect.TypeOf(registry.AuthResponse{})): {Name: "AuthResponse"},
typeToKey(reflect.TypeOf(registry.DistributionInspect{})): {
Name: "DistributionInspectResponse",
},
typeToKey(reflect.TypeOf(registry.SearchResult{})): {Name: "ImageSearchResponse"},
typeToKey(reflect.TypeOf(swarm.RuntimeSpec{})): {Name: "SwarmRuntimeSpec"},
typeToKey(reflect.TypeOf(swarm.ConfigSpec{})): {Name: "SwarmConfigSpec"},
Expand Down Expand Up @@ -343,6 +346,9 @@ var dockerTypesToReflect = []reflect.Type{
reflect.TypeOf(events.Actor{}),
reflect.TypeOf(events.Message{}),

// GET /distribution/{name}/json
reflect.TypeOf(registry.DistributionInspect{}),

// POST /images/create
reflect.TypeOf(ImagesCreateParameters{}),
reflect.TypeOf(jsonstream.Message{}),
Expand Down