From 3af9c297e901923c22c8bf5caa3db69a741d22d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:19:09 +0000 Subject: [PATCH 1/7] Add distribution inspect API surface Agent-Logs-Url: https://github.com/bbartels/Docker.DotNet/sessions/291566e2-fea2-4eb9-acb2-377176972eb0 Co-authored-by: bbartels <23058572+bbartels@users.noreply.github.com> --- src/Docker.DotNet/DockerClient.cs | 5 ++- .../Endpoints/DistributionOperations.cs | 30 +++++++++++++ .../Endpoints/IDistributionOperations.cs | 18 ++++++++ src/Docker.DotNet/IDockerClient.cs | 4 +- .../DistributionInspectResponse.Generated.cs | 16 +++++++ .../IDistributionOperationsTests.cs | 43 +++++++++++++++++++ 6 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 src/Docker.DotNet/Endpoints/DistributionOperations.cs create mode 100644 src/Docker.DotNet/Endpoints/IDistributionOperations.cs create mode 100644 src/Docker.DotNet/Models/DistributionInspectResponse.Generated.cs create mode 100644 test/Docker.DotNet.Tests/IDistributionOperationsTests.cs diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 6328e8f7..15192548 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -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); @@ -57,6 +58,8 @@ internal DockerClient( public IConfigOperations Configs { get; } + public IDistributionOperations Distribution { get; } + public ISwarmOperations Swarm { get; } public ITasksOperations Tasks { get; } @@ -455,4 +458,4 @@ private static async Task HandleIfErrorResponseAsync(HttpStatusCode statusCode, private struct NoContent; } -internal delegate void ApiResponseErrorHandlingDelegate(HttpStatusCode statusCode, string? responseBody); \ No newline at end of file +internal delegate void ApiResponseErrorHandlingDelegate(HttpStatusCode statusCode, string? responseBody); diff --git a/src/Docker.DotNet/Endpoints/DistributionOperations.cs b/src/Docker.DotNet/Endpoints/DistributionOperations.cs new file mode 100644 index 00000000..2fb580ab --- /dev/null +++ b/src/Docker.DotNet/Endpoints/DistributionOperations.cs @@ -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 InspectAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + return await _client.MakeRequestAsync([NoSuchImageHandler], HttpMethod.Get, $"distribution/{name}/json", cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/Docker.DotNet/Endpoints/IDistributionOperations.cs b/src/Docker.DotNet/Endpoints/IDistributionOperations.cs new file mode 100644 index 00000000..16b9f845 --- /dev/null +++ b/src/Docker.DotNet/Endpoints/IDistributionOperations.cs @@ -0,0 +1,18 @@ +namespace Docker.DotNet; + +public interface IDistributionOperations +{ + /// + /// Retrieves low-level information about an image from a registry. + /// + /// An image name or reference. + /// When triggered, the operation will stop at the next available time, if possible. + /// + /// The equivalent command in the Docker CLI is docker manifest inspect. + /// + /// One or more of the inputs was . + /// No such image was found. + /// The input is invalid or the daemon experienced an error. + /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. + Task InspectAsync(string name, CancellationToken cancellationToken = default); +} diff --git a/src/Docker.DotNet/IDockerClient.cs b/src/Docker.DotNet/IDockerClient.cs index 12830241..216fd73f 100644 --- a/src/Docker.DotNet/IDockerClient.cs +++ b/src/Docker.DotNet/IDockerClient.cs @@ -18,6 +18,8 @@ public interface IDockerClient : IDisposable IConfigOperations Configs { get; } + IDistributionOperations Distribution { get; } + ISwarmOperations Swarm { get; } ITasksOperations Tasks { get; } @@ -25,4 +27,4 @@ public interface IDockerClient : IDisposable IPluginOperations Plugin { get; } IExecOperations Exec { get; } -} \ No newline at end of file +} diff --git a/src/Docker.DotNet/Models/DistributionInspectResponse.Generated.cs b/src/Docker.DotNet/Models/DistributionInspectResponse.Generated.cs new file mode 100644 index 00000000..c63a563d --- /dev/null +++ b/src/Docker.DotNet/Models/DistributionInspectResponse.Generated.cs @@ -0,0 +1,16 @@ +#nullable enable +namespace Docker.DotNet.Models +{ + /// + /// DistributionInspectResponse contains response of Engine API: + /// GET "/distribution/{name}/json" + /// + public class DistributionInspectResponse // (registry.DistributionInspect) + { + [JsonPropertyName("Descriptor")] + public Descriptor Descriptor { get; set; } = default!; + + [JsonPropertyName("Platforms")] + public IList Platforms { get; set; } = default!; + } +} diff --git a/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs b/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs new file mode 100644 index 00000000..a20171b6 --- /dev/null +++ b/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs @@ -0,0 +1,43 @@ +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( + () => _testFixture.DockerClient.Distribution.InspectAsync( + "docker.io/library/definitely-not-a-real-image:does-not-exist", + TestContext.Current.CancellationToken)); + } + + [Fact] + public Task InspectAsync_NullOrEmptyName_ThrowsArgumentNullException() + { + return Assert.ThrowsAsync( + () => _testFixture.DockerClient.Distribution.InspectAsync( + string.Empty, + TestContext.Current.CancellationToken)); + } +} From d68bb2dd6f9257a60ced5dd02ca17642b481d667 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:34:03 +0000 Subject: [PATCH 2/7] Fix distribution inspect test coverage Agent-Logs-Url: https://github.com/bbartels/Docker.DotNet/sessions/291566e2-fea2-4eb9-acb2-377176972eb0 Co-authored-by: bbartels <23058572+bbartels@users.noreply.github.com> --- .../Models/DockerModelsJsonSerializerContext.Generated.cs | 1 + test/Docker.DotNet.Tests/IDistributionOperationsTests.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs b/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs index 2f904917..cbfc5906 100644 --- a/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs +++ b/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs @@ -57,6 +57,7 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(DeviceMapping))] [JsonSerializable(typeof(DeviceRequest))] [JsonSerializable(typeof(DiscreteGenericResource))] + [JsonSerializable(typeof(DistributionInspectResponse))] [JsonSerializable(typeof(DispatcherConfig))] [JsonSerializable(typeof(DockerOCIImageConfig))] [JsonSerializable(typeof(DockerOCIImageConfigExt))] diff --git a/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs b/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs index a20171b6..7d795ee7 100644 --- a/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs @@ -28,7 +28,7 @@ public Task InspectAsync_UnknownImage_ThrowsDockerImageNotFoundException() { return Assert.ThrowsAsync( () => _testFixture.DockerClient.Distribution.InspectAsync( - "docker.io/library/definitely-not-a-real-image:does-not-exist", + "alpine:does-not-exist", TestContext.Current.CancellationToken)); } From 9180f4855bbdb617d3a7200a908fc8c6d12b5f49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:38:25 +0000 Subject: [PATCH 3/7] Cover null and empty distribution names Agent-Logs-Url: https://github.com/bbartels/Docker.DotNet/sessions/291566e2-fea2-4eb9-acb2-377176972eb0 Co-authored-by: bbartels <23058572+bbartels@users.noreply.github.com> --- test/Docker.DotNet.Tests/IDistributionOperationsTests.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs b/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs index 7d795ee7..3cb25885 100644 --- a/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs @@ -33,9 +33,14 @@ public Task InspectAsync_UnknownImage_ThrowsDockerImageNotFoundException() } [Fact] - public Task InspectAsync_NullOrEmptyName_ThrowsArgumentNullException() + public async Task InspectAsync_NullOrEmptyName_ThrowsArgumentNullException() { - return Assert.ThrowsAsync( + await Assert.ThrowsAsync( + () => _testFixture.DockerClient.Distribution.InspectAsync( + null!, + TestContext.Current.CancellationToken)); + + await Assert.ThrowsAsync( () => _testFixture.DockerClient.Distribution.InspectAsync( string.Empty, TestContext.Current.CancellationToken)); From 3725e5476c16a13cb303b149d7ebac29e3928e5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:41:40 +0000 Subject: [PATCH 4/7] Align distribution test cancellation tokens Agent-Logs-Url: https://github.com/bbartels/Docker.DotNet/sessions/291566e2-fea2-4eb9-acb2-377176972eb0 Co-authored-by: bbartels <23058572+bbartels@users.noreply.github.com> --- test/Docker.DotNet.Tests/IDistributionOperationsTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs b/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs index 3cb25885..7cecd4e2 100644 --- a/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs @@ -29,7 +29,7 @@ public Task InspectAsync_UnknownImage_ThrowsDockerImageNotFoundException() return Assert.ThrowsAsync( () => _testFixture.DockerClient.Distribution.InspectAsync( "alpine:does-not-exist", - TestContext.Current.CancellationToken)); + _testFixture.Cts.Token)); } [Fact] @@ -38,11 +38,11 @@ public async Task InspectAsync_NullOrEmptyName_ThrowsArgumentNullException() await Assert.ThrowsAsync( () => _testFixture.DockerClient.Distribution.InspectAsync( null!, - TestContext.Current.CancellationToken)); + _testFixture.Cts.Token)); await Assert.ThrowsAsync( () => _testFixture.DockerClient.Distribution.InspectAsync( string.Empty, - TestContext.Current.CancellationToken)); + _testFixture.Cts.Token)); } } From 6a06db769d4380dd33830084d67c1e7ee32663ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:03:28 +0000 Subject: [PATCH 5/7] Add distribution inspect specgen entries Agent-Logs-Url: https://github.com/bbartels/Docker.DotNet/sessions/15862762-6b54-4094-a000-63d1c9d81798 Co-authored-by: bbartels <23058572+bbartels@users.noreply.github.com> --- tools/specgen/specgen.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/specgen/specgen.go b/tools/specgen/specgen.go index 36cc8cbb..7645d413 100644 --- a/tools/specgen/specgen.go +++ b/tools/specgen/specgen.go @@ -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"}, @@ -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{}), From 75e120c408f0beb5fa2e375ffadab20911991665 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:04:26 +0000 Subject: [PATCH 6/7] Regenerate distribution inspect models Agent-Logs-Url: https://github.com/bbartels/Docker.DotNet/sessions/15862762-6b54-4094-a000-63d1c9d81798 Co-authored-by: bbartels <23058572+bbartels@users.noreply.github.com> --- .../Models/DistributionInspectResponse.Generated.cs | 12 ++++++++++-- .../DockerModelsJsonSerializerContext.Generated.cs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Docker.DotNet/Models/DistributionInspectResponse.Generated.cs b/src/Docker.DotNet/Models/DistributionInspectResponse.Generated.cs index c63a563d..081ba156 100644 --- a/src/Docker.DotNet/Models/DistributionInspectResponse.Generated.cs +++ b/src/Docker.DotNet/Models/DistributionInspectResponse.Generated.cs @@ -2,14 +2,22 @@ namespace Docker.DotNet.Models { /// - /// DistributionInspectResponse contains response of Engine API: - /// GET "/distribution/{name}/json" + /// DistributionInspect describes the result obtained from contacting the + /// registry to retrieve image metadata /// public class DistributionInspectResponse // (registry.DistributionInspect) { + /// + /// Descriptor contains information about the manifest, including + /// the content addressable digest + /// [JsonPropertyName("Descriptor")] public Descriptor Descriptor { get; set; } = default!; + /// + /// Platforms contains the list of platforms supported by the image, + /// obtained by parsing the manifest + /// [JsonPropertyName("Platforms")] public IList Platforms { get; set; } = default!; } diff --git a/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs b/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs index cbfc5906..90224138 100644 --- a/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs +++ b/src/Docker.DotNet/Models/DockerModelsJsonSerializerContext.Generated.cs @@ -57,8 +57,8 @@ namespace Docker.DotNet.Models [JsonSerializable(typeof(DeviceMapping))] [JsonSerializable(typeof(DeviceRequest))] [JsonSerializable(typeof(DiscreteGenericResource))] - [JsonSerializable(typeof(DistributionInspectResponse))] [JsonSerializable(typeof(DispatcherConfig))] + [JsonSerializable(typeof(DistributionInspectResponse))] [JsonSerializable(typeof(DockerOCIImageConfig))] [JsonSerializable(typeof(DockerOCIImageConfigExt))] [JsonSerializable(typeof(Driver))] From 91333d0d63848b25365e99b907698e412602b4d0 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:06:35 +0200 Subject: [PATCH 7/7] chore: Remove unnecessary changes --- src/Docker.DotNet/DockerClient.cs | 2 +- src/Docker.DotNet/Endpoints/DistributionOperations.cs | 2 +- src/Docker.DotNet/Endpoints/IDistributionOperations.cs | 2 +- src/Docker.DotNet/IDockerClient.cs | 2 +- test/Docker.DotNet.Tests/IDistributionOperationsTests.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 15192548..a5da6fe1 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -458,4 +458,4 @@ private static async Task HandleIfErrorResponseAsync(HttpStatusCode statusCode, private struct NoContent; } -internal delegate void ApiResponseErrorHandlingDelegate(HttpStatusCode statusCode, string? responseBody); +internal delegate void ApiResponseErrorHandlingDelegate(HttpStatusCode statusCode, string? responseBody); \ No newline at end of file diff --git a/src/Docker.DotNet/Endpoints/DistributionOperations.cs b/src/Docker.DotNet/Endpoints/DistributionOperations.cs index 2fb580ab..8a29a3dc 100644 --- a/src/Docker.DotNet/Endpoints/DistributionOperations.cs +++ b/src/Docker.DotNet/Endpoints/DistributionOperations.cs @@ -27,4 +27,4 @@ public async Task InspectAsync(string name, Cancell return await _client.MakeRequestAsync([NoSuchImageHandler], HttpMethod.Get, $"distribution/{name}/json", cancellationToken) .ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Docker.DotNet/Endpoints/IDistributionOperations.cs b/src/Docker.DotNet/Endpoints/IDistributionOperations.cs index 16b9f845..85f72148 100644 --- a/src/Docker.DotNet/Endpoints/IDistributionOperations.cs +++ b/src/Docker.DotNet/Endpoints/IDistributionOperations.cs @@ -15,4 +15,4 @@ public interface IDistributionOperations /// The input is invalid or the daemon experienced an error. /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation or timeout. Task InspectAsync(string name, CancellationToken cancellationToken = default); -} +} \ No newline at end of file diff --git a/src/Docker.DotNet/IDockerClient.cs b/src/Docker.DotNet/IDockerClient.cs index 216fd73f..80e11387 100644 --- a/src/Docker.DotNet/IDockerClient.cs +++ b/src/Docker.DotNet/IDockerClient.cs @@ -27,4 +27,4 @@ public interface IDockerClient : IDisposable IPluginOperations Plugin { get; } IExecOperations Exec { get; } -} +} \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs b/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs index 7cecd4e2..5d8d592c 100644 --- a/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IDistributionOperationsTests.cs @@ -45,4 +45,4 @@ await Assert.ThrowsAsync( string.Empty, _testFixture.Cts.Token)); } -} +} \ No newline at end of file