Skip to content

Commit 9679fe4

Browse files
authored
feat: unix socket support for flagd provider (#42)
Signed-off-by: Florian Bacher <[email protected]>
1 parent 4c81dd3 commit 9679fe4

File tree

6 files changed

+120
-27
lines changed

6 files changed

+120
-27
lines changed

.github/workflows/linux-ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616

1717
strategy:
1818
matrix:
19-
version: [net6.0]
19+
version: [net6.0,net7.0]
2020

2121
steps:
2222
- uses: actions/checkout@v3

.github/workflows/windows-ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616

1717
strategy:
1818
matrix:
19-
version: [net462,net6.0]
19+
version: [net462,net6.0,net7.0]
2020

2121
steps:
2222
- uses: actions/checkout@v3

build/Common.prod.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
</PropertyGroup>
77

88
<PropertyGroup>
9-
<TargetFrameworks>netstandard2.0;net462</TargetFrameworks>
9+
<TargetFrameworks>netstandard2.0;net462;net5.0;net6.0;net7.0</TargetFrameworks>
1010
<RepositoryType>git</RepositoryType>
1111
<RepositoryUrl>https://github.com/open-feature/dotnet-sdk</RepositoryUrl>
1212
<Description>OpenFeature is an open standard for feature flag management, created to support a robust feature flag ecosystem using cloud native technologies. OpenFeature will provide a unified API and SDK, and a developer-first, cloud-native implementation, with extensibility for open source and commercial offerings.</Description>

src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs

+50-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Linq;
33
using System.Threading.Tasks;
4+
45
using Google.Protobuf.WellKnownTypes;
56
using Grpc.Core;
67
using Grpc.Net.Client;
@@ -12,6 +13,7 @@
1213
using Value = OpenFeature.Model.Value;
1314
using ProtoValue = Google.Protobuf.WellKnownTypes.Value;
1415
using System.Net.Http;
16+
using System.Net.Sockets;
1517

1618
namespace OpenFeature.Contrib.Providers.Flagd
1719
{
@@ -26,27 +28,38 @@ public sealed class FlagdProvider : FeatureProvider
2628
/// <summary>
2729
/// Constructor of the provider. This constructor uses the value of the following
2830
/// environment variables to initialise its client:
29-
/// FLAGD_HOST - The host name of the flagd server (default="localhost")
30-
/// FLAGD_PORT - The port of the flagd server (default="8013")
31-
/// FLAGD_TLS - Determines whether to use https or not (default="false")
31+
/// FLAGD_HOST - The host name of the flagd server (default="localhost")
32+
/// FLAGD_PORT - The port of the flagd server (default="8013")
33+
/// FLAGD_TLS - Determines whether to use https or not (default="false")
34+
/// FLAGD_SOCKET_PATH - Path to the unix socket (default="")
3235
/// </summary>
3336
public FlagdProvider()
3437
{
3538
var flagdHost = Environment.GetEnvironmentVariable("FLAGD_HOST") ?? "localhost";
3639
var flagdPort = Environment.GetEnvironmentVariable("FLAGD_PORT") ?? "8013";
3740
var flagdUseTLSStr = Environment.GetEnvironmentVariable("FLAGD_TLS") ?? "false";
41+
var flagdSocketPath = Environment.GetEnvironmentVariable("FLAGD_SOCKET_PATH") ?? "";
3842

3943

40-
var protocol = "http";
41-
var useTLS = bool.Parse(flagdUseTLSStr);
42-
43-
if (useTLS)
44+
Uri uri;
45+
if (flagdSocketPath != "")
4446
{
45-
protocol = "https";
47+
uri = new Uri("unix://" + flagdSocketPath);
4648
}
49+
else
50+
{
51+
var protocol = "http";
52+
var useTLS = bool.Parse(flagdUseTLSStr);
4753

48-
var url = new Uri(protocol + "://" + flagdHost + ":" + flagdPort);
49-
_client = buildClientForPlatform(url);
54+
if (useTLS)
55+
{
56+
protocol = "https";
57+
}
58+
59+
uri = new Uri(protocol + "://" + flagdHost + ":" + flagdPort);
60+
}
61+
62+
_client = buildClientForPlatform(uri);
5063
}
5164

5265
/// <summary>
@@ -373,14 +386,37 @@ private static Value ConvertToPrimitiveValue(ProtoValue value)
373386

374387
private static Service.ServiceClient buildClientForPlatform(Uri url)
375388
{
376-
#if NETSTANDARD2_0
377-
return new Service.ServiceClient(GrpcChannel.ForAddress(url));
389+
var useUnixSocket = url.ToString().StartsWith("unix://");
390+
391+
if (!useUnixSocket)
392+
{
393+
#if NET462
394+
return new Service.ServiceClient(GrpcChannel.ForAddress(url, new GrpcChannelOptions
395+
{
396+
HttpHandler = new WinHttpHandler()
397+
}));
378398
#else
379-
return new Service.ServiceClient(GrpcChannel.ForAddress(url, new GrpcChannelOptions
399+
return new Service.ServiceClient(GrpcChannel.ForAddress(url));
400+
#endif
401+
}
402+
403+
#if NET5_0_OR_GREATER
404+
var udsEndPoint = new UnixDomainSocketEndPoint(url.ToString().Substring("unix://".Length));
405+
var connectionFactory = new UnixDomainSocketConnectionFactory(udsEndPoint);
406+
var socketsHttpHandler = new SocketsHttpHandler
407+
{
408+
ConnectCallback = connectionFactory.ConnectAsync
409+
};
410+
411+
// point to localhost and let the custom ConnectCallback handle the communication over the unix socket
412+
// see https://learn.microsoft.com/en-us/aspnet/core/grpc/interprocess-uds?view=aspnetcore-7.0 for more details
413+
return new Service.ServiceClient(GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
380414
{
381-
HttpHandler = new WinHttpHandler()
415+
HttpHandler = socketsHttpHandler
382416
}));
383417
#endif
418+
// unix socket support is not available in this dotnet version
419+
throw new Exception("unix sockets are not supported in this version.");
384420
}
385421
}
386422
}

src/OpenFeature.Contrib.Providers.Flagd/README.md

+18-10
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,29 @@ namespace OpenFeatureTestApp
7676

7777
The URI of the flagd server to which the `flagd Provider` connects to can either be passed directly to the constructor, or be configured using the following environment variables:
7878

79-
| Option name | Environment variable name | Type | Default | Values |
80-
| --------------------- | ------------------------------- | ------- | --------- | ------------- |
81-
| host | FLAGD_HOST | string | localhost | |
82-
| port | FLAGD_PORT | number | 8013 | |
83-
| tls | FLAGD_TLS | boolean | false | |
79+
| Option name | Environment variable name | Type | Default | Values |
80+
|------------------|--------------------------------|---------|-----------| ------------- |
81+
| host | FLAGD_HOST | string | localhost | |
82+
| port | FLAGD_PORT | number | 8013 | |
83+
| tls | FLAGD_TLS | boolean | false | |
84+
| unix socket path | FLAGD_SOCKET_PATH | string | | |
8485

85-
So for example, if you would like to pass the URI directly, you can initialise it as follows:
86+
Note that if `FLAGD_SOCKET_PATH` is set, this value takes precedence, and the other variables (`FLAGD_HOST`, `FLAGD_PORT`, `FLAGD_TLS`) are disregarded.
87+
88+
89+
If you rely on the environment variables listed above, you can use the empty constructor which then configures the provider accordingly:
8690

8791
```csharp
88-
var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013"));
92+
var flagdProvider = new FlagdProvider();
8993
```
9094

91-
Or, if you rely on the environment variables listed above, you can use the empty costructor:
92-
95+
Alternatively, if you would like to pass the URI directly, you can initialise it as follows:
9396

9497
```csharp
95-
var flagdProvider = new FlagdProvider();
98+
// either communicate with Flagd over HTTP ...
99+
var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013"));
100+
101+
// ... or use the unix:// prefix if the provider should communicate via a unix socket
102+
var unixFlagdProvider = new FlagdProvider(new Uri("unix://socket.tmp"));
96103
```
104+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System;
2+
using System.IO;
3+
using System.Net;
4+
using System.Net.Http;
5+
using System.Net.Sockets;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace OpenFeature.Contrib.Providers.Flagd
10+
{
11+
/// <inheritdoc/>
12+
public class UnixDomainSocketConnectionFactory
13+
{
14+
private readonly EndPoint _endPoint;
15+
16+
/// <summary>
17+
/// Constructor of the connection factory
18+
/// <param name="endpoint">The path to the unix socket</param>
19+
/// </summary>
20+
public UnixDomainSocketConnectionFactory(EndPoint endpoint)
21+
{
22+
_endPoint = endpoint;
23+
}
24+
25+
#if NET5_0_OR_GREATER
26+
/// <summary>
27+
/// ConnectAsync is a custom implementation of the ConnectAsync method used by the grpc client
28+
/// </summary>
29+
/// <param name="_">unused - SocketsHttpConnectionContext</param>
30+
/// <param name="cancellationToken">The cancellation token</param>
31+
/// <returns>A ValueTask object representing the given</returns>
32+
public async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext _, CancellationToken cancellationToken = default)
33+
{
34+
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
35+
36+
try
37+
{
38+
await socket.ConnectAsync(_endPoint, cancellationToken).ConfigureAwait(false);
39+
return new NetworkStream(socket, true);
40+
}
41+
catch (Exception ex)
42+
{
43+
socket.Dispose();
44+
throw new HttpRequestException($"Error connecting to '{_endPoint}'.", ex);
45+
}
46+
}
47+
#endif
48+
}
49+
}

0 commit comments

Comments
 (0)