diff --git a/Consul.Test/BaseFixture.cs b/Consul.Test/BaseFixture.cs
index 97096ade4..db45aff5b 100644
--- a/Consul.Test/BaseFixture.cs
+++ b/Consul.Test/BaseFixture.cs
@@ -118,7 +118,7 @@ public async Task InitializeAsync()
}
}
- public class EnterpriseOnlyFact : FactAttribute
+ public class EnterpriseOnlyFact : SkippableFactAttribute
{
public EnterpriseOnlyFact()
{
diff --git a/Consul.Test/NamespaceTest.cs b/Consul.Test/NamespaceTest.cs
new file mode 100644
index 000000000..28c5d127a
--- /dev/null
+++ b/Consul.Test/NamespaceTest.cs
@@ -0,0 +1,171 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2020 G-Research Limited
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// -----------------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using NuGet.Versioning;
+using Xunit;
+
+namespace Consul.Test
+{
+ public class NamespaceTest : BaseFixture
+ {
+ [EnterpriseOnlyFact]
+ public async Task Namespaces_CreateNamespace()
+ {
+ var cutOffVersion = SemanticVersion.Parse("1.7.0");
+ Skip.If(AgentVersion < cutOffVersion, $"Current version is {AgentVersion}, but `Namespaces` is only supported from Consul {cutOffVersion}");
+
+ var name = "test";
+
+ var ns = new Namespace
+ {
+ Name = name
+ };
+
+ var request = await _client.Namespaces.Create(ns);
+
+ Assert.Equal(request.Response.Name, name);
+ }
+
+ [EnterpriseOnlyFact]
+ public async Task Namespaces_UpdateNamespace()
+ {
+ var cutOffVersion = SemanticVersion.Parse("1.7.0");
+ Skip.If(AgentVersion < cutOffVersion, $"Current version is {AgentVersion}, but `Namespaces` is only supported from Consul {cutOffVersion}");
+
+ var name = "test";
+
+ var ns = new Namespace
+ {
+ Name = name
+ };
+
+ await _client.Namespaces.Create(ns);
+
+ var description = "updated namespace";
+
+ var newNamespace = new Namespace
+ {
+ Name = name,
+ Description = description
+ };
+
+ var updateRequest = await _client.Namespaces.Update(newNamespace);
+
+ Assert.Equal(updateRequest.Response.Description, description);
+ }
+
+ [EnterpriseOnlyFact]
+ public async Task Namespaces_ReadNamespace()
+ {
+ var cutOffVersion = SemanticVersion.Parse("1.7.0");
+ Skip.If(AgentVersion < cutOffVersion, $"Current version is {AgentVersion}, but `Namespaces` is only supported from Consul {cutOffVersion}");
+
+ var name = "test";
+
+ var ns = new Namespace
+ {
+ Name = name
+ };
+
+ await _client.Namespaces.Create(ns);
+ var request = await _client.Namespaces.Read(name);
+
+ Assert.Equal(request.Response.Name, name);
+ }
+
+
+ [EnterpriseOnlyFact]
+ public async Task Namespaces_ListNamespaces()
+ {
+ var cutOffVersion = SemanticVersion.Parse("1.7.0");
+ Skip.If(AgentVersion < cutOffVersion, $"Current version is {AgentVersion}, but `Namespaces` is only supported from Consul {cutOffVersion}");
+
+ var testNames = new HashSet { "test-a", "test-b", "test-c" };
+
+ foreach (var name in testNames)
+ {
+ var ns = new Namespace
+ {
+ Name = name
+ };
+
+ await _client.Namespaces.Create(ns);
+ }
+
+ var request = await _client.Namespaces.List();
+ testNames.Add("default");
+ Assert.True(new HashSet(request.Response.Select(x => x.Name)).SetEquals(testNames));
+ }
+
+ [EnterpriseOnlyFact]
+ public async Task Namespaces_DeleteNamespace()
+ {
+ var cutOffVersion = SemanticVersion.Parse("1.7.0");
+ Skip.If(AgentVersion < cutOffVersion, $"Current version is {AgentVersion}, but `Namespaces` is only supported from Consul {cutOffVersion}");
+
+ var name = "test";
+
+ var ns = new Namespace
+ {
+ Name = name
+ };
+
+ var createRequest = await _client.Namespaces.Create(ns);
+ Assert.Equal(name, createRequest.Response.Name);
+ Assert.Null(createRequest.Response.DeletedAt);
+
+ await _client.Namespaces.Delete(name);
+
+ var readRequest = await _client.Namespaces.Read(name);
+ Assert.NotNull(readRequest.Response.DeletedAt);
+ }
+
+ [EnterpriseOnlyFact]
+ public async Task Namespaces_KVIsolation()
+ {
+ var cutOffVersion = SemanticVersion.Parse("1.7.0");
+ Skip.If(AgentVersion < cutOffVersion, $"Current version is {AgentVersion}, but `Namespaces` is only supported from Consul {cutOffVersion}");
+
+ var name = "test";
+
+ await _client.Namespaces.Create(new Namespace
+ {
+ Name = name
+ });
+
+ var key = "key";
+
+ var requestPair = new KVPair
+ {
+ Key = key,
+ Value = Encoding.UTF8.GetBytes("value")
+ };
+
+ await _client.KV.Put(requestPair, new WriteOptions { Namespace = name });
+ var namespaceResponsePair = await _client.KV.Get(key, new QueryOptions { Namespace = name });
+ Assert.Equal(requestPair.Value, namespaceResponsePair.Response.Value);
+
+ var defaultNamespaceResponsePair = await _client.KV.Get(key);
+ Assert.Null(defaultNamespaceResponsePair.Response?.Value);
+ }
+ }
+}
diff --git a/Consul/Client.cs b/Consul/Client.cs
index fd76ac99a..674ea7ff5 100644
--- a/Consul/Client.cs
+++ b/Consul/Client.cs
@@ -68,6 +68,12 @@ internal bool DisableServerCertificateValidation
///
public Uri Address { get; set; }
+ ///
+ /// Namespace is the name of the namespace to send along for the request
+ /// when no other Namespace is present in the QueryOptions
+ ///
+ public string Namespace { get; set; }
+
///
/// Datacenter to provide with each request. If not provided, the default agent datacenter is used.
///
@@ -86,10 +92,7 @@ internal bool DisableServerCertificateValidation
#endif
public NetworkCredential HttpAuth
{
- internal get
- {
- return _httpauth;
- }
+ internal get => _httpauth;
set
{
_httpauth = value;
@@ -148,6 +151,12 @@ public ConsulClientConfiguration()
UriBuilder consulAddress = new UriBuilder("http://127.0.0.1:8500");
ConfigureFromEnvironment(consulAddress);
Address = consulAddress.Uri;
+
+ string ns = Environment.GetEnvironmentVariable("CONSUL_NAMESPACE");
+ if (!string.IsNullOrEmpty(ns))
+ {
+ Namespace = ns;
+ }
}
///
@@ -234,9 +243,10 @@ private void ConfigureFromEnvironment(UriBuilder consulAddress)
#pragma warning restore CS0618 // Type or member is obsolete
}
- if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CONSUL_HTTP_TOKEN")))
+ string token = Environment.GetEnvironmentVariable("CONSUL_HTTP_TOKEN");
+ if (!string.IsNullOrEmpty(token))
{
- Token = Environment.GetEnvironmentVariable("CONSUL_HTTP_TOKEN");
+ Token = token;
}
}
@@ -471,6 +481,7 @@ private void InitializeEndpoints()
_token = new Lazy(() => new Token(this));
_aclReplication = new Lazy(() => new ACLReplication(this));
_authMethod = new Lazy(() => new AuthMethod(this));
+ _namespaces = new Lazy(() => new Namespaces(this));
}
#region IDisposable Support
diff --git a/Consul/Client_DeleteRequests.cs b/Consul/Client_DeleteRequests.cs
index 34b3e42db..7d8b98dce 100644
--- a/Consul/Client_DeleteRequests.cs
+++ b/Consul/Client_DeleteRequests.cs
@@ -38,7 +38,7 @@ public DeleteReturnRequest(ConsulClient client, string url, WriteOptions options
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
Options = options ?? WriteOptions.Default;
}
@@ -93,6 +93,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
@@ -119,7 +124,7 @@ public DeleteRequest(ConsulClient client, string url, WriteOptions options = nul
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
Options = options ?? WriteOptions.Default;
}
@@ -168,6 +173,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
@@ -190,13 +200,13 @@ protected override void ApplyHeaders(HttpRequestMessage message, ConsulClientCon
public class DeleteAcceptingRequest : ConsulRequest
{
public WriteOptions Options { get; set; }
- private TIn _body;
+ private readonly TIn _body;
public DeleteAcceptingRequest(ConsulClient client, string url, TIn body, WriteOptions options = null) : base(client, url, HttpMethod.Delete)
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
_body = body;
Options = options ?? WriteOptions.Default;
@@ -217,7 +227,7 @@ public async Task Execute(CancellationToken ct)
if (typeof(TIn) == typeof(byte[]))
{
- content = new ByteArrayContent((_body as byte[]) ?? new byte[0]);
+ content = new ByteArrayContent((_body as byte[]) ?? Array.Empty());
}
else if (typeof(TIn) == typeof(Stream))
{
@@ -263,6 +273,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
diff --git a/Consul/Client_GetRequests.cs b/Consul/Client_GetRequests.cs
index d15cb2a0c..dbc3c9ded 100644
--- a/Consul/Client_GetRequests.cs
+++ b/Consul/Client_GetRequests.cs
@@ -144,6 +144,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
@@ -348,6 +353,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
diff --git a/Consul/Client_Options.cs b/Consul/Client_Options.cs
index ebd5e72dc..958b7ccbe 100644
--- a/Consul/Client_Options.cs
+++ b/Consul/Client_Options.cs
@@ -37,6 +37,12 @@ public class QueryOptions
WaitIndex = 0
};
+ ///
+ /// Namespace is the name of the namespace to send along for the request when no other Namespace is present in the QueryOptions.
+ /// Namespace is an Enterprise-only feature.
+ ///
+ public string Namespace { get; set; }
+
///
/// Providing a datacenter overwrites the DC provided by the Config.
///
@@ -48,6 +54,7 @@ public class QueryOptions
public ConsistencyMode Consistency { get; set; }
///
+ /// WaitIndex is used to enable a blocking query. Waits until the timeout or the next index is reached.
/// UseCache requests that the agent cache results locally.
/// See https://www.consul.io/api/features/caching.html for more details on the semantics.
///
@@ -106,6 +113,12 @@ public class WriteOptions
Token = string.Empty
};
+ ///
+ /// Namespace is the name of the namespace to send along for the request when no other Namespace is present in the QueryOptions
+ /// Namespace is an Enterprise-only feature.
+ ///
+ public string Namespace { get; set; }
+
///
/// Providing a datacenter overwrites the DC provided by the Config
///
diff --git a/Consul/Client_PostRequests.cs b/Consul/Client_PostRequests.cs
index f8a6a769c..ba1850061 100644
--- a/Consul/Client_PostRequests.cs
+++ b/Consul/Client_PostRequests.cs
@@ -39,7 +39,7 @@ public PostReturningRequest(ConsulClient client, string url, WriteOptions option
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
Options = options ?? WriteOptions.Default;
@@ -97,6 +97,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
@@ -119,13 +124,13 @@ protected override void ApplyHeaders(HttpRequestMessage message, ConsulClientCon
public class PostRequest : ConsulRequest
{
public WriteOptions Options { get; set; }
- private TIn _body;
+ private readonly TIn _body;
public PostRequest(ConsulClient client, string url, TIn body, WriteOptions options = null) : base(client, url, HttpMethod.Post)
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
_body = body;
Options = options ?? WriteOptions.Default;
@@ -192,6 +197,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
@@ -215,13 +225,13 @@ protected override void ApplyHeaders(HttpRequestMessage message, ConsulClientCon
public class PostRequest : ConsulRequest
{
public WriteOptions Options { get; set; }
- private TIn _body;
+ private readonly TIn _body;
public PostRequest(ConsulClient client, string url, TIn body, WriteOptions options = null) : base(client, url, HttpMethod.Post)
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
_body = body;
Options = options ?? WriteOptions.Default;
@@ -293,6 +303,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
@@ -314,13 +329,13 @@ protected override void ApplyHeaders(HttpRequestMessage message, ConsulClientCon
public class PostRequest : ConsulRequest
{
public WriteOptions Options { get; set; }
- private string _body;
+ private readonly string _body;
public PostRequest(ConsulClient client, string url, string body, WriteOptions options = null) : base(client, url, HttpMethod.Post)
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
_body = body;
Options = options ?? WriteOptions.Default;
@@ -336,10 +351,8 @@ public async Task> Execute(CancellationToken ct)
Client.CheckDisposed();
var timer = Stopwatch.StartNew();
var result = new WriteResult();
-
- HttpContent content = null;
var bodyBytes = Encoding.UTF8.GetBytes(_body);
- content = new ByteArrayContent(bodyBytes);
+ HttpContent content = new ByteArrayContent(bodyBytes);
var message = new HttpRequestMessage(HttpMethod.Post, BuildConsulUri(Endpoint, Params));
ApplyHeaders(message, Client.Config);
@@ -383,6 +396,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
diff --git a/Consul/Client_PutRequests.cs b/Consul/Client_PutRequests.cs
index 11cdd4daa..7ec0b3915 100644
--- a/Consul/Client_PutRequests.cs
+++ b/Consul/Client_PutRequests.cs
@@ -38,7 +38,7 @@ public PutReturningRequest(ConsulClient client, string url, WriteOptions options
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
Options = options ?? WriteOptions.Default;
}
@@ -92,6 +92,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
@@ -118,7 +123,7 @@ public PutNothingRequest(ConsulClient client, string url, WriteOptions options =
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
Options = options ?? WriteOptions.Default;
}
@@ -167,6 +172,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
@@ -189,13 +199,13 @@ protected override void ApplyHeaders(HttpRequestMessage message, ConsulClientCon
public class PutRequest : ConsulRequest
{
public WriteOptions Options { get; set; }
- private TIn _body;
+ private readonly TIn _body;
public PutRequest(ConsulClient client, string url, TIn body, WriteOptions options = null) : base(client, url, HttpMethod.Put)
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
_body = body;
Options = options ?? WriteOptions.Default;
@@ -216,7 +226,7 @@ public async Task Execute(CancellationToken ct)
if (typeof(TIn) == typeof(byte[]))
{
- content = new ByteArrayContent((_body as byte[]) ?? new byte[0]);
+ content = new ByteArrayContent((_body as byte[]) ?? Array.Empty());
}
else if (typeof(TIn) == typeof(Stream))
{
@@ -262,6 +272,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
@@ -285,13 +300,13 @@ protected override void ApplyHeaders(HttpRequestMessage message, ConsulClientCon
public class PutRequest : ConsulRequest
{
public WriteOptions Options { get; set; }
- private TIn _body;
+ private readonly TIn _body;
public PutRequest(ConsulClient client, string url, TIn body, WriteOptions options = null) : base(client, url, HttpMethod.Put)
{
if (string.IsNullOrEmpty(url))
{
- throw new ArgumentException(nameof(url));
+ throw new ArgumentException(null, nameof(url));
}
_body = body;
Options = options ?? WriteOptions.Default;
@@ -312,7 +327,7 @@ public async Task> Execute(CancellationToken ct)
if (typeof(TIn) == typeof(byte[]))
{
- var bodyBytes = (_body as byte[]);
+ var bodyBytes = _body as byte[];
if (bodyBytes != null)
{
content = new ByteArrayContent(bodyBytes);
@@ -367,6 +382,11 @@ protected override void ApplyOptions(ConsulClientConfiguration clientConfig)
return;
}
+ if (!string.IsNullOrEmpty(Options.Namespace))
+ {
+ Params["ns"] = Options.Namespace;
+ }
+
if (!string.IsNullOrEmpty(Options.Datacenter))
{
Params["dc"] = Options.Datacenter;
diff --git a/Consul/Client_Request.cs b/Consul/Client_Request.cs
index 24723f819..965aa54da 100644
--- a/Consul/Client_Request.cs
+++ b/Consul/Client_Request.cs
@@ -18,7 +18,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
using System.Net.Http;
using Newtonsoft.Json;
@@ -72,6 +71,10 @@ internal ConsulRequest(ConsulClient client, string url, HttpMethod method)
{
Params["wait"] = client.Config.WaitTime.Value.ToGoDuration();
}
+ if (!string.IsNullOrEmpty(client.Config.Namespace))
+ {
+ Params["ns"] = client.Config.Namespace;
+ }
}
protected abstract void ApplyOptions(ConsulClientConfiguration clientConfig);
@@ -79,8 +82,10 @@ internal ConsulRequest(ConsulClient client, string url, HttpMethod method)
protected Uri BuildConsulUri(string url, Dictionary p)
{
- var builder = new UriBuilder(Client.Config.Address);
- builder.Path = url;
+ var builder = new UriBuilder(Client.Config.Address)
+ {
+ Path = url
+ };
ApplyOptions(Client.Config);
@@ -113,7 +118,7 @@ protected TOut Deserialize(Stream stream)
}
}
- protected byte[] Serialize(object value)
+ protected static byte[] Serialize(object value)
{
return System.Text.Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value));
}
diff --git a/Consul/Interfaces/INamespacesEndpoint.cs b/Consul/Interfaces/INamespacesEndpoint.cs
new file mode 100644
index 000000000..25f40d2d2
--- /dev/null
+++ b/Consul/Interfaces/INamespacesEndpoint.cs
@@ -0,0 +1,40 @@
+// -----------------------------------------------------------------------
+//
+// Copyright 2020 G-Research Limited
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// -----------------------------------------------------------------------
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Consul
+{
+ ///
+ /// The interface for the Namespaces API Endpoints
+ ///
+ public interface INamespacesEndpoint
+ {
+ Task> Create(Namespace ns, WriteOptions q, CancellationToken ct = default);
+ Task> Create(Namespace ns, CancellationToken ct = default);
+ Task> Update(Namespace ns, WriteOptions q, CancellationToken ct = default);
+ Task> Update(Namespace ns, CancellationToken ct = default);
+ Task> Read(string name, QueryOptions q, CancellationToken ct = default);
+ Task> Read(string name, CancellationToken ct = default);
+ Task> List(QueryOptions q, CancellationToken ct = default);
+ Task> List(CancellationToken ct = default);
+ Task Delete(string name, WriteOptions q, CancellationToken ct = default);
+ Task Delete(string name, CancellationToken ct = default);
+ }
+}
diff --git a/Consul/Namespace.cs b/Consul/Namespace.cs
new file mode 100644
index 000000000..7ae11f85d
--- /dev/null
+++ b/Consul/Namespace.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace Consul
+{
+ public class Namespace
+ {
+ public string Name { get; set; }
+
+ public string Description { get; set; }
+
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+ public NamespaceACLConfig ACLs { get; set; }
+
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
+ public Dictionary Meta { get; set; }
+ }
+
+ public class NamespaceResponse : Namespace
+ {
+ public ulong CreateIndex { get; set; }
+ public ulong ModifyIndex { get; set; }
+ public DateTime? DeletedAt { get; set; }
+ }
+
+ public class NamespaceACLConfig
+ {
+ public List PolicyDefaults { get; set; }
+ public List RoleDefaults { get; set; }
+ }
+
+ public class Namespaces : INamespacesEndpoint
+ {
+ private readonly ConsulClient _client;
+
+ internal Namespaces(ConsulClient c)
+ {
+ _client = c;
+ }
+
+ public async Task> Create(Namespace ns, WriteOptions q, CancellationToken ct = default)
+ {
+ var res = await _client.Put("v1/namespace", ns, q).Execute(ct).ConfigureAwait(false);
+ return new WriteResult(res, res.Response);
+ }
+
+ public async Task> Create(Namespace ns, CancellationToken ct = default)
+ {
+ var res = await _client.Put("v1/namespace", ns, WriteOptions.Default).Execute(ct).ConfigureAwait(false);
+ return new WriteResult(res, res.Response);
+ }
+
+ public async Task> Update(Namespace ns, WriteOptions q, CancellationToken ct = default)
+ {
+ var res = await _client.Put($"v1/namespace/{ns.Name}", ns, q).Execute(ct).ConfigureAwait(false);
+ return new WriteResult(res, res.Response);
+ }
+
+ public async Task> Update(Namespace ns, CancellationToken ct = default)
+ {
+ var res = await _client.Put($"v1/namespace/{ns.Name}", ns, WriteOptions.Default).Execute(ct).ConfigureAwait(false);
+ return new WriteResult(res, res.Response);
+ }
+
+ public Task> Read(string name, QueryOptions q, CancellationToken ct = default)
+ {
+ return _client.Get($"v1/namespace/{name}", q).Execute(ct);
+ }
+
+ public Task> Read(string name, CancellationToken ct = default)
+ {
+ return _client.Get($"v1/namespace/{name}", QueryOptions.Default).Execute(ct);
+ }
+
+ public Task> List(QueryOptions q, CancellationToken ct = default)
+ {
+ return _client.Get($"v1/namespaces", q).Execute(ct);
+ }
+
+ public Task> List(CancellationToken ct = default)
+ {
+ return _client.Get($"v1/namespaces", QueryOptions.Default).Execute(ct);
+ }
+
+ public Task Delete(string name, WriteOptions q, CancellationToken ct = default)
+ {
+ return _client.Delete($"v1/namespace/{name}", q).Execute(ct);
+ }
+
+ public Task Delete(string name, CancellationToken ct = default)
+ {
+ return _client.Delete($"v1/namespace/{name}", WriteOptions.Default).Execute(ct);
+ }
+ }
+
+ public partial class ConsulClient : IConsulClient
+ {
+ private Lazy _namespaces;
+
+ ///
+ /// Namespaces returns a handle to the namespaces endpoint
+ ///
+ public INamespacesEndpoint Namespaces => _namespaces.Value;
+ }
+}
diff --git a/docs/docs/2-guides/1-basic-usage.mdx b/docs/docs/2-guides/1-basic-usage.mdx
index 222ccbc5d..721b4e8a1 100644
--- a/docs/docs/2-guides/1-basic-usage.mdx
+++ b/docs/docs/2-guides/1-basic-usage.mdx
@@ -100,3 +100,10 @@ lock. It is an implementation of the [Consul Leader Election](https://consul.io/
Semaphore is used to implement a distributed semaphore using the Consul
KV primitives. It is an implementation of the [Consul Semaphore](https://www.consul.io/docs/guides/semaphore.html) guide.
+
+### Namespaces
+
+Namespaces help reduce operational challenges by removing restrictions
+around uniqueness of resource names across distinct teams, and enable operators to provide self-service through
+delegation of administrative privileges. It is an implementation of
+[Consul Namespaces](https://developer.hashicorp.com/consul/docs/enterprise/namespaces).
diff --git a/docs/docs/2-guides/3-supported-apis.mdx b/docs/docs/2-guides/3-supported-apis.mdx
index 938abe801..4ef452995 100644
--- a/docs/docs/2-guides/3-supported-apis.mdx
+++ b/docs/docs/2-guides/3-supported-apis.mdx
@@ -45,4 +45,10 @@ import {ConsulAPIBadge} from "@site/src/components/CustomBadge";
| KV Store | Read Key | GET /v1/kv/:key | ✅ |
| | Create/Update Key | PUT /v1/kv/:key | ✅ |
| | Delete Key | DELETE /v2/kv/:key | ✅ |
+| Namespace | List Namespaces | GET /v1/namespaces | ✅ |
+| | Read Namespace | GET /v1/namespace/:name | ✅ |
+| | Create Namespace | PUT /v1/namespace | ✅ |
+| | Update Namespace | PUT /v1/namespace/:name | ✅ |
+| | Delete Namespace | DELETE /v1/namespace/:name | ✅ |
+