From 593786f9665f1cca5e7c9d51c0827253adb4ae3b Mon Sep 17 00:00:00 2001 From: saber-wang <45062099+saber-wang@users.noreply.github.com> Date: Sat, 24 Sep 2022 03:06:01 +0800 Subject: [PATCH] add metadata api (#947) * feat: add metadata api Signed-off-by: saberwang * Using HTTP API to get and set dapr metadata Signed-off-by: saberwang * style Signed-off-by: saberwang * using grpc to add metadata api Signed-off-by: saberwang * style Signed-off-by: saberwang Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> Signed-off-by: addjuarez --- src/Dapr.Client/DaprClient.cs | 32 ++++- src/Dapr.Client/DaprClientGrpc.cs | 40 ++++++ src/Dapr.Client/DaprMetadata.cs | 126 ++++++++++++++++ .../DaprClientTest.InvokeMethodAsync.cs | 4 +- .../DaprClientTest.InvokeMethodGrpcAsync.cs | 136 +++++++++++++++--- 5 files changed, 318 insertions(+), 20 deletions(-) create mode 100644 src/Dapr.Client/DaprMetadata.cs diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 88949c2c0..26bf1d27f 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -338,6 +338,36 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, stri /// A that will return when the operation has completed. public abstract Task ShutdownSidecarAsync(CancellationToken cancellationToken = default); + /// + /// Calls the sidecar's metadata endpoint which returns information including: + /// + /// + /// The sidecar's ID. + /// + /// + /// The registered/active actors if any. + /// + /// + /// Registered components including name, type, version, and information on capabilities if present. + /// + /// + /// Any extended metadata that has been set via + /// + /// + /// + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task GetMetadataAsync(CancellationToken cancellationToken = default); + + /// + /// Perform service add extended metadata to the Dapr sidecar. + /// + /// Custom attribute name + /// Custom attribute value + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task SetMetadataAsync(string attributeName, string attributeValue, CancellationToken cancellationToken = default); + /// /// Perform service invocation using the request provided by . The response will /// be returned without performing any validation on the status code. diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index a21b30caf..cb3111462 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -1367,6 +1367,46 @@ public async override Task ShutdownSidecarAsync(CancellationToken cancellationTo await client.ShutdownAsync(new Empty(), CreateCallOptions(null, cancellationToken)); } + /// + public override async Task GetMetadataAsync(CancellationToken cancellationToken = default) + { + var options = CreateCallOptions(headers: null, cancellationToken); + try + { + var response = await client.GetMetadataAsync(new Empty(), options); + return new DaprMetadata(response.Id, + response.ActiveActorsCount.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList(), + response.ExtendedMetadata.ToDictionary(c => c.Key, c => c.Value), + response.RegisteredComponents.Select(c => new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList()); + } + catch (RpcException ex) + { + throw new DaprException("Get metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } + + /// + public override async Task SetMetadataAsync(string attributeName, string attributeValue, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(attributeName, nameof(attributeName)); + + var envelope = new Autogenerated.SetMetadataRequest() + { + Key = attributeName, + Value = attributeValue + }; + + var options = CreateCallOptions(headers: null, cancellationToken); + + try + { + _ = await this.Client.SetMetadataAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("Set metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } #endregion protected override void Dispose(bool disposing) diff --git a/src/Dapr.Client/DaprMetadata.cs b/src/Dapr.Client/DaprMetadata.cs new file mode 100644 index 000000000..4cd812e04 --- /dev/null +++ b/src/Dapr.Client/DaprMetadata.cs @@ -0,0 +1,126 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// 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; + +namespace Dapr.Client +{ + /// + /// Represents a metadata object returned from dapr sidecar. + /// + public sealed class DaprMetadata + { + /// + /// Initializes a new instance of the class. + /// + /// The application id. + /// The registered actors metadata. + /// The list of custom attributes as key-value pairs, where key is the attribute name. + /// The loaded components metadata. + public DaprMetadata(string id, IReadOnlyList actors, IReadOnlyDictionary extended, IReadOnlyList components) + { + Id = id; + Actors = actors; + Extended = extended; + Components = components; + } + + /// + /// Gets the application id. + /// + public string Id { get; } + + /// + /// Gets the registered actors metadata. + /// + public IReadOnlyList Actors { get; } + + /// + /// Gets the list of custom attributes as key-value pairs, where key is the attribute name. + /// + public IReadOnlyDictionary Extended { get; } + + /// + /// Gets the loaded components metadata. + /// + public IReadOnlyList Components { get; } + } + + /// + /// Represents a actor metadata object returned from dapr sidecar. + /// + public sealed class DaprActorMetadata + { + /// + /// Initializes a new instance of the class. + /// + /// This registered actor type. + /// The number of actors running. + public DaprActorMetadata(string type, int count) + { + Type = type; + Count = count; + } + + /// + /// Gets the registered actor type. + /// + public string Type { get; } + + /// + /// Gets the number of actors running. + /// + public int Count { get; } + } + + /// + /// Represents a components metadata object returned from dapr sidecar. + /// + public sealed class DaprComponentsMetadata + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the component. + /// The component type. + /// The component version. + /// The supported capabilities for this component type and version. + public DaprComponentsMetadata(string name, string type, string version, string[] capabilities) + { + Name = name; + Type = type; + Version = version; + Capabilities = capabilities; + } + + /// + /// Gets the name of the component. + /// + public string Name { get; } + + /// + /// Gets the component type. + /// + public string Type { get; } + + /// + /// Gets the component version. + /// + public string Version { get; } + + /// + /// Gets the supported capabilities for this component type and version. + /// + public string[] Capabilities { get; } + } +} diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs index 6c5488674..5d46000a1 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ namespace Dapr.Client.Test { using System; + using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Json; @@ -22,6 +23,7 @@ namespace Dapr.Client.Test using System.Threading; using System.Threading.Tasks; using Dapr.Client; + using FluentAssertions; using Xunit; // Most of the InvokeMethodAsync functionality on DaprClient is non-abstract methods that diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs index 93462415a..5412c4063 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ public partial class DaprClientTest [Fact] public async Task InvokeMethodGrpcAsync_WithCancelledToken() { - await using var client = TestClient.CreateForDaprClient(c => + await using var client = TestClient.CreateForDaprClient(c => { c.UseJsonSerializationOptions(this.jsonSerializerOptions); }); @@ -50,7 +50,7 @@ await Assert.ThrowsAsync(async () => [Fact] public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeAndData() { - await using var client = TestClient.CreateForDaprClient(c => + await using var client = TestClient.CreateForDaprClient(c => { c.UseJsonSerializationOptions(this.jsonSerializerOptions); }); @@ -88,7 +88,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeAndData_Thr Data = Any.Pack(data), }; - var response = + var response = client.Call() .SetResponse(invokeResponse) .Build(); @@ -105,7 +105,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeAndData_Thr .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) .Throws(rpcException); - var ex = await Assert.ThrowsAsync(async () => + var ex = await Assert.ThrowsAsync(async () => { await client.DaprClient.InvokeMethodGrpcAsync("test", "test", new Request() { RequestParameter = "Hello " }); }); @@ -115,7 +115,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeAndData_Thr [Fact] public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeNoData() { - await using var client = TestClient.CreateForDaprClient(c => + await using var client = TestClient.CreateForDaprClient(c => { c.UseJsonSerializationOptions(this.jsonSerializerOptions); }); @@ -153,7 +153,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeNoData_Thro Data = Any.Pack(data), }; - var response = + var response = client.Call() .SetResponse(invokeResponse) .Build(); @@ -170,7 +170,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeNoData_Thro .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) .Throws(rpcException); - var ex = await Assert.ThrowsAsync(async () => + var ex = await Assert.ThrowsAsync(async () => { await client.DaprClient.InvokeMethodGrpcAsync("test", "test"); }); @@ -188,11 +188,11 @@ public void InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData() Data = Any.Pack(data), }; - var response = + var response = client.Call() .SetResponse(invokeResponse) .Build(); - + client.Mock .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) .Returns(response); @@ -210,7 +210,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData_T Data = Any.Pack(data), }; - var response = + var response = client.Call() .SetResponse(invokeResponse) .Build(); @@ -228,7 +228,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData_T .Setup(m => m.InvokeServiceAsync(It.IsAny(), It.IsAny())) .Throws(rpcException); - var ex = await Assert.ThrowsAsync(async () => + var ex = await Assert.ThrowsAsync(async () => { await client.DaprClient.InvokeMethodGrpcAsync("test", "test", new Request() { RequestParameter = "Hello " }); }); @@ -238,7 +238,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData_T [Fact] public async Task InvokeMethodGrpcAsync_WithNoReturnTypeAndData() { - await using var client = TestClient.CreateForDaprClient(c => + await using var client = TestClient.CreateForDaprClient(c => { c.UseJsonSerializationOptions(this.jsonSerializerOptions); }); @@ -256,7 +256,7 @@ public async Task InvokeMethodGrpcAsync_WithNoReturnTypeAndData() envelope.Id.Should().Be("test"); envelope.Message.Method.Should().Be("test"); envelope.Message.ContentType.Should().Be(Constants.ContentTypeApplicationGrpc); - + var actual = envelope.Message.Data.Unpack(); Assert.Equal(invokeRequest.RequestParameter, actual.RequestParameter); } @@ -264,7 +264,7 @@ public async Task InvokeMethodGrpcAsync_WithNoReturnTypeAndData() [Fact] public async Task InvokeMethodGrpcAsync_WithReturnTypeAndData() { - await using var client = TestClient.CreateForDaprClient(c => + await using var client = TestClient.CreateForDaprClient(c => { c.UseJsonSerializationOptions(this.jsonSerializerOptions); }); @@ -303,7 +303,7 @@ public async Task InvokeMethodGrpcAsync_AppCallback_SayHello() // Configure Client var httpClient = new AppCallbackClient(new DaprAppCallbackService()); var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions(){ HttpClient = httpClient, }) + .UseGrpcChannelOptions(new GrpcChannelOptions() { HttpClient = httpClient, }) .UseJsonSerializationOptions(this.jsonSerializerOptions) .Build(); @@ -320,7 +320,7 @@ public async Task InvokeMethodGrpcAsync_AppCallback_RepeatedField() // Configure Client var httpClient = new AppCallbackClient(new DaprAppCallbackService()); var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions(){ HttpClient = httpClient, }) + .UseGrpcChannelOptions(new GrpcChannelOptions() { HttpClient = httpClient, }) .UseJsonSerializationOptions(this.jsonSerializerOptions) .Build(); @@ -343,7 +343,7 @@ public async Task InvokeMethodGrpcAsync_AppCallback_UnexpectedMethod() // Configure Client var httpClient = new AppCallbackClient(new DaprAppCallbackService()); var daprClient = new DaprClientBuilder() - .UseGrpcChannelOptions(new GrpcChannelOptions(){ HttpClient = httpClient, }) + .UseGrpcChannelOptions(new GrpcChannelOptions() { HttpClient = httpClient, }) .UseJsonSerializationOptions(this.jsonSerializerOptions) .Build(); @@ -354,6 +354,106 @@ public async Task InvokeMethodGrpcAsync_AppCallback_UnexpectedMethod() response.Name.Should().Be("unexpected"); } + + [Fact] + public async Task GetMetadataAsync_WrapsRpcException() + { + var client = new MockClient(); + + const string rpcExceptionMessage = "RPC exception"; + const StatusCode rpcStatusCode = StatusCode.Unavailable; + const string rpcStatusDetail = "Non success"; + + var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); + var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); + + client.Mock + .Setup(m => m.GetMetadataAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.GetMetadataAsync(default); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task GetMetadataAsync_WithReturnTypeAndData() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetMetadataAsync(default); + }); + + + // Create Response & Respond + var response = new Autogen.Grpc.v1.GetMetadataResponse() + { + Id = "testId", + }; + response.ActiveActorsCount.Add(new ActiveActorsCount { Type = "testType", Count = 1 }); + response.RegisteredComponents.Add(new RegisteredComponents { Name = "testName", Type = "testType", Version = "V1" }); + response.ExtendedMetadata.Add("e1", "v1"); + + // Validate Response + var metadata = await request.CompleteWithMessageAsync(response); + metadata.Id.Should().Be("testId"); + metadata.Extended.Should().Contain(new System.Collections.Generic.KeyValuePair("e1", "v1")); + metadata.Actors.Should().Contain(actors => actors.Count == 1 && actors.Type == "testType"); + metadata.Components.Should().Contain(components => components.Name == "testName" && components.Type == "testType" && components.Version == "V1" && components.Capabilities.Length == 0); + } + + [Fact] + public async Task SetMetadataAsync_WrapsRpcException() + { + var client = new MockClient(); + + const string rpcExceptionMessage = "RPC exception"; + const StatusCode rpcStatusCode = StatusCode.Unavailable; + const string rpcStatusDetail = "Non success"; + + var rpcStatus = new Status(rpcStatusCode, rpcStatusDetail); + var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); + + client.Mock + .Setup(m => m.SetMetadataAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.SetMetadataAsync("testName", "", default); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task SetMetadataAsync_WithReturnTypeAndData() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = await client.CaptureGrpcRequestAsync(daprClient => + { + return daprClient.SetMetadataAsync("test", "testv", default); + }); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.Key.Should().Be("test"); + envelope.Value.Should().Be("testv"); + + await request.CompleteWithMessageAsync(new Empty()); + + } + // Test implementation of the AppCallback.AppCallbackBase service private class DaprAppCallbackService : AppCallback.Autogen.Grpc.v1.AppCallback.AppCallbackBase {