diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f426961b..0559e652 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,21 +8,139 @@ on:
jobs:
build:
runs-on: ubuntu-24.04
+ services:
+ # Docker without TLS (plain TCP) !DEPRECATED! with next docker release
+ docker-without-tls:
+ image: docker:29.1.1-dind
+ env:
+ DOCKER_TLS_CERTDIR: ""
+ ports:
+ - 2375:2375
+ options: >-
+ --privileged
+
+ # Docker with TLS (secure TCP)
+ docker-with-tls:
+ image: docker:29.1.1-dind
+ env:
+ DOCKER_TLS_CERTDIR: /certs
+ ports:
+ - 2376:2376
+ options: >-
+ --privileged
+ volumes:
+ - /home/runner/certs:/certs
+
strategy:
+ fail-fast: false
matrix:
- framework:
- - net8.0
- - net9.0
- - net10.0
+ dotnet:
+ - sdk: 8.x
+ tfm: net8.0
+ - sdk: 9.x
+ tfm: net9.0
+ - sdk: 10.x
+ tfm: net10.0
+ docker:
+ - name: unix
+ docker_host: unix:///var/run/docker.sock
+ tls_verify: ""
+ cert_path: ""
+ native_http: 0
+ needs_dind: false
+ - name: tcp-2375
+ docker_host: tcp://localhost:2375
+ tls_verify: ""
+ cert_path: ""
+ native_http: 0
+ needs_dind: true
+ - name: tcp-2376-tls
+ docker_host: tcp://localhost:2376
+ tls_verify: 1
+ cert_path: /home/runner/certs/client
+ native_http: 0
+ needs_dind: true
+ - name: tcp-2375-native
+ docker_host: tcp://localhost:2375
+ tls_verify: ""
+ cert_path: ""
+ native_http: 1
+ needs_dind: true
+ - name: tcp-2376-tls-native
+ docker_host: tcp://localhost:2376
+ tls_verify: 1
+ cert_path: /home/runner/certs/client
+ native_http: 1
+ needs_dind: true
+
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
fetch-depth: 0
+
- name: Setup .NET Core
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
- dotnet-version: 10.x
+ dotnet-version: ${{ matrix.dotnet.sdk }}
+
- name: Build
- run: dotnet build -c Release --framework ${{ matrix.framework }}
- - name: Test
- run: dotnet test -c Release --framework ${{ matrix.framework }} --no-build --logger console
+ run: >-
+ dotnet build
+ --configuration Release
+ --framework ${{ matrix.dotnet.tfm }}
+
+ - name: Create client PKCS#12 bundle
+ if: ${{ matrix.docker.tls_verify == 1 }}
+ run: |
+ sudo chown -R $USER:$USER $HOME/certs
+ openssl pkcs12 -export \
+ -out "$HOME/certs/client/client.pfx" \
+ -inkey "$HOME/certs/client/key.pem" \
+ -in "$HOME/certs/client/cert.pem" \
+ -certfile "$HOME/certs/client/ca.pem" \
+ -passout pass:
+
+ - name: Wait for Docker to be healthy (2375)
+ if: ${{ matrix.docker.needs_dind && matrix.docker.docker_host == 'tcp://localhost:2375' }}
+ run: |
+ for i in {1..10}; do
+ if docker --host=tcp://localhost:2375 version; then
+ echo "Docker is ready on port 2375"
+ exit 0
+ fi
+ echo "Waiting for Docker on port 2375..."
+ sleep 3
+ done
+ echo "Docker on port 2375 did not become ready in time."
+ exit 1
+
+ - name: Wait for Docker to be healthy (2376)
+ if: ${{ matrix.docker.needs_dind && matrix.docker.docker_host == 'tcp://localhost:2376' }}
+ run: |
+ for i in {1..10}; do
+ if docker --host=tcp://localhost:2376 --tlsverify \
+ --tlscacert="$HOME/certs/client/ca.pem" \
+ --tlscert="$HOME/certs/client/cert.pem" \
+ --tlskey="$HOME/certs/client/key.pem" version; then
+ echo "Docker is ready on port 2376"
+ exit 0
+ fi
+ echo "Waiting for Docker on port 2376..."
+ sleep 3
+ done
+ echo "Docker on port 2376 did not become ready in time."
+ exit 1
+
+ - name: Test (${{ matrix.docker.name }})
+ run: >-
+ dotnet test
+ --configuration Release
+ --framework ${{ matrix.dotnet.tfm }}
+ --no-restore
+ --no-build
+ --logger console
+ env:
+ DOCKER_HOST: ${{ matrix.docker.docker_host }}
+ DOCKER_TLS_VERIFY: ${{ matrix.docker.tls_verify }}
+ DOCKER_CERT_PATH: ${{ matrix.docker.cert_path }}
+ DOCKER_DOTNET_NATIVE_HTTP_ENABLED: ${{ matrix.docker.native_http }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index e2464a2e..927743e3 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -12,11 +12,11 @@ jobs:
build:
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup .NET Core
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.x
- name: Install NBGV tool
@@ -28,7 +28,7 @@ jobs:
- name: Push packages to NuGet.org
run: dotnet nuget push ./packages/Docker.DotNet.*.nupkg --skip-duplicate -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json
- name: Create Release
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
script: |
github.rest.repos.createRelease({
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 00000000..c8be7ea0
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,18 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Docker.DotNet.sln b/Docker.DotNet.sln
index 158d90e7..fb833bc3 100644
--- a/Docker.DotNet.sln
+++ b/Docker.DotNet.sln
@@ -1,19 +1,30 @@
-
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26228.9
+# Visual Studio Version 17
+VisualStudioVersion = 17.10.35201.131
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{85990620-78A6-4381-8BD6-84E6D0CF0649}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet", "src\Docker.DotNet\Docker.DotNet.csproj", "{C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.BasicAuth", "src\Docker.DotNet.BasicAuth\Docker.DotNet.BasicAuth.csproj", "{E1F24B25-E027-45E0-A6E1-E08138F1F95D}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.BasicAuth", "src\Docker.DotNet.BasicAuth\Docker.DotNet.BasicAuth.csproj", "{E1F24B25-E027-45E0-A6E1-E08138F1F95D}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.Handler.Abstractions", "src\Docker.DotNet.Handler.Abstractions\Docker.DotNet.Handler.Abstractions.csproj", "{22C42314-615F-4B11-B111-58F1D6D54F4D}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.X509", "src\Docker.DotNet.X509\Docker.DotNet.X509.csproj", "{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.LegacyHttp", "src\Docker.DotNet.LegacyHttp\Docker.DotNet.LegacyHttp.csproj", "{D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.NativeHttp", "src\Docker.DotNet.NativeHttp\Docker.DotNet.NativeHttp.csproj", "{E5F6A7B8-C9D0-41E2-3F45-5678901234EF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.NPipe", "src\Docker.DotNet.NPipe\Docker.DotNet.NPipe.csproj", "{A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.Unix", "src\Docker.DotNet.Unix\Docker.DotNet.Unix.csproj", "{B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.X509", "src\Docker.DotNet.X509\Docker.DotNet.X509.csproj", "{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet", "src\Docker.DotNet\Docker.DotNet.csproj", "{C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Net.Http.Client", "src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.shproj", "{DAE2DE68-9B3E-4D5D-8802-EC97B94160ED}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -25,18 +36,6 @@ Global
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x64.Build.0 = Debug|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x86.Build.0 = Debug|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|Any CPU.Build.0 = Release|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x64.ActiveCfg = Release|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x64.Build.0 = Release|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.ActiveCfg = Release|Any CPU
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.Build.0 = Release|Any CPU
{E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -49,6 +48,66 @@ Global
{E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Release|x64.Build.0 = Release|Any CPU
{E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Release|x86.ActiveCfg = Release|Any CPU
{E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Release|x86.Build.0 = Release|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x64.Build.0 = Debug|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x86.Build.0 = Debug|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x64.ActiveCfg = Release|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x64.Build.0 = Release|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x86.ActiveCfg = Release|Any CPU
+ {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x86.Build.0 = Release|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x64.Build.0 = Debug|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x86.Build.0 = Debug|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x64.ActiveCfg = Release|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x64.Build.0 = Release|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x86.ActiveCfg = Release|Any CPU
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x86.Build.0 = Release|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x64.Build.0 = Debug|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x86.Build.0 = Debug|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x64.ActiveCfg = Release|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x64.Build.0 = Release|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x86.ActiveCfg = Release|Any CPU
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x86.Build.0 = Release|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.Build.0 = Release|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.Build.0 = Release|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.Build.0 = Debug|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.Build.0 = Debug|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.ActiveCfg = Release|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.Build.0 = Release|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.ActiveCfg = Release|Any CPU
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.Build.0 = Release|Any CPU
{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|Any CPU.Build.0 = Debug|Any CPU
{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -61,6 +120,18 @@ Global
{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x64.Build.0 = Release|Any CPU
{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x86.ActiveCfg = Release|Any CPU
{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x86.Build.0 = Release|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x64.Build.0 = Debug|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x86.Build.0 = Debug|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x64.ActiveCfg = Release|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x64.Build.0 = Release|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.ActiveCfg = Release|Any CPU
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.Build.0 = Release|Any CPU
{248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.Build.0 = Debug|Any CPU
{248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -78,9 +149,24 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
{E1F24B25-E027-45E0-A6E1-E08138F1F95D} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
+ {22C42314-615F-4B11-B111-58F1D6D54F4D} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
+ {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
+ {E5F6A7B8-C9D0-41E2-3F45-5678901234EF} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
+ {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
+ {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
+ {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
+ {DAE2DE68-9B3E-4D5D-8802-EC97B94160ED} = {85990620-78A6-4381-8BD6-84E6D0CF0649}
{248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A}
EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {8F2F229F-C66D-43E4-B804-E5F37DC157CB}
+ EndGlobalSection
+ GlobalSection(SharedMSBuildProjectFiles) = preSolution
+ src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.projitems*{a1b2c3d4-e5f6-47a8-9b0c-1234567890ab}*SharedItemsImports = 5
+ src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.projitems*{b2c3d4e5-f6a7-48b9-0c1d-2345678901bc}*SharedItemsImports = 5
+ src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.projitems*{d4e5f6a7-b8c9-40d1-2e3f-4567890123de}*SharedItemsImports = 5
+ src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.projitems*{dae2de68-9b3e-4d5d-8802-ec97b94160ed}*SharedItemsImports = 13
+ EndGlobalSection
EndGlobal
diff --git a/README.md b/README.md
index 276271ed..5e2775a6 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,9 @@ You can add this library to your project using [NuGet][nuget].
Run the following command in the "Package Manager Console":
-> PM> Install-Package Docker.DotNet.Enhanced
+```console
+PM> Install-Package Docker.DotNet.Enhanced
+```
**Visual Studio**
@@ -29,7 +31,9 @@ Right click to your project in Visual Studio, choose "Manage NuGet Packages" and
Run the following command from your favorite shell or terminal:
-> dotnet add package Docker.DotNet.Enhanced
+```console
+dotnet add package Docker.DotNet.Enhanced
+```
**Development Builds**
@@ -37,39 +41,46 @@ Run the following command from your favorite shell or terminal:
## Usage
-You can initialize the client like the following:
+You can initialize the client as follows:
```csharp
using Docker.DotNet;
DockerClient client = new DockerClientConfiguration(
new Uri("http://ubuntu-docker.cloudapp.net:4243"))
- .CreateClient();
+ .CreateClient();
```
-or to connect to your local [Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install/) daemon using named pipes or your local [Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install/) daemon using Unix sockets:
+**Named Pipe (Windows):**
```csharp
using Docker.DotNet;
-DockerClient client = new DockerClientConfiguration()
+DockerClient client = new DockerClientConfiguration(
+ new Uri("npipe://./pipe/docker_engine"))
.CreateClient();
```
-For a custom endpoint, you can also pass a named pipe or a Unix socket to the `DockerClientConfiguration` constructor. For example:
+**Unix Domain Socket (Linux/macOS):**
```csharp
-// Default Docker Engine on Windows
using Docker.DotNet;
DockerClient client = new DockerClientConfiguration(
- new Uri("npipe://./pipe/docker_engine"))
- .CreateClient();
+ new Uri("unix:///var/run/docker.sock"))
+ .CreateClient();
+```
+
+**Note:**
+For HTTP(S) connections or special authentication types (e.g. X509, BasicAuth), see the corresponding sections below.
-// Default Docker Engine on Linux
+To connect to your local [Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install/) instance via named pipe or your local [Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install/) instance via Unix socket:
+
+```csharp
using Docker.DotNet;
-DockerClient client = new DockerClientConfiguration(
- new Uri("unix:///var/run/docker.sock"))
- .CreateClient();
+DockerClient client = new DockerClientConfiguration()
+ .CreateClient();
```
+For a custom endpoint, you can also explicitly pass a named pipe or Unix socket to the `DockerClientConfiguration` constructor (see examples above).
+
#### Example: List containers
```csharp
@@ -173,7 +184,9 @@ You can cancel streaming using the cancellation token. Or, if you wish to contin
If you are [running Docker with TLS (HTTPS)][docker-tls], you can authenticate to the Docker instance using the [**`Docker.DotNet.Enhanced.X509`**][Docker.DotNet.X509] package. You can get this package from NuGet or by running the following command in the "Package Manager Console":
- PM> Install-Package Docker.DotNet.Enhanced.X509
+```console
+PM> Install-Package Docker.DotNet.Enhanced.X509
+```
Once you add `Docker.DotNet.Enhanced.X509` to your project, use the `CertificateCredentials` type:
@@ -187,7 +200,9 @@ If you don't want to authenticate you can omit the `credentials` parameter, whic
The `CertFile` in the example above should be a PFX file (PKCS12 format), if you have PEM formatted certificates which Docker normally uses you can either convert it programmatically or use `openssl` tool to generate a PFX:
- openssl pkcs12 -export -inkey key.pem -in cert.pem -out key.pfx
+```console
+openssl pkcs12 -export -inkey key.pem -in cert.pem -out key.pfx
+```
(Here, your private key is `key.pem`, public key is `cert.pem` and output file is named `key.pfx`.) This will prompt a password for PFX file and then you can use this PFX file on Windows. If the certificate is self-signed, your application may reject the server certificate, in this case you might want to disable server certificate validation:
@@ -200,7 +215,9 @@ credentials.ServerCertificateValidationCallback = (o, c, ch, er) => true;
If the Docker instance is secured with "Basic" HTTP authentication, you can use the [**`Docker.DotNet.Enhanced.BasicAuth`**][Docker.DotNet.BasicAuth] package. Get this package from NuGet or by running the following command in the "Package Manager Console":
- PM> Install-Package Docker.DotNet.Enhanced.BasicAuth
+```console
+PM> Install-Package Docker.DotNet.Enhanced.BasicAuth
+```
Once you added `Docker.DotNet.Enhanced.BasicAuth` to your project, use `BasicAuthCredentials` type:
@@ -226,13 +243,13 @@ DockerClient client = config.CreateClient(new Version(1, 49));
Here are typical exceptions thrown from the client library:
-* **`DockerApiException`** is thrown when Docker Engine API responds with a non-success result. Subclasses:
- * **``DockerContainerNotFoundException``**
- * **``DockerImageNotFoundException``**
-* **`TaskCanceledException`** is thrown from `System.Net.Http.HttpClient` library by design. It is not a friendly exception, but it indicates your request has timed out. (default request timeout is 100 seconds.)
- * Long-running methods (e.g. `WaitContainerAsync`, `StopContainerAsync`) and methods that return Stream (e.g. `CreateImageAsync`, `GetContainerLogsAsync`) have timeout value overridden with infinite timespan by this library.
-* **`ArgumentNullException`** is thrown when one of the required parameters are missing/empty.
- * Consider reading the [Docker Remote API reference][docker-remote-api] and source code of the corresponding method you are going to use in from this library. This way you can easily find out which parameters are required and their format.
+- **`DockerApiException`** is thrown when Docker Engine API responds with a non-success result. Subclasses:
+ - **`DockerContainerNotFoundException`**
+ - **`DockerImageNotFoundException`**
+- **`TaskCanceledException`** is thrown from `System.Net.Http.HttpClient` library by design. It is not a friendly exception, but it indicates your request has timed out. (default request timeout is 100 seconds.)
+ - Long-running methods (e.g. `WaitContainerAsync`, `StopContainerAsync`) and methods that return Stream (e.g. `CreateImageAsync`, `GetContainerLogsAsync`) have timeout value overridden with infinite timespan by this library.
+- **`ArgumentNullException`** is thrown when one of the required parameters are missing/empty.
+ - Consider reading the [Docker Remote API reference][docker-remote-api] and source code of the corresponding method you are going to use in from this library. This way you can easily find out which parameters are required and their format.
## License
diff --git a/StrongNamePublicKeys.cs b/StrongNamePublicKeys.cs
index 6841e783..58566321 100644
--- a/StrongNamePublicKeys.cs
+++ b/StrongNamePublicKeys.cs
@@ -3,8 +3,8 @@
///
internal static class StrongNamePublicKeys
{
- ///
- /// The public key used for assemblies in this repo (Key.snk).
- ///
- public const string DockerDotNetPublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010013a6d952388480a1ce272e8c8ac11d710668c8723e696a421190445a1e6198288112f5e04eb99a626f8bb1454cdf30ebfb0a09cb7fc7b299cb03aa6fea1ae9a58f05f9fb92a85ce82ad4490bb2f0074822d8b0a786684f26a6eb1765f9026dae4857925b4e077d04b6311bec7dacf8e8a031dcc9f7e0384bca914256abee25d9";
+ ///
+ /// The public key used for assemblies in this repo (Key.snk).
+ ///
+ public const string DockerDotNetPublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010013a6d952388480a1ce272e8c8ac11d710668c8723e696a421190445a1e6198288112f5e04eb99a626f8bb1454cdf30ebfb0a09cb7fc7b299cb03aa6fea1ae9a58f05f9fb92a85ce82ad4490bb2f0074822d8b0a786684f26a6eb1765f9026dae4857925b4e077d04b6311bec7dacf8e8a031dcc9f7e0384bca914256abee25d9";
}
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 5d87b16b..00b70293 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -31,10 +31,4 @@
-
-
- all
- runtime; build; native; contentfiles; analyzers
-
-
diff --git a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj
index 8080b7f7..4bfe41cf 100644
--- a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj
+++ b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj
@@ -2,10 +2,10 @@
Docker.DotNet.BasicAuth
Docker.DotNet.Enhanced.BasicAuth
- Docker.DotNet.BasicAuth is a library that allows you to use basic authentication with a remote Docker engine programmatically in your .NET applications.
+ A Docker.DotNet extension that adds Basic Authentication for remote Docker Engine connections.
-
+
@@ -16,5 +16,6 @@
+
-
\ No newline at end of file
+
diff --git a/src/Docker.DotNet/Credentials.cs b/src/Docker.DotNet.Handler.Abstractions/Credentials.cs
similarity index 82%
rename from src/Docker.DotNet/Credentials.cs
rename to src/Docker.DotNet.Handler.Abstractions/Credentials.cs
index a20428a6..5d80ee26 100644
--- a/src/Docker.DotNet/Credentials.cs
+++ b/src/Docker.DotNet.Handler.Abstractions/Credentials.cs
@@ -1,4 +1,4 @@
-namespace Docker.DotNet;
+namespace Docker.DotNet.Handler.Abstractions;
public abstract class Credentials : IDisposable
{
diff --git a/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj b/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj
new file mode 100644
index 00000000..44805ec3
--- /dev/null
+++ b/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj
@@ -0,0 +1,18 @@
+
+
+ Docker.DotNet.Handler.Abstractions
+ Docker.DotNet.Enhanced.Handler.Abstractions
+ An abstraction layer for Docker.DotNet that defines the classes and interfaces for implementing Docker Engine handlers.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Docker.DotNet.Handler.Abstractions/IDockerClientConfiguration.cs b/src/Docker.DotNet.Handler.Abstractions/IDockerClientConfiguration.cs
new file mode 100644
index 00000000..d984100f
--- /dev/null
+++ b/src/Docker.DotNet.Handler.Abstractions/IDockerClientConfiguration.cs
@@ -0,0 +1,17 @@
+namespace Docker.DotNet.Handler.Abstractions;
+
+public interface IDockerClientConfiguration
+{
+ ///
+ /// Gets the collection of default HTTP request headers.
+ ///
+ public IReadOnlyDictionary DefaultHttpRequestHeaders { get; }
+
+ public Uri EndpointBaseUri { get; }
+
+ public Credentials Credentials { get; }
+
+ public TimeSpan DefaultTimeout { get; }
+
+ public TimeSpan NamedPipeConnectTimeout { get; }
+}
\ No newline at end of file
diff --git a/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs b/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs
new file mode 100644
index 00000000..6bbc6929
--- /dev/null
+++ b/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs
@@ -0,0 +1,6 @@
+namespace Docker.DotNet.Handler.Abstractions;
+
+public interface IDockerHandlerFactory : IStreamHijacker
+{
+ Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger);
+}
\ No newline at end of file
diff --git a/src/Docker.DotNet/IPeekableStream.cs b/src/Docker.DotNet.Handler.Abstractions/IPeekableStream.cs
similarity index 95%
rename from src/Docker.DotNet/IPeekableStream.cs
rename to src/Docker.DotNet.Handler.Abstractions/IPeekableStream.cs
index 89a7c099..09d62e1d 100644
--- a/src/Docker.DotNet/IPeekableStream.cs
+++ b/src/Docker.DotNet.Handler.Abstractions/IPeekableStream.cs
@@ -1,4 +1,4 @@
-namespace Docker.DotNet;
+namespace Docker.DotNet.Handler.Abstractions;
public interface IPeekableStream
{
diff --git a/src/Docker.DotNet.Handler.Abstractions/IStreamHijacker.cs b/src/Docker.DotNet.Handler.Abstractions/IStreamHijacker.cs
new file mode 100644
index 00000000..8d733b55
--- /dev/null
+++ b/src/Docker.DotNet.Handler.Abstractions/IStreamHijacker.cs
@@ -0,0 +1,6 @@
+namespace Docker.DotNet.Handler.Abstractions;
+
+public interface IStreamHijacker
+{
+ Task HijackStreamAsync(HttpContent content);
+}
\ No newline at end of file
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/WriteClosableStream.cs b/src/Docker.DotNet.Handler.Abstractions/WriteClosableStream.cs
similarity index 75%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/WriteClosableStream.cs
rename to src/Docker.DotNet.Handler.Abstractions/WriteClosableStream.cs
index a6009f42..84aa4209 100644
--- a/src/Docker.DotNet/Microsoft.Net.Http.Client/WriteClosableStream.cs
+++ b/src/Docker.DotNet.Handler.Abstractions/WriteClosableStream.cs
@@ -1,4 +1,4 @@
-namespace Microsoft.Net.Http.Client;
+namespace Docker.DotNet.Handler.Abstractions;
public abstract class WriteClosableStream : Stream
{
diff --git a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj
new file mode 100644
index 00000000..4fd99d00
--- /dev/null
+++ b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj
@@ -0,0 +1,24 @@
+
+
+ Docker.DotNet.LegacyHttp
+ Docker.DotNet.Enhanced.LegacyHttp
+ A Docker.DotNet transport implementation for HTTP(S) Docker Engine connections.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs b/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs
new file mode 100644
index 00000000..24f8ccab
--- /dev/null
+++ b/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs
@@ -0,0 +1,27 @@
+namespace Docker.DotNet.LegacyHttp;
+
+public sealed class DockerHandlerFactory : IDockerHandlerFactory
+{
+ private DockerHandlerFactory()
+ {
+ }
+
+ public static IDockerHandlerFactory Instance { get; } = new DockerHandlerFactory();
+
+ public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger)
+ {
+ var scheme = configuration.Credentials.IsTlsCredentials() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
+ uri = new UriBuilder(uri) { Scheme = scheme }.Uri;
+ return new Tuple(new ManagedHandler(logger), uri);
+ }
+
+ public Task HijackStreamAsync(HttpContent content)
+ {
+ if (content is not HttpConnectionResponseContent hijackable)
+ {
+ throw new NotSupportedException("The content type is not supported for stream hijacking.");
+ }
+
+ return Task.FromResult(hijackable.HijackStream());
+ }
+}
\ No newline at end of file
diff --git a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj
new file mode 100644
index 00000000..f1cf22f2
--- /dev/null
+++ b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj
@@ -0,0 +1,26 @@
+
+
+ Docker.DotNet.NPipe
+ Docker.DotNet.Enhanced.NPipe
+ A Docker.DotNet transport implementation for Windows named pipe (npipe) Docker Engine connections.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs
new file mode 100644
index 00000000..03e83709
--- /dev/null
+++ b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs
@@ -0,0 +1,60 @@
+namespace Docker.DotNet.NPipe;
+
+public sealed class DockerHandlerFactory : IDockerHandlerFactory
+{
+ private DockerHandlerFactory()
+ {
+ }
+
+ public static IDockerHandlerFactory Instance { get; } = new DockerHandlerFactory();
+
+ public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger)
+ {
+ if (configuration.Credentials.IsTlsCredentials())
+ {
+ throw new NotSupportedException("TLS is not supported over npipe.");
+ }
+
+ var segments = uri.Segments;
+
+ if (segments.Length != 3 || !"pipe/".Equals(segments[1], StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException("The endpoint is not a npipe URI.");
+ }
+
+ var pipeName = uri.Segments[2];
+
+ var serverName = "localhost".Equals(uri.Host, StringComparison.OrdinalIgnoreCase) ? "." : uri.Host;
+ uri = new UriBuilder(Uri.UriSchemeHttp, pipeName).Uri;
+
+ var streamOpener = new ManagedHandler.StreamOpener(async (_, _, cancellationToken) =>
+ {
+ var clientStream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
+
+ var dockerStream = new DockerPipeStream(clientStream);
+
+#if NETSTANDARD
+ var namedPipeConnectTimeout = (int)configuration.NamedPipeConnectTimeout.TotalMilliseconds;
+#else
+ var namedPipeConnectTimeout = configuration.NamedPipeConnectTimeout;
+#endif
+
+ await clientStream.ConnectAsync(namedPipeConnectTimeout, cancellationToken)
+ .ConfigureAwait(false);
+
+ return dockerStream;
+ });
+
+ return new Tuple(new ManagedHandler(streamOpener, logger), uri);
+ }
+
+ public Task HijackStreamAsync(HttpContent content)
+ {
+ if (content is not HttpConnectionResponseContent hijackable)
+ {
+ throw new NotSupportedException("The content type is not supported for stream hijacking.");
+ }
+
+ return Task.FromResult(hijackable.HijackStream());
+ }
+}
\ No newline at end of file
diff --git a/src/Docker.DotNet/DockerPipeStream.cs b/src/Docker.DotNet.NPipe/DockerPipeStream.cs
similarity index 88%
rename from src/Docker.DotNet/DockerPipeStream.cs
rename to src/Docker.DotNet.NPipe/DockerPipeStream.cs
index 596ebb07..edf1d960 100644
--- a/src/Docker.DotNet/DockerPipeStream.cs
+++ b/src/Docker.DotNet.NPipe/DockerPipeStream.cs
@@ -1,12 +1,17 @@
-namespace Docker.DotNet;
+namespace Docker.DotNet.NPipe;
-internal class DockerPipeStream : WriteClosableStream, IPeekableStream
+internal sealed class DockerPipeStream : WriteClosableStream, IPeekableStream
{
private readonly EventWaitHandle _event = new EventWaitHandle(false, EventResetMode.AutoReset);
private readonly PipeStream _stream;
public DockerPipeStream(PipeStream stream)
{
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ throw new PlatformNotSupportedException("DockerPipeStream is only supported on Windows.");
+ }
+
_stream = stream;
}
@@ -40,6 +45,9 @@ public override long Position
public override void CloseWrite()
{
+ const int errorIoPending = 997;
+
+#pragma warning disable CA1416
// The Docker daemon expects a write of zero bytes to signal the end of writes. Use native
// calls to achieve this since CoreCLR ignores a zero-byte write.
var overlapped = new NativeOverlapped();
@@ -49,12 +57,13 @@ public override void CloseWrite()
// Set the low bit to tell Windows not to send the result of this IO to the
// completion port.
overlapped.EventHandle = (IntPtr)(handle.DangerousGetHandle().ToInt64() | 1);
+#pragma warning restore CA1416
+
if (WriteFile(_stream.SafePipeHandle, IntPtr.Zero, 0, IntPtr.Zero, ref overlapped) == 0)
{
- const int ERROR_IO_PENDING = 997;
- if (Marshal.GetLastWin32Error() == ERROR_IO_PENDING)
+ if (Marshal.GetLastWin32Error() == errorIoPending)
{
- if (GetOverlappedResult(_stream.SafePipeHandle, ref overlapped, out var _, 1) == 0)
+ if (GetOverlappedResult(_stream.SafePipeHandle, ref overlapped, out _, 1) == 0)
{
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
diff --git a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj
new file mode 100644
index 00000000..84632d93
--- /dev/null
+++ b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj
@@ -0,0 +1,24 @@
+
+
+ Docker.DotNet.NativeHttp
+ Docker.DotNet.Enhanced.NativeHttp
+ A Docker.DotNet transport implementation for HTTP(S) Docker Engine connections.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs b/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs
new file mode 100644
index 00000000..762c1260
--- /dev/null
+++ b/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs
@@ -0,0 +1,43 @@
+namespace Docker.DotNet.NativeHttp;
+
+public sealed class DockerHandlerFactory : IDockerHandlerFactory
+{
+ private const int MaxConnectionsPerServer = 10;
+
+ private static readonly TimeSpan PooledConnectionLifetime = TimeSpan.FromMinutes(5);
+
+ private static readonly TimeSpan PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2);
+
+ private DockerHandlerFactory()
+ {
+ }
+
+ public static IDockerHandlerFactory Instance { get; } = new DockerHandlerFactory();
+
+ public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger)
+ {
+ var scheme = configuration.Credentials.IsTlsCredentials() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
+ uri = new UriBuilder(uri) { Scheme = scheme }.Uri;
+
+#if NET6_0_OR_GREATER
+ var handler = new SocketsHttpHandler
+ {
+ MaxConnectionsPerServer = MaxConnectionsPerServer,
+ PooledConnectionLifetime = PooledConnectionLifetime,
+ PooledConnectionIdleTimeout = PooledConnectionIdleTimeout,
+ };
+#else
+ var handler = new HttpClientHandler();
+#endif
+
+ return new Tuple(handler, uri);
+ }
+
+ public async Task HijackStreamAsync(HttpContent content)
+ {
+ var stream = await content.ReadAsStreamAsync()
+ .ConfigureAwait(false);
+
+ return new WriteClosableStreamWrapper(stream);
+ }
+}
\ No newline at end of file
diff --git a/src/Docker.DotNet.NativeHttp/WriteClosableStreamWrapper.cs b/src/Docker.DotNet.NativeHttp/WriteClosableStreamWrapper.cs
new file mode 100644
index 00000000..0ac93df8
--- /dev/null
+++ b/src/Docker.DotNet.NativeHttp/WriteClosableStreamWrapper.cs
@@ -0,0 +1,56 @@
+namespace Docker.DotNet.NativeHttp;
+
+internal sealed class WriteClosableStreamWrapper(Stream stream) : WriteClosableStream
+{
+ private readonly Stream _stream = stream ?? throw new ArgumentNullException(nameof(stream));
+
+ public override bool CanRead
+ => _stream.CanRead;
+
+ public override bool CanWrite
+ => _stream.CanWrite;
+
+ public override bool CanSeek
+ => _stream.CanSeek;
+
+ public override bool CanCloseWrite
+ => true;
+
+ public override long Length
+ => _stream.Length;
+
+ public override long Position
+ {
+ get => _stream.Position;
+ set => _stream.Position = value;
+ }
+
+ public override void Flush()
+ => _stream.Flush();
+
+ public override int Read(byte[] buffer, int offset, int count)
+ => _stream.Read(buffer, offset, count);
+
+ public override void Write(byte[] buffer, int offset, int count)
+ => _stream.Write(buffer, offset, count);
+
+ public override long Seek(long offset, SeekOrigin origin)
+ => _stream.Seek(offset, origin);
+
+ // Replace with half-close logic if available.
+ public override void CloseWrite()
+ => _stream.Close();
+
+ public override void SetLength(long value)
+ => _stream.SetLength(value);
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _stream.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
\ No newline at end of file
diff --git a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj
new file mode 100644
index 00000000..328e5446
--- /dev/null
+++ b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj
@@ -0,0 +1,28 @@
+
+
+ Docker.DotNet.Unix
+ Docker.DotNet.Enhanced.Unix
+ A Docker.DotNet transport implementation for Unix domain socket Docker Engine connections.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Docker.DotNet.Unix/DockerHandlerFactory.cs b/src/Docker.DotNet.Unix/DockerHandlerFactory.cs
new file mode 100644
index 00000000..a0e4be0c
--- /dev/null
+++ b/src/Docker.DotNet.Unix/DockerHandlerFactory.cs
@@ -0,0 +1,41 @@
+namespace Docker.DotNet.Unix;
+
+public sealed class DockerHandlerFactory : IDockerHandlerFactory
+{
+ private DockerHandlerFactory()
+ {
+ }
+
+ public static IDockerHandlerFactory Instance { get; } = new DockerHandlerFactory();
+
+ public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger)
+ {
+ var socketName = uri.Segments.Last();
+ var socketPath = uri.LocalPath;
+ uri = new UriBuilder(Uri.UriSchemeHttp, socketName).Uri;
+
+ var socketOpener = new ManagedHandler.SocketOpener(async (_, _, _) =>
+ {
+ var endpoint = new UnixDomainSocketEndPoint(socketPath);
+
+ var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
+
+ await socket.ConnectAsync(endpoint)
+ .ConfigureAwait(false);
+
+ return socket;
+ });
+
+ return new Tuple(new ManagedHandler(socketOpener, logger), uri);
+ }
+
+ public Task HijackStreamAsync(HttpContent content)
+ {
+ if (content is not HttpConnectionResponseContent hijackable)
+ {
+ throw new NotSupportedException("The content type is not supported for stream hijacking.");
+ }
+
+ return Task.FromResult(hijackable.HijackStream());
+ }
+}
\ No newline at end of file
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs b/src/Docker.DotNet.Unix/UnixDomainSocketEndPoint.cs
similarity index 98%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs
rename to src/Docker.DotNet.Unix/UnixDomainSocketEndPoint.cs
index e24ed924..15bb77d8 100644
--- a/src/Docker.DotNet/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs
+++ b/src/Docker.DotNet.Unix/UnixDomainSocketEndPoint.cs
@@ -1,4 +1,4 @@
-namespace Microsoft.Net.Http.Client;
+namespace Docker.DotNet.Unix;
internal sealed class UnixDomainSocketEndPoint : EndPoint
{
diff --git a/src/Docker.DotNet.X509/CertificateCredentials.cs b/src/Docker.DotNet.X509/CertificateCredentials.cs
index e5deb654..d6289f47 100644
--- a/src/Docker.DotNet.X509/CertificateCredentials.cs
+++ b/src/Docker.DotNet.X509/CertificateCredentials.cs
@@ -24,19 +24,54 @@ public override bool IsTlsCredentials()
public override HttpMessageHandler GetHandler(HttpMessageHandler handler)
{
- if (handler is not ManagedHandler managedHandler)
+#if NET6_0_OR_GREATER
+ if (handler is SocketsHttpHandler nativeHandler)
{
- return handler;
- }
+ nativeHandler.AllowAutoRedirect = true;
+ nativeHandler.MaxAutomaticRedirections = 20;
- if (!managedHandler.ClientCertificates.Contains(_certificate))
+ nativeHandler.SslOptions = new SslClientAuthenticationOptions
+ {
+ ClientCertificates = new X509CertificateCollection { _certificate },
+ CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
+ EnabledSslProtocols = SslProtocols.Tls12,
+ RemoteCertificateValidationCallback = (message, certificate, chain, errors) => ServerCertificateValidationCallback?.Invoke(message, certificate, chain, errors) ?? false
+ };
+ return nativeHandler;
+ }
+#else
+ if (handler is HttpClientHandler nativeHandler)
{
- managedHandler.ClientCertificates.Add(_certificate);
+ if (!nativeHandler.ClientCertificates.Contains(_certificate))
+ {
+ nativeHandler.ClientCertificates.Add(_certificate);
+ }
+
+ nativeHandler.AllowAutoRedirect = true;
+ nativeHandler.MaxAutomaticRedirections = 20;
+
+ nativeHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
+ nativeHandler.CheckCertificateRevocationList = false;
+ nativeHandler.SslProtocols = SslProtocols.Tls12;
+ nativeHandler.ServerCertificateCustomValidationCallback += (message, certificate, chain, errors) => ServerCertificateValidationCallback?.Invoke(message, certificate, chain, errors) ?? false;
+ return nativeHandler;
}
+#endif
+ else if (handler is ManagedHandler managedHandler)
+ {
+ if (!managedHandler.ClientCertificates.Contains(_certificate))
+ {
+ managedHandler.ClientCertificates.Add(_certificate);
+ }
- managedHandler.ServerCertificateValidationCallback = ServerCertificateValidationCallback;
+ managedHandler.ServerCertificateValidationCallback = ServerCertificateValidationCallback;
- return handler;
+ return handler;
+ }
+ else
+ {
+ return handler;
+ }
}
protected virtual void Dispose(bool disposing)
diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj
index d3984ba6..18f30387 100644
--- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj
+++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj
@@ -2,25 +2,30 @@
Docker.DotNet.X509
Docker.DotNet.Enhanced.X509
- Docker.DotNet.X509 is a library that allows you to use certificate authentication with a remote Docker engine programmatically in your .NET applications.
+ A Docker.DotNet extension that adds X.509 client certificate authentication for remote Docker Engine connections.
-
+
-
+
+
-
-
-
+
+
+
+
+
+
-
\ No newline at end of file
+
+
diff --git a/src/Docker.DotNet.X509/DockerTlsCertificates.cs b/src/Docker.DotNet.X509/DockerTlsCertificates.cs
new file mode 100644
index 00000000..afd82368
--- /dev/null
+++ b/src/Docker.DotNet.X509/DockerTlsCertificates.cs
@@ -0,0 +1,177 @@
+namespace Docker.DotNet.X509;
+
+public sealed class DockerTlsCertificates
+{
+ private const string DefaultCaPemFileName = "ca.pem";
+
+ private const string DefaultCertPemFileName = "cert.pem";
+
+ private const string DefaultKeyPemFileName = "key.pem";
+
+ public DockerTlsCertificates(
+ X509Certificate2 certificate,
+ X509Certificate2 certificateAuthorityCertificate = null)
+ {
+ Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate));
+ CertificateAuthorityCertificate = certificateAuthorityCertificate;
+ }
+
+ private X509Certificate2 Certificate { get; }
+
+ private X509Certificate2 CertificateAuthorityCertificate { get; }
+
+ public static DockerTlsCertificates LoadFromDirectory(
+ string directoryPath,
+ bool loadCertificateAuthority = true,
+ string caPemFileName = DefaultCaPemFileName,
+ string certPemFileName = DefaultCertPemFileName,
+ string keyPemFileName = DefaultKeyPemFileName)
+ {
+ if (!Directory.Exists(directoryPath))
+ {
+ throw new DirectoryNotFoundException(directoryPath);
+ }
+
+ X509Certificate2 caCertificate = null;
+
+ var certPemPath = Path.Combine(directoryPath, certPemFileName);
+ var keyPemPath = Path.Combine(directoryPath, keyPemFileName);
+ var caPemPath = Path.Combine(directoryPath, caPemFileName);
+
+ var certificate = LoadCertificateFromPemFiles(certPemPath, keyPemPath);
+
+ if (loadCertificateAuthority && File.Exists(caPemPath))
+ {
+ caCertificate = LoadCertificateAuthorityFromPemFile(caPemPath);
+ }
+
+ return new DockerTlsCertificates(certificate, caCertificate);
+ }
+
+ public static X509Certificate2 LoadCertificateFromPemFiles(string certPemPath, string keyPemPath)
+ {
+ EnsureFileExists(certPemPath);
+ EnsureFileExists(keyPemPath);
+
+#if NET9_0_OR_GREATER
+ var certificate = X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath);
+
+ if (OperatingSystem.IsWindows())
+ {
+ var pfxBytes = certificate.Export(X509ContentType.Pfx);
+ certificate.Dispose();
+ return X509CertificateLoader.LoadPkcs12(pfxBytes, password: null);
+ }
+
+ return certificate;
+#elif NET6_0_OR_GREATER
+ var certificate = X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath);
+
+ if (OperatingSystem.IsWindows())
+ {
+ var pfxBytes = certificate.Export(X509ContentType.Pfx);
+ certificate.Dispose();
+ return new X509Certificate2(pfxBytes);
+ }
+
+ return certificate;
+#elif NETSTANDARD
+ return Polyfills.X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath);
+#else
+ return X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath);
+#endif
+ }
+
+ public static X509Certificate2 LoadCertificateFromPfxFile(string pfxPath, string password)
+ {
+ EnsureFileExists(pfxPath);
+
+ password ??= string.Empty;
+
+#if NET9_0_OR_GREATER
+ return X509CertificateLoader.LoadPkcs12FromFile(pfxPath, password);
+#elif NETSTANDARD
+ return new X509Certificate2(File.ReadAllBytes(pfxPath), password);
+#else
+ return new X509Certificate2(pfxPath, password, X509KeyStorageFlags.EphemeralKeySet);
+#endif
+ }
+
+ public static X509Certificate2 LoadCertificateAuthorityFromPemFile(string caPemPath)
+ {
+ EnsureFileExists(caPemPath);
+
+#if NET9_0_OR_GREATER
+ return X509CertificateLoader.LoadCertificateFromFile(caPemPath);
+#elif NETSTANDARD
+ return Polyfills.X509Certificate2.CreateFromPemFile(caPemPath);
+#else
+ return new X509Certificate2(caPemPath);
+#endif
+ }
+
+ public CertificateCredentials CreateCredentials()
+ {
+ var credentials = new CertificateCredentials(Certificate);
+
+ if (CertificateAuthorityCertificate is not null)
+ {
+ credentials.ServerCertificateValidationCallback = CreateCertificateAuthorityValidationCallback(CertificateAuthorityCertificate);
+ }
+
+ return credentials;
+ }
+
+ public static RemoteCertificateValidationCallback CreateCertificateAuthorityValidationCallback(X509Certificate2 certificateAuthorityCertificate)
+ {
+ if (certificateAuthorityCertificate is null)
+ {
+ throw new ArgumentNullException(nameof(certificateAuthorityCertificate));
+ }
+
+ return (_, certificate, _, _) =>
+ {
+ if (certificate is null)
+ {
+ return false;
+ }
+
+ var serverCertificate2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);
+
+ using var chain = new X509Chain();
+ chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+
+#if NET5_0_OR_GREATER
+ chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
+ chain.ChainPolicy.CustomTrustStore.Add(certificateAuthorityCertificate);
+ return chain.Build(serverCertificate2);
+#else
+ chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
+ chain.ChainPolicy.ExtraStore.Add(certificateAuthorityCertificate);
+
+ if (!chain.Build(serverCertificate2))
+ {
+ return false;
+ }
+
+ foreach (var chainElement in chain.ChainElements)
+ {
+ if (string.Equals(chainElement.Certificate.Thumbprint, certificateAuthorityCertificate.Thumbprint, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+#endif
+ };
+ }
+
+ private static void EnsureFileExists(string path)
+ {
+ if (!File.Exists(path))
+ {
+ throw new FileNotFoundException(path);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Docker.DotNet.X509/RSAUtil.cs b/src/Docker.DotNet.X509/RSAUtil.cs
index 9f9ceda9..66d8064f 100644
--- a/src/Docker.DotNet.X509/RSAUtil.cs
+++ b/src/Docker.DotNet.X509/RSAUtil.cs
@@ -1,5 +1,6 @@
namespace Docker.DotNet.X509;
+[Obsolete("RSAUtil is obsolete. Use DockerTlsCertificates instead.")]
public static class RSAUtil
{
public static X509Certificate2 GetCertFromPFX(string pfxFilePath, string password)
diff --git a/src/Docker.DotNet.X509/X509Certificate2.cs b/src/Docker.DotNet.X509/X509Certificate2.cs
index e1a438ca..26e30b4b 100644
--- a/src/Docker.DotNet.X509/X509Certificate2.cs
+++ b/src/Docker.DotNet.X509/X509Certificate2.cs
@@ -12,6 +12,18 @@ public static class X509Certificate2
{
private static readonly X509CertificateParser CertificateParser = new X509CertificateParser();
+ public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromPemFile(string certPemFilePath)
+ {
+ if (!File.Exists(certPemFilePath))
+ {
+ throw new FileNotFoundException(certPemFilePath);
+ }
+
+ var certificate = CertificateParser.ReadCertificate(File.ReadAllBytes(certPemFilePath));
+
+ return new System.Security.Cryptography.X509Certificates.X509Certificate2(certificate.GetEncoded());
+ }
+
public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath)
{
if (!File.Exists(certPemFilePath))
diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj
index de9933c9..38f2905b 100644
--- a/src/Docker.DotNet/Docker.DotNet.csproj
+++ b/src/Docker.DotNet/Docker.DotNet.csproj
@@ -2,18 +2,25 @@
Docker.DotNet
Docker.DotNet.Enhanced
- Docker.DotNet 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.
+ A .NET client for the Docker Engine API with fully asynchronous, non-blocking, object-oriented APIs.
-
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -24,8 +31,8 @@
-
+
@@ -45,9 +52,9 @@
+
-
-
\ No newline at end of file
+
diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs
index 2c32db03..2d59b558 100644
--- a/src/Docker.DotNet/DockerClient.cs
+++ b/src/Docker.DotNet/DockerClient.cs
@@ -1,7 +1,6 @@
namespace Docker.DotNet;
using System;
-using System.IO.Pipes;
public sealed class DockerClient : IDockerClient
{
@@ -15,9 +14,17 @@ public sealed class DockerClient : IDockerClient
private readonly Version _requestedApiVersion;
- internal DockerClient(DockerClientConfiguration configuration, Version requestedApiVersion, ILogger logger = null)
+ private readonly IDockerHandlerFactory _handlerFactory;
+
+ internal DockerClient(DockerClientConfiguration configuration, Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null)
{
+ if (handlerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(handlerFactory));
+ }
+
_requestedApiVersion = requestedApiVersion;
+ _handlerFactory = handlerFactory;
Configuration = configuration;
DefaultTimeout = configuration.DefaultTimeout;
@@ -34,81 +41,11 @@ internal DockerClient(DockerClientConfiguration configuration, Version requested
Plugin = new PluginOperations(this);
Exec = new ExecOperations(this);
- ManagedHandler handler;
- var uri = Configuration.EndpointBaseUri;
- switch (uri.Scheme.ToLowerInvariant())
- {
- case "npipe":
- if (Configuration.Credentials.IsTlsCredentials())
- {
- throw new Exception("TLS not supported over npipe");
- }
-
- var segments = uri.Segments;
- if (segments.Length != 3 || !segments[1].Equals("pipe/", StringComparison.OrdinalIgnoreCase))
- {
- throw new ArgumentException($"{Configuration.EndpointBaseUri} is not a valid npipe URI");
- }
-
- var serverName = uri.Host;
- if (string.Equals(serverName, "localhost", StringComparison.OrdinalIgnoreCase))
- {
- // npipe schemes dont work with npipe://localhost/... and need npipe://./... so fix that for a client here.
- serverName = ".";
- }
-
- var pipeName = uri.Segments[2];
-
- uri = new UriBuilder("http", pipeName).Uri;
- handler = new ManagedHandler(async (host, port, cancellationToken) =>
- {
- var timeout = (int)Configuration.NamedPipeConnectTimeout.TotalMilliseconds;
- var stream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
- var dockerStream = new DockerPipeStream(stream);
-
- await stream.ConnectAsync(timeout, cancellationToken)
- .ConfigureAwait(false);
-
- return dockerStream;
- }, logger);
- break;
-
- case "tcp":
- case "http":
- var builder = new UriBuilder(uri)
- {
- Scheme = configuration.Credentials.IsTlsCredentials() ? "https" : "http"
- };
- uri = builder.Uri;
- handler = new ManagedHandler(logger);
- break;
-
- case "https":
- handler = new ManagedHandler(logger);
- break;
-
- case "unix":
- var pipeString = uri.LocalPath;
- handler = new ManagedHandler(async (host, port, cancellationToken) =>
- {
- var sock = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
-
- await sock.ConnectAsync(new Microsoft.Net.Http.Client.UnixDomainSocketEndPoint(pipeString))
- .ConfigureAwait(false);
-
- return sock;
- }, logger);
- uri = new UriBuilder("http", uri.Segments.Last()).Uri;
- break;
-
- default:
- throw new Exception($"Unknown URL scheme {configuration.EndpointBaseUri.Scheme}");
- }
-
- _endpointBaseUri = uri;
+ var (handler, endpoint) = _handlerFactory.CreateHandler(Configuration.EndpointBaseUri, Configuration, logger);
_client = new HttpClient(Configuration.Credentials.GetHandler(handler), true);
_client.Timeout = Timeout.InfiniteTimeSpan;
+ _endpointBaseUri = endpoint;
}
public DockerClientConfiguration Configuration { get; }
@@ -395,12 +332,8 @@ internal async Task MakeRequestForHijackedStreamAsync(
await HandleIfErrorResponseAsync(response.StatusCode, response, errorHandlers)
.ConfigureAwait(false);
- if (response.Content is not HttpConnectionResponseContent content)
- {
- throw new NotSupportedException("message handler does not support hijacked streams");
- }
-
- return content.HijackStream();
+ return await _handlerFactory.HijackStreamAsync(response.Content)
+ .ConfigureAwait(false);
}
private async Task PrivateMakeRequestAsync(
@@ -473,7 +406,7 @@ private async Task HandleIfErrorResponseAsync(HttpStatusCode statusCode, HttpRes
if (isErrorResponse)
{
// If it is not an error response, we do not read the response body because the caller may wish to consume it.
- // If it is an error response, we do because there is nothing else going to be done with it anyway and
+ // If it is an error response, we do because there is nothing else going to be done with it anyway, and
// we want to report the response body in the error message as it contains potentially useful info.
responseBody = await response.Content.ReadAsStringAsync()
.ConfigureAwait(false);
@@ -504,7 +437,7 @@ private async Task HandleIfErrorResponseAsync(HttpStatusCode statusCode, HttpRes
if (isErrorResponse)
{
// If it is not an error response, we do not read the response body because the caller may wish to consume it.
- // If it is an error response, we do because there is nothing else going to be done with it anyway and
+ // If it is an error response, we do because there is nothing else going to be done with it anyway, and
// we want to report the response body in the error message as it contains potentially useful info.
responseBody = await response.Content.ReadAsStringAsync()
.ConfigureAwait(false);
diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs
index a1fb82d3..ccf40f59 100644
--- a/src/Docker.DotNet/DockerClientConfiguration.cs
+++ b/src/Docker.DotNet/DockerClientConfiguration.cs
@@ -2,8 +2,10 @@ namespace Docker.DotNet;
using System;
-public class DockerClientConfiguration : IDisposable
+public class DockerClientConfiguration : IDockerClientConfiguration, IDisposable
{
+ private static readonly bool NativeHttpEnabled = Environment.GetEnvironmentVariable("DOCKER_DOTNET_NATIVE_HTTP_ENABLED") == "1";
+
public DockerClientConfiguration(
Credentials credentials = null,
TimeSpan defaultTimeout = default,
@@ -52,7 +54,27 @@ public DockerClientConfiguration(
public DockerClient CreateClient(Version requestedApiVersion = null, ILogger logger = null)
{
- return new DockerClient(this, requestedApiVersion, logger);
+ var handlerFactory = EndpointBaseUri.Scheme.ToLowerInvariant() switch
+ {
+ "npipe" => NPipe.DockerHandlerFactory.Instance,
+ "unix" => Unix.DockerHandlerFactory.Instance,
+ "tcp" or "http" or "https" => NativeHttpEnabled
+ ? NativeHttp.DockerHandlerFactory.Instance
+ : LegacyHttp.DockerHandlerFactory.Instance,
+ _ => throw new NotSupportedException($"The URI scheme '{EndpointBaseUri.Scheme}' is not supported.")
+ };
+
+ return CreateClient(requestedApiVersion, handlerFactory, logger);
+ }
+
+ public DockerClient CreateClient(Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null)
+ {
+ if (handlerFactory == null)
+ {
+ throw new ArgumentNullException(nameof(handlerFactory));
+ }
+
+ return new DockerClient(this, requestedApiVersion, handlerFactory, logger);
}
public void Dispose()
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs
similarity index 100%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs
rename to src/Microsoft.Net.Http.Client/BufferedReadStream.cs
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs
similarity index 100%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs
rename to src/Microsoft.Net.Http.Client/ChunkedReadStream.cs
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs b/src/Microsoft.Net.Http.Client/ChunkedWriteStream.cs
similarity index 100%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs
rename to src/Microsoft.Net.Http.Client/ChunkedWriteStream.cs
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ContentLengthReadStream.cs b/src/Microsoft.Net.Http.Client/ContentLengthReadStream.cs
similarity index 100%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/ContentLengthReadStream.cs
rename to src/Microsoft.Net.Http.Client/ContentLengthReadStream.cs
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs
similarity index 100%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs
rename to src/Microsoft.Net.Http.Client/HttpConnection.cs
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs
similarity index 100%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs
rename to src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs b/src/Microsoft.Net.Http.Client/ManagedHandler.cs
similarity index 99%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs
rename to src/Microsoft.Net.Http.Client/ManagedHandler.cs
index 728cf6eb..a7f55cf5 100644
--- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs
+++ b/src/Microsoft.Net.Http.Client/ManagedHandler.cs
@@ -1,7 +1,5 @@
namespace Microsoft.Net.Http.Client;
-using System;
-
public class ManagedHandler : HttpMessageHandler
{
private readonly ILogger _logger;
diff --git a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems
new file mode 100644
index 00000000..5c2c9157
--- /dev/null
+++ b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems
@@ -0,0 +1,39 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ dae2de68-9b3e-4d5d-8802-ec97b94160ed
+
+
+ Microsoft.Net.Http.Client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj
new file mode 100644
index 00000000..b7fd1252
--- /dev/null
+++ b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj
@@ -0,0 +1,13 @@
+
+
+
+ dae2de68-9b3e-4d5d-8802-ec97b94160ed
+ 14.0
+
+
+
+
+
+
+
+
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ProxyMode.cs b/src/Microsoft.Net.Http.Client/ProxyMode.cs
similarity index 100%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/ProxyMode.cs
rename to src/Microsoft.Net.Http.Client/ProxyMode.cs
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/RedirectMode.cs b/src/Microsoft.Net.Http.Client/RedirectMode.cs
similarity index 100%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/RedirectMode.cs
rename to src/Microsoft.Net.Http.Client/RedirectMode.cs
diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/RequestExtensions.cs b/src/Microsoft.Net.Http.Client/RequestExtensions.cs
similarity index 100%
rename from src/Docker.DotNet/Microsoft.Net.Http.Client/RequestExtensions.cs
rename to src/Microsoft.Net.Http.Client/RequestExtensions.cs
diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj
index 57b89dd0..0c0c14ed 100644
--- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj
+++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj
@@ -5,14 +5,14 @@
false
-
-
-
+
+
+
+
-
@@ -21,17 +21,25 @@
+
-
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs
index cc2ae303..ff3ebab9 100644
--- a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs
+++ b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs
@@ -190,7 +190,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync(
containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5));
- await Assert.ThrowsAsync(() => _testFixture.DockerClient.Containers.GetContainerLogsAsync(
+ await Assert.ThrowsAnyAsync(() => _testFixture.DockerClient.Containers.GetContainerLogsAsync(
createContainerResponse.ID,
new ContainerLogsParameters
{
@@ -240,7 +240,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync(
containerLogsCts.Token
);
- await Assert.ThrowsAsync(() => containerLogsTask);
+ await Assert.ThrowsAnyAsync(() => containerLogsTask);
}
[Fact]
@@ -288,7 +288,7 @@ await _testFixture.DockerClient.Containers.StopContainerAsync(
_testFixture.Cts.Token
);
- await Assert.ThrowsAsync(() => containerLogsTask);
+ await Assert.ThrowsAnyAsync(() => containerLogsTask);
_testOutputHelper.WriteLine($"Line count: {logList.Count}");
Assert.NotEmpty(logList);
@@ -682,7 +682,7 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio
// Will wait forever here if cancellation fails.
var waitContainerTask = _testFixture.DockerClient.Containers.WaitContainerAsync(createContainerResponse.ID, waitContainerCts.Token);
- _ = await Assert.ThrowsAsync(() => waitContainerTask);
+ _ = await Assert.ThrowsAnyAsync(() => waitContainerTask);
stopWatch.Stop();
diff --git a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs
index 3609a79c..b94faa24 100644
--- a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs
+++ b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs
@@ -225,6 +225,10 @@ await _testFixture.DockerClient.Images.TagImageAsync(
using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token);
var task = Task.Run(() => _testFixture.DockerClient.System.MonitorEventsAsync(eventsParams, progress, cts.Token));
+ // Wait briefly to ensure the monitoring task is fully established before triggering Docker events.
+ // Ideally, the API would return (or signal) once monitoring is active.
+ await Task.Delay(TimeSpan.FromSeconds(1));
+
await _testFixture.DockerClient.Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag });
await _testFixture.DockerClient.Images.DeleteImageAsync($"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters());
diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs
index b32ecd6e..62ac4f62 100644
--- a/test/Docker.DotNet.Tests/TestFixture.cs
+++ b/test/Docker.DotNet.Tests/TestFixture.cs
@@ -21,7 +21,7 @@ public sealed class TestFixture : Progress, IAsyncLifetime, IDispos
public TestFixture(IMessageSink messageSink)
{
_messageSink = messageSink;
- DockerClientConfiguration = new DockerClientConfiguration();
+ DockerClientConfiguration = CreateDockerClientConfigurationFromEnvironment();
DockerClient = DockerClientConfiguration.CreateClient(logger: this);
Cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
Cts.Token.Register(() => throw new TimeoutException("Docker.DotNet tests timed out."));
@@ -194,6 +194,62 @@ protected override void OnReport(JSONMessage value)
this.LogInformation("Progress: '{Progress}'.", message);
}
+ private static DockerClientConfiguration CreateDockerClientConfigurationFromEnvironment()
+ {
+ var dockerHost = Environment.GetEnvironmentVariable("DOCKER_HOST");
+
+ // Fall back to OS-specific default (npipe on Windows, unix socket on Linux/macOS).
+ if (string.IsNullOrWhiteSpace(dockerHost))
+ {
+ return new DockerClientConfiguration();
+ }
+
+ var endpoint = new Uri(dockerHost);
+ var credentials = CreateCredentialsFromEnvironment();
+
+ return new DockerClientConfiguration(endpoint, credentials);
+ }
+
+ private static Credentials CreateCredentialsFromEnvironment()
+ {
+ var tlsVerify = Environment.GetEnvironmentVariable("DOCKER_TLS_VERIFY");
+ if (!string.Equals(tlsVerify, "1", StringComparison.Ordinal))
+ {
+ return new AnonymousCredentials();
+ }
+
+ var certPath = Environment.GetEnvironmentVariable("DOCKER_CERT_PATH");
+ if (string.IsNullOrWhiteSpace(certPath))
+ {
+ throw new InvalidOperationException("DOCKER_TLS_VERIFY=1 requires DOCKER_CERT_PATH to be set.");
+ }
+
+ var caPemPath = Path.Combine(certPath, "ca.pem");
+ var certPemPath = Path.Combine(certPath, "cert.pem");
+ var keyPemPath = Path.Combine(certPath, "key.pem");
+ var pfxPath = Path.Combine(certPath, "client.pfx");
+
+ DockerTlsCertificates tlsCertificates;
+
+ if (File.Exists(certPemPath) && File.Exists(keyPemPath))
+ {
+ tlsCertificates = DockerTlsCertificates.LoadFromDirectory(certPath, loadCertificateAuthority: true);
+ }
+ else if (File.Exists(pfxPath))
+ {
+ var clientCertificate = DockerTlsCertificates.LoadCertificateFromPfxFile(pfxPath, string.Empty);
+ var caCertificate = File.Exists(caPemPath) ? DockerTlsCertificates.LoadCertificateAuthorityFromPemFile(caPemPath) : null;
+
+ tlsCertificates = new DockerTlsCertificates(clientCertificate, caCertificate);
+ }
+ else
+ {
+ throw new FileNotFoundException($"Could not locate Docker TLS client credentials. Looked for '{certPemPath}', '{keyPemPath}', and '{pfxPath}'.");
+ }
+
+ return tlsCertificates.CreateCredentials();
+ }
+
private sealed class Disposable : IDisposable
{
public void Dispose()