From 299046bdd4922b91c3c9cef978803e9445a4286d Mon Sep 17 00:00:00 2001
From: Matt Morrison <3241034+Emdot@users.noreply.github.com>
Date: Sun, 21 Feb 2021 15:43:42 -0600
Subject: [PATCH 1/3] Start of the new user layer. Adds the client, connection
to daemons, and authentication with registries.
---
.dockerignore | 25 ++
.editorconfig | 31 +++
CodeMaid.config | 73 +++++
Docker.DotNet.sln | 39 ++-
.../DockerClientConfiguration.cs | 4 +-
src/DockerSdk/.editorconfig | 123 +++++++++
src/DockerSdk/CertificateLoader.cs | 20 ++
src/DockerSdk/ClientOptions.cs | 109 ++++++++
src/DockerSdk/DockerClient.cs | 203 ++++++++++++++
src/DockerSdk/DockerException.cs | 50 ++++
src/DockerSdk/DockerSdk.csproj | 21 ++
src/DockerSdk/DockerVersionException.cs | 28 ++
src/DockerSdk/Properties/TestAccess.cs | 2 +
src/DockerSdk/Registries/RegistryAccess.cs | 254 ++++++++++++++++++
src/DockerSdk/Registries/RegistryEntry.cs | 56 ++++
test/DockerSdk.Tests/.editorconfig | 123 +++++++++
test/DockerSdk.Tests/Cli.cs | 112 ++++++++
test/DockerSdk.Tests/ClientTests.cs | 34 +++
test/DockerSdk.Tests/ClientUnitTests.cs | 125 +++++++++
test/DockerSdk.Tests/DockerSdk.Tests.csproj | 39 +++
test/DockerSdk.Tests/Fixture.cs | 26 ++
test/DockerSdk.Tests/RegistryAccessTests.cs | 110 ++++++++
.../RegistryAccessUnitTests.cs | 104 +++++++
.../scripts/docker-compose.yml | 18 ++
.../scripts/registry-htpasswd-tls/Dockerfile | 22 ++
.../scripts/registry-open/Dockerfile | 3 +
26 files changed, 1750 insertions(+), 4 deletions(-)
create mode 100644 .dockerignore
create mode 100644 .editorconfig
create mode 100644 CodeMaid.config
create mode 100644 src/DockerSdk/.editorconfig
create mode 100644 src/DockerSdk/CertificateLoader.cs
create mode 100644 src/DockerSdk/ClientOptions.cs
create mode 100644 src/DockerSdk/DockerClient.cs
create mode 100644 src/DockerSdk/DockerException.cs
create mode 100644 src/DockerSdk/DockerSdk.csproj
create mode 100644 src/DockerSdk/DockerVersionException.cs
create mode 100644 src/DockerSdk/Properties/TestAccess.cs
create mode 100644 src/DockerSdk/Registries/RegistryAccess.cs
create mode 100644 src/DockerSdk/Registries/RegistryEntry.cs
create mode 100644 test/DockerSdk.Tests/.editorconfig
create mode 100644 test/DockerSdk.Tests/Cli.cs
create mode 100644 test/DockerSdk.Tests/ClientTests.cs
create mode 100644 test/DockerSdk.Tests/ClientUnitTests.cs
create mode 100644 test/DockerSdk.Tests/DockerSdk.Tests.csproj
create mode 100644 test/DockerSdk.Tests/Fixture.cs
create mode 100644 test/DockerSdk.Tests/RegistryAccessTests.cs
create mode 100644 test/DockerSdk.Tests/RegistryAccessUnitTests.cs
create mode 100644 test/DockerSdk.Tests/scripts/docker-compose.yml
create mode 100644 test/DockerSdk.Tests/scripts/registry-htpasswd-tls/Dockerfile
create mode 100644 test/DockerSdk.Tests/scripts/registry-open/Dockerfile
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..3729ff0cd
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,25 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..7bae3b1db
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,31 @@
+# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
+
+###############################
+# Core EditorConfig Options #
+###############################
+
+root = true
+
+# All files
+[*]
+indent_style = space
+
+# Code files
+[*.cs]
+indent_size = 4
+insert_final_newline = true
+charset = utf-8-bom
+
+# Dockerfiles
+[Dockerfile]
+indent_size = 2
+
+# YAML
+[*.yml]
+indent_size = 2
+
+# Scripts
+[*.ps1]
+indent_size = 4
+insert_final_newline = true
+charset = utf-8-bom
diff --git a/CodeMaid.config b/CodeMaid.config
new file mode 100644
index 000000000..39f9eb882
--- /dev/null
+++ b/CodeMaid.config
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+ \.Designer\.cs$||\.Designer\.vb$||\.resx$||\.min\.css$||\.min\.js$||\.generated\.cs$||\.generated\.vb$
+
+
+ True
+
+
+ False
+
+
+ True
+
+
+ Structs||7||Structs + Enums + Classes + Interfaces
+
+
+ Constructors||1||Constructors
+
+
+ Methods||6||Methods
+
+
+ Properties||4||Properties + Indexers
+
+
+ Enums||7||Structs + Enums + Classes + Interfaces
+
+
+ Destructors||8||Destructors
+
+
+ Delegates||2||Delegates
+
+
+ Indexers||4||Properties + Indexers
+
+
+ Classes||7||Structs + Enums + Classes + Interfaces
+
+
+ Fields||5||Fields
+
+
+ Interfaces||7||Structs + Enums + Classes + Interfaces
+
+
+ Events||3||Events
+
+
+ 1
+
+
+ True
+
+
+ 120
+
+
+ False
+
+
+
+
\ No newline at end of file
diff --git a/Docker.DotNet.sln b/Docker.DotNet.sln
index 4f5c83527..d3559c506 100644
--- a/Docker.DotNet.sln
+++ b/Docker.DotNet.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26228.9
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30804.86
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{85990620-78A6-4381-8BD6-84E6D0CF0649}"
EndProject
@@ -15,6 +15,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.X509", "src\D
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DockerSdk", "src\DockerSdk\DockerSdk.csproj", "{2E0D6ACC-A935-465E-ADD3-E06A6A426408}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DockerSdk.Tests", "test\DockerSdk.Tests\DockerSdk.Tests.csproj", "{94D3A9F4-36DD-4104-8919-621C3B2ECB99}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FC99294-813B-450A-9DD2-7D3126B17A55}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -73,6 +79,30 @@ Global
{248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.Build.0 = Release|Any CPU
{248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.ActiveCfg = Release|Any CPU
{248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.Build.0 = Release|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Debug|x64.Build.0 = Debug|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Debug|x86.Build.0 = Debug|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Release|x64.ActiveCfg = Release|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Release|x64.Build.0 = Release|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Release|x86.ActiveCfg = Release|Any CPU
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408}.Release|x86.Build.0 = Release|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Debug|x64.Build.0 = Debug|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Debug|x86.Build.0 = Debug|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Release|Any CPU.Build.0 = Release|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Release|x64.ActiveCfg = Release|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Release|x64.Build.0 = Release|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Release|x86.ActiveCfg = Release|Any CPU
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -82,5 +112,10 @@ Global
{E1F24B25-E027-45E0-A6E1-E08138F1F95D} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
{248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A}
+ {2E0D6ACC-A935-465E-ADD3-E06A6A426408} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
+ {94D3A9F4-36DD-4104-8919-621C3B2ECB99} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {2FB3FE97-BC33-411B-92BB-69801DD461D1}
EndGlobalSection
EndGlobal
diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs
index 9417d1f00..32a58c98c 100644
--- a/src/Docker.DotNet/DockerClientConfiguration.cs
+++ b/src/Docker.DotNet/DockerClientConfiguration.cs
@@ -14,7 +14,7 @@ public class DockerClientConfiguration : IDisposable
public TimeSpan NamedPipeConnectTimeout { get; set; } = TimeSpan.FromMilliseconds(100);
- private static Uri LocalDockerUri()
+ public static Uri LocalDockerUri()
{
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
return isWindows ? new Uri("npipe://./pipe/docker_engine") : new Uri("unix:/var/run/docker.sock");
@@ -59,4 +59,4 @@ public void Dispose()
Credentials.Dispose();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/DockerSdk/.editorconfig b/src/DockerSdk/.editorconfig
new file mode 100644
index 000000000..b82d1b0e9
--- /dev/null
+++ b/src/DockerSdk/.editorconfig
@@ -0,0 +1,123 @@
+# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
+
+###############################
+# Core EditorConfig Options #
+###############################
+
+# inherited
+
+###############################
+# .NET Coding Conventions #
+###############################
+
+[*.cs]
+
+# Organize usings
+dotnet_sort_system_directives_first = true
+# this. preferences
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_property = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_event = false:silent
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+dotnet_style_readonly_field = true:suggestion
+# Expression-level preferences
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+
+###############################
+# Naming Conventions #
+###############################
+
+# Style Definitions
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+# Use PascalCase for constant fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+
+###############################
+# C# Coding Conventions #
+###############################
+
+[*.cs]
+
+# var preferences
+csharp_style_var_for_built_in_types = true:silent
+csharp_style_var_when_type_is_apparent = true:silent
+csharp_style_var_elsewhere = true:silent
+# Expression-bodied members
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+# Pattern matching preferences
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+# Null-checking preferences
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+# Modifier preferences
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
+# Expression-level preferences
+csharp_prefer_braces = true:silent
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+
+###############################
+# C# Formatting Rules #
+###############################
+
+# New line preferences
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+# Indentation preferences
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+# Wrapping preferences
+csharp_preserve_single_line_statements = true
+csharp_preserve_single_line_blocks = true
+
diff --git a/src/DockerSdk/CertificateLoader.cs b/src/DockerSdk/CertificateLoader.cs
new file mode 100644
index 000000000..5efc7f215
--- /dev/null
+++ b/src/DockerSdk/CertificateLoader.cs
@@ -0,0 +1,20 @@
+using System.Security.Cryptography.X509Certificates;
+
+namespace DockerSdk
+{
+ internal static class CertificateLoader
+ {
+ ///
+ /// Loads certificates from a file.
+ ///
+ /// The file to load from.
+ /// The resultant collection of certificates.
+ /// The file did not represent a valid certificate.
+ public static X509Certificate2Collection Load(string path)
+ {
+ var collection = new X509Certificate2Collection();
+ collection.Import(path);
+ return collection;
+ }
+ }
+}
diff --git a/src/DockerSdk/ClientOptions.cs b/src/DockerSdk/ClientOptions.cs
new file mode 100644
index 000000000..1685832c2
--- /dev/null
+++ b/src/DockerSdk/ClientOptions.cs
@@ -0,0 +1,109 @@
+using System;
+using System.IO;
+using System.Security.Cryptography.X509Certificates;
+using Api = Docker.DotNet;
+
+namespace DockerSdk
+{
+ ///
+ /// Specifies where to find a Docker daemon and how the SDK should connect to it.
+ ///
+ ///
+ public class ClientOptions
+ {
+ ///
+ /// Gets or sets the set of certificates to use when communicating with the daemon.
+ ///
+ public X509Certificate2Collection Certificates { get; set; } = new X509Certificate2Collection();
+
+ ///
+ /// Gets or sets the credentials to use for connecting to the Docker daemon.
+ ///
+ public Api.Credentials Credentials { get; set; }
+
+ ///
+ /// Gets or sets the Docker daemon URL to connect to. The default is localhost using a platform-appropriate
+ /// transport.
+ ///
+ public Uri DaemonUri { get; set; } = Api.DockerClientConfiguration.LocalDockerUri();
+
+ ///
+ /// Gets or sets how long the SDK should wait for responses to messages it sends to the Docker daemon.
+ ///
+ /// Some SDK methods override this value.
+ public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(60);
+
+ ///
+ /// Gets or sets a value indicating whether to use TLS for communications with the daemon.
+ /// Defaults to .
+ ///
+ public bool UseTls { get; set; } = true;
+
+ ///
+ /// Generates a object based on the local machine's environment variables.
+ ///
+ /// The generated .
+ ///
+ /// If any of the relevant environment variables are not set, the object will use
+ /// the default value for its corresponding property. Note that the default for /
+ /// DOCKER_HOST is not valid for connecting to a Docker daemon.
This method uses the following
+ /// environment variables:
+ ///
+ /// -
+ /// DOCKER_HOST: The URL for the Docker daemon to connect to. Corresponds to the
+ /// property.
+ ///
+ /// -
+ /// DOCKER_CERT_PATH: A filesystem path to read certificates from. Corresponds to the property.
+ ///
+ /// -
+ /// DOCKER_TLS_VERIFY: If set, the connection will use TLS. Corresponds to the
+ /// property.
+ ///
+ /// -
+ /// COMPOSER_HTTP_TIMEOUT: The communications timeout to use, in seconds. Corresponds to the property.
+ ///
+ ///
+ ///
+ public static ClientOptions FromEnvironment()
+ {
+ var output = new ClientOptions();
+
+ var daemonUriString = Environment.GetEnvironmentVariable("DOCKER_HOST");
+ if (!string.IsNullOrEmpty(daemonUriString) && Uri.TryCreate(daemonUriString, UriKind.Absolute, out Uri daemonUri))
+ {
+ output.DaemonUri = daemonUri;
+ }
+
+ var certPath = Environment.GetEnvironmentVariable("DOCKER_CERT_PATH");
+ if (!string.IsNullOrEmpty(certPath) && Directory.Exists(certPath))
+ {
+ output.Certificates = CertificateLoader.Load(certPath);
+ }
+
+ if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOCKER_TLS_VERIFY")))
+ {
+ output.UseTls = true;
+ }
+
+ var timeoutStringInSeconds = Environment.GetEnvironmentVariable("COMPOSER_HTTP_TIMEOUT");
+ if (!string.IsNullOrEmpty(timeoutStringInSeconds) && double.TryParse(timeoutStringInSeconds, out double timeoutInSeconds))
+ {
+ output.DefaultTimeout = TimeSpan.FromSeconds(timeoutInSeconds);
+ }
+
+ return output;
+ }
+
+ ///
+ /// Creates a client configuration object for use by the underlying .NET Docker API.
+ ///
+ /// An equivalent object.
+ internal Api.DockerClientConfiguration ToCore() =>
+ new Api.DockerClientConfiguration(DaemonUri, Credentials, DefaultTimeout);
+ }
+}
diff --git a/src/DockerSdk/DockerClient.cs b/src/DockerSdk/DockerClient.cs
new file mode 100644
index 000000000..c7fdd640a
--- /dev/null
+++ b/src/DockerSdk/DockerClient.cs
@@ -0,0 +1,203 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using DockerSdk.Registries;
+
+using Core = Docker.DotNet;
+using CoreModels = Docker.DotNet.Models;
+
+namespace DockerSdk
+{
+ ///
+ /// Provides remote access to a Docker daemon.
+ ///
+ public class DockerClient : IDisposable
+ {
+ private DockerClient(Core.DockerClient core, ClientOptions options, Version negotiatedApiVersion)
+ {
+ Core = core;
+ Options = options;
+ ApiVersion = negotiatedApiVersion;
+ Registries = new RegistryAccess(this);
+ }
+
+ ///
+ /// Gets the version of the Docker API that will be used to communicate with the Docker daemon.
+ ///
+ /// This will always be the highest version that both sides support.
+ public Version ApiVersion { get; }
+
+ ///
+ /// Provides access to functionality related to Docker registries.
+ ///
+ public RegistryAccess Registries { get; }
+
+ ///
+ /// Gets the core client, which is what does all the heavy lifting for communicating with the Docker daemon.
+ ///
+ internal Core.IDockerClient Core { get; }
+
+ internal ClientOptions Options { get; }
+
+ ///
+ /// The minimum Docker API version that the SDK supports.
+ ///
+ private static readonly Version _libraryMaxApiVersion = new Version("1.41");
+
+ ///
+ /// The maximum Docker API version that the SDK supports.
+ ///
+ private static readonly Version _libraryMinApiVersion = new Version("1.41");
+
+ private bool _isDisposed;
+
+ ///
+ /// Creates a new Docker client and connects it to the local Docker daemon.
+ ///
+ /// A token used to cancel the operation.
+ /// A that completes when the connection has been established.
+ ///
+ /// The API versions that the SDK supports don't overlap with the API versions that the daemon supports.
+ ///
+ /// An internal error occurred within the daemon.
+ ///
+ /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate
+ /// validation, or timeout.
+ ///
+ public static Task StartAsync(CancellationToken ct = default)
+ => StartAsync(new ClientOptions(), ct);
+
+ ///
+ /// Creates a new Docker client and connects it to a Docker daemon.
+ ///
+ /// The URL of the Docker daemon to connect to.
+ /// A token used to cancel the operation.
+ /// A that completes when the connection has been established.
+ ///
+ /// The API versions that the SDK supports don't overlap with the API versions that the daemon supports.
+ ///
+ /// An internal error occurred within the daemon.
+ ///
+ /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate
+ /// validation, or timeout.
+ ///
+ /// The URL is .
+ public static Task StartAsync(Uri daemonUrl, CancellationToken ct = default)
+ => StartAsync(new ClientOptions { DaemonUri = daemonUrl }, ct);
+
+ ///
+ /// Creates a new Docker client and connects it to a Docker daemon.
+ ///
+ /// Details on how to connect and how the client should behave.
+ /// A token used to cancel the operation.
+ /// A that completes when the connection has been established.
+ /// is .
+ ///
+ /// The API versions that the SDK supports don't overlap with the API versions that the daemon supports.
+ ///
+ /// An internal error occurred within the daemon.
+ ///
+ /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate
+ /// validation, or timeout.
+ ///
+ public static async Task StartAsync(ClientOptions options, CancellationToken ct = default)
+ {
+ if (options is null)
+ throw new ArgumentNullException(nameof(options));
+ if (options.DaemonUri is null)
+ throw new ArgumentException("The daemon URL is required.", nameof(options));
+
+ // First, establish a connection with the daemon.
+ Core.DockerClient core = options.ToCore().CreateClient();
+
+ // Now figure out which API version to use. This will be the max API version that both sides support.
+ CoreModels.VersionResponse versionInfo = await core.System.GetVersionAsync(ct).ConfigureAwait(false);
+ var negotiatedApiVersion = DetermineVersionToUse(_libraryMinApiVersion, versionInfo, _libraryMaxApiVersion);
+
+ // Replace the non-versioned core instance with a versioned instance.
+ core.Dispose();
+ core = options.ToCore().CreateClient(negotiatedApiVersion);
+
+ // Now remove the reference to the credentials so they can drop out of memory as soon as possible.
+ options.Credentials = null;
+
+ return new DockerClient(core, options, negotiatedApiVersion);
+ }
+
+ ///
+ /// Determines which API version to use for communications between the SDK and the Docker daemon, or throws an
+ /// exception if there's no acceptable answer.
+ ///
+ /// The lowest API version that the SDK supports.
+ /// Information about what versions the daemon supports.
+ /// The highest API version that the SDK supports.
+ /// The API version to use.
+ ///
+ /// There's no overlap between what the two sides can accept.
+ ///
+ internal static Version DetermineVersionToUse(Version libraryMin, CoreModels.VersionResponse daemonInfo, Version libraryMax)
+ {
+ // Watch out for daemons that are so old that they don't even report APIVersion.
+ if (string.IsNullOrEmpty(daemonInfo.APIVersion))
+ throw new DockerVersionException($"The daemon did not report its supported API version, which likely means that it is extremely old. The SDK only supports API versions down to v{libraryMin.ToString(2)}.");
+
+ // Check that we support the daemon's max API version.
+ var daemonMaxVersion = new Version(daemonInfo.APIVersion);
+ if (daemonMaxVersion < libraryMin)
+ throw new DockerVersionException($"Version mismatch: The Docker daemon only supports API versions up to v{daemonMaxVersion.ToString(2)}, and the Docker SDK library only supports API versions down to v{libraryMin.ToString(2)}.");
+
+ // Check the daemon's min API version.
+ var daemonMinVersion = new Version(daemonInfo.MinAPIVersion);
+ if (daemonMinVersion > libraryMax)
+ throw new DockerVersionException($"Version mismatch: The Docker daemon supports API versions v{daemonMinVersion.ToString(2)} through v{daemonMaxVersion.ToString(2)}, and the Docker SDK library supports API versions v{libraryMin.ToString(2)} through v{libraryMax.ToString(2)}.");
+
+ // Use the highest version that both sides support.
+ return daemonMaxVersion < libraryMax ? daemonMaxVersion : libraryMax;
+ }
+
+ ///
+ /// Throws an exception if the negotiated API version is not in the expected range.
+ ///
+ /// The minimum allowed API version, in MAJOR.MINOR format.
+ ///
+ /// The maximum allowed API version, in MAJOR.MINOR format, or for no upper limit.
+ ///
+ /// The negotiated API version is not in the expected range.
+ /// This object has been disposed.
+ ///
+ internal void RequireApiVersion(string minVersion = null, string maxVersion = null)
+ {
+ if (_isDisposed)
+ throw new ObjectDisposedException("This object has been disposed.");
+
+ var version = ApiVersion;
+ if (minVersion != null && version < new Version(minVersion))
+ throw new NotSupportedException($"This feature is not available until API version v{minVersion}. You are currently using API version v{version.ToString(2)}.");
+ if (maxVersion != null && version > new Version(maxVersion))
+ throw new NotSupportedException($"This feature has not been available since API version v{maxVersion}. You are currently using API version v{version.ToString(2)}.");
+ }
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_isDisposed)
+ {
+ if (disposing)
+ {
+ Core?.Dispose();
+ }
+
+ _isDisposed = true;
+ }
+ }
+
+ #endregion IDisposable
+ }
+}
diff --git a/src/DockerSdk/DockerException.cs b/src/DockerSdk/DockerException.cs
new file mode 100644
index 000000000..930e73711
--- /dev/null
+++ b/src/DockerSdk/DockerException.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Net;
+using Api = Docker.DotNet;
+
+namespace DockerSdk
+{
+ ///
+ /// Base class for exceptions that are specific to the Docker client's functionality.
+ ///
+ [Serializable]
+ public class DockerException : Exception
+ {
+ public DockerException()
+ {
+ }
+
+ public DockerException(string message) : base(message)
+ {
+ }
+
+ public DockerException(string message, Exception inner)
+ : base(message + " See inner exception for details.", inner)
+ {
+ }
+
+ protected DockerException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+
+ ///
+ /// Creates a friendlier exception from an exception generated by the core API.
+ ///
+ /// The exception from the core API.
+ /// The wrapper exception.
+ internal static DockerException Wrap(Api.DockerApiException ex)
+ {
+ switch (ex.StatusCode)
+ {
+ case HttpStatusCode.BadRequest:
+ return new DockerException("The Docker daemon rejected the request because a parameter is invalid.", ex);
+
+ case HttpStatusCode.InternalServerError:
+ return new DockerException("The Docker daemon reported an internal error.", ex);
+
+ default:
+ return new DockerException($"The Docker daemon responded with unexpected status code {(int)ex.StatusCode}.", ex);
+ }
+ }
+ }
+}
diff --git a/src/DockerSdk/DockerSdk.csproj b/src/DockerSdk/DockerSdk.csproj
new file mode 100644
index 000000000..2df15f5a1
--- /dev/null
+++ b/src/DockerSdk/DockerSdk.csproj
@@ -0,0 +1,21 @@
+
+
+
+ DockerSdk
+ DockerSdk is a library that allows you to interact with the Docker Remote API programmatically with fully asynchronous, non-blocking, and object-oriented code in your .NET applications.
+ DockerSdk
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DockerSdk/DockerVersionException.cs b/src/DockerSdk/DockerVersionException.cs
new file mode 100644
index 000000000..a6c435110
--- /dev/null
+++ b/src/DockerSdk/DockerVersionException.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace DockerSdk
+{
+ ///
+ /// Indicates that API version negotiation failed because there is no overlap between the versions that the SDK
+ /// supports with the versions the Docker daemon supports.
+ ///
+ [Serializable]
+ public class DockerVersionException : DockerException
+ {
+ public DockerVersionException()
+ {
+ }
+
+ public DockerVersionException(string message) : base(message)
+ {
+ }
+
+ public DockerVersionException(string message, Exception inner) : base(message, inner)
+ {
+ }
+
+ protected DockerVersionException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/src/DockerSdk/Properties/TestAccess.cs b/src/DockerSdk/Properties/TestAccess.cs
new file mode 100644
index 000000000..16c4c6ad5
--- /dev/null
+++ b/src/DockerSdk/Properties/TestAccess.cs
@@ -0,0 +1,2 @@
+// Allow the test project to access this project's internals.
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DockerSdk.Tests")]
diff --git a/src/DockerSdk/Registries/RegistryAccess.cs b/src/DockerSdk/Registries/RegistryAccess.cs
new file mode 100644
index 000000000..2682f9551
--- /dev/null
+++ b/src/DockerSdk/Registries/RegistryAccess.cs
@@ -0,0 +1,254 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Core = Docker.DotNet;
+using CoreModels = Docker.DotNet.Models;
+
+namespace DockerSdk.Registries
+{
+ ///
+ /// Caches credentials for Docker registries and provides a means to check the credentials against the registry.
+ ///
+ public class RegistryAccess
+ {
+ internal RegistryAccess(DockerClient client)
+ {
+ _client = client;
+ AddBuiltInRegistries();
+ }
+
+ ///
+ /// Gets the Docker registries that have cache entries.
+ ///
+ public IEnumerable Registries => _entriesByServer.Keys;
+
+ private readonly DockerClient _client;
+
+ private readonly Dictionary _entriesByServer = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
+
+ ///
+ /// Specifies that you want to use anonymous access to the indicated registry.
+ ///
+ /// The name of the registry, as used in image names.
+ /// The input is null, empty, or malformatted.
+ public void AddAnonymous(string registry)
+ {
+ // Scrub the input.
+ ValidateRegistryName(registry);
+
+ // Get or add an entry for the registry.
+ if (!_entriesByServer.TryGetValue(registry, out RegistryEntry entry))
+ entry = _entriesByServer[registry] = new RegistryEntry(registry);
+
+ entry.AuthObject = new CoreModels.AuthConfig { ServerAddress = registry };
+ entry.IsAnonymous = true;
+ }
+
+ ///
+ /// Specifies that you want to use basic
+ /// authentication for access to the indicated registry.
+ ///
+ /// The name of the registry, as used in image names.
+ /// The username to use for the registry.
+ /// The password to use for the registry.
+ ///
+ /// The registry input is null, empty, or malformatted; or the username or password are null or empty.
+ ///
+ public void AddBasicAuth(string registry, string username, string password)
+ {
+ // Scrub the inputs.
+ ValidateRegistryName(registry);
+ if (string.IsNullOrEmpty(username))
+ throw new ArgumentException($"'{nameof(username)}' cannot be null or empty", nameof(username));
+ if (string.IsNullOrEmpty(password))
+ throw new ArgumentException($"'{nameof(password)}' cannot be null or empty", nameof(password));
+
+ // Get or add an entry for the registry.
+ if (!_entriesByServer.TryGetValue(registry, out RegistryEntry entry))
+ entry = _entriesByServer[registry] = new RegistryEntry(registry);
+
+ entry.AuthObject = new CoreModels.AuthConfig { ServerAddress = registry, Username = username, Password = password };
+ entry.IsAnonymous = false;
+ }
+
+ ///
+ /// Specifies that you want to use an identity token for authenticating with the indicated registry.
+ ///
+ /// The name of the registry, as used in image names.
+ /// An identity token granted by the registry.
+ ///
+ /// The registry input is null, empty, or malformatted; or the identity token is null or empty.
+ ///
+ public void AddIdentityToken(string registry, string identityToken)
+ {
+ // Scrub the inputs.
+ ValidateRegistryName(registry);
+ if (string.IsNullOrEmpty(identityToken))
+ throw new ArgumentException($"'{nameof(identityToken)}' cannot be null or empty", nameof(identityToken));
+
+ // Get or add an entry for the registry.
+ if (!_entriesByServer.TryGetValue(registry, out RegistryEntry entry))
+ entry = _entriesByServer[registry] = new RegistryEntry(registry);
+
+ entry.AuthObject = new CoreModels.AuthConfig { ServerAddress = registry, IdentityToken = identityToken };
+ entry.IsAnonymous = false;
+ }
+
+ ///
+ /// Tests whether the client can authenticate with the indicated registry.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Use the various Add* methods to supply authentication instructions. If no such instructions are
+ /// provided, this method will try anonymous access.
+ ///
+ /// The registry input is null, empty, or malformatted.
+ ///
+ /// The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate
+ /// validation, or timeout.
+ ///
+ public async Task CheckAuthenticationAsync(string registry, CancellationToken ct = default)
+ {
+ // Scrub the inputs.
+ ValidateRegistryName(registry);
+
+ // If we have an entry for that registry, get the auth object. Otherwise create a new auth object, and set a
+ // flag indicating that the registry isn't already known.
+ bool isInCache = TryGetAuthObject(registry, out CoreModels.AuthConfig authObject);
+ if (!isInCache)
+ authObject = new CoreModels.AuthConfig { ServerAddress = registry };
+
+ try
+ {
+ // Try to authenticate.
+ await _client.Core.System.AuthenticateAsync(authObject, ct).ConfigureAwait(false);
+ }
+ catch (Core.DockerApiException ex)
+ {
+ if (ex.StatusCode == HttpStatusCode.Unauthorized)
+ return false;
+ if (ex.StatusCode == HttpStatusCode.InternalServerError && ex.Message.Contains("401 Unauthorized"))
+ return false;
+ if (ex.StatusCode == HttpStatusCode.InternalServerError && ex.Message.Contains("no basic auth credentials"))
+ return false; // the registry only accepts basic auth and was given something else
+
+ throw DockerException.Wrap(ex);
+ }
+
+ // TODO after https://github.com/dotnet/Docker.DotNet/issues/493 is resolved: if we're given an auth token,
+ // clear username/pass and start using that instead
+
+ // If it wasn't in cache but it accepted anonymous access, cache it now.
+ if (!isInCache)
+ AddAnonymous(registry);
+
+ return true;
+ }
+
+ ///
+ /// Removes all custom registry entries.
+ ///
+ public void Clear()
+ {
+ _entriesByServer.Clear();
+ AddBuiltInRegistries();
+ }
+
+ ///
+ /// Parses an image's name to determine which registry the image is associated with.
+ ///
+ /// The name of image. (Not the ID.)
+ /// The registry hostname, possibly including a port.
+ public string GetRegistryName(string imageName)
+ {
+ if (string.IsNullOrEmpty(imageName))
+ throw new ArgumentException("Must not be null or empty.", nameof(imageName));
+
+ // There's always at least one non-host component, so if there's only one component we know there's no host.
+ // In that case Dockerhub is the registry.
+ var components = imageName.Split('/');
+ if (components.Length == 1)
+ return "docker.io";
+
+ // If there's a host component, it's always the first component.
+ var candidate = components[0];
+
+ // The only components that are allowed a : character are host components and final components. We're
+ // looking at a non-final component, so if we find a : we know that we have a registry.
+ if (candidate.Contains(':'))
+ return candidate;
+
+ // Only host components are allowed to use uppercase letters, so that's the next most straightforward way to
+ // test.
+ if (candidate.Any(char.IsUpper))
+ return candidate;
+
+ // The remaining criteria are ambiguous, so we're basically making educated guesses. First, check for
+ // well-known references to the current host. Note that we don't need to check the IP v6 loopback address
+ // because it can't be part of a valid image name.
+ if (candidate == "localhost" || candidate == "127.0.0.1")
+ return candidate;
+
+ // Otherwise, if we have a cached registry with this name, treat it as a registry.
+ if (_entriesByServer.ContainsKey(candidate))
+ return candidate;
+
+ // Otherwise, if it contains any . characters, assume that it's a hostname.
+ if (candidate.Contains('.'))
+ return candidate;
+
+ // Otherwise, assume that it isn't a host name, in which case we use Dockerhub.
+ return "docker.io";
+ }
+
+ ///
+ /// Removes an entry from the cache.
+ ///
+ /// The host name of the registry to remove.
+ /// True if the entry was removed, or false if the entry was not present.
+ /// This method is equivalent to docker logout.
+ public bool Remove(string registry) => _entriesByServer.Remove(registry);
+
+ ///
+ /// Gets the auth information for the given image's registry.
+ ///
+ /// The name of image. ( Not the ID.)
+ /// Auth details for communicating with the image's registry, if known.
+ internal bool TryGetAuthObject(string registry, out CoreModels.AuthConfig authObject)
+ {
+ if (!_entriesByServer.TryGetValue(registry, out RegistryEntry entry))
+ {
+ authObject = null;
+ return false;
+ }
+
+ authObject = entry.AuthObject;
+ return true;
+ }
+
+ private static void ValidateRegistryName(string registry)
+ {
+ if (string.IsNullOrEmpty(registry))
+ throw new ArgumentException($"'{nameof(registry)}' cannot be null or empty", nameof(registry));
+ if (registry.Contains("//"))
+ throw new ArgumentException("The registry name must not include the protocol.", nameof(registry));
+ if (registry.Contains('/'))
+ throw new ArgumentException("The registry name must not include a path.", nameof(registry));
+ }
+
+ private void AddBuiltInRegistries()
+ {
+ // These common registries allow anonymous access for public images.
+ AddAnonymous("docker.io"); // Dockerhub
+ AddAnonymous("ghcr.io"); // Github
+ AddAnonymous("mcr.microsoft.com"); // Microsoft Azure
+ AddAnonymous("public.ecr.aws"); // Amazon AWS
+ }
+ }
+}
diff --git a/src/DockerSdk/Registries/RegistryEntry.cs b/src/DockerSdk/Registries/RegistryEntry.cs
new file mode 100644
index 000000000..947b8d563
--- /dev/null
+++ b/src/DockerSdk/Registries/RegistryEntry.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Text;
+using Newtonsoft.Json;
+using CoreModels = Docker.DotNet.Models;
+
+namespace DockerSdk.Registries
+{
+ ///
+ /// Holds authentication information for a Docker registry.
+ ///
+ internal class RegistryEntry
+ {
+ public RegistryEntry(string server)
+ {
+ Server = server;
+ }
+
+ ///
+ /// Gets or sets the auth object that the core API uses for authentication.
+ ///
+ public CoreModels.AuthConfig AuthObject
+ {
+ get => Decode(encodedAuthData);
+ set => encodedAuthData = Encode(value);
+ }
+
+ public bool IsAnonymous { get; set; }
+
+ public string Server { get; }
+
+ private string encodedAuthData;
+
+ ///
+ /// De-obfuscates an auth object.
+ ///
+ /// An obsfucated, serialized form of the given auth details.
+ /// An object holding the auth details.
+ private static CoreModels.AuthConfig Decode(string codedForm)
+ {
+ var json = Encoding.UTF8.GetString(Convert.FromBase64String(codedForm));
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ ///
+ /// Obsfucates the auth object. This doesn't add much real security, but at least the passwords won't be plainly
+ /// visible in memory dumps.
+ ///
+ /// An object holding the auth details.
+ /// An obsfucated, serialized form of the given auth details.
+ private static string Encode(CoreModels.AuthConfig auth)
+ {
+ var json = JsonConvert.SerializeObject(auth);
+ return Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
+ }
+ }
+}
diff --git a/test/DockerSdk.Tests/.editorconfig b/test/DockerSdk.Tests/.editorconfig
new file mode 100644
index 000000000..b82d1b0e9
--- /dev/null
+++ b/test/DockerSdk.Tests/.editorconfig
@@ -0,0 +1,123 @@
+# To learn more about .editorconfig see https://aka.ms/editorconfigdocs
+
+###############################
+# Core EditorConfig Options #
+###############################
+
+# inherited
+
+###############################
+# .NET Coding Conventions #
+###############################
+
+[*.cs]
+
+# Organize usings
+dotnet_sort_system_directives_first = true
+# this. preferences
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_property = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_event = false:silent
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+dotnet_style_readonly_field = true:suggestion
+# Expression-level preferences
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+
+###############################
+# Naming Conventions #
+###############################
+
+# Style Definitions
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+# Use PascalCase for constant fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+
+###############################
+# C# Coding Conventions #
+###############################
+
+[*.cs]
+
+# var preferences
+csharp_style_var_for_built_in_types = true:silent
+csharp_style_var_when_type_is_apparent = true:silent
+csharp_style_var_elsewhere = true:silent
+# Expression-bodied members
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_accessors = true:silent
+# Pattern matching preferences
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+# Null-checking preferences
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+# Modifier preferences
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
+# Expression-level preferences
+csharp_prefer_braces = true:silent
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+
+###############################
+# C# Formatting Rules #
+###############################
+
+# New line preferences
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+# Indentation preferences
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+# Wrapping preferences
+csharp_preserve_single_line_statements = true
+csharp_preserve_single_line_blocks = true
+
diff --git a/test/DockerSdk.Tests/Cli.cs b/test/DockerSdk.Tests/Cli.cs
new file mode 100644
index 000000000..3635999da
--- /dev/null
+++ b/test/DockerSdk.Tests/Cli.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace DockerSdk.Tests
+{
+ ///
+ /// Runs commands on in a PowerShell command line. This is meant for setting up tests via the docker CLI.
+ ///
+ internal static class Cli
+ {
+ public static string ReadAndDeleteSingleLineFile(string path)
+ {
+ var line = File.ReadAllLines(path)[0].Trim();
+ File.Delete(path);
+ return line;
+ }
+
+ ///
+ /// Runs a series of commands in a Powershell prompt.
+ ///
+ /// An array where each line is a command to run.
+ /// An array of lines written to stdout across all commands.
+ ///
+ /// If any command returns a non-zero exit code or writes to stderr, the subsequent commands do not run.
+ /// Use of stdin is not supported.
+ ///
+ ///
+ /// One of the commands either wrote to stdout or returned a non-zero exit code.
+ ///
+ public static string[] Run(params string[] commands)
+ => Run(false, commands);
+
+ ///
+ /// Runs a series of commands in a Powershell prompt.
+ ///
+ /// True to ignore any errors that the commands may raise.
+ /// An array where each line is a command to run.
+ /// An array of lines written to stdout across all commands.
+ ///
+ /// If any command returns a non-zero exit code or writes to stderr, the subsequent commands do not run, unless
+ /// is true.
Use of stdin is not supported.
+ ///
+ ///
+ /// One of the commands either wrote to stdout or returned a non-zero exit code, and was false.
+ ///
+ public static string[] Run(bool ignoreErrors, params string[] commands)
+ {
+ var output = new List();
+ foreach (var command in commands)
+ output.AddRange(Run(command, ignoreErrors));
+ return output.ToArray();
+ }
+
+ ///
+ /// Runs a command in a Powershell prompt.
+ ///
+ /// The command to run.
+ /// An array of lines written to stdout.
+ ///
+ /// The command either wrote to stdout or returned a non-zero exit code.
+ ///
+ public static string[] Run(string command)
+ => Run(command, false);
+
+ ///
+ /// Runs a command in a Powershell prompt.
+ ///
+ /// The command to run.
+ /// True to ignore any errors that the command may raise.
+ /// An array of lines written to stdout.
+ ///
+ /// The command either wrote to stdout or returned a non-zero exit code, and is
+ /// false.
+ ///
+ public static string[] Run(string command, bool ignoreErrors)
+ {
+ var pi = new ProcessStartInfo("pwsh.exe", "-Command -")
+ {
+ CreateNoWindow = true,
+ LoadUserProfile = false,
+ UseShellExecute = false, // needed for .Net Framework, which defaults it to true
+ RedirectStandardInput = true,
+ RedirectStandardError = true,
+ RedirectStandardOutput = true,
+ };
+ using var process = Process.Start(pi);
+
+ process.StandardInput.WriteLine(command);
+ process.StandardInput.WriteLine("exit");
+ process.WaitForExit();
+
+ if (!ignoreErrors)
+ {
+ var errors = process.StandardError.ReadToEnd();
+ if (!string.IsNullOrEmpty(errors))
+ throw new InvalidOperationException(errors);
+
+ if (process.ExitCode != 0)
+ throw new InvalidOperationException($"The process exited with code {process.ExitCode}.");
+ }
+
+ return process.StandardOutput.ReadToEnd()
+ .Split('\n')
+ .Select(line => line.Trim())
+ .ToArray();
+ }
+ }
+}
diff --git a/test/DockerSdk.Tests/ClientTests.cs b/test/DockerSdk.Tests/ClientTests.cs
new file mode 100644
index 000000000..2c0069347
--- /dev/null
+++ b/test/DockerSdk.Tests/ClientTests.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Xunit;
+
+namespace DockerSdk.Tests
+{
+ [Collection("Common")]
+ public class ClientTests
+ {
+ [Fact]
+ public async Task ConnectAsync_Defaults_HasCorrectApiVersion()
+ {
+ using var client = await DockerClient.StartAsync();
+ client.ApiVersion.ToString().Should().Be("1.41");
+ }
+
+ [Fact]
+ public async Task ConnectAsync_Defaults_ReturnsClientObject()
+ {
+ using var docker = await DockerClient.StartAsync();
+
+ docker.Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task ConnectAsync_NoSuchPlace_Throws()
+ {
+ await Assert.ThrowsAsync(
+ () => DockerClient.StartAsync(new Uri("http://localhost:123")));
+ }
+ }
+}
diff --git a/test/DockerSdk.Tests/ClientUnitTests.cs b/test/DockerSdk.Tests/ClientUnitTests.cs
new file mode 100644
index 000000000..4553e895f
--- /dev/null
+++ b/test/DockerSdk.Tests/ClientUnitTests.cs
@@ -0,0 +1,125 @@
+using System;
+using FluentAssertions;
+using Xunit;
+using CoreModels = Docker.DotNet.Models;
+
+namespace DockerSdk.Tests
+{
+ // These tests can be done in parallel with other tests.
+ public class ClientUnitTests
+ {
+ [Fact]
+ public void DetermineVersionToUse_DaemonRangeEngulfsSdkRange_ReturnsSdkMax()
+ {
+ var input = new CoreModels.VersionResponse
+ {
+ MinAPIVersion = "1.11",
+ APIVersion = "1.91",
+ };
+ var libMin = new Version("1.31");
+ var libMax = new Version("1.71");
+
+ var result = DockerClient.DetermineVersionToUse(libMin, input, libMax);
+
+ result.ToString().Should().Be("1.71");
+ }
+
+ [Fact]
+ public void DetermineVersionToUse_ExactlyOneVersionEach_SameVersion_ReturnsThatVersion()
+ {
+ var input = new CoreModels.VersionResponse
+ {
+ MinAPIVersion = "1.23",
+ APIVersion = "1.23",
+ };
+ var libMin = new Version("1.23");
+ var libMax = new Version("1.23");
+
+ var result = DockerClient.DetermineVersionToUse(libMin, input, libMax);
+
+ result.ToString().Should().Be("1.23");
+ }
+
+ [Fact]
+ public void DetermineVersionToUse_NoOverlap_DaemonHigher_Throws()
+ {
+ var input = new CoreModels.VersionResponse
+ {
+ MinAPIVersion = "1.71",
+ APIVersion = "1.91",
+ };
+ var libMin = new Version("1.11");
+ var libMax = new Version("1.31");
+
+ var ex = Assert.Throws(
+ () => DockerClient.DetermineVersionToUse(libMin, input, libMax));
+
+ ex.Message.Should().Be("Version mismatch: The Docker daemon supports API versions v1.71 through v1.91, and the Docker SDK library supports API versions v1.11 through v1.31.");
+ }
+
+ [Fact]
+ public void DetermineVersionToUse_NoOverlap_SdkHigher_Throws()
+ {
+ var input = new CoreModels.VersionResponse
+ {
+ MinAPIVersion = "1.11",
+ APIVersion = "1.31",
+ };
+ var libMin = new Version("1.71");
+ var libMax = new Version("1.91");
+
+ var ex = Assert.Throws(
+ () => DockerClient.DetermineVersionToUse(libMin, input, libMax));
+
+ ex.Message.Should().Be("Version mismatch: The Docker daemon only supports API versions up to v1.31, and the Docker SDK library only supports API versions down to v1.71.");
+ }
+
+ [Fact]
+ public void DetermineVersionToUse_Overlap_DaemonHigher_ReturnsSdkMax()
+ {
+ var input = new CoreModels.VersionResponse
+ {
+ MinAPIVersion = "1.31",
+ APIVersion = "1.91",
+ };
+ var libMin = new Version("1.11");
+ var libMax = new Version("1.71");
+
+ var result = DockerClient.DetermineVersionToUse(libMin, input, libMax);
+
+ result.ToString().Should().Be("1.71");
+ }
+
+ [Fact]
+ public void DetermineVersionToUse_Overlap_SdkHigher_ReturnsDaemonMax()
+ {
+ var input = new CoreModels.VersionResponse
+ {
+ MinAPIVersion = "1.11",
+ APIVersion = "1.71",
+ };
+ var libMin = new Version("1.31");
+ var libMax = new Version("1.91");
+
+ var result = DockerClient.DetermineVersionToUse(libMin, input, libMax);
+
+ result.ToString().Should().Be("1.71");
+ }
+
+ [Fact]
+ public void DetermineVersionToUse_SdkRangeEngulfsDaemonRange_ReturnsDaemonMax()
+ {
+ var input = new CoreModels.VersionResponse
+ {
+ MinAPIVersion = "1.31",
+ APIVersion = "1.71",
+ };
+ var libMin = new Version("1.11");
+ var libMax = new Version("1.91");
+
+ var result = DockerClient.DetermineVersionToUse(libMin, input, libMax);
+
+ result.ToString().Should().Be("1.71");
+ }
+ }
+}
diff --git a/test/DockerSdk.Tests/DockerSdk.Tests.csproj b/test/DockerSdk.Tests/DockerSdk.Tests.csproj
new file mode 100644
index 000000000..17e3ced7b
--- /dev/null
+++ b/test/DockerSdk.Tests/DockerSdk.Tests.csproj
@@ -0,0 +1,39 @@
+
+
+
+ netcoreapp3.1
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/test/DockerSdk.Tests/Fixture.cs b/test/DockerSdk.Tests/Fixture.cs
new file mode 100644
index 000000000..f745f0bdb
--- /dev/null
+++ b/test/DockerSdk.Tests/Fixture.cs
@@ -0,0 +1,26 @@
+using System;
+using Xunit;
+
+namespace DockerSdk.Tests
+{
+ public sealed class Fixture : IDisposable
+ {
+ public Fixture()
+ {
+ // Start up the test environment.
+ Cli.Run("cd scripts && docker-compose up --build --detach --no-color", ignoreErrors: true);
+ }
+
+ public void Dispose()
+ {
+ // Shut down the test environment.
+ Cli.Run("cd scripts && docker-compose down", ignoreErrors: true);
+ }
+ }
+
+ [CollectionDefinition("Common")]
+ public class FixtureCollection : ICollectionFixture
+ {
+ // This class is never instantiated. It's simply a marker class used by XUnit.
+ }
+}
diff --git a/test/DockerSdk.Tests/RegistryAccessTests.cs b/test/DockerSdk.Tests/RegistryAccessTests.cs
new file mode 100644
index 000000000..8088aacae
--- /dev/null
+++ b/test/DockerSdk.Tests/RegistryAccessTests.cs
@@ -0,0 +1,110 @@
+using System.Threading.Tasks;
+using FluentAssertions;
+using Xunit;
+
+namespace DockerSdk.Tests
+{
+ [Collection("Common")]
+ public class RegistryAccessTests
+ {
+ private const string BasicAuthRegistry = "localhost:5001";
+ private const string OpenRegistry = "localhost:4000";
+
+ [Fact]
+ public async Task CheckAuthenticationAsync_AnonymousAccess_ToBasicAuthRegistry_Fails()
+ {
+ using var client = await DockerClient.StartAsync();
+ client.Registries.AddAnonymous(BasicAuthRegistry);
+
+ var result = await client.Registries.CheckAuthenticationAsync(BasicAuthRegistry);
+
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task CheckAuthenticationAsync_AnonymousAccess_ToDockerhub_Suceeds()
+ {
+ using var client = await DockerClient.StartAsync();
+
+ var result = await client.Registries.CheckAuthenticationAsync("docker.io");
+
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task CheckAuthenticationAsync_AnonymousAccess_ToRecognizedOpenRegistry_Suceeds()
+ {
+ using var client = await DockerClient.StartAsync();
+ client.Registries.AddAnonymous(OpenRegistry);
+
+ var result = await client.Registries.CheckAuthenticationAsync(OpenRegistry);
+
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task CheckAuthenticationAsync_AnonymousAccess_ToUnrecognizedOpenRegistry_Suceeds()
+ {
+ using var client = await DockerClient.StartAsync();
+
+ var result = await client.Registries.CheckAuthenticationAsync(OpenRegistry);
+
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task CheckAuthenticationAsync_BasicAuth_ToOpenRegistry_Succeeds()
+ {
+ using var client = await DockerClient.StartAsync();
+ client.Registries.AddBasicAuth(OpenRegistry, "donald", "duck");
+
+ var result = await client.Registries.CheckAuthenticationAsync(OpenRegistry);
+
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task CheckAuthenticationAsync_CorrectCredentials_ToBasicAuthRegistry_Fails()
+ {
+ using var client = await DockerClient.StartAsync();
+ client.Registries.AddBasicAuth(BasicAuthRegistry, "testuser", "testpassword");
+
+ var result = await client.Registries.CheckAuthenticationAsync(BasicAuthRegistry);
+
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task CheckAuthenticationAsync_IncorrectCredentials_ToBasicAuthRegistry_Fails()
+ {
+ using var client = await DockerClient.StartAsync();
+ client.Registries.AddBasicAuth(BasicAuthRegistry, "wronguser", "wrongpassword");
+
+ var result = await client.Registries.CheckAuthenticationAsync(BasicAuthRegistry);
+
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task CheckAuthenticationAsync_MadeUpAuthToken_ToBasicAuthRegistry_Fails()
+ {
+ using var client = await DockerClient.StartAsync();
+ client.Registries.AddIdentityToken(BasicAuthRegistry, "faketoken");
+
+ var result = await client.Registries.CheckAuthenticationAsync("localhost:5001");
+
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task CheckAuthenticationAsync_MadeUpAuthToken_ToOpenRegistry_Succeeds()
+ {
+ using var client = await DockerClient.StartAsync();
+ client.Registries.AddIdentityToken(OpenRegistry, "faketoken");
+
+ var result = await client.Registries.CheckAuthenticationAsync(OpenRegistry);
+
+ result.Should().BeTrue();
+ }
+ }
+}
diff --git a/test/DockerSdk.Tests/RegistryAccessUnitTests.cs b/test/DockerSdk.Tests/RegistryAccessUnitTests.cs
new file mode 100644
index 000000000..ff8da1983
--- /dev/null
+++ b/test/DockerSdk.Tests/RegistryAccessUnitTests.cs
@@ -0,0 +1,104 @@
+using DockerSdk.Registries;
+using FluentAssertions;
+using Xunit;
+
+using CoreModels = Docker.DotNet.Models;
+
+namespace DockerSdk.Tests
+{
+ // These tests can run in parallel with other tests.
+ public class RegistryAccessUnitTests
+ {
+ [Fact]
+ public void AddAnonymous_AddsEmptyAuthObject()
+ {
+ var access = new RegistryAccess(null);
+
+ access.AddAnonymous("abc");
+
+ access.TryGetAuthObject("abc", out CoreModels.AuthConfig actual).Should().BeTrue(); ;
+ actual.Should().BeEquivalentTo(new CoreModels.AuthConfig { ServerAddress = "abc" });
+ }
+
+ [Fact]
+ public void AddBasicAuth_AddsUserPasswordAuthObject()
+ {
+ var access = new RegistryAccess(null);
+
+ access.AddBasicAuth("abc", "donald", "duck");
+
+ access.TryGetAuthObject("abc", out CoreModels.AuthConfig actual).Should().BeTrue(); ;
+ actual.Should().BeEquivalentTo(new CoreModels.AuthConfig { ServerAddress = "abc", Username = "donald", Password = "duck" });
+ }
+
+ [Fact]
+ public void AddBasicAuth_ToAnonymousRegistry_ConvertsEntryToBasicAuth()
+ {
+ var access = new RegistryAccess(null);
+
+ access.AddBasicAuth("docker.io", "donald", "duck");
+
+ access.TryGetAuthObject("docker.io", out CoreModels.AuthConfig actual).Should().BeTrue(); ;
+ actual.Should().BeEquivalentTo(new CoreModels.AuthConfig { ServerAddress = "docker.io", Username = "donald", Password = "duck" });
+ }
+
+ [Fact]
+ public void AddIdentityToken_AddsIdentityTokenAuthObject()
+ {
+ var access = new RegistryAccess(null);
+
+ access.AddIdentityToken("abc", "123-456-789");
+
+ access.TryGetAuthObject("abc", out CoreModels.AuthConfig actual).Should().BeTrue(); ;
+ actual.Should().BeEquivalentTo(new CoreModels.AuthConfig { ServerAddress = "abc", IdentityToken = "123-456-789" });
+ }
+
+ [Fact]
+ public void Clear_PreservesBuiltIns()
+ {
+ var access = new RegistryAccess(null);
+
+ access.Clear();
+
+ access.Registries.Should().Contain("docker.io");
+ }
+
+ [Theory]
+ [InlineData("test", "docker.io")]
+ [InlineData("example.com/test", "example.com")]
+ [InlineData("example:123/test", "example:123")]
+ [InlineData("example/test:123", "docker.io")]
+ [InlineData("example.com", "docker.io")]
+ [InlineData("example.com:123", "docker.io")]
+ [InlineData("Example/test", "Example")]
+ [InlineData("localhost/test", "localhost")]
+ public void GetRegistryName_ProducesExpectedOutput(string imageName, string expected)
+ {
+ var access = new RegistryAccess(null);
+
+ var actual = access.GetRegistryName(imageName);
+
+ actual.Should().Be(expected);
+ }
+
+ [Fact]
+ public void Remove_RemovesEntry()
+ {
+ var access = new RegistryAccess(null);
+
+ access.Remove("docker.io");
+
+ access.TryGetAuthObject("docker.io", out _).Should().BeFalse();
+ }
+
+ [Fact]
+ public void TryGetAuthObject_UnknownRegistry_ReturnsFalse()
+ {
+ var access = new RegistryAccess(null);
+
+ var result = access.TryGetAuthObject("example.com", out _);
+
+ result.Should().BeFalse();
+ }
+ }
+}
diff --git a/test/DockerSdk.Tests/scripts/docker-compose.yml b/test/DockerSdk.Tests/scripts/docker-compose.yml
new file mode 100644
index 000000000..49b9ebd0e
--- /dev/null
+++ b/test/DockerSdk.Tests/scripts/docker-compose.yml
@@ -0,0 +1,18 @@
+# Sets up the docker containers needed by the project's tests.
+version: '3.9'
+
+services:
+
+ registry-open:
+ build: registry-open
+ image: ddnt:registry-open
+ container_name: ddnt-registry-open
+ ports:
+ - "4000:80"
+
+ registry-htpasswd-tls:
+ build: registry-htpasswd-tls
+ image: ddnt:registry-htpasswd-tls
+ container_name: ddnt-registry-htpasswd-tls
+ ports:
+ - "5001:443"
diff --git a/test/DockerSdk.Tests/scripts/registry-htpasswd-tls/Dockerfile b/test/DockerSdk.Tests/scripts/registry-htpasswd-tls/Dockerfile
new file mode 100644
index 000000000..46d308535
--- /dev/null
+++ b/test/DockerSdk.Tests/scripts/registry-htpasswd-tls/Dockerfile
@@ -0,0 +1,22 @@
+# Creates an image that hosts a Docker registry. The registry uses basic
+# authentication with TLS. The username is "testuser" and the password is
+# "testpassword".
+
+FROM alpine AS build
+RUN apk update
+RUN apk add openssl
+RUN openssl req -newkey rsa:2048 -nodes -keyout sdk-test.key -x509 -days 3650 -out sdk-test.crt -subj "/C=US/ST=Wisconsin/L=Madison/O=Emdot/CN=docker-net-sdk-tests"
+RUN apk add apache2-utils
+RUN htpasswd -Bbn testuser testpassword > htpasswd
+
+FROM registry:2
+RUN mkdir /certs
+COPY --from=build sdk-test.* /certs/
+RUN mkdir /auth
+COPY --from=build htpasswd /auth/
+ENV REGISTRY_HTTP_ADDR=0.0.0.0:443
+ENV REGISTRY_HTTP_TLS_CERTIFICATE=/certs/sdk-test.crt
+ENV REGISTRY_HTTP_TLS_KEY=/certs/sdk-test.key
+ENV REGISTRY_AUTH=htpasswd
+ENV REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm"
+ENV REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
diff --git a/test/DockerSdk.Tests/scripts/registry-open/Dockerfile b/test/DockerSdk.Tests/scripts/registry-open/Dockerfile
new file mode 100644
index 000000000..58d4034ed
--- /dev/null
+++ b/test/DockerSdk.Tests/scripts/registry-open/Dockerfile
@@ -0,0 +1,3 @@
+# Creates an image that hosts a Docker registry. It accepts all authentication attempts.
+FROM registry:2
+ENV REGISTRY_HTTP_ADDR=0.0.0.0:80
\ No newline at end of file
From b34e3502defac16cc96bf65fb92c4f40abfd67c3 Mon Sep 17 00:00:00 2001
From: Matt Morrison <3241034+Emdot@users.noreply.github.com>
Date: Mon, 22 Feb 2021 18:29:31 -0600
Subject: [PATCH 2/3] Don''t set LoadUserProfile, because it's Windows-only.
---
test/DockerSdk.Tests/Cli.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/test/DockerSdk.Tests/Cli.cs b/test/DockerSdk.Tests/Cli.cs
index 3635999da..5fbe1c89d 100644
--- a/test/DockerSdk.Tests/Cli.cs
+++ b/test/DockerSdk.Tests/Cli.cs
@@ -81,7 +81,6 @@ public static string[] Run(string command, bool ignoreErrors)
var pi = new ProcessStartInfo("pwsh.exe", "-Command -")
{
CreateNoWindow = true,
- LoadUserProfile = false,
UseShellExecute = false, // needed for .Net Framework, which defaults it to true
RedirectStandardInput = true,
RedirectStandardError = true,
From da2e268b99dba7a2c3db7ea8b3dfa60826e73ede Mon Sep 17 00:00:00 2001
From: Matt Morrison <3241034+Emdot@users.noreply.github.com>
Date: Tue, 23 Feb 2021 18:59:54 -0600
Subject: [PATCH 3/3] Fixed CLI.cs to work on non-Windows OSes.
---
test/DockerSdk.Tests/Cli.cs | 58 +++++++++++++++++++++++++++----------
1 file changed, 42 insertions(+), 16 deletions(-)
diff --git a/test/DockerSdk.Tests/Cli.cs b/test/DockerSdk.Tests/Cli.cs
index 5fbe1c89d..a26986638 100644
--- a/test/DockerSdk.Tests/Cli.cs
+++ b/test/DockerSdk.Tests/Cli.cs
@@ -78,7 +78,15 @@ public static string[] Run(string command)
///
public static string[] Run(string command, bool ignoreErrors)
{
- var pi = new ProcessStartInfo("pwsh.exe", "-Command -")
+ // Get the name of the PowerShell executable, which depends on the platform.
+ string powershellCommand
+ = Environment.OSVersion.Platform == PlatformID.Win32NT
+ ? "pwsh.exe"
+ : "pwsh";
+
+ // Declare how to start the process. The trailing - in the parameters means
+ // to read the command from stdin.
+ var pi = new ProcessStartInfo(powershellCommand, "-Command -")
{
CreateNoWindow = true,
UseShellExecute = false, // needed for .Net Framework, which defaults it to true
@@ -86,26 +94,44 @@ public static string[] Run(string command, bool ignoreErrors)
RedirectStandardError = true,
RedirectStandardOutput = true,
};
- using var process = Process.Start(pi);
- process.StandardInput.WriteLine(command);
- process.StandardInput.WriteLine("exit");
- process.WaitForExit();
+ // Start the process.
+ Process process;
+ try
+ {
+ process = Process.Start(pi);
+ }
+ catch (System.ComponentModel.Win32Exception ex)
+ {
+ throw new InvalidOperationException($"Failed to start PowerShell. Is it installed and in the PATH? (Command: {pi.FileName} {pi.Arguments})", ex);
+ }
- if (!ignoreErrors)
+ using (process)
{
- var errors = process.StandardError.ReadToEnd();
- if (!string.IsNullOrEmpty(errors))
- throw new InvalidOperationException(errors);
+ // Feed the command to PowerShell via stdin.
+ process.StandardInput.WriteLine(command);
- if (process.ExitCode != 0)
- throw new InvalidOperationException($"The process exited with code {process.ExitCode}.");
- }
+ // Shut down the process.
+ process.StandardInput.WriteLine("exit");
+ process.WaitForExit();
+
+ // Throw an exception if errors were detected. (Unless supressed.)
+ if (!ignoreErrors)
+ {
+ var errors = process.StandardError.ReadToEnd();
+ if (!string.IsNullOrEmpty(errors))
+ throw new InvalidOperationException(errors);
- return process.StandardOutput.ReadToEnd()
- .Split('\n')
- .Select(line => line.Trim())
- .ToArray();
+ if (process.ExitCode != 0)
+ throw new InvalidOperationException($"The process exited with code {process.ExitCode}.");
+ }
+
+ // Return the text that the process wrote to stdout.
+ return process.StandardOutput.ReadToEnd()
+ .Split('\n')
+ .Select(line => line.Trim())
+ .ToArray();
+ }
}
}
}