From ab27fe30f3bb0887b9c0a2d58103202b45523a3c Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Thu, 8 Aug 2024 23:17:16 -0400 Subject: [PATCH 01/17] Modernizing. --- Docker.Registry.DotNet.sln | 4 +- Docker.Registry.DotNet.sln.DotSettings | 3 +- .../Docker.Registry.Cli.csproj | 2 +- .../AnonymousOAuthAuthenticationProvider.cs | 31 +- .../Authentication/AuthenticateParser.cs | 144 +++-- .../Authentication/AuthenticationProvider.cs | 76 ++- .../BasicAuthenticationProvider.cs | 59 +- .../Authentication/ParsedAuthentication.cs | 20 +- .../PasswordOAuthAuthenticationProvider.cs | 46 +- .../DictionaryExtensions.cs | 4 - .../Docker.Registry.DotNet.csproj | 50 +- .../Endpoints/IBlobOperations.cs | 82 ++- .../Endpoints/IBlobUploadOperations.cs | 259 +++++---- .../Endpoints/ICatalogOperations.cs | 32 +- .../Endpoints/IManifestOperations.cs | 7 - .../Endpoints/ISystemOperations.cs | 14 +- .../Endpoints/ITagOperations.cs | 22 +- .../Implementations/BlobOperations.cs | 57 +- .../Implementations/BlobUploadOperations.cs | 147 +++-- .../Implementations/CatalogOperations.cs | 31 +- .../Implementations/ManifestOperations.cs | 120 ++--- .../Implementations/SystemOperations.cs | 22 +- .../Implementations/TagOperations.cs | 33 +- src/Docker.Registry.DotNet/GlobalUsings.cs | 38 ++ .../Helpers/EnumerableExtensions.cs | 12 +- .../Helpers/HttpUtility.cs | 163 +++--- .../Helpers/IDictionaryExtensions.cs | 19 +- .../Helpers/IQueryString.cs | 9 +- .../Helpers/JsonSerializer.cs | 42 +- .../Helpers/QueryString.cs | 25 +- .../Helpers/QueryStringExtensions.cs | 93 ++-- .../Helpers/StringExtensions.cs | 16 +- .../Models/BlobHeader.cs | 17 +- .../Models/BlobUploadStatus.cs | 27 +- src/Docker.Registry.DotNet/Models/Catalog.cs | 13 +- .../Models/CatalogParameters.cs | 31 +- .../Models/CompletedUploadResponse.cs | 25 +- src/Docker.Registry.DotNet/Models/Config.cs | 61 +-- .../Models/GetBlobResponse.cs | 18 +- .../Models/GetImageManifestResult.cs | 31 +- .../Models/ImageManifest.cs | 19 +- .../Models/ImageManifest2_1.cs | 73 ++- .../Models/ImageManifest2_2.cs | 43 +- .../InitiateMonolithicUploadResponse.cs | 13 +- .../Models/ListImageTagsParameters.cs | 17 +- .../Models/ListImageTagsResponse.cs | 17 +- src/Docker.Registry.DotNet/Models/Manifest.cs | 69 ++- .../Models/ManifestFsLayer.cs | 13 +- .../Models/ManifestHistory.cs | 13 +- .../Models/ManifestLayer.cs | 63 ++- .../Models/ManifestList.cs | 35 +- .../Models/ManifestMediaTypes.cs | 84 ++- .../Models/ManifestSignature.cs | 21 +- .../Models/ManifestSignatureHeader.cs | 13 +- .../Models/MountParameters.cs | 25 +- .../Models/MountResponse.cs | 33 +- src/Docker.Registry.DotNet/Models/Platform.cs | 75 ++- .../Models/PushManifestResponse.cs | 31 +- .../Models/ResumableUpload.cs | 33 +- .../OAuth/OAuthClient.cs | 149 +++--- .../OAuth/OAuthToken.cs | 26 +- .../QueryParameterAttribute.cs | 16 +- .../Registry/IRegistryClient.cs | 73 ++- .../Registry/NetworkClient.cs | 503 +++++++++--------- .../Registry/RegistryApiException.cs | 39 +- .../Registry/RegistryApiResponse.cs | 40 +- .../Registry/RegistryClient.cs | 61 +-- .../Registry/RegistryConnectionException.cs | 37 +- .../Registry/UnauthorizedApiException.cs | 17 +- .../RegistryClientConfiguration.cs | 144 +++-- .../Docker.Registry.DotNet.Tests.csproj | 38 +- 71 files changed, 1675 insertions(+), 2063 deletions(-) create mode 100644 src/Docker.Registry.DotNet/GlobalUsings.cs diff --git a/Docker.Registry.DotNet.sln b/Docker.Registry.DotNet.sln index abeccec..5ea2e1e 100644 --- a/Docker.Registry.DotNet.sln +++ b/Docker.Registry.DotNet.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32414.318 @@ -19,7 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "0 - Assets", "0 - Assets", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "3 - Samples", "3 - Samples", "{BBCE947D-B371-4738-98A2-F3FA9E934BF6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DockerRegistryExplorer", "samples\DockerRegistryExplorer\DockerRegistryExplorer.csproj", "{513C7B92-BFAF-4E0A-B8D9-FC0E7283CD62}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DockerRegistryExplorer", "samples\DockerRegistryExplorer\DockerRegistryExplorer.csproj", "{513C7B92-BFAF-4E0A-B8D9-FC0E7283CD62}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.Registry.Cli", "samples\Docker.Registry.Cli\Docker.Registry.Cli.csproj", "{DE73EA84-AE2A-4060-AA59-0EE409845232}" EndProject diff --git a/Docker.Registry.DotNet.sln.DotSettings b/Docker.Registry.DotNet.sln.DotSettings index 0c239a6..39b2b43 100644 --- a/Docker.Registry.DotNet.sln.DotSettings +++ b/Docker.Registry.DotNet.sln.DotSettings @@ -1,5 +1,5 @@  - Copyright 2017-$CURRENT_YEAR$ Rich Quackenbush, Jaben Cargman + Copyright 2017-${CurrentDate.Year} Rich Quackenbush, Jaben Cargman and Docker.Registry.DotNet Contributors Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + True True True True \ No newline at end of file diff --git a/samples/Docker.Registry.Cli/Docker.Registry.Cli.csproj b/samples/Docker.Registry.Cli/Docker.Registry.Cli.csproj index 132be79..ba7ae57 100644 --- a/samples/Docker.Registry.Cli/Docker.Registry.Cli.csproj +++ b/samples/Docker.Registry.Cli/Docker.Registry.Cli.csproj @@ -2,7 +2,7 @@ Exe - net6 + net8.0 diff --git a/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs index 20f3223..00ccf31 100644 --- a/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs @@ -13,32 +13,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Authentication; -using Docker.Registry.DotNet.OAuth; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Authentication +[PublicAPI] +public class AnonymousOAuthAuthenticationProvider : AuthenticationProvider { - [PublicAPI] - public class AnonymousOAuthAuthenticationProvider : AuthenticationProvider - { - private readonly OAuthClient _client = new OAuthClient(); + private readonly OAuthClient _client = new OAuthClient(); - private static string Schema { get; } = "Bearer"; + private static string Schema { get; } = "Bearer"; - public override Task AuthenticateAsync(HttpRequestMessage request) - { + public override Task AuthenticateAsync(HttpRequestMessage request) + { return Task.CompletedTask; } - public override async Task AuthenticateAsync( - HttpRequestMessage request, - HttpResponseMessage response) - { + public override async Task AuthenticateAsync( + HttpRequestMessage request, + HttpResponseMessage response) + { var header = this.TryGetSchemaHeader(response, Schema); //Get the bearer bits @@ -53,5 +45,4 @@ public override async Task AuthenticateAsync( //Set the header request.Headers.Authorization = new AuthenticationHeaderValue(Schema, token.Token); } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/AuthenticateParser.cs b/src/Docker.Registry.DotNet/Authentication/AuthenticateParser.cs index d50d131..de47364 100644 --- a/src/Docker.Registry.DotNet/Authentication/AuthenticateParser.cs +++ b/src/Docker.Registry.DotNet/Authentication/AuthenticateParser.cs @@ -13,105 +13,97 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +namespace Docker.Registry.DotNet.Authentication; -namespace Docker.Registry.DotNet.Authentication +internal static class AuthenticateParser { - internal static class AuthenticateParser + public static IDictionary Parse(string value) { - public static IDictionary Parse(string value) - { - //https://stackoverflow.com/questions/45516717/extracting-and-parsing-the-www-authenticate-header-from-httpresponsemessage-in/45516809#45516809 - return SplitWWWAuthenticateHeader(value).ToDictionary(GetKey, GetValue); - } + //https://stackoverflow.com/questions/45516717/extracting-and-parsing-the-www-authenticate-header-from-httpresponsemessage-in/45516809#45516809 + return SplitWWWAuthenticateHeader(value).ToDictionary(GetKey, GetValue); + } - private static IEnumerable SplitWWWAuthenticateHeader(string value) + private static IEnumerable SplitWWWAuthenticateHeader(string value) + { + var builder = new StringBuilder(); + var inQuotes = false; + for (var i = 0; i < value.Length; i++) { - var builder = new StringBuilder(); - var inQuotes = false; - for (var i = 0; i < value.Length; i++) + var charI = value[i]; + switch (charI) { - var charI = value[i]; - switch (charI) - { - case '\"': - if (inQuotes) + case '\"': + if (inQuotes) + { + yield return builder.ToString(); + builder.Clear(); + inQuotes = false; + } + else + { + inQuotes = true; + } + + break; + + case ',': + if (inQuotes) + { + builder.Append(charI); + } + else + { + if (builder.Length > 0) { yield return builder.ToString(); builder.Clear(); - inQuotes = false; - } - else - { - inQuotes = true; } + } - break; + break; - case ',': - if (inQuotes) - { - builder.Append(charI); - } - else - { - if (builder.Length > 0) - { - yield return builder.ToString(); - builder.Clear(); - } - } - - break; - - default: - builder.Append(charI); - break; - } + default: + builder.Append(charI); + break; } - - if (builder.Length > 0) yield return builder.ToString(); } - public static ParsedAuthentication ParseTyped(string value) - { - var parsed = Parse(value); + if (builder.Length > 0) yield return builder.ToString(); + } - return new ParsedAuthentication( - parsed.GetValueOrDefault("realm"), - parsed.GetValueOrDefault("service"), - parsed.GetValueOrDefault("scope")); - } + public static ParsedAuthentication ParseTyped(string value) + { + var parsed = Parse(value); - private static string GetKey(string pair) - { - int equalPos = pair.IndexOf("=", StringComparison.Ordinal); + return new ParsedAuthentication( + parsed.GetValueOrDefault("realm"), + parsed.GetValueOrDefault("service"), + parsed.GetValueOrDefault("scope")); + } - if (equalPos < 1) - throw new FormatException("No '=' found."); + private static string GetKey(string pair) + { + var equalPos = pair.IndexOf("=", StringComparison.Ordinal); - return pair.Substring(0, equalPos); - } + if (equalPos < 1) + throw new FormatException("No '=' found."); - private static string GetValue(string pair) - { - int equalPos = pair.IndexOf("=", StringComparison.Ordinal); + return pair.Substring(0, equalPos); + } - if (equalPos < 1) - throw new FormatException("No '=' found."); + private static string GetValue(string pair) + { + var equalPos = pair.IndexOf("=", StringComparison.Ordinal); - string value = pair.Substring(equalPos + 1).Trim(); + if (equalPos < 1) + throw new FormatException("No '=' found."); - //Trim quotes - if (value.StartsWith("\"") && value.EndsWith("\"")) - { - value = value.Substring(1, value.Length - 2); - } + var value = pair.Substring(equalPos + 1).Trim(); - return value; - } + //Trim quotes + if (value.StartsWith("\"") && value.EndsWith("\"")) + value = value.Substring(1, value.Length - 2); + + return value; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs index cfc4a31..80dc535 100644 --- a/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs @@ -13,56 +13,46 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Authentication; -using Docker.Registry.DotNet.Helpers; - -namespace Docker.Registry.DotNet.Authentication +/// +/// Authentication provider. +/// +public abstract class AuthenticationProvider { /// - /// Authentication provider. + /// Called on the initial send /// - public abstract class AuthenticationProvider - { - /// - /// Called on the initial send - /// - /// - /// - public abstract Task AuthenticateAsync(HttpRequestMessage request); + /// + /// + public abstract Task AuthenticateAsync(HttpRequestMessage request); - /// - /// Called when the send is challenged. - /// - /// - /// - /// - public abstract Task AuthenticateAsync( - HttpRequestMessage request, - HttpResponseMessage response); + /// + /// Called when the send is challenged. + /// + /// + /// + /// + public abstract Task AuthenticateAsync( + HttpRequestMessage request, + HttpResponseMessage response); - /// - /// Gets the schema header from the http response. - /// - /// - /// - /// - protected AuthenticationHeaderValue TryGetSchemaHeader( - HttpResponseMessage response, - string schema) - { - var header = response.GetHeaderBySchema(schema); + /// + /// Gets the schema header from the http response. + /// + /// + /// + /// + protected AuthenticationHeaderValue TryGetSchemaHeader( + HttpResponseMessage response, + string schema) + { + var header = response.GetHeaderBySchema(schema); - if (header == null) - { - throw new InvalidOperationException( - $"No WWW-Authenticate challenge was found for schema {schema}"); - } + if (header == null) + throw new InvalidOperationException( + $"No WWW-Authenticate challenge was found for schema {schema}"); - return header; - } + return header; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs index f843cd3..c189d2f 100644 --- a/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs @@ -1,4 +1,4 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman // and Docker.Registry.DotNet Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,50 +13,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Authentication; -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Authentication +[PublicAPI] +public class BasicAuthenticationProvider(string username, string password) : AuthenticationProvider { - [PublicAPI] - public class BasicAuthenticationProvider : AuthenticationProvider - { - private readonly string _password; - - private readonly string _username; - - public BasicAuthenticationProvider(string username, string password) - { - this._username = username; - this._password = password; - } + private static string Schema { get; } = "Basic"; - private static string Schema { get; } = "Basic"; - - public override Task AuthenticateAsync(HttpRequestMessage request) - { - return Task.CompletedTask; - } + public override Task AuthenticateAsync(HttpRequestMessage request) + { + return Task.CompletedTask; + } - public override Task AuthenticateAsync( - HttpRequestMessage request, - HttpResponseMessage response) - { - this.TryGetSchemaHeader(response, Schema); + public override Task AuthenticateAsync( + HttpRequestMessage request, + HttpResponseMessage response) + { + this.TryGetSchemaHeader(response, Schema); - var passBytes = Encoding.UTF8.GetBytes($"{this._username}:{this._password}"); - var base64Pass = Convert.ToBase64String(passBytes); + var passBytes = Encoding.UTF8.GetBytes($"{username}:{password}"); + var base64Pass = Convert.ToBase64String(passBytes); - //Set the header - request.Headers.Authorization = - new AuthenticationHeaderValue(Schema, base64Pass); + //Set the header + request.Headers.Authorization = + new AuthenticationHeaderValue(Schema, base64Pass); - return Task.CompletedTask; - } + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/ParsedAuthentication.cs b/src/Docker.Registry.DotNet/Authentication/ParsedAuthentication.cs index 36e64ae..8c5dced 100644 --- a/src/Docker.Registry.DotNet/Authentication/ParsedAuthentication.cs +++ b/src/Docker.Registry.DotNet/Authentication/ParsedAuthentication.cs @@ -13,21 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Authentication -{ - internal class ParsedAuthentication - { - public ParsedAuthentication(string realm, string service, string scope) - { - Realm = realm; - Service = service; - Scope = scope; - } +namespace Docker.Registry.DotNet.Authentication; - public string Realm { get; } +internal class ParsedAuthentication(string realm, string service, string scope) +{ + public string Realm { get; } = realm; - public string Service { get; } + public string Service { get; } = service; - public string Scope { get; } - } + public string Scope { get; } = scope; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs index c2a48ca..c203979 100644 --- a/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs @@ -13,42 +13,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Authentication; -using Docker.Registry.DotNet.OAuth; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Authentication +[PublicAPI] +public class PasswordOAuthAuthenticationProvider(string username, string password) + : AuthenticationProvider { - [PublicAPI] - public class PasswordOAuthAuthenticationProvider : AuthenticationProvider - { - private readonly OAuthClient _client = new OAuthClient(); + private readonly OAuthClient _client = new OAuthClient(); - private readonly string _password; + private static string Schema { get; } = "Bearer"; - private readonly string _username; - - public PasswordOAuthAuthenticationProvider(string username, string password) - { - this._username = username; - this._password = password; - } - - private static string Schema { get; } = "Bearer"; - - public override Task AuthenticateAsync(HttpRequestMessage request) - { + public override Task AuthenticateAsync(HttpRequestMessage request) + { return Task.CompletedTask; } - public override async Task AuthenticateAsync( - HttpRequestMessage request, - HttpResponseMessage response) - { + public override async Task AuthenticateAsync( + HttpRequestMessage request, + HttpResponseMessage response) + { var header = this.TryGetSchemaHeader(response, Schema); //Get the bearer bits @@ -68,12 +51,11 @@ public override async Task AuthenticateAsync( bearerBits.Realm, bearerBits.Service, scope, - this._username, - this._password); + username, + password); //Set the header request.Headers.Authorization = new AuthenticationHeaderValue(Schema, token.AccessToken); } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/DictionaryExtensions.cs b/src/Docker.Registry.DotNet/DictionaryExtensions.cs index 151aec3..b3abfb4 100644 --- a/src/Docker.Registry.DotNet/DictionaryExtensions.cs +++ b/src/Docker.Registry.DotNet/DictionaryExtensions.cs @@ -13,10 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; -using System.Linq; - namespace Docker.Registry.DotNet; internal static class DictionaryExtensions diff --git a/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj b/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj index 703ef90..4c28218 100644 --- a/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj +++ b/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj @@ -1,14 +1,15 @@  - net46;netstandard1.6;netstandard2.0 - 1.6.1 + netstandard2.0;net6.0;net7.0;net8.0 bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml latest + enable + enable 1701;1702;1591 - 1.2.0 + 1.3.0 Docker.Registry.DotNet Rich Quackenbush, Jaben Cargman and the Docker.Registry.DotNet Contributors Copyright © Rich Quackenbush, Jaben Cargman and the Docker.Registry.DotNet Contributors 2017-2022 @@ -30,42 +31,19 @@ - - - - - TRACE - - - - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs b/src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs index dd11bbb..12c5d54 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs @@ -13,54 +13,46 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Endpoints; -using Docker.Registry.DotNet.Models; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Endpoints +[PublicAPI] +public interface IBlobOperations { + /// + /// Retrieve the blob from the registry identified by digest. Performs a monolithic download of the blob. + /// + /// + /// + /// + /// [PublicAPI] - public interface IBlobOperations - { - /// - /// Retrieve the blob from the registry identified by digest. Performs a monolithic download of the blob. - /// - /// - /// - /// - /// - [PublicAPI] - Task GetBlobAsync( - string name, - string digest, - CancellationToken cancellationToken = default); + Task GetBlobAsync( + string name, + string digest, + CancellationToken cancellationToken = default); - /// - /// Delete the blob identified by name and digest. - /// - /// - /// - /// - /// - [PublicAPI] - Task DeleteBlobAsync( - string name, - string digest, - CancellationToken cancellationToken = default); + /// + /// Delete the blob identified by name and digest. + /// + /// + /// + /// + /// + [PublicAPI] + Task DeleteBlobAsync( + string name, + string digest, + CancellationToken cancellationToken = default); - /// - /// Existing Layers - /// - /// - /// - /// - /// - Task IsExistBlobAsync( - string name, - string digest, - CancellationToken cancellationToken = default); - } + /// + /// Existing Layers + /// + /// + /// + /// + /// + Task IsExistBlobAsync( + string name, + string digest, + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs b/src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs index 1970df7..00e2448 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs @@ -13,149 +13,140 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.IO; -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Endpoints; -using Docker.Registry.DotNet.Models; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Endpoints +[PublicAPI] +public interface IBlobUploadOperations { + /// + /// + /// + /// + /// + /// + /// + /// [PublicAPI] - public interface IBlobUploadOperations - { - /// - /// - /// - /// - /// - /// - /// - /// - [PublicAPI] - Task UploadBlobAsync( - string name, - int contentLength, - Stream stream, - string digest, - CancellationToken cancellationToken = default); + Task UploadBlobAsync( + string name, + int contentLength, + Stream stream, + string digest, + CancellationToken cancellationToken = default); - /// - /// Initiate a resumable blob upload. If successful, an upload location will be provided to complete the upload. - /// Optionally, if the digest parameter is present, the request body will be used to complete the upload in a single - /// request. - /// - /// - /// - /// - /// - [PublicAPI] - Task InitiateBlobUploadAsync( - string name, - Stream stream = null, - CancellationToken cancellationToken = default); + /// + /// Initiate a resumable blob upload. If successful, an upload location will be provided to complete the upload. + /// Optionally, if the digest parameter is present, the request body will be used to complete the upload in a single + /// request. + /// + /// + /// + /// + /// + [PublicAPI] + Task InitiateBlobUploadAsync( + string name, + Stream? stream = null, + CancellationToken cancellationToken = default); - /// - /// Mount a blob identified by the mount parameter from another repository. - /// - /// - /// - /// - /// - [PublicAPI] - Task MountBlobAsync( - string name, - MountParameters parameters, - CancellationToken cancellationToken = default); + /// + /// Mount a blob identified by the mount parameter from another repository. + /// + /// + /// + /// + /// + [PublicAPI] + Task MountBlobAsync( + string name, + MountParameters parameters, + CancellationToken cancellationToken = default); - /// - /// Retrieve status of upload identified by uuid. The primary purpose of this endpoint is to resolve the current status - /// of a resumable upload. - /// - /// - /// - /// - /// - [PublicAPI] - Task GetBlobUploadStatus( - string name, - string uuid, - CancellationToken cancellationToken = default); + /// + /// Retrieve status of upload identified by uuid. The primary purpose of this endpoint is to resolve the current status + /// of a resumable upload. + /// + /// + /// + /// + /// + [PublicAPI] + Task GetBlobUploadStatus( + string name, + string uuid, + CancellationToken cancellationToken = default); - /// - /// Upload a chunk of data for the specified upload. - /// - /// - /// - /// - /// - /// - /// - [PublicAPI] - Task UploadBlobChunkAsync( - ResumableUpload resumable, - Stream chunk, - long? from = null, - long? to = null, - CancellationToken cancellationToken = default); + /// + /// Upload a chunk of data for the specified upload. + /// + /// + /// + /// + /// + /// + /// + [PublicAPI] + Task UploadBlobChunkAsync( + ResumableUpload resumable, + Stream chunk, + long? from = null, + long? to = null, + CancellationToken cancellationToken = default); - /// - /// Complete the upload specified by ResumableUploadResponse, optionally appending the body as the final chunk. - /// - /// - /// - /// - /// - /// - /// - /// - [PublicAPI] - Task CompleteBlobUploadAsync( - ResumableUpload resumable, - string digest, - Stream chunk = null, - long? from = null, - long? to = null, - CancellationToken cancellationToken = default); + /// + /// Complete the upload specified by ResumableUploadResponse, optionally appending the body as the final chunk. + /// + /// + /// + /// + /// + /// + /// + /// + [PublicAPI] + Task CompleteBlobUploadAsync( + ResumableUpload resumable, + string digest, + Stream? chunk = null, + long? from = null, + long? to = null, + CancellationToken cancellationToken = default); - /// - /// Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished uploads - /// will eventually timeout. - /// - /// - /// - /// - /// - [PublicAPI] - Task CancelBlobUploadAsync( - string name, - string uuid, - CancellationToken cancellationToken = default); + /// + /// Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished uploads + /// will eventually timeout. + /// + /// + /// + /// + /// + [PublicAPI] + Task CancelBlobUploadAsync( + string name, + string uuid, + CancellationToken cancellationToken = default); - /// - /// Starting An Upload - /// - /// - /// - /// - Task StartUploadBlobAsync( - string name, - CancellationToken cancellationToken = default); + /// + /// Starting An Upload + /// + /// + /// + /// + Task StartUploadBlobAsync( + string name, + CancellationToken cancellationToken = default); - /// - /// A monolithic upload is simply a chunked upload with a single chunk and may be favored by clients that would like to avoided the complexity of chunking - /// - /// - /// - /// - /// - /// - Task MonolithicUploadBlobAsync( - ResumableUpload resumable, - string digest, - Stream stream, - CancellationToken cancellationToken = default); - } + /// + /// A monolithic upload is simply a chunked upload with a single chunk and may be favored by clients that would like to avoided the complexity of chunking + /// + /// + /// + /// + /// + /// + Task MonolithicUploadBlobAsync( + ResumableUpload resumable, + string digest, + Stream stream, + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs b/src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs index 0ff10fa..787a74d 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs @@ -13,27 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Endpoints; -using Docker.Registry.DotNet.Models; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Endpoints +[PublicAPI] +public interface ICatalogOperations { + /// + /// Retrieve a sorted, json list of repositories available in the registry. + /// + /// + /// + /// [PublicAPI] - public interface ICatalogOperations - { - /// - /// Retrieve a sorted, json list of repositories available in the registry. - /// - /// - /// - /// - [PublicAPI] - Task GetCatalogAsync( - CatalogParameters parameters = null, - CancellationToken cancellationToken = default); - } + Task GetCatalogAsync( + CatalogParameters? parameters = null, + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs b/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs index 7dd0bf8..05d14c1 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs @@ -13,13 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Threading; -using System.Threading.Tasks; - -using Docker.Registry.DotNet.Models; - -using JetBrains.Annotations; - namespace Docker.Registry.DotNet.Endpoints; /// diff --git a/src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs b/src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs index 228710c..d07dfb8 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs @@ -13,17 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Endpoints; -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Endpoints +[PublicAPI] +public interface ISystemOperations { [PublicAPI] - public interface ISystemOperations - { - [PublicAPI] - Task PingAsync(CancellationToken cancellationToken = default); - } + Task PingAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs b/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs index 1788c4a..d21bd27 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs @@ -13,22 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Endpoints; -using Docker.Registry.DotNet.Models; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Endpoints +[PublicAPI] +public interface ITagOperations { [PublicAPI] - public interface ITagOperations - { - [PublicAPI] - Task ListImageTagsAsync( - string name, - ListImageTagsParameters parameters = null, - CancellationToken cancellationToken = default); - } + Task ListImageTagsAsync( + string name, + ListImageTagsParameters? parameters = null, + CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs index c0a473d..0144ee8 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs @@ -13,34 +13,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Endpoints.Implementations; -using Docker.Registry.DotNet.Helpers; -using Docker.Registry.DotNet.Models; -using Docker.Registry.DotNet.Registry; - -namespace Docker.Registry.DotNet.Endpoints.Implementations +internal class BlobOperations(NetworkClient client) : IBlobOperations { - internal class BlobOperations : IBlobOperations + public async Task GetBlobAsync( + string name, + string digest, + CancellationToken cancellationToken = default) { - private readonly NetworkClient _client; - - public BlobOperations(NetworkClient client) - { - this._client = client; - } - - public async Task GetBlobAsync( - string name, - string digest, - CancellationToken cancellationToken = default) - { var url = $"v2/{name}/blobs/{digest}"; - var response = await this._client.MakeRequestForStreamedResponseAsync( + var response = await client.MakeRequestForStreamedResponseAsync( cancellationToken, HttpMethod.Get, url); @@ -50,32 +34,31 @@ public async Task GetBlobAsync( response.Body); } - public Task DeleteBlobAsync( - string name, - string digest, - CancellationToken cancellationToken = default) - { + public Task DeleteBlobAsync( + string name, + string digest, + CancellationToken cancellationToken = default) + { var url = $"v2/{name}/blobs/{digest}"; - return this._client.MakeRequestAsync(cancellationToken, HttpMethod.Delete, url); + return client.MakeRequestAsync(cancellationToken, HttpMethod.Delete, url); } - public async Task IsExistBlobAsync( - string name, - string digest, - CancellationToken cancellationToken = default) - { + public async Task IsExistBlobAsync( + string name, + string digest, + CancellationToken cancellationToken = default) + { var path = $"v2/{name}/blobs/{digest}"; - var response = await this._client.MakeRequestNotErrorAsync( + var response = await client.MakeRequestNotErrorAsync( cancellationToken, HttpMethod.Head, path); if (response.StatusCode != HttpStatusCode.NotFound) - this._client.HandleIfErrorResponse(response); + client.HandleIfErrorResponse(response); return response.StatusCode == HttpStatusCode.OK; } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs index 5b40145..0648903 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs @@ -13,33 +13,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; - -using Docker.Registry.DotNet.Helpers; -using Docker.Registry.DotNet.Models; -using Docker.Registry.DotNet.Registry; - -namespace Docker.Registry.DotNet.Endpoints.Implementations +namespace Docker.Registry.DotNet.Endpoints.Implementations; + +internal class BlobUploadOperations : IBlobUploadOperations { - internal class BlobUploadOperations : IBlobUploadOperations - { - private readonly NetworkClient _client; + private readonly NetworkClient _client; - internal BlobUploadOperations(NetworkClient client) - { + internal BlobUploadOperations(NetworkClient client) + { this._client = client; } - public async Task StartUploadBlobAsync( - string name, - CancellationToken cancellationToken = default) - { + public async Task StartUploadBlobAsync( + string name, + CancellationToken cancellationToken = default) + { var path = $"v2/{name}/blobs/uploads/"; var response = await this._client.MakeRequestAsync( cancellationToken, @@ -53,12 +41,12 @@ public async Task StartUploadBlobAsync( }; } - public Task MonolithicUploadBlobAsync( - ResumableUpload resumable, - string digest, - Stream stream, - CancellationToken cancellationToken = default) - { + public Task MonolithicUploadBlobAsync( + ResumableUpload resumable, + string digest, + Stream stream, + CancellationToken cancellationToken = default) + { return CompleteBlobUploadAsync( resumable, digest, @@ -66,19 +54,19 @@ public Task MonolithicUploadBlobAsync( cancellationToken: cancellationToken); } - public Task InitiateBlobUploadAsync( - string name, - Stream stream = null, - CancellationToken cancellationToken = default) - { + public Task InitiateBlobUploadAsync( + string name, + Stream? stream = null, + CancellationToken cancellationToken = default) + { throw new NotImplementedException(); } - public async Task MountBlobAsync( - string name, - MountParameters parameters, - CancellationToken cancellationToken = default) - { + public async Task MountBlobAsync( + string name, + MountParameters parameters, + CancellationToken cancellationToken = default) + { var queryString = new QueryString(); queryString.Add("mount", parameters.Digest); queryString.Add("from", parameters.From); @@ -96,11 +84,11 @@ public async Task MountBlobAsync( }; } - public async Task GetBlobUploadStatus( - string name, - string uuid, - CancellationToken cancellationToken = default) - { + public async Task GetBlobUploadStatus( + string name, + string uuid, + CancellationToken cancellationToken = default) + { var response = await this._client.MakeRequestAsync( cancellationToken, HttpMethod.Get, @@ -113,13 +101,13 @@ public async Task GetBlobUploadStatus( }; } - public async Task UploadBlobChunkAsync( - ResumableUpload resumable, - Stream chunk, - long? from = null, - long? to = null, - CancellationToken cancellationToken = default) - { + public async Task UploadBlobChunkAsync( + ResumableUpload resumable, + Stream chunk, + long? from = null, + long? to = null, + CancellationToken cancellationToken = default) + { var response = await this._client.MakeRequestAsync( cancellationToken, new HttpMethod("PATCH"), @@ -144,14 +132,14 @@ public async Task UploadBlobChunkAsync( }; } - public async Task CompleteBlobUploadAsync( - ResumableUpload resumable, - string digest, - Stream chunk = null, - long? from = null, - long? to = null, - CancellationToken cancellationToken = default) - { + public async Task CompleteBlobUploadAsync( + ResumableUpload resumable, + string digest, + Stream? chunk = null, + long? from = null, + long? to = null, + CancellationToken cancellationToken = default) + { var queryString = new QueryString(); queryString.Add("digest", digest); @@ -180,32 +168,32 @@ public async Task CompleteBlobUploadAsync( }; } - public Task CancelBlobUploadAsync( - string name, - string uuid, - CancellationToken cancellationToken = default) - { + public Task CancelBlobUploadAsync( + string name, + string uuid, + CancellationToken cancellationToken = default) + { var path = $"v2/{name}/blobs/uploads/{uuid}"; return this._client.MakeRequestAsync(cancellationToken, HttpMethod.Delete, path); } - /// - /// Perform a monolithic upload. - /// - /// - /// - /// - /// - /// - /// - public async Task UploadBlobAsync( - string name, - int contentLength, - Stream stream, - string digest, - CancellationToken cancellationToken = default) - { + /// + /// Perform a monolithic upload. + /// + /// + /// + /// + /// + /// + /// + public async Task UploadBlobAsync( + string name, + int contentLength, + Stream stream, + string digest, + CancellationToken cancellationToken = default) + { var path = $"v2/{name}/blobs/uploads/"; var response = await this._client.MakeRequestAsync( @@ -314,5 +302,4 @@ await this._client.MakeRequestAsync( //await _client.MakeRequestAsync(cancellationToken, HttpMethod.Put, location, queryString); } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs index 3be09cb..11de1b8 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs @@ -13,32 +13,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Endpoints.Implementations; -using Docker.Registry.DotNet.Helpers; -using Docker.Registry.DotNet.Models; -using Docker.Registry.DotNet.Registry; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Endpoints.Implementations +internal class CatalogOperations(NetworkClient client) : ICatalogOperations { - internal class CatalogOperations : ICatalogOperations - { - private readonly NetworkClient _client; + private readonly NetworkClient _client = client ?? throw new ArgumentNullException(nameof(client)); - public CatalogOperations([NotNull] NetworkClient client) - { - this._client = client ?? throw new ArgumentNullException(nameof(client)); - } - - public async Task GetCatalogAsync( - CatalogParameters parameters = null, - CancellationToken cancellationToken = default) - { + public async Task GetCatalogAsync( + CatalogParameters? parameters = null, + CancellationToken cancellationToken = default) + { parameters = parameters ?? new CatalogParameters(); var queryParameters = new QueryString(); @@ -53,5 +37,4 @@ public async Task GetCatalogAsync( return this._client.JsonSerializer.DeserializeObject(response.Body); } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs index 39bfec0..f9e0bab 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs @@ -13,38 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Runtime.Serialization; -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Endpoints.Implementations; -using Docker.Registry.DotNet.Helpers; -using Docker.Registry.DotNet.Models; -using Docker.Registry.DotNet.Registry; - -using JetBrains.Annotations; - -using Newtonsoft.Json; - -namespace Docker.Registry.DotNet.Endpoints.Implementations +internal class ManifestOperations(NetworkClient client) : IManifestOperations { - internal class ManifestOperations : IManifestOperations + public async Task GetManifestAsync( + string name, + string reference, + CancellationToken cancellationToken = default) { - private readonly NetworkClient _client; - - public ManifestOperations(NetworkClient client) - { - this._client = client; - } - - public async Task GetManifestAsync( - string name, - string reference, - CancellationToken cancellationToken = default) - { var headers = new Dictionary { { @@ -53,7 +30,7 @@ public async Task GetManifestAsync( } }; - var response = await this._client.MakeRequestAsync( + var response = await client.MakeRequestAsync( cancellationToken, HttpMethod.Head, $"v2/{name}/manifests/{reference}", @@ -62,7 +39,7 @@ public async Task GetManifestAsync( var digestReference = response.GetHeader("Docker-Content-Digest"); - response = await this._client.MakeRequestAsync( + response = await client.MakeRequestAsync( cancellationToken, HttpMethod.Get, $"v2/{name}/manifests/{digestReference}", @@ -77,7 +54,7 @@ public async Task GetManifestAsync( case ManifestMediaTypes.ManifestSchema1Signed: return new GetImageManifestResult( contentType, - this._client.JsonSerializer.DeserializeObject( + client.JsonSerializer.DeserializeObject( response.Body), response.Body) { @@ -88,7 +65,7 @@ public async Task GetManifestAsync( case ManifestMediaTypes.ManifestSchema2: return new GetImageManifestResult( contentType, - this._client.JsonSerializer.DeserializeObject( + client.JsonSerializer.DeserializeObject( response.Body), response.Body) { @@ -98,7 +75,7 @@ public async Task GetManifestAsync( case ManifestMediaTypes.ManifestList: return new GetImageManifestResult( contentType, - this._client.JsonSerializer.DeserializeObject(response.Body), + client.JsonSerializer.DeserializeObject(response.Body), response.Body); default: @@ -106,12 +83,12 @@ public async Task GetManifestAsync( } } - public async Task PutManifestAsync( - string name, - string reference, - ImageManifest manifest, - CancellationToken cancellationToken) - { + public async Task PutManifestAsync( + string name, + string reference, + ImageManifest manifest, + CancellationToken cancellationToken) + { string manifestMediaType = null; if (manifest is ImageManifest2_1) manifestMediaType = ManifestMediaTypes.ManifestSchema1; @@ -120,14 +97,14 @@ public async Task PutManifestAsync( if (manifest is ManifestList) manifestMediaType = ManifestMediaTypes.ManifestList; - var response = await this._client.MakeRequestAsync( + var response = await client.MakeRequestAsync( cancellationToken, HttpMethod.Put, $"v2/{name}/manifests/{reference}", content: () => { var content = new StringContent( - this._client.JsonSerializer.SerializeObject(manifest)); + client.JsonSerializer.SerializeObject(manifest)); content.Headers.ContentType = new MediaTypeHeaderValue(manifestMediaType); return content; @@ -141,23 +118,23 @@ public async Task PutManifestAsync( }; } - //public Task DoesManifestExistAsync(string name, string reference, CancellationToken cancellation = default) - //{ - // throw new NotImplementedException(); - //} + //public Task DoesManifestExistAsync(string name, string reference, CancellationToken cancellation = default) + //{ + // throw new NotImplementedException(); + //} - public async Task DeleteManifestAsync( - string name, - string reference, - CancellationToken cancellationToken = default) - { + public async Task DeleteManifestAsync( + string name, + string reference, + CancellationToken cancellationToken = default) + { var path = $"v2/{name}/manifests/{reference}"; - await this._client.MakeRequestAsync(cancellationToken, HttpMethod.Delete, path); + await client.MakeRequestAsync(cancellationToken, HttpMethod.Delete, path); } - private string GetContentType(string contentTypeHeader, string manifest) - { + private string GetContentType(string contentTypeHeader, string manifest) + { if (!string.IsNullOrWhiteSpace(contentTypeHeader)) return contentTypeHeader; @@ -176,12 +153,12 @@ private string GetContentType(string contentTypeHeader, string manifest) $"Unable to determine schema type from version {check.SchemaVersion}"); } - [PublicAPI] - public async Task GetManifestRawAsync( - string name, - string reference, - CancellationToken cancellationToken) - { + [PublicAPI] + public async Task GetManifestRawAsync( + string name, + string reference, + CancellationToken cancellationToken) + { var headers = new Dictionary { { @@ -190,7 +167,7 @@ public async Task GetManifestRawAsync( } }; - var response = await this._client.MakeRequestAsync( + var response = await client.MakeRequestAsync( cancellationToken, HttpMethod.Get, $"v2/{name}/manifests/{reference}", @@ -200,16 +177,15 @@ public async Task GetManifestRawAsync( return response.Body; } - private class SchemaCheck - { - /// - /// This field specifies the image manifest schema version as an integer. - /// - [DataMember(Name = "schemaVersion")] - public int? SchemaVersion { get; set; } - - [DataMember(Name = "mediaType")] - public string MediaType { get; set; } - } + private class SchemaCheck + { + /// + /// This field specifies the image manifest schema version as an integer. + /// + [DataMember(Name = "schemaVersion")] + public int? SchemaVersion { get; set; } + + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs index 51f7731..23ea093 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs @@ -13,26 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Endpoints.Implementations; -using Docker.Registry.DotNet.Registry; - -namespace Docker.Registry.DotNet.Endpoints.Implementations +internal class SystemOperations(NetworkClient client) : ISystemOperations { - internal class SystemOperations : ISystemOperations + public Task PingAsync(CancellationToken cancellationToken = default) { - private readonly NetworkClient _client; - - public SystemOperations(NetworkClient client) - { - this._client = client; - } - - public Task PingAsync(CancellationToken cancellationToken = default) - { - return this._client.MakeRequestAsync(cancellationToken, HttpMethod.Get, "v2/"); + return client.MakeRequestAsync(cancellationToken, HttpMethod.Get, "v2/"); } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs index 2dca040..4634608 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs @@ -13,33 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.Endpoints.Implementations; -using Docker.Registry.DotNet.Helpers; -using Docker.Registry.DotNet.Models; -using Docker.Registry.DotNet.Registry; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Endpoints.Implementations +internal class TagOperations(NetworkClient client) : ITagOperations { - internal class TagOperations : ITagOperations - { - private readonly NetworkClient _client; + private readonly NetworkClient _client = client ?? throw new ArgumentNullException(nameof(client)); - public TagOperations([NotNull] NetworkClient client) - { - this._client = client ?? throw new ArgumentNullException(nameof(client)); - } - - public async Task ListImageTagsAsync( - string name, - ListImageTagsParameters parameters = null, - CancellationToken cancellationToken = default) - { + public async Task ListImageTagsAsync( + string name, + ListImageTagsParameters? parameters = null, + CancellationToken cancellationToken = default) + { if (string.IsNullOrEmpty(name)) throw new ArgumentException( $"'{nameof(name)}' cannot be null or empty", @@ -60,5 +44,4 @@ public async Task ListImageTagsAsync( return this._client.JsonSerializer.DeserializeObject( response.Body); } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/GlobalUsings.cs b/src/Docker.Registry.DotNet/GlobalUsings.cs new file mode 100644 index 0000000..9f8e4fe --- /dev/null +++ b/src/Docker.Registry.DotNet/GlobalUsings.cs @@ -0,0 +1,38 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Net; +global using System.Net.Http; +global using System.Net.Http.Headers; +global using System.Runtime.Serialization; +global using System.Text; +global using System.Threading; +global using System.Threading.Tasks; + +global using Docker.Registry.DotNet.Authentication; +global using Docker.Registry.DotNet.Endpoints; +global using Docker.Registry.DotNet.Helpers; +global using Docker.Registry.DotNet.Models; +global using Docker.Registry.DotNet.OAuth; +global using Docker.Registry.DotNet.QueryParameters; +global using Docker.Registry.DotNet.Registry; + +global using JetBrains.Annotations; + +global using Newtonsoft.Json; \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/EnumerableExtensions.cs b/src/Docker.Registry.DotNet/Helpers/EnumerableExtensions.cs index b22c2f4..5b750ab 100644 --- a/src/Docker.Registry.DotNet/Helpers/EnumerableExtensions.cs +++ b/src/Docker.Registry.DotNet/Helpers/EnumerableExtensions.cs @@ -13,16 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Collections.Generic; -using System.Linq; +namespace Docker.Registry.DotNet.Helpers; -namespace Docker.Registry.DotNet.Helpers +public static class EnumerableExtensions { - public static class EnumerableExtensions + public static IEnumerable IfNullEmpty(this IEnumerable? enumerable) { - public static IEnumerable IfNullEmpty(this IEnumerable enumerable) - { - return enumerable ?? Enumerable.Empty(); + return enumerable ?? []; } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs b/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs index 38f326d..f31bfa8 100644 --- a/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs +++ b/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs @@ -13,116 +13,105 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; +namespace Docker.Registry.DotNet.Helpers; -using Docker.Registry.DotNet.Registry; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Helpers +internal static class HttpUtility { - internal static class HttpUtility + internal static Uri BuildUri(this Uri baseUri, string path, IQueryString? queryString) { - internal static Uri BuildUri(this Uri baseUri, string path, IQueryString queryString) - { - if (baseUri == null) throw new ArgumentNullException(nameof(baseUri)); - - var pathIsUri = Uri.TryCreate(path, UriKind.Absolute, out Uri uri); + if (baseUri == null) throw new ArgumentNullException(nameof(baseUri)); - if (!pathIsUri) - uri = baseUri; + var pathIsUri = Uri.TryCreate(path, UriKind.Absolute, out Uri uri); - var builder = new UriBuilder(uri); + if (!pathIsUri) + uri = baseUri; - if (!pathIsUri && !string.IsNullOrEmpty(path)) builder.Path += path; + var builder = new UriBuilder(uri); - if (queryString != null) - { - if (string.IsNullOrWhiteSpace(builder.Query)) - builder.Query = queryString.GetQueryString(); - else - builder.Query += "&" + queryString.GetQueryString(); - } + if (!pathIsUri && !string.IsNullOrEmpty(path)) builder.Path += path; - return builder.Uri; + if (queryString != null) + { + if (string.IsNullOrWhiteSpace(builder.Query)) + builder.Query = queryString.GetQueryString(); + else + builder.Query += "&" + queryString.GetQueryString(); } - /// - /// Attempts to retrieve the value of a response header. - /// - /// - /// - /// The first value if one is found, null otherwise. - public static string GetHeader([NotNull] this RegistryApiResponse response, string name) - { - if (response == null) throw new ArgumentNullException(nameof(response)); + return builder.Uri; + } - return response.Headers - .FirstOrDefault(h => h.Key.Equals(name, StringComparison.OrdinalIgnoreCase)).Value - ?.FirstOrDefault(); - } + /// + /// Attempts to retrieve the value of a response header. + /// + /// + /// + /// The first value if one is found, null otherwise. + public static string GetHeader(this RegistryApiResponse response, string name) + { + if (response == null) throw new ArgumentNullException(nameof(response)); - /// - /// Attempts to retrieve the value of a response header. - /// - /// - /// - /// The first value if one is found, null otherwise. - public static string[] GetHeaders( - this IEnumerable> headers, - string name) - { - return headers - .IfNullEmpty() - .Where(h => h.Key == name) - .Select(h => h.Value?.FirstOrDefault()) - .ToArray(); - } + return response.Headers + .FirstOrDefault(h => h.Key.Equals(name, StringComparison.OrdinalIgnoreCase)).Value + ?.FirstOrDefault(); + } - public static void AddRange( - [NotNull] this HttpRequestHeaders header, - IEnumerable> headers) - { - if (header == null) throw new ArgumentNullException(nameof(header)); + /// + /// Attempts to retrieve the value of a response header. + /// + /// + /// + /// The first value if one is found, null otherwise. + public static string[] GetHeaders( + this IEnumerable> headers, + string name) + { + return headers + .IfNullEmpty() + .Where(h => h.Key == name) + .Select(h => h.Value?.FirstOrDefault()) + .ToArray(); + } - foreach (var item in headers.IfNullEmpty()) header.Add(item.Key, item.Value); - } + public static void AddRange( + this HttpRequestHeaders header, + IEnumerable>? headers) + { + if (header == null) throw new ArgumentNullException(nameof(header)); - public static AuthenticationHeaderValue GetHeaderBySchema( - [NotNull] this HttpResponseMessage response, - string schema) - { - if (response == null) throw new ArgumentNullException(nameof(response)); + foreach (var item in headers.IfNullEmpty()) header.Add(item.Key, item.Value); + } - return response.Headers.WwwAuthenticate.FirstOrDefault(s => s.Scheme == schema); - } + public static AuthenticationHeaderValue GetHeaderBySchema( + this HttpResponseMessage response, + string schema) + { + if (response == null) throw new ArgumentNullException(nameof(response)); - public static int? GetContentLength([NotNull] this HttpResponseHeaders responseHeaders) - { - if (responseHeaders == null) throw new ArgumentNullException(nameof(responseHeaders)); + return response.Headers.WwwAuthenticate.FirstOrDefault(s => s.Scheme == schema); + } - if (!responseHeaders.TryGetValues("Content-Length", out var values)) return null; + public static int? GetContentLength(this HttpResponseHeaders responseHeaders) + { + if (responseHeaders == null) throw new ArgumentNullException(nameof(responseHeaders)); - var raw = values.FirstOrDefault(); + if (!responseHeaders.TryGetValues("Content-Length", out var values)) return null; - if (int.TryParse(raw, out var parsed)) return parsed; + var raw = values.FirstOrDefault(); - return null; - } + if (int.TryParse(raw, out var parsed)) return parsed; - public static string GetString( - [NotNull] this HttpResponseHeaders responseHeaders, - string name) - { - if (responseHeaders == null) throw new ArgumentNullException(nameof(responseHeaders)); + return null; + } - if (!responseHeaders.TryGetValues(name, out var values)) return null; + public static string GetString( + this HttpResponseHeaders responseHeaders, + string name) + { + if (responseHeaders == null) throw new ArgumentNullException(nameof(responseHeaders)); - return values.FirstOrDefault(); - } + if (!responseHeaders.TryGetValues(name, out var values)) return null; + + return values.FirstOrDefault(); } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/IDictionaryExtensions.cs b/src/Docker.Registry.DotNet/Helpers/IDictionaryExtensions.cs index f201715..9a63432 100644 --- a/src/Docker.Registry.DotNet/Helpers/IDictionaryExtensions.cs +++ b/src/Docker.Registry.DotNet/Helpers/IDictionaryExtensions.cs @@ -13,16 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; -using System.Linq; +namespace Docker.Registry.DotNet.Helpers; -namespace Docker.Registry.DotNet.Helpers +internal static class IDictionaryExtensions { - internal static class IDictionaryExtensions + public static string GetQueryString(this IDictionary values) { - public static string GetQueryString(this IDictionary values) - { return string.Join( "&", values.Select( @@ -32,14 +28,13 @@ public static string GetQueryString(this IDictionary values) v => $"{Uri.EscapeUriString(pair.Key)}={Uri.EscapeDataString(v)}")))); } - public static TValue GetValueOrDefault( - this IDictionary dict, - TKey key) - { + public static TValue GetValueOrDefault( + this IDictionary dict, + TKey key) + { if (dict.TryGetValue(key, out var value)) return value; return default; } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/IQueryString.cs b/src/Docker.Registry.DotNet/Helpers/IQueryString.cs index a2dd6c8..8b16d97 100644 --- a/src/Docker.Registry.DotNet/Helpers/IQueryString.cs +++ b/src/Docker.Registry.DotNet/Helpers/IQueryString.cs @@ -13,10 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Helpers +namespace Docker.Registry.DotNet.Helpers; + +internal interface IQueryString { - internal interface IQueryString - { - string GetQueryString(); - } + string GetQueryString(); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs b/src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs index e8ff326..efd416e 100644 --- a/src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs +++ b/src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs @@ -13,37 +13,35 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Newtonsoft.Json; using Newtonsoft.Json.Converters; -namespace Docker.Registry.DotNet.Helpers +namespace Docker.Registry.DotNet.Helpers; + +/// +/// Facade for . +/// +internal class JsonSerializer { - /// - /// Facade for . - /// - internal class JsonSerializer + private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { - private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + NullValueHandling = NullValueHandling.Ignore, + Converters = { - NullValueHandling = NullValueHandling.Ignore, - Converters = - { - //new JsonIso8601AndUnixEpochDateConverter(), - //new JsonVersionConverter(), - new StringEnumConverter() - //new TimeSpanSecondsConverter(), - //new TimeSpanNanosecondsConverter() - } - }; + //new JsonIso8601AndUnixEpochDateConverter(), + //new JsonVersionConverter(), + new StringEnumConverter() + //new TimeSpanSecondsConverter(), + //new TimeSpanNanosecondsConverter() + } + }; - public T DeserializeObject(string json) - { + public T DeserializeObject(string json) + { return JsonConvert.DeserializeObject(json, Settings); } - public string SerializeObject(T value) - { + public string SerializeObject(T value) + { return JsonConvert.SerializeObject(value, Settings); } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/QueryString.cs b/src/Docker.Registry.DotNet/Helpers/QueryString.cs index f69a3a2..72bd7a0 100644 --- a/src/Docker.Registry.DotNet/Helpers/QueryString.cs +++ b/src/Docker.Registry.DotNet/Helpers/QueryString.cs @@ -13,18 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; -using System.Linq; +namespace Docker.Registry.DotNet.Helpers; -namespace Docker.Registry.DotNet.Helpers +internal class QueryString : IQueryString { - internal class QueryString : IQueryString - { - private readonly Dictionary _values = new Dictionary(); + private readonly Dictionary _values = new Dictionary(); - public string GetQueryString() - { + public string GetQueryString() + { return string.Join( "&", this._values.Select( @@ -34,14 +30,13 @@ public string GetQueryString() v => $"{Uri.EscapeUriString(pair.Key)}={Uri.EscapeDataString(v)}")))); } - public void Add(string key, string value) - { - this._values.Add(key, new[] { value }); + public void Add(string key, string value) + { + this._values.Add(key, [value]); } - public void Add(string key, string[] values) - { + public void Add(string key, string[] values) + { this._values.Add(key, values); } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs b/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs index 5d87a0d..bc2261a 100644 --- a/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs +++ b/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs @@ -13,68 +13,59 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Reflection; -using Docker.Registry.DotNet.QueryParameters; +namespace Docker.Registry.DotNet.Helpers; -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Helpers +internal static class QueryStringExtensions { - internal static class QueryStringExtensions + /// + /// Adds query parameters using reflection. Object must have [QueryParameter] attributes + /// on it's properties for it to map properly. + /// + /// + /// + /// + internal static void AddFromObjectWithQueryParameters( + this QueryString queryString, + T instance) + where T : class { - /// - /// Adds query parameters using reflection. Object must have [QueryParameter] attributes - /// on it's properties for it to map properly. - /// - /// - /// - /// - internal static void AddFromObjectWithQueryParameters( - this QueryString queryString, - [NotNull] T instance) - where T : class - { - if (instance == null) throw new ArgumentNullException(nameof(instance)); + if (instance == null) throw new ArgumentNullException(nameof(instance)); - var propertyInfos = instance.GetType().GetProperties(); + var propertyInfos = instance.GetType().GetProperties(); - foreach (var p in propertyInfos) + foreach (var p in propertyInfos) + { + var attribute = p.GetCustomAttribute(); + if (attribute != null) { - var attribute = p.GetCustomAttribute(); - if (attribute != null) - { - // TODO: Use a nuget like FastMember to improve performance here or switch to static delegate generation - var value = p.GetValue(instance, null); - if (value != null) - { - queryString.Add(attribute.Key, value.ToString()); - } - } + // TODO: Use a nuget like FastMember to improve performance here or switch to static delegate generation + var value = p.GetValue(instance, null); + if (value != null) queryString.Add(attribute.Key, value.ToString()); } } + } - /// - /// Adds the value to the query string if it's not null. - /// - /// - /// - /// - internal static void AddIfNotNull(this QueryString queryString, string key, T? value) - where T : struct - { - if (value != null) queryString.Add(key, $"{value.Value}"); - } + /// + /// Adds the value to the query string if it's not null. + /// + /// + /// + /// + internal static void AddIfNotNull(this QueryString queryString, string key, T? value) + where T : struct + { + if (value != null) queryString.Add(key, $"{value.Value}"); + } - /// - /// - /// - /// - /// - internal static void AddIfNotEmpty(this QueryString queryString, string key, string value) - { - if (!string.IsNullOrWhiteSpace(value)) queryString.Add(key, value); - } + /// + /// + /// + /// + /// + internal static void AddIfNotEmpty(this QueryString queryString, string key, string value) + { + if (!string.IsNullOrWhiteSpace(value)) queryString.Add(key, value); } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/StringExtensions.cs b/src/Docker.Registry.DotNet/Helpers/StringExtensions.cs index da55cf6..c554c17 100644 --- a/src/Docker.Registry.DotNet/Helpers/StringExtensions.cs +++ b/src/Docker.Registry.DotNet/Helpers/StringExtensions.cs @@ -13,18 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Collections.Generic; -using System.Linq; +namespace Docker.Registry.DotNet.Helpers; -namespace Docker.Registry.DotNet.Helpers +public static class StringExtensions { - public static class StringExtensions + public static string ToDelimitedString( + this IEnumerable strings, + string delimiter = "") { - public static string ToDelimitedString( - this IEnumerable strings, - string delimiter = "") - { - return string.Join(delimiter, strings.IfNullEmpty().ToArray()); - } + return string.Join(delimiter, strings.IfNullEmpty().ToArray()); } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/BlobHeader.cs b/src/Docker.Registry.DotNet/Models/BlobHeader.cs index c81f1a0..e4bb6f8 100644 --- a/src/Docker.Registry.DotNet/Models/BlobHeader.cs +++ b/src/Docker.Registry.DotNet/Models/BlobHeader.cs @@ -13,18 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -using JetBrains.Annotations; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[PublicAPI] +public class BlobHeader { - [PublicAPI] - public class BlobHeader + internal BlobHeader(string dockerContentDigest) { - internal BlobHeader(string dockerContentDigest) - { - this.DockerContentDigest = dockerContentDigest; - } - - public string DockerContentDigest { get; } + this.DockerContentDigest = dockerContentDigest; } + + public string DockerContentDigest { get; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/BlobUploadStatus.cs b/src/Docker.Registry.DotNet/Models/BlobUploadStatus.cs index e15c261..7e55dcc 100644 --- a/src/Docker.Registry.DotNet/Models/BlobUploadStatus.cs +++ b/src/Docker.Registry.DotNet/Models/BlobUploadStatus.cs @@ -13,22 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -using JetBrains.Annotations; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[PublicAPI] +public class BlobUploadStatus { - [PublicAPI] - public class BlobUploadStatus - { - /// - /// Range header indicating the progress of the upload. When starting an upload, it will return an empty range, since - /// no content has been received. - /// - public string Range { get; set; } + /// + /// Range header indicating the progress of the upload. When starting an upload, it will return an empty range, since + /// no content has been received. + /// + public string? Range { get; set; } - /// - /// Identifies the docker upload uuid for the current request. - /// - public string DockerUploadUuid { get; set; } - } + /// + /// Identifies the docker upload uuid for the current request. + /// + public string? DockerUploadUuid { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/Catalog.cs b/src/Docker.Registry.DotNet/Models/Catalog.cs index 1b2a28f..1837679 100644 --- a/src/Docker.Registry.DotNet/Models/Catalog.cs +++ b/src/Docker.Registry.DotNet/Models/Catalog.cs @@ -13,14 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class Catalog { - [DataContract] - public class Catalog - { - [DataMember(Name = "repositories", EmitDefaultValue = false)] - public string[] Repositories { get; set; } - } + [DataMember(Name = "repositories", EmitDefaultValue = false)] + public string[]? Repositories { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/CatalogParameters.cs b/src/Docker.Registry.DotNet/Models/CatalogParameters.cs index ee0a72a..e483b5a 100644 --- a/src/Docker.Registry.DotNet/Models/CatalogParameters.cs +++ b/src/Docker.Registry.DotNet/Models/CatalogParameters.cs @@ -13,25 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Docker.Registry.DotNet.QueryParameters; +namespace Docker.Registry.DotNet.Models; -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Models +[PublicAPI] +public class CatalogParameters { - [PublicAPI] - public class CatalogParameters - { - /// - /// Limit the number of entries in each response. It not present, all entries will be returned - /// - [QueryParameter("n")] - public int? Number { get; set; } + /// + /// Limit the number of entries in each response. It not present, all entries will be returned + /// + [QueryParameter("n")] + public int? Number { get; set; } - /// - /// Result set will include values lexically after last. - /// - [QueryParameter("last")] - public int? Last { get; set; } - } + /// + /// Result set will include values lexically after last. + /// + [QueryParameter("last")] + public int? Last { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/CompletedUploadResponse.cs b/src/Docker.Registry.DotNet/Models/CompletedUploadResponse.cs index 9d04efe..af3ffee 100644 --- a/src/Docker.Registry.DotNet/Models/CompletedUploadResponse.cs +++ b/src/Docker.Registry.DotNet/Models/CompletedUploadResponse.cs @@ -13,21 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models +namespace Docker.Registry.DotNet.Models; + +/// +/// A completed upload response. +/// +public class CompletedUploadResponse { /// - /// A completed upload response. + /// The Location will contain the registry URL to access the accepted layer file /// - public class CompletedUploadResponse - { - /// - /// The Location will contain the registry URL to access the accepted layer file - /// - public string Location { get; set; } + public string? Location { get; set; } - /// - /// The DockerContentDigest returns the canonical digest of the uploaded blob which may differ from the provided digest. Most clients may ignore the value but if it is used, the client should verify the value against the uploaded blob data. - /// - public string DockerContentDigest { get; set; } - } + /// + /// The DockerContentDigest returns the canonical digest of the uploaded blob which may differ from the provided digest. Most clients may ignore the value but if it is used, the client should verify the value against the uploaded blob data. + /// + public string? DockerContentDigest { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/Config.cs b/src/Docker.Registry.DotNet/Models/Config.cs index 3b27080..4488fae 100644 --- a/src/Docker.Registry.DotNet/Models/Config.cs +++ b/src/Docker.Registry.DotNet/Models/Config.cs @@ -13,41 +13,38 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class Config { - [DataContract] - public class Config - { - /// - /// The MIME type of the referenced object. This should generally be application/vnd.docker.image.rootfs.diff.tar.gzip. - /// Layers of type application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be pulled from a remote location but - /// they should never be pushed. - /// - [DataMember(Name = "mediaType")] - public string MediaType { get; set; } + /// + /// The MIME type of the referenced object. This should generally be application/vnd.docker.image.rootfs.diff.tar.gzip. + /// Layers of type application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be pulled from a remote location but + /// they should never be pushed. + /// + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } - /// - /// The size in bytes of the object. This field exists so that a client will have an expected size for the content - /// before validating. If the length of the retrieved content does not match the specified length, the content should - /// not be trusted. - /// - [DataMember(Name = "size")] - public long Size { get; set; } + /// + /// The size in bytes of the object. This field exists so that a client will have an expected size for the content + /// before validating. If the length of the retrieved content does not match the specified length, the content should + /// not be trusted. + /// + [DataMember(Name = "size")] + public long Size { get; set; } - /// - /// The digest of the content, as defined by the Registry V2 HTTP API Specificiation. - /// https://docs.docker.com/registry/spec/api/#digest-parameter - /// - [DataMember(Name = "digest")] - public string Digest { get; set; } + /// + /// The digest of the content, as defined by the Registry V2 HTTP API Specificiation. + /// https://docs.docker.com/registry/spec/api/#digest-parameter + /// + [DataMember(Name = "digest")] + public string? Digest { get; set; } - /// - /// Provides a list of URLs from which the content may be fetched. Content should be verified against the digest and - /// size. This field is optional and uncommon. - /// - [DataMember(Name = "urls")] - public string[] Urls { get; set; } - } + /// + /// Provides a list of URLs from which the content may be fetched. Content should be verified against the digest and + /// size. This field is optional and uncommon. + /// + [DataMember(Name = "urls")] + public string[]? Urls { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/GetBlobResponse.cs b/src/Docker.Registry.DotNet/Models/GetBlobResponse.cs index c723229..a14a3f5 100644 --- a/src/Docker.Registry.DotNet/Models/GetBlobResponse.cs +++ b/src/Docker.Registry.DotNet/Models/GetBlobResponse.cs @@ -13,24 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.IO; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +public class GetBlobResponse : BlobHeader, IDisposable { - public class GetBlobResponse : BlobHeader, IDisposable + internal GetBlobResponse(string dockerContentDigest, Stream stream) + : base(dockerContentDigest) { - internal GetBlobResponse(string dockerContentDigest, Stream stream) - : base(dockerContentDigest) - { this.Stream = stream; } - public Stream Stream { get; } + public Stream Stream { get; } - public void Dispose() - { + public void Dispose() + { this.Stream?.Dispose(); } - } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/GetImageManifestResult.cs b/src/Docker.Registry.DotNet/Models/GetImageManifestResult.cs index c507c06..0d0b9f5 100644 --- a/src/Docker.Registry.DotNet/Models/GetImageManifestResult.cs +++ b/src/Docker.Registry.DotNet/Models/GetImageManifestResult.cs @@ -13,31 +13,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models +namespace Docker.Registry.DotNet.Models; + +public class GetImageManifestResult { - public class GetImageManifestResult + internal GetImageManifestResult(string mediaType, ImageManifest manifest, string content) { - internal GetImageManifestResult(string mediaType, ImageManifest manifest, string content) - { this.Manifest = manifest; this.Content = content; this.MediaType = mediaType; } - public string DockerContentDigest { get; internal set; } + public string DockerContentDigest { get; internal set; } - public string Etag { get; internal set; } + public string Etag { get; internal set; } - public string MediaType { get; } + public string MediaType { get; } - /// - /// The image manifest - /// - public ImageManifest Manifest { get; } + /// + /// The image manifest + /// + public ImageManifest Manifest { get; } - /// - /// Gets the original, raw body returned from the server. - /// - public string Content { get; } - } + /// + /// Gets the original, raw body returned from the server. + /// + public string Content { get; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ImageManifest.cs b/src/Docker.Registry.DotNet/Models/ImageManifest.cs index f05ed3d..67677d8 100644 --- a/src/Docker.Registry.DotNet/Models/ImageManifest.cs +++ b/src/Docker.Registry.DotNet/Models/ImageManifest.cs @@ -13,17 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public abstract class ImageManifest { - [DataContract] - public abstract class ImageManifest - { - /// - /// This field specifies the image manifest schema version as an integer. - /// - [DataMember(Name = "schemaVersion")] - public int SchemaVersion { get; set; } - } + /// + /// This field specifies the image manifest schema version as an integer. + /// + [DataMember(Name = "schemaVersion")] + public int SchemaVersion { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ImageManifest2_1.cs b/src/Docker.Registry.DotNet/Models/ImageManifest2_1.cs index 691c55d..7d03e15 100644 --- a/src/Docker.Registry.DotNet/Models/ImageManifest2_1.cs +++ b/src/Docker.Registry.DotNet/Models/ImageManifest2_1.cs @@ -13,51 +13,48 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +/// +/// Image Manifest Version 2, Schema 1 +/// +[DataContract] +public class ImageManifest2_1 : ImageManifest { /// - /// Image Manifest Version 2, Schema 1 + /// name is the name of the image’s repository /// - [DataContract] - public class ImageManifest2_1 : ImageManifest - { - /// - /// name is the name of the image’s repository - /// - [DataMember(Name = "name")] - public string Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - /// - /// tag is the tag of the image - /// - [DataMember(Name = "tag")] - public string Tag { get; set; } + /// + /// tag is the tag of the image + /// + [DataMember(Name = "tag")] + public string? Tag { get; set; } - /// - /// architecture is the host architecture on which this image is intended to run. This is for information purposes and not currently used by the engine - /// - [DataMember(Name = "architecture")] - public string Architecture { get; set; } + /// + /// architecture is the host architecture on which this image is intended to run. This is for information purposes and not currently used by the engine + /// + [DataMember(Name = "architecture")] + public string? Architecture { get; set; } - /// - /// fsLayers is a list of filesystem layer blob sums contained in this image. - /// - [DataMember(Name = "fsLayers")] - public ManifestFsLayer[] FsLayers { get; set; } + /// + /// fsLayers is a list of filesystem layer blob sums contained in this image. + /// + [DataMember(Name = "fsLayers")] + public ManifestFsLayer[]? FsLayers { get; set; } - /// - /// history is a list of unstructured historical data for v1 compatibility. It contains ID of the image layer and ID of the layer’s parent layers. - /// - [DataMember(Name = "history")] - public ManifestHistory[] History { get; set; } + /// + /// history is a list of unstructured historical data for v1 compatibility. It contains ID of the image layer and ID of the layer’s parent layers. + /// + [DataMember(Name = "history")] + public ManifestHistory[]? History { get; set; } - /// - /// Signed manifests provides an envelope for a signed image manifest. A signed manifest consists of an image manifest along with an additional field containing the signature of the manifest. - /// The docker client can verify signed manifests and displays a message to the user. - /// - [DataMember(Name = "signatures")] - public ManifestSignature[] Signatures { get; set; } - } + /// + /// Signed manifests provides an envelope for a signed image manifest. A signed manifest consists of an image manifest along with an additional field containing the signature of the manifest. + /// The docker client can verify signed manifests and displays a message to the user. + /// + [DataMember(Name = "signatures")] + public ManifestSignature[]? Signatures { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ImageManifest2_2.cs b/src/Docker.Registry.DotNet/Models/ImageManifest2_2.cs index 51919e0..8b5eef1 100644 --- a/src/Docker.Registry.DotNet/Models/ImageManifest2_2.cs +++ b/src/Docker.Registry.DotNet/Models/ImageManifest2_2.cs @@ -13,32 +13,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class ImageManifest2_2 : ImageManifest { - [DataContract] - public class ImageManifest2_2 : ImageManifest - { - /// - /// The MIME type of the referenced object - /// - [DataMember(Name = "mediaType")] - public string MediaType { get; set; } + /// + /// The MIME type of the referenced object + /// + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } - /// - /// The config field references a configuration object for a container, by digest. This configuration - /// item is a JSON blob that the runtime uses to set up the container. This new schema uses a tweaked - /// version of this configuration to allow image content-addressability on the daemon side. - /// + /// + /// The config field references a configuration object for a container, by digest. This configuration + /// item is a JSON blob that the runtime uses to set up the container. This new schema uses a tweaked + /// version of this configuration to allow image content-addressability on the daemon side. + /// - [DataMember(Name = "config")] - public Config Config { get; set; } + [DataMember(Name = "config")] + public Config? Config { get; set; } - /// - /// The layer list is ordered starting from the base image (opposite order of schema1). - /// - [DataMember(Name = "layers")] - public ManifestLayer[] Layers { get; set; } - } + /// + /// The layer list is ordered starting from the base image (opposite order of schema1). + /// + [DataMember(Name = "layers")] + public ManifestLayer[]? Layers { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/InitiateMonolithicUploadResponse.cs b/src/Docker.Registry.DotNet/Models/InitiateMonolithicUploadResponse.cs index 1da6eee..e1320c6 100644 --- a/src/Docker.Registry.DotNet/Models/InitiateMonolithicUploadResponse.cs +++ b/src/Docker.Registry.DotNet/Models/InitiateMonolithicUploadResponse.cs @@ -13,14 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models +namespace Docker.Registry.DotNet.Models; + +public class InitiateMonolithicUploadResponse { - public class InitiateMonolithicUploadResponse - { - public string Location { get; set; } + public string? Location { get; set; } - public int ContentLength { get; set; } + public int ContentLength { get; set; } - public string DockerUploadUuid { get; set; } - } + public string? DockerUploadUuid { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ListImageTagsParameters.cs b/src/Docker.Registry.DotNet/Models/ListImageTagsParameters.cs index fe28a9c..d07a425 100644 --- a/src/Docker.Registry.DotNet/Models/ListImageTagsParameters.cs +++ b/src/Docker.Registry.DotNet/Models/ListImageTagsParameters.cs @@ -13,16 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Docker.Registry.DotNet.QueryParameters; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +public class ListImageTagsParameters { - public class ListImageTagsParameters - { - /// - /// Limit the number of entries in each response. It not present, all entries will be returned - /// - [QueryParameter("n")] - public int? Number { get; set; } - } + /// + /// Limit the number of entries in each response. It not present, all entries will be returned + /// + [QueryParameter("n")] + public int? Number { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ListImageTagsResponse.cs b/src/Docker.Registry.DotNet/Models/ListImageTagsResponse.cs index 8a4b680..4565a24 100644 --- a/src/Docker.Registry.DotNet/Models/ListImageTagsResponse.cs +++ b/src/Docker.Registry.DotNet/Models/ListImageTagsResponse.cs @@ -13,17 +13,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class ListImageTagsResponse { - [DataContract] - public class ListImageTagsResponse - { - [DataMember(Name = "name")] - public string Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "tags")] - public string[] Tags { get; set; } - } + [DataMember(Name = "tags")] + public string[]? Tags { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/Manifest.cs b/src/Docker.Registry.DotNet/Models/Manifest.cs index b26989e..2c3ef93 100644 --- a/src/Docker.Registry.DotNet/Models/Manifest.cs +++ b/src/Docker.Registry.DotNet/Models/Manifest.cs @@ -13,46 +13,41 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Models +[PublicAPI] +public class Manifest { - [PublicAPI] - public class Manifest - { - /// - /// The MIME type of the referenced object. This will generally be application/vnd.docker.image.manifest.v2+json, but - /// it could also be application/vnd.docker.image.manifest.v1+json if the manifest list references a legacy schema-1 - /// manifest. - /// - [DataMember(Name = "mediaType")] - public string MediaType { get; set; } + /// + /// The MIME type of the referenced object. This will generally be application/vnd.docker.image.manifest.v2+json, but + /// it could also be application/vnd.docker.image.manifest.v1+json if the manifest list references a legacy schema-1 + /// manifest. + /// + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } - /// - /// The size in bytes of the object. This field exists so that a client will have an expected size for the content - /// before validating. If the length of the retrieved content does not match the specified length, the content should - /// not be trusted. - /// - [DataMember(Name = "size")] - public int Size { get; set; } + /// + /// The size in bytes of the object. This field exists so that a client will have an expected size for the content + /// before validating. If the length of the retrieved content does not match the specified length, the content should + /// not be trusted. + /// + [DataMember(Name = "size")] + public int Size { get; set; } - /// - /// The digest of the content, as defined by the Registry V2 HTTP API Specificiation. - /// - /// https://docs.docker.com/registry/spec/api/#digest-parameter - [DataMember(Name = "digest")] - public string Digest { get; set; } + /// + /// The digest of the content, as defined by the Registry V2 HTTP API Specificiation. + /// + /// https://docs.docker.com/registry/spec/api/#digest-parameter + [DataMember(Name = "digest")] + public string? Digest { get; set; } - /// - /// The platform object describes the platform which the image in the manifest runs on. A full list of valid operating - /// system and architecture values are listed in the Go language documentation for $GOOS and $GOARCH - /// - /// - /// https://golang.org/doc/install/source#environment - /// - [DataMember(Name = "platform")] - public Platform Platform { get; set; } - } + /// + /// The platform object describes the platform which the image in the manifest runs on. A full list of valid operating + /// system and architecture values are listed in the Go language documentation for $GOOS and $GOARCH + /// + /// + /// https://golang.org/doc/install/source#environment + /// + [DataMember(Name = "platform")] + public Platform? Platform { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestFsLayer.cs b/src/Docker.Registry.DotNet/Models/ManifestFsLayer.cs index 507bcad..cea1b5a 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestFsLayer.cs +++ b/src/Docker.Registry.DotNet/Models/ManifestFsLayer.cs @@ -13,14 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class ManifestFsLayer { - [DataContract] - public class ManifestFsLayer - { - [DataMember(Name = "blobSum")] - public string BlobSum { get; set; } - } + [DataMember(Name = "blobSum")] + public string? BlobSum { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestHistory.cs b/src/Docker.Registry.DotNet/Models/ManifestHistory.cs index 0956167..3e1e8cf 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestHistory.cs +++ b/src/Docker.Registry.DotNet/Models/ManifestHistory.cs @@ -13,14 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class ManifestHistory { - [DataContract] - public class ManifestHistory - { - [DataMember(Name = "v1Compatibility")] - public string V1Compatibility { get; set; } - } + [DataMember(Name = "v1Compatibility")] + public string? V1Compatibility { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestLayer.cs b/src/Docker.Registry.DotNet/Models/ManifestLayer.cs index 6d2dd57..a2ac2ed 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestLayer.cs +++ b/src/Docker.Registry.DotNet/Models/ManifestLayer.cs @@ -13,42 +13,39 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class ManifestLayer { - [DataContract] - public class ManifestLayer - { - /// - /// The MIME type of the referenced object. This should generally be application/vnd.docker.image.rootfs.diff.tar.gzip. - /// Layers of type application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be pulled from a remote location but - /// they should never be pushed. - /// - [DataMember(Name = "mediaType")] - public string MediaType { get; set; } + /// + /// The MIME type of the referenced object. This should generally be application/vnd.docker.image.rootfs.diff.tar.gzip. + /// Layers of type application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be pulled from a remote location but + /// they should never be pushed. + /// + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } - /// - /// The size in bytes of the object. This field exists so that a client will have an expected size for the content - /// before validating. If the length of the retrieved content does not match the specified length, the content should - /// not be trusted. - /// - [DataMember(Name = "size")] - public long Size { get; set; } + /// + /// The size in bytes of the object. This field exists so that a client will have an expected size for the content + /// before validating. If the length of the retrieved content does not match the specified length, the content should + /// not be trusted. + /// + [DataMember(Name = "size")] + public long Size { get; set; } - /// - /// The digest of the content, as defined by the Registry V2 HTTP API Specification. - /// - /// https://docs.docker.com/registry/spec/api/#digest-parameter - [DataMember(Name = "digest")] - public string Digest { get; set; } + /// + /// The digest of the content, as defined by the Registry V2 HTTP API Specification. + /// + /// https://docs.docker.com/registry/spec/api/#digest-parameter + [DataMember(Name = "digest")] + public string? Digest { get; set; } - /// - /// Provides a list of URLs from which the content may be fetched. Content should be verified against the digest and - /// size. - /// This field is optional and uncommon. - /// - [DataMember(Name = "urls")] - public string[] Urls { get; set; } - } + /// + /// Provides a list of URLs from which the content may be fetched. Content should be verified against the digest and + /// size. + /// This field is optional and uncommon. + /// + [DataMember(Name = "urls")] + public string[]? Urls { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestList.cs b/src/Docker.Registry.DotNet/Models/ManifestList.cs index 1aebb6b..ac5ad58 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestList.cs +++ b/src/Docker.Registry.DotNet/Models/ManifestList.cs @@ -13,28 +13,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +/// +/// The manifest list is the “fat manifest” which points to specific image manifests for one or more platforms. Its use +/// is optional, and relatively few images will use one of these manifests. A client will distinguish a manifest list +/// from an image manifest based on the Content-Type returned in the HTTP response. +/// +public class ManifestList : ImageManifest { /// - /// The manifest list is the “fat manifest” which points to specific image manifests for one or more platforms. Its use - /// is optional, and relatively few images will use one of these manifests. A client will distinguish a manifest list - /// from an image manifest based on the Content-Type returned in the HTTP response. + /// The MIME type of the manifest list. This should be set to + /// application/vnd.docker.distribution.manifest.list.v2+json. /// - public class ManifestList : ImageManifest - { - /// - /// The MIME type of the manifest list. This should be set to - /// application/vnd.docker.distribution.manifest.list.v2+json. - /// - [DataMember(Name = "mediaType")] - public string MediaType { get; set; } + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } - /// - /// The manifests field contains a list of manifests for specific platforms. - /// - [DataMember(Name = "manifests")] - public Manifest[] Manifests { get; set; } - } + /// + /// The manifests field contains a list of manifests for specific platforms. + /// + [DataMember(Name = "manifests")] + public Manifest[]? Manifests { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestMediaTypes.cs b/src/Docker.Registry.DotNet/Models/ManifestMediaTypes.cs index 28a93c4..e1c2143 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestMediaTypes.cs +++ b/src/Docker.Registry.DotNet/Models/ManifestMediaTypes.cs @@ -13,55 +13,53 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models -{ - // https://docs.docker.com/registry/spec/manifest-v2-1/#signed-manifests +namespace Docker.Registry.DotNet.Models; +// https://docs.docker.com/registry/spec/manifest-v2-1/#signed-manifests - public static class ManifestMediaTypes - { - /// - /// schema1 (existing manifest format). Note that “application/json” will also be accepted for schema 1. - /// - public const string ManifestSchema1 = - "application/vnd.docker.distribution.manifest.v1+json"; +public static class ManifestMediaTypes +{ + /// + /// schema1 (existing manifest format). Note that “application/json” will also be accepted for schema 1. + /// + public const string ManifestSchema1 = + "application/vnd.docker.distribution.manifest.v1+json"; - /// - /// schema1 (existing manifest format) signed. - /// - public const string ManifestSchema1Signed = - "application/vnd.docker.distribution.manifest.v1+prettyjws"; + /// + /// schema1 (existing manifest format) signed. + /// + public const string ManifestSchema1Signed = + "application/vnd.docker.distribution.manifest.v1+prettyjws"; - /// - /// New image manifest format (schemaVersion = 2) - /// - public const string ManifestSchema2 = - "application/vnd.docker.distribution.manifest.v2+json"; + /// + /// New image manifest format (schemaVersion = 2) + /// + public const string ManifestSchema2 = + "application/vnd.docker.distribution.manifest.v2+json"; - /// - /// Manifest list, aka “fat manifest” - /// - public const string ManifestList = - "application/vnd.docker.distribution.manifest.list.v2+json"; + /// + /// Manifest list, aka “fat manifest” + /// + public const string ManifestList = + "application/vnd.docker.distribution.manifest.list.v2+json"; - /// - /// Container config JSON - /// - public const string ContainerConfig = "application/vnd.docker.container.image.v1+json"; + /// + /// Container config JSON + /// + public const string ContainerConfig = "application/vnd.docker.container.image.v1+json"; - /// - /// “Layer”, as a gzipped tar - /// - public const string GzippedTar = "application/vnd.docker.image.rootfs.diff.tar.gzip"; + /// + /// “Layer”, as a gzipped tar + /// + public const string GzippedTar = "application/vnd.docker.image.rootfs.diff.tar.gzip"; - /// - /// “Layer”, as a gzipped tar that should never be pushed - /// - public const string GzippedLayer = - "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"; + /// + /// “Layer”, as a gzipped tar that should never be pushed + /// + public const string GzippedLayer = + "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"; - /// - /// Plugin config JSON - /// - public const string PluginConfigJson = "application/vnd.docker.plugin.v1+json"; - } + /// + /// Plugin config JSON + /// + public const string PluginConfigJson = "application/vnd.docker.plugin.v1+json"; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestSignature.cs b/src/Docker.Registry.DotNet/Models/ManifestSignature.cs index fd3544c..cec302f 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestSignature.cs +++ b/src/Docker.Registry.DotNet/Models/ManifestSignature.cs @@ -13,20 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class ManifestSignature { - [DataContract] - public class ManifestSignature - { - [DataMember(Name = "header")] - public ManifestSignatureHeader Header { get; set; } + [DataMember(Name = "header")] + public ManifestSignatureHeader? Header { get; set; } - [DataMember(Name = "signature")] - public string Signature { get; set; } + [DataMember(Name = "signature")] + public string? Signature { get; set; } - [DataMember(Name = "protected")] - public string Protected { get; set; } - } + [DataMember(Name = "protected")] + public string? Protected { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestSignatureHeader.cs b/src/Docker.Registry.DotNet/Models/ManifestSignatureHeader.cs index 2783ca1..c204b45 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestSignatureHeader.cs +++ b/src/Docker.Registry.DotNet/Models/ManifestSignatureHeader.cs @@ -13,14 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class ManifestSignatureHeader { - [DataContract] - public class ManifestSignatureHeader - { - [DataMember(Name = "alg")] - public string Alg { get; set; } - } + [DataMember(Name = "alg")] + public string? Alg { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/MountParameters.cs b/src/Docker.Registry.DotNet/Models/MountParameters.cs index 8b7cef3..64bc004 100644 --- a/src/Docker.Registry.DotNet/Models/MountParameters.cs +++ b/src/Docker.Registry.DotNet/Models/MountParameters.cs @@ -13,21 +13,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class MountParameters { - [DataContract] - public class MountParameters - { - /// - /// Digest of blob to mount from the source repository. - /// - public string Digest { get; set; } + /// + /// Digest of blob to mount from the source repository. + /// + public string? Digest { get; set; } - /// - /// Name of the source repository. - /// - public string From { get; set; } - } + /// + /// Name of the source repository. + /// + public string? From { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/MountResponse.cs b/src/Docker.Registry.DotNet/Models/MountResponse.cs index 0b9ca99..31bb065 100644 --- a/src/Docker.Registry.DotNet/Models/MountResponse.cs +++ b/src/Docker.Registry.DotNet/Models/MountResponse.cs @@ -13,24 +13,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models +namespace Docker.Registry.DotNet.Models; + +public class MountResponse { - public class MountResponse - { - /// - /// - /// - public string Location { get; set; } + /// + /// + /// + public string? Location { get; set; } - /// - /// Identifies the docker upload uuid for the current request. - /// - public string DockerUploadUuid { get; set; } + /// + /// Identifies the docker upload uuid for the current request. + /// + public string? DockerUploadUuid { get; set; } - /// - /// If the blob is successfully mounted Created is true,Otherwise, it is flse - /// If a mount fails due to invalid repository or digest arguments, the registry will fall back to the standard upload behavior And with the upload URL in the - /// - public bool Created { get; set; } - } + /// + /// If the blob is successfully mounted Created is true,Otherwise, it is flse + /// If a mount fails due to invalid repository or digest arguments, the registry will fall back to the standard upload behavior And with the upload URL in the + /// + public bool Created { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/Platform.cs b/src/Docker.Registry.DotNet/Models/Platform.cs index a029e6c..339cd74 100644 --- a/src/Docker.Registry.DotNet/Models/Platform.cs +++ b/src/Docker.Registry.DotNet/Models/Platform.cs @@ -13,50 +13,47 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.Models; -namespace Docker.Registry.DotNet.Models +[DataContract] +public class Platform { - [DataContract] - public class Platform - { - /// - /// The architecture field specifies the CPU architecture, for example amd64 or ppc64le - /// - [DataMember(Name = "architecture")] - public string Architecture { get; set; } + /// + /// The architecture field specifies the CPU architecture, for example amd64 or ppc64le + /// + [DataMember(Name = "architecture")] + public string? Architecture { get; set; } - /// - /// The os field specifies the operating system, for example linux or windows. - /// - [DataMember(Name = "os")] - public string Os { get; set; } + /// + /// The os field specifies the operating system, for example linux or windows. + /// + [DataMember(Name = "os")] + public string? Os { get; set; } - /// - /// The optional os.version field specifies the operating system version, for example 10.0.10586. - /// - [DataMember(Name = "os.version")] - public string OsVersion { get; set; } + /// + /// The optional os.version field specifies the operating system version, for example 10.0.10586. + /// + [DataMember(Name = "os.version")] + public string? OsVersion { get; set; } - /// - /// The optional os.features field specifies an array of strings, each listing a required OS feature (for example on - /// Windows win32k) - /// - [DataMember(Name = "os.features")] - public string OsFeatures { get; set; } + /// + /// The optional os.features field specifies an array of strings, each listing a required OS feature (for example on + /// Windows win32k) + /// + [DataMember(Name = "os.features")] + public string? OsFeatures { get; set; } - /// - /// The optional variant field specifies a variant of the CPU, for example armv6l to specify a particular CPU variant - /// of the ARM CPU. - /// - [DataMember(Name = "variant")] - public string Variant { get; set; } + /// + /// The optional variant field specifies a variant of the CPU, for example armv6l to specify a particular CPU variant + /// of the ARM CPU. + /// + [DataMember(Name = "variant")] + public string? Variant { get; set; } - /// - /// The optional features field specifies an array of strings, each listing a required CPU feature (for example sse4 or - /// aes). - /// - [DataMember(Name = "features")] - public string[] Features { get; set; } - } + /// + /// The optional features field specifies an array of strings, each listing a required CPU feature (for example sse4 or + /// aes). + /// + [DataMember(Name = "features")] + public string[]? Features { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/PushManifestResponse.cs b/src/Docker.Registry.DotNet/Models/PushManifestResponse.cs index 577ff1d..1bdf80c 100644 --- a/src/Docker.Registry.DotNet/Models/PushManifestResponse.cs +++ b/src/Docker.Registry.DotNet/Models/PushManifestResponse.cs @@ -13,23 +13,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models +namespace Docker.Registry.DotNet.Models; + +public class PushManifestResponse { - public class PushManifestResponse - { - /// - /// The canonical location url of the uploaded manifest. - /// - public string Location { get; set; } + /// + /// The canonical location url of the uploaded manifest. + /// + public string? Location { get; set; } - /// - /// The Content-Length header must be zero and the body must be empty. - /// - public string ContentLength { get; set; } + /// + /// The Content-Length header must be zero and the body must be empty. + /// + public string? ContentLength { get; set; } - /// - /// Digest of the targeted content for the request. - /// - public string DockerContentDigest { get; set; } - } + /// + /// Digest of the targeted content for the request. + /// + public string? DockerContentDigest { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ResumableUpload.cs b/src/Docker.Registry.DotNet/Models/ResumableUpload.cs index 8386273..eb8ea3c 100644 --- a/src/Docker.Registry.DotNet/Models/ResumableUpload.cs +++ b/src/Docker.Registry.DotNet/Models/ResumableUpload.cs @@ -13,26 +13,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models +namespace Docker.Registry.DotNet.Models; + +/// +/// A resumable upload response. +/// +public class ResumableUpload { /// - /// A resumable upload response. + /// The location of the created upload. Clients should use the contents verbatim to complete the upload, adding parameters where required. /// - public class ResumableUpload - { - /// - /// The location of the created upload. Clients should use the contents verbatim to complete the upload, adding parameters where required. - /// - public string Location { get; set; } + public string? Location { get; set; } - /// - /// Range header indicating the progress of the upload. When starting an upload, it will return an empty range, since no content has been received. - /// - public string Range { get; set; } + /// + /// Range header indicating the progress of the upload. When starting an upload, it will return an empty range, since no content has been received. + /// + public string? Range { get; set; } - /// - /// Identifies the docker upload uuid for the current request. - /// - public string DockerUploadUuid { get; set; } - } + /// + /// Identifies the docker upload uuid for the current request. + /// + public string? DockerUploadUuid { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs b/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs index 7e5c30d..e7ae15e 100644 --- a/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs +++ b/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs @@ -13,101 +13,90 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +namespace Docker.Registry.DotNet.OAuth; -using Docker.Registry.DotNet.Helpers; - -using Newtonsoft.Json; - -namespace Docker.Registry.DotNet.OAuth +internal class OAuthClient { - internal class OAuthClient + private readonly HttpClient _client = new(); + + private async Task GetTokenInnerAsync( + string realm, + string service, + string scope, + string username, + string password, + CancellationToken cancellationToken = default) { - private readonly HttpClient _client = new HttpClient(); + HttpRequestMessage request; - private async Task GetTokenInnerAsync( - string realm, - string service, - string scope, - string username, - string password, - CancellationToken cancellationToken = default) + if (username == null || password == null) { - HttpRequestMessage request; - - if (username == null || password == null) - { - var queryString = new QueryString(); + var queryString = new QueryString(); - queryString.AddIfNotEmpty("service", service); - queryString.AddIfNotEmpty("scope", scope); + queryString.AddIfNotEmpty("service", service); + queryString.AddIfNotEmpty("scope", scope); - var builder = new UriBuilder(new Uri(realm)) - { - Query = queryString.GetQueryString() - }; - - request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); - } - else + var builder = new UriBuilder(new Uri(realm)) { - request = new HttpRequestMessage(HttpMethod.Post, realm) - { - Content = new FormUrlEncodedContent( - new Dictionary - { - { "client_id", "Docker.Registry.DotNet" }, - { "grant_type", "password" }, - { "username", username }, - { "password", password }, - { "service", service }, - { "scope", scope }, - } - ), - }; - } + Query = queryString.GetQueryString() + }; - using (var response = await this._client.SendAsync(request, cancellationToken)) + request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); + } + else + { + request = new HttpRequestMessage(HttpMethod.Post, realm) { - if (!response.IsSuccessStatusCode) - throw new UnauthorizedAccessException("Unable to authenticate."); + Content = new FormUrlEncodedContent( + new Dictionary + { + { "client_id", "Docker.Registry.DotNet" }, + { "grant_type", "password" }, + { "username", username }, + { "password", password }, + { "service", service }, + { "scope", scope } + } + ) + }; + } - var body = await response.Content.ReadAsStringAsync(); + using (var response = await this._client.SendAsync(request, cancellationToken)) + { + if (!response.IsSuccessStatusCode) + throw new UnauthorizedAccessException("Unable to authenticate."); - var token = JsonConvert.DeserializeObject(body); + var body = await response.Content.ReadAsStringAsync(); - return token; - } - } + var token = JsonConvert.DeserializeObject(body); - public Task GetTokenAsync( - string realm, - string service, - string scope, - CancellationToken cancellationToken = default) - { - return this.GetTokenInnerAsync(realm, service, scope, null, null, cancellationToken); + return token; } + } - public Task GetTokenAsync( - string realm, - string service, - string scope, - string username, - string password, - CancellationToken cancellationToken = default) - { - return this.GetTokenInnerAsync( - realm, - service, - scope, - username, - password, - cancellationToken); - } + public Task GetTokenAsync( + string realm, + string service, + string scope, + CancellationToken cancellationToken = default) + { + return this.GetTokenInnerAsync(realm, service, scope, null, null, cancellationToken); + } + + public Task GetTokenAsync( + string realm, + string service, + string scope, + string username, + string password, + CancellationToken cancellationToken = default) + { + return this.GetTokenInnerAsync( + realm, + service, + scope, + username, + password, + cancellationToken); } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/OAuth/OAuthToken.cs b/src/Docker.Registry.DotNet/OAuth/OAuthToken.cs index f8aef3f..c7b2c84 100644 --- a/src/Docker.Registry.DotNet/OAuth/OAuthToken.cs +++ b/src/Docker.Registry.DotNet/OAuth/OAuthToken.cs @@ -13,24 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Runtime.Serialization; +namespace Docker.Registry.DotNet.OAuth; -namespace Docker.Registry.DotNet.OAuth +[DataContract] +internal class OAuthToken { - [DataContract] - internal class OAuthToken - { - [DataMember(Name = "token")] - public string Token { get; set; } + [DataMember(Name = "token")] + public string? Token { get; set; } - [DataMember(Name = "access_token")] - public string AccessToken { get; set; } + [DataMember(Name = "access_token")] + public string? AccessToken { get; set; } - [DataMember(Name = "expires_in")] - public int ExpiresIn { get; set; } + [DataMember(Name = "expires_in")] + public int ExpiresIn { get; set; } - [DataMember(Name = "issued_at")] - public DateTime IssuedAt { get; set; } - } + [DataMember(Name = "issued_at")] + public DateTime IssuedAt { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/QueryParameters/QueryParameterAttribute.cs b/src/Docker.Registry.DotNet/QueryParameters/QueryParameterAttribute.cs index 29aa929..d1154a0 100644 --- a/src/Docker.Registry.DotNet/QueryParameters/QueryParameterAttribute.cs +++ b/src/Docker.Registry.DotNet/QueryParameters/QueryParameterAttribute.cs @@ -13,18 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; +namespace Docker.Registry.DotNet.QueryParameters; -namespace Docker.Registry.DotNet.QueryParameters +[AttributeUsage(AttributeTargets.Property)] +internal class QueryParameterAttribute(string key) : Attribute { - [AttributeUsage(AttributeTargets.Property)] - internal class QueryParameterAttribute : Attribute - { - public QueryParameterAttribute(string key) - { - this.Key = key; - } - - public string Key { get; } - } + public string Key { get; } = key; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/IRegistryClient.cs b/src/Docker.Registry.DotNet/Registry/IRegistryClient.cs index 1102986..6ef209a 100644 --- a/src/Docker.Registry.DotNet/Registry/IRegistryClient.cs +++ b/src/Docker.Registry.DotNet/Registry/IRegistryClient.cs @@ -13,53 +13,46 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; +namespace Docker.Registry.DotNet.Registry; -using Docker.Registry.DotNet.Endpoints; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet.Registry +/// +/// The registry client. +/// +public interface IRegistryClient : IDisposable { /// - /// The registry client. + /// Manifest operations /// - public interface IRegistryClient : IDisposable - { - /// - /// Manifest operations - /// - [PublicAPI] - IManifestOperations Manifest { get; } + [PublicAPI] + IManifestOperations Manifest { get; } - /// - /// Catalog operations - /// - [PublicAPI] - ICatalogOperations Catalog { get; } + /// + /// Catalog operations + /// + [PublicAPI] + ICatalogOperations Catalog { get; } - /// - /// Blog operations - /// - [PublicAPI] - IBlobOperations Blobs { get; } + /// + /// Blog operations + /// + [PublicAPI] + IBlobOperations Blobs { get; } - /// - /// Blob Upload operations - /// - [PublicAPI] - IBlobUploadOperations BlobUploads { get; } + /// + /// Blob Upload operations + /// + [PublicAPI] + IBlobUploadOperations BlobUploads { get; } - /// - /// Tag operations - /// - [PublicAPI] - ITagOperations Tags { get; } + /// + /// Tag operations + /// + [PublicAPI] + ITagOperations Tags { get; } - /// - /// System operations - /// - [PublicAPI] - ISystemOperations System { get; } - } + /// + /// System operations + /// + [PublicAPI] + ISystemOperations System { get; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs b/src/Docker.Registry.DotNet/Registry/NetworkClient.cs index dcd615c..cdb05c7 100644 --- a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs +++ b/src/Docker.Registry.DotNet/Registry/NetworkClient.cs @@ -13,311 +13,300 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -using Docker.Registry.DotNet.Authentication; -using Docker.Registry.DotNet.Helpers; - -namespace Docker.Registry.DotNet.Registry +using JsonSerializer = Docker.Registry.DotNet.Helpers.JsonSerializer; + +namespace Docker.Registry.DotNet.Registry; + +internal class NetworkClient : IDisposable { - internal class NetworkClient : IDisposable - { - private const string UserAgent = "Docker.Registry.DotNet"; + private const string UserAgent = "Docker.Registry.DotNet"; - private static readonly TimeSpan InfiniteTimeout = - TimeSpan.FromMilliseconds(Timeout.Infinite); + private static readonly TimeSpan InfiniteTimeout = + TimeSpan.FromMilliseconds(Timeout.Infinite); - private readonly AuthenticationProvider _authenticationProvider; + private readonly AuthenticationProvider _authenticationProvider; - private readonly HttpClient _client; + private readonly HttpClient _client; - private readonly RegistryClientConfiguration _configuration; + private readonly RegistryClientConfiguration _configuration; - private readonly IEnumerable> _errorHandlers = - new Action[] - { - r => - { - if (r.StatusCode == HttpStatusCode.Unauthorized) - throw new UnauthorizedApiException(r); - } - }; - - private Uri _effectiveEndpointBaseUri; - - public NetworkClient( - RegistryClientConfiguration configuration, - AuthenticationProvider authenticationProvider) + private readonly IEnumerable> _errorHandlers = + new Action[] { - this._configuration = - configuration ?? throw new ArgumentNullException(nameof(configuration)); + r => + { + if (r.StatusCode == HttpStatusCode.Unauthorized) + throw new UnauthorizedApiException(r); + } + }; - this._authenticationProvider = authenticationProvider - ?? throw new ArgumentNullException( - nameof(authenticationProvider)); - if (configuration.HttpMessageHandler is null) - this._client = new HttpClient(); - else - this._client = new HttpClient(configuration.HttpMessageHandler); + private Uri? _effectiveEndpointBaseUri; - this.DefaultTimeout = configuration.DefaultTimeout; + public NetworkClient( + RegistryClientConfiguration configuration, + AuthenticationProvider authenticationProvider) + { + this._configuration = + configuration ?? throw new ArgumentNullException(nameof(configuration)); - this.JsonSerializer = new JsonSerializer(); + this._authenticationProvider = authenticationProvider + ?? throw new ArgumentNullException( + nameof(authenticationProvider)); - if (this._configuration.EndpointBaseUri != null) - this._effectiveEndpointBaseUri = this._configuration.EndpointBaseUri; - } + this._client = configuration.HttpMessageHandler is null + ? new HttpClient() + : new HttpClient(configuration.HttpMessageHandler); + + this.DefaultTimeout = configuration.DefaultTimeout; + + this.JsonSerializer = new JsonSerializer(); + + if (this._configuration.EndpointBaseUri != null) + this._effectiveEndpointBaseUri = this._configuration.EndpointBaseUri; + } + + public TimeSpan DefaultTimeout { get; set; } + + public JsonSerializer JsonSerializer { get; } + + public void Dispose() + { + this._client?.Dispose(); + } + + /// + /// Ensures that we have configured (and potentially probed) the end point. + /// + /// + private async Task EnsureConnection() + { + if (this._effectiveEndpointBaseUri != null) return; - public TimeSpan DefaultTimeout { get; set; } + var tryUrls = new List(); - public JsonSerializer JsonSerializer { get; } + // clean up the host + var host = this._configuration.Host.ToLower().Trim(); - public void Dispose() + if (host.StartsWith("http")) { - this._client?.Dispose(); + // includes schema -- don't add + tryUrls.Add(host); } - - /// - /// Ensures that we have configured (and potentially probed) the end point. - /// - /// - private async Task EnsureConnection() + else { - if (this._effectiveEndpointBaseUri != null) return; - - var tryUrls = new List(); + tryUrls.Add($"https://{host}"); + tryUrls.Add($"http://{host}"); + } - // clean up the host - var host = this._configuration.Host.ToLower().Trim(); + var exceptions = new List(); - if (host.StartsWith("http")) + foreach (var url in tryUrls) + try { - // includes schema -- don't add - tryUrls.Add(host); + await this.ProbeSingleAsync($"{url}/v2/"); + this._effectiveEndpointBaseUri = new Uri(url); + return; } - else + catch (Exception ex) { - tryUrls.Add($"https://{host}"); - tryUrls.Add($"http://{host}"); + exceptions.Add(ex); } - var exceptions = new List(); - - foreach (var url in tryUrls) - try - { - await this.ProbeSingleAsync($"{url}/v2/"); - this._effectiveEndpointBaseUri = new Uri(url); - return; - } - catch (Exception ex) - { - exceptions.Add(ex); - } - - throw new RegistryConnectionException( - $"Unable to connect to any: {tryUrls.Select(s => $"'{s}/v2/'").ToDelimitedString(", ")}'", - new AggregateException(exceptions)); - } + throw new RegistryConnectionException( + $"Unable to connect to any: {tryUrls.Select(s => $"'{s}/v2/'").ToDelimitedString(", ")}'", + new AggregateException(exceptions)); + } - private async Task ProbeSingleAsync(string uri) + private async Task ProbeSingleAsync(string uri) + { + using var request = new HttpRequestMessage(HttpMethod.Get, uri); + using (await this._client.SendAsync(request)) { - using var request = new HttpRequestMessage(HttpMethod.Get, uri); - using (await this._client.SendAsync(request)) - { - } } + } - internal async Task> MakeRequestAsync( - CancellationToken cancellationToken, - HttpMethod method, - string path, - IQueryString queryString = null, - IDictionary headers = null, - Func content = null) - { - //Console.WriteLine( - // "Requesting Path: {0} Method: {1} QueryString: {2}", - // path, - // method, - // queryString?.GetQueryString()); - - using var response = await this.InternalMakeRequestAsync( - this.DefaultTimeout, - HttpCompletionOption.ResponseContentRead, - method, - path, - queryString, - headers, - content, - cancellationToken); - - var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - var apiResponse = new RegistryApiResponse( - response.StatusCode, - responseBody, - response.Headers); - - this.HandleIfErrorResponse(apiResponse); - - return apiResponse; - } + internal async Task> MakeRequestAsync( + CancellationToken cancellationToken, + HttpMethod method, + string path, + IQueryString? queryString = null, + IDictionary? headers = null, + Func? content = null) + { + //Console.WriteLine( + // "Requesting Path: {0} Method: {1} QueryString: {2}", + // path, + // method, + // queryString?.GetQueryString()); + + using var response = await this.InternalMakeRequestAsync( + this.DefaultTimeout, + HttpCompletionOption.ResponseContentRead, + method, + path, + queryString, + headers, + content, + cancellationToken); + + var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + var apiResponse = new RegistryApiResponse( + response.StatusCode, + responseBody, + response.Headers); + + this.HandleIfErrorResponse(apiResponse); + + return apiResponse; + } - internal async Task> MakeRequestNotErrorAsync( - CancellationToken cancellationToken, - HttpMethod method, - string path, - IQueryString queryString = null, - IDictionary headers = null, - Func content = null) - { - //Console.WriteLine( - // "Requesting Path: {0} Method: {1} QueryString: {2}", - // path, - // method, - // queryString?.GetQueryString()); - - using var response = await this.InternalMakeRequestAsync( - this.DefaultTimeout, - HttpCompletionOption.ResponseContentRead, - method, - path, - queryString, - headers, - content, - cancellationToken); - - var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - var apiResponse = new RegistryApiResponse( - response.StatusCode, - responseBody, - response.Headers); - - return apiResponse; - } + internal async Task> MakeRequestNotErrorAsync( + CancellationToken cancellationToken, + HttpMethod method, + string path, + IQueryString? queryString = null, + IDictionary? headers = null, + Func? content = null) + { + //Console.WriteLine( + // "Requesting Path: {0} Method: {1} QueryString: {2}", + // path, + // method, + // queryString?.GetQueryString()); + + using var response = await this.InternalMakeRequestAsync( + this.DefaultTimeout, + HttpCompletionOption.ResponseContentRead, + method, + path, + queryString, + headers, + content, + cancellationToken); + + var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + var apiResponse = new RegistryApiResponse( + response.StatusCode, + responseBody, + response.Headers); + + return apiResponse; + } - internal async Task> MakeRequestForStreamedResponseAsync( - CancellationToken cancellationToken, - HttpMethod method, - string path, - IQueryString queryString = null) - { - var response = await this.InternalMakeRequestAsync( - InfiniteTimeout, - HttpCompletionOption.ResponseHeadersRead, - method, - path, - queryString, - null, - null, - cancellationToken); - - var body = await response.Content.ReadAsStreamAsync(); - - var apiResponse = new RegistryApiResponse( - response.StatusCode, - body, - response.Headers); - - this.HandleIfErrorResponse(apiResponse); - - return apiResponse; - } + internal async Task> MakeRequestForStreamedResponseAsync( + CancellationToken cancellationToken, + HttpMethod method, + string path, + IQueryString? queryString = null) + { + var response = await this.InternalMakeRequestAsync( + InfiniteTimeout, + HttpCompletionOption.ResponseHeadersRead, + method, + path, + queryString, + null, + null, + cancellationToken); + + var body = await response.Content.ReadAsStreamAsync(); + + var apiResponse = new RegistryApiResponse( + response.StatusCode, + body, + response.Headers); + + this.HandleIfErrorResponse(apiResponse); + + return apiResponse; + } - private async Task InternalMakeRequestAsync( - TimeSpan timeout, - HttpCompletionOption completionOption, - HttpMethod method, - string path, - IQueryString queryString, - IDictionary headers, - Func content, - CancellationToken cancellationToken) - { - await this.EnsureConnection(); + private async Task InternalMakeRequestAsync( + TimeSpan timeout, + HttpCompletionOption completionOption, + HttpMethod method, + string path, + IQueryString queryString, + IDictionary headers, + Func content, + CancellationToken cancellationToken) + { + await this.EnsureConnection(); - var request = this.PrepareRequest(method, path, queryString, headers, content); + var request = this.PrepareRequest(method, path, queryString, headers, content); - if (timeout != InfiniteTimeout) - { - var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - timeoutTokenSource.CancelAfter(timeout); - cancellationToken = timeoutTokenSource.Token; - } + if (timeout != InfiniteTimeout) + { + var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutTokenSource.CancelAfter(timeout); + cancellationToken = timeoutTokenSource.Token; + } - await this._authenticationProvider.AuthenticateAsync(request); + await this._authenticationProvider.AuthenticateAsync(request); - var response = await this._client.SendAsync( - request, - completionOption, - cancellationToken); + var response = await this._client.SendAsync( + request, + completionOption, + cancellationToken); - if (response.StatusCode != HttpStatusCode.Unauthorized) return response; + if (response.StatusCode != HttpStatusCode.Unauthorized) return response; - //Prepare another request (we can't reuse the same request) - var request2 = this.PrepareRequest(method, path, queryString, headers, content); + //Prepare another request (we can't reuse the same request) + var request2 = this.PrepareRequest(method, path, queryString, headers, content); - //Authenticate given the challenge - await this._authenticationProvider.AuthenticateAsync(request2, response); + //Authenticate given the challenge + await this._authenticationProvider.AuthenticateAsync(request2, response); - //Send it again - response = await this._client.SendAsync( - request2, - completionOption, - cancellationToken); + //Send it again + response = await this._client.SendAsync( + request2, + completionOption, + cancellationToken); - return response; - } + return response; + } - internal void HandleIfErrorResponse(RegistryApiResponse response) - { - // If no customer handlers just default the response. - foreach (var handler in this._errorHandlers) handler(response); + internal void HandleIfErrorResponse(RegistryApiResponse response) + { + // If no customer handlers just default the response. + foreach (var handler in this._errorHandlers) handler(response); - // No custom handler was fired. Default the response for generic success/failures. - if (response.StatusCode is < HttpStatusCode.OK or >= HttpStatusCode.BadRequest) - throw new RegistryApiException(response); - } + // No custom handler was fired. Default the response for generic success/failures. + if (response.StatusCode is < HttpStatusCode.OK or >= HttpStatusCode.BadRequest) + throw new RegistryApiException(response); + } - internal void HandleIfErrorResponse(RegistryApiResponse response) - { - // If no customer handlers just default the response. - foreach (var handler in this._errorHandlers) handler(response); + internal void HandleIfErrorResponse(RegistryApiResponse response) + { + // If no customer handlers just default the response. + foreach (var handler in this._errorHandlers) handler(response); - // No custom handler was fired. Default the response for generic success/failures. - if (response.StatusCode is < HttpStatusCode.OK or >= HttpStatusCode.BadRequest) - throw new RegistryApiException(response); - } + // No custom handler was fired. Default the response for generic success/failures. + if (response.StatusCode is < HttpStatusCode.OK or >= HttpStatusCode.BadRequest) + throw new RegistryApiException(response); + } - internal HttpRequestMessage PrepareRequest( - HttpMethod method, - string path, - IQueryString queryString, - IDictionary headers, - Func content) - { - if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); + internal HttpRequestMessage PrepareRequest( + HttpMethod method, + string path, + IQueryString queryString, + IDictionary headers, + Func content) + { + if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); - var request = new HttpRequestMessage( - method, - this._effectiveEndpointBaseUri.BuildUri(path, queryString)); + var request = new HttpRequestMessage( + method, + this._effectiveEndpointBaseUri.BuildUri(path, queryString)); - request.Headers.Add("User-Agent", UserAgent); - request.Headers.AddRange(headers); + request.Headers.Add("User-Agent", UserAgent); + request.Headers.AddRange(headers); - //Create the content - request.Content = content?.Invoke(); + //Create the content + request.Content = content?.Invoke(); - return request; - } + return request; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/RegistryApiException.cs b/src/Docker.Registry.DotNet/Registry/RegistryApiException.cs index a86dafb..6e33856 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryApiException.cs +++ b/src/Docker.Registry.DotNet/Registry/RegistryApiException.cs @@ -13,34 +13,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Net; -using System.Net.Http.Headers; +namespace Docker.Registry.DotNet.Registry; -namespace Docker.Registry.DotNet.Registry +public class RegistryApiException : Exception { - public class RegistryApiException : Exception + internal RegistryApiException(RegistryApiResponse response) + : base($"Docker API responded with status code={response.StatusCode}") { - internal RegistryApiException(RegistryApiResponse response) - : base($"Docker API responded with status code={response.StatusCode}") - { - this.StatusCode = response.StatusCode; - this.Headers = response.Headers; - } + this.StatusCode = response.StatusCode; + this.Headers = response.Headers; + } - public HttpStatusCode StatusCode { get; } + public HttpStatusCode StatusCode { get; } - public HttpResponseHeaders Headers { get; } - } + public HttpResponseHeaders Headers { get; } +} - public class RegistryApiException : RegistryApiException +public class RegistryApiException : RegistryApiException +{ + internal RegistryApiException(RegistryApiResponse response) + : base(response) { - internal RegistryApiException(RegistryApiResponse response) - : base(response) - { - this.Body = response.Body; - } - - public TBody Body { get; } + this.Body = response.Body; } + + public TBody Body { get; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs b/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs index 69b0a3d..770af4d 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs +++ b/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs @@ -13,35 +13,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Net; -using System.Net.Http.Headers; +namespace Docker.Registry.DotNet.Registry; -namespace Docker.Registry.DotNet.Registry +internal abstract class RegistryApiResponse(HttpStatusCode statusCode, HttpResponseHeaders headers) { - internal abstract class RegistryApiResponse - { - protected RegistryApiResponse(HttpStatusCode statusCode, HttpResponseHeaders headers) - { - this.StatusCode = statusCode; - this.Headers = headers; - } - - public HttpStatusCode StatusCode { get; } + public HttpStatusCode StatusCode { get; } = statusCode; - public HttpResponseHeaders Headers { get; } - } + public HttpResponseHeaders Headers { get; } = headers; +} - internal class RegistryApiResponse : RegistryApiResponse +internal class RegistryApiResponse : RegistryApiResponse +{ + internal RegistryApiResponse( + HttpStatusCode statusCode, + TBody body, + HttpResponseHeaders headers) + : base(statusCode, headers) { - internal RegistryApiResponse( - HttpStatusCode statusCode, - TBody body, - HttpResponseHeaders headers) - : base(statusCode, headers) - { - this.Body = body; - } - - public TBody Body { get; } + this.Body = body; } + + public TBody Body { get; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/RegistryClient.cs b/src/Docker.Registry.DotNet/Registry/RegistryClient.cs index 55b0da2..f7628bb 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryClient.cs +++ b/src/Docker.Registry.DotNet/Registry/RegistryClient.cs @@ -13,52 +13,47 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; - -using Docker.Registry.DotNet.Authentication; -using Docker.Registry.DotNet.Endpoints; using Docker.Registry.DotNet.Endpoints.Implementations; -namespace Docker.Registry.DotNet.Registry +namespace Docker.Registry.DotNet.Registry; + +internal sealed class RegistryClient : IRegistryClient { - internal sealed class RegistryClient : IRegistryClient - { - private readonly NetworkClient _client; + private readonly NetworkClient _client; - public RegistryClient( - RegistryClientConfiguration configuration, - AuthenticationProvider authenticationProvider) - { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + public RegistryClient( + RegistryClientConfiguration configuration, + AuthenticationProvider authenticationProvider) + { + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - if (authenticationProvider == null) - throw new ArgumentNullException(nameof(authenticationProvider)); + if (authenticationProvider == null) + throw new ArgumentNullException(nameof(authenticationProvider)); - _client = new NetworkClient(configuration, authenticationProvider); + _client = new NetworkClient(configuration, authenticationProvider); - this.Manifest = new ManifestOperations(_client); - this.Catalog = new CatalogOperations(_client); - this.Blobs = new BlobOperations(_client); - this.BlobUploads = new BlobUploadOperations(_client); - this.System = new SystemOperations(_client); - this.Tags = new TagOperations(_client); - } + this.Manifest = new ManifestOperations(_client); + this.Catalog = new CatalogOperations(_client); + this.Blobs = new BlobOperations(_client); + this.BlobUploads = new BlobUploadOperations(_client); + this.System = new SystemOperations(_client); + this.Tags = new TagOperations(_client); + } - public IBlobUploadOperations BlobUploads { get; } + public IBlobUploadOperations BlobUploads { get; } - public IManifestOperations Manifest { get; } + public IManifestOperations Manifest { get; } - public ICatalogOperations Catalog { get; } + public ICatalogOperations Catalog { get; } - public IBlobOperations Blobs { get; } + public IBlobOperations Blobs { get; } - public ITagOperations Tags { get; } + public ITagOperations Tags { get; } - public ISystemOperations System { get; } + public ISystemOperations System { get; } - public void Dispose() - { - _client?.Dispose(); - } + public void Dispose() + { + _client?.Dispose(); } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/RegistryConnectionException.cs b/src/Docker.Registry.DotNet/Registry/RegistryConnectionException.cs index 39ffe17..c397da4 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryConnectionException.cs +++ b/src/Docker.Registry.DotNet/Registry/RegistryConnectionException.cs @@ -13,30 +13,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; +namespace Docker.Registry.DotNet.Registry; -namespace Docker.Registry.DotNet.Registry +/// +/// Thrown when connecting to a registry fails. +/// +public class RegistryConnectionException : Exception { - /// - /// Thrown when connecting to a registry fails. - /// - public class RegistryConnectionException : Exception + /// + public RegistryConnectionException() { - /// - public RegistryConnectionException() - { - } + } - /// - public RegistryConnectionException(string message) - : base(message) - { - } + /// + public RegistryConnectionException(string message) + : base(message) + { + } - /// - public RegistryConnectionException(string message, Exception innerException) - : base(message, innerException) - { - } + /// + public RegistryConnectionException(string message, Exception innerException) + : base(message, innerException) + { } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/UnauthorizedApiException.cs b/src/Docker.Registry.DotNet/Registry/UnauthorizedApiException.cs index 064684d..e309aa0 100644 --- a/src/Docker.Registry.DotNet/Registry/UnauthorizedApiException.cs +++ b/src/Docker.Registry.DotNet/Registry/UnauthorizedApiException.cs @@ -13,16 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Registry +namespace Docker.Registry.DotNet.Registry; + +/// +/// Thrown when an api response is returned as unauthorized. +/// +public class UnauthorizedApiException : RegistryApiException { - /// - /// Thrown when an api response is returned as unauthorized. - /// - public class UnauthorizedApiException : RegistryApiException + internal UnauthorizedApiException(RegistryApiResponse response) + : base(response) { - internal UnauthorizedApiException(RegistryApiResponse response) - : base(response) - { - } } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs index c08e1e1..6b63b72 100644 --- a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs +++ b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs @@ -13,99 +13,89 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Net.Http; -using System.Threading; +namespace Docker.Registry.DotNet; -using Docker.Registry.DotNet.Authentication; -using Docker.Registry.DotNet.Registry; - -using JetBrains.Annotations; - -namespace Docker.Registry.DotNet +public class RegistryClientConfiguration { - public class RegistryClientConfiguration + /// + /// Creates an instance of the RegistryClientConfiguration. + /// + /// + /// + public RegistryClientConfiguration(string host, TimeSpan defaultTimeout = default) + : this(defaultTimeout) { - /// - /// Creates an instance of the RegistryClientConfiguration. - /// - /// - /// - public RegistryClientConfiguration(string host, TimeSpan defaultTimeout = default) - : this(defaultTimeout) - { - if (string.IsNullOrWhiteSpace(host)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(host)); + if (string.IsNullOrWhiteSpace(host)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(host)); - this.Host = host; - } + this.Host = host; + } - /// - /// Creates an instance of the RegistryClientConfiguration. - /// - /// - /// - /// - public RegistryClientConfiguration( - string host, - HttpMessageHandler httpMessageHandler, - TimeSpan defaultTimeout = default) - : this(defaultTimeout) - { - if (string.IsNullOrWhiteSpace(host)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(host)); + /// + /// Creates an instance of the RegistryClientConfiguration. + /// + /// + /// + /// + public RegistryClientConfiguration( + string host, + HttpMessageHandler? httpMessageHandler, + TimeSpan defaultTimeout = default) + : this(defaultTimeout) + { + if (string.IsNullOrWhiteSpace(host)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(host)); - this.Host = host; - this.HttpMessageHandler = httpMessageHandler; - } + this.Host = host; + this.HttpMessageHandler = httpMessageHandler; + } - /// - /// Obsolete constructor that allows a uri to be used to specify a registry. - /// - /// - /// - [Obsolete("Use the constructor that allows you to specify a host.")] - public RegistryClientConfiguration(Uri endpoint, TimeSpan defaultTimeout = default) - : this(defaultTimeout) - { - if (endpoint == null) - throw new ArgumentNullException(nameof(endpoint)); + /// + /// Obsolete constructor that allows a uri to be used to specify a registry. + /// + /// + /// + [Obsolete("Use the constructor that allows you to specify a host.")] + public RegistryClientConfiguration(Uri endpoint, TimeSpan defaultTimeout = default) + : this(defaultTimeout) + { + if (endpoint == null) + throw new ArgumentNullException(nameof(endpoint)); - this.EndpointBaseUri = endpoint; - } + this.EndpointBaseUri = endpoint; + } - private RegistryClientConfiguration(TimeSpan defaultTimeout) + private RegistryClientConfiguration(TimeSpan defaultTimeout) + { + if (defaultTimeout != TimeSpan.Zero) { - if (defaultTimeout != TimeSpan.Zero) - { - if (defaultTimeout < Timeout.InfiniteTimeSpan) - // TODO: Should be a resource for localization. - // TODO: Is this a good message? - throw new ArgumentException( - "Timeout must be greater than Timeout.Infinite", - nameof(defaultTimeout)); - this.DefaultTimeout = defaultTimeout; - } + if (defaultTimeout < Timeout.InfiniteTimeSpan) + // TODO: Should be a resource for localization. + // TODO: Is this a good message? + throw new ArgumentException( + "Timeout must be greater than Timeout.Infinite", + nameof(defaultTimeout)); + this.DefaultTimeout = defaultTimeout; } + } - public Uri EndpointBaseUri { get; } + public Uri? EndpointBaseUri { get; } - public string Host { get; } + public string Host { get; } - public HttpMessageHandler HttpMessageHandler { get; } + public HttpMessageHandler? HttpMessageHandler { get; } - public TimeSpan DefaultTimeout { get; internal set; } = TimeSpan.FromSeconds(100); + public TimeSpan DefaultTimeout { get; internal set; } = TimeSpan.FromSeconds(100); - [PublicAPI] - public IRegistryClient CreateClient() - { - return new RegistryClient(this, new AnonymousOAuthAuthenticationProvider()); - } + [PublicAPI] + public IRegistryClient CreateClient() + { + return new RegistryClient(this, new AnonymousOAuthAuthenticationProvider()); + } - [PublicAPI] - public IRegistryClient CreateClient(AuthenticationProvider authenticationProvider) - { - return new RegistryClient(this, authenticationProvider); - } + [PublicAPI] + public IRegistryClient CreateClient(AuthenticationProvider authenticationProvider) + { + return new RegistryClient(this, authenticationProvider); } } \ No newline at end of file diff --git a/test/Docker.Registry.DotNet.Tests/Docker.Registry.DotNet.Tests.csproj b/test/Docker.Registry.DotNet.Tests/Docker.Registry.DotNet.Tests.csproj index 018530e..7fe5895 100644 --- a/test/Docker.Registry.DotNet.Tests/Docker.Registry.DotNet.Tests.csproj +++ b/test/Docker.Registry.DotNet.Tests/Docker.Registry.DotNet.Tests.csproj @@ -1,20 +1,28 @@ - - netcoreapp3.1 + + net8.0 + latest + enable + enable + false + - false - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - - - - + + + - - - - - + \ No newline at end of file From 998764e3d407ea8f7858609ef7718587ce163103 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Thu, 8 Aug 2024 23:23:50 -0400 Subject: [PATCH 02/17] Additional clean up. --- .../Docker.Registry.DotNet.csproj | 2 +- .../{ => Helpers}/DictionaryExtensions.cs | 2 +- .../Helpers/IDictionaryExtensions.cs | 40 ------------------- .../Helpers/QueryString.cs | 26 ++++++------ .../OAuth/OAuthClient.cs | 19 +++++---- .../Registry/NetworkClient.cs | 12 +++--- 6 files changed, 30 insertions(+), 71 deletions(-) rename src/Docker.Registry.DotNet/{ => Helpers}/DictionaryExtensions.cs (96%) delete mode 100644 src/Docker.Registry.DotNet/Helpers/IDictionaryExtensions.cs diff --git a/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj b/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj index 4c28218..ddac3bc 100644 --- a/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj +++ b/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj @@ -1,6 +1,6 @@  - netstandard2.0;net6.0;net7.0;net8.0 + netstandard2.0;net5.0;net6.0;net7.0;net8.0 bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml latest enable diff --git a/src/Docker.Registry.DotNet/DictionaryExtensions.cs b/src/Docker.Registry.DotNet/Helpers/DictionaryExtensions.cs similarity index 96% rename from src/Docker.Registry.DotNet/DictionaryExtensions.cs rename to src/Docker.Registry.DotNet/Helpers/DictionaryExtensions.cs index b3abfb4..2449338 100644 --- a/src/Docker.Registry.DotNet/DictionaryExtensions.cs +++ b/src/Docker.Registry.DotNet/Helpers/DictionaryExtensions.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet; +namespace Docker.Registry.DotNet.Helpers; internal static class DictionaryExtensions { diff --git a/src/Docker.Registry.DotNet/Helpers/IDictionaryExtensions.cs b/src/Docker.Registry.DotNet/Helpers/IDictionaryExtensions.cs deleted file mode 100644 index 9a63432..0000000 --- a/src/Docker.Registry.DotNet/Helpers/IDictionaryExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Helpers; - -internal static class IDictionaryExtensions -{ - public static string GetQueryString(this IDictionary values) - { - return string.Join( - "&", - values.Select( - pair => string.Join( - "&", - pair.Value.Select( - v => $"{Uri.EscapeUriString(pair.Key)}={Uri.EscapeDataString(v)}")))); - } - - public static TValue GetValueOrDefault( - this IDictionary dict, - TKey key) - { - if (dict.TryGetValue(key, out var value)) - return value; - - return default; - } -} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/QueryString.cs b/src/Docker.Registry.DotNet/Helpers/QueryString.cs index 72bd7a0..0fca59f 100644 --- a/src/Docker.Registry.DotNet/Helpers/QueryString.cs +++ b/src/Docker.Registry.DotNet/Helpers/QueryString.cs @@ -17,26 +17,26 @@ namespace Docker.Registry.DotNet.Helpers; internal class QueryString : IQueryString { - private readonly Dictionary _values = new Dictionary(); + private readonly Dictionary _values = new(); public string GetQueryString() { - return string.Join( - "&", - this._values.Select( - pair => string.Join( - "&", - pair.Value.Select( - v => $"{Uri.EscapeUriString(pair.Key)}={Uri.EscapeDataString(v)}")))); - } + return string.Join( + "&", + this._values.Select( + pair => string.Join( + "&", + pair.Value.Select( + v => $"{Uri.EscapeUriString(pair.Key)}={Uri.EscapeDataString(v)}")))); + } public void Add(string key, string value) { - this._values.Add(key, [value]); - } + this._values.Add(key, [value]); + } public void Add(string key, string[] values) { - this._values.Add(key, values); - } + this._values.Add(key, values); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs b/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs index e7ae15e..848038d 100644 --- a/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs +++ b/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs @@ -23,8 +23,8 @@ private async Task GetTokenInnerAsync( string realm, string service, string scope, - string username, - string password, + string? username, + string? password, CancellationToken cancellationToken = default) { HttpRequestMessage request; @@ -61,17 +61,16 @@ private async Task GetTokenInnerAsync( }; } - using (var response = await this._client.SendAsync(request, cancellationToken)) - { - if (!response.IsSuccessStatusCode) - throw new UnauthorizedAccessException("Unable to authenticate."); + using var response = await this._client.SendAsync(request, cancellationToken); - var body = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + throw new UnauthorizedAccessException("Unable to authenticate."); - var token = JsonConvert.DeserializeObject(body); + var body = await response.Content.ReadAsStringAsync(); - return token; - } + var token = JsonConvert.DeserializeObject(body); + + return token; } public Task GetTokenAsync( diff --git a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs b/src/Docker.Registry.DotNet/Registry/NetworkClient.cs index cdb05c7..b92bc5a 100644 --- a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs +++ b/src/Docker.Registry.DotNet/Registry/NetworkClient.cs @@ -228,9 +228,9 @@ private async Task InternalMakeRequestAsync( HttpCompletionOption completionOption, HttpMethod method, string path, - IQueryString queryString, - IDictionary headers, - Func content, + IQueryString? queryString, + IDictionary? headers, + Func? content, CancellationToken cancellationToken) { await this.EnsureConnection(); @@ -291,9 +291,9 @@ internal void HandleIfErrorResponse(RegistryApiResponse response) internal HttpRequestMessage PrepareRequest( HttpMethod method, string path, - IQueryString queryString, - IDictionary headers, - Func content) + IQueryString? queryString, + IDictionary? headers, + Func? content) { if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); From 8abe2842cff497e4c2fbb02b60c37557f756b8ce Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Fri, 9 Aug 2024 01:14:21 -0400 Subject: [PATCH 03/17] Removing Async wording. Cleanup. --- .../ViewModel/RepositoryViewModel.cs | 4 +- .../AnonymousOAuthAuthenticationProvider.cs | 32 +- .../Authentication/AuthenticationProvider.cs | 4 +- .../BasicAuthenticationProvider.cs | 4 +- .../PasswordOAuthAuthenticationProvider.cs | 8 +- .../Endpoints/IBlobOperations.cs | 18 +- .../Endpoints/IBlobUploadOperations.cs | 57 +-- .../Endpoints/ICatalogOperations.cs | 6 +- .../Endpoints/IManifestOperations.cs | 24 +- .../Endpoints/ISystemOperations.cs | 2 +- .../Endpoints/ITagOperations.cs | 6 +- .../Implementations/BlobOperations.cs | 57 ++- .../Implementations/BlobUploadOperations.cs | 387 ++++++++---------- .../Implementations/CatalogOperations.cs | 26 +- .../Implementations/ManifestOperations.cs | 261 ++++++------ .../Implementations/SystemOperations.cs | 6 +- .../Implementations/TagOperations.cs | 38 +- .../Helpers/HttpContentHelper.cs | 41 ++ .../Helpers/HttpUtility.cs | 2 +- ...QueryString.cs => IReadOnlyQueryString.cs} | 2 +- .../Helpers/QueryString.cs | 33 +- .../Helpers/QueryStringExtensions.cs | 40 +- .../Models/{Catalog.cs => CatalogResponse.cs} | 44 +- ...agsParameters.cs => ListTagsParameters.cs} | 48 +-- ...ageTagsResponse.cs => ListTagsResponse.cs} | 50 +-- .../OAuth/OAuthClient.cs | 27 +- .../Registry/IRegistryUriBuilder.cs | 32 ++ .../Registry/NetworkClient.cs | 90 ++-- .../Registry/RegistryApiResponse.cs | 4 +- .../Registry/RegistryUriBuilder.cs | 49 +++ .../RegistryClientConfiguration.cs | 17 - 31 files changed, 719 insertions(+), 700 deletions(-) create mode 100644 src/Docker.Registry.DotNet/Helpers/HttpContentHelper.cs rename src/Docker.Registry.DotNet/Helpers/{IQueryString.cs => IReadOnlyQueryString.cs} (94%) rename src/Docker.Registry.DotNet/Models/{Catalog.cs => CatalogResponse.cs} (88%) rename src/Docker.Registry.DotNet/Models/{ListImageTagsParameters.cs => ListTagsParameters.cs} (93%) rename src/Docker.Registry.DotNet/Models/{ListImageTagsResponse.cs => ListTagsResponse.cs} (89%) create mode 100644 src/Docker.Registry.DotNet/Registry/IRegistryUriBuilder.cs create mode 100644 src/Docker.Registry.DotNet/Registry/RegistryUriBuilder.cs diff --git a/samples/DockerRegistryExplorer/ViewModel/RepositoryViewModel.cs b/samples/DockerRegistryExplorer/ViewModel/RepositoryViewModel.cs index 324452b..0f728d5 100644 --- a/samples/DockerRegistryExplorer/ViewModel/RepositoryViewModel.cs +++ b/samples/DockerRegistryExplorer/ViewModel/RepositoryViewModel.cs @@ -66,9 +66,9 @@ public void Refresh() private async Task ListImagesTags() { - var tags = await this._registryClient.Tags.ListImageTagsAsync( + var tags = await this._registryClient.Tags.ListTags( this.Name, - new ListImageTagsParameters()); + new ListTagsParameters()); if (tags.Tags == null) this.Tags = new TagViewModel[] { }; else diff --git a/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs index 00ccf31..4c44e34 100644 --- a/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs @@ -18,31 +18,31 @@ namespace Docker.Registry.DotNet.Authentication; [PublicAPI] public class AnonymousOAuthAuthenticationProvider : AuthenticationProvider { - private readonly OAuthClient _client = new OAuthClient(); + private readonly OAuthClient _client = new(); private static string Schema { get; } = "Bearer"; - public override Task AuthenticateAsync(HttpRequestMessage request) + public override Task Authenticate(HttpRequestMessage request) { - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public override async Task AuthenticateAsync( + public override async Task Authenticate( HttpRequestMessage request, HttpResponseMessage response) { - var header = this.TryGetSchemaHeader(response, Schema); + var header = this.TryGetSchemaHeader(response, Schema); - //Get the bearer bits - var bearerBits = AuthenticateParser.ParseTyped(header.Parameter); + //Get the bearer bits + var bearerBits = AuthenticateParser.ParseTyped(header.Parameter); - //Get the token - var token = await this._client.GetTokenAsync( - bearerBits.Realm, - bearerBits.Service, - bearerBits.Scope); + //Get the token + var token = await this._client.GetToken( + bearerBits.Realm, + bearerBits.Service, + bearerBits.Scope); - //Set the header - request.Headers.Authorization = new AuthenticationHeaderValue(Schema, token.Token); - } + //Set the header + request.Headers.Authorization = new AuthenticationHeaderValue(Schema, token.Token); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs index 80dc535..91a4c15 100644 --- a/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs @@ -25,7 +25,7 @@ public abstract class AuthenticationProvider /// /// /// - public abstract Task AuthenticateAsync(HttpRequestMessage request); + public abstract Task Authenticate(HttpRequestMessage request); /// /// Called when the send is challenged. @@ -33,7 +33,7 @@ public abstract class AuthenticationProvider /// /// /// - public abstract Task AuthenticateAsync( + public abstract Task Authenticate( HttpRequestMessage request, HttpResponseMessage response); diff --git a/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs index c189d2f..2a8d77a 100644 --- a/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs @@ -20,12 +20,12 @@ public class BasicAuthenticationProvider(string username, string password) : Aut { private static string Schema { get; } = "Basic"; - public override Task AuthenticateAsync(HttpRequestMessage request) + public override Task Authenticate(HttpRequestMessage request) { return Task.CompletedTask; } - public override Task AuthenticateAsync( + public override Task Authenticate( HttpRequestMessage request, HttpResponseMessage response) { diff --git a/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs index c203979..849697a 100644 --- a/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs @@ -23,12 +23,12 @@ public class PasswordOAuthAuthenticationProvider(string username, string passwor private static string Schema { get; } = "Bearer"; - public override Task AuthenticateAsync(HttpRequestMessage request) + public override Task Authenticate(HttpRequestMessage request) { return Task.CompletedTask; } - public override async Task AuthenticateAsync( + public override async Task Authenticate( HttpRequestMessage request, HttpResponseMessage response) { @@ -37,7 +37,7 @@ public override async Task AuthenticateAsync( //Get the bearer bits var bearerBits = AuthenticateParser.ParseTyped(header.Parameter); - string scope = null; + string? scope = null; if (!string.IsNullOrWhiteSpace(bearerBits.Scope)) { @@ -47,7 +47,7 @@ public override async Task AuthenticateAsync( } //Get the token - var token = await this._client.GetTokenAsync( + var token = await this._client.GetToken( bearerBits.Realm, bearerBits.Service, scope, diff --git a/src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs b/src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs index 12c5d54..8554160 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs @@ -23,36 +23,36 @@ public interface IBlobOperations /// /// /// - /// + /// /// [PublicAPI] - Task GetBlobAsync( + Task GetBlob( string name, string digest, - CancellationToken cancellationToken = default); + CancellationToken token = default); /// /// Delete the blob identified by name and digest. /// /// /// - /// + /// /// [PublicAPI] - Task DeleteBlobAsync( + Task DeleteBlob( string name, string digest, - CancellationToken cancellationToken = default); + CancellationToken token = default); /// /// Existing Layers /// /// /// - /// + /// /// - Task IsExistBlobAsync( + Task BlobExists( string name, string digest, - CancellationToken cancellationToken = default); + CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs b/src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs index 00e2448..38ad40a 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs @@ -24,43 +24,28 @@ public interface IBlobUploadOperations /// /// /// - /// + /// /// [PublicAPI] - Task UploadBlobAsync( + Task UploadBlob( string name, int contentLength, Stream stream, string digest, - CancellationToken cancellationToken = default); - - /// - /// Initiate a resumable blob upload. If successful, an upload location will be provided to complete the upload. - /// Optionally, if the digest parameter is present, the request body will be used to complete the upload in a single - /// request. - /// - /// - /// - /// - /// - [PublicAPI] - Task InitiateBlobUploadAsync( - string name, - Stream? stream = null, - CancellationToken cancellationToken = default); + CancellationToken token = default); /// /// Mount a blob identified by the mount parameter from another repository. /// /// /// - /// + /// /// [PublicAPI] - Task MountBlobAsync( + Task MountBlob( string name, MountParameters parameters, - CancellationToken cancellationToken = default); + CancellationToken token = default); /// /// Retrieve status of upload identified by uuid. The primary purpose of this endpoint is to resolve the current status @@ -83,15 +68,15 @@ Task GetBlobUploadStatus( /// /// /// - /// + /// /// [PublicAPI] - Task UploadBlobChunkAsync( + Task UploadBlobChunk( ResumableUpload resumable, Stream chunk, long? from = null, long? to = null, - CancellationToken cancellationToken = default); + CancellationToken token = default); /// /// Complete the upload specified by ResumableUploadResponse, optionally appending the body as the final chunk. @@ -101,16 +86,16 @@ Task UploadBlobChunkAsync( /// /// /// - /// + /// /// [PublicAPI] - Task CompleteBlobUploadAsync( + Task CompleteBlobUpload( ResumableUpload resumable, string digest, Stream? chunk = null, long? from = null, long? to = null, - CancellationToken cancellationToken = default); + CancellationToken token = default); /// /// Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished uploads @@ -118,23 +103,23 @@ Task CompleteBlobUploadAsync( /// /// /// - /// + /// /// [PublicAPI] - Task CancelBlobUploadAsync( + Task CancelBlobUpload( string name, string uuid, - CancellationToken cancellationToken = default); + CancellationToken token = default); /// /// Starting An Upload /// /// - /// + /// /// - Task StartUploadBlobAsync( + Task StartUploadBlob( string name, - CancellationToken cancellationToken = default); + CancellationToken token = default); /// /// A monolithic upload is simply a chunked upload with a single chunk and may be favored by clients that would like to avoided the complexity of chunking @@ -142,11 +127,11 @@ Task StartUploadBlobAsync( /// /// /// - /// + /// /// - Task MonolithicUploadBlobAsync( + Task MonolithicUploadBlob( ResumableUpload resumable, string digest, Stream stream, - CancellationToken cancellationToken = default); + CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs b/src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs index 787a74d..6f41832 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs @@ -22,10 +22,10 @@ public interface ICatalogOperations /// Retrieve a sorted, json list of repositories available in the registry. /// /// - /// + /// /// [PublicAPI] - Task GetCatalogAsync( + Task GetCatalog( CatalogParameters? parameters = null, - CancellationToken cancellationToken = default); + CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs b/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs index 05d14c1..c30026c 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs @@ -26,24 +26,24 @@ public interface IManifestOperations /// /// /// - /// + /// /// - Task DeleteManifestAsync( + Task DeleteManifest( string name, string reference, - CancellationToken cancellationToken = default); + CancellationToken token = default); /// /// Fetch the manifest identified by name and reference raw. /// /// /// - /// + /// /// - Task GetManifestRawAsync( + Task GetManifestRaw( string name, string reference, - CancellationToken cancellationToken); + CancellationToken token); /// /// Fetch the manifest identified by name and reference where reference can be a tag or digest. A HEAD request can also @@ -51,13 +51,13 @@ Task GetManifestRawAsync( /// /// /// - /// + /// /// [PublicAPI] - Task GetManifestAsync( + Task GetManifest( string name, string reference, - CancellationToken cancellationToken = default); + CancellationToken token = default); ///// ///// Returns true if the image exists, false otherwise. @@ -74,11 +74,11 @@ Task GetManifestAsync( /// /// /// - /// + /// /// - Task PutManifestAsync( + Task PutManifest( string name, string reference, ImageManifest manifest, - CancellationToken cancellationToken = default); + CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs b/src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs index d07dfb8..a74e11d 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs @@ -19,5 +19,5 @@ namespace Docker.Registry.DotNet.Endpoints; public interface ISystemOperations { [PublicAPI] - Task PingAsync(CancellationToken cancellationToken = default); + Task Ping(CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs b/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs index d21bd27..ae02540 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs @@ -19,8 +19,8 @@ namespace Docker.Registry.DotNet.Endpoints; public interface ITagOperations { [PublicAPI] - Task ListImageTagsAsync( + Task ListTags( string name, - ListImageTagsParameters? parameters = null, - CancellationToken cancellationToken = default); + ListTagsParameters? parameters = null, + CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs index 0144ee8..788a0ec 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs @@ -17,48 +17,47 @@ namespace Docker.Registry.DotNet.Endpoints.Implementations; internal class BlobOperations(NetworkClient client) : IBlobOperations { - public async Task GetBlobAsync( + public async Task GetBlob( string name, string digest, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - var url = $"v2/{name}/blobs/{digest}"; + var response = await client.MakeRequestForStreamedResponseAsync( + HttpMethod.Get, + $"{client.RegistryVersion}/{name}/blobs/{digest}", + token: token); - var response = await client.MakeRequestForStreamedResponseAsync( - cancellationToken, - HttpMethod.Get, - url); + return new GetBlobResponse( + response.Headers.GetString("Docker-Content-Digest"), + response.Body); + } - return new GetBlobResponse( - response.Headers.GetString("Docker-Content-Digest"), - response.Body); - } - - public Task DeleteBlobAsync( + public Task DeleteBlob( string name, string digest, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - var url = $"v2/{name}/blobs/{digest}"; - - return client.MakeRequestAsync(cancellationToken, HttpMethod.Delete, url); - } + return client.MakeRequest( + HttpMethod.Delete, + $"{client.RegistryVersion}/{name}/blobs/{digest}", + token: token); + } - public async Task IsExistBlobAsync( + public async Task BlobExists( string name, string digest, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - var path = $"v2/{name}/blobs/{digest}"; + var path = $"{client.RegistryVersion}/{name}/blobs/{digest}"; - var response = await client.MakeRequestNotErrorAsync( - cancellationToken, - HttpMethod.Head, - path); + var response = await client.MakeRequestNotErrorAsync( + HttpMethod.Head, + path, + token: token); - if (response.StatusCode != HttpStatusCode.NotFound) - client.HandleIfErrorResponse(response); + if (response.StatusCode != HttpStatusCode.NotFound) + client.HandleIfErrorResponse(response); - return response.StatusCode == HttpStatusCode.OK; - } + return response.StatusCode == HttpStatusCode.OK; + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs index 0648903..0069cbc 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs @@ -13,170 +13,159 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Diagnostics; + namespace Docker.Registry.DotNet.Endpoints.Implementations; -internal class BlobUploadOperations : IBlobUploadOperations +internal class BlobUploadOperations(NetworkClient client) : IBlobUploadOperations { - private readonly NetworkClient _client; - - internal BlobUploadOperations(NetworkClient client) - { - this._client = client; - } - - public async Task StartUploadBlobAsync( + public async Task StartUploadBlob( string name, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - var path = $"v2/{name}/blobs/uploads/"; - var response = await this._client.MakeRequestAsync( - cancellationToken, - HttpMethod.Post, - path); - return new ResumableUpload - { - DockerUploadUuid = response.Headers.GetString("Docker-Upload-UUID"), - Location = response.Headers.GetString("location"), - Range = response.Headers.GetString("Range") - }; - } - - public Task MonolithicUploadBlobAsync( + var path = $"{client.RegistryVersion}/{name}/blobs/uploads/"; + var response = await client.MakeRequest( + HttpMethod.Post, + path, + token: token); + + return new ResumableUpload + { + DockerUploadUuid = response.Headers.GetString("Docker-Upload-UUID"), + Location = response.Headers.GetString("location"), + Range = response.Headers.GetString("Range") + }; + } + + public Task MonolithicUploadBlob( ResumableUpload resumable, string digest, Stream stream, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - return CompleteBlobUploadAsync( - resumable, - digest, - stream, - cancellationToken: cancellationToken); - } - - public Task InitiateBlobUploadAsync( - string name, - Stream? stream = null, - CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public async Task MountBlobAsync( + return this.CompleteBlobUpload( + resumable, + digest, + stream, + token: token); + } + + public async Task MountBlob( string name, MountParameters parameters, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - var queryString = new QueryString(); - queryString.Add("mount", parameters.Digest); - queryString.Add("from", parameters.From); - - var response = await this._client.MakeRequestAsync( - cancellationToken, - HttpMethod.Post, - $"v2/{name}/blobs/uploads/", - queryString); - return new MountResponse - { - DockerUploadUuid = response.Headers.GetString("Docker-Upload-UUID"), - Location = response.Headers.GetString("location"), - Created = response.StatusCode == HttpStatusCode.Created, - }; - } + var queryString = new QueryString(); + queryString.Add("mount", parameters.Digest); + queryString.Add("from", parameters.From); + + var response = await client.MakeRequest( + HttpMethod.Post, + $"{client.RegistryVersion}/{name}/blobs/uploads/", + queryString, + token: token); + + return new MountResponse + { + DockerUploadUuid = response.Headers.GetString("Docker-Upload-UUID"), + Location = response.Headers.GetString("location"), + Created = response.StatusCode == HttpStatusCode.Created + }; + } public async Task GetBlobUploadStatus( string name, string uuid, CancellationToken cancellationToken = default) { - var response = await this._client.MakeRequestAsync( - cancellationToken, - HttpMethod.Get, - $"v2/{name}/blobs/uploads/{uuid}"); - - return new BlobUploadStatus - { - DockerUploadUuid = response.Headers.GetString("Docker-Upload-UUID"), - Range = response.Headers.GetString("Range") - }; - } - - public async Task UploadBlobChunkAsync( + var response = await client.MakeRequest( + HttpMethod.Get, + $"{name}/blobs/uploads/{uuid}", + token: cancellationToken); + + return new BlobUploadStatus + { + DockerUploadUuid = response.Headers.GetString("Docker-Upload-UUID"), + Range = response.Headers.GetString("Range") + }; + } + + public async Task UploadBlobChunk( ResumableUpload resumable, Stream chunk, long? from = null, long? to = null, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - var response = await this._client.MakeRequestAsync( - cancellationToken, - new HttpMethod("PATCH"), - resumable.Location, - content: () => - { - chunk.Position = 0; - var content = new StreamContent(chunk); - content.Headers.ContentLength = chunk.Length; - content.Headers.ContentType = - new MediaTypeHeaderValue("application/octet-stream"); - content.Headers.ContentRange = - new ContentRangeHeaderValue(from ?? 0, to ?? chunk.Length); - return content; - }).ConfigureAwait(false); - - return new ResumableUpload + var response = await client.MakeRequest( + new HttpMethod("PATCH"), + resumable.Location, + content: () => { - DockerUploadUuid = response.Headers.GetString("Docker-Upload-UUID"), - Location = response.Headers.GetString("location"), - Range = response.Headers.GetString("Range") - }; - } - - public async Task CompleteBlobUploadAsync( + chunk.Position = 0; + var content = new StreamContent(chunk); + content.Headers.ContentLength = chunk.Length; + content.Headers.ContentType = + new MediaTypeHeaderValue("application/octet-stream"); + content.Headers.ContentRange = + new ContentRangeHeaderValue(from ?? 0, to ?? chunk.Length); + return content; + }, + token: token); + + return new ResumableUpload + { + DockerUploadUuid = response.Headers.GetString("Docker-Upload-UUID"), + Location = response.Headers.GetString("location"), + Range = response.Headers.GetString("Range") + }; + } + + public async Task CompleteBlobUpload( ResumableUpload resumable, string digest, Stream? chunk = null, long? from = null, long? to = null, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - var queryString = new QueryString(); - queryString.Add("digest", digest); - - var response = await this._client.MakeRequestAsync( - cancellationToken, - HttpMethod.Put, - resumable.Location, - queryString, - content: () => - { - if (chunk is null) chunk = new MemoryStream(); - chunk.Position = 0; - var content = new StreamContent(chunk); - content.Headers.ContentLength = chunk.Length; - content.Headers.ContentType = - new MediaTypeHeaderValue("application/octet-stream"); - content.Headers.ContentRange = - new ContentRangeHeaderValue(from ?? 0, to ?? chunk.Length); - return content; - }).ConfigureAwait(false); - - return new CompletedUploadResponse + var queryString = new QueryString(); + queryString.Add("digest", digest); + + var response = await client.MakeRequest( + HttpMethod.Put, + resumable.Location, + queryString, + content: () => { - DockerContentDigest = response.Headers.GetString("Docker-Content-Digest"), - Location = response.Headers.GetString("location"), - }; - } - - public Task CancelBlobUploadAsync( + if (chunk is null) chunk = new MemoryStream(); + chunk.Position = 0; + var content = new StreamContent(chunk); + content.Headers.ContentLength = chunk.Length; + content.Headers.ContentType = + new MediaTypeHeaderValue("application/octet-stream"); + content.Headers.ContentRange = + new ContentRangeHeaderValue(from ?? 0, to ?? chunk.Length); + return content; + }, + token: token); + + return new CompletedUploadResponse + { + DockerContentDigest = response.Headers.GetString("Docker-Content-Digest"), + Location = response.Headers.GetString("location") + }; + } + + public Task CancelBlobUpload( string name, string uuid, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - var path = $"v2/{name}/blobs/uploads/{uuid}"; + var path = $"{client.RegistryVersion}/{name}/blobs/uploads/{uuid}"; - return this._client.MakeRequestAsync(cancellationToken, HttpMethod.Delete, path); - } + return client.MakeRequest(HttpMethod.Delete, path, token: token); + } /// /// Perform a monolithic upload. @@ -185,121 +174,75 @@ public Task CancelBlobUploadAsync( /// /// /// - /// + /// /// - public async Task UploadBlobAsync( + public async Task UploadBlob( string name, int contentLength, Stream stream, string digest, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - var path = $"v2/{name}/blobs/uploads/"; - - var response = await this._client.MakeRequestAsync( - cancellationToken, - HttpMethod.Post, - path); - - var uuid = response.Headers.GetString("Docker-Upload-UUID"); - - Console.WriteLine($"Uploading with uuid: {uuid}"); - - var location = response.Headers.GetString("Location"); + var path = $"{client.RegistryVersion}/{name}/blobs/uploads/"; - Console.WriteLine($"Using location: {location}"); + var response = await client.MakeRequest( + HttpMethod.Post, + path, + token: token); - //await GetBlobUploadStatus(name, uuid, cancellationToken); + var uuid = response.Headers.GetString("Docker-Upload-UUID"); - try - { - using (var client = new HttpClient()) - { - var progressResponse = await client.GetAsync(location, cancellationToken); - - //Send the contents of the whole file - var content = new StreamContent(stream); - - content.Headers.ContentLength = stream.Length; - content.Headers.ContentType = - new MediaTypeHeaderValue("application/octet-stream"); - content.Headers.ContentRange = new ContentRangeHeaderValue(0, stream.Length); - - var request = new HttpRequestMessage( - new HttpMethod("PATCH"), - location + $"&digest={digest}") - { - Content = content - }; - - var response2 = await client.SendAsync(request, cancellationToken); - - if (response2.StatusCode < HttpStatusCode.OK - || response2.StatusCode >= HttpStatusCode.BadRequest) - throw new RegistryApiException( - new RegistryApiResponse( - response2.StatusCode, - null, - response.Headers)); - - progressResponse = await client.GetAsync(location, cancellationToken); - } + Debug.WriteLine($"Uploading with uuid: {uuid}"); - ////{ + var location = response.Headers.GetString("Location"); - //// var queryString = new QueryString(); + Debug.WriteLine($"Using location: {location}"); - //// queryString.Add("digest", digest); + //await GetBlobUploadStatus(name, uuid, cancellationToken); - //// var content = new StreamContent(stream); + try + { + using var blobClient = new HttpClient(); - //// content.Headers.ContentLength = 0; - //// content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); - //// //content.Headers.ContentRange = new ContentRangeHeaderValue(0, stream.Length); + var progressResponse = await blobClient.GetAsync(location, token); - //// await _client.MakeRequestAsync(cancellationToken, HttpMethod.Put, $"v2/{name}/blobs/uploads/{uuid}", - //// queryString); - ////} + //Send the contents of the whole file + var content = new StreamContent(stream); - //using (var client = new HttpClient()) - //{ - // var content = new StringContent(""); + content.Headers.ContentLength = stream.Length; + content.Headers.ContentType = + new MediaTypeHeaderValue("application/octet-stream"); + content.Headers.ContentRange = new ContentRangeHeaderValue(0, stream.Length); - // content.Headers.ContentLength = 0; - // //content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); - - // var request = new HttpRequestMessage(HttpMethod.Put, new Uri($"http://10.0.4.44:5000/v2/{name}/blobs/uploads/{uuid}&digest={digest}")) - // { - // Content = content - // }; - - // var response2 = await client.SendAsync(request, cancellationToken); - - // //content.Headers.ContentRange = new ContentRangeHeaderValue(0, stream.Length); - - // if (response2.StatusCode < HttpStatusCode.OK || response2.StatusCode >= HttpStatusCode.BadRequest) - // { - // throw new RegistryApiException(new RegistryApiResponse(response2.StatusCode, null, response.Headers)); - // } - //} - } - catch (Exception ex) + var request = new HttpRequestMessage( + new HttpMethod("PATCH"), + $"{location}&digest={digest}") { - Console.WriteLine(ex.Message); - Console.WriteLine("Attempting to cancel the upload..."); + Content = content + }; - await this._client.MakeRequestAsync( - cancellationToken, - HttpMethod.Delete, - $"v2/{name}/blobs/uploads/{uuid}"); + var response2 = await blobClient.SendAsync(request, token); - throw; - } + if (response2.StatusCode is < HttpStatusCode.OK or >= HttpStatusCode.BadRequest) + throw new RegistryApiException( + new RegistryApiResponse( + response2.StatusCode, + null, + response.Headers)); - //string path2 = $"v2/{name}/blobs/uploads/{uuid}"; + progressResponse = await blobClient.GetAsync(location, token); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + Console.WriteLine("Attempting to cancel the upload..."); - //var response2 = await _client.MakeRequestAsync(cancellationToken, HttpMethod.Put, path2, queryString); + await client.MakeRequest( + HttpMethod.Delete, + $"{client.RegistryVersion}/{name}/blobs/uploads/{uuid}", + token: token); - //await _client.MakeRequestAsync(cancellationToken, HttpMethod.Put, location, queryString); + throw; } + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs index 11de1b8..f67fc3a 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs @@ -17,24 +17,22 @@ namespace Docker.Registry.DotNet.Endpoints.Implementations; internal class CatalogOperations(NetworkClient client) : ICatalogOperations { - private readonly NetworkClient _client = client ?? throw new ArgumentNullException(nameof(client)); - - public async Task GetCatalogAsync( + public async Task GetCatalog( CatalogParameters? parameters = null, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - parameters = parameters ?? new CatalogParameters(); + parameters ??= new CatalogParameters(); - var queryParameters = new QueryString(); + var queryParameters = new QueryString(); - queryParameters.AddFromObjectWithQueryParameters(parameters); + queryParameters.AddFromObject(parameters); - var response = await this._client.MakeRequestAsync( - cancellationToken, - HttpMethod.Get, - "v2/_catalog", - queryParameters).ConfigureAwait(false); + var response = await client.MakeRequest( + HttpMethod.Get, + $"{client.RegistryVersion}/_catalog", + queryParameters, + token: token); - return this._client.JsonSerializer.DeserializeObject(response.Body); - } + return client.JsonSerializer.DeserializeObject(response.Body); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs index f9e0bab..9104aa6 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs @@ -17,165 +17,160 @@ namespace Docker.Registry.DotNet.Endpoints.Implementations; internal class ManifestOperations(NetworkClient client) : IManifestOperations { - public async Task GetManifestAsync( + public async Task GetManifest( string name, string reference, - CancellationToken cancellationToken = default) + CancellationToken token = default) { - var headers = new Dictionary + var headers = new Dictionary + { { - { - "Accept", - $"{ManifestMediaTypes.ManifestSchema1}, {ManifestMediaTypes.ManifestSchema2}, {ManifestMediaTypes.ManifestList}, {ManifestMediaTypes.ManifestSchema1Signed}" - } - }; - - var response = await client.MakeRequestAsync( - cancellationToken, - HttpMethod.Head, - $"v2/{name}/manifests/{reference}", - null, - headers).ConfigureAwait(false); - - var digestReference = response.GetHeader("Docker-Content-Digest"); - - response = await client.MakeRequestAsync( - cancellationToken, - HttpMethod.Get, - $"v2/{name}/manifests/{digestReference}", - null, - headers).ConfigureAwait(false); - - var contentType = this.GetContentType(response.GetHeader("ContentType"), response.Body); - - switch (contentType) - { - case ManifestMediaTypes.ManifestSchema1: - case ManifestMediaTypes.ManifestSchema1Signed: - return new GetImageManifestResult( - contentType, - client.JsonSerializer.DeserializeObject( - response.Body), - response.Body) - { - DockerContentDigest = response.GetHeader("Docker-Content-Digest"), - Etag = response.GetHeader("Etag") - }; - - case ManifestMediaTypes.ManifestSchema2: - return new GetImageManifestResult( - contentType, - client.JsonSerializer.DeserializeObject( - response.Body), - response.Body) - { - DockerContentDigest = response.GetHeader("Docker-Content-Digest") - }; - - case ManifestMediaTypes.ManifestList: - return new GetImageManifestResult( - contentType, - client.JsonSerializer.DeserializeObject(response.Body), - response.Body); - - default: - throw new Exception($"Unexpected ContentType '{contentType}'."); + "Accept", + $"{ManifestMediaTypes.ManifestSchema1}, {ManifestMediaTypes.ManifestSchema2}, {ManifestMediaTypes.ManifestList}, {ManifestMediaTypes.ManifestSchema1Signed}" } + }; + + var response = await client.MakeRequest( + HttpMethod.Head, + $"{client.RegistryVersion}/{name}/manifests/{reference}", + null, + headers, + token: token); + + var digestReference = response.GetHeader("Docker-Content-Digest"); + + response = await client.MakeRequest( + HttpMethod.Get, + $"{client.RegistryVersion}/{name}/manifests/{digestReference}", + null, + headers, + token: token); + + var contentType = this.GetContentType(response.GetHeader("ContentType"), response.Body); + + switch (contentType) + { + case ManifestMediaTypes.ManifestSchema1: + case ManifestMediaTypes.ManifestSchema1Signed: + return new GetImageManifestResult( + contentType, + client.JsonSerializer.DeserializeObject( + response.Body), + response.Body) + { + DockerContentDigest = response.GetHeader("Docker-Content-Digest"), + Etag = response.GetHeader("Etag") + }; + + case ManifestMediaTypes.ManifestSchema2: + return new GetImageManifestResult( + contentType, + client.JsonSerializer.DeserializeObject( + response.Body), + response.Body) + { + DockerContentDigest = response.GetHeader("Docker-Content-Digest") + }; + + case ManifestMediaTypes.ManifestList: + return new GetImageManifestResult( + contentType, + client.JsonSerializer.DeserializeObject(response.Body), + response.Body); + + default: + throw new Exception($"Unexpected ContentType '{contentType}'."); } + } - public async Task PutManifestAsync( + public async Task PutManifest( string name, string reference, ImageManifest manifest, - CancellationToken cancellationToken) + CancellationToken token) { - string manifestMediaType = null; - if (manifest is ImageManifest2_1) - manifestMediaType = ManifestMediaTypes.ManifestSchema1; - if (manifest is ImageManifest2_2) - manifestMediaType = ManifestMediaTypes.ManifestSchema2; - if (manifest is ManifestList) - manifestMediaType = ManifestMediaTypes.ManifestList; - - var response = await client.MakeRequestAsync( - cancellationToken, - HttpMethod.Put, - $"v2/{name}/manifests/{reference}", - content: () => - { - var content = new StringContent( - client.JsonSerializer.SerializeObject(manifest)); - content.Headers.ContentType = - new MediaTypeHeaderValue(manifestMediaType); - return content; - }).ConfigureAwait(false); - - return new PushManifestResponse + string? manifestMediaType = null; + if (manifest is ImageManifest2_1) + manifestMediaType = ManifestMediaTypes.ManifestSchema1; + if (manifest is ImageManifest2_2) + manifestMediaType = ManifestMediaTypes.ManifestSchema2; + if (manifest is ManifestList) + manifestMediaType = ManifestMediaTypes.ManifestList; + + var response = await client.MakeRequest( + HttpMethod.Put, + $"{client.RegistryVersion}/{name}/manifests/{reference}", + content: () => { - DockerContentDigest = response.GetHeader("Docker-Content-Digest"), - ContentLength = response.GetHeader("Content-Length"), - Location = response.GetHeader("location"), - }; - } + var content = new StringContent( + client.JsonSerializer.SerializeObject(manifest)); + content.Headers.ContentType = + new MediaTypeHeaderValue(manifestMediaType); + return content; + }, + token: token); + + return new PushManifestResponse + { + DockerContentDigest = response.GetHeader("Docker-Content-Digest"), + ContentLength = response.GetHeader("Content-Length"), + Location = response.GetHeader("location") + }; + } + + public async Task DeleteManifest( + string name, + string reference, + CancellationToken token = default) + { + var path = $"{client.RegistryVersion}/{name}/manifests/{reference}"; - //public Task DoesManifestExistAsync(string name, string reference, CancellationToken cancellation = default) - //{ - // throw new NotImplementedException(); - //} + await client.MakeRequest(HttpMethod.Delete, path, token: token); + } - public async Task DeleteManifestAsync( + [PublicAPI] + public async Task GetManifestRaw( string name, string reference, - CancellationToken cancellationToken = default) + CancellationToken token) { - var path = $"v2/{name}/manifests/{reference}"; + var headers = new Dictionary + { + { + "Accept", + $"{ManifestMediaTypes.ManifestSchema1}, {ManifestMediaTypes.ManifestSchema2}, {ManifestMediaTypes.ManifestList}, {ManifestMediaTypes.ManifestSchema1Signed}" + } + }; - await client.MakeRequestAsync(cancellationToken, HttpMethod.Delete, path); - } + var response = await client.MakeRequest( + HttpMethod.Get, + $"{client.RegistryVersion}/{name}/manifests/{reference}", + null, + headers, + token: token); + + return response.Body; + } private string GetContentType(string contentTypeHeader, string manifest) { - if (!string.IsNullOrWhiteSpace(contentTypeHeader)) - return contentTypeHeader; - - var check = JsonConvert.DeserializeObject(manifest); + if (!string.IsNullOrWhiteSpace(contentTypeHeader)) + return contentTypeHeader; - if (!string.IsNullOrWhiteSpace(check.MediaType)) - return check.MediaType; + var check = JsonConvert.DeserializeObject(manifest); - if (check.SchemaVersion == null) - return ManifestMediaTypes.ManifestSchema1; + if (!string.IsNullOrWhiteSpace(check.MediaType)) + return check.MediaType; - if (check.SchemaVersion.Value == 2) - return ManifestMediaTypes.ManifestSchema2; + if (check.SchemaVersion == null) + return ManifestMediaTypes.ManifestSchema1; - throw new Exception( - $"Unable to determine schema type from version {check.SchemaVersion}"); - } + if (check.SchemaVersion.Value == 2) + return ManifestMediaTypes.ManifestSchema2; - [PublicAPI] - public async Task GetManifestRawAsync( - string name, - string reference, - CancellationToken cancellationToken) - { - var headers = new Dictionary - { - { - "Accept", - $"{ManifestMediaTypes.ManifestSchema1}, {ManifestMediaTypes.ManifestSchema2}, {ManifestMediaTypes.ManifestList}, {ManifestMediaTypes.ManifestSchema1Signed}" - } - }; - - var response = await client.MakeRequestAsync( - cancellationToken, - HttpMethod.Get, - $"v2/{name}/manifests/{reference}", - null, - headers).ConfigureAwait(false); - - return response.Body; - } + throw new Exception( + $"Unable to determine schema type from version {check.SchemaVersion}"); + } private class SchemaCheck { diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs index 23ea093..f3816af 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs @@ -17,8 +17,8 @@ namespace Docker.Registry.DotNet.Endpoints.Implementations; internal class SystemOperations(NetworkClient client) : ISystemOperations { - public Task PingAsync(CancellationToken cancellationToken = default) + public virtual Task Ping(CancellationToken token = default) { - return client.MakeRequestAsync(cancellationToken, HttpMethod.Get, "v2/"); - } + return client.MakeRequest(HttpMethod.Get, "", token: token); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs index 4634608..a636d04 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs @@ -17,31 +17,29 @@ namespace Docker.Registry.DotNet.Endpoints.Implementations; internal class TagOperations(NetworkClient client) : ITagOperations { - private readonly NetworkClient _client = client ?? throw new ArgumentNullException(nameof(client)); - - public async Task ListImageTagsAsync( + public async Task ListTags( string name, - ListImageTagsParameters? parameters = null, - CancellationToken cancellationToken = default) + ListTagsParameters? parameters = null, + CancellationToken token = default) { - if (string.IsNullOrEmpty(name)) - throw new ArgumentException( - $"'{nameof(name)}' cannot be null or empty", - nameof(name)); + if (string.IsNullOrEmpty(name)) + throw new ArgumentException( + $"'{nameof(name)}' cannot be null or empty", + nameof(name)); - parameters = parameters ?? new ListImageTagsParameters(); + parameters ??= new ListTagsParameters(); - var queryString = new QueryString(); + var queryString = new QueryString(); - queryString.AddFromObjectWithQueryParameters(parameters); + queryString.AddFromObject(parameters); - var response = await this._client.MakeRequestAsync( - cancellationToken, - HttpMethod.Get, - $"v2/{name}/tags/list", - queryString).ConfigureAwait(false); + var response = await client.MakeRequest( + HttpMethod.Get, + $"{client.RegistryVersion}/{name}/tags/list", + queryString, + token: token); - return this._client.JsonSerializer.DeserializeObject( - response.Body); - } + return client.JsonSerializer.DeserializeObject( + response.Body); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/HttpContentHelper.cs b/src/Docker.Registry.DotNet/Helpers/HttpContentHelper.cs new file mode 100644 index 0000000..d27aebd --- /dev/null +++ b/src/Docker.Registry.DotNet/Helpers/HttpContentHelper.cs @@ -0,0 +1,41 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Helpers; + +public static class HttpContentHelper +{ + public static Task ReadAsStringAsyncWithCancellation( + this HttpContent content, + CancellationToken token) + { +#if NET5_0_OR_GREATER + return content.ReadAsStringAsync(token); +#else + return content.ReadAsStringAsync(); +#endif + } + + public static Task ReadAsStreamAsyncWithCancellation( + this HttpContent content, + CancellationToken token) + { +#if NET5_0_OR_GREATER + return content.ReadAsStreamAsync(token); +#else + return content.ReadAsStreamAsync(); +#endif + } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs b/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs index f31bfa8..9eee56d 100644 --- a/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs +++ b/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs @@ -17,7 +17,7 @@ namespace Docker.Registry.DotNet.Helpers; internal static class HttpUtility { - internal static Uri BuildUri(this Uri baseUri, string path, IQueryString? queryString) + internal static Uri BuildUri(this Uri baseUri, string path, IReadOnlyQueryString? queryString) { if (baseUri == null) throw new ArgumentNullException(nameof(baseUri)); diff --git a/src/Docker.Registry.DotNet/Helpers/IQueryString.cs b/src/Docker.Registry.DotNet/Helpers/IReadOnlyQueryString.cs similarity index 94% rename from src/Docker.Registry.DotNet/Helpers/IQueryString.cs rename to src/Docker.Registry.DotNet/Helpers/IReadOnlyQueryString.cs index 8b16d97..559b294 100644 --- a/src/Docker.Registry.DotNet/Helpers/IQueryString.cs +++ b/src/Docker.Registry.DotNet/Helpers/IReadOnlyQueryString.cs @@ -15,7 +15,7 @@ namespace Docker.Registry.DotNet.Helpers; -internal interface IQueryString +internal interface IReadOnlyQueryString { string GetQueryString(); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/QueryString.cs b/src/Docker.Registry.DotNet/Helpers/QueryString.cs index 0fca59f..a8a1212 100644 --- a/src/Docker.Registry.DotNet/Helpers/QueryString.cs +++ b/src/Docker.Registry.DotNet/Helpers/QueryString.cs @@ -13,9 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Reflection; + namespace Docker.Registry.DotNet.Helpers; -internal class QueryString : IQueryString +internal class QueryString : IReadOnlyQueryString { private readonly Dictionary _values = new(); @@ -30,13 +32,38 @@ public string GetQueryString() v => $"{Uri.EscapeUriString(pair.Key)}={Uri.EscapeDataString(v)}")))); } - public void Add(string key, string value) + public void Add(string key, string? value) { - this._values.Add(key, [value]); + this._values.Add(key, [value ?? string.Empty]); } public void Add(string key, string[] values) { this._values.Add(key, values); } + + /// + /// Adds query parameters using reflection. Object must have [QueryParameter] attributes + /// on its properties for it to map properly. + /// + /// + /// + public void AddFromObject(T instance) + where T : class + { + if (instance == null) throw new ArgumentNullException(nameof(instance)); + + var propertyInfos = instance.GetType().GetProperties(); + + foreach (var p in propertyInfos) + { + var attribute = p.GetCustomAttribute(); + if (attribute != null) + { + // TODO: Maybe switch to FastMember to improve performance here or switch to static delegate generation + var value = p.GetValue(instance, null); + if (value != null) this.Add(attribute.Key, value.ToString()); + } + } + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs b/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs index bc2261a..a5d9182 100644 --- a/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs +++ b/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs @@ -19,53 +19,25 @@ namespace Docker.Registry.DotNet.Helpers; internal static class QueryStringExtensions { - /// - /// Adds query parameters using reflection. Object must have [QueryParameter] attributes - /// on it's properties for it to map properly. - /// - /// - /// - /// - internal static void AddFromObjectWithQueryParameters( - this QueryString queryString, - T instance) - where T : class - { - if (instance == null) throw new ArgumentNullException(nameof(instance)); - - var propertyInfos = instance.GetType().GetProperties(); - - foreach (var p in propertyInfos) - { - var attribute = p.GetCustomAttribute(); - if (attribute != null) - { - // TODO: Use a nuget like FastMember to improve performance here or switch to static delegate generation - var value = p.GetValue(instance, null); - if (value != null) queryString.Add(attribute.Key, value.ToString()); - } - } - } - /// /// Adds the value to the query string if it's not null. /// - /// + /// /// /// - internal static void AddIfNotNull(this QueryString queryString, string key, T? value) + internal static void AddIfNotNull(this QueryString readOnlyQueryString, string key, T? value) where T : struct { - if (value != null) queryString.Add(key, $"{value.Value}"); + if (value != null) readOnlyQueryString.Add(key, $"{value.Value}"); } /// /// - /// + /// /// /// - internal static void AddIfNotEmpty(this QueryString queryString, string key, string value) + internal static void AddIfNotEmpty(this QueryString readOnlyQueryString, string key, string value) { - if (!string.IsNullOrWhiteSpace(value)) queryString.Add(key, value); + if (!string.IsNullOrWhiteSpace(value)) readOnlyQueryString.Add(key, value); } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/Catalog.cs b/src/Docker.Registry.DotNet/Models/CatalogResponse.cs similarity index 88% rename from src/Docker.Registry.DotNet/Models/Catalog.cs rename to src/Docker.Registry.DotNet/Models/CatalogResponse.cs index 1837679..3eb6f23 100644 --- a/src/Docker.Registry.DotNet/Models/Catalog.cs +++ b/src/Docker.Registry.DotNet/Models/CatalogResponse.cs @@ -1,23 +1,23 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class Catalog -{ - [DataMember(Name = "repositories", EmitDefaultValue = false)] - public string[]? Repositories { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Models; + +[DataContract] +public class CatalogResponse +{ + [DataMember(Name = "repositories", EmitDefaultValue = false)] + public IReadOnlyCollection Repositories { get; set; } = []; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ListImageTagsParameters.cs b/src/Docker.Registry.DotNet/Models/ListTagsParameters.cs similarity index 93% rename from src/Docker.Registry.DotNet/Models/ListImageTagsParameters.cs rename to src/Docker.Registry.DotNet/Models/ListTagsParameters.cs index d07a425..de077d5 100644 --- a/src/Docker.Registry.DotNet/Models/ListImageTagsParameters.cs +++ b/src/Docker.Registry.DotNet/Models/ListTagsParameters.cs @@ -1,25 +1,25 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -public class ListImageTagsParameters -{ - /// - /// Limit the number of entries in each response. It not present, all entries will be returned - /// - [QueryParameter("n")] - public int? Number { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Models; + +public class ListTagsParameters +{ + /// + /// Limit the number of entries in each response. It not present, all entries will be returned + /// + [QueryParameter("n")] + public int? Number { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ListImageTagsResponse.cs b/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs similarity index 89% rename from src/Docker.Registry.DotNet/Models/ListImageTagsResponse.cs rename to src/Docker.Registry.DotNet/Models/ListTagsResponse.cs index 4565a24..ac8a8da 100644 --- a/src/Docker.Registry.DotNet/Models/ListImageTagsResponse.cs +++ b/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs @@ -1,26 +1,26 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class ListImageTagsResponse -{ - [DataMember(Name = "name")] - public string? Name { get; set; } - - [DataMember(Name = "tags")] - public string[]? Tags { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Models; + +[DataContract] +public class ListTagsResponse +{ + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "tags")] + public IReadOnlyCollection Tags { get; set; } = []; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs b/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs index 848038d..7173879 100644 --- a/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs +++ b/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs @@ -13,19 +13,21 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Diagnostics; + namespace Docker.Registry.DotNet.OAuth; internal class OAuthClient { private readonly HttpClient _client = new(); - private async Task GetTokenInnerAsync( + private async Task GetTokenInner( string realm, string service, string scope, string? username, string? password, - CancellationToken cancellationToken = default) + CancellationToken token = default) { HttpRequestMessage request; @@ -61,28 +63,31 @@ private async Task GetTokenInnerAsync( }; } - using var response = await this._client.SendAsync(request, cancellationToken); + using var response = await this._client.SendAsync(request, token); if (!response.IsSuccessStatusCode) - throw new UnauthorizedAccessException("Unable to authenticate."); + { + throw new UnauthorizedAccessException( + $"Unable to authenticate: {await response.Content.ReadAsStringAsyncWithCancellation(token)}"); + } - var body = await response.Content.ReadAsStringAsync(); + var body = await response.Content.ReadAsStringAsyncWithCancellation(token); - var token = JsonConvert.DeserializeObject(body); + var authToken = JsonConvert.DeserializeObject(body); - return token; + return authToken; } - public Task GetTokenAsync( + public Task GetToken( string realm, string service, string scope, CancellationToken cancellationToken = default) { - return this.GetTokenInnerAsync(realm, service, scope, null, null, cancellationToken); + return this.GetTokenInner(realm, service, scope, null, null, cancellationToken); } - public Task GetTokenAsync( + public Task GetToken( string realm, string service, string scope, @@ -90,7 +95,7 @@ public Task GetTokenAsync( string password, CancellationToken cancellationToken = default) { - return this.GetTokenInnerAsync( + return this.GetTokenInner( realm, service, scope, diff --git a/src/Docker.Registry.DotNet/Registry/IRegistryUriBuilder.cs b/src/Docker.Registry.DotNet/Registry/IRegistryUriBuilder.cs new file mode 100644 index 0000000..242091f --- /dev/null +++ b/src/Docker.Registry.DotNet/Registry/IRegistryUriBuilder.cs @@ -0,0 +1,32 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Registry; + +public interface IRegistryUriBuilder +{ + Uri Build(string? path = null, string? queryString = null); +} + +internal static class RegistryUriBuilderExtensions +{ + public static Uri Build( + this IRegistryUriBuilder uriBuilder, + string? path = null, + IReadOnlyQueryString? queryString = null) + { + return uriBuilder.Build(path, queryString?.GetQueryString()); + } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs b/src/Docker.Registry.DotNet/Registry/NetworkClient.cs index b92bc5a..731a672 100644 --- a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs +++ b/src/Docker.Registry.DotNet/Registry/NetworkClient.cs @@ -40,7 +40,7 @@ internal class NetworkClient : IDisposable } }; - private Uri? _effectiveEndpointBaseUri; + internal IRegistryUriBuilder? UriBuilder; public NetworkClient( RegistryClientConfiguration configuration, @@ -60,18 +60,17 @@ public NetworkClient( this.DefaultTimeout = configuration.DefaultTimeout; this.JsonSerializer = new JsonSerializer(); - - if (this._configuration.EndpointBaseUri != null) - this._effectiveEndpointBaseUri = this._configuration.EndpointBaseUri; } + public string RegistryVersion { get; } = "v2"; + public TimeSpan DefaultTimeout { get; set; } public JsonSerializer JsonSerializer { get; } public void Dispose() { - this._client?.Dispose(); + this._client.Dispose(); } /// @@ -80,7 +79,7 @@ public void Dispose() /// private async Task EnsureConnection() { - if (this._effectiveEndpointBaseUri != null) return; + if (this.UriBuilder != null) return; var tryUrls = new List(); @@ -103,8 +102,9 @@ private async Task EnsureConnection() foreach (var url in tryUrls) try { - await this.ProbeSingleAsync($"{url}/v2/"); - this._effectiveEndpointBaseUri = new Uri(url); + var registryUriBuilder = new RegistryUriBuilder(url); + await this.ProbeSingleEndpoint(registryUriBuilder); + this.UriBuilder = registryUriBuilder; return; } catch (Exception ex) @@ -113,32 +113,26 @@ private async Task EnsureConnection() } throw new RegistryConnectionException( - $"Unable to connect to any: {tryUrls.Select(s => $"'{s}/v2/'").ToDelimitedString(", ")}'", + $"Unable to connect to any: {tryUrls.Select(s => $"'{s}/{this.RegistryVersion}/'").ToDelimitedString(", ")}'", new AggregateException(exceptions)); } - private async Task ProbeSingleAsync(string uri) + private async Task ProbeSingleEndpoint(IRegistryUriBuilder uriBuilder) { - using var request = new HttpRequestMessage(HttpMethod.Get, uri); + using var request = new HttpRequestMessage(HttpMethod.Get, uriBuilder.Build()); using (await this._client.SendAsync(request)) { } } - internal async Task> MakeRequestAsync( - CancellationToken cancellationToken, + internal async Task> MakeRequest( HttpMethod method, string path, - IQueryString? queryString = null, + IReadOnlyQueryString? queryString = null, IDictionary? headers = null, - Func? content = null) + Func? content = null, + CancellationToken token = default) { - //Console.WriteLine( - // "Requesting Path: {0} Method: {1} QueryString: {2}", - // path, - // method, - // queryString?.GetQueryString()); - using var response = await this.InternalMakeRequestAsync( this.DefaultTimeout, HttpCompletionOption.ResponseContentRead, @@ -147,9 +141,9 @@ internal async Task> MakeRequestAsync( queryString, headers, content, - cancellationToken); + token); - var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var responseBody = await response.Content.ReadAsStringAsyncWithCancellation(token); var apiResponse = new RegistryApiResponse( response.StatusCode, @@ -162,19 +156,13 @@ internal async Task> MakeRequestAsync( } internal async Task> MakeRequestNotErrorAsync( - CancellationToken cancellationToken, HttpMethod method, string path, - IQueryString? queryString = null, + IReadOnlyQueryString? queryString = null, IDictionary? headers = null, - Func? content = null) + Func? content = null, + CancellationToken token = default) { - //Console.WriteLine( - // "Requesting Path: {0} Method: {1} QueryString: {2}", - // path, - // method, - // queryString?.GetQueryString()); - using var response = await this.InternalMakeRequestAsync( this.DefaultTimeout, HttpCompletionOption.ResponseContentRead, @@ -183,9 +171,10 @@ internal async Task> MakeRequestNotErrorAsync( queryString, headers, content, - cancellationToken); + token); - var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var responseBody = await response.Content.ReadAsStringAsyncWithCancellation(token) + .ConfigureAwait(false); var apiResponse = new RegistryApiResponse( response.StatusCode, @@ -196,10 +185,10 @@ internal async Task> MakeRequestNotErrorAsync( } internal async Task> MakeRequestForStreamedResponseAsync( - CancellationToken cancellationToken, HttpMethod method, string path, - IQueryString? queryString = null) + IReadOnlyQueryString? queryString = null, + CancellationToken token = default) { var response = await this.InternalMakeRequestAsync( InfiniteTimeout, @@ -209,9 +198,9 @@ internal async Task> MakeRequestForStreamedResponseA queryString, null, null, - cancellationToken); + token); - var body = await response.Content.ReadAsStreamAsync(); + var body = await response.Content.ReadAsStreamAsyncWithCancellation(token); var apiResponse = new RegistryApiResponse( response.StatusCode, @@ -228,23 +217,29 @@ private async Task InternalMakeRequestAsync( HttpCompletionOption completionOption, HttpMethod method, string path, - IQueryString? queryString, + IReadOnlyQueryString? queryString, IDictionary? headers, Func? content, CancellationToken cancellationToken) { await this.EnsureConnection(); - var request = this.PrepareRequest(method, path, queryString, headers, content); + if (this.UriBuilder == null) + throw new ArgumentNullException(nameof(this.UriBuilder), "Could not find URI builder"); + + var builtUri = this.UriBuilder.Build(path, queryString); + + var request = this.PrepareRequest(method, builtUri, headers, content); if (timeout != InfiniteTimeout) { - var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var timeoutTokenSource = + CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutTokenSource.CancelAfter(timeout); cancellationToken = timeoutTokenSource.Token; } - await this._authenticationProvider.AuthenticateAsync(request); + await this._authenticationProvider.Authenticate(request); var response = await this._client.SendAsync( request, @@ -254,10 +249,10 @@ private async Task InternalMakeRequestAsync( if (response.StatusCode != HttpStatusCode.Unauthorized) return response; //Prepare another request (we can't reuse the same request) - var request2 = this.PrepareRequest(method, path, queryString, headers, content); + var request2 = this.PrepareRequest(method, builtUri, headers, content); //Authenticate given the challenge - await this._authenticationProvider.AuthenticateAsync(request2, response); + await this._authenticationProvider.Authenticate(request2, response); //Send it again response = await this._client.SendAsync( @@ -290,16 +285,13 @@ internal void HandleIfErrorResponse(RegistryApiResponse response) internal HttpRequestMessage PrepareRequest( HttpMethod method, - string path, - IQueryString? queryString, + Uri uri, IDictionary? headers, Func? content) { - if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); - var request = new HttpRequestMessage( method, - this._effectiveEndpointBaseUri.BuildUri(path, queryString)); + uri); request.Headers.Add("User-Agent", UserAgent); request.Headers.AddRange(headers); diff --git a/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs b/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs index 770af4d..1f3fe39 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs +++ b/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs @@ -26,12 +26,12 @@ internal class RegistryApiResponse : RegistryApiResponse { internal RegistryApiResponse( HttpStatusCode statusCode, - TBody body, + TBody? body, HttpResponseHeaders headers) : base(statusCode, headers) { this.Body = body; } - public TBody Body { get; } + public TBody? Body { get; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/RegistryUriBuilder.cs b/src/Docker.Registry.DotNet/Registry/RegistryUriBuilder.cs new file mode 100644 index 0000000..8578937 --- /dev/null +++ b/src/Docker.Registry.DotNet/Registry/RegistryUriBuilder.cs @@ -0,0 +1,49 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Registry; + +public class RegistryUriBuilder(Uri baseUri) : IRegistryUriBuilder +{ + public RegistryUriBuilder(string baseUri) + : this(new Uri(baseUri)) + { + } + + public virtual Uri Build(string? path = null, string? queryString = null) + { + var pathIsUri = false; + + path = path?.Trim() ?? string.Empty; + + if (Uri.TryCreate(path, UriKind.Absolute, out var uri)) pathIsUri = true; + else + // not absolute -- use the base uri + uri = baseUri; + + var builder = new UriBuilder(uri); + + if (!pathIsUri && !string.IsNullOrEmpty(path)) + builder.Path = path; + + if (!string.IsNullOrWhiteSpace(queryString)) + { + if (string.IsNullOrWhiteSpace(builder.Query)) builder.Query = queryString!.Trim(); + else builder.Query += $"&{queryString!.Trim()}"; + } + + return builder.Uri; + } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs index 6b63b72..bdf4bd9 100644 --- a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs +++ b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs @@ -50,21 +50,6 @@ public RegistryClientConfiguration( this.HttpMessageHandler = httpMessageHandler; } - /// - /// Obsolete constructor that allows a uri to be used to specify a registry. - /// - /// - /// - [Obsolete("Use the constructor that allows you to specify a host.")] - public RegistryClientConfiguration(Uri endpoint, TimeSpan defaultTimeout = default) - : this(defaultTimeout) - { - if (endpoint == null) - throw new ArgumentNullException(nameof(endpoint)); - - this.EndpointBaseUri = endpoint; - } - private RegistryClientConfiguration(TimeSpan defaultTimeout) { if (defaultTimeout != TimeSpan.Zero) @@ -79,8 +64,6 @@ private RegistryClientConfiguration(TimeSpan defaultTimeout) } } - public Uri? EndpointBaseUri { get; } - public string Host { get; } public HttpMessageHandler? HttpMessageHandler { get; } From 9e69d11b505f86f0ba5bf18ce6610e5d9566f7d8 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Fri, 9 Aug 2024 02:37:10 -0400 Subject: [PATCH 04/17] Supports Docker Hub endpoint: Repository. --- .../AnonymousOAuthAuthenticationProvider.cs | 3 +- .../Authentication/AuthenticationProvider.cs | 4 +- .../BasicAuthenticationProvider.cs | 3 +- .../DockerHubJwtAuthenticationProvider.cs | 73 ++++++++ .../PasswordOAuthAuthenticationProvider.cs | 60 +++--- .../Implementations/RepositoryOperations.cs | 171 ++++++++++++++++++ .../Helpers/HttpUtility.cs | 2 +- .../Helpers/JsonSerializer.cs | 13 +- .../Helpers/QueryString.cs | 4 +- .../Helpers/QueryStringExtensions.cs | 2 +- .../OAuth/OAuthClient.cs | 4 +- .../Registry/IRegistryClient.cs | 4 + .../Registry/NetworkClient.cs | 6 +- .../Registry/RegistryClient.cs | 3 + 14 files changed, 306 insertions(+), 46 deletions(-) create mode 100644 src/Docker.Registry.DotNet/Authentication/DockerHubJwtAuthenticationProvider.cs create mode 100644 src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs diff --git a/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs index 4c44e34..3087881 100644 --- a/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs @@ -29,7 +29,8 @@ public override Task Authenticate(HttpRequestMessage request) public override async Task Authenticate( HttpRequestMessage request, - HttpResponseMessage response) + HttpResponseMessage response, + IRegistryUriBuilder uriBuilder) { var header = this.TryGetSchemaHeader(response, Schema); diff --git a/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs index 91a4c15..4fa7151 100644 --- a/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs @@ -32,10 +32,12 @@ public abstract class AuthenticationProvider /// /// /// + /// /// public abstract Task Authenticate( HttpRequestMessage request, - HttpResponseMessage response); + HttpResponseMessage response, + IRegistryUriBuilder builder); /// /// Gets the schema header from the http response. diff --git a/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs index 2a8d77a..a694b6d 100644 --- a/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs @@ -27,7 +27,8 @@ public override Task Authenticate(HttpRequestMessage request) public override Task Authenticate( HttpRequestMessage request, - HttpResponseMessage response) + HttpResponseMessage response, + IRegistryUriBuilder uriBuilder) { this.TryGetSchemaHeader(response, Schema); diff --git a/src/Docker.Registry.DotNet/Authentication/DockerHubJwtAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/DockerHubJwtAuthenticationProvider.cs new file mode 100644 index 0000000..5882ed1 --- /dev/null +++ b/src/Docker.Registry.DotNet/Authentication/DockerHubJwtAuthenticationProvider.cs @@ -0,0 +1,73 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Authentication; + +public class DockerHubJwtAuthenticationProvider(string username, string password) + : AuthenticationProvider +{ + private static readonly HttpClient _client = new(); + + private static string Schema { get; } = "Bearer"; + + public override Task Authenticate(HttpRequestMessage request) + { + return Task.CompletedTask; + } + + private async Task PostAuth( + IRegistryUriBuilder uriBuilder, + CancellationToken token = default) + { + var request = new HttpRequestMessage(HttpMethod.Post, uriBuilder.Build("/v2/users/login")) + { + Content = new StringContent( + JsonConvert.SerializeObject( + new Dictionary + { + { "username", username }, + { "password", password } + }) + ) + }; + + using var response = await _client.SendAsync(request, token); + + if (!response.IsSuccessStatusCode) + throw new UnauthorizedAccessException( + $"Unable to authenticate: {await response.Content.ReadAsStringAsyncWithCancellation(token)}"); + + var body = await response.Content.ReadAsStringAsyncWithCancellation(token); + + var authToken = JsonConvert.DeserializeObject(body); + + return authToken; + } + + public override async Task Authenticate( + HttpRequestMessage request, + HttpResponseMessage response, + IRegistryUriBuilder uriBuilder) + { + var tokenResponse = await this.PostAuth(uriBuilder); + + if (tokenResponse?.Token == null) + throw new UnauthorizedAccessException("Failed to authenticate. Token was empty."); + + //Set the header + request.Headers.Authorization = + new AuthenticationHeaderValue(Schema, tokenResponse.Token); + } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs index 849697a..9059a22 100644 --- a/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs @@ -19,43 +19,43 @@ namespace Docker.Registry.DotNet.Authentication; public class PasswordOAuthAuthenticationProvider(string username, string password) : AuthenticationProvider { - private readonly OAuthClient _client = new OAuthClient(); + private readonly OAuthClient _client = new(); private static string Schema { get; } = "Bearer"; public override Task Authenticate(HttpRequestMessage request) { - return Task.CompletedTask; - } + return Task.CompletedTask; + } public override async Task Authenticate( HttpRequestMessage request, - HttpResponseMessage response) + HttpResponseMessage response, + IRegistryUriBuilder uriBuilder) { - var header = this.TryGetSchemaHeader(response, Schema); - - //Get the bearer bits - var bearerBits = AuthenticateParser.ParseTyped(header.Parameter); - - string? scope = null; - - if (!string.IsNullOrWhiteSpace(bearerBits.Scope)) - { - //Also include the repository(plugin) resource type to be able to access plugin repositories. - //See https://docs.docker.com/registry/spec/auth/scope/ - scope = $"{bearerBits.Scope} {bearerBits.Scope?.Replace("repository:", "repository(plugin):")}"; - } - - //Get the token - var token = await this._client.GetToken( - bearerBits.Realm, - bearerBits.Service, - scope, - username, - password); - - //Set the header - request.Headers.Authorization = - new AuthenticationHeaderValue(Schema, token.AccessToken); - } + var header = this.TryGetSchemaHeader(response, Schema); + + //Get the bearer bits + var bearerBits = AuthenticateParser.ParseTyped(header.Parameter); + + string? scope = null; + + if (!string.IsNullOrWhiteSpace(bearerBits.Scope)) + //Also include the repository(plugin) resource type to be able to access plugin repositories. + //See https://docs.docker.com/registry/spec/auth/scope/ + scope = + $"{bearerBits.Scope} {bearerBits.Scope?.Replace("repository:", "repository(plugin):")}"; + + //Get the token + var token = await this._client.GetToken( + bearerBits.Realm, + bearerBits.Service, + scope, + username, + password); + + //Set the header + request.Headers.Authorization = + new AuthenticationHeaderValue(Schema, token.AccessToken); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs new file mode 100644 index 0000000..e7f8f49 --- /dev/null +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs @@ -0,0 +1,171 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Newtonsoft.Json.Converters; + +namespace Docker.Registry.DotNet.Endpoints.Implementations; + +public interface IRepositoryOperations +{ + Task ListRepositoryTags( + string @namespace, + string repository, + RepositoryTagsParameters? parameters = null, + CancellationToken token = default); +} + +internal class RepositoryOperations(NetworkClient client) : IRepositoryOperations +{ + public async Task ListRepositoryTags( + string @namespace, + string repository, + RepositoryTagsParameters? parameters = null, + CancellationToken token = default) + { + var queryString = new QueryString(); + queryString.AddFromObject(parameters ?? new RepositoryTagsParameters()); + + var response = await client.MakeRequest( + HttpMethod.Get, + $"{client.RegistryVersion}/namespaces/{@namespace}/repositories/{repository}/tags", + queryString, + token: token); + + return client.JsonSerializer.DeserializeObject(response.Body); + } +} + +[PublicAPI] +public class RepositoryTagsParameters +{ + /// + /// Current page. + /// + [QueryParameter("page")] + public int Page { get; set; } = 1; + + /// + /// Page Size -- max is 100 + /// + [QueryParameter("page_size")] + public int PageSize { get; set; } = 10; +} + +public class ListRepositoryTagsResponse +{ + [JsonProperty("count")] + public int Count { get; set; } + + [JsonProperty("next")] + public string Next { get; set; } + + [JsonProperty("previous")] + public object Previous { get; set; } + + [JsonProperty("results")] + public List Tags { get; set; } +} + +public class RepositoryTag +{ + [JsonProperty("creator")] + public int Creator { get; set; } + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("images")] + public List Images { get; set; } + + [JsonProperty("last_updated")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime LastUpdated { get; set; } + + [JsonProperty("last_updater")] + public int LastUpdater { get; set; } + + [JsonProperty("last_updater_username")] + public string LastUpdaterUsername { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("repository")] + public int Repository { get; set; } + + [JsonProperty("full_size")] + public long FullSize { get; set; } + + [JsonProperty("v2")] + public bool V2 { get; set; } + + [JsonProperty("tag_status")] + public string TagStatus { get; set; } + + [JsonProperty("tag_last_pulled")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime TagLastPulled { get; set; } + + [JsonProperty("tag_last_pushed")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime TagLastPushed { get; set; } + + [JsonProperty("media_type")] + public string MediaType { get; set; } + + [JsonProperty("content_type")] + public string ContentType { get; set; } + + [JsonProperty("digest")] + public string Digest { get; set; } +} + +public class TagImage +{ + [JsonProperty("architecture")] + public string Architecture { get; set; } + + [JsonProperty("features")] + public string Features { get; set; } + + [JsonProperty("variant")] + public object Variant { get; set; } + + [JsonProperty("digest")] + public string Digest { get; set; } + + [JsonProperty("os")] + public string Os { get; set; } + + [JsonProperty("os_features")] + public string OsFeatures { get; set; } + + [JsonProperty("os_version")] + public object OsVersion { get; set; } + + [JsonProperty("size")] + public long Size { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("last_pulled")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime LastPulled { get; set; } + + [JsonProperty("last_pushed")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime LastPushed { get; set; } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs b/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs index 9eee56d..c93ea70 100644 --- a/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs +++ b/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs @@ -82,7 +82,7 @@ public static void AddRange( foreach (var item in headers.IfNullEmpty()) header.Add(item.Key, item.Value); } - public static AuthenticationHeaderValue GetHeaderBySchema( + public static AuthenticationHeaderValue? GetHeaderBySchema( this HttpResponseMessage response, string schema) { diff --git a/src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs b/src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs index efd416e..54ec724 100644 --- a/src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs +++ b/src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs @@ -22,14 +22,15 @@ namespace Docker.Registry.DotNet.Helpers; /// internal class JsonSerializer { - private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + private static readonly JsonSerializerSettings Settings = new() { NullValueHandling = NullValueHandling.Ignore, Converters = { //new JsonIso8601AndUnixEpochDateConverter(), //new JsonVersionConverter(), - new StringEnumConverter() + new StringEnumConverter(), + new IsoDateTimeConverter() //new TimeSpanSecondsConverter(), //new TimeSpanNanosecondsConverter() } @@ -37,11 +38,11 @@ internal class JsonSerializer public T DeserializeObject(string json) { - return JsonConvert.DeserializeObject(json, Settings); - } + return JsonConvert.DeserializeObject(json, Settings); + } public string SerializeObject(T value) { - return JsonConvert.SerializeObject(value, Settings); - } + return JsonConvert.SerializeObject(value, Settings); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/QueryString.cs b/src/Docker.Registry.DotNet/Helpers/QueryString.cs index a8a1212..6f86a8d 100644 --- a/src/Docker.Registry.DotNet/Helpers/QueryString.cs +++ b/src/Docker.Registry.DotNet/Helpers/QueryString.cs @@ -48,10 +48,10 @@ public void Add(string key, string[] values) /// /// /// - public void AddFromObject(T instance) + public void AddFromObject(T? instance) where T : class { - if (instance == null) throw new ArgumentNullException(nameof(instance)); + if (instance == null) return; var propertyInfos = instance.GetType().GetProperties(); diff --git a/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs b/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs index a5d9182..818f3aa 100644 --- a/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs +++ b/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs @@ -36,7 +36,7 @@ internal static void AddIfNotNull(this QueryString readOnlyQueryString, strin /// /// /// - internal static void AddIfNotEmpty(this QueryString readOnlyQueryString, string key, string value) + internal static void AddIfNotEmpty(this QueryString readOnlyQueryString, string key, string? value) { if (!string.IsNullOrWhiteSpace(value)) readOnlyQueryString.Add(key, value); } diff --git a/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs b/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs index 7173879..6740232 100644 --- a/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs +++ b/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs @@ -23,8 +23,8 @@ internal class OAuthClient private async Task GetTokenInner( string realm, - string service, - string scope, + string? service, + string? scope, string? username, string? password, CancellationToken token = default) diff --git a/src/Docker.Registry.DotNet/Registry/IRegistryClient.cs b/src/Docker.Registry.DotNet/Registry/IRegistryClient.cs index 6ef209a..22f2b24 100644 --- a/src/Docker.Registry.DotNet/Registry/IRegistryClient.cs +++ b/src/Docker.Registry.DotNet/Registry/IRegistryClient.cs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Docker.Registry.DotNet.Endpoints.Implementations; + namespace Docker.Registry.DotNet.Registry; /// @@ -55,4 +57,6 @@ public interface IRegistryClient : IDisposable /// [PublicAPI] ISystemOperations System { get; } + + IRepositoryOperations Repository { get; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs b/src/Docker.Registry.DotNet/Registry/NetworkClient.cs index 731a672..820d752 100644 --- a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs +++ b/src/Docker.Registry.DotNet/Registry/NetworkClient.cs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Diagnostics; + using JsonSerializer = Docker.Registry.DotNet.Helpers.JsonSerializer; namespace Docker.Registry.DotNet.Registry; @@ -229,6 +231,8 @@ private async Task InternalMakeRequestAsync( var builtUri = this.UriBuilder.Build(path, queryString); + Debug.WriteLine("Built URI: " + builtUri); + var request = this.PrepareRequest(method, builtUri, headers, content); if (timeout != InfiniteTimeout) @@ -252,7 +256,7 @@ private async Task InternalMakeRequestAsync( var request2 = this.PrepareRequest(method, builtUri, headers, content); //Authenticate given the challenge - await this._authenticationProvider.Authenticate(request2, response); + await this._authenticationProvider.Authenticate(request2, response, this.UriBuilder); //Send it again response = await this._client.SendAsync( diff --git a/src/Docker.Registry.DotNet/Registry/RegistryClient.cs b/src/Docker.Registry.DotNet/Registry/RegistryClient.cs index f7628bb..717464e 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryClient.cs +++ b/src/Docker.Registry.DotNet/Registry/RegistryClient.cs @@ -38,8 +38,11 @@ public RegistryClient( this.BlobUploads = new BlobUploadOperations(_client); this.System = new SystemOperations(_client); this.Tags = new TagOperations(_client); + this.Repository = new RepositoryOperations(this._client); } + public IRepositoryOperations Repository { get; set; } + public IBlobUploadOperations BlobUploads { get; } public IManifestOperations Manifest { get; } From b1575c2072d9db6c0c0a3ddc3540ef875ba507e4 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Fri, 9 Aug 2024 15:25:58 -0400 Subject: [PATCH 05/17] Added concept of ImageReference. --- .../Domain/Tags/ImageReference.cs | 147 ++++++++++++++++++ .../Endpoints/IManifestOperations.cs | 32 ++-- .../Endpoints/ITagOperations.cs | 2 +- .../Implementations/ManifestOperations.cs | 118 +++++++------- .../Implementations/TagOperations.cs | 19 ++- .../Helpers/CharHelpers.cs | 26 ++++ .../Helpers/CollectionExtensions.cs | 26 ++++ .../Helpers/StringExtensions.cs | 9 ++ .../Models/ListTagsResponse.cs | 9 +- .../Registry/NetworkClient.cs | 8 +- .../Registry/RegistryApiResponse.cs | 7 +- .../Authentication/AuthenticateParserTests.cs | 67 ++++++-- .../Docker.Registry.DotNet.Tests.csproj | 9 +- .../ImageReferenceTests.cs | 96 ++++++++++++ 14 files changed, 453 insertions(+), 122 deletions(-) create mode 100644 src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs create mode 100644 src/Docker.Registry.DotNet/Helpers/CharHelpers.cs create mode 100644 src/Docker.Registry.DotNet/Helpers/CollectionExtensions.cs create mode 100644 test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs diff --git a/src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs b/src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs new file mode 100644 index 0000000..550b377 --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs @@ -0,0 +1,147 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace Docker.Registry.DotNet.Domain.Tags; + +public record ImageReference +{ + public ImageReference(string value) + { + var parsedTag = value?.Trim() ?? string.Empty; + + if (IsSha256Digest(parsedTag.ToLower())) + { + // save as all lower + this.Value = parsedTag.ToLower(); + } + else + { + var errors = ValidateTag(parsedTag).ToList(); + + if (errors.Any()) + { + throw new ArgumentException( + $"Invalid Image Reference: {errors.ToDelimitedString(", ")}", + nameof(value)); + } + + this.Value = parsedTag; + } + } + + public static ImageReference Latest { get; } = new("latest"); + + public string Value { get; init; } + + public bool IsDigest => IsSha256Digest(Value); + + public bool IsTag => !this.IsDigest; + + public void Deconstruct(out string value) + { + value = this.Value; + } + + public static ImageReference Create(string tag) + { + return new ImageReference(tag); + } + + public static bool TryCreate(string reference, [NotNullWhen(true)] out ImageReference? imageTag) + { + reference = reference?.Trim() ?? string.Empty; + + var errors = ValidateTag(reference).ToList(); + + imageTag = null; + + if (errors.Any()) + { + return false; + } + + imageTag = new ImageReference(reference); + + return true; + } + + static bool IsSha256Digest(string value) + { + const string DigestPrefix = "sha256:"; + + if (value?.StartsWith(DigestPrefix) ?? false) + { + // sha256 hash is always 64 characters + string hash = value.TakeAfter(DigestPrefix.Length) ?? string.Empty; + + if (hash.Length == 64 && hash.All(c => c.IsHexLowerCase())) + { + return true; + } + } + + return false; + } + + private static IEnumerable ValidateTag(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + yield return "Value must not be null or empty"; + + yield break; + } + + if (value.Length > 128) + { + yield return $"Value is too large. Value is {value.Length + } characters and maximum tag size is 128 characters."; + + yield break; + } + + if (IsSha256Digest(value.ToLower())) + { + yield break; + } + + var validChars = new[] { '-', '_', '.' }; + + var invalidCharacters = +#if NET7_0_OR_GREATER + value.Where(c => !char.IsAsciiLetterOrDigit(c) && !validChars.Contains(c)).ToArray(); +#else + value.Where(c => !char.IsLetterOrDigit(c) && !validChars.Contains(c)).ToArray(); +#endif + + if (invalidCharacters.Any()) + { + yield return @$"Value ""{value}"" is invalid characters: ""{ + invalidCharacters.Select(s => $"{s}").ToDelimitedString(",") + }"". Image References can only contain lowercase and uppercase letters, digits, underscores, periods, and hyphens."; + + yield break; + } + + if (value.StartsWith(".") || value.StartsWith("-")) + { + yield return @$"Value ""{value}"" is invalid. Image References can't start with a period or hyphen."; + } + } + + public override string ToString() => this.Value; +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs b/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs index c30026c..f81b107 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs @@ -1,4 +1,4 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman // and Docker.Registry.DotNet Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Docker.Registry.DotNet.Domain.Tags; + namespace Docker.Registry.DotNet.Endpoints; /// @@ -28,10 +30,7 @@ public interface IManifestOperations /// /// /// - Task DeleteManifest( - string name, - string reference, - CancellationToken token = default); + Task DeleteManifest(string name, ImageReference reference, CancellationToken token = default); /// /// Fetch the manifest identified by name and reference raw. @@ -40,10 +39,9 @@ Task DeleteManifest( /// /// /// - Task GetManifestRaw( - string name, - string reference, - CancellationToken token); + Task GetManifestRaw(string name, ImageReference reference, CancellationToken token = default); + + Task GetDigest(string name, ImageReference reference, CancellationToken token = default); /// /// Fetch the manifest identified by name and reference where reference can be a tag or digest. A HEAD request can also @@ -54,19 +52,7 @@ Task GetManifestRaw( /// /// [PublicAPI] - Task GetManifest( - string name, - string reference, - CancellationToken token = default); - - ///// - ///// Returns true if the image exists, false otherwise. - ///// - ///// - ///// - ///// - ///// - //Task DoesManifestExistAsync(string name, string reference, CancellationToken cancellation = default); + Task GetManifest(string name, ImageReference reference, CancellationToken token = default); /// /// Put the manifest identified by name and reference where reference can be a tag or digest. @@ -78,7 +64,7 @@ Task GetManifest( /// Task PutManifest( string name, - string reference, + ImageReference reference, ImageManifest manifest, CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs b/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs index ae02540..1e4e963 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs @@ -19,7 +19,7 @@ namespace Docker.Registry.DotNet.Endpoints; public interface ITagOperations { [PublicAPI] - Task ListTags( + Task ListTags( string name, ListTagsParameters? parameters = null, CancellationToken token = default); diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs index 9104aa6..45fc520 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs @@ -1,4 +1,4 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman // and Docker.Registry.DotNet Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,38 +13,36 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Docker.Registry.DotNet.Domain.Tags; + namespace Docker.Registry.DotNet.Endpoints.Implementations; internal class ManifestOperations(NetworkClient client) : IManifestOperations { + static readonly IReadOnlyDictionary _manifestHeaders = new Dictionary + { + { + "Accept", + $"{ManifestMediaTypes.ManifestSchema1}, {ManifestMediaTypes.ManifestSchema2}, { + ManifestMediaTypes.ManifestList}, {ManifestMediaTypes.ManifestSchema1Signed}" + } + }; + public async Task GetManifest( string name, - string reference, + ImageReference reference, CancellationToken token = default) { - var headers = new Dictionary - { - { - "Accept", - $"{ManifestMediaTypes.ManifestSchema1}, {ManifestMediaTypes.ManifestSchema2}, {ManifestMediaTypes.ManifestList}, {ManifestMediaTypes.ManifestSchema1Signed}" - } - }; + var digestReference = await this.GetDigest(name, reference, token); - var response = await client.MakeRequest( - HttpMethod.Head, - $"{client.RegistryVersion}/{name}/manifests/{reference}", - null, - headers, - token: token); - - var digestReference = response.GetHeader("Docker-Content-Digest"); + if (digestReference == null) + { + throw new ArgumentNullException( + nameof(digestReference), + @$"Failed getting the digest reference for ""{reference}"""); + } - response = await client.MakeRequest( - HttpMethod.Get, - $"{client.RegistryVersion}/{name}/manifests/{digestReference}", - null, - headers, - token: token); + var response = await this.MakeManifestRequest(name, digestReference, token); var contentType = this.GetContentType(response.GetHeader("ContentType"), response.Body); @@ -54,23 +52,17 @@ public async Task GetManifest( case ManifestMediaTypes.ManifestSchema1Signed: return new GetImageManifestResult( contentType, - client.JsonSerializer.DeserializeObject( - response.Body), + client.JsonSerializer.DeserializeObject(response.Body), response.Body) { - DockerContentDigest = response.GetHeader("Docker-Content-Digest"), - Etag = response.GetHeader("Etag") + DockerContentDigest = response.GetHeader("Docker-Content-Digest"), Etag = response.GetHeader("Etag") }; case ManifestMediaTypes.ManifestSchema2: return new GetImageManifestResult( contentType, - client.JsonSerializer.DeserializeObject( - response.Body), - response.Body) - { - DockerContentDigest = response.GetHeader("Docker-Content-Digest") - }; + client.JsonSerializer.DeserializeObject(response.Body), + response.Body) { DockerContentDigest = response.GetHeader("Docker-Content-Digest") }; case ManifestMediaTypes.ManifestList: return new GetImageManifestResult( @@ -85,7 +77,7 @@ public async Task GetManifest( public async Task PutManifest( string name, - string reference, + ImageReference reference, ImageManifest manifest, CancellationToken token) { @@ -102,10 +94,8 @@ public async Task PutManifest( $"{client.RegistryVersion}/{name}/manifests/{reference}", content: () => { - var content = new StringContent( - client.JsonSerializer.SerializeObject(manifest)); - content.Headers.ContentType = - new MediaTypeHeaderValue(manifestMediaType); + var content = new StringContent(client.JsonSerializer.SerializeObject(manifest)); + content.Headers.ContentType = new MediaTypeHeaderValue(manifestMediaType); return content; }, token: token); @@ -118,10 +108,7 @@ public async Task PutManifest( }; } - public async Task DeleteManifest( - string name, - string reference, - CancellationToken token = default) + public async Task DeleteManifest(string name, ImageReference reference, CancellationToken token = default) { var path = $"{client.RegistryVersion}/{name}/manifests/{reference}"; @@ -129,27 +116,43 @@ public async Task DeleteManifest( } [PublicAPI] - public async Task GetManifestRaw( - string name, - string reference, - CancellationToken token) + public async Task GetManifestRaw(string name, ImageReference reference, CancellationToken token) + { + var response = await MakeManifestRequest(name, reference, token); + + return response.Body; + } + + public async Task GetDigest(string name, ImageReference reference, CancellationToken token = default) { - var headers = new Dictionary + if (!reference.IsTag) { - { - "Accept", - $"{ManifestMediaTypes.ManifestSchema1}, {ManifestMediaTypes.ManifestSchema2}, {ManifestMediaTypes.ManifestList}, {ManifestMediaTypes.ManifestSchema1Signed}" - } - }; + throw new ArgumentOutOfRangeException(nameof(reference), $@"Reference must be a tag to get the digest: ""{reference}"""); + } - var response = await client.MakeRequest( + var response = await MakeManifestRequest(name, reference, token); + + var digestValue = response.GetHeader("Docker-Content-Digest"); + + if (ImageReference.TryCreate(digestValue, out var digest)) + { + return digest; + } + + return null; + } + + private async Task> MakeManifestRequest( + string name, + ImageReference reference, + CancellationToken token) + { + return await client.MakeRequest( HttpMethod.Get, $"{client.RegistryVersion}/{name}/manifests/{reference}", null, - headers, + _manifestHeaders, token: token); - - return response.Body; } private string GetContentType(string contentTypeHeader, string manifest) @@ -168,8 +171,7 @@ private string GetContentType(string contentTypeHeader, string manifest) if (check.SchemaVersion.Value == 2) return ManifestMediaTypes.ManifestSchema2; - throw new Exception( - $"Unable to determine schema type from version {check.SchemaVersion}"); + throw new Exception($"Unable to determine schema type from version {check.SchemaVersion}"); } private class SchemaCheck diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs index a636d04..9d80ce6 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs @@ -1,4 +1,4 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman // and Docker.Registry.DotNet Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,19 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Docker.Registry.DotNet.Domain.Tags; + namespace Docker.Registry.DotNet.Endpoints.Implementations; internal class TagOperations(NetworkClient client) : ITagOperations { - public async Task ListTags( + public async Task ListTags( string name, ListTagsParameters? parameters = null, CancellationToken token = default) { if (string.IsNullOrEmpty(name)) - throw new ArgumentException( - $"'{nameof(name)}' cannot be null or empty", - nameof(name)); + throw new ArgumentException($"'{nameof(name)}' cannot be null or empty", nameof(name)); parameters ??= new ListTagsParameters(); @@ -39,7 +39,12 @@ public async Task ListTags( queryString, token: token); - return client.JsonSerializer.DeserializeObject( - response.Body); + var listTags = client.JsonSerializer.DeserializeObject(response.Body); + + return listTags == null + ? ListTagResponseModel.Empty + : new ListTagResponseModel( + listTags.Name ?? string.Empty, + listTags.Tags.Select(ImageReference.Create).ToHashSet()); } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/CharHelpers.cs b/src/Docker.Registry.DotNet/Helpers/CharHelpers.cs new file mode 100644 index 0000000..cda234b --- /dev/null +++ b/src/Docker.Registry.DotNet/Helpers/CharHelpers.cs @@ -0,0 +1,26 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Helpers; + +internal static class CharHelpers +{ + /// + /// Only for lower-case strings + /// + /// + /// + internal static bool IsHexLowerCase(this char c) => c is >= '0' and <= '9' or >= 'a' and <= 'f'; +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/CollectionExtensions.cs b/src/Docker.Registry.DotNet/Helpers/CollectionExtensions.cs new file mode 100644 index 0000000..ed35571 --- /dev/null +++ b/src/Docker.Registry.DotNet/Helpers/CollectionExtensions.cs @@ -0,0 +1,26 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Helpers; + +internal static class CollectionExtensions +{ +#if !NET5_0_OR_GREATER + internal static HashSet ToHashSet(this IEnumerable items) + { + return [..items]; + } +#endif +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/StringExtensions.cs b/src/Docker.Registry.DotNet/Helpers/StringExtensions.cs index c554c17..13c8af8 100644 --- a/src/Docker.Registry.DotNet/Helpers/StringExtensions.cs +++ b/src/Docker.Registry.DotNet/Helpers/StringExtensions.cs @@ -23,4 +23,13 @@ public static string ToDelimitedString( { return string.Join(delimiter, strings.IfNullEmpty().ToArray()); } + + public static string? TakeAfter(this string? str, int afterIndex) + { + if (str == null) return null; + + int strLength = str.Length; + + return afterIndex >= strLength ? str : str.Substring(afterIndex, strLength - afterIndex); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs b/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs index ac8a8da..2abc459 100644 --- a/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs +++ b/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs @@ -13,14 +13,21 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Docker.Registry.DotNet.Domain.Tags; + namespace Docker.Registry.DotNet.Models; [DataContract] -public class ListTagsResponse +internal class ListTagsResponseDto { [DataMember(Name = "name")] public string? Name { get; set; } [DataMember(Name = "tags")] public IReadOnlyCollection Tags { get; set; } = []; +} + +public record ListTagResponseModel(string Name, IReadOnlyCollection Tags) +{ + public static ListTagResponseModel Empty { get; } = new("", []); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs b/src/Docker.Registry.DotNet/Registry/NetworkClient.cs index 820d752..d4da159 100644 --- a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs +++ b/src/Docker.Registry.DotNet/Registry/NetworkClient.cs @@ -131,7 +131,7 @@ internal async Task> MakeRequest( HttpMethod method, string path, IReadOnlyQueryString? queryString = null, - IDictionary? headers = null, + IReadOnlyDictionary? headers = null, Func? content = null, CancellationToken token = default) { @@ -161,7 +161,7 @@ internal async Task> MakeRequestNotErrorAsync( HttpMethod method, string path, IReadOnlyQueryString? queryString = null, - IDictionary? headers = null, + IReadOnlyDictionary? headers = null, Func? content = null, CancellationToken token = default) { @@ -220,7 +220,7 @@ private async Task InternalMakeRequestAsync( HttpMethod method, string path, IReadOnlyQueryString? queryString, - IDictionary? headers, + IReadOnlyDictionary? headers, Func? content, CancellationToken cancellationToken) { @@ -290,7 +290,7 @@ internal void HandleIfErrorResponse(RegistryApiResponse response) internal HttpRequestMessage PrepareRequest( HttpMethod method, Uri uri, - IDictionary? headers, + IReadOnlyDictionary? headers, Func? content) { var request = new HttpRequestMessage( diff --git a/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs b/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs index 1f3fe39..9f1158f 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs +++ b/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs @@ -1,4 +1,4 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman // and Docker.Registry.DotNet Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,10 +24,7 @@ internal abstract class RegistryApiResponse(HttpStatusCode statusCode, HttpRespo internal class RegistryApiResponse : RegistryApiResponse { - internal RegistryApiResponse( - HttpStatusCode statusCode, - TBody? body, - HttpResponseHeaders headers) + internal RegistryApiResponse(HttpStatusCode statusCode, TBody? body, HttpResponseHeaders headers) : base(statusCode, headers) { this.Body = body; diff --git a/test/Docker.Registry.DotNet.Tests/Authentication/AuthenticateParserTests.cs b/test/Docker.Registry.DotNet.Tests/Authentication/AuthenticateParserTests.cs index 1ee2540..432147a 100644 --- a/test/Docker.Registry.DotNet.Tests/Authentication/AuthenticateParserTests.cs +++ b/test/Docker.Registry.DotNet.Tests/Authentication/AuthenticateParserTests.cs @@ -1,22 +1,55 @@ -using Docker.Registry.DotNet.Authentication; -using Xunit; +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -namespace Docker.Registry.DotNet.Tests +using Docker.Registry.DotNet.Authentication; + +using FluentAssertions; + +using NUnit.Framework; + +namespace Docker.Registry.DotNet.Tests.Authentication; + +[TestFixture] +public class AuthenticateParserTests { - public class AuthenticateParserTests + [TestCase("realm=test realm,service=test service,scope=test scope", "test realm", "test service", "test scope")] + [TestCase( + "realm=\"test realm\",service=\"test service\",scope=\"test scope\"", + "test realm", + "test service", + "test scope")] + [TestCase( + "realm=test realm,service=test service,scope=\"scope1,scope2\"", + "test realm", + "test service", + "scope1,scope2")] + [TestCase( + "realm=\"test realm\",service=\"test service\",scope=\"scope1,scope2\"", + "test realm", + "test service", + "scope1,scope2")] + public void GivenACommaDelimitedChallengeHeader_WhenIParseItAsTyped_ThenItShouldReturnTheCorrectSegments( + string header, + string expectedRealm, + string expectedService, + string expectedScope) { - [Theory] - [InlineData("realm=test realm,service=test service,scope=test scope", "test realm", "test service", "test scope")] - [InlineData("realm=\"test realm\",service=\"test service\",scope=\"test scope\"", "test realm", "test service", "test scope")] - [InlineData("realm=test realm,service=test service,scope=\"scope1,scope2\"", "test realm", "test service", "scope1,scope2")] - [InlineData("realm=\"test realm\",service=\"test service\",scope=\"scope1,scope2\"", "test realm", "test service", "scope1,scope2")] - public void GivenACommaDelimitedChallengeHeader_WhenIParseItAsTyped_ThenItShouldReturnTheCorrectSegments( - string header, string expectedRealm, string expectedService, string expectedScope) - { - var actual = AuthenticateParser.ParseTyped(header); - Assert.Equal(expectedRealm, actual.Realm); - Assert.Equal(expectedService, actual.Service); - Assert.Equal(expectedScope, actual.Scope); - } + var actual = AuthenticateParser.ParseTyped(header); + + expectedRealm.Should().Be(actual.Realm); + expectedService.Should().Be(actual.Service); + expectedScope.Should().Be(actual.Scope); } } \ No newline at end of file diff --git a/test/Docker.Registry.DotNet.Tests/Docker.Registry.DotNet.Tests.csproj b/test/Docker.Registry.DotNet.Tests/Docker.Registry.DotNet.Tests.csproj index 7fe5895..805dc72 100644 --- a/test/Docker.Registry.DotNet.Tests/Docker.Registry.DotNet.Tests.csproj +++ b/test/Docker.Registry.DotNet.Tests/Docker.Registry.DotNet.Tests.csproj @@ -9,12 +9,9 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs b/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs new file mode 100644 index 0000000..ac0a1fd --- /dev/null +++ b/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs @@ -0,0 +1,96 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Domain.Tags; + +using FluentAssertions; + +using NUnit.Framework; + +namespace Docker.Registry.DotNet.Tests; + +[TestFixture] +public class ImageReferenceTests +{ + [Test] + public void ImageReferenceWithDigestShouldWork() + { + string digest = " sha256:0d1c30c6bf461513951e4875fe7846f9e2f25fdfec09f4be6b39dbe639d362ca "; + + var imageRef = ImageReference.Create(digest); + + imageRef.Should().NotBeNull(); + + imageRef.Value.Should().Be(digest.Trim().ToLower()); + + imageRef.IsDigest.Should().BeTrue(); + } + + [Test] + public void ImageReferenceWithTagShouldWork() + { + string tag = " 1.2.34.5-master "; + + var imageRef = ImageReference.Create(tag); + + imageRef.Should().NotBeNull(); + + imageRef.Value.Should().Be(tag.Trim()); + + imageRef.IsTag.Should().BeTrue(); + } + + [Test] + public void ImageReferenceWithInvalidDigestShouldFail() + { + string digest = " sha256:0d1c30c6bf461513951e4_75fe7846f9e2f25fdfec09f4be6b.9dbe639d362ca "; + + var action = () => ImageReference.Create(digest); + + action.Should().Throw().WithMessage("*is invalid*"); + } + + [Test] + public void TagEmptyShouldFail() + { + var action = () => ImageReference.Create(""); + + action.Should().Throw().WithMessage("*not be null or empty*"); + } + + [Test] + public void TagTooLargeShouldFail() + { + var action = () => ImageReference.Create(new string(Enumerable.Repeat('b', 129).ToArray())); + + action.Should().Throw().WithMessage("*value is too large*"); + } + + [Test] + public void TagWithInvalidCharactersShouldFail() + { + var action = () => ImageReference.Create("fdsklfkdjsl dfsakldfskljdfs"); + + action.Should().Throw().WithMessage("*is invalid*"); + } + + [Test] + public void TagWithInvalidStartShouldFail() + { + var action = () => ImageReference.Create(".blahblah"); + + action.Should().Throw().WithMessage("*can't start*"); + } +} \ No newline at end of file From 4f17c1046fce014b78847ddf0e928d63f717694d Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Fri, 9 Aug 2024 18:27:22 -0400 Subject: [PATCH 06/17] Added List Tags by Digests. --- .../Domain/Tags/ImageDigest.cs | 86 ++++++++++++ .../Domain/Tags/ImageReference.cs | 129 ++++-------------- .../Domain/Tags/ImageTag.cs | 109 +++++++++++++++ .../Endpoints/IManifestOperations.cs | 2 +- .../Endpoints/ITagOperations.cs | 5 +- .../Implementations/ManifestOperations.cs | 29 ++-- .../Implementations/TagOperations.cs | 35 ++++- .../Helpers/CharHelpers.cs | 2 +- .../Models/ListTagsParameters.cs | 2 +- .../Models/ListTagsResponse.cs | 11 +- .../ImageReferenceTests.cs | 7 +- 11 files changed, 291 insertions(+), 126 deletions(-) create mode 100644 src/Docker.Registry.DotNet/Domain/Tags/ImageDigest.cs create mode 100644 src/Docker.Registry.DotNet/Domain/Tags/ImageTag.cs diff --git a/src/Docker.Registry.DotNet/Domain/Tags/ImageDigest.cs b/src/Docker.Registry.DotNet/Domain/Tags/ImageDigest.cs new file mode 100644 index 0000000..a14e1fd --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Tags/ImageDigest.cs @@ -0,0 +1,86 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace Docker.Registry.DotNet.Domain.Tags; + +public record ImageDigest +{ + public ImageDigest(string value) + { + var digest = value?.Trim()?.ToLower() ?? string.Empty; + + if (!IsValidDigest(digest)) + { + throw new ArgumentException($"Invalid Digest: {digest}", nameof(value)); + } + + this.Value = digest; + } + + public string Value { get; init; } + + public ImageReference ToReference() => new(this); + + public void Deconstruct(out string value) + { + value = this.Value; + } + + public static ImageDigest Create(string digest) + { + return new ImageDigest(digest); + } + + public static bool TryCreate(string reference, [NotNullWhen(true)] out ImageDigest? digest) + { + reference = reference?.Trim() ?? string.Empty; + + if (IsValidDigest(reference)) + { + digest = new ImageDigest(reference); + return true; + } + + digest = null; + + return false; + } + + /// + /// Based on this site: https://ktomk.github.io/pipelines/doc/DOCKER-NAME-TAG.html + /// + /// + /// + static bool IsValidDigest(string value) + { + var twoParts = value?.Split(':') ?? []; + + if (twoParts.Length == 2) + { + var hash = twoParts[1].Trim(); + + if (hash.Length == 64 && hash.All(c => c.IsHash())) + { + return true; + } + } + + return false; + } + + public override string ToString() => this.Value; +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs b/src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs index 550b377..2421c18 100644 --- a/src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs +++ b/src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs @@ -13,135 +13,66 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Diagnostics.CodeAnalysis; - namespace Docker.Registry.DotNet.Domain.Tags; public record ImageReference { - public ImageReference(string value) + public ImageReference(ImageTag tag) { - var parsedTag = value?.Trim() ?? string.Empty; - - if (IsSha256Digest(parsedTag.ToLower())) - { - // save as all lower - this.Value = parsedTag.ToLower(); - } - else - { - var errors = ValidateTag(parsedTag).ToList(); - - if (errors.Any()) - { - throw new ArgumentException( - $"Invalid Image Reference: {errors.ToDelimitedString(", ")}", - nameof(value)); - } - - this.Value = parsedTag; - } + this.Tag = tag ?? throw new ArgumentNullException(nameof(tag), "Invalid Tag"); } - public static ImageReference Latest { get; } = new("latest"); + public ImageReference(ImageDigest digest) + { + this.Digest = digest ?? throw new ArgumentNullException(nameof(digest), "Invalid Digest"); + } - public string Value { get; init; } + public ImageTag? Tag { get; init; } - public bool IsDigest => IsSha256Digest(Value); + public ImageDigest? Digest { get; init; } - public bool IsTag => !this.IsDigest; + public bool IsDigest => Digest != null; - public void Deconstruct(out string value) - { - value = this.Value; - } + public bool IsTag => Tag != null; - public static ImageReference Create(string tag) + public static ImageReference Create(ImageTag tag) { return new ImageReference(tag); } - public static bool TryCreate(string reference, [NotNullWhen(true)] out ImageReference? imageTag) + public static ImageReference Create(ImageDigest digest) { - reference = reference?.Trim() ?? string.Empty; - - var errors = ValidateTag(reference).ToList(); - - imageTag = null; - - if (errors.Any()) - { - return false; - } - - imageTag = new ImageReference(reference); - - return true; + return new ImageReference(digest); } - static bool IsSha256Digest(string value) + public static ImageReference Create(string reference) { - const string DigestPrefix = "sha256:"; - - if (value?.StartsWith(DigestPrefix) ?? false) - { - // sha256 hash is always 64 characters - string hash = value.TakeAfter(DigestPrefix.Length) ?? string.Empty; - - if (hash.Length == 64 && hash.All(c => c.IsHexLowerCase())) - { - return true; - } - } - - return false; + return ImageDigest.TryCreate(reference, out var digest) ? Create(digest) : Create(ImageTag.Create(reference)); } - private static IEnumerable ValidateTag(string value) + public static bool TryCreate(string reference, out ImageReference? imageReference) { - if (string.IsNullOrWhiteSpace(value)) + if (ImageDigest.TryCreate(reference, out var digest)) { - yield return "Value must not be null or empty"; - - yield break; - } - - if (value.Length > 128) - { - yield return $"Value is too large. Value is {value.Length - } characters and maximum tag size is 128 characters."; - - yield break; + imageReference = Create(digest); + return true; } - if (IsSha256Digest(value.ToLower())) + if (ImageTag.TryCreate(reference, out var tag)) { - yield break; + imageReference = Create(tag); + return true; } - var validChars = new[] { '-', '_', '.' }; - - var invalidCharacters = -#if NET7_0_OR_GREATER - value.Where(c => !char.IsAsciiLetterOrDigit(c) && !validChars.Contains(c)).ToArray(); -#else - value.Where(c => !char.IsLetterOrDigit(c) && !validChars.Contains(c)).ToArray(); -#endif - - if (invalidCharacters.Any()) - { - yield return @$"Value ""{value}"" is invalid characters: ""{ - invalidCharacters.Select(s => $"{s}").ToDelimitedString(",") - }"". Image References can only contain lowercase and uppercase letters, digits, underscores, periods, and hyphens."; + imageReference = null; + return false; + } - yield break; - } + public override string ToString() + { + if (this.IsTag) return this.Tag?.ToString()!; + if (this.IsDigest) return this.Digest?.ToString()!; - if (value.StartsWith(".") || value.StartsWith("-")) - { - yield return @$"Value ""{value}"" is invalid. Image References can't start with a period or hyphen."; - } + return base.ToString(); } - - public override string ToString() => this.Value; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Domain/Tags/ImageTag.cs b/src/Docker.Registry.DotNet/Domain/Tags/ImageTag.cs new file mode 100644 index 0000000..ae0cf2e --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Tags/ImageTag.cs @@ -0,0 +1,109 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace Docker.Registry.DotNet.Domain.Tags; + +public record ImageTag +{ + public ImageTag(string value) + { + var parsedTag = value?.Trim() ?? string.Empty; + + var errors = ValidateTag(parsedTag).ToList(); + + if (errors.Any()) + { + throw new ArgumentException($"Invalid Image Reference: {errors.ToDelimitedString(", ")}", nameof(value)); + } + + this.Value = parsedTag; + } + + public string Value { get; init; } + + public static ImageTag Latest { get; } = new("latest"); + + public ImageReference ToReference() => new(this); + + public void Deconstruct(out string value) + { + value = this.Value; + } + + public static ImageTag Create(string tag) => new(tag); + + public static bool TryCreate(string reference, [NotNullWhen(true)] out ImageTag? imageTag) + { + reference = reference?.Trim() ?? string.Empty; + + var errors = ValidateTag(reference).ToList(); + + imageTag = null; + + if (errors.Any()) + { + return false; + } + + imageTag = new ImageTag(reference); + + return true; + } + + static IEnumerable ValidateTag(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + yield return "Value must not be null or empty"; + + yield break; + } + + if (value.Length > 128) + { + yield return $"Value is too large. Value is {value.Length + } characters and maximum tag size is 128 characters."; + + yield break; + } + + var validChars = new[] { '-', '_', '.' }; + + var invalidCharacters = +#if NET7_0_OR_GREATER + value.Where(c => !char.IsAsciiLetterOrDigit(c) && !validChars.Contains(c)).ToArray(); +#else + value.Where(c => !char.IsLetterOrDigit(c) && !validChars.Contains(c)).ToArray(); +#endif + + if (invalidCharacters.Any()) + { + yield return @$"Value ""{value}"" is invalid characters: ""{ + invalidCharacters.Select(s => $"{s}").ToDelimitedString(",") + }"". Image References can only contain lowercase and uppercase letters, digits, underscores, periods, and hyphens."; + + yield break; + } + + if (value.StartsWith(".") || value.StartsWith("-")) + { + yield return @$"Value ""{value}"" is invalid. Image References can't start with a period or hyphen."; + } + } + + public override string ToString() => this.Value; +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs b/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs index f81b107..30f591c 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs @@ -41,7 +41,7 @@ public interface IManifestOperations /// Task GetManifestRaw(string name, ImageReference reference, CancellationToken token = default); - Task GetDigest(string name, ImageReference reference, CancellationToken token = default); + Task GetDigest(string name, ImageTag tag, CancellationToken token = default); /// /// Fetch the manifest identified by name and reference where reference can be a tag or digest. A HEAD request can also diff --git a/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs b/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs index 1e4e963..52ed82b 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs @@ -1,4 +1,4 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman // and Docker.Registry.DotNet Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,9 +18,10 @@ namespace Docker.Registry.DotNet.Endpoints; [PublicAPI] public interface ITagOperations { - [PublicAPI] Task ListTags( string name, ListTagsParameters? parameters = null, CancellationToken token = default); + + Task ListTagsByDigests(string name, CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs index 45fc520..07a2c44 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs @@ -33,7 +33,16 @@ public async Task GetManifest( ImageReference reference, CancellationToken token = default) { - var digestReference = await this.GetDigest(name, reference, token); + ImageDigest? digestReference = null; + + if (reference.IsTag) + { + digestReference = await this.GetDigest(name, reference.Tag!, token); + } + else if (reference.IsDigest) + { + digestReference = reference.Digest; + } if (digestReference == null) { @@ -42,7 +51,7 @@ public async Task GetManifest( @$"Failed getting the digest reference for ""{reference}"""); } - var response = await this.MakeManifestRequest(name, digestReference, token); + var response = await this.MakeManifestRequest(name, digestReference.ToReference(), token); var contentType = this.GetContentType(response.GetHeader("ContentType"), response.Body); @@ -123,23 +132,13 @@ public async Task DeleteManifest(string name, ImageReference reference, Cancella return response.Body; } - public async Task GetDigest(string name, ImageReference reference, CancellationToken token = default) + public async Task GetDigest(string name, ImageTag tag, CancellationToken token = default) { - if (!reference.IsTag) - { - throw new ArgumentOutOfRangeException(nameof(reference), $@"Reference must be a tag to get the digest: ""{reference}"""); - } - - var response = await MakeManifestRequest(name, reference, token); + var response = await MakeManifestRequest(name, tag.ToReference(), token); var digestValue = response.GetHeader("Docker-Content-Digest"); - if (ImageReference.TryCreate(digestValue, out var digest)) - { - return digest; - } - - return null; + return ImageDigest.TryCreate(digestValue, out var digest) ? digest : null; } private async Task> MakeManifestRequest( diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs index 9d80ce6..edd161c 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs @@ -45,6 +45,39 @@ public async Task ListTags( ? ListTagResponseModel.Empty : new ListTagResponseModel( listTags.Name ?? string.Empty, - listTags.Tags.Select(ImageReference.Create).ToHashSet()); + listTags.Tags.Select(ImageTag.Create).ToHashSet()); + } + + public async Task ListTagsByDigests(string name, CancellationToken token = default) + { + var tags = await this.ListTags(name, token: token); + + var manifestOperations = new ManifestOperations(client); + + var digestLookup = new Dictionary>(); + + var tasks = tags.Tags.Select(async t => (Tag: t, Digest: await manifestOperations.GetDigest(name, t, token))) + .ToList(); + + var tagDigestList = (await Task.WhenAll(tasks)).Where(s => s.Digest != null).ToList(); + + foreach (var item in tagDigestList) + { + if (item.Digest != null) + { + if (digestLookup.TryGetValue(item.Digest, out var list)) + { + list.Add(item.Tag); + } + else + { + digestLookup.Add(item.Digest, [item.Tag]); + } + } + } + + return new ListTagByDigestResponseModel( + name, + digestLookup.Select(s => new DigestTagModel(s.Key, digestLookup[s.Key].ToHashSet())).ToList()); } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/CharHelpers.cs b/src/Docker.Registry.DotNet/Helpers/CharHelpers.cs index cda234b..74afc7e 100644 --- a/src/Docker.Registry.DotNet/Helpers/CharHelpers.cs +++ b/src/Docker.Registry.DotNet/Helpers/CharHelpers.cs @@ -22,5 +22,5 @@ internal static class CharHelpers /// /// /// - internal static bool IsHexLowerCase(this char c) => c is >= '0' and <= '9' or >= 'a' and <= 'f'; + internal static bool IsHash(this char c) => c is >= '0' and <= '9' or >= 'a' and <= 'f' or '=' or '_' or '-'; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ListTagsParameters.cs b/src/Docker.Registry.DotNet/Models/ListTagsParameters.cs index de077d5..934c9e7 100644 --- a/src/Docker.Registry.DotNet/Models/ListTagsParameters.cs +++ b/src/Docker.Registry.DotNet/Models/ListTagsParameters.cs @@ -18,7 +18,7 @@ namespace Docker.Registry.DotNet.Models; public class ListTagsParameters { /// - /// Limit the number of entries in each response. It not present, all entries will be returned + /// Limit the number of entries in each response. Default is all entries. /// [QueryParameter("n")] public int? Number { get; set; } diff --git a/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs b/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs index 2abc459..eefa136 100644 --- a/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs +++ b/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs @@ -1,4 +1,4 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman // and Docker.Registry.DotNet Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,14 @@ internal class ListTagsResponseDto public IReadOnlyCollection Tags { get; set; } = []; } -public record ListTagResponseModel(string Name, IReadOnlyCollection Tags) +public record ListTagResponseModel(string Name, IReadOnlyCollection Tags) { public static ListTagResponseModel Empty { get; } = new("", []); +} + +public record DigestTagModel(ImageDigest Digest, IReadOnlyCollection Tags); + +public record ListTagByDigestResponseModel(string Name, IReadOnlyCollection Tags) +{ + public static ListTagByDigestResponseModel Empty { get; } = new("", new List()); } \ No newline at end of file diff --git a/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs b/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs index ac0a1fd..59ea432 100644 --- a/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs +++ b/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs @@ -24,11 +24,10 @@ namespace Docker.Registry.DotNet.Tests; [TestFixture] public class ImageReferenceTests { - [Test] - public void ImageReferenceWithDigestShouldWork() + [TestCase(" sha256:0d1c30c6bf461513951e4875fe7846f9e2f25fdfec09f4be6b39dbe639d362ca ")] + [TestCase("pipelines@sha256:2ef9a59041a7c4f36001abaec4fe7c10c26c1ead4da11515ba2af346fe60ddac")] + public void ImageReferenceWithDigestShouldWork(string digest) { - string digest = " sha256:0d1c30c6bf461513951e4875fe7846f9e2f25fdfec09f4be6b39dbe639d362ca "; - var imageRef = ImageReference.Create(digest); imageRef.Should().NotBeNull(); From 5571d913847a2a1f651fc3a954e38bf8f214f2df Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Fri, 9 Aug 2024 19:58:03 -0400 Subject: [PATCH 07/17] Fixed up the tests. --- test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs b/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs index 59ea432..82804db 100644 --- a/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs +++ b/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs @@ -32,7 +32,7 @@ public void ImageReferenceWithDigestShouldWork(string digest) imageRef.Should().NotBeNull(); - imageRef.Value.Should().Be(digest.Trim().ToLower()); + imageRef.Digest?.ToString().Should().Be(digest.Trim().ToLower()); imageRef.IsDigest.Should().BeTrue(); } @@ -46,7 +46,7 @@ public void ImageReferenceWithTagShouldWork() imageRef.Should().NotBeNull(); - imageRef.Value.Should().Be(tag.Trim()); + imageRef.Tag?.ToString().Should().Be(tag.Trim()); imageRef.IsTag.Should().BeTrue(); } From 74a693b37f4adf0442e8abd14c4f96ae1def0a96 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sat, 10 Aug 2024 12:30:59 -0400 Subject: [PATCH 08/17] Added comment --- .../Endpoints/Implementations/RepositoryOperations.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs index e7f8f49..ff02fa9 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs +++ b/src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs @@ -17,6 +17,9 @@ namespace Docker.Registry.DotNet.Endpoints.Implementations; +/// +/// Operations on the Docker Repository. +/// public interface IRepositoryOperations { Task ListRepositoryTags( From 4ea7d7078d1b3e8a97945d31b6bb8b1b8db61b03 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 11 Aug 2024 00:36:16 -0400 Subject: [PATCH 09/17] A few changes ;) Switched to more DDD style configuration with a builder. Added Legacy helpers. Simplified Authentication with builders. --- samples/Docker.Registry.Cli/Program.cs | 4 +- .../AnonymousOAuthAuthenticationProvider.cs | 98 ++++--- .../Authentication/AuthenticateParser.cs | 216 +++++++------- .../Authentication/AuthenticationProvider.cs | 118 ++++---- .../BasicAuthenticationProvider.cs | 2 +- .../DockerHubJwtAuthenticationProvider.cs | 4 +- .../Authentication/ParsedAuthentication.cs | 48 +-- .../PasswordOAuthAuthenticationProvider.cs | 122 ++++---- .../Endpoints}/BlobOperations.cs | 22 +- .../Endpoints}/BlobUploadOperations.cs | 16 +- .../Endpoints}/CatalogOperations.cs | 23 +- .../Endpoints}/ManifestOperations.cs | 24 +- .../Endpoints/RepositoryOperations.cs | 40 +++ .../Application/Endpoints/SystemOperations.cs | 40 +++ .../Endpoints}/TagOperations.cs | 9 +- .../{ => Application}/OAuth/OAuthClient.cs | 212 +++++++------- .../{ => Application}/OAuth/OAuthToken.cs | 62 ++-- .../QueryStrings}/QueryString.cs | 9 +- .../QueryStrings}/QueryStringExtensions.cs | 4 +- .../Registry/RegistryClient.cs} | 118 +++----- .../Registry/RegistryUriBuilder.cs | 9 +- .../{Models => Domain/Blobs}/BlobHeader.cs | 52 ++-- .../Blobs}/BlobUploadStatus.cs | 60 ++-- .../Blobs}/GetBlobResponse.cs | 62 ++-- .../Catalogs}/CatalogParameters.cs | 64 ++-- .../Catalogs}/CatalogResponse.cs | 2 +- .../DockerRegistryConstants.cs} | 13 +- .../{ => Domain}/Endpoints/IBlobOperations.cs | 116 ++++---- .../Endpoints/IBlobUploadOperations.cs | 275 +++++++++--------- .../Endpoints/ICatalogOperations.cs | 62 ++-- .../Endpoints/IManifestOperations.cs | 158 +++++----- .../Endpoints/IRepositoryOperations.cs} | 25 +- .../Endpoints/ISystemOperations.cs | 44 +-- .../{ => Domain}/Endpoints/ITagOperations.cs | 52 ++-- .../{Tags => ImageReferences}/ImageDigest.cs | 2 +- .../ImageReference.cs | 6 +- .../{Tags => ImageReferences}/ImageTag.cs | 2 +- .../Manifests}/GetImageManifestResult.cs | 82 +++--- .../Manifests}/ImageManifest.cs | 50 ++-- .../Manifests}/ImageManifest2_1.cs | 118 ++++---- .../Manifests}/ImageManifest2_2.cs | 80 ++--- .../{Models => Domain/Manifests}/Manifest.cs | 104 +++---- .../Manifests}/ManifestFsLayer.cs | 44 +-- .../Manifests}/ManifestHistory.cs | 44 +-- .../Manifests}/ManifestLayer.cs | 100 +++---- .../Manifests}/ManifestList.cs | 72 ++--- .../Manifests}/ManifestMediaTypes.cs | 128 ++++---- .../Manifests}/ManifestSignature.cs | 56 ++-- .../Manifests}/ManifestSignatureHeader.cs | 44 +-- .../Models/CompletedUploadResponse.cs | 2 +- .../{ => Domain}/Models/Config.cs | 98 +++---- .../InitiateMonolithicUploadResponse.cs | 48 +-- .../{ => Domain}/Models/ListTagsParameters.cs | 2 +- .../{ => Domain}/Models/ListTagsResponse.cs | 4 +- .../{ => Domain}/Models/MountParameters.cs | 58 ++-- .../{ => Domain}/Models/MountResponse.cs | 68 ++--- .../{ => Domain}/Models/Platform.cs | 116 ++++---- .../Models/PushManifestResponse.cs | 2 +- .../{ => Domain}/Models/ResumableUpload.cs | 2 +- .../QueryParameterAttribute.cs | 2 +- .../QueryStrings}/IReadOnlyQueryString.cs | 2 +- .../{ => Domain}/Registry/IRegistryClient.cs | 18 +- .../Domain/Registry/IRegistryUriBuilder.cs | 63 ++++ .../Registry/RegistryApiException.cs | 24 +- .../Registry/RegistryApiResponse.cs | 31 +- .../Registry/RegistryConnectionException.cs | 2 +- .../Registry/UnauthorizedApiException.cs | 18 +- .../Repository/ListRepositoryTagsResponse.cs | 16 + .../Domain/Repository/RepositoryTag.cs | 57 ++++ .../Domain/Repository/RepositoryTagImage.cs | 41 +++ .../Repository/RepositoryTagsParameters.cs | 17 ++ .../Implementations/RepositoryOperations.cs | 174 ----------- src/Docker.Registry.DotNet/GlobalUsings.cs | 25 +- .../Helpers/CharHelpers.cs | 2 +- .../Helpers/CollectionExtensions.cs | 2 +- .../Helpers/DictionaryExtensions.cs | 2 +- .../Helpers/EnumerableExtensions.cs | 2 +- .../Helpers/HttpContentHelper.cs | 2 +- .../Helpers/HttpUtility.cs | 4 +- .../Helpers/StringExtensions.cs | 2 +- .../Json}/JsonSerializer.cs | 2 +- .../Registry/RegistryClient.cs | 62 ---- .../RegistryClientConfiguration.cs | 129 +++++--- .../RegistryClientConfigurationExtensions.cs | 82 ++++++ .../RegistryClientLegacyHelpers.cs | 248 ++++++++++++++++ .../Authentication/AuthenticateParserTests.cs | 2 +- .../ImageReferenceTests.cs | 25 +- 87 files changed, 2553 insertions(+), 2020 deletions(-) rename src/Docker.Registry.DotNet/{ => Application}/Authentication/AnonymousOAuthAuthenticationProvider.cs (93%) rename src/Docker.Registry.DotNet/{ => Application}/Authentication/AuthenticateParser.cs (95%) rename src/Docker.Registry.DotNet/{ => Application}/Authentication/AuthenticationProvider.cs (94%) rename src/Docker.Registry.DotNet/{ => Application}/Authentication/BasicAuthenticationProvider.cs (96%) rename src/Docker.Registry.DotNet/{ => Application}/Authentication/DockerHubJwtAuthenticationProvider.cs (95%) rename src/Docker.Registry.DotNet/{ => Application}/Authentication/ParsedAuthentication.cs (92%) rename src/Docker.Registry.DotNet/{ => Application}/Authentication/PasswordOAuthAuthenticationProvider.cs (94%) rename src/Docker.Registry.DotNet/{Endpoints/Implementations => Application/Endpoints}/BlobOperations.cs (69%) rename src/Docker.Registry.DotNet/{Endpoints/Implementations => Application/Endpoints}/BlobUploadOperations.cs (95%) rename src/Docker.Registry.DotNet/{Endpoints/Implementations => Application/Endpoints}/CatalogOperations.cs (58%) rename src/Docker.Registry.DotNet/{Endpoints/Implementations => Application/Endpoints}/ManifestOperations.cs (88%) create mode 100644 src/Docker.Registry.DotNet/Application/Endpoints/RepositoryOperations.cs create mode 100644 src/Docker.Registry.DotNet/Application/Endpoints/SystemOperations.cs rename src/Docker.Registry.DotNet/{Endpoints/Implementations => Application/Endpoints}/TagOperations.cs (90%) rename src/Docker.Registry.DotNet/{ => Application}/OAuth/OAuthClient.cs (90%) rename src/Docker.Registry.DotNet/{ => Application}/OAuth/OAuthToken.cs (93%) rename src/Docker.Registry.DotNet/{Helpers => Application/QueryStrings}/QueryString.cs (90%) rename src/Docker.Registry.DotNet/{Helpers => Application/QueryStrings}/QueryStringExtensions.cs (95%) rename src/Docker.Registry.DotNet/{Registry/NetworkClient.cs => Application/Registry/RegistryClient.cs} (72%) rename src/Docker.Registry.DotNet/{ => Application}/Registry/RegistryUriBuilder.cs (91%) rename src/Docker.Registry.DotNet/{Models => Domain/Blobs}/BlobHeader.cs (92%) rename src/Docker.Registry.DotNet/{Models => Domain/Blobs}/BlobUploadStatus.cs (94%) rename src/Docker.Registry.DotNet/{Models => Domain/Blobs}/GetBlobResponse.cs (93%) rename src/Docker.Registry.DotNet/{Models => Domain/Catalogs}/CatalogParameters.cs (81%) rename src/Docker.Registry.DotNet/{Models => Domain/Catalogs}/CatalogResponse.cs (94%) rename src/Docker.Registry.DotNet/{Endpoints/Implementations/SystemOperations.cs => Domain/DockerRegistryConstants.cs} (64%) rename src/Docker.Registry.DotNet/{ => Domain}/Endpoints/IBlobOperations.cs (94%) rename src/Docker.Registry.DotNet/{ => Domain}/Endpoints/IBlobUploadOperations.cs (94%) rename src/Docker.Registry.DotNet/{ => Domain}/Endpoints/ICatalogOperations.cs (91%) rename src/Docker.Registry.DotNet/{ => Domain}/Endpoints/IManifestOperations.cs (72%) rename src/Docker.Registry.DotNet/{Registry/IRegistryUriBuilder.cs => Domain/Endpoints/IRepositoryOperations.cs} (59%) rename src/Docker.Registry.DotNet/{ => Domain}/Endpoints/ISystemOperations.cs (92%) rename src/Docker.Registry.DotNet/{ => Domain}/Endpoints/ITagOperations.cs (93%) rename src/Docker.Registry.DotNet/Domain/{Tags => ImageReferences}/ImageDigest.cs (97%) rename src/Docker.Registry.DotNet/Domain/{Tags => ImageReferences}/ImageReference.cs (93%) rename src/Docker.Registry.DotNet/Domain/{Tags => ImageReferences}/ImageTag.cs (98%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/GetImageManifestResult.cs (94%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/ImageManifest.cs (93%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/ImageManifest2_1.cs (95%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/ImageManifest2_2.cs (95%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/Manifest.cs (95%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/ManifestFsLayer.cs (92%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/ManifestHistory.cs (92%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/ManifestLayer.cs (95%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/ManifestList.cs (95%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/ManifestMediaTypes.cs (95%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/ManifestSignature.cs (93%) rename src/Docker.Registry.DotNet/{Models => Domain/Manifests}/ManifestSignatureHeader.cs (92%) rename src/Docker.Registry.DotNet/{ => Domain}/Models/CompletedUploadResponse.cs (96%) rename src/Docker.Registry.DotNet/{ => Domain}/Models/Config.cs (95%) rename src/Docker.Registry.DotNet/{ => Domain}/Models/InitiateMonolithicUploadResponse.cs (92%) rename src/Docker.Registry.DotNet/{ => Domain}/Models/ListTagsParameters.cs (94%) rename src/Docker.Registry.DotNet/{ => Domain}/Models/ListTagsResponse.cs (93%) rename src/Docker.Registry.DotNet/{ => Domain}/Models/MountParameters.cs (93%) rename src/Docker.Registry.DotNet/{ => Domain}/Models/MountResponse.cs (94%) rename src/Docker.Registry.DotNet/{ => Domain}/Models/Platform.cs (95%) rename src/Docker.Registry.DotNet/{ => Domain}/Models/PushManifestResponse.cs (96%) rename src/Docker.Registry.DotNet/{ => Domain}/Models/ResumableUpload.cs (96%) rename src/Docker.Registry.DotNet/{ => Domain}/QueryParameters/QueryParameterAttribute.cs (93%) rename src/Docker.Registry.DotNet/{Helpers => Domain/QueryStrings}/IReadOnlyQueryString.cs (93%) rename src/Docker.Registry.DotNet/{ => Domain}/Registry/IRegistryClient.cs (68%) create mode 100644 src/Docker.Registry.DotNet/Domain/Registry/IRegistryUriBuilder.cs rename src/Docker.Registry.DotNet/{ => Domain}/Registry/RegistryApiException.cs (58%) rename src/Docker.Registry.DotNet/{ => Domain}/Registry/RegistryApiResponse.cs (53%) rename src/Docker.Registry.DotNet/{ => Domain}/Registry/RegistryConnectionException.cs (95%) rename src/Docker.Registry.DotNet/{ => Domain}/Registry/UnauthorizedApiException.cs (56%) create mode 100644 src/Docker.Registry.DotNet/Domain/Repository/ListRepositoryTagsResponse.cs create mode 100644 src/Docker.Registry.DotNet/Domain/Repository/RepositoryTag.cs create mode 100644 src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagImage.cs create mode 100644 src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagsParameters.cs delete mode 100644 src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs rename src/Docker.Registry.DotNet/{ => Infrastructure}/Helpers/CharHelpers.cs (94%) rename src/Docker.Registry.DotNet/{ => Infrastructure}/Helpers/CollectionExtensions.cs (93%) rename src/Docker.Registry.DotNet/{ => Infrastructure}/Helpers/DictionaryExtensions.cs (95%) rename src/Docker.Registry.DotNet/{ => Infrastructure}/Helpers/EnumerableExtensions.cs (93%) rename src/Docker.Registry.DotNet/{ => Infrastructure}/Helpers/HttpContentHelper.cs (95%) rename src/Docker.Registry.DotNet/{ => Infrastructure}/Helpers/HttpUtility.cs (97%) rename src/Docker.Registry.DotNet/{ => Infrastructure}/Helpers/StringExtensions.cs (95%) rename src/Docker.Registry.DotNet/{Helpers => Infrastructure/Json}/JsonSerializer.cs (96%) delete mode 100644 src/Docker.Registry.DotNet/Registry/RegistryClient.cs create mode 100644 src/Docker.Registry.DotNet/RegistryClientConfigurationExtensions.cs create mode 100644 src/Docker.Registry.DotNet/RegistryClientLegacyHelpers.cs diff --git a/samples/Docker.Registry.Cli/Program.cs b/samples/Docker.Registry.Cli/Program.cs index 70745bf..42bdac9 100644 --- a/samples/Docker.Registry.Cli/Program.cs +++ b/samples/Docker.Registry.Cli/Program.cs @@ -4,9 +4,7 @@ using System.Security.Cryptography; using System.Threading.Tasks; using Docker.Registry.DotNet; -using Docker.Registry.DotNet.Authentication; -using Docker.Registry.DotNet.Models; -using Docker.Registry.DotNet.Registry; +using Docker.Registry.DotNet.Domain.Registry; namespace Docker.Registry.Cli { diff --git a/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Application/Authentication/AnonymousOAuthAuthenticationProvider.cs similarity index 93% rename from src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs rename to src/Docker.Registry.DotNet/Application/Authentication/AnonymousOAuthAuthenticationProvider.cs index 3087881..0c33e0e 100644 --- a/src/Docker.Registry.DotNet/Authentication/AnonymousOAuthAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/AnonymousOAuthAuthenticationProvider.cs @@ -1,49 +1,51 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Authentication; - -[PublicAPI] -public class AnonymousOAuthAuthenticationProvider : AuthenticationProvider -{ - private readonly OAuthClient _client = new(); - - private static string Schema { get; } = "Bearer"; - - public override Task Authenticate(HttpRequestMessage request) - { - return Task.CompletedTask; - } - - public override async Task Authenticate( - HttpRequestMessage request, - HttpResponseMessage response, - IRegistryUriBuilder uriBuilder) - { - var header = this.TryGetSchemaHeader(response, Schema); - - //Get the bearer bits - var bearerBits = AuthenticateParser.ParseTyped(header.Parameter); - - //Get the token - var token = await this._client.GetToken( - bearerBits.Realm, - bearerBits.Service, - bearerBits.Scope); - - //Set the header - request.Headers.Authorization = new AuthenticationHeaderValue(Schema, token.Token); - } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Application.OAuth; + +namespace Docker.Registry.DotNet.Application.Authentication; + +[PublicAPI] +public class AnonymousOAuthAuthenticationProvider : AuthenticationProvider +{ + private readonly OAuthClient _client = new(); + + private static string Schema { get; } = "Bearer"; + + public override Task Authenticate(HttpRequestMessage request) + { + return Task.CompletedTask; + } + + public override async Task Authenticate( + HttpRequestMessage request, + HttpResponseMessage response, + IRegistryUriBuilder uriBuilder) + { + var header = this.TryGetSchemaHeader(response, Schema); + + //Get the bearer bits + var bearerBits = AuthenticateParser.ParseTyped(header.Parameter); + + //Get the token + var token = await this._client.GetToken( + bearerBits.Realm, + bearerBits.Service, + bearerBits.Scope); + + //Set the header + request.Headers.Authorization = new AuthenticationHeaderValue(Schema, token.Token); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/AuthenticateParser.cs b/src/Docker.Registry.DotNet/Application/Authentication/AuthenticateParser.cs similarity index 95% rename from src/Docker.Registry.DotNet/Authentication/AuthenticateParser.cs rename to src/Docker.Registry.DotNet/Application/Authentication/AuthenticateParser.cs index de47364..27dfca2 100644 --- a/src/Docker.Registry.DotNet/Authentication/AuthenticateParser.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/AuthenticateParser.cs @@ -1,109 +1,109 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Authentication; - -internal static class AuthenticateParser -{ - public static IDictionary Parse(string value) - { - //https://stackoverflow.com/questions/45516717/extracting-and-parsing-the-www-authenticate-header-from-httpresponsemessage-in/45516809#45516809 - return SplitWWWAuthenticateHeader(value).ToDictionary(GetKey, GetValue); - } - - private static IEnumerable SplitWWWAuthenticateHeader(string value) - { - var builder = new StringBuilder(); - var inQuotes = false; - for (var i = 0; i < value.Length; i++) - { - var charI = value[i]; - switch (charI) - { - case '\"': - if (inQuotes) - { - yield return builder.ToString(); - builder.Clear(); - inQuotes = false; - } - else - { - inQuotes = true; - } - - break; - - case ',': - if (inQuotes) - { - builder.Append(charI); - } - else - { - if (builder.Length > 0) - { - yield return builder.ToString(); - builder.Clear(); - } - } - - break; - - default: - builder.Append(charI); - break; - } - } - - if (builder.Length > 0) yield return builder.ToString(); - } - - public static ParsedAuthentication ParseTyped(string value) - { - var parsed = Parse(value); - - return new ParsedAuthentication( - parsed.GetValueOrDefault("realm"), - parsed.GetValueOrDefault("service"), - parsed.GetValueOrDefault("scope")); - } - - private static string GetKey(string pair) - { - var equalPos = pair.IndexOf("=", StringComparison.Ordinal); - - if (equalPos < 1) - throw new FormatException("No '=' found."); - - return pair.Substring(0, equalPos); - } - - private static string GetValue(string pair) - { - var equalPos = pair.IndexOf("=", StringComparison.Ordinal); - - if (equalPos < 1) - throw new FormatException("No '=' found."); - - var value = pair.Substring(equalPos + 1).Trim(); - - //Trim quotes - if (value.StartsWith("\"") && value.EndsWith("\"")) - value = value.Substring(1, value.Length - 2); - - return value; - } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Application.Authentication; + +internal static class AuthenticateParser +{ + public static IDictionary Parse(string value) + { + //https://stackoverflow.com/questions/45516717/extracting-and-parsing-the-www-authenticate-header-from-httpresponsemessage-in/45516809#45516809 + return SplitWWWAuthenticateHeader(value).ToDictionary(GetKey, GetValue); + } + + private static IEnumerable SplitWWWAuthenticateHeader(string value) + { + var builder = new StringBuilder(); + var inQuotes = false; + for (var i = 0; i < value.Length; i++) + { + var charI = value[i]; + switch (charI) + { + case '\"': + if (inQuotes) + { + yield return builder.ToString(); + builder.Clear(); + inQuotes = false; + } + else + { + inQuotes = true; + } + + break; + + case ',': + if (inQuotes) + { + builder.Append(charI); + } + else + { + if (builder.Length > 0) + { + yield return builder.ToString(); + builder.Clear(); + } + } + + break; + + default: + builder.Append(charI); + break; + } + } + + if (builder.Length > 0) yield return builder.ToString(); + } + + public static ParsedAuthentication ParseTyped(string value) + { + var parsed = Parse(value); + + return new ParsedAuthentication( + parsed.GetValueOrDefault("realm"), + parsed.GetValueOrDefault("service"), + parsed.GetValueOrDefault("scope")); + } + + private static string GetKey(string pair) + { + var equalPos = pair.IndexOf("=", StringComparison.Ordinal); + + if (equalPos < 1) + throw new FormatException("No '=' found."); + + return pair.Substring(0, equalPos); + } + + private static string GetValue(string pair) + { + var equalPos = pair.IndexOf("=", StringComparison.Ordinal); + + if (equalPos < 1) + throw new FormatException("No '=' found."); + + var value = pair.Substring(equalPos + 1).Trim(); + + //Trim quotes + if (value.StartsWith("\"") && value.EndsWith("\"")) + value = value.Substring(1, value.Length - 2); + + return value; + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs b/src/Docker.Registry.DotNet/Application/Authentication/AuthenticationProvider.cs similarity index 94% rename from src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs rename to src/Docker.Registry.DotNet/Application/Authentication/AuthenticationProvider.cs index 4fa7151..e2f5714 100644 --- a/src/Docker.Registry.DotNet/Authentication/AuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/AuthenticationProvider.cs @@ -1,60 +1,60 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Authentication; - -/// -/// Authentication provider. -/// -public abstract class AuthenticationProvider -{ - /// - /// Called on the initial send - /// - /// - /// - public abstract Task Authenticate(HttpRequestMessage request); - - /// - /// Called when the send is challenged. - /// - /// - /// - /// - /// - public abstract Task Authenticate( - HttpRequestMessage request, - HttpResponseMessage response, - IRegistryUriBuilder builder); - - /// - /// Gets the schema header from the http response. - /// - /// - /// - /// - protected AuthenticationHeaderValue TryGetSchemaHeader( - HttpResponseMessage response, - string schema) - { - var header = response.GetHeaderBySchema(schema); - - if (header == null) - throw new InvalidOperationException( - $"No WWW-Authenticate challenge was found for schema {schema}"); - - return header; - } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Application.Authentication; + +/// +/// Authentication provider. +/// +public abstract class AuthenticationProvider +{ + /// + /// Called on the initial send + /// + /// + /// + public abstract Task Authenticate(HttpRequestMessage request); + + /// + /// Called when the send is challenged. + /// + /// + /// + /// + /// + public abstract Task Authenticate( + HttpRequestMessage request, + HttpResponseMessage response, + IRegistryUriBuilder builder); + + /// + /// Gets the schema header from the http response. + /// + /// + /// + /// + protected AuthenticationHeaderValue TryGetSchemaHeader( + HttpResponseMessage response, + string schema) + { + var header = response.GetHeaderBySchema(schema); + + if (header == null) + throw new InvalidOperationException( + $"No WWW-Authenticate challenge was found for schema {schema}"); + + return header; + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Application/Authentication/BasicAuthenticationProvider.cs similarity index 96% rename from src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs rename to src/Docker.Registry.DotNet/Application/Authentication/BasicAuthenticationProvider.cs index a694b6d..7362802 100644 --- a/src/Docker.Registry.DotNet/Authentication/BasicAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/BasicAuthenticationProvider.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Authentication; +namespace Docker.Registry.DotNet.Application.Authentication; [PublicAPI] public class BasicAuthenticationProvider(string username, string password) : AuthenticationProvider diff --git a/src/Docker.Registry.DotNet/Authentication/DockerHubJwtAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Application/Authentication/DockerHubJwtAuthenticationProvider.cs similarity index 95% rename from src/Docker.Registry.DotNet/Authentication/DockerHubJwtAuthenticationProvider.cs rename to src/Docker.Registry.DotNet/Application/Authentication/DockerHubJwtAuthenticationProvider.cs index 5882ed1..7adff27 100644 --- a/src/Docker.Registry.DotNet/Authentication/DockerHubJwtAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/DockerHubJwtAuthenticationProvider.cs @@ -13,7 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Authentication; +using Docker.Registry.DotNet.Application.OAuth; + +namespace Docker.Registry.DotNet.Application.Authentication; public class DockerHubJwtAuthenticationProvider(string username, string password) : AuthenticationProvider diff --git a/src/Docker.Registry.DotNet/Authentication/ParsedAuthentication.cs b/src/Docker.Registry.DotNet/Application/Authentication/ParsedAuthentication.cs similarity index 92% rename from src/Docker.Registry.DotNet/Authentication/ParsedAuthentication.cs rename to src/Docker.Registry.DotNet/Application/Authentication/ParsedAuthentication.cs index 8c5dced..c281cfa 100644 --- a/src/Docker.Registry.DotNet/Authentication/ParsedAuthentication.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/ParsedAuthentication.cs @@ -1,25 +1,25 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Authentication; - -internal class ParsedAuthentication(string realm, string service, string scope) -{ - public string Realm { get; } = realm; - - public string Service { get; } = service; - - public string Scope { get; } = scope; +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Application.Authentication; + +internal class ParsedAuthentication(string realm, string service, string scope) +{ + public string Realm { get; } = realm; + + public string Service { get; } = service; + + public string Scope { get; } = scope; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Application/Authentication/PasswordOAuthAuthenticationProvider.cs similarity index 94% rename from src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs rename to src/Docker.Registry.DotNet/Application/Authentication/PasswordOAuthAuthenticationProvider.cs index 9059a22..0dbb55e 100644 --- a/src/Docker.Registry.DotNet/Authentication/PasswordOAuthAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/PasswordOAuthAuthenticationProvider.cs @@ -1,61 +1,63 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Authentication; - -[PublicAPI] -public class PasswordOAuthAuthenticationProvider(string username, string password) - : AuthenticationProvider -{ - private readonly OAuthClient _client = new(); - - private static string Schema { get; } = "Bearer"; - - public override Task Authenticate(HttpRequestMessage request) - { - return Task.CompletedTask; - } - - public override async Task Authenticate( - HttpRequestMessage request, - HttpResponseMessage response, - IRegistryUriBuilder uriBuilder) - { - var header = this.TryGetSchemaHeader(response, Schema); - - //Get the bearer bits - var bearerBits = AuthenticateParser.ParseTyped(header.Parameter); - - string? scope = null; - - if (!string.IsNullOrWhiteSpace(bearerBits.Scope)) - //Also include the repository(plugin) resource type to be able to access plugin repositories. - //See https://docs.docker.com/registry/spec/auth/scope/ - scope = - $"{bearerBits.Scope} {bearerBits.Scope?.Replace("repository:", "repository(plugin):")}"; - - //Get the token - var token = await this._client.GetToken( - bearerBits.Realm, - bearerBits.Service, - scope, - username, - password); - - //Set the header - request.Headers.Authorization = - new AuthenticationHeaderValue(Schema, token.AccessToken); - } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Application.OAuth; + +namespace Docker.Registry.DotNet.Application.Authentication; + +[PublicAPI] +public class PasswordOAuthAuthenticationProvider(string username, string password) + : AuthenticationProvider +{ + private readonly OAuthClient _client = new(); + + private static string Schema { get; } = "Bearer"; + + public override Task Authenticate(HttpRequestMessage request) + { + return Task.CompletedTask; + } + + public override async Task Authenticate( + HttpRequestMessage request, + HttpResponseMessage response, + IRegistryUriBuilder uriBuilder) + { + var header = this.TryGetSchemaHeader(response, Schema); + + //Get the bearer bits + var bearerBits = AuthenticateParser.ParseTyped(header.Parameter); + + string? scope = null; + + if (!string.IsNullOrWhiteSpace(bearerBits.Scope)) + //Also include the repository(plugin) resource type to be able to access plugin repositories. + //See https://docs.docker.com/registry/spec/auth/scope/ + scope = + $"{bearerBits.Scope} {bearerBits.Scope?.Replace("repository:", "repository(plugin):")}"; + + //Get the token + var token = await this._client.GetToken( + bearerBits.Realm, + bearerBits.Service, + scope, + username, + password); + + //Set the header + request.Headers.Authorization = + new AuthenticationHeaderValue(Schema, token.AccessToken); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs b/src/Docker.Registry.DotNet/Application/Endpoints/BlobOperations.cs similarity index 69% rename from src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs rename to src/Docker.Registry.DotNet/Application/Endpoints/BlobOperations.cs index 788a0ec..cdfc2b0 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobOperations.cs +++ b/src/Docker.Registry.DotNet/Application/Endpoints/BlobOperations.cs @@ -13,9 +13,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Endpoints.Implementations; -internal class BlobOperations(NetworkClient client) : IBlobOperations +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Domain.Blobs; + +namespace Docker.Registry.DotNet.Application.Endpoints; + +internal class BlobOperations(RegistryClient client) : IBlobOperations { public async Task GetBlob( string name, diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs b/src/Docker.Registry.DotNet/Application/Endpoints/BlobUploadOperations.cs similarity index 95% rename from src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs rename to src/Docker.Registry.DotNet/Application/Endpoints/BlobUploadOperations.cs index 0069cbc..489341e 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/BlobUploadOperations.cs +++ b/src/Docker.Registry.DotNet/Application/Endpoints/BlobUploadOperations.cs @@ -13,11 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Diagnostics; +using Docker.Registry.DotNet.Application.QueryStrings; +using Docker.Registry.DotNet.Domain.Blobs; +using Docker.Registry.DotNet.Domain.ImageReferences; -namespace Docker.Registry.DotNet.Endpoints.Implementations; +namespace Docker.Registry.DotNet.Application.Endpoints; -internal class BlobUploadOperations(NetworkClient client) : IBlobUploadOperations +internal class BlobUploadOperations(RegistryClient client) : IBlobUploadOperations { public async Task StartUploadBlob( string name, @@ -39,11 +41,11 @@ public async Task StartUploadBlob( public Task MonolithicUploadBlob( ResumableUpload resumable, - string digest, + ImageDigest digest, Stream stream, CancellationToken token = default) { - return this.CompleteBlobUpload( + return CompleteBlobUpload( resumable, digest, stream, @@ -123,7 +125,7 @@ public async Task UploadBlobChunk( public async Task CompleteBlobUpload( ResumableUpload resumable, - string digest, + ImageDigest digest, Stream? chunk = null, long? from = null, long? to = null, @@ -180,7 +182,7 @@ public async Task UploadBlob( string name, int contentLength, Stream stream, - string digest, + ImageDigest digest, CancellationToken token = default) { var path = $"{client.RegistryVersion}/{name}/blobs/uploads/"; diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs b/src/Docker.Registry.DotNet/Application/Endpoints/CatalogOperations.cs similarity index 58% rename from src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs rename to src/Docker.Registry.DotNet/Application/Endpoints/CatalogOperations.cs index f67fc3a..2d791a4 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/CatalogOperations.cs +++ b/src/Docker.Registry.DotNet/Application/Endpoints/CatalogOperations.cs @@ -13,9 +13,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Endpoints.Implementations; -internal class CatalogOperations(NetworkClient client) : ICatalogOperations +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Application.QueryStrings; +using Docker.Registry.DotNet.Domain.Catalogs; + +namespace Docker.Registry.DotNet.Application.Endpoints; + +internal class CatalogOperations(RegistryClient client) : ICatalogOperations { public async Task GetCatalog( CatalogParameters? parameters = null, diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs b/src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs similarity index 88% rename from src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs rename to src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs index 07a2c44..3461127 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/ManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs @@ -13,11 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Docker.Registry.DotNet.Domain.Tags; +using Docker.Registry.DotNet.Domain.ImageReferences; +using Docker.Registry.DotNet.Domain.Manifests; -namespace Docker.Registry.DotNet.Endpoints.Implementations; +namespace Docker.Registry.DotNet.Application.Endpoints; -internal class ManifestOperations(NetworkClient client) : IManifestOperations +internal class ManifestOperations(RegistryClient client) : IManifestOperations { static readonly IReadOnlyDictionary _manifestHeaders = new Dictionary { @@ -37,7 +38,7 @@ public async Task GetManifest( if (reference.IsTag) { - digestReference = await this.GetDigest(name, reference.Tag!, token); + digestReference = await GetDigest(name, reference.Tag!, token); } else if (reference.IsDigest) { @@ -51,9 +52,9 @@ public async Task GetManifest( @$"Failed getting the digest reference for ""{reference}"""); } - var response = await this.MakeManifestRequest(name, digestReference.ToReference(), token); + var response = await MakeManifestRequest(name, digestReference.ToReference(), token); - var contentType = this.GetContentType(response.GetHeader("ContentType"), response.Body); + var contentType = GetContentType(response.GetHeader("ContentType"), response.Body); switch (contentType) { @@ -64,14 +65,16 @@ public async Task GetManifest( client.JsonSerializer.DeserializeObject(response.Body), response.Body) { - DockerContentDigest = response.GetHeader("Docker-Content-Digest"), Etag = response.GetHeader("Etag") + DockerContentDigest = response.GetHeader("Docker-Content-Digest"), + Etag = response.GetHeader("Etag") }; case ManifestMediaTypes.ManifestSchema2: return new GetImageManifestResult( contentType, client.JsonSerializer.DeserializeObject(response.Body), - response.Body) { DockerContentDigest = response.GetHeader("Docker-Content-Digest") }; + response.Body) + { DockerContentDigest = response.GetHeader("Docker-Content-Digest") }; case ManifestMediaTypes.ManifestList: return new GetImageManifestResult( @@ -125,7 +128,10 @@ public async Task DeleteManifest(string name, ImageReference reference, Cancella } [PublicAPI] - public async Task GetManifestRaw(string name, ImageReference reference, CancellationToken token) + public async Task GetManifestRaw( + string name, + ImageReference reference, + CancellationToken token) { var response = await MakeManifestRequest(name, reference, token); diff --git a/src/Docker.Registry.DotNet/Application/Endpoints/RepositoryOperations.cs b/src/Docker.Registry.DotNet/Application/Endpoints/RepositoryOperations.cs new file mode 100644 index 0000000..6d68b04 --- /dev/null +++ b/src/Docker.Registry.DotNet/Application/Endpoints/RepositoryOperations.cs @@ -0,0 +1,40 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Application.QueryStrings; +using Docker.Registry.DotNet.Domain.Repository; + +namespace Docker.Registry.DotNet.Application.Endpoints; + +internal class RepositoryOperations(RegistryClient client) : IRepositoryOperations +{ + public async Task ListRepositoryTags( + string @namespace, + string repository, + RepositoryTagsParameters? parameters = null, + CancellationToken token = default) + { + var queryString = new QueryString(); + queryString.AddFromObject(parameters ?? new RepositoryTagsParameters()); + + var response = await client.MakeRequest( + HttpMethod.Get, + $"{client.RegistryVersion}/namespaces/{@namespace}/repositories/{repository}/tags", + queryString, + token: token); + + return client.JsonSerializer.DeserializeObject(response.Body); + } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Application/Endpoints/SystemOperations.cs b/src/Docker.Registry.DotNet/Application/Endpoints/SystemOperations.cs new file mode 100644 index 0000000..06845a4 --- /dev/null +++ b/src/Docker.Registry.DotNet/Application/Endpoints/SystemOperations.cs @@ -0,0 +1,40 @@ +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Application.Endpoints; + +internal class SystemOperations(RegistryClient client) : ISystemOperations +{ + public virtual Task Ping(CancellationToken token = default) + { + return client.MakeRequest(HttpMethod.Get, "", token: token); + } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs b/src/Docker.Registry.DotNet/Application/Endpoints/TagOperations.cs similarity index 90% rename from src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs rename to src/Docker.Registry.DotNet/Application/Endpoints/TagOperations.cs index edd161c..60438c4 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/TagOperations.cs +++ b/src/Docker.Registry.DotNet/Application/Endpoints/TagOperations.cs @@ -13,11 +13,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Docker.Registry.DotNet.Domain.Tags; +using Docker.Registry.DotNet.Application.QueryStrings; +using Docker.Registry.DotNet.Domain.ImageReferences; -namespace Docker.Registry.DotNet.Endpoints.Implementations; +namespace Docker.Registry.DotNet.Application.Endpoints; -internal class TagOperations(NetworkClient client) : ITagOperations +internal class TagOperations(RegistryClient client) : ITagOperations { public async Task ListTags( string name, @@ -50,7 +51,7 @@ public async Task ListTags( public async Task ListTagsByDigests(string name, CancellationToken token = default) { - var tags = await this.ListTags(name, token: token); + var tags = await ListTags(name, token: token); var manifestOperations = new ManifestOperations(client); diff --git a/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs b/src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs similarity index 90% rename from src/Docker.Registry.DotNet/OAuth/OAuthClient.cs rename to src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs index 6740232..2557150 100644 --- a/src/Docker.Registry.DotNet/OAuth/OAuthClient.cs +++ b/src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs @@ -1,106 +1,108 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System.Diagnostics; - -namespace Docker.Registry.DotNet.OAuth; - -internal class OAuthClient -{ - private readonly HttpClient _client = new(); - - private async Task GetTokenInner( - string realm, - string? service, - string? scope, - string? username, - string? password, - CancellationToken token = default) - { - HttpRequestMessage request; - - if (username == null || password == null) - { - var queryString = new QueryString(); - - queryString.AddIfNotEmpty("service", service); - queryString.AddIfNotEmpty("scope", scope); - - var builder = new UriBuilder(new Uri(realm)) - { - Query = queryString.GetQueryString() - }; - - request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); - } - else - { - request = new HttpRequestMessage(HttpMethod.Post, realm) - { - Content = new FormUrlEncodedContent( - new Dictionary - { - { "client_id", "Docker.Registry.DotNet" }, - { "grant_type", "password" }, - { "username", username }, - { "password", password }, - { "service", service }, - { "scope", scope } - } - ) - }; - } - - using var response = await this._client.SendAsync(request, token); - - if (!response.IsSuccessStatusCode) - { - throw new UnauthorizedAccessException( - $"Unable to authenticate: {await response.Content.ReadAsStringAsyncWithCancellation(token)}"); - } - - var body = await response.Content.ReadAsStringAsyncWithCancellation(token); - - var authToken = JsonConvert.DeserializeObject(body); - - return authToken; - } - - public Task GetToken( - string realm, - string service, - string scope, - CancellationToken cancellationToken = default) - { - return this.GetTokenInner(realm, service, scope, null, null, cancellationToken); - } - - public Task GetToken( - string realm, - string service, - string scope, - string username, - string password, - CancellationToken cancellationToken = default) - { - return this.GetTokenInner( - realm, - service, - scope, - username, - password, - cancellationToken); - } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Application.QueryStrings; + +namespace Docker.Registry.DotNet.Application.OAuth; + +internal class OAuthClient +{ + private static readonly HttpClient _client = new(); + + private async Task GetTokenInner( + string realm, + string? service, + string? scope, + string? username, + string? password, + CancellationToken token = default) + { + HttpRequestMessage request; + + if (username == null || password == null) + { + var queryString = new QueryString(); + + queryString.AddIfNotEmpty("service", service); + queryString.AddIfNotEmpty("scope", scope); + + var builder = new UriBuilder(new Uri(realm)) + { + Query = queryString.GetQueryString() + }; + + request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); + } + else + { + request = new HttpRequestMessage(HttpMethod.Post, realm) + { + Content = new FormUrlEncodedContent( + new Dictionary + { + { "client_id", "Docker.Registry.DotNet" }, + { "grant_type", "password" }, + { "username", username }, + { "password", password }, + { "service", service }, + { "scope", scope } + } + ) + }; + } + + Debug.WriteLine("OAuth Client GetToken"); + + using var response = await _client.SendAsync(request, token); + + if (!response.IsSuccessStatusCode) + { + throw new UnauthorizedAccessException( + $"Unable to authenticate: {await response.Content.ReadAsStringAsyncWithCancellation(token)}"); + } + + var body = await response.Content.ReadAsStringAsyncWithCancellation(token); + + var authToken = JsonConvert.DeserializeObject(body); + + return authToken; + } + + public Task GetToken( + string realm, + string service, + string scope, + CancellationToken cancellationToken = default) + { + return this.GetTokenInner(realm, service, scope, null, null, cancellationToken); + } + + public Task GetToken( + string realm, + string service, + string? scope, + string username, + string password, + CancellationToken cancellationToken = default) + { + return this.GetTokenInner( + realm, + service, + scope, + username, + password, + cancellationToken); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/OAuth/OAuthToken.cs b/src/Docker.Registry.DotNet/Application/OAuth/OAuthToken.cs similarity index 93% rename from src/Docker.Registry.DotNet/OAuth/OAuthToken.cs rename to src/Docker.Registry.DotNet/Application/OAuth/OAuthToken.cs index c7b2c84..9b7ad68 100644 --- a/src/Docker.Registry.DotNet/OAuth/OAuthToken.cs +++ b/src/Docker.Registry.DotNet/Application/OAuth/OAuthToken.cs @@ -1,32 +1,32 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.OAuth; - -[DataContract] -internal class OAuthToken -{ - [DataMember(Name = "token")] - public string? Token { get; set; } - - [DataMember(Name = "access_token")] - public string? AccessToken { get; set; } - - [DataMember(Name = "expires_in")] - public int ExpiresIn { get; set; } - - [DataMember(Name = "issued_at")] - public DateTime IssuedAt { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Application.OAuth; + +[DataContract] +internal class OAuthToken +{ + [DataMember(Name = "token")] + public string? Token { get; set; } + + [DataMember(Name = "access_token")] + public string? AccessToken { get; set; } + + [DataMember(Name = "expires_in")] + public int ExpiresIn { get; set; } + + [DataMember(Name = "issued_at")] + public DateTime IssuedAt { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/QueryString.cs b/src/Docker.Registry.DotNet/Application/QueryStrings/QueryString.cs similarity index 90% rename from src/Docker.Registry.DotNet/Helpers/QueryString.cs rename to src/Docker.Registry.DotNet/Application/QueryStrings/QueryString.cs index 6f86a8d..d544340 100644 --- a/src/Docker.Registry.DotNet/Helpers/QueryString.cs +++ b/src/Docker.Registry.DotNet/Application/QueryStrings/QueryString.cs @@ -15,7 +15,9 @@ using System.Reflection; -namespace Docker.Registry.DotNet.Helpers; +using Docker.Registry.DotNet.Domain.QueryStrings; + +namespace Docker.Registry.DotNet.Application.QueryStrings; internal class QueryString : IReadOnlyQueryString { @@ -37,6 +39,11 @@ public void Add(string key, string? value) this._values.Add(key, [value ?? string.Empty]); } + public void Add(string key, object? value) + { + this._values.Add(key, [value?.ToString() ?? string.Empty]); + } + public void Add(string key, string[] values) { this._values.Add(key, values); diff --git a/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs b/src/Docker.Registry.DotNet/Application/QueryStrings/QueryStringExtensions.cs similarity index 95% rename from src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs rename to src/Docker.Registry.DotNet/Application/QueryStrings/QueryStringExtensions.cs index 818f3aa..f478def 100644 --- a/src/Docker.Registry.DotNet/Helpers/QueryStringExtensions.cs +++ b/src/Docker.Registry.DotNet/Application/QueryStrings/QueryStringExtensions.cs @@ -13,9 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Reflection; - -namespace Docker.Registry.DotNet.Helpers; +namespace Docker.Registry.DotNet.Application.QueryStrings; internal static class QueryStringExtensions { diff --git a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs b/src/Docker.Registry.DotNet/Application/Registry/RegistryClient.cs similarity index 72% rename from src/Docker.Registry.DotNet/Registry/NetworkClient.cs rename to src/Docker.Registry.DotNet/Application/Registry/RegistryClient.cs index d4da159..63de924 100644 --- a/src/Docker.Registry.DotNet/Registry/NetworkClient.cs +++ b/src/Docker.Registry.DotNet/Application/Registry/RegistryClient.cs @@ -13,17 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Diagnostics; +using Docker.Registry.DotNet.Application.Endpoints; +using Docker.Registry.DotNet.Domain; +using Docker.Registry.DotNet.Domain.QueryStrings; -using JsonSerializer = Docker.Registry.DotNet.Helpers.JsonSerializer; +namespace Docker.Registry.DotNet.Application.Registry; -namespace Docker.Registry.DotNet.Registry; - -internal class NetworkClient : IDisposable +public class RegistryClient : IRegistryClient { - private const string UserAgent = "Docker.Registry.DotNet"; - - private static readonly TimeSpan InfiniteTimeout = + private static readonly TimeSpan _infiniteTimeout = TimeSpan.FromMilliseconds(Timeout.Infinite); private readonly AuthenticationProvider _authenticationProvider; @@ -44,87 +42,57 @@ internal class NetworkClient : IDisposable internal IRegistryUriBuilder? UriBuilder; - public NetworkClient( + public RegistryClient( RegistryClientConfiguration configuration, AuthenticationProvider authenticationProvider) { - this._configuration = - configuration ?? throw new ArgumentNullException(nameof(configuration)); - - this._authenticationProvider = authenticationProvider - ?? throw new ArgumentNullException( - nameof(authenticationProvider)); + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + if (authenticationProvider == null) throw new ArgumentNullException(nameof(authenticationProvider)); + if (configuration.BaseAddress == null) throw new ArgumentNullException(nameof(configuration.BaseAddress)); + this._authenticationProvider = authenticationProvider; this._client = configuration.HttpMessageHandler is null ? new HttpClient() : new HttpClient(configuration.HttpMessageHandler); - - this.DefaultTimeout = configuration.DefaultTimeout; - - this.JsonSerializer = new JsonSerializer(); + this._configuration = configuration; + this.UriBuilder = new RegistryUriBuilder(configuration.BaseAddress); + + this.Manifest = new ManifestOperations(this); + this.Catalog = new CatalogOperations(this); + this.Blobs = new BlobOperations(this); + this.BlobUploads = new BlobUploadOperations(this); + this.System = new SystemOperations(this); + this.Tags = new TagOperations(this); + this.Repository = new RepositoryOperations(this); } - public string RegistryVersion { get; } = "v2"; + internal string RegistryVersion => DockerRegistryConstants.RegistryVersion; - public TimeSpan DefaultTimeout { get; set; } + internal TimeSpan DefaultTimeout => this._configuration.DefaultTimeout; - public JsonSerializer JsonSerializer { get; } + internal JsonSerializer JsonSerializer { get; } = new(); - public void Dispose() - { - this._client.Dispose(); - } + #region Operations - /// - /// Ensures that we have configured (and potentially probed) the end point. - /// - /// - private async Task EnsureConnection() - { - if (this.UriBuilder != null) return; + public IRepositoryOperations Repository { get; set; } - var tryUrls = new List(); + public IBlobUploadOperations BlobUploads { get; } - // clean up the host - var host = this._configuration.Host.ToLower().Trim(); + public IManifestOperations Manifest { get; } - if (host.StartsWith("http")) - { - // includes schema -- don't add - tryUrls.Add(host); - } - else - { - tryUrls.Add($"https://{host}"); - tryUrls.Add($"http://{host}"); - } + public ICatalogOperations Catalog { get; } - var exceptions = new List(); + public IBlobOperations Blobs { get; } - foreach (var url in tryUrls) - try - { - var registryUriBuilder = new RegistryUriBuilder(url); - await this.ProbeSingleEndpoint(registryUriBuilder); - this.UriBuilder = registryUriBuilder; - return; - } - catch (Exception ex) - { - exceptions.Add(ex); - } + public ITagOperations Tags { get; } - throw new RegistryConnectionException( - $"Unable to connect to any: {tryUrls.Select(s => $"'{s}/{this.RegistryVersion}/'").ToDelimitedString(", ")}'", - new AggregateException(exceptions)); - } + public ISystemOperations System { get; } + + #endregion - private async Task ProbeSingleEndpoint(IRegistryUriBuilder uriBuilder) + public void Dispose() { - using var request = new HttpRequestMessage(HttpMethod.Get, uriBuilder.Build()); - using (await this._client.SendAsync(request)) - { - } + this._client.Dispose(); } internal async Task> MakeRequest( @@ -193,7 +161,7 @@ internal async Task> MakeRequestForStreamedResponseA CancellationToken token = default) { var response = await this.InternalMakeRequestAsync( - InfiniteTimeout, + _infiniteTimeout, HttpCompletionOption.ResponseHeadersRead, method, path, @@ -224,18 +192,16 @@ private async Task InternalMakeRequestAsync( Func? content, CancellationToken cancellationToken) { - await this.EnsureConnection(); - if (this.UriBuilder == null) throw new ArgumentNullException(nameof(this.UriBuilder), "Could not find URI builder"); var builtUri = this.UriBuilder.Build(path, queryString); - Debug.WriteLine("Built URI: " + builtUri); + Debug.WriteLine($"Built URI: {builtUri}"); var request = this.PrepareRequest(method, builtUri, headers, content); - if (timeout != InfiniteTimeout) + if (timeout != _infiniteTimeout) { var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -293,11 +259,9 @@ internal HttpRequestMessage PrepareRequest( IReadOnlyDictionary? headers, Func? content) { - var request = new HttpRequestMessage( - method, - uri); + var request = new HttpRequestMessage(method, uri); - request.Headers.Add("User-Agent", UserAgent); + request.Headers.Add("User-Agent", DockerRegistryConstants.UserAgent); request.Headers.AddRange(headers); //Create the content diff --git a/src/Docker.Registry.DotNet/Registry/RegistryUriBuilder.cs b/src/Docker.Registry.DotNet/Application/Registry/RegistryUriBuilder.cs similarity index 91% rename from src/Docker.Registry.DotNet/Registry/RegistryUriBuilder.cs rename to src/Docker.Registry.DotNet/Application/Registry/RegistryUriBuilder.cs index 8578937..7c887ba 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryUriBuilder.cs +++ b/src/Docker.Registry.DotNet/Application/Registry/RegistryUriBuilder.cs @@ -13,15 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Registry; +namespace Docker.Registry.DotNet.Application.Registry; public class RegistryUriBuilder(Uri baseUri) : IRegistryUriBuilder { - public RegistryUriBuilder(string baseUri) - : this(new Uri(baseUri)) - { - } - public virtual Uri Build(string? path = null, string? queryString = null) { var pathIsUri = false; @@ -30,8 +25,10 @@ public virtual Uri Build(string? path = null, string? queryString = null) if (Uri.TryCreate(path, UriKind.Absolute, out var uri)) pathIsUri = true; else + { // not absolute -- use the base uri uri = baseUri; + } var builder = new UriBuilder(uri); diff --git a/src/Docker.Registry.DotNet/Models/BlobHeader.cs b/src/Docker.Registry.DotNet/Domain/Blobs/BlobHeader.cs similarity index 92% rename from src/Docker.Registry.DotNet/Models/BlobHeader.cs rename to src/Docker.Registry.DotNet/Domain/Blobs/BlobHeader.cs index e4bb6f8..e51ad2b 100644 --- a/src/Docker.Registry.DotNet/Models/BlobHeader.cs +++ b/src/Docker.Registry.DotNet/Domain/Blobs/BlobHeader.cs @@ -1,27 +1,27 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[PublicAPI] -public class BlobHeader -{ - internal BlobHeader(string dockerContentDigest) - { - this.DockerContentDigest = dockerContentDigest; - } - - public string DockerContentDigest { get; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Blobs; + +[PublicAPI] +public class BlobHeader +{ + internal BlobHeader(string dockerContentDigest) + { + this.DockerContentDigest = dockerContentDigest; + } + + public string DockerContentDigest { get; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/BlobUploadStatus.cs b/src/Docker.Registry.DotNet/Domain/Blobs/BlobUploadStatus.cs similarity index 94% rename from src/Docker.Registry.DotNet/Models/BlobUploadStatus.cs rename to src/Docker.Registry.DotNet/Domain/Blobs/BlobUploadStatus.cs index 7e55dcc..5f7854c 100644 --- a/src/Docker.Registry.DotNet/Models/BlobUploadStatus.cs +++ b/src/Docker.Registry.DotNet/Domain/Blobs/BlobUploadStatus.cs @@ -1,31 +1,31 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[PublicAPI] -public class BlobUploadStatus -{ - /// - /// Range header indicating the progress of the upload. When starting an upload, it will return an empty range, since - /// no content has been received. - /// - public string? Range { get; set; } - - /// - /// Identifies the docker upload uuid for the current request. - /// - public string? DockerUploadUuid { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Blobs; + +[PublicAPI] +public class BlobUploadStatus +{ + /// + /// Range header indicating the progress of the upload. When starting an upload, it will return an empty range, since + /// no content has been received. + /// + public string? Range { get; set; } + + /// + /// Identifies the docker upload uuid for the current request. + /// + public string? DockerUploadUuid { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/GetBlobResponse.cs b/src/Docker.Registry.DotNet/Domain/Blobs/GetBlobResponse.cs similarity index 93% rename from src/Docker.Registry.DotNet/Models/GetBlobResponse.cs rename to src/Docker.Registry.DotNet/Domain/Blobs/GetBlobResponse.cs index a14a3f5..84fffb0 100644 --- a/src/Docker.Registry.DotNet/Models/GetBlobResponse.cs +++ b/src/Docker.Registry.DotNet/Domain/Blobs/GetBlobResponse.cs @@ -1,32 +1,32 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -public class GetBlobResponse : BlobHeader, IDisposable -{ - internal GetBlobResponse(string dockerContentDigest, Stream stream) - : base(dockerContentDigest) - { - this.Stream = stream; - } - - public Stream Stream { get; } - - public void Dispose() - { - this.Stream?.Dispose(); - } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Blobs; + +public class GetBlobResponse : BlobHeader, IDisposable +{ + internal GetBlobResponse(string dockerContentDigest, Stream stream) + : base(dockerContentDigest) + { + this.Stream = stream; + } + + public Stream Stream { get; } + + public void Dispose() + { + this.Stream?.Dispose(); + } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/CatalogParameters.cs b/src/Docker.Registry.DotNet/Domain/Catalogs/CatalogParameters.cs similarity index 81% rename from src/Docker.Registry.DotNet/Models/CatalogParameters.cs rename to src/Docker.Registry.DotNet/Domain/Catalogs/CatalogParameters.cs index e483b5a..ff92969 100644 --- a/src/Docker.Registry.DotNet/Models/CatalogParameters.cs +++ b/src/Docker.Registry.DotNet/Domain/Catalogs/CatalogParameters.cs @@ -1,32 +1,34 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[PublicAPI] -public class CatalogParameters -{ - /// - /// Limit the number of entries in each response. It not present, all entries will be returned - /// - [QueryParameter("n")] - public int? Number { get; set; } - - /// - /// Result set will include values lexically after last. - /// - [QueryParameter("last")] - public int? Last { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Catalogs; + +[PublicAPI] +public class CatalogParameters +{ + /// + /// Limit the number of entries in each response. If it's not present, all entries will be returned. + /// + [QueryParameter("n")] + public int? Number { get; set; } + + /// + /// Result set will include values lexically after last. + /// + [QueryParameter("last")] + public int? Last { get; set; } + + public static CatalogParameters Empty { get; } = new(); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/CatalogResponse.cs b/src/Docker.Registry.DotNet/Domain/Catalogs/CatalogResponse.cs similarity index 94% rename from src/Docker.Registry.DotNet/Models/CatalogResponse.cs rename to src/Docker.Registry.DotNet/Domain/Catalogs/CatalogResponse.cs index 3eb6f23..db57ff8 100644 --- a/src/Docker.Registry.DotNet/Models/CatalogResponse.cs +++ b/src/Docker.Registry.DotNet/Domain/Catalogs/CatalogResponse.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models; +namespace Docker.Registry.DotNet.Domain.Catalogs; [DataContract] public class CatalogResponse diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs b/src/Docker.Registry.DotNet/Domain/DockerRegistryConstants.cs similarity index 64% rename from src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs rename to src/Docker.Registry.DotNet/Domain/DockerRegistryConstants.cs index f3816af..d73fbce 100644 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/SystemOperations.cs +++ b/src/Docker.Registry.DotNet/Domain/DockerRegistryConstants.cs @@ -1,4 +1,4 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman // and Docker.Registry.DotNet Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,12 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Endpoints.Implementations; +namespace Docker.Registry.DotNet.Domain; -internal class SystemOperations(NetworkClient client) : ISystemOperations +public class DockerRegistryConstants { - public virtual Task Ping(CancellationToken token = default) - { - return client.MakeRequest(HttpMethod.Get, "", token: token); - } + public const string RegistryVersion = "v2"; + + public const string UserAgent = "Docker.Registry.DotNet"; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs b/src/Docker.Registry.DotNet/Domain/Endpoints/IBlobOperations.cs similarity index 94% rename from src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs rename to src/Docker.Registry.DotNet/Domain/Endpoints/IBlobOperations.cs index 8554160..a36d318 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IBlobOperations.cs +++ b/src/Docker.Registry.DotNet/Domain/Endpoints/IBlobOperations.cs @@ -1,58 +1,60 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Endpoints; - -[PublicAPI] -public interface IBlobOperations -{ - /// - /// Retrieve the blob from the registry identified by digest. Performs a monolithic download of the blob. - /// - /// - /// - /// - /// - [PublicAPI] - Task GetBlob( - string name, - string digest, - CancellationToken token = default); - - /// - /// Delete the blob identified by name and digest. - /// - /// - /// - /// - /// - [PublicAPI] - Task DeleteBlob( - string name, - string digest, - CancellationToken token = default); - - /// - /// Existing Layers - /// - /// - /// - /// - /// - Task BlobExists( - string name, - string digest, - CancellationToken token = default); +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Domain.Blobs; + +namespace Docker.Registry.DotNet.Domain.Endpoints; + +[PublicAPI] +public interface IBlobOperations +{ + /// + /// Retrieve the blob from the registry identified by digest. Performs a monolithic download of the blob. + /// + /// + /// + /// + /// + [PublicAPI] + Task GetBlob( + string name, + string digest, + CancellationToken token = default); + + /// + /// Delete the blob identified by name and digest. + /// + /// + /// + /// + /// + [PublicAPI] + Task DeleteBlob( + string name, + string digest, + CancellationToken token = default); + + /// + /// Existing Layers + /// + /// + /// + /// + /// + Task BlobExists( + string name, + string digest, + CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs b/src/Docker.Registry.DotNet/Domain/Endpoints/IBlobUploadOperations.cs similarity index 94% rename from src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs rename to src/Docker.Registry.DotNet/Domain/Endpoints/IBlobUploadOperations.cs index 38ad40a..f9af325 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IBlobUploadOperations.cs +++ b/src/Docker.Registry.DotNet/Domain/Endpoints/IBlobUploadOperations.cs @@ -1,137 +1,140 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Endpoints; - -[PublicAPI] -public interface IBlobUploadOperations -{ - /// - /// - /// - /// - /// - /// - /// - /// - [PublicAPI] - Task UploadBlob( - string name, - int contentLength, - Stream stream, - string digest, - CancellationToken token = default); - - /// - /// Mount a blob identified by the mount parameter from another repository. - /// - /// - /// - /// - /// - [PublicAPI] - Task MountBlob( - string name, - MountParameters parameters, - CancellationToken token = default); - - /// - /// Retrieve status of upload identified by uuid. The primary purpose of this endpoint is to resolve the current status - /// of a resumable upload. - /// - /// - /// - /// - /// - [PublicAPI] - Task GetBlobUploadStatus( - string name, - string uuid, - CancellationToken cancellationToken = default); - - /// - /// Upload a chunk of data for the specified upload. - /// - /// - /// - /// - /// - /// - /// - [PublicAPI] - Task UploadBlobChunk( - ResumableUpload resumable, - Stream chunk, - long? from = null, - long? to = null, - CancellationToken token = default); - - /// - /// Complete the upload specified by ResumableUploadResponse, optionally appending the body as the final chunk. - /// - /// - /// - /// - /// - /// - /// - /// - [PublicAPI] - Task CompleteBlobUpload( - ResumableUpload resumable, - string digest, - Stream? chunk = null, - long? from = null, - long? to = null, - CancellationToken token = default); - - /// - /// Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished uploads - /// will eventually timeout. - /// - /// - /// - /// - /// - [PublicAPI] - Task CancelBlobUpload( - string name, - string uuid, - CancellationToken token = default); - - /// - /// Starting An Upload - /// - /// - /// - /// - Task StartUploadBlob( - string name, - CancellationToken token = default); - - /// - /// A monolithic upload is simply a chunked upload with a single chunk and may be favored by clients that would like to avoided the complexity of chunking - /// - /// - /// - /// - /// - /// - Task MonolithicUploadBlob( - ResumableUpload resumable, - string digest, - Stream stream, - CancellationToken token = default); +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Domain.Blobs; +using Docker.Registry.DotNet.Domain.ImageReferences; + +namespace Docker.Registry.DotNet.Domain.Endpoints; + +[PublicAPI] +public interface IBlobUploadOperations +{ + /// + /// + /// + /// + /// + /// + /// + /// + [PublicAPI] + Task UploadBlob( + string name, + int contentLength, + Stream stream, + ImageDigest digest, + CancellationToken token = default); + + /// + /// Mount a blob identified by the mount parameter from another repository. + /// + /// + /// + /// + /// + [PublicAPI] + Task MountBlob( + string name, + MountParameters parameters, + CancellationToken token = default); + + /// + /// Retrieve status of upload identified by uuid. The primary purpose of this endpoint is to resolve the current status + /// of a resumable upload. + /// + /// + /// + /// + /// + [PublicAPI] + Task GetBlobUploadStatus( + string name, + string uuid, + CancellationToken cancellationToken = default); + + /// + /// Upload a chunk of data for the specified upload. + /// + /// + /// + /// + /// + /// + /// + [PublicAPI] + Task UploadBlobChunk( + ResumableUpload resumable, + Stream chunk, + long? from = null, + long? to = null, + CancellationToken token = default); + + /// + /// Complete the upload specified by ResumableUploadResponse, optionally appending the body as the final chunk. + /// + /// + /// + /// + /// + /// + /// + /// + [PublicAPI] + Task CompleteBlobUpload( + ResumableUpload resumable, + ImageDigest digest, + Stream? chunk = null, + long? from = null, + long? to = null, + CancellationToken token = default); + + /// + /// Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished uploads + /// will eventually timeout. + /// + /// + /// + /// + /// + [PublicAPI] + Task CancelBlobUpload( + string name, + string uuid, + CancellationToken token = default); + + /// + /// Starting An Upload + /// + /// + /// + /// + Task StartUploadBlob( + string name, + CancellationToken token = default); + + /// + /// A monolithic upload is simply a chunked upload with a single chunk and may be favored by clients that would like to avoided the complexity of chunking + /// + /// + /// + /// + /// + /// + Task MonolithicUploadBlob( + ResumableUpload resumable, + ImageDigest digest, + Stream stream, + CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs b/src/Docker.Registry.DotNet/Domain/Endpoints/ICatalogOperations.cs similarity index 91% rename from src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs rename to src/Docker.Registry.DotNet/Domain/Endpoints/ICatalogOperations.cs index 6f41832..0323036 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ICatalogOperations.cs +++ b/src/Docker.Registry.DotNet/Domain/Endpoints/ICatalogOperations.cs @@ -1,31 +1,33 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Endpoints; - -[PublicAPI] -public interface ICatalogOperations -{ - /// - /// Retrieve a sorted, json list of repositories available in the registry. - /// - /// - /// - /// - [PublicAPI] - Task GetCatalog( - CatalogParameters? parameters = null, - CancellationToken token = default); +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Domain.Catalogs; + +namespace Docker.Registry.DotNet.Domain.Endpoints; + +[PublicAPI] +public interface ICatalogOperations +{ + /// + /// Retrieve a sorted, json list of repositories available in the registry. + /// + /// + /// + /// + [PublicAPI] + Task GetCatalog( + CatalogParameters? parameters = null, + CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs b/src/Docker.Registry.DotNet/Domain/Endpoints/IManifestOperations.cs similarity index 72% rename from src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs rename to src/Docker.Registry.DotNet/Domain/Endpoints/IManifestOperations.cs index 30f591c..0a5c58e 100644 --- a/src/Docker.Registry.DotNet/Endpoints/IManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Domain/Endpoints/IManifestOperations.cs @@ -1,70 +1,90 @@ -// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using Docker.Registry.DotNet.Domain.Tags; - -namespace Docker.Registry.DotNet.Endpoints; - -/// -/// Manifest operations. -/// -[PublicAPI] -public interface IManifestOperations -{ - /// - /// Delete the manifest. - /// - /// - /// - /// - /// - Task DeleteManifest(string name, ImageReference reference, CancellationToken token = default); - - /// - /// Fetch the manifest identified by name and reference raw. - /// - /// - /// - /// - /// - Task GetManifestRaw(string name, ImageReference reference, CancellationToken token = default); - - Task GetDigest(string name, ImageTag tag, CancellationToken token = default); - - /// - /// Fetch the manifest identified by name and reference where reference can be a tag or digest. A HEAD request can also - /// be issued to this endpoint to obtain resource information without receiving all data. - /// - /// - /// - /// - /// - [PublicAPI] - Task GetManifest(string name, ImageReference reference, CancellationToken token = default); - - /// - /// Put the manifest identified by name and reference where reference can be a tag or digest. - /// - /// - /// - /// - /// - /// - Task PutManifest( - string name, - ImageReference reference, - ImageManifest manifest, - CancellationToken token = default); +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Domain.ImageReferences; +using Docker.Registry.DotNet.Domain.Manifests; + +namespace Docker.Registry.DotNet.Domain.Endpoints; + +/// +/// Manifest operations. +/// +[PublicAPI] +public interface IManifestOperations +{ + /// + /// Delete the manifest. + /// + /// + /// + /// + /// + Task DeleteManifest(string name, ImageReference reference, CancellationToken token = default); + + /// + /// Fetch the manifest identified by name and reference raw. + /// + /// + /// + /// + /// + Task GetManifestRaw( + string name, + ImageReference reference, + CancellationToken token = default); + + Task GetDigest(string name, ImageTag tag, CancellationToken token = default); + + /// + /// Fetch the manifest identified by name and reference where reference can be a tag or digest. A HEAD request can also + /// be issued to this endpoint to obtain resource information without receiving all data. + /// + /// + /// + /// + /// + [PublicAPI] + Task GetManifest(string name, ImageReference reference, CancellationToken token = default); + + /// + /// Put the manifest identified by name and reference where reference can be a tag or digest. + /// + /// + /// + /// + /// + /// + Task PutManifest( + string name, + ImageReference reference, + ImageManifest manifest, + CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/IRegistryUriBuilder.cs b/src/Docker.Registry.DotNet/Domain/Endpoints/IRepositoryOperations.cs similarity index 59% rename from src/Docker.Registry.DotNet/Registry/IRegistryUriBuilder.cs rename to src/Docker.Registry.DotNet/Domain/Endpoints/IRepositoryOperations.cs index 242091f..e6206df 100644 --- a/src/Docker.Registry.DotNet/Registry/IRegistryUriBuilder.cs +++ b/src/Docker.Registry.DotNet/Domain/Endpoints/IRepositoryOperations.cs @@ -13,20 +13,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Registry; +using Docker.Registry.DotNet.Application.Endpoints; +using Docker.Registry.DotNet.Domain.Repository; -public interface IRegistryUriBuilder -{ - Uri Build(string? path = null, string? queryString = null); -} +namespace Docker.Registry.DotNet.Domain.Endpoints; -internal static class RegistryUriBuilderExtensions +/// +/// Operations on the Docker Repository. +/// +public interface IRepositoryOperations { - public static Uri Build( - this IRegistryUriBuilder uriBuilder, - string? path = null, - IReadOnlyQueryString? queryString = null) - { - return uriBuilder.Build(path, queryString?.GetQueryString()); - } + Task ListRepositoryTags( + string @namespace, + string repository, + RepositoryTagsParameters? parameters = null, + CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs b/src/Docker.Registry.DotNet/Domain/Endpoints/ISystemOperations.cs similarity index 92% rename from src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs rename to src/Docker.Registry.DotNet/Domain/Endpoints/ISystemOperations.cs index a74e11d..48e6e19 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ISystemOperations.cs +++ b/src/Docker.Registry.DotNet/Domain/Endpoints/ISystemOperations.cs @@ -1,23 +1,23 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Endpoints; - -[PublicAPI] -public interface ISystemOperations -{ - [PublicAPI] - Task Ping(CancellationToken token = default); +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Endpoints; + +[PublicAPI] +public interface ISystemOperations +{ + [PublicAPI] + Task Ping(CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs b/src/Docker.Registry.DotNet/Domain/Endpoints/ITagOperations.cs similarity index 93% rename from src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs rename to src/Docker.Registry.DotNet/Domain/Endpoints/ITagOperations.cs index 52ed82b..9a6e1bc 100644 --- a/src/Docker.Registry.DotNet/Endpoints/ITagOperations.cs +++ b/src/Docker.Registry.DotNet/Domain/Endpoints/ITagOperations.cs @@ -1,27 +1,27 @@ -// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Endpoints; - -[PublicAPI] -public interface ITagOperations -{ - Task ListTags( - string name, - ListTagsParameters? parameters = null, - CancellationToken token = default); - - Task ListTagsByDigests(string name, CancellationToken token = default); +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Endpoints; + +[PublicAPI] +public interface ITagOperations +{ + Task ListTags( + string name, + ListTagsParameters? parameters = null, + CancellationToken token = default); + + Task ListTagsByDigests(string name, CancellationToken token = default); } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Domain/Tags/ImageDigest.cs b/src/Docker.Registry.DotNet/Domain/ImageReferences/ImageDigest.cs similarity index 97% rename from src/Docker.Registry.DotNet/Domain/Tags/ImageDigest.cs rename to src/Docker.Registry.DotNet/Domain/ImageReferences/ImageDigest.cs index a14e1fd..e947964 100644 --- a/src/Docker.Registry.DotNet/Domain/Tags/ImageDigest.cs +++ b/src/Docker.Registry.DotNet/Domain/ImageReferences/ImageDigest.cs @@ -15,7 +15,7 @@ using System.Diagnostics.CodeAnalysis; -namespace Docker.Registry.DotNet.Domain.Tags; +namespace Docker.Registry.DotNet.Domain.ImageReferences; public record ImageDigest { diff --git a/src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs b/src/Docker.Registry.DotNet/Domain/ImageReferences/ImageReference.cs similarity index 93% rename from src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs rename to src/Docker.Registry.DotNet/Domain/ImageReferences/ImageReference.cs index 2421c18..cbb3d6f 100644 --- a/src/Docker.Registry.DotNet/Domain/Tags/ImageReference.cs +++ b/src/Docker.Registry.DotNet/Domain/ImageReferences/ImageReference.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Domain.Tags; +namespace Docker.Registry.DotNet.Domain.ImageReferences; public record ImageReference { @@ -31,9 +31,9 @@ public ImageReference(ImageDigest digest) public ImageDigest? Digest { get; init; } - public bool IsDigest => Digest != null; + public bool IsDigest => this.Digest != null; - public bool IsTag => Tag != null; + public bool IsTag => this.Tag != null; public static ImageReference Create(ImageTag tag) { diff --git a/src/Docker.Registry.DotNet/Domain/Tags/ImageTag.cs b/src/Docker.Registry.DotNet/Domain/ImageReferences/ImageTag.cs similarity index 98% rename from src/Docker.Registry.DotNet/Domain/Tags/ImageTag.cs rename to src/Docker.Registry.DotNet/Domain/ImageReferences/ImageTag.cs index ae0cf2e..9b7c734 100644 --- a/src/Docker.Registry.DotNet/Domain/Tags/ImageTag.cs +++ b/src/Docker.Registry.DotNet/Domain/ImageReferences/ImageTag.cs @@ -15,7 +15,7 @@ using System.Diagnostics.CodeAnalysis; -namespace Docker.Registry.DotNet.Domain.Tags; +namespace Docker.Registry.DotNet.Domain.ImageReferences; public record ImageTag { diff --git a/src/Docker.Registry.DotNet/Models/GetImageManifestResult.cs b/src/Docker.Registry.DotNet/Domain/Manifests/GetImageManifestResult.cs similarity index 94% rename from src/Docker.Registry.DotNet/Models/GetImageManifestResult.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/GetImageManifestResult.cs index 0d0b9f5..bce6d19 100644 --- a/src/Docker.Registry.DotNet/Models/GetImageManifestResult.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/GetImageManifestResult.cs @@ -1,42 +1,42 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -public class GetImageManifestResult -{ - internal GetImageManifestResult(string mediaType, ImageManifest manifest, string content) - { - this.Manifest = manifest; - this.Content = content; - this.MediaType = mediaType; - } - - public string DockerContentDigest { get; internal set; } - - public string Etag { get; internal set; } - - public string MediaType { get; } - - /// - /// The image manifest - /// - public ImageManifest Manifest { get; } - - /// - /// Gets the original, raw body returned from the server. - /// - public string Content { get; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +public class GetImageManifestResult +{ + internal GetImageManifestResult(string mediaType, ImageManifest manifest, string content) + { + this.Manifest = manifest; + this.Content = content; + this.MediaType = mediaType; + } + + public string DockerContentDigest { get; internal set; } + + public string Etag { get; internal set; } + + public string MediaType { get; } + + /// + /// The image manifest + /// + public ImageManifest Manifest { get; } + + /// + /// Gets the original, raw body returned from the server. + /// + public string Content { get; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ImageManifest.cs b/src/Docker.Registry.DotNet/Domain/Manifests/ImageManifest.cs similarity index 93% rename from src/Docker.Registry.DotNet/Models/ImageManifest.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/ImageManifest.cs index 67677d8..b4c74f1 100644 --- a/src/Docker.Registry.DotNet/Models/ImageManifest.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/ImageManifest.cs @@ -1,26 +1,26 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public abstract class ImageManifest -{ - /// - /// This field specifies the image manifest schema version as an integer. - /// - [DataMember(Name = "schemaVersion")] - public int SchemaVersion { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +[DataContract] +public abstract class ImageManifest +{ + /// + /// This field specifies the image manifest schema version as an integer. + /// + [DataMember(Name = "schemaVersion")] + public int SchemaVersion { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ImageManifest2_1.cs b/src/Docker.Registry.DotNet/Domain/Manifests/ImageManifest2_1.cs similarity index 95% rename from src/Docker.Registry.DotNet/Models/ImageManifest2_1.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/ImageManifest2_1.cs index 7d03e15..e816ede 100644 --- a/src/Docker.Registry.DotNet/Models/ImageManifest2_1.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/ImageManifest2_1.cs @@ -1,60 +1,60 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -/// -/// Image Manifest Version 2, Schema 1 -/// -[DataContract] -public class ImageManifest2_1 : ImageManifest -{ - /// - /// name is the name of the image’s repository - /// - [DataMember(Name = "name")] - public string? Name { get; set; } - - /// - /// tag is the tag of the image - /// - [DataMember(Name = "tag")] - public string? Tag { get; set; } - - /// - /// architecture is the host architecture on which this image is intended to run. This is for information purposes and not currently used by the engine - /// - [DataMember(Name = "architecture")] - public string? Architecture { get; set; } - - /// - /// fsLayers is a list of filesystem layer blob sums contained in this image. - /// - [DataMember(Name = "fsLayers")] - public ManifestFsLayer[]? FsLayers { get; set; } - - /// - /// history is a list of unstructured historical data for v1 compatibility. It contains ID of the image layer and ID of the layer’s parent layers. - /// - [DataMember(Name = "history")] - public ManifestHistory[]? History { get; set; } - - /// - /// Signed manifests provides an envelope for a signed image manifest. A signed manifest consists of an image manifest along with an additional field containing the signature of the manifest. - /// The docker client can verify signed manifests and displays a message to the user. - /// - [DataMember(Name = "signatures")] - public ManifestSignature[]? Signatures { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +/// +/// Image Manifest Version 2, Schema 1 +/// +[DataContract] +public class ImageManifest2_1 : ImageManifest +{ + /// + /// name is the name of the image’s repository + /// + [DataMember(Name = "name")] + public string? Name { get; set; } + + /// + /// tag is the tag of the image + /// + [DataMember(Name = "tag")] + public string? Tag { get; set; } + + /// + /// architecture is the host architecture on which this image is intended to run. This is for information purposes and not currently used by the engine + /// + [DataMember(Name = "architecture")] + public string? Architecture { get; set; } + + /// + /// fsLayers is a list of filesystem layer blob sums contained in this image. + /// + [DataMember(Name = "fsLayers")] + public ManifestFsLayer[]? FsLayers { get; set; } + + /// + /// history is a list of unstructured historical data for v1 compatibility. It contains ID of the image layer and ID of the layer’s parent layers. + /// + [DataMember(Name = "history")] + public ManifestHistory[]? History { get; set; } + + /// + /// Signed manifests provides an envelope for a signed image manifest. A signed manifest consists of an image manifest along with an additional field containing the signature of the manifest. + /// The docker client can verify signed manifests and displays a message to the user. + /// + [DataMember(Name = "signatures")] + public ManifestSignature[]? Signatures { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ImageManifest2_2.cs b/src/Docker.Registry.DotNet/Domain/Manifests/ImageManifest2_2.cs similarity index 95% rename from src/Docker.Registry.DotNet/Models/ImageManifest2_2.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/ImageManifest2_2.cs index 8b5eef1..cf63716 100644 --- a/src/Docker.Registry.DotNet/Models/ImageManifest2_2.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/ImageManifest2_2.cs @@ -1,41 +1,41 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class ImageManifest2_2 : ImageManifest -{ - /// - /// The MIME type of the referenced object - /// - [DataMember(Name = "mediaType")] - public string? MediaType { get; set; } - - /// - /// The config field references a configuration object for a container, by digest. This configuration - /// item is a JSON blob that the runtime uses to set up the container. This new schema uses a tweaked - /// version of this configuration to allow image content-addressability on the daemon side. - /// - - [DataMember(Name = "config")] - public Config? Config { get; set; } - - /// - /// The layer list is ordered starting from the base image (opposite order of schema1). - /// - [DataMember(Name = "layers")] - public ManifestLayer[]? Layers { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +[DataContract] +public class ImageManifest2_2 : ImageManifest +{ + /// + /// The MIME type of the referenced object + /// + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } + + /// + /// The config field references a configuration object for a container, by digest. This configuration + /// item is a JSON blob that the runtime uses to set up the container. This new schema uses a tweaked + /// version of this configuration to allow image content-addressability on the daemon side. + /// + + [DataMember(Name = "config")] + public Config? Config { get; set; } + + /// + /// The layer list is ordered starting from the base image (opposite order of schema1). + /// + [DataMember(Name = "layers")] + public ManifestLayer[]? Layers { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/Manifest.cs b/src/Docker.Registry.DotNet/Domain/Manifests/Manifest.cs similarity index 95% rename from src/Docker.Registry.DotNet/Models/Manifest.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/Manifest.cs index 2c3ef93..99be96a 100644 --- a/src/Docker.Registry.DotNet/Models/Manifest.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/Manifest.cs @@ -1,53 +1,53 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[PublicAPI] -public class Manifest -{ - /// - /// The MIME type of the referenced object. This will generally be application/vnd.docker.image.manifest.v2+json, but - /// it could also be application/vnd.docker.image.manifest.v1+json if the manifest list references a legacy schema-1 - /// manifest. - /// - [DataMember(Name = "mediaType")] - public string? MediaType { get; set; } - - /// - /// The size in bytes of the object. This field exists so that a client will have an expected size for the content - /// before validating. If the length of the retrieved content does not match the specified length, the content should - /// not be trusted. - /// - [DataMember(Name = "size")] - public int Size { get; set; } - - /// - /// The digest of the content, as defined by the Registry V2 HTTP API Specificiation. - /// - /// https://docs.docker.com/registry/spec/api/#digest-parameter - [DataMember(Name = "digest")] - public string? Digest { get; set; } - - /// - /// The platform object describes the platform which the image in the manifest runs on. A full list of valid operating - /// system and architecture values are listed in the Go language documentation for $GOOS and $GOARCH - /// - /// - /// https://golang.org/doc/install/source#environment - /// - [DataMember(Name = "platform")] - public Platform? Platform { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +[PublicAPI] +public class Manifest +{ + /// + /// The MIME type of the referenced object. This will generally be application/vnd.docker.image.manifest.v2+json, but + /// it could also be application/vnd.docker.image.manifest.v1+json if the manifest list references a legacy schema-1 + /// manifest. + /// + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } + + /// + /// The size in bytes of the object. This field exists so that a client will have an expected size for the content + /// before validating. If the length of the retrieved content does not match the specified length, the content should + /// not be trusted. + /// + [DataMember(Name = "size")] + public int Size { get; set; } + + /// + /// The digest of the content, as defined by the Registry V2 HTTP API Specificiation. + /// + /// https://docs.docker.com/registry/spec/api/#digest-parameter + [DataMember(Name = "digest")] + public string? Digest { get; set; } + + /// + /// The platform object describes the platform which the image in the manifest runs on. A full list of valid operating + /// system and architecture values are listed in the Go language documentation for $GOOS and $GOARCH + /// + /// + /// https://golang.org/doc/install/source#environment + /// + [DataMember(Name = "platform")] + public Platform? Platform { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestFsLayer.cs b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestFsLayer.cs similarity index 92% rename from src/Docker.Registry.DotNet/Models/ManifestFsLayer.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/ManifestFsLayer.cs index cea1b5a..ac74ea8 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestFsLayer.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestFsLayer.cs @@ -1,23 +1,23 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class ManifestFsLayer -{ - [DataMember(Name = "blobSum")] - public string? BlobSum { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +[DataContract] +public class ManifestFsLayer +{ + [DataMember(Name = "blobSum")] + public string? BlobSum { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestHistory.cs b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestHistory.cs similarity index 92% rename from src/Docker.Registry.DotNet/Models/ManifestHistory.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/ManifestHistory.cs index 3e1e8cf..6763bea 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestHistory.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestHistory.cs @@ -1,23 +1,23 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class ManifestHistory -{ - [DataMember(Name = "v1Compatibility")] - public string? V1Compatibility { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +[DataContract] +public class ManifestHistory +{ + [DataMember(Name = "v1Compatibility")] + public string? V1Compatibility { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestLayer.cs b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestLayer.cs similarity index 95% rename from src/Docker.Registry.DotNet/Models/ManifestLayer.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/ManifestLayer.cs index a2ac2ed..6e6cd2a 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestLayer.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestLayer.cs @@ -1,51 +1,51 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class ManifestLayer -{ - /// - /// The MIME type of the referenced object. This should generally be application/vnd.docker.image.rootfs.diff.tar.gzip. - /// Layers of type application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be pulled from a remote location but - /// they should never be pushed. - /// - [DataMember(Name = "mediaType")] - public string? MediaType { get; set; } - - /// - /// The size in bytes of the object. This field exists so that a client will have an expected size for the content - /// before validating. If the length of the retrieved content does not match the specified length, the content should - /// not be trusted. - /// - [DataMember(Name = "size")] - public long Size { get; set; } - - /// - /// The digest of the content, as defined by the Registry V2 HTTP API Specification. - /// - /// https://docs.docker.com/registry/spec/api/#digest-parameter - [DataMember(Name = "digest")] - public string? Digest { get; set; } - - /// - /// Provides a list of URLs from which the content may be fetched. Content should be verified against the digest and - /// size. - /// This field is optional and uncommon. - /// - [DataMember(Name = "urls")] - public string[]? Urls { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +[DataContract] +public class ManifestLayer +{ + /// + /// The MIME type of the referenced object. This should generally be application/vnd.docker.image.rootfs.diff.tar.gzip. + /// Layers of type application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be pulled from a remote location but + /// they should never be pushed. + /// + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } + + /// + /// The size in bytes of the object. This field exists so that a client will have an expected size for the content + /// before validating. If the length of the retrieved content does not match the specified length, the content should + /// not be trusted. + /// + [DataMember(Name = "size")] + public long Size { get; set; } + + /// + /// The digest of the content, as defined by the Registry V2 HTTP API Specification. + /// + /// https://docs.docker.com/registry/spec/api/#digest-parameter + [DataMember(Name = "digest")] + public string? Digest { get; set; } + + /// + /// Provides a list of URLs from which the content may be fetched. Content should be verified against the digest and + /// size. + /// This field is optional and uncommon. + /// + [DataMember(Name = "urls")] + public string[]? Urls { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestList.cs b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestList.cs similarity index 95% rename from src/Docker.Registry.DotNet/Models/ManifestList.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/ManifestList.cs index ac5ad58..4bccda6 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestList.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestList.cs @@ -1,37 +1,37 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -/// -/// The manifest list is the “fat manifest” which points to specific image manifests for one or more platforms. Its use -/// is optional, and relatively few images will use one of these manifests. A client will distinguish a manifest list -/// from an image manifest based on the Content-Type returned in the HTTP response. -/// -public class ManifestList : ImageManifest -{ - /// - /// The MIME type of the manifest list. This should be set to - /// application/vnd.docker.distribution.manifest.list.v2+json. - /// - [DataMember(Name = "mediaType")] - public string? MediaType { get; set; } - - /// - /// The manifests field contains a list of manifests for specific platforms. - /// - [DataMember(Name = "manifests")] - public Manifest[]? Manifests { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +/// +/// The manifest list is the “fat manifest” which points to specific image manifests for one or more platforms. Its use +/// is optional, and relatively few images will use one of these manifests. A client will distinguish a manifest list +/// from an image manifest based on the Content-Type returned in the HTTP response. +/// +public class ManifestList : ImageManifest +{ + /// + /// The MIME type of the manifest list. This should be set to + /// application/vnd.docker.distribution.manifest.list.v2+json. + /// + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } + + /// + /// The manifests field contains a list of manifests for specific platforms. + /// + [DataMember(Name = "manifests")] + public Manifest[]? Manifests { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestMediaTypes.cs b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestMediaTypes.cs similarity index 95% rename from src/Docker.Registry.DotNet/Models/ManifestMediaTypes.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/ManifestMediaTypes.cs index e1c2143..b5d5627 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestMediaTypes.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestMediaTypes.cs @@ -1,65 +1,65 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; -// https://docs.docker.com/registry/spec/manifest-v2-1/#signed-manifests - -public static class ManifestMediaTypes -{ - /// - /// schema1 (existing manifest format). Note that “application/json” will also be accepted for schema 1. - /// - public const string ManifestSchema1 = - "application/vnd.docker.distribution.manifest.v1+json"; - - /// - /// schema1 (existing manifest format) signed. - /// - public const string ManifestSchema1Signed = - "application/vnd.docker.distribution.manifest.v1+prettyjws"; - - /// - /// New image manifest format (schemaVersion = 2) - /// - public const string ManifestSchema2 = - "application/vnd.docker.distribution.manifest.v2+json"; - - /// - /// Manifest list, aka “fat manifest” - /// - public const string ManifestList = - "application/vnd.docker.distribution.manifest.list.v2+json"; - - /// - /// Container config JSON - /// - public const string ContainerConfig = "application/vnd.docker.container.image.v1+json"; - - /// - /// “Layer”, as a gzipped tar - /// - public const string GzippedTar = "application/vnd.docker.image.rootfs.diff.tar.gzip"; - - /// - /// “Layer”, as a gzipped tar that should never be pushed - /// - public const string GzippedLayer = - "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"; - - /// - /// Plugin config JSON - /// - public const string PluginConfigJson = "application/vnd.docker.plugin.v1+json"; +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; +// https://docs.docker.com/registry/spec/manifest-v2-1/#signed-manifests + +public static class ManifestMediaTypes +{ + /// + /// schema1 (existing manifest format). Note that “application/json” will also be accepted for schema 1. + /// + public const string ManifestSchema1 = + "application/vnd.docker.distribution.manifest.v1+json"; + + /// + /// schema1 (existing manifest format) signed. + /// + public const string ManifestSchema1Signed = + "application/vnd.docker.distribution.manifest.v1+prettyjws"; + + /// + /// New image manifest format (schemaVersion = 2) + /// + public const string ManifestSchema2 = + "application/vnd.docker.distribution.manifest.v2+json"; + + /// + /// Manifest list, aka “fat manifest” + /// + public const string ManifestList = + "application/vnd.docker.distribution.manifest.list.v2+json"; + + /// + /// Container config JSON + /// + public const string ContainerConfig = "application/vnd.docker.container.image.v1+json"; + + /// + /// “Layer”, as a gzipped tar + /// + public const string GzippedTar = "application/vnd.docker.image.rootfs.diff.tar.gzip"; + + /// + /// “Layer”, as a gzipped tar that should never be pushed + /// + public const string GzippedLayer = + "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"; + + /// + /// Plugin config JSON + /// + public const string PluginConfigJson = "application/vnd.docker.plugin.v1+json"; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestSignature.cs b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestSignature.cs similarity index 93% rename from src/Docker.Registry.DotNet/Models/ManifestSignature.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/ManifestSignature.cs index cec302f..de38dbf 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestSignature.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestSignature.cs @@ -1,29 +1,29 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class ManifestSignature -{ - [DataMember(Name = "header")] - public ManifestSignatureHeader? Header { get; set; } - - [DataMember(Name = "signature")] - public string? Signature { get; set; } - - [DataMember(Name = "protected")] - public string? Protected { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +[DataContract] +public class ManifestSignature +{ + [DataMember(Name = "header")] + public ManifestSignatureHeader? Header { get; set; } + + [DataMember(Name = "signature")] + public string? Signature { get; set; } + + [DataMember(Name = "protected")] + public string? Protected { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ManifestSignatureHeader.cs b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestSignatureHeader.cs similarity index 92% rename from src/Docker.Registry.DotNet/Models/ManifestSignatureHeader.cs rename to src/Docker.Registry.DotNet/Domain/Manifests/ManifestSignatureHeader.cs index c204b45..1e37056 100644 --- a/src/Docker.Registry.DotNet/Models/ManifestSignatureHeader.cs +++ b/src/Docker.Registry.DotNet/Domain/Manifests/ManifestSignatureHeader.cs @@ -1,23 +1,23 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class ManifestSignatureHeader -{ - [DataMember(Name = "alg")] - public string? Alg { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Manifests; + +[DataContract] +public class ManifestSignatureHeader +{ + [DataMember(Name = "alg")] + public string? Alg { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/CompletedUploadResponse.cs b/src/Docker.Registry.DotNet/Domain/Models/CompletedUploadResponse.cs similarity index 96% rename from src/Docker.Registry.DotNet/Models/CompletedUploadResponse.cs rename to src/Docker.Registry.DotNet/Domain/Models/CompletedUploadResponse.cs index af3ffee..c09b467 100644 --- a/src/Docker.Registry.DotNet/Models/CompletedUploadResponse.cs +++ b/src/Docker.Registry.DotNet/Domain/Models/CompletedUploadResponse.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models; +namespace Docker.Registry.DotNet.Domain.Models; /// /// A completed upload response. diff --git a/src/Docker.Registry.DotNet/Models/Config.cs b/src/Docker.Registry.DotNet/Domain/Models/Config.cs similarity index 95% rename from src/Docker.Registry.DotNet/Models/Config.cs rename to src/Docker.Registry.DotNet/Domain/Models/Config.cs index 4488fae..7d0f73b 100644 --- a/src/Docker.Registry.DotNet/Models/Config.cs +++ b/src/Docker.Registry.DotNet/Domain/Models/Config.cs @@ -1,50 +1,50 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class Config -{ - /// - /// The MIME type of the referenced object. This should generally be application/vnd.docker.image.rootfs.diff.tar.gzip. - /// Layers of type application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be pulled from a remote location but - /// they should never be pushed. - /// - [DataMember(Name = "mediaType")] - public string? MediaType { get; set; } - - /// - /// The size in bytes of the object. This field exists so that a client will have an expected size for the content - /// before validating. If the length of the retrieved content does not match the specified length, the content should - /// not be trusted. - /// - [DataMember(Name = "size")] - public long Size { get; set; } - - /// - /// The digest of the content, as defined by the Registry V2 HTTP API Specificiation. - /// https://docs.docker.com/registry/spec/api/#digest-parameter - /// - [DataMember(Name = "digest")] - public string? Digest { get; set; } - - /// - /// Provides a list of URLs from which the content may be fetched. Content should be verified against the digest and - /// size. This field is optional and uncommon. - /// - [DataMember(Name = "urls")] - public string[]? Urls { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Models; + +[DataContract] +public class Config +{ + /// + /// The MIME type of the referenced object. This should generally be application/vnd.docker.image.rootfs.diff.tar.gzip. + /// Layers of type application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be pulled from a remote location but + /// they should never be pushed. + /// + [DataMember(Name = "mediaType")] + public string? MediaType { get; set; } + + /// + /// The size in bytes of the object. This field exists so that a client will have an expected size for the content + /// before validating. If the length of the retrieved content does not match the specified length, the content should + /// not be trusted. + /// + [DataMember(Name = "size")] + public long Size { get; set; } + + /// + /// The digest of the content, as defined by the Registry V2 HTTP API Specificiation. + /// https://docs.docker.com/registry/spec/api/#digest-parameter + /// + [DataMember(Name = "digest")] + public string? Digest { get; set; } + + /// + /// Provides a list of URLs from which the content may be fetched. Content should be verified against the digest and + /// size. This field is optional and uncommon. + /// + [DataMember(Name = "urls")] + public string[]? Urls { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/InitiateMonolithicUploadResponse.cs b/src/Docker.Registry.DotNet/Domain/Models/InitiateMonolithicUploadResponse.cs similarity index 92% rename from src/Docker.Registry.DotNet/Models/InitiateMonolithicUploadResponse.cs rename to src/Docker.Registry.DotNet/Domain/Models/InitiateMonolithicUploadResponse.cs index e1320c6..62ff0d7 100644 --- a/src/Docker.Registry.DotNet/Models/InitiateMonolithicUploadResponse.cs +++ b/src/Docker.Registry.DotNet/Domain/Models/InitiateMonolithicUploadResponse.cs @@ -1,25 +1,25 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -public class InitiateMonolithicUploadResponse -{ - public string? Location { get; set; } - - public int ContentLength { get; set; } - - public string? DockerUploadUuid { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Models; + +public class InitiateMonolithicUploadResponse +{ + public string? Location { get; set; } + + public int ContentLength { get; set; } + + public string? DockerUploadUuid { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/ListTagsParameters.cs b/src/Docker.Registry.DotNet/Domain/Models/ListTagsParameters.cs similarity index 94% rename from src/Docker.Registry.DotNet/Models/ListTagsParameters.cs rename to src/Docker.Registry.DotNet/Domain/Models/ListTagsParameters.cs index 934c9e7..dbfba23 100644 --- a/src/Docker.Registry.DotNet/Models/ListTagsParameters.cs +++ b/src/Docker.Registry.DotNet/Domain/Models/ListTagsParameters.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models; +namespace Docker.Registry.DotNet.Domain.Models; public class ListTagsParameters { diff --git a/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs b/src/Docker.Registry.DotNet/Domain/Models/ListTagsResponse.cs similarity index 93% rename from src/Docker.Registry.DotNet/Models/ListTagsResponse.cs rename to src/Docker.Registry.DotNet/Domain/Models/ListTagsResponse.cs index eefa136..450197f 100644 --- a/src/Docker.Registry.DotNet/Models/ListTagsResponse.cs +++ b/src/Docker.Registry.DotNet/Domain/Models/ListTagsResponse.cs @@ -13,9 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Docker.Registry.DotNet.Domain.Tags; +using Docker.Registry.DotNet.Domain.ImageReferences; -namespace Docker.Registry.DotNet.Models; +namespace Docker.Registry.DotNet.Domain.Models; [DataContract] internal class ListTagsResponseDto diff --git a/src/Docker.Registry.DotNet/Models/MountParameters.cs b/src/Docker.Registry.DotNet/Domain/Models/MountParameters.cs similarity index 93% rename from src/Docker.Registry.DotNet/Models/MountParameters.cs rename to src/Docker.Registry.DotNet/Domain/Models/MountParameters.cs index 64bc004..c60537e 100644 --- a/src/Docker.Registry.DotNet/Models/MountParameters.cs +++ b/src/Docker.Registry.DotNet/Domain/Models/MountParameters.cs @@ -1,30 +1,30 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class MountParameters -{ - /// - /// Digest of blob to mount from the source repository. - /// - public string? Digest { get; set; } - - /// - /// Name of the source repository. - /// - public string? From { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Models; + +[DataContract] +public class MountParameters +{ + /// + /// Digest of blob to mount from the source repository. + /// + public string? Digest { get; set; } + + /// + /// Name of the source repository. + /// + public string? From { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/MountResponse.cs b/src/Docker.Registry.DotNet/Domain/Models/MountResponse.cs similarity index 94% rename from src/Docker.Registry.DotNet/Models/MountResponse.cs rename to src/Docker.Registry.DotNet/Domain/Models/MountResponse.cs index 31bb065..3e1e079 100644 --- a/src/Docker.Registry.DotNet/Models/MountResponse.cs +++ b/src/Docker.Registry.DotNet/Domain/Models/MountResponse.cs @@ -1,35 +1,35 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -public class MountResponse -{ - /// - /// - /// - public string? Location { get; set; } - - /// - /// Identifies the docker upload uuid for the current request. - /// - public string? DockerUploadUuid { get; set; } - - /// - /// If the blob is successfully mounted Created is true,Otherwise, it is flse - /// If a mount fails due to invalid repository or digest arguments, the registry will fall back to the standard upload behavior And with the upload URL in the - /// - public bool Created { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Models; + +public class MountResponse +{ + /// + /// + /// + public string? Location { get; set; } + + /// + /// Identifies the docker upload uuid for the current request. + /// + public string? DockerUploadUuid { get; set; } + + /// + /// If the blob is successfully mounted Created is true,Otherwise, it is flse + /// If a mount fails due to invalid repository or digest arguments, the registry will fall back to the standard upload behavior And with the upload URL in the + /// + public bool Created { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/Platform.cs b/src/Docker.Registry.DotNet/Domain/Models/Platform.cs similarity index 95% rename from src/Docker.Registry.DotNet/Models/Platform.cs rename to src/Docker.Registry.DotNet/Domain/Models/Platform.cs index 339cd74..37034b2 100644 --- a/src/Docker.Registry.DotNet/Models/Platform.cs +++ b/src/Docker.Registry.DotNet/Domain/Models/Platform.cs @@ -1,59 +1,59 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -namespace Docker.Registry.DotNet.Models; - -[DataContract] -public class Platform -{ - /// - /// The architecture field specifies the CPU architecture, for example amd64 or ppc64le - /// - [DataMember(Name = "architecture")] - public string? Architecture { get; set; } - - /// - /// The os field specifies the operating system, for example linux or windows. - /// - [DataMember(Name = "os")] - public string? Os { get; set; } - - /// - /// The optional os.version field specifies the operating system version, for example 10.0.10586. - /// - [DataMember(Name = "os.version")] - public string? OsVersion { get; set; } - - /// - /// The optional os.features field specifies an array of strings, each listing a required OS feature (for example on - /// Windows win32k) - /// - [DataMember(Name = "os.features")] - public string? OsFeatures { get; set; } - - /// - /// The optional variant field specifies a variant of the CPU, for example armv6l to specify a particular CPU variant - /// of the ARM CPU. - /// - [DataMember(Name = "variant")] - public string? Variant { get; set; } - - /// - /// The optional features field specifies an array of strings, each listing a required CPU feature (for example sse4 or - /// aes). - /// - [DataMember(Name = "features")] - public string[]? Features { get; set; } +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Models; + +[DataContract] +public class Platform +{ + /// + /// The architecture field specifies the CPU architecture, for example amd64 or ppc64le + /// + [DataMember(Name = "architecture")] + public string? Architecture { get; set; } + + /// + /// The os field specifies the operating system, for example linux or windows. + /// + [DataMember(Name = "os")] + public string? Os { get; set; } + + /// + /// The optional os.version field specifies the operating system version, for example 10.0.10586. + /// + [DataMember(Name = "os.version")] + public string? OsVersion { get; set; } + + /// + /// The optional os.features field specifies an array of strings, each listing a required OS feature (for example on + /// Windows win32k) + /// + [DataMember(Name = "os.features")] + public string? OsFeatures { get; set; } + + /// + /// The optional variant field specifies a variant of the CPU, for example armv6l to specify a particular CPU variant + /// of the ARM CPU. + /// + [DataMember(Name = "variant")] + public string? Variant { get; set; } + + /// + /// The optional features field specifies an array of strings, each listing a required CPU feature (for example sse4 or + /// aes). + /// + [DataMember(Name = "features")] + public string[]? Features { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Models/PushManifestResponse.cs b/src/Docker.Registry.DotNet/Domain/Models/PushManifestResponse.cs similarity index 96% rename from src/Docker.Registry.DotNet/Models/PushManifestResponse.cs rename to src/Docker.Registry.DotNet/Domain/Models/PushManifestResponse.cs index 1bdf80c..f0b42ab 100644 --- a/src/Docker.Registry.DotNet/Models/PushManifestResponse.cs +++ b/src/Docker.Registry.DotNet/Domain/Models/PushManifestResponse.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models; +namespace Docker.Registry.DotNet.Domain.Models; public class PushManifestResponse { diff --git a/src/Docker.Registry.DotNet/Models/ResumableUpload.cs b/src/Docker.Registry.DotNet/Domain/Models/ResumableUpload.cs similarity index 96% rename from src/Docker.Registry.DotNet/Models/ResumableUpload.cs rename to src/Docker.Registry.DotNet/Domain/Models/ResumableUpload.cs index eb8ea3c..3e65237 100644 --- a/src/Docker.Registry.DotNet/Models/ResumableUpload.cs +++ b/src/Docker.Registry.DotNet/Domain/Models/ResumableUpload.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Models; +namespace Docker.Registry.DotNet.Domain.Models; /// /// A resumable upload response. diff --git a/src/Docker.Registry.DotNet/QueryParameters/QueryParameterAttribute.cs b/src/Docker.Registry.DotNet/Domain/QueryParameters/QueryParameterAttribute.cs similarity index 93% rename from src/Docker.Registry.DotNet/QueryParameters/QueryParameterAttribute.cs rename to src/Docker.Registry.DotNet/Domain/QueryParameters/QueryParameterAttribute.cs index d1154a0..028189f 100644 --- a/src/Docker.Registry.DotNet/QueryParameters/QueryParameterAttribute.cs +++ b/src/Docker.Registry.DotNet/Domain/QueryParameters/QueryParameterAttribute.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.QueryParameters; +namespace Docker.Registry.DotNet.Domain.QueryParameters; [AttributeUsage(AttributeTargets.Property)] internal class QueryParameterAttribute(string key) : Attribute diff --git a/src/Docker.Registry.DotNet/Helpers/IReadOnlyQueryString.cs b/src/Docker.Registry.DotNet/Domain/QueryStrings/IReadOnlyQueryString.cs similarity index 93% rename from src/Docker.Registry.DotNet/Helpers/IReadOnlyQueryString.cs rename to src/Docker.Registry.DotNet/Domain/QueryStrings/IReadOnlyQueryString.cs index 559b294..8e0c897 100644 --- a/src/Docker.Registry.DotNet/Helpers/IReadOnlyQueryString.cs +++ b/src/Docker.Registry.DotNet/Domain/QueryStrings/IReadOnlyQueryString.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Helpers; +namespace Docker.Registry.DotNet.Domain.QueryStrings; internal interface IReadOnlyQueryString { diff --git a/src/Docker.Registry.DotNet/Registry/IRegistryClient.cs b/src/Docker.Registry.DotNet/Domain/Registry/IRegistryClient.cs similarity index 68% rename from src/Docker.Registry.DotNet/Registry/IRegistryClient.cs rename to src/Docker.Registry.DotNet/Domain/Registry/IRegistryClient.cs index 22f2b24..c7606ec 100644 --- a/src/Docker.Registry.DotNet/Registry/IRegistryClient.cs +++ b/src/Docker.Registry.DotNet/Domain/Registry/IRegistryClient.cs @@ -13,9 +13,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Docker.Registry.DotNet.Endpoints.Implementations; -namespace Docker.Registry.DotNet.Registry; +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Registry; /// /// The registry client. diff --git a/src/Docker.Registry.DotNet/Domain/Registry/IRegistryUriBuilder.cs b/src/Docker.Registry.DotNet/Domain/Registry/IRegistryUriBuilder.cs new file mode 100644 index 0000000..f2c7d85 --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Registry/IRegistryUriBuilder.cs @@ -0,0 +1,63 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/* Unmerged change from project 'Docker.Registry.DotNet (netstandard2.0)' +Before: +namespace Docker.Registry.DotNet.Registry; +After: +using Docker; +using Docker.Registry; +using Docker.Registry.DotNet; +using Docker.Registry.DotNet.Domain.Registry; +using Docker.Registry.DotNet.Registry; + +namespace Docker.Registry.DotNet.Registry; +*/ + +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Domain.QueryStrings; + +namespace Docker.Registry.DotNet.Domain.Registry; + +public interface IRegistryUriBuilder +{ + Uri Build(string? path = null, string? queryString = null); +} + +internal static class RegistryUriBuilderExtensions +{ + public static Uri Build( + this IRegistryUriBuilder uriBuilder, + string? path = null, + IReadOnlyQueryString? queryString = null) + { + return uriBuilder.Build(path, queryString?.GetQueryString()); + } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Registry/RegistryApiException.cs b/src/Docker.Registry.DotNet/Domain/Registry/RegistryApiException.cs similarity index 58% rename from src/Docker.Registry.DotNet/Registry/RegistryApiException.cs rename to src/Docker.Registry.DotNet/Domain/Registry/RegistryApiException.cs index 6e33856..e332380 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryApiException.cs +++ b/src/Docker.Registry.DotNet/Domain/Registry/RegistryApiException.cs @@ -13,15 +13,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Registry; + +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Registry; public class RegistryApiException : Exception { internal RegistryApiException(RegistryApiResponse response) : base($"Docker API responded with status code={response.StatusCode}") { - this.StatusCode = response.StatusCode; - this.Headers = response.Headers; + StatusCode = response.StatusCode; + Headers = response.Headers; } public HttpStatusCode StatusCode { get; } @@ -34,7 +50,7 @@ public class RegistryApiException : RegistryApiException internal RegistryApiException(RegistryApiResponse response) : base(response) { - this.Body = response.Body; + Body = response.Body; } public TBody Body { get; } diff --git a/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs b/src/Docker.Registry.DotNet/Domain/Registry/RegistryApiResponse.cs similarity index 53% rename from src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs rename to src/Docker.Registry.DotNet/Domain/Registry/RegistryApiResponse.cs index 9f1158f..b4df85b 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryApiResponse.cs +++ b/src/Docker.Registry.DotNet/Domain/Registry/RegistryApiResponse.cs @@ -13,7 +13,36 @@ // See the License for the specific language governing permissions and // limitations under the License. + +/* Unmerged change from project 'Docker.Registry.DotNet (netstandard2.0)' +Before: +namespace Docker.Registry.DotNet.Registry; +After: +using Docker; +using Docker.Registry; +using Docker.Registry.DotNet; +using Docker.Registry.DotNet.Domain.Registry; +using Docker.Registry.DotNet.Registry; + namespace Docker.Registry.DotNet.Registry; +*/ + +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Registry; internal abstract class RegistryApiResponse(HttpStatusCode statusCode, HttpResponseHeaders headers) { @@ -27,7 +56,7 @@ internal class RegistryApiResponse : RegistryApiResponse internal RegistryApiResponse(HttpStatusCode statusCode, TBody? body, HttpResponseHeaders headers) : base(statusCode, headers) { - this.Body = body; + Body = body; } public TBody? Body { get; } diff --git a/src/Docker.Registry.DotNet/Registry/RegistryConnectionException.cs b/src/Docker.Registry.DotNet/Domain/Registry/RegistryConnectionException.cs similarity index 95% rename from src/Docker.Registry.DotNet/Registry/RegistryConnectionException.cs rename to src/Docker.Registry.DotNet/Domain/Registry/RegistryConnectionException.cs index c397da4..b8af14f 100644 --- a/src/Docker.Registry.DotNet/Registry/RegistryConnectionException.cs +++ b/src/Docker.Registry.DotNet/Domain/Registry/RegistryConnectionException.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Registry; +namespace Docker.Registry.DotNet.Domain.Registry; /// /// Thrown when connecting to a registry fails. diff --git a/src/Docker.Registry.DotNet/Registry/UnauthorizedApiException.cs b/src/Docker.Registry.DotNet/Domain/Registry/UnauthorizedApiException.cs similarity index 56% rename from src/Docker.Registry.DotNet/Registry/UnauthorizedApiException.cs rename to src/Docker.Registry.DotNet/Domain/Registry/UnauthorizedApiException.cs index e309aa0..48f972b 100644 --- a/src/Docker.Registry.DotNet/Registry/UnauthorizedApiException.cs +++ b/src/Docker.Registry.DotNet/Domain/Registry/UnauthorizedApiException.cs @@ -13,7 +13,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Registry; + +// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Registry; /// /// Thrown when an api response is returned as unauthorized. diff --git a/src/Docker.Registry.DotNet/Domain/Repository/ListRepositoryTagsResponse.cs b/src/Docker.Registry.DotNet/Domain/Repository/ListRepositoryTagsResponse.cs new file mode 100644 index 0000000..b3e6325 --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Repository/ListRepositoryTagsResponse.cs @@ -0,0 +1,16 @@ +namespace Docker.Registry.DotNet.Domain.Repository; + +public class ListRepositoryTagsResponse +{ + [JsonProperty("count")] + public int Count { get; set; } + + [JsonProperty("next")] + public string Next { get; set; } + + [JsonProperty("previous")] + public object Previous { get; set; } + + [JsonProperty("results")] + public List Tags { get; set; } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTag.cs b/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTag.cs new file mode 100644 index 0000000..9d2ef5a --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTag.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json.Converters; + +namespace Docker.Registry.DotNet.Domain.Repository; + +public class RepositoryTag +{ + [JsonProperty("creator")] + public int Creator { get; set; } + + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("images")] + public List Images { get; set; } + + [JsonProperty("last_updated")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime LastUpdated { get; set; } + + [JsonProperty("last_updater")] + public int LastUpdater { get; set; } + + [JsonProperty("last_updater_username")] + public string LastUpdaterUsername { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("repository")] + public int Repository { get; set; } + + [JsonProperty("full_size")] + public long FullSize { get; set; } + + [JsonProperty("v2")] + public bool V2 { get; set; } + + [JsonProperty("tag_status")] + public string TagStatus { get; set; } + + [JsonProperty("tag_last_pulled")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime TagLastPulled { get; set; } + + [JsonProperty("tag_last_pushed")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime TagLastPushed { get; set; } + + [JsonProperty("media_type")] + public string MediaType { get; set; } + + [JsonProperty("content_type")] + public string ContentType { get; set; } + + [JsonProperty("digest")] + public string Digest { get; set; } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagImage.cs b/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagImage.cs new file mode 100644 index 0000000..6d03e68 --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagImage.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json.Converters; + +namespace Docker.Registry.DotNet.Domain.Repository; + +public class RepositoryTagImage +{ + [JsonProperty("architecture")] + public string Architecture { get; set; } + + [JsonProperty("features")] + public string Features { get; set; } + + [JsonProperty("variant")] + public object Variant { get; set; } + + [JsonProperty("digest")] + public string Digest { get; set; } + + [JsonProperty("os")] + public string Os { get; set; } + + [JsonProperty("os_features")] + public string OsFeatures { get; set; } + + [JsonProperty("os_version")] + public object OsVersion { get; set; } + + [JsonProperty("size")] + public long Size { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("last_pulled")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime LastPulled { get; set; } + + [JsonProperty("last_pushed")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime LastPushed { get; set; } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagsParameters.cs b/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagsParameters.cs new file mode 100644 index 0000000..f417350 --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagsParameters.cs @@ -0,0 +1,17 @@ +namespace Docker.Registry.DotNet.Domain.Repository; + +[PublicAPI] +public class RepositoryTagsParameters +{ + /// + /// Current page. + /// + [QueryParameter("page")] + public int Page { get; set; } = 1; + + /// + /// Page Size -- max is 100 + /// + [QueryParameter("page_size")] + public int PageSize { get; set; } = 10; +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs b/src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs deleted file mode 100644 index ff02fa9..0000000 --- a/src/Docker.Registry.DotNet/Endpoints/Implementations/RepositoryOperations.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using Newtonsoft.Json.Converters; - -namespace Docker.Registry.DotNet.Endpoints.Implementations; - -/// -/// Operations on the Docker Repository. -/// -public interface IRepositoryOperations -{ - Task ListRepositoryTags( - string @namespace, - string repository, - RepositoryTagsParameters? parameters = null, - CancellationToken token = default); -} - -internal class RepositoryOperations(NetworkClient client) : IRepositoryOperations -{ - public async Task ListRepositoryTags( - string @namespace, - string repository, - RepositoryTagsParameters? parameters = null, - CancellationToken token = default) - { - var queryString = new QueryString(); - queryString.AddFromObject(parameters ?? new RepositoryTagsParameters()); - - var response = await client.MakeRequest( - HttpMethod.Get, - $"{client.RegistryVersion}/namespaces/{@namespace}/repositories/{repository}/tags", - queryString, - token: token); - - return client.JsonSerializer.DeserializeObject(response.Body); - } -} - -[PublicAPI] -public class RepositoryTagsParameters -{ - /// - /// Current page. - /// - [QueryParameter("page")] - public int Page { get; set; } = 1; - - /// - /// Page Size -- max is 100 - /// - [QueryParameter("page_size")] - public int PageSize { get; set; } = 10; -} - -public class ListRepositoryTagsResponse -{ - [JsonProperty("count")] - public int Count { get; set; } - - [JsonProperty("next")] - public string Next { get; set; } - - [JsonProperty("previous")] - public object Previous { get; set; } - - [JsonProperty("results")] - public List Tags { get; set; } -} - -public class RepositoryTag -{ - [JsonProperty("creator")] - public int Creator { get; set; } - - [JsonProperty("id")] - public int Id { get; set; } - - [JsonProperty("images")] - public List Images { get; set; } - - [JsonProperty("last_updated")] - [JsonConverter(typeof(IsoDateTimeConverter))] - public DateTime LastUpdated { get; set; } - - [JsonProperty("last_updater")] - public int LastUpdater { get; set; } - - [JsonProperty("last_updater_username")] - public string LastUpdaterUsername { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("repository")] - public int Repository { get; set; } - - [JsonProperty("full_size")] - public long FullSize { get; set; } - - [JsonProperty("v2")] - public bool V2 { get; set; } - - [JsonProperty("tag_status")] - public string TagStatus { get; set; } - - [JsonProperty("tag_last_pulled")] - [JsonConverter(typeof(IsoDateTimeConverter))] - public DateTime TagLastPulled { get; set; } - - [JsonProperty("tag_last_pushed")] - [JsonConverter(typeof(IsoDateTimeConverter))] - public DateTime TagLastPushed { get; set; } - - [JsonProperty("media_type")] - public string MediaType { get; set; } - - [JsonProperty("content_type")] - public string ContentType { get; set; } - - [JsonProperty("digest")] - public string Digest { get; set; } -} - -public class TagImage -{ - [JsonProperty("architecture")] - public string Architecture { get; set; } - - [JsonProperty("features")] - public string Features { get; set; } - - [JsonProperty("variant")] - public object Variant { get; set; } - - [JsonProperty("digest")] - public string Digest { get; set; } - - [JsonProperty("os")] - public string Os { get; set; } - - [JsonProperty("os_features")] - public string OsFeatures { get; set; } - - [JsonProperty("os_version")] - public object OsVersion { get; set; } - - [JsonProperty("size")] - public long Size { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("last_pulled")] - [JsonConverter(typeof(IsoDateTimeConverter))] - public DateTime LastPulled { get; set; } - - [JsonProperty("last_pushed")] - [JsonConverter(typeof(IsoDateTimeConverter))] - public DateTime LastPushed { get; set; } -} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/GlobalUsings.cs b/src/Docker.Registry.DotNet/GlobalUsings.cs index 9f8e4fe..75e8af2 100644 --- a/src/Docker.Registry.DotNet/GlobalUsings.cs +++ b/src/Docker.Registry.DotNet/GlobalUsings.cs @@ -14,25 +14,22 @@ // limitations under the License. global using System; -global using System.Collections.Generic; -global using System.IO; -global using System.Linq; +global using System.Diagnostics; global using System.Net; -global using System.Net.Http; global using System.Net.Http.Headers; global using System.Runtime.Serialization; global using System.Text; -global using System.Threading; -global using System.Threading.Tasks; -global using Docker.Registry.DotNet.Authentication; -global using Docker.Registry.DotNet.Endpoints; -global using Docker.Registry.DotNet.Helpers; -global using Docker.Registry.DotNet.Models; -global using Docker.Registry.DotNet.OAuth; -global using Docker.Registry.DotNet.QueryParameters; -global using Docker.Registry.DotNet.Registry; +global using Docker.Registry.DotNet.Application.Authentication; +global using Docker.Registry.DotNet.Application.Registry; +global using Docker.Registry.DotNet.Domain.Endpoints; +global using Docker.Registry.DotNet.Domain.Models; +global using Docker.Registry.DotNet.Domain.QueryParameters; +global using Docker.Registry.DotNet.Domain.Registry; +global using Docker.Registry.DotNet.Infrastructure.Helpers; global using JetBrains.Annotations; -global using Newtonsoft.Json; \ No newline at end of file +global using Newtonsoft.Json; + +global using JsonSerializer = Docker.Registry.DotNet.Infrastructure.Json.JsonSerializer; \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Helpers/CharHelpers.cs b/src/Docker.Registry.DotNet/Infrastructure/Helpers/CharHelpers.cs similarity index 94% rename from src/Docker.Registry.DotNet/Helpers/CharHelpers.cs rename to src/Docker.Registry.DotNet/Infrastructure/Helpers/CharHelpers.cs index 74afc7e..91b0fbd 100644 --- a/src/Docker.Registry.DotNet/Helpers/CharHelpers.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Helpers/CharHelpers.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Helpers; +namespace Docker.Registry.DotNet.Infrastructure.Helpers; internal static class CharHelpers { diff --git a/src/Docker.Registry.DotNet/Helpers/CollectionExtensions.cs b/src/Docker.Registry.DotNet/Infrastructure/Helpers/CollectionExtensions.cs similarity index 93% rename from src/Docker.Registry.DotNet/Helpers/CollectionExtensions.cs rename to src/Docker.Registry.DotNet/Infrastructure/Helpers/CollectionExtensions.cs index ed35571..13212ad 100644 --- a/src/Docker.Registry.DotNet/Helpers/CollectionExtensions.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Helpers/CollectionExtensions.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Helpers; +namespace Docker.Registry.DotNet.Infrastructure.Helpers; internal static class CollectionExtensions { diff --git a/src/Docker.Registry.DotNet/Helpers/DictionaryExtensions.cs b/src/Docker.Registry.DotNet/Infrastructure/Helpers/DictionaryExtensions.cs similarity index 95% rename from src/Docker.Registry.DotNet/Helpers/DictionaryExtensions.cs rename to src/Docker.Registry.DotNet/Infrastructure/Helpers/DictionaryExtensions.cs index 2449338..63f41ea 100644 --- a/src/Docker.Registry.DotNet/Helpers/DictionaryExtensions.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Helpers/DictionaryExtensions.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Helpers; +namespace Docker.Registry.DotNet.Infrastructure.Helpers; internal static class DictionaryExtensions { diff --git a/src/Docker.Registry.DotNet/Helpers/EnumerableExtensions.cs b/src/Docker.Registry.DotNet/Infrastructure/Helpers/EnumerableExtensions.cs similarity index 93% rename from src/Docker.Registry.DotNet/Helpers/EnumerableExtensions.cs rename to src/Docker.Registry.DotNet/Infrastructure/Helpers/EnumerableExtensions.cs index 5b750ab..885e063 100644 --- a/src/Docker.Registry.DotNet/Helpers/EnumerableExtensions.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Helpers/EnumerableExtensions.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Helpers; +namespace Docker.Registry.DotNet.Infrastructure.Helpers; public static class EnumerableExtensions { diff --git a/src/Docker.Registry.DotNet/Helpers/HttpContentHelper.cs b/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpContentHelper.cs similarity index 95% rename from src/Docker.Registry.DotNet/Helpers/HttpContentHelper.cs rename to src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpContentHelper.cs index d27aebd..7b2d95a 100644 --- a/src/Docker.Registry.DotNet/Helpers/HttpContentHelper.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpContentHelper.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Helpers; +namespace Docker.Registry.DotNet.Infrastructure.Helpers; public static class HttpContentHelper { diff --git a/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs b/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs similarity index 97% rename from src/Docker.Registry.DotNet/Helpers/HttpUtility.cs rename to src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs index c93ea70..73008c4 100644 --- a/src/Docker.Registry.DotNet/Helpers/HttpUtility.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs @@ -13,7 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Helpers; +using Docker.Registry.DotNet.Domain.QueryStrings; + +namespace Docker.Registry.DotNet.Infrastructure.Helpers; internal static class HttpUtility { diff --git a/src/Docker.Registry.DotNet/Helpers/StringExtensions.cs b/src/Docker.Registry.DotNet/Infrastructure/Helpers/StringExtensions.cs similarity index 95% rename from src/Docker.Registry.DotNet/Helpers/StringExtensions.cs rename to src/Docker.Registry.DotNet/Infrastructure/Helpers/StringExtensions.cs index 13c8af8..6d732f0 100644 --- a/src/Docker.Registry.DotNet/Helpers/StringExtensions.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Helpers/StringExtensions.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Docker.Registry.DotNet.Helpers; +namespace Docker.Registry.DotNet.Infrastructure.Helpers; public static class StringExtensions { diff --git a/src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs b/src/Docker.Registry.DotNet/Infrastructure/Json/JsonSerializer.cs similarity index 96% rename from src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs rename to src/Docker.Registry.DotNet/Infrastructure/Json/JsonSerializer.cs index 54ec724..83393da 100644 --- a/src/Docker.Registry.DotNet/Helpers/JsonSerializer.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Json/JsonSerializer.cs @@ -15,7 +15,7 @@ using Newtonsoft.Json.Converters; -namespace Docker.Registry.DotNet.Helpers; +namespace Docker.Registry.DotNet.Infrastructure.Json; /// /// Facade for . diff --git a/src/Docker.Registry.DotNet/Registry/RegistryClient.cs b/src/Docker.Registry.DotNet/Registry/RegistryClient.cs deleted file mode 100644 index 717464e..0000000 --- a/src/Docker.Registry.DotNet/Registry/RegistryClient.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using Docker.Registry.DotNet.Endpoints.Implementations; - -namespace Docker.Registry.DotNet.Registry; - -internal sealed class RegistryClient : IRegistryClient -{ - private readonly NetworkClient _client; - - public RegistryClient( - RegistryClientConfiguration configuration, - AuthenticationProvider authenticationProvider) - { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - - if (authenticationProvider == null) - throw new ArgumentNullException(nameof(authenticationProvider)); - - _client = new NetworkClient(configuration, authenticationProvider); - - this.Manifest = new ManifestOperations(_client); - this.Catalog = new CatalogOperations(_client); - this.Blobs = new BlobOperations(_client); - this.BlobUploads = new BlobUploadOperations(_client); - this.System = new SystemOperations(_client); - this.Tags = new TagOperations(_client); - this.Repository = new RepositoryOperations(this._client); - } - - public IRepositoryOperations Repository { get; set; } - - public IBlobUploadOperations BlobUploads { get; } - - public IManifestOperations Manifest { get; } - - public ICatalogOperations Catalog { get; } - - public IBlobOperations Blobs { get; } - - public ITagOperations Tags { get; } - - public ISystemOperations System { get; } - - public void Dispose() - { - _client?.Dispose(); - } -} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs index bdf4bd9..ebd7fcb 100644 --- a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs +++ b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs @@ -1,4 +1,4 @@ -// Copyright 2017-2022 Rich Quackenbush, Jaben Cargman +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman // and Docker.Registry.DotNet Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,68 +17,113 @@ namespace Docker.Registry.DotNet; public class RegistryClientConfiguration { - /// - /// Creates an instance of the RegistryClientConfiguration. - /// - /// - /// - public RegistryClientConfiguration(string host, TimeSpan defaultTimeout = default) - : this(defaultTimeout) + private TimeSpan _defaultTimeout = TimeSpan.FromSeconds(100); + + public RegistryClientConfiguration(string baseAddress, TimeSpan defaultTimeout = default) { - if (string.IsNullOrWhiteSpace(host)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(host)); + var valid = Uri.TryCreate(baseAddress, UriKind.Absolute, out var parsedBaseAddress); - this.Host = host; + if (valid) this.SetBaseAddress(parsedBaseAddress); + else throw new ArgumentException("BaseAddress is not a valid Uri", nameof(baseAddress)); + + this.SetDefaultTimeout(defaultTimeout); } - /// - /// Creates an instance of the RegistryClientConfiguration. - /// - /// - /// - /// public RegistryClientConfiguration( - string host, - HttpMessageHandler? httpMessageHandler, + Uri baseAddress, + HttpMessageHandler? httpMessageHandler = null, TimeSpan defaultTimeout = default) - : this(defaultTimeout) { - if (string.IsNullOrWhiteSpace(host)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(host)); + this.SetBaseAddress(baseAddress); + this.SetDefaultTimeout(defaultTimeout); + this.SetHttpMessageHandler(httpMessageHandler); + } - this.Host = host; - this.HttpMessageHandler = httpMessageHandler; + public RegistryClientConfiguration() + { } - private RegistryClientConfiguration(TimeSpan defaultTimeout) + public Uri? BaseAddress { get; private set; } + + public HttpMessageHandler? HttpMessageHandler { get; private set; } + + /// + /// Defaults to AnonymousOAuthAuthenticationProvider + /// + public AuthenticationProvider AuthenticationProvider { get; private set; } = + new AnonymousOAuthAuthenticationProvider(); + + public TimeSpan DefaultTimeout { - if (defaultTimeout != TimeSpan.Zero) + get => this._defaultTimeout; + private set { - if (defaultTimeout < Timeout.InfiniteTimeSpan) - // TODO: Should be a resource for localization. - // TODO: Is this a good message? - throw new ArgumentException( - "Timeout must be greater than Timeout.Infinite", - nameof(defaultTimeout)); - this.DefaultTimeout = defaultTimeout; + if (value != this._defaultTimeout && value != default) + { + if (value < Timeout.InfiniteTimeSpan) + throw new ArgumentException( + "Timeout must be less than Timeout.Infinite", + nameof(this.DefaultTimeout)); + + this._defaultTimeout = value; + } } } - public string Host { get; } + public RegistryClientConfiguration SetBaseAddress(Uri baseAddress) + { + if (baseAddress == null) + throw new ArgumentException("BaseAddress cannot be null.", nameof(this.BaseAddress)); - public HttpMessageHandler? HttpMessageHandler { get; } + if (baseAddress.Scheme is not ("http" or "https")) + { + throw new ArgumentOutOfRangeException( + nameof(BaseAddress), + "Base Address Uri must start with http:// or https://"); + } - public TimeSpan DefaultTimeout { get; internal set; } = TimeSpan.FromSeconds(100); + this.BaseAddress = baseAddress; - [PublicAPI] - public IRegistryClient CreateClient() + return this; + } + + public RegistryClientConfiguration SetDefaultTimeout(TimeSpan defaultTimeout) + { + this.DefaultTimeout = defaultTimeout; + + return this; + } + + public RegistryClientConfiguration SetHttpMessageHandler(HttpMessageHandler? messageHandler) + { + this.HttpMessageHandler = messageHandler; + + return this; + } + + /// + /// Defaults to AnonymousOAuthAuthenticationProvider + /// + public RegistryClientConfiguration SetAuthenticationProvider( + AuthenticationProvider authenticationProvider) { - return new RegistryClient(this, new AnonymousOAuthAuthenticationProvider()); + this.AuthenticationProvider = authenticationProvider + ?? throw new ArgumentNullException( + nameof(authenticationProvider)); + + return this; } - [PublicAPI] - public IRegistryClient CreateClient(AuthenticationProvider authenticationProvider) + private void RunValidationRules() { - return new RegistryClient(this, authenticationProvider); + if (this.BaseAddress == null) + throw new ArgumentException("BaseAddress cannot be null.", nameof(this.BaseAddress)); + } + + public IRegistryClient CreateClient() + { + this.RunValidationRules(); + + return new RegistryClient(this, this.AuthenticationProvider); } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/RegistryClientConfigurationExtensions.cs b/src/Docker.Registry.DotNet/RegistryClientConfigurationExtensions.cs new file mode 100644 index 0000000..d6df192 --- /dev/null +++ b/src/Docker.Registry.DotNet/RegistryClientConfigurationExtensions.cs @@ -0,0 +1,82 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet; + +public static class RegistryClientConfigurationExtensions +{ + public static RegistryClientConfiguration UseBasicAuthentication( + this RegistryClientConfiguration configuration, + string username, + string password) + { + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + + configuration.SetAuthenticationProvider( + new PasswordOAuthAuthenticationProvider(username, password)); + + return configuration; + } + + public static RegistryClientConfiguration UsePasswordOAuthAuthentication( + this RegistryClientConfiguration configuration, + string username, + string password) + { + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + + configuration.SetAuthenticationProvider( + new PasswordOAuthAuthenticationProvider(username, password)); + + return configuration; + } + + /// + /// Supports Docker Hub Authentication. + /// + /// + /// + /// + /// + /// + public static RegistryClientConfiguration UseDockerHubAuthentication( + this RegistryClientConfiguration configuration, + string username, + string password) + { + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + + configuration.SetAuthenticationProvider( + new DockerHubJwtAuthenticationProvider(username, password)); + + return configuration; + } + + /// + /// Default authentication provider. + /// + /// + /// + /// + public static RegistryClientConfiguration UseAnonymousOAuthAuthentication( + this RegistryClientConfiguration configuration) + { + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + + configuration.SetAuthenticationProvider(new AnonymousOAuthAuthenticationProvider()); + + return configuration; + } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/RegistryClientLegacyHelpers.cs b/src/Docker.Registry.DotNet/RegistryClientLegacyHelpers.cs new file mode 100644 index 0000000..151acdd --- /dev/null +++ b/src/Docker.Registry.DotNet/RegistryClientLegacyHelpers.cs @@ -0,0 +1,248 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Docker.Registry.DotNet.Domain.Blobs; +using Docker.Registry.DotNet.Domain.Catalogs; +using Docker.Registry.DotNet.Domain.ImageReferences; +using Docker.Registry.DotNet.Domain.Manifests; + +namespace Docker.Registry.DotNet; + +public static class RegistryClientLegacyHelpers +{ + [Obsolete("Use Ping() instead")] + public static Task PingAsync( + this ISystemOperations operations, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.Ping(token); + } + + [Obsolete("Use BlobExists() instead")] + public static Task IsExistBlobAsync( + this IBlobOperations operations, + string name, + string digest, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.BlobExists(name, digest, token); + } + + [Obsolete("Use DeleteBlob() instead")] + public static Task DeleteBlobAsync( + this IBlobOperations operations, + string name, + string digest, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.DeleteBlob(name, digest, token); + } + + [Obsolete("Use GetBlob() instead")] + public static Task GetBlobAsync( + this IBlobOperations operations, + string name, + string digest, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.GetBlob(name, digest, token); + } + + [Obsolete("Use GetCatalog() instead")] + public static Task GetCatalogAsync( + this ICatalogOperations operations, + CatalogParameters? parameters = null, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.GetCatalog(parameters ?? CatalogParameters.Empty, token); + } + + [Obsolete("Use GetManifest() instead")] + public static Task GetManifestAsync( + this IManifestOperations operations, + string name, + string reference, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.GetManifest(name, ImageReference.Create(reference), token); + } + + [Obsolete("Use DeleteManifest() instead")] + public static Task DeleteManifestAsync( + this IManifestOperations operations, + string name, + string reference, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.DeleteManifest(name, ImageReference.Create(reference), token); + } + + [Obsolete("Use PutManifest() instead")] + public static Task PutManifestAsync( + this IManifestOperations operations, + string name, + string reference, + ImageManifest manifest, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.PutManifest(name, ImageReference.Create(reference), manifest, token); + } + + [Obsolete("Use GetManifestRaw() instead")] + public static Task GetManifestRawAsync( + this IManifestOperations operations, + string name, + string reference, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.GetManifestRaw(name, ImageReference.Create(reference), token); + } + + [Obsolete("Use ListTags() instead")] + public static Task ListImageTagsAsync( + this ITagOperations operations, + string name, + ListTagsParameters? parameters = null, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.ListTags(name, parameters, token); + } + + [Obsolete("Use MonolithicUploadBlob() instead")] + public static Task MonolithicUploadBlobAsync( + this IBlobUploadOperations operations, + ResumableUpload resumable, + string digest, + Stream stream, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.MonolithicUploadBlob( + resumable, + ImageDigest.Create(digest), + stream, + token); + } + + [Obsolete("Use UploadBlob() instead")] + public static Task UploadBlobAsync( + this IBlobUploadOperations operations, + string name, + int contentLength, + Stream stream, + string digest, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.UploadBlob( + name, + contentLength, + stream, + ImageDigest.Create(digest), + token); + } + + [Obsolete("Use MountBlob() instead")] + public static Task MountBlobAsync( + this IBlobUploadOperations operations, + string name, + MountParameters parameters, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.MountBlob(name, parameters, token); + } + + [Obsolete("Use UploadBlobChunk() instead")] + public static Task UploadBlobChunkAsync( + this IBlobUploadOperations operations, + ResumableUpload resumable, + Stream chunk, + long? from = null, + long? to = null, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.UploadBlobChunk(resumable, chunk, from, to, token); + } + + [Obsolete("Use CompleteBlobUpload() instead")] + public static Task CompleteBlobUploadAsync( + this IBlobUploadOperations operations, + ResumableUpload resumable, + string digest, + Stream chunk, + long? from = null, + long? to = null, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.CompleteBlobUpload( + resumable, + ImageDigest.Create(digest), + chunk, + from, + to, + token); + } + + [Obsolete("Use CancelBlobUpload() instead")] + public static Task CompleteBlobUploadAsync( + this IBlobUploadOperations operations, + string name, + string uuid, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.CancelBlobUpload(name, uuid, token); + } + + [Obsolete("Use StartUploadBlob() instead")] + public static Task StartUploadBlobAsync( + this IBlobUploadOperations operations, + string name, + CancellationToken token = default) + { + if (operations == null) throw new ArgumentNullException(nameof(operations)); + + return operations.StartUploadBlob(name, token); + } +} \ No newline at end of file diff --git a/test/Docker.Registry.DotNet.Tests/Authentication/AuthenticateParserTests.cs b/test/Docker.Registry.DotNet.Tests/Authentication/AuthenticateParserTests.cs index 432147a..ac37d9d 100644 --- a/test/Docker.Registry.DotNet.Tests/Authentication/AuthenticateParserTests.cs +++ b/test/Docker.Registry.DotNet.Tests/Authentication/AuthenticateParserTests.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Docker.Registry.DotNet.Authentication; +using Docker.Registry.DotNet.Application.Authentication; using FluentAssertions; diff --git a/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs b/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs index 82804db..4d325a9 100644 --- a/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs +++ b/test/Docker.Registry.DotNet.Tests/ImageReferenceTests.cs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Docker.Registry.DotNet.Domain.Tags; +using Docker.Registry.DotNet.Domain.ImageReferences; using FluentAssertions; @@ -37,10 +37,21 @@ public void ImageReferenceWithDigestShouldWork(string digest) imageRef.IsDigest.Should().BeTrue(); } + [Test] + public void ImageReferenceWithInvalidDigestShouldFail() + { + var digest = + " sha256:0d1c30c6bf461513951e4_75fe7846f9e2f25fdfec09f4be6b.9dbe639d362ca "; + + var action = () => ImageReference.Create(digest); + + action.Should().Throw().WithMessage("*is invalid*"); + } + [Test] public void ImageReferenceWithTagShouldWork() { - string tag = " 1.2.34.5-master "; + var tag = " 1.2.34.5-master "; var imageRef = ImageReference.Create(tag); @@ -51,16 +62,6 @@ public void ImageReferenceWithTagShouldWork() imageRef.IsTag.Should().BeTrue(); } - [Test] - public void ImageReferenceWithInvalidDigestShouldFail() - { - string digest = " sha256:0d1c30c6bf461513951e4_75fe7846f9e2f25fdfec09f4be6b.9dbe639d362ca "; - - var action = () => ImageReference.Create(digest); - - action.Should().Throw().WithMessage("*is invalid*"); - } - [Test] public void TagEmptyShouldFail() { From 415a604cfb0a5a369c43c0de5578c6f9ef1fadde Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 11 Aug 2024 00:41:15 -0400 Subject: [PATCH 10/17] Updated the publish/build action to support .NET 8. --- .github/workflows/publish.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dcc7ada..c7f1435 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,26 +5,29 @@ on: [push, pull_request] jobs: build: runs-on: ubuntu-latest + strategy: + matrix: + dotnet-version: [8.x] steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Dotnet - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.100 + dotnet-version: ${{ matrix.dotnet-version }} - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.9.7 + uses: gittools/actions/gitversion/setup@v1.1.1 with: - versionSpec: '5.x' + versionSpec: '5.x' - name: GitVersion id: gitversion - uses: gittools/actions/gitversion/execute@v0.9.7 + uses: gittools/actions/gitversion/execute@v1.1.1 with: useConfigFile: true @@ -34,4 +37,5 @@ jobs: - name: Publish if: github.event_name != 'pull_request' && (github.ref_name == 'master') run: | - dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' -k ${{ secrets.NUGETKEY }} + dotnet nuget push **/*.nupkg --source 'https://api.nuget.org/v3/index.json' -k ${{ secrets.NUGETKEY }} --skip-duplicate + dotnet nuget push **/*.snupkg --source 'https://api.nuget.org/v3/index.json' -k ${{ secrets.NUGETKEY }} --skip-duplicate From cfe693bf9899b3888d38686596e988fa6720d8cc Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 11 Aug 2024 00:50:17 -0400 Subject: [PATCH 11/17] Not a fan of generic Exception(). Minor mondernization. --- .../Endpoints/ManifestOperations.cs | 70 +++++++++---------- ...ifestSchemaVersionNotSupportedException.cs | 18 +++++ 2 files changed, 53 insertions(+), 35 deletions(-) create mode 100644 src/Docker.Registry.DotNet/Domain/Endpoints/ManifestSchemaVersionNotSupportedException.cs diff --git a/src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs b/src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs index 3461127..fb10a94 100644 --- a/src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs @@ -20,14 +20,15 @@ namespace Docker.Registry.DotNet.Application.Endpoints; internal class ManifestOperations(RegistryClient client) : IManifestOperations { - static readonly IReadOnlyDictionary _manifestHeaders = new Dictionary - { + private static readonly IReadOnlyDictionary _manifestHeaders = + new Dictionary { - "Accept", - $"{ManifestMediaTypes.ManifestSchema1}, {ManifestMediaTypes.ManifestSchema2}, { - ManifestMediaTypes.ManifestList}, {ManifestMediaTypes.ManifestSchema1Signed}" - } - }; + { + "Accept", + $"{ManifestMediaTypes.ManifestSchema1}, {ManifestMediaTypes.ManifestSchema2}, { + ManifestMediaTypes.ManifestList}, {ManifestMediaTypes.ManifestSchema1Signed}" + } + }; public async Task GetManifest( string name, @@ -36,25 +37,17 @@ public async Task GetManifest( { ImageDigest? digestReference = null; - if (reference.IsTag) - { - digestReference = await GetDigest(name, reference.Tag!, token); - } - else if (reference.IsDigest) - { - digestReference = reference.Digest; - } + if (reference.IsTag) digestReference = await this.GetDigest(name, reference.Tag!, token); + else if (reference.IsDigest) digestReference = reference.Digest; if (digestReference == null) - { throw new ArgumentNullException( nameof(digestReference), @$"Failed getting the digest reference for ""{reference}"""); - } - var response = await MakeManifestRequest(name, digestReference.ToReference(), token); + var response = await this.MakeManifestRequest(name, digestReference.ToReference(), token); - var contentType = GetContentType(response.GetHeader("ContentType"), response.Body); + var contentType = this.GetContentType(response.GetHeader("ContentType"), response.Body); switch (contentType) { @@ -71,10 +64,10 @@ public async Task GetManifest( case ManifestMediaTypes.ManifestSchema2: return new GetImageManifestResult( - contentType, - client.JsonSerializer.DeserializeObject(response.Body), - response.Body) - { DockerContentDigest = response.GetHeader("Docker-Content-Digest") }; + contentType, + client.JsonSerializer.DeserializeObject(response.Body), + response.Body) + { DockerContentDigest = response.GetHeader("Docker-Content-Digest") }; case ManifestMediaTypes.ManifestList: return new GetImageManifestResult( @@ -93,13 +86,13 @@ public async Task PutManifest( ImageManifest manifest, CancellationToken token) { - string? manifestMediaType = null; - if (manifest is ImageManifest2_1) - manifestMediaType = ManifestMediaTypes.ManifestSchema1; - if (manifest is ImageManifest2_2) - manifestMediaType = ManifestMediaTypes.ManifestSchema2; - if (manifest is ManifestList) - manifestMediaType = ManifestMediaTypes.ManifestList; + var manifestMediaType = manifest switch + { + ImageManifest2_1 => ManifestMediaTypes.ManifestSchema1, + ImageManifest2_2 => ManifestMediaTypes.ManifestSchema2, + ManifestList => ManifestMediaTypes.ManifestList, + _ => null + }; var response = await client.MakeRequest( HttpMethod.Put, @@ -120,7 +113,10 @@ public async Task PutManifest( }; } - public async Task DeleteManifest(string name, ImageReference reference, CancellationToken token = default) + public async Task DeleteManifest( + string name, + ImageReference reference, + CancellationToken token = default) { var path = $"{client.RegistryVersion}/{name}/manifests/{reference}"; @@ -133,14 +129,17 @@ public async Task GetManifestRaw( ImageReference reference, CancellationToken token) { - var response = await MakeManifestRequest(name, reference, token); + var response = await this.MakeManifestRequest(name, reference, token); return response.Body; } - public async Task GetDigest(string name, ImageTag tag, CancellationToken token = default) + public async Task GetDigest( + string name, + ImageTag tag, + CancellationToken token = default) { - var response = await MakeManifestRequest(name, tag.ToReference(), token); + var response = await this.MakeManifestRequest(name, tag.ToReference(), token); var digestValue = response.GetHeader("Docker-Content-Digest"); @@ -176,7 +175,8 @@ private string GetContentType(string contentTypeHeader, string manifest) if (check.SchemaVersion.Value == 2) return ManifestMediaTypes.ManifestSchema2; - throw new Exception($"Unable to determine schema type from version {check.SchemaVersion}"); + throw new ManifestSchemaVersionNotSupportedException( + $"Unable to determine schema type from version {check.SchemaVersion}"); } private class SchemaCheck diff --git a/src/Docker.Registry.DotNet/Domain/Endpoints/ManifestSchemaVersionNotSupportedException.cs b/src/Docker.Registry.DotNet/Domain/Endpoints/ManifestSchemaVersionNotSupportedException.cs new file mode 100644 index 0000000..09eb15e --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Endpoints/ManifestSchemaVersionNotSupportedException.cs @@ -0,0 +1,18 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Endpoints; + +internal class ManifestSchemaVersionNotSupportedException(string message) : Exception(message); \ No newline at end of file From 969df9f94c44e3f2c2ed84b539078a1035f6fd11 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 11 Aug 2024 00:52:23 -0400 Subject: [PATCH 12/17] Another generic exception gone. --- .../Endpoints/ManifestOperations.cs | 40 +++++++++---------- .../UnknownManifestContentTypeException.cs | 18 +++++++++ 2 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 src/Docker.Registry.DotNet/Domain/Endpoints/UnknownManifestContentTypeException.cs diff --git a/src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs b/src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs index fb10a94..d36b6f8 100644 --- a/src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs +++ b/src/Docker.Registry.DotNet/Application/Endpoints/ManifestOperations.cs @@ -49,35 +49,31 @@ public async Task GetManifest( var contentType = this.GetContentType(response.GetHeader("ContentType"), response.Body); - switch (contentType) + return contentType switch { - case ManifestMediaTypes.ManifestSchema1: - case ManifestMediaTypes.ManifestSchema1Signed: - return new GetImageManifestResult( + ManifestMediaTypes.ManifestSchema1 or ManifestMediaTypes.ManifestSchema1Signed => new + GetImageManifestResult( contentType, client.JsonSerializer.DeserializeObject(response.Body), response.Body) { DockerContentDigest = response.GetHeader("Docker-Content-Digest"), Etag = response.GetHeader("Etag") - }; - - case ManifestMediaTypes.ManifestSchema2: - return new GetImageManifestResult( - contentType, - client.JsonSerializer.DeserializeObject(response.Body), - response.Body) - { DockerContentDigest = response.GetHeader("Docker-Content-Digest") }; - - case ManifestMediaTypes.ManifestList: - return new GetImageManifestResult( - contentType, - client.JsonSerializer.DeserializeObject(response.Body), - response.Body); - - default: - throw new Exception($"Unexpected ContentType '{contentType}'."); - } + }, + ManifestMediaTypes.ManifestSchema2 => new GetImageManifestResult( + contentType, + client.JsonSerializer.DeserializeObject(response.Body), + response.Body) + { + DockerContentDigest = response.GetHeader("Docker-Content-Digest") + }, + ManifestMediaTypes.ManifestList => new GetImageManifestResult( + contentType, + client.JsonSerializer.DeserializeObject(response.Body), + response.Body), + _ => throw new UnknownManifestContentTypeException( + $"Unexpected ContentType '{contentType}'.") + }; } public async Task PutManifest( diff --git a/src/Docker.Registry.DotNet/Domain/Endpoints/UnknownManifestContentTypeException.cs b/src/Docker.Registry.DotNet/Domain/Endpoints/UnknownManifestContentTypeException.cs new file mode 100644 index 0000000..f64a020 --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Endpoints/UnknownManifestContentTypeException.cs @@ -0,0 +1,18 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Endpoints; + +internal class UnknownManifestContentTypeException(string message) : Exception(message); \ No newline at end of file From e5a58b41e75d233312fd17fcf176f24412a224f0 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 11 Aug 2024 01:01:04 -0400 Subject: [PATCH 13/17] Improved the example. --- README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 18663b7..5913269 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,14 @@ dotnet add package Docker.Registry.DotNet # Usage ```csharp -var configuration = new RegistryClientConfiguration("localhost:5000"); +var configuration = new RegistryClientConfiguration("http://localhost:5000"); using (var client = configuration.CreateClient()) { - await client.System.PingAsync(); + // get catalog + var catalog = await await client.Catalog.GetCatalog(); + + // list tags for the first catalog + var tags = await client.Tags.ListTags(catalog?.Repositories.FirstOrDefault()); } ``` - -# Changelog - -### v1.1.33 -* Added Basic Authentication (thanks [Zguy](https://github.com/Zguy)). -* Fixed issue with operational parameters (thanks [lostllama](https://github.com/lostllama)). -* Fixed issue with large manifest layers (thanks [msvprogs](https://github.com/msvprogs)). From ded5cdfcfc528fb817989ff8e8a3166f2ae64fa3 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 11 Aug 2024 01:58:54 -0400 Subject: [PATCH 14/17] The DockerAuth Provider never worked. Switched to using the ActivitySource to "log" stuff internally. --- .../AnonymousOAuthAuthenticationProvider.cs | 6 +- .../Authentication/AuthenticationProvider.cs | 7 +- .../BasicAuthenticationProvider.cs | 6 +- .../DockerHubJwtAuthenticationProvider.cs | 75 ------------------- .../PasswordOAuthAuthenticationProvider.cs | 6 +- .../Endpoints/BlobUploadOperations.cs | 7 +- .../Application/OAuth/OAuthClient.cs | 6 +- .../Application/Registry/RegistryClient.cs | 72 ++++++++++-------- .../Registry/RegistryUriBuilder.cs | 2 + src/Docker.Registry.DotNet/Assembly.cs | 13 +++- .../Docker.Registry.DotNet.csproj | 2 + .../FrozenRegistryClientConfigurationImpl.cs | 23 ++++++ .../IFrozenRegistryClientConfiguration.cs | 26 +++++++ .../Domain/DockerRegistryConstants.cs | 4 +- .../RegistryClientConfiguration.cs | 30 +++----- .../RegistryClientConfigurationExtensions.cs | 23 +----- 16 files changed, 145 insertions(+), 163 deletions(-) delete mode 100644 src/Docker.Registry.DotNet/Application/Authentication/DockerHubJwtAuthenticationProvider.cs create mode 100644 src/Docker.Registry.DotNet/Domain/Configuration/FrozenRegistryClientConfigurationImpl.cs create mode 100644 src/Docker.Registry.DotNet/Domain/Configuration/IFrozenRegistryClientConfiguration.cs diff --git a/src/Docker.Registry.DotNet/Application/Authentication/AnonymousOAuthAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Application/Authentication/AnonymousOAuthAuthenticationProvider.cs index 0c33e0e..409e005 100644 --- a/src/Docker.Registry.DotNet/Application/Authentication/AnonymousOAuthAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/AnonymousOAuthAuthenticationProvider.cs @@ -24,8 +24,10 @@ public class AnonymousOAuthAuthenticationProvider : AuthenticationProvider private static string Schema { get; } = "Bearer"; - public override Task Authenticate(HttpRequestMessage request) + public override Task Authenticate(HttpRequestMessage request, IRegistryUriBuilder uriBuilder) { + using var activity = Assembly.Source.StartActivity("AnonymousOAuthAuthenticationProvider.Authenticate(request)"); + return Task.CompletedTask; } @@ -34,6 +36,8 @@ public override async Task Authenticate( HttpResponseMessage response, IRegistryUriBuilder uriBuilder) { + using var activity = Assembly.Source.StartActivity("AnonymousOAuthAuthenticationProvider.Authenticate(request, response)"); + var header = this.TryGetSchemaHeader(response, Schema); //Get the bearer bits diff --git a/src/Docker.Registry.DotNet/Application/Authentication/AuthenticationProvider.cs b/src/Docker.Registry.DotNet/Application/Authentication/AuthenticationProvider.cs index e2f5714..3b24bc9 100644 --- a/src/Docker.Registry.DotNet/Application/Authentication/AuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/AuthenticationProvider.cs @@ -21,14 +21,15 @@ namespace Docker.Registry.DotNet.Application.Authentication; public abstract class AuthenticationProvider { /// - /// Called on the initial send + /// Called on initial connection /// /// + /// /// - public abstract Task Authenticate(HttpRequestMessage request); + public abstract Task Authenticate(HttpRequestMessage request, IRegistryUriBuilder uriBuilder); /// - /// Called when the send is challenged. + /// Called when connection is challenged. /// /// /// diff --git a/src/Docker.Registry.DotNet/Application/Authentication/BasicAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Application/Authentication/BasicAuthenticationProvider.cs index 7362802..114dd5a 100644 --- a/src/Docker.Registry.DotNet/Application/Authentication/BasicAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/BasicAuthenticationProvider.cs @@ -20,8 +20,10 @@ public class BasicAuthenticationProvider(string username, string password) : Aut { private static string Schema { get; } = "Basic"; - public override Task Authenticate(HttpRequestMessage request) + public override Task Authenticate(HttpRequestMessage request, IRegistryUriBuilder uriBuilder) { + using var activity = Assembly.Source.StartActivity("BasicAuthenticationProvider.Authenticate(request)"); + return Task.CompletedTask; } @@ -30,6 +32,8 @@ public override Task Authenticate( HttpResponseMessage response, IRegistryUriBuilder uriBuilder) { + using var activity = Assembly.Source.StartActivity("BasicAuthenticationProvider.Authenticate(request, response)"); + this.TryGetSchemaHeader(response, Schema); var passBytes = Encoding.UTF8.GetBytes($"{username}:{password}"); diff --git a/src/Docker.Registry.DotNet/Application/Authentication/DockerHubJwtAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Application/Authentication/DockerHubJwtAuthenticationProvider.cs deleted file mode 100644 index 7adff27..0000000 --- a/src/Docker.Registry.DotNet/Application/Authentication/DockerHubJwtAuthenticationProvider.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman -// and Docker.Registry.DotNet Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using Docker.Registry.DotNet.Application.OAuth; - -namespace Docker.Registry.DotNet.Application.Authentication; - -public class DockerHubJwtAuthenticationProvider(string username, string password) - : AuthenticationProvider -{ - private static readonly HttpClient _client = new(); - - private static string Schema { get; } = "Bearer"; - - public override Task Authenticate(HttpRequestMessage request) - { - return Task.CompletedTask; - } - - private async Task PostAuth( - IRegistryUriBuilder uriBuilder, - CancellationToken token = default) - { - var request = new HttpRequestMessage(HttpMethod.Post, uriBuilder.Build("/v2/users/login")) - { - Content = new StringContent( - JsonConvert.SerializeObject( - new Dictionary - { - { "username", username }, - { "password", password } - }) - ) - }; - - using var response = await _client.SendAsync(request, token); - - if (!response.IsSuccessStatusCode) - throw new UnauthorizedAccessException( - $"Unable to authenticate: {await response.Content.ReadAsStringAsyncWithCancellation(token)}"); - - var body = await response.Content.ReadAsStringAsyncWithCancellation(token); - - var authToken = JsonConvert.DeserializeObject(body); - - return authToken; - } - - public override async Task Authenticate( - HttpRequestMessage request, - HttpResponseMessage response, - IRegistryUriBuilder uriBuilder) - { - var tokenResponse = await this.PostAuth(uriBuilder); - - if (tokenResponse?.Token == null) - throw new UnauthorizedAccessException("Failed to authenticate. Token was empty."); - - //Set the header - request.Headers.Authorization = - new AuthenticationHeaderValue(Schema, tokenResponse.Token); - } -} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Application/Authentication/PasswordOAuthAuthenticationProvider.cs b/src/Docker.Registry.DotNet/Application/Authentication/PasswordOAuthAuthenticationProvider.cs index 0dbb55e..9f05ea6 100644 --- a/src/Docker.Registry.DotNet/Application/Authentication/PasswordOAuthAuthenticationProvider.cs +++ b/src/Docker.Registry.DotNet/Application/Authentication/PasswordOAuthAuthenticationProvider.cs @@ -25,8 +25,10 @@ public class PasswordOAuthAuthenticationProvider(string username, string passwor private static string Schema { get; } = "Bearer"; - public override Task Authenticate(HttpRequestMessage request) + public override Task Authenticate(HttpRequestMessage request, IRegistryUriBuilder uriBuilder) { + using var activity = Assembly.Source.StartActivity("PasswordOAuthAuthenticationProvider.Authenticate(request)"); + return Task.CompletedTask; } @@ -35,6 +37,8 @@ public override async Task Authenticate( HttpResponseMessage response, IRegistryUriBuilder uriBuilder) { + using var activity = Assembly.Source.StartActivity("PasswordOAuthAuthenticationProvider.Authenticate(request, response)"); + var header = this.TryGetSchemaHeader(response, Schema); //Get the bearer bits diff --git a/src/Docker.Registry.DotNet/Application/Endpoints/BlobUploadOperations.cs b/src/Docker.Registry.DotNet/Application/Endpoints/BlobUploadOperations.cs index 489341e..c2d8c3c 100644 --- a/src/Docker.Registry.DotNet/Application/Endpoints/BlobUploadOperations.cs +++ b/src/Docker.Registry.DotNet/Application/Endpoints/BlobUploadOperations.cs @@ -193,14 +193,9 @@ public async Task UploadBlob( token: token); var uuid = response.Headers.GetString("Docker-Upload-UUID"); - - Debug.WriteLine($"Uploading with uuid: {uuid}"); - var location = response.Headers.GetString("Location"); - Debug.WriteLine($"Using location: {location}"); - - //await GetBlobUploadStatus(name, uuid, cancellationToken); + //await GetBlobUploadStatus(name, uuid, token); try { diff --git a/src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs b/src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs index 2557150..9b2ca9f 100644 --- a/src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs +++ b/src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs @@ -29,6 +29,8 @@ internal class OAuthClient string? password, CancellationToken token = default) { + using var activity = Assembly.Source.StartActivity("OAuthClient.GetTokenInner()"); + HttpRequestMessage request; if (username == null || password == null) @@ -63,12 +65,14 @@ internal class OAuthClient }; } - Debug.WriteLine("OAuth Client GetToken"); + activity?.AddEvent(new ActivityEvent("Getting Token")); using var response = await _client.SendAsync(request, token); if (!response.IsSuccessStatusCode) { + activity?.AddEvent(new ActivityEvent("Failed to Authenticate")); + throw new UnauthorizedAccessException( $"Unable to authenticate: {await response.Content.ReadAsStringAsyncWithCancellation(token)}"); } diff --git a/src/Docker.Registry.DotNet/Application/Registry/RegistryClient.cs b/src/Docker.Registry.DotNet/Application/Registry/RegistryClient.cs index 63de924..aa9a1d5 100644 --- a/src/Docker.Registry.DotNet/Application/Registry/RegistryClient.cs +++ b/src/Docker.Registry.DotNet/Application/Registry/RegistryClient.cs @@ -15,6 +15,7 @@ using Docker.Registry.DotNet.Application.Endpoints; using Docker.Registry.DotNet.Domain; +using Docker.Registry.DotNet.Domain.Configuration; using Docker.Registry.DotNet.Domain.QueryStrings; namespace Docker.Registry.DotNet.Application.Registry; @@ -24,11 +25,9 @@ public class RegistryClient : IRegistryClient private static readonly TimeSpan _infiniteTimeout = TimeSpan.FromMilliseconds(Timeout.Infinite); - private readonly AuthenticationProvider _authenticationProvider; - private readonly HttpClient _client; - private readonly RegistryClientConfiguration _configuration; + private readonly IFrozenRegistryClientConfiguration _configuration; private readonly IEnumerable> _errorHandlers = new Action[] @@ -43,20 +42,20 @@ public class RegistryClient : IRegistryClient internal IRegistryUriBuilder? UriBuilder; public RegistryClient( - RegistryClientConfiguration configuration, - AuthenticationProvider authenticationProvider) + IFrozenRegistryClientConfiguration configuration) { if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - if (authenticationProvider == null) throw new ArgumentNullException(nameof(authenticationProvider)); - if (configuration.BaseAddress == null) throw new ArgumentNullException(nameof(configuration.BaseAddress)); + if (configuration.AuthenticationProvider == null) + throw new ArgumentNullException(nameof(configuration.AuthenticationProvider)); + if (configuration.BaseAddress == null) + throw new ArgumentNullException(nameof(configuration.BaseAddress)); - this._authenticationProvider = authenticationProvider; + this._configuration = configuration; this._client = configuration.HttpMessageHandler is null ? new HttpClient() : new HttpClient(configuration.HttpMessageHandler); - this._configuration = configuration; this.UriBuilder = new RegistryUriBuilder(configuration.BaseAddress); - + this.Manifest = new ManifestOperations(this); this.Catalog = new CatalogOperations(this); this.Blobs = new BlobOperations(this); @@ -66,30 +65,15 @@ public RegistryClient( this.Repository = new RepositoryOperations(this); } + private AuthenticationProvider AuthenticationProvider => + this._configuration.AuthenticationProvider; + internal string RegistryVersion => DockerRegistryConstants.RegistryVersion; internal TimeSpan DefaultTimeout => this._configuration.DefaultTimeout; internal JsonSerializer JsonSerializer { get; } = new(); - #region Operations - - public IRepositoryOperations Repository { get; set; } - - public IBlobUploadOperations BlobUploads { get; } - - public IManifestOperations Manifest { get; } - - public ICatalogOperations Catalog { get; } - - public IBlobOperations Blobs { get; } - - public ITagOperations Tags { get; } - - public ISystemOperations System { get; } - - #endregion - public void Dispose() { this._client.Dispose(); @@ -195,9 +179,9 @@ private async Task InternalMakeRequestAsync( if (this.UriBuilder == null) throw new ArgumentNullException(nameof(this.UriBuilder), "Could not find URI builder"); - var builtUri = this.UriBuilder.Build(path, queryString); + using var activity = Assembly.Source.StartActivity("RegistryClient.InternalMakeRequestAsync()"); - Debug.WriteLine($"Built URI: {builtUri}"); + var builtUri = this.UriBuilder.Build(path, queryString); var request = this.PrepareRequest(method, builtUri, headers, content); @@ -209,7 +193,9 @@ private async Task InternalMakeRequestAsync( cancellationToken = timeoutTokenSource.Token; } - await this._authenticationProvider.Authenticate(request); + await this.AuthenticationProvider.Authenticate(request, this.UriBuilder); + + activity?.AddEvent(new ActivityEvent($"Sending Request to: {request.RequestUri}")); var response = await this._client.SendAsync( request, @@ -218,11 +204,13 @@ private async Task InternalMakeRequestAsync( if (response.StatusCode != HttpStatusCode.Unauthorized) return response; + activity?.AddEvent(new ActivityEvent("Authorization Challenged")); + //Prepare another request (we can't reuse the same request) var request2 = this.PrepareRequest(method, builtUri, headers, content); //Authenticate given the challenge - await this._authenticationProvider.Authenticate(request2, response, this.UriBuilder); + await this.AuthenticationProvider.Authenticate(request2, response, this.UriBuilder); //Send it again response = await this._client.SendAsync( @@ -261,7 +249,7 @@ internal HttpRequestMessage PrepareRequest( { var request = new HttpRequestMessage(method, uri); - request.Headers.Add("User-Agent", DockerRegistryConstants.UserAgent); + request.Headers.Add("User-Agent", DockerRegistryConstants.Name); request.Headers.AddRange(headers); //Create the content @@ -269,4 +257,22 @@ internal HttpRequestMessage PrepareRequest( return request; } + + #region Operations + + public IRepositoryOperations Repository { get; set; } + + public IBlobUploadOperations BlobUploads { get; } + + public IManifestOperations Manifest { get; } + + public ICatalogOperations Catalog { get; } + + public IBlobOperations Blobs { get; } + + public ITagOperations Tags { get; } + + public ISystemOperations System { get; } + + #endregion } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Application/Registry/RegistryUriBuilder.cs b/src/Docker.Registry.DotNet/Application/Registry/RegistryUriBuilder.cs index 7c887ba..6283b68 100644 --- a/src/Docker.Registry.DotNet/Application/Registry/RegistryUriBuilder.cs +++ b/src/Docker.Registry.DotNet/Application/Registry/RegistryUriBuilder.cs @@ -19,6 +19,8 @@ public class RegistryUriBuilder(Uri baseUri) : IRegistryUriBuilder { public virtual Uri Build(string? path = null, string? queryString = null) { + using var activity = Assembly.Source.StartActivity("RegistryUriBuilder.Build()"); + var pathIsUri = false; path = path?.Trim() ?? string.Empty; diff --git a/src/Docker.Registry.DotNet/Assembly.cs b/src/Docker.Registry.DotNet/Assembly.cs index ccb1d56..072abba 100644 --- a/src/Docker.Registry.DotNet/Assembly.cs +++ b/src/Docker.Registry.DotNet/Assembly.cs @@ -15,4 +15,15 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Docker.Registry.DotNet.Tests")] \ No newline at end of file +using Docker.Registry.DotNet.Domain; + +[assembly: InternalsVisibleTo("Docker.Registry.DotNet.Tests")] + +namespace Docker.Registry.DotNet; + +internal sealed class Assembly +{ + internal static ActivitySource Source = new( + DockerRegistryConstants.Name, + DockerRegistryConstants.Version); +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj b/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj index ddac3bc..f89c4b3 100644 --- a/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj +++ b/src/Docker.Registry.DotNet/Docker.Registry.DotNet.csproj @@ -40,10 +40,12 @@ + + diff --git a/src/Docker.Registry.DotNet/Domain/Configuration/FrozenRegistryClientConfigurationImpl.cs b/src/Docker.Registry.DotNet/Domain/Configuration/FrozenRegistryClientConfigurationImpl.cs new file mode 100644 index 0000000..3fee955 --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Configuration/FrozenRegistryClientConfigurationImpl.cs @@ -0,0 +1,23 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Configuration; + +public record FrozenRegistryClientConfigurationImpl( + Uri BaseAddress, + HttpMessageHandler? HttpMessageHandler, + AuthenticationProvider AuthenticationProvider, + TimeSpan DefaultTimeout) + : IFrozenRegistryClientConfiguration; \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Domain/Configuration/IFrozenRegistryClientConfiguration.cs b/src/Docker.Registry.DotNet/Domain/Configuration/IFrozenRegistryClientConfiguration.cs new file mode 100644 index 0000000..378c23c --- /dev/null +++ b/src/Docker.Registry.DotNet/Domain/Configuration/IFrozenRegistryClientConfiguration.cs @@ -0,0 +1,26 @@ +// Copyright 2017-2024 Rich Quackenbush, Jaben Cargman +// and Docker.Registry.DotNet Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Docker.Registry.DotNet.Domain.Configuration; + +public interface IFrozenRegistryClientConfiguration +{ + Uri? BaseAddress { get; } + + HttpMessageHandler? HttpMessageHandler { get; } + AuthenticationProvider AuthenticationProvider { get; } + + TimeSpan DefaultTimeout { get; } +} \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Domain/DockerRegistryConstants.cs b/src/Docker.Registry.DotNet/Domain/DockerRegistryConstants.cs index d73fbce..441e397 100644 --- a/src/Docker.Registry.DotNet/Domain/DockerRegistryConstants.cs +++ b/src/Docker.Registry.DotNet/Domain/DockerRegistryConstants.cs @@ -19,5 +19,7 @@ public class DockerRegistryConstants { public const string RegistryVersion = "v2"; - public const string UserAgent = "Docker.Registry.DotNet"; + public const string Name = "Docker.Registry.DotNet"; + + public const string Version = "1.3.0"; } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs index ebd7fcb..799d3de 100644 --- a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs +++ b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs @@ -13,6 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Docker.Registry.DotNet.Domain.Configuration; + namespace Docker.Registry.DotNet; public class RegistryClientConfiguration @@ -60,11 +62,6 @@ private set { if (value != this._defaultTimeout && value != default) { - if (value < Timeout.InfiniteTimeSpan) - throw new ArgumentException( - "Timeout must be less than Timeout.Infinite", - nameof(this.DefaultTimeout)); - this._defaultTimeout = value; } } @@ -76,11 +73,9 @@ public RegistryClientConfiguration SetBaseAddress(Uri baseAddress) throw new ArgumentException("BaseAddress cannot be null.", nameof(this.BaseAddress)); if (baseAddress.Scheme is not ("http" or "https")) - { throw new ArgumentOutOfRangeException( - nameof(BaseAddress), + nameof(this.BaseAddress), "Base Address Uri must start with http:// or https://"); - } this.BaseAddress = baseAddress; @@ -105,25 +100,24 @@ public RegistryClientConfiguration SetHttpMessageHandler(HttpMessageHandler? mes /// Defaults to AnonymousOAuthAuthenticationProvider /// public RegistryClientConfiguration SetAuthenticationProvider( - AuthenticationProvider authenticationProvider) + AuthenticationProvider? authenticationProvider) { this.AuthenticationProvider = authenticationProvider - ?? throw new ArgumentNullException( - nameof(authenticationProvider)); + ?? new AnonymousOAuthAuthenticationProvider(); return this; } - private void RunValidationRules() + public IRegistryClient CreateClient() { if (this.BaseAddress == null) throw new ArgumentException("BaseAddress cannot be null.", nameof(this.BaseAddress)); - } - - public IRegistryClient CreateClient() - { - this.RunValidationRules(); - return new RegistryClient(this, this.AuthenticationProvider); + return new RegistryClient( + new FrozenRegistryClientConfigurationImpl( + this.BaseAddress, + this.HttpMessageHandler, + this.AuthenticationProvider, + this.DefaultTimeout)); } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/RegistryClientConfigurationExtensions.cs b/src/Docker.Registry.DotNet/RegistryClientConfigurationExtensions.cs index d6df192..7375d0d 100644 --- a/src/Docker.Registry.DotNet/RegistryClientConfigurationExtensions.cs +++ b/src/Docker.Registry.DotNet/RegistryClientConfigurationExtensions.cs @@ -25,7 +25,7 @@ public static RegistryClientConfiguration UseBasicAuthentication( if (configuration == null) throw new ArgumentNullException(nameof(configuration)); configuration.SetAuthenticationProvider( - new PasswordOAuthAuthenticationProvider(username, password)); + new BasicAuthenticationProvider(username, password)); return configuration; } @@ -43,27 +43,6 @@ public static RegistryClientConfiguration UsePasswordOAuthAuthentication( return configuration; } - /// - /// Supports Docker Hub Authentication. - /// - /// - /// - /// - /// - /// - public static RegistryClientConfiguration UseDockerHubAuthentication( - this RegistryClientConfiguration configuration, - string username, - string password) - { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - - configuration.SetAuthenticationProvider( - new DockerHubJwtAuthenticationProvider(username, password)); - - return configuration; - } - /// /// Default authentication provider. /// From efaa48b5608f854170f5d693ae6279ea3414732a Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 11 Aug 2024 02:14:57 -0400 Subject: [PATCH 15/17] Suggestions from the AI Actually pretty useful. --- README.md | 36 ++++++++++++++++++- .../Application/OAuth/OAuthClient.cs | 29 +++++++++------ .../Repository/ListRepositoryTagsResponse.cs | 2 +- .../Domain/Repository/RepositoryTag.cs | 14 ++++---- .../Domain/Repository/RepositoryTagImage.cs | 16 ++++----- .../Infrastructure/Helpers/HttpUtility.cs | 6 ++-- .../Infrastructure/Json/JsonSerializer.cs | 2 +- .../RegistryClientConfiguration.cs | 6 ++-- .../RegistryClientLegacyHelpers.cs | 2 +- 9 files changed, 78 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 5913269..98b50d6 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,49 @@ dotnet add package Docker.Registry.DotNet ``` # Usage + +### Local Hub + ```csharp var configuration = new RegistryClientConfiguration("http://localhost:5000"); +//configuration.UsePasswordOAuthAuthentication("username", "password") + using (var client = configuration.CreateClient()) { // get catalog - var catalog = await await client.Catalog.GetCatalog(); + var catalog = await client.Catalog.GetCatalog(); // list tags for the first catalog var tags = await client.Tags.ListTags(catalog?.Repositories.FirstOrDefault()); } ``` + +### Remote Hub with Authentication + +```csharp +var configuration = new RegistryClientConfiguration("https://proget.mycompany.com"); + +configuration.UsePasswordOAuthAuthentication("username", "password") + +using (var client = configuration.CreateClient()) +{ + // get catalog + var catalog = await client.Catalog.GetCatalog(); + + // list tags for the first catalog + var tags = await client.Tags.ListTags(catalog?.Repositories.FirstOrDefault()); +} +``` + +### Docker Hub + +```csharp +var configuration = new RegistryClientConfiguration("https://hub.docker.com"); + +using (var client = configuration.CreateClient()) +{ + // load respository + var tags = await client.Repository.ListRepositoryTags("grafana", "loki-docker-driver"); +} +``` \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs b/src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs index 9b2ca9f..86f4e6a 100644 --- a/src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs +++ b/src/Docker.Registry.DotNet/Application/OAuth/OAuthClient.cs @@ -67,21 +67,30 @@ internal class OAuthClient activity?.AddEvent(new ActivityEvent("Getting Token")); - using var response = await _client.SendAsync(request, token); - - if (!response.IsSuccessStatusCode) + try { - activity?.AddEvent(new ActivityEvent("Failed to Authenticate")); + using var response = await _client.SendAsync(request, token); - throw new UnauthorizedAccessException( - $"Unable to authenticate: {await response.Content.ReadAsStringAsyncWithCancellation(token)}"); - } + if (!response.IsSuccessStatusCode) + { + activity?.AddEvent(new ActivityEvent("Failed to Authenticate")); + + throw new UnauthorizedAccessException( + $"Unable to authenticate: {await response.Content.ReadAsStringAsyncWithCancellation(token)}"); + } - var body = await response.Content.ReadAsStringAsyncWithCancellation(token); + var body = await response.Content.ReadAsStringAsyncWithCancellation(token); - var authToken = JsonConvert.DeserializeObject(body); + var authToken = JsonConvert.DeserializeObject(body); - return authToken; + return authToken; + } + catch (Exception ex) + { + activity?.AddTag("Authentication Exception", ex); + + throw new UnauthorizedAccessException($"Unable to authenticate: {ex}"); + } } public Task GetToken( diff --git a/src/Docker.Registry.DotNet/Domain/Repository/ListRepositoryTagsResponse.cs b/src/Docker.Registry.DotNet/Domain/Repository/ListRepositoryTagsResponse.cs index b3e6325..b8cedf3 100644 --- a/src/Docker.Registry.DotNet/Domain/Repository/ListRepositoryTagsResponse.cs +++ b/src/Docker.Registry.DotNet/Domain/Repository/ListRepositoryTagsResponse.cs @@ -9,7 +9,7 @@ public class ListRepositoryTagsResponse public string Next { get; set; } [JsonProperty("previous")] - public object Previous { get; set; } + public string Previous { get; set; } [JsonProperty("results")] public List Tags { get; set; } diff --git a/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTag.cs b/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTag.cs index 9d2ef5a..290bfa8 100644 --- a/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTag.cs +++ b/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTag.cs @@ -11,7 +11,7 @@ public class RepositoryTag public int Id { get; set; } [JsonProperty("images")] - public List Images { get; set; } + public List Images { get; set; } = new List(); [JsonProperty("last_updated")] [JsonConverter(typeof(IsoDateTimeConverter))] @@ -21,10 +21,10 @@ public class RepositoryTag public int LastUpdater { get; set; } [JsonProperty("last_updater_username")] - public string LastUpdaterUsername { get; set; } + public string? LastUpdaterUsername { get; set; } [JsonProperty("name")] - public string Name { get; set; } + public string? Name { get; set; } [JsonProperty("repository")] public int Repository { get; set; } @@ -36,7 +36,7 @@ public class RepositoryTag public bool V2 { get; set; } [JsonProperty("tag_status")] - public string TagStatus { get; set; } + public string? TagStatus { get; set; } [JsonProperty("tag_last_pulled")] [JsonConverter(typeof(IsoDateTimeConverter))] @@ -47,11 +47,11 @@ public class RepositoryTag public DateTime TagLastPushed { get; set; } [JsonProperty("media_type")] - public string MediaType { get; set; } + public string? MediaType { get; set; } [JsonProperty("content_type")] - public string ContentType { get; set; } + public string? ContentType { get; set; } [JsonProperty("digest")] - public string Digest { get; set; } + public string? Digest { get; set; } } \ No newline at end of file diff --git a/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagImage.cs b/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagImage.cs index 6d03e68..1666194 100644 --- a/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagImage.cs +++ b/src/Docker.Registry.DotNet/Domain/Repository/RepositoryTagImage.cs @@ -5,31 +5,31 @@ namespace Docker.Registry.DotNet.Domain.Repository; public class RepositoryTagImage { [JsonProperty("architecture")] - public string Architecture { get; set; } + public string? Architecture { get; set; } [JsonProperty("features")] - public string Features { get; set; } + public string? Features { get; set; } [JsonProperty("variant")] - public object Variant { get; set; } + public object? Variant { get; set; } [JsonProperty("digest")] - public string Digest { get; set; } + public string? Digest { get; set; } [JsonProperty("os")] - public string Os { get; set; } + public string? Os { get; set; } [JsonProperty("os_features")] - public string OsFeatures { get; set; } + public string? OsFeatures { get; set; } [JsonProperty("os_version")] - public object OsVersion { get; set; } + public object? OsVersion { get; set; } [JsonProperty("size")] public long Size { get; set; } [JsonProperty("status")] - public string Status { get; set; } + public string? Status { get; set; } [JsonProperty("last_pulled")] [JsonConverter(typeof(IsoDateTimeConverter))] diff --git a/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs b/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs index 73008c4..88637fd 100644 --- a/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs @@ -49,7 +49,7 @@ internal static Uri BuildUri(this Uri baseUri, string path, IReadOnlyQueryString /// /// /// The first value if one is found, null otherwise. - public static string GetHeader(this RegistryApiResponse response, string name) + public static string? GetHeader(this RegistryApiResponse response, string name) { if (response == null) throw new ArgumentNullException(nameof(response)); @@ -64,7 +64,7 @@ public static string GetHeader(this RegistryApiResponse response, string name) /// /// /// The first value if one is found, null otherwise. - public static string[] GetHeaders( + public static string?[] GetHeaders( this IEnumerable> headers, string name) { @@ -106,7 +106,7 @@ public static void AddRange( return null; } - public static string GetString( + public static string? GetString( this HttpResponseHeaders responseHeaders, string name) { diff --git a/src/Docker.Registry.DotNet/Infrastructure/Json/JsonSerializer.cs b/src/Docker.Registry.DotNet/Infrastructure/Json/JsonSerializer.cs index 83393da..8a1f8fb 100644 --- a/src/Docker.Registry.DotNet/Infrastructure/Json/JsonSerializer.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Json/JsonSerializer.cs @@ -36,7 +36,7 @@ internal class JsonSerializer } }; - public T DeserializeObject(string json) + public T? DeserializeObject(string json) { return JsonConvert.DeserializeObject(json, Settings); } diff --git a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs index 799d3de..8e6be7a 100644 --- a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs +++ b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs @@ -23,9 +23,9 @@ public class RegistryClientConfiguration public RegistryClientConfiguration(string baseAddress, TimeSpan defaultTimeout = default) { - var valid = Uri.TryCreate(baseAddress, UriKind.Absolute, out var parsedBaseAddress); + var isValidUri = Uri.TryCreate(baseAddress, UriKind.Absolute, out var parsedBaseAddress); - if (valid) this.SetBaseAddress(parsedBaseAddress); + if (isValidUri ) this.SetBaseAddress(parsedBaseAddress); else throw new ArgumentException("BaseAddress is not a valid Uri", nameof(baseAddress)); this.SetDefaultTimeout(defaultTimeout); @@ -75,7 +75,7 @@ public RegistryClientConfiguration SetBaseAddress(Uri baseAddress) if (baseAddress.Scheme is not ("http" or "https")) throw new ArgumentOutOfRangeException( nameof(this.BaseAddress), - "Base Address Uri must start with http:// or https://"); + "BaseAddress must use either http or https schema."); this.BaseAddress = baseAddress; diff --git a/src/Docker.Registry.DotNet/RegistryClientLegacyHelpers.cs b/src/Docker.Registry.DotNet/RegistryClientLegacyHelpers.cs index 151acdd..a59e47c 100644 --- a/src/Docker.Registry.DotNet/RegistryClientLegacyHelpers.cs +++ b/src/Docker.Registry.DotNet/RegistryClientLegacyHelpers.cs @@ -224,7 +224,7 @@ public static Task CompleteBlobUploadAsync( } [Obsolete("Use CancelBlobUpload() instead")] - public static Task CompleteBlobUploadAsync( + public static Task CancelBlobUploadAsync( this IBlobUploadOperations operations, string name, string uuid, From 2a22290e1968fe23b8a4d03924923ba431f12f14 Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Sun, 11 Aug 2024 02:22:21 -0400 Subject: [PATCH 16/17] Bug found by coderabbitAI. --- .../Infrastructure/Helpers/HttpUtility.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs b/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs index 88637fd..26bc6ad 100644 --- a/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs +++ b/src/Docker.Registry.DotNet/Infrastructure/Helpers/HttpUtility.cs @@ -37,7 +37,8 @@ internal static Uri BuildUri(this Uri baseUri, string path, IReadOnlyQueryString if (string.IsNullOrWhiteSpace(builder.Query)) builder.Query = queryString.GetQueryString(); else - builder.Query += "&" + queryString.GetQueryString(); + builder.Query += (builder.Query.EndsWith("&") ? string.Empty : "&") + + queryString.GetQueryString(); } return builder.Uri; From dfe2eb643e4cd8ede570a450bf718328efbfad9d Mon Sep 17 00:00:00 2001 From: Jaben Cargman Date: Mon, 12 Aug 2024 14:13:34 -0400 Subject: [PATCH 17/17] Backwards compatibility -- with Obsolete.. --- Docker.Registry.DotNet.sln | 1 - src/Docker.Registry.DotNet/RegistryClientConfiguration.cs | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Docker.Registry.DotNet.sln b/Docker.Registry.DotNet.sln index 5ea2e1e..31c6097 100644 --- a/Docker.Registry.DotNet.sln +++ b/Docker.Registry.DotNet.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32414.318 diff --git a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs index 8e6be7a..9f25fdc 100644 --- a/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs +++ b/src/Docker.Registry.DotNet/RegistryClientConfiguration.cs @@ -120,4 +120,12 @@ public IRegistryClient CreateClient() this.AuthenticationProvider, this.DefaultTimeout)); } + + [Obsolete("Use Configuration.SetAuthenticationProvider() instead.")] + public IRegistryClient CreateClient(AuthenticationProvider authenticationProvider) + { + this.SetAuthenticationProvider(authenticationProvider); + + return this.CreateClient(); + } } \ No newline at end of file