Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,37 +44,64 @@ If you intend to use development builds of Docker.DotNet and don't want to compi

## Usage

You can initialize the client like the following:
### Default client (docker CLI–style resolution)

The parameterless constructor resolves the Docker endpoint the same way the `docker` CLI does. This works on Linux, macOS, and Windows:

1. **Environment variables** — `DOCKER_HOST` (with `DOCKER_TLS_VERIFY` upgrading `tcp://` to `https://`).
2. **Docker context** — `DOCKER_CONTEXT`, or the `currentContext` field in `~/.docker/config.json` (`%USERPROFILE%\.docker\config.json` on Windows). The endpoint is read from `~/.docker/contexts/meta/<sha256(name)>/meta.json`. `DOCKER_CONFIG` is honored if set.
3. **Platform default** — `unix:///var/run/docker.sock` on Linux/macOS, `npipe://./pipe/docker_engine` on Windows.

```csharp
using Docker.DotNet;
DockerClient client = new DockerClientConfiguration(
new Uri("http://ubuntu-docker.cloudapp.net:4243"))
DockerClient client = new DockerClientConfiguration()
.CreateClient();
```
or to connect to your local [Docker for Windows](https://docs.docker.com/docker-for-windows/) daemon using named pipes or your local [Docker for Mac](https://docs.docker.com/docker-for-mac/) daemon using Unix sockets:

### Explicit endpoint

Pass a `Uri` to target a specific endpoint:

```csharp
using Docker.DotNet;
DockerClient client = new DockerClientConfiguration()
DockerClient client = new DockerClientConfiguration(
new Uri("http://ubuntu-docker.cloudapp.net:4243"))
.CreateClient();
```

For a custom endpoint, you can also pass a named pipe or a Unix socket to the `DockerClientConfiguration` constructor. For example:

```csharp
// Default Docker Engine on Windows
using Docker.DotNet;
// Docker Engine on Windows
DockerClient client = new DockerClientConfiguration(
new Uri("npipe://./pipe/docker_engine"))
.CreateClient();
// Default Docker Engine on Linux
using Docker.DotNet;
// Docker Engine on Linux/macOS
DockerClient client = new DockerClientConfiguration(
new Uri("unix:///var/run/docker.sock"))
.CreateClient();
```

### Specific Docker context by name

Use `FromContext` to target a context by name (reads `~/.docker/contexts/meta/<sha256(name)>/meta.json`):

```csharp
using Docker.DotNet;
DockerClient client = DockerClientConfiguration
.FromContext("desktop-linux")
.CreateClient();
```

You can combine it with credentials (TLS, basic auth, etc.) the same way as the regular constructor:

```csharp
var credentials = new CertificateCredentials(new X509Certificate2("cert.pfx", "password"));
DockerClient client = DockerClientConfiguration
.FromContext("my-remote", credentials)
.CreateClient();
```

> **Note:** `ssh://` endpoints (from SSH-based Docker contexts) are not supported. To connect to a remote daemon over SSH, set up an SSH tunnel and point `DOCKER_HOST` (or a context) at the forwarded socket.

#### Example: List containers

```csharp
Expand Down
5 changes: 5 additions & 0 deletions src/Docker.DotNet/DockerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ await sock.ConnectAsync(new Microsoft.Net.Http.Client.UnixDomainSocketEndPoint(p
uri = new UriBuilder("http", uri.Segments.Last()).Uri;
break;

case "ssh":
throw new NotSupportedException(
$"SSH endpoints are not supported by Docker.DotNet ({configuration.EndpointBaseUri}). " +
"Use a tcp/https/unix/npipe endpoint, or set up an SSH tunnel and point DOCKER_HOST at the forwarded socket.");

default:
throw new Exception($"Unknown URL scheme {configuration.EndpointBaseUri.Scheme}");
}
Expand Down
34 changes: 26 additions & 8 deletions src/Docker.DotNet/DockerClientConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;

namespace Docker.DotNet
{
public class DockerClientConfiguration : IDisposable
{
/// <summary>
/// Creates a configuration whose endpoint is resolved like the docker CLI:
/// the <c>DOCKER_HOST</c> environment variable first, then the active Docker
/// context (<c>DOCKER_CONTEXT</c> or <c>currentContext</c> in
/// <c>~/.docker/config.json</c>), then the platform default socket.
/// </summary>
public DockerClientConfiguration(
Credentials credentials = null,
TimeSpan defaultTimeout = default,
TimeSpan namedPipeConnectTimeout = default,
IReadOnlyDictionary<string, string> defaultHttpRequestHeaders = null)
: this(GetLocalDockerEndpoint(), credentials, defaultTimeout, namedPipeConnectTimeout, defaultHttpRequestHeaders)
: this(DockerContextResolver.Resolve(), credentials, defaultTimeout, namedPipeConnectTimeout, defaultHttpRequestHeaders)
{
}

/// <summary>
/// Creates a configuration that targets the named Docker context, reading
/// its endpoint from <c>~/.docker/contexts/meta/&lt;sha256(name)&gt;/meta.json</c>.
/// </summary>
public static DockerClientConfiguration FromContext(
string contextName,
Credentials credentials = null,
TimeSpan defaultTimeout = default,
TimeSpan namedPipeConnectTimeout = default,
IReadOnlyDictionary<string, string> defaultHttpRequestHeaders = null)
{
return new DockerClientConfiguration(
DockerContextResolver.ResolveForContext(contextName),
credentials,
defaultTimeout,
namedPipeConnectTimeout,
defaultHttpRequestHeaders);
}

public DockerClientConfiguration(
Expand Down Expand Up @@ -62,11 +86,5 @@ public void Dispose()
{
Credentials.Dispose();
}

private static Uri GetLocalDockerEndpoint()
{
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
return isWindows ? new Uri("npipe://./pipe/docker_engine") : new Uri("unix:/var/run/docker.sock");
}
}
}
210 changes: 210 additions & 0 deletions src/Docker.DotNet/DockerContextResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace Docker.DotNet
{
/// <summary>
/// Resolves the Docker daemon endpoint the same way the docker CLI does:
/// environment variables first, then the current Docker context, then the
/// platform default socket. Works on Linux, macOS, and Windows.
/// </summary>
internal static class DockerContextResolver
{
private const string DefaultContextName = "default";
private const string DockerEndpointKey = "docker";

public static Uri Resolve()
{
var host = Environment.GetEnvironmentVariable("DOCKER_HOST");
if (!string.IsNullOrEmpty(host))
{
return BuildHostUri(host, IsTlsVerifyEnabled());
}

var contextName = Environment.GetEnvironmentVariable("DOCKER_CONTEXT");
if (string.IsNullOrEmpty(contextName))
{
contextName = TryReadCurrentContext();
}

if (!string.IsNullOrEmpty(contextName) && !string.Equals(contextName, DefaultContextName, StringComparison.Ordinal))
{
var contextHost = TryReadContextHost(contextName);
if (!string.IsNullOrEmpty(contextHost))
{
return BuildHostUri(contextHost, IsTlsVerifyEnabled());
}
}

return GetPlatformDefault();
}

public static Uri ResolveForContext(string contextName)
{
if (string.IsNullOrEmpty(contextName))
{
throw new ArgumentException("Context name must be provided", nameof(contextName));
}

if (string.Equals(contextName, DefaultContextName, StringComparison.Ordinal))
{
return GetPlatformDefault();
}

var host = TryReadContextHost(contextName);
if (string.IsNullOrEmpty(host))
{
throw new InvalidOperationException($"Docker context '{contextName}' was not found under '{GetContextsMetaDirectory()}'.");
}

return BuildHostUri(host, tlsVerify: false);
}

private static bool IsTlsVerifyEnabled()
{
var value = Environment.GetEnvironmentVariable("DOCKER_TLS_VERIFY");
return !string.IsNullOrEmpty(value);
}

private static Uri BuildHostUri(string host, bool tlsVerify)
{
if (tlsVerify && host.StartsWith("tcp://", StringComparison.OrdinalIgnoreCase))
{
host = "https://" + host.Substring("tcp://".Length);
}

host = NormalizeNpipe(host);

return new Uri(host);
}

// Docker stores Windows named-pipe endpoints in Go's native form
// (npipe:////./pipe/<name>), but DockerClient's npipe handler expects
// npipe://./pipe/<name>. Collapse the former (and the localhost variant)
// to the latter so resolved contexts are usable on Windows.
private static string NormalizeNpipe(string host)
{
const string scheme = "npipe://";
if (!host.StartsWith(scheme, StringComparison.OrdinalIgnoreCase))
{
return host;
}

var pipe = host.Substring(scheme.Length).TrimStart('/');
if (pipe.StartsWith("./", StringComparison.Ordinal))
{
pipe = pipe.Substring("./".Length);
}
else if (pipe.StartsWith("localhost/", StringComparison.OrdinalIgnoreCase))
{
pipe = pipe.Substring("localhost/".Length);
}

return scheme + "./" + pipe;
}

private static string TryReadCurrentContext()
{
var configPath = Path.Combine(GetDockerConfigDirectory(), "config.json");
if (!File.Exists(configPath))
{
return null;
}

try
{
using var stream = File.OpenRead(configPath);
using var document = JsonDocument.Parse(stream);
if (document.RootElement.TryGetProperty("currentContext", out var element) && element.ValueKind == JsonValueKind.String)
{
return element.GetString();
}
}
catch (JsonException)
{
}
catch (IOException)
{
}

return null;
}

private static string TryReadContextHost(string contextName)
{
var metaPath = Path.Combine(GetContextsMetaDirectory(), Sha256Hex(contextName), "meta.json");
if (!File.Exists(metaPath))
{
return null;
}

try
{
using var stream = File.OpenRead(metaPath);
using var document = JsonDocument.Parse(stream);
if (!document.RootElement.TryGetProperty("Endpoints", out var endpoints) || endpoints.ValueKind != JsonValueKind.Object)
{
return null;
}

if (!endpoints.TryGetProperty(DockerEndpointKey, out var dockerEndpoint) || dockerEndpoint.ValueKind != JsonValueKind.Object)
{
return null;
}

if (dockerEndpoint.TryGetProperty("Host", out var hostElement) && hostElement.ValueKind == JsonValueKind.String)
{
return hostElement.GetString();
}
}
catch (JsonException)
{
}
catch (IOException)
{
}

return null;
}

private static string GetDockerConfigDirectory()
{
var dockerConfig = Environment.GetEnvironmentVariable("DOCKER_CONFIG");
if (!string.IsNullOrEmpty(dockerConfig))
{
return dockerConfig;
}

return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker");
}

private static string GetContextsMetaDirectory()
{
return Path.Combine(GetDockerConfigDirectory(), "contexts", "meta");
}

private static string Sha256Hex(string value)
{
using var sha = SHA256.Create();
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(value));
var builder = new StringBuilder(bytes.Length * 2);
foreach (var b in bytes)
{
builder.Append(b.ToString("x2"));
}

return builder.ToString();
}

private static Uri GetPlatformDefault()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? new Uri("npipe://./pipe/docker_engine")
: new Uri("unix:/var/run/docker.sock");
}
}
}
Loading