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()