Skip to content

Commit 7464735

Browse files
feat: Support for the DAPR_HTTP_ENDPOINT and DAPR_GRPC_ENDPOINT environment variables. Adds support for DAPR_API_TOKEN to gRPC client (#519)
* feat: Adds endpoint parsing Signed-off-by: Elena Kolevska <[email protected]> * Adds support for the DAPR_HTTP_ENDPOINT environment variable Signed-off-by: Elena Kolevska <[email protected]> * Adds tests for endpoint environment variables (HTTP only) Signed-off-by: Elena Kolevska <[email protected]> * test: Adds tests for endpoint environment variables (HTTP only) Signed-off-by: Elena Kolevska <[email protected]> * fix(style) Linter fixes Signed-off-by: Elena Kolevska <[email protected]> * Adds support for dapr-api-token metadata Signed-off-by: Elena Kolevska <[email protected]> * Adds support for the DAPR_GRPC_ENDPOINT environment variable Signed-off-by: Elena Kolevska <[email protected]> * Fixes linter issues Signed-off-by: Elena Kolevska <[email protected]> * Fixes linter issues Signed-off-by: Elena Kolevska <[email protected]> * Only add api token interceptor if it’s specified Signed-off-by: Elena Kolevska <[email protected]> * Reorganises the code a bit Signed-off-by: Elena Kolevska <[email protected]> * Runs pretty-fix Signed-off-by: Elena Kolevska <[email protected]> * Adds test for scheme prefix removal for grpc Signed-off-by: Elena Kolevska <[email protected]> * Apply suggestions from code review Co-authored-by: Shubham Sharma <[email protected]> Signed-off-by: Elena Kolevska <[email protected]> * Apply suggestions from code review Co-authored-by: Shubham Sharma <[email protected]> Signed-off-by: Elena Kolevska <[email protected]> * Adds examples for the parseEndpoint function Signed-off-by: Elena Kolevska <[email protected]> * Adds tests for the dapr-api-token metadata in gRPC calls Signed-off-by: Elena Kolevska <[email protected]> * Updates after review Signed-off-by: Elena Kolevska <[email protected]> * docs: Adds info and examples about the new environment variables to the docs Signed-off-by: Elena Kolevska <[email protected]> * Addresses review comments Signed-off-by: Elena Kolevska <[email protected]> * Small formatting fix Signed-off-by: Elena Kolevska <[email protected]> * Fixes docs Signed-off-by: Elena Kolevska <[email protected]> --------- Signed-off-by: Elena Kolevska <[email protected]> Signed-off-by: Elena Kolevska <[email protected]> Co-authored-by: Shubham Sharma <[email protected]>
1 parent df7eff2 commit 7464735

File tree

11 files changed

+769
-28
lines changed

11 files changed

+769
-28
lines changed

.gitignore

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
### vscode ###
2-
.vscode/*
2+
.vscode
33
!.vscode/settings.json
44
!.vscode/tasks.json
55
!.vscode/launch.json
@@ -144,4 +144,11 @@ temp/
144144
build/
145145

146146
# version file is auto-generated
147-
src/version.ts
147+
src/version.ts
148+
149+
# OSX
150+
/.DS_Store
151+
152+
# JetBrains
153+
/.idea
154+

daprdocs/content/en/js-sdk-docs/js-client/_index.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,22 @@ dapr run --app-id example-sdk --app-protocol grpc -- npm run start
7777
npm run start:dapr-grpc
7878
```
7979

80+
### Environment Variables
81+
82+
You can use the `DAPR_HTTP_ENDPOINT` and `DAPR_GRPC_ENDPOINT` environment variables to set the Dapr Sidecar's HTTP and gRPC endpoints respectively. When these variables are set, the `daprHost` and `daprPort` don't have to be passed to the constructor, the client will parse them automatically out of the provided endpoints.
83+
84+
```typescript
85+
import { DaprClient, CommunicationProtocol } from "@dapr/dapr";
86+
87+
// Using HTTP, when DAPR_HTTP_ENDPOINT is set
88+
const client = new DaprClient();
89+
90+
// Using gRPC, when DAPR_GRPC_ENDPOINT is set
91+
const client = new DaprClient({ communicationProtocol: CommunicationProtocol.GRPC });
92+
```
93+
94+
If the environment variables are set, but `daprHost` and `daprPort` values are passed to the constructor, the latter will take precedence over the environment variables.
95+
8096
## General
8197

8298
### Increasing Body Size

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
"test:load:http": "TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --components-path ./test/components -- npm run test:load 'test/load'",
1010
"test:e2e": "jest --runInBand --detectOpenHandles",
1111
"test:e2e:all": "npm run test:e2e:http; npm run test:e2e:grpc; npm run test:e2e:common",
12-
"test:e2e:grpc": "npm run test:e2e:grpc:client && npm run test:e2e:grpc:server",
13-
"test:e2e:grpc:client": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/(client).test.ts' ]",
14-
"test:e2e:grpc:server": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --dapr-http-max-request-size 10 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/(server).test.ts' ]",
12+
"test:e2e:grpc": "npm run test:e2e:grpc:client && npm run test:e2e:grpc:server && npm run test:e2e:grpc:clientWithApiToken",
13+
"test:e2e:grpc:client": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/*client.test.ts' ]",
14+
"test:e2e:grpc:clientWithApiToken": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 DAPR_API_TOKEN=test dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/clientWithApiToken.test.ts' ]",
15+
"test:e2e:grpc:server": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --dapr-http-max-request-size 10 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/*server.test.ts' ]",
1516
"test:e2e:http": "npm run test:e2e:http:client && npm run test:e2e:http:server && npm run test:e2e:http:actors",
1617
"test:e2e:http:client": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/http/(client).test.ts' ]",
1718
"test:e2e:http:server": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --dapr-http-max-request-size 10 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/http/(server).test.ts' ]",

src/implementation/Client/GRPCClient/GRPCClient.ts

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,18 @@ export default class GRPCClient implements IClient {
2828
private readonly client: GrpcDaprClient;
2929
private readonly clientCredentials: grpc.ChannelCredentials;
3030
private readonly logger: Logger;
31+
private readonly grpcClientOptions: Partial<grpc.ClientOptions>;
3132

3233
constructor(options: DaprClientOptions) {
3334
this.options = options;
3435
this.clientCredentials = this.generateCredentials();
36+
this.grpcClientOptions = this.generateChannelOptions();
37+
3538
this.logger = new Logger("GRPCClient", "GRPCClient", options.logger);
3639
this.isInitialized = false;
3740

3841
this.logger.info(`Opening connection to ${this.options.daprHost}:${this.options.daprPort}`);
39-
this.client = this.generateClient(this.options.daprHost, this.options.daprPort, this.clientCredentials);
42+
this.client = this.generateClient(this.options.daprHost, this.options.daprPort);
4043
}
4144

4245
async getClient(requiresInitialization = true): Promise<GrpcDaprClient> {
@@ -52,8 +55,20 @@ export default class GRPCClient implements IClient {
5255
return this.clientCredentials;
5356
}
5457

55-
private generateChannelOptions(): Record<string, string | number> {
56-
const options: Record<string, string | number> = {};
58+
getGrpcClientOptions(): grpc.ClientOptions {
59+
return this.grpcClientOptions;
60+
}
61+
62+
private generateCredentials(): grpc.ChannelCredentials {
63+
if (this.options.daprHost.startsWith("https")) {
64+
return grpc.ChannelCredentials.createSsl();
65+
}
66+
return grpc.ChannelCredentials.createInsecure();
67+
}
68+
69+
private generateChannelOptions(): Partial<grpc.ClientOptions> {
70+
// const options: Record<string, string | number> = {};
71+
let options: Partial<grpc.ClientOptions> = {};
5772

5873
// See: GRPC_ARG_MAX_SEND_MESSAGE_LENGTH, it is in bytes
5974
// https://grpc.github.io/grpc/core/group__grpc__arg__keys.html#ga813f94f9ac3174571dd712c96cdbbdc1
@@ -67,20 +82,48 @@ export default class GRPCClient implements IClient {
6782
// Add user agent
6883
options["grpc.primary_user_agent"] = "dapr-sdk-js/v" + SDK_VERSION;
6984

85+
// Add interceptors if we have an API token
86+
if (this.options.daprApiToken !== "") {
87+
options = {
88+
interceptors: [this.generateInterceptors()],
89+
...options,
90+
};
91+
}
92+
7093
return options;
7194
}
7295

73-
private generateClient(host: string, port: string, credentials: grpc.ChannelCredentials): GrpcDaprClient {
74-
const options = this.generateChannelOptions();
75-
const client = new GrpcDaprClient(`${host}:${port}`, credentials, options);
96+
private generateClient(host: string, port: string): GrpcDaprClient {
97+
return new GrpcDaprClient(
98+
GRPCClient.getEndpoint(host, port),
99+
this.getClientCredentials(),
100+
this.getGrpcClientOptions(),
101+
);
102+
}
76103

77-
return client;
104+
// The grpc client doesn't allow http:// or https:// for grpc connections,
105+
// so we need to remove it, if it exists
106+
static getEndpoint(host: string, port: string): string {
107+
let endpoint = `${host}:${port}`;
108+
const parts = endpoint.split("://");
109+
if (parts.length > 1 && parts[0].startsWith("http")) {
110+
endpoint = parts[1];
111+
}
112+
113+
return endpoint;
78114
}
79115

80-
// @todo: look into making secure credentials
81-
private generateCredentials(): grpc.ChannelCredentials {
82-
const credsChannel = grpc.ChannelCredentials.createInsecure();
83-
return credsChannel;
116+
private generateInterceptors(): (options: any, nextCall: any) => grpc.InterceptingCall {
117+
return (options: any, nextCall: any) => {
118+
return new grpc.InterceptingCall(nextCall(options), {
119+
start: (metadata, listener, next) => {
120+
if (metadata.get("dapr-api-token").length == 0) {
121+
metadata.add("dapr-api-token", this.options.daprApiToken as grpc.MetadataValue);
122+
}
123+
next(metadata, listener);
124+
},
125+
});
126+
};
84127
}
85128

86129
setIsInitialized(isInitialized: boolean): void {

src/implementation/Client/GRPCClient/GRPCClientProxy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ export class GRPCClientProxy<T> {
6060
this.grpcClientOptions.interceptors = [];
6161
}
6262

63-
this.grpcClientOptions.interceptors = [...this.generateInterceptors(), ...this.grpcClientOptions.interceptors];
63+
this.grpcClientOptions.interceptors = [
64+
...this.generateInterceptors(),
65+
...(this.grpcClient.getGrpcClientOptions().interceptors ?? []),
66+
...this.grpcClientOptions.interceptors,
67+
];
6468

6569
const clientCustom = new this.clsProxy(
6670
`${this.grpcClient.options.daprHost}:${this.grpcClient.options.daprPort}`,

src/utils/Client.util.ts

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { Settings } from "./Settings.util";
3030
import { LoggerOptions } from "../types/logger/LoggerOptions";
3131
import { StateConsistencyEnum } from "../enum/StateConsistency.enum";
3232
import { StateConcurrencyEnum } from "../enum/StateConcurrency.enum";
33-
import { URLSearchParams } from "url";
33+
import { URL, URLSearchParams } from "url";
3434
/**
3535
* Adds metadata to a map.
3636
* @param map Input map
@@ -253,24 +253,91 @@ function getType(o: any) {
253253
/**
254254
* Prepares DaprClientOptions for use by the DaprClient/DaprServer.
255255
* If the user does not provide a value for a mandatory option, the default value is used.
256-
* @param clientoptions DaprClientOptions
256+
* @param clientOptions DaprClientOptions
257257
* @param defaultCommunicationProtocol CommunicationProtocolEnum
258+
* @param defaultLoggerOptions
258259
* @returns DaprClientOptions
259260
*/
260261
export function getClientOptions(
261-
clientoptions: Partial<DaprClientOptions> | undefined,
262+
clientOptions: Partial<DaprClientOptions> | undefined,
262263
defaultCommunicationProtocol: CommunicationProtocolEnum,
263264
defaultLoggerOptions: LoggerOptions | undefined,
264265
): DaprClientOptions {
265-
const clientCommunicationProtocol = clientoptions?.communicationProtocol ?? defaultCommunicationProtocol;
266+
const clientCommunicationProtocol = clientOptions?.communicationProtocol ?? defaultCommunicationProtocol;
267+
268+
// We decide the host/port/endpoint here
269+
let daprEndpoint = "";
270+
if (clientCommunicationProtocol == CommunicationProtocolEnum.HTTP) {
271+
daprEndpoint = Settings.getDefaultHttpEndpoint();
272+
} else if (clientCommunicationProtocol == CommunicationProtocolEnum.GRPC) {
273+
daprEndpoint = Settings.getDefaultGrpcEndpoint();
274+
}
275+
276+
let host = Settings.getDefaultHost();
277+
let port = Settings.getDefaultPort(clientCommunicationProtocol);
278+
279+
if (clientOptions?.daprHost || clientOptions?.daprPort) {
280+
host = clientOptions?.daprHost ?? host;
281+
port = clientOptions?.daprPort ?? port;
282+
} else if (daprEndpoint != "") {
283+
const [scheme, fqdn, p] = parseEndpoint(daprEndpoint);
284+
host = `${scheme}://${fqdn}`;
285+
port = p;
286+
}
287+
266288
return {
267-
daprHost: clientoptions?.daprHost ?? Settings.getDefaultHost(),
268-
daprPort: clientoptions?.daprPort ?? Settings.getDefaultPort(clientCommunicationProtocol),
289+
daprHost: host,
290+
daprPort: port,
269291
communicationProtocol: clientCommunicationProtocol,
270-
isKeepAlive: clientoptions?.isKeepAlive,
271-
logger: clientoptions?.logger ?? defaultLoggerOptions,
272-
actor: clientoptions?.actor,
273-
daprApiToken: clientoptions?.daprApiToken,
274-
maxBodySizeMb: clientoptions?.maxBodySizeMb,
292+
isKeepAlive: clientOptions?.isKeepAlive,
293+
logger: clientOptions?.logger ?? defaultLoggerOptions,
294+
actor: clientOptions?.actor,
295+
daprApiToken: clientOptions?.daprApiToken,
296+
maxBodySizeMb: clientOptions?.maxBodySizeMb,
275297
};
276298
}
299+
300+
/**
301+
* Scheme, fqdn and port
302+
*/
303+
type EndpointTuple = [string, string, string];
304+
305+
/**
306+
* Parses an endpoint to scheme, fqdn and port
307+
* Examples:
308+
* - http://localhost:3500 -> [http, localhost, 3500]
309+
* - localhost:3500 -> [http, localhost, 3500]
310+
* - :3500 -> [http, localhost, 3500]
311+
* - localhost -> [http, localhost, 80]
312+
* - https://localhost:3500 -> [https, localhost, 3500]
313+
* - [::1]:3500 -> [http, ::1, 3500]
314+
* - [::1] -> [http, ::1, 80]
315+
* - http://[2001:db8:1f70:0:999:de8:7648:6e8]:5000 -> [http, 2001:db8:1f70:0:999:de8:7648:6e8, 5000]
316+
* @throws Error if the address is invalid
317+
* @param address Endpoint address
318+
* @returns EndpointTuple (scheme, fqdn, port)
319+
*/
320+
export function parseEndpoint(address: string): EndpointTuple {
321+
// Prefix with a scheme and host when they're not present,
322+
// because the URL library won't parse it otherwise
323+
if (address.startsWith(":")) {
324+
address = "http://localhost" + address;
325+
}
326+
if (!address.includes("://")) {
327+
address = "http://" + address;
328+
}
329+
330+
let scheme, fqdn, port: string;
331+
332+
try {
333+
const myURL = new URL(address);
334+
scheme = myURL.protocol.replace(":", "");
335+
fqdn = myURL.hostname.replace("[", "");
336+
fqdn = fqdn.replace("]", "");
337+
port = myURL.port || (myURL.protocol == "https:" ? "443" : "80");
338+
} catch (error) {
339+
throw new Error(`Invalid address: ${address}`);
340+
}
341+
342+
return [scheme, fqdn, port];
343+
}

src/utils/Settings.util.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export class Settings {
2020
private static readonly defaultHttpPort: string = "3500";
2121
private static readonly defaultGrpcAppPort: string = "50000";
2222
private static readonly defaultGrpcPort: string = "50001";
23+
private static readonly defaultHttpEndpoint: string = "";
24+
private static readonly defaultGrpcEndpoint: string = "";
2325
private static readonly defaultCommunicationProtocol: CommunicationProtocolEnum = CommunicationProtocolEnum.HTTP;
2426
private static readonly defaultKeepAlive: boolean = true;
2527
private static readonly defaultStateGetBulkParallelism: number = 10;
@@ -85,6 +87,14 @@ export class Settings {
8587
return process.env.APP_PORT ?? Settings.defaultGrpcAppPort;
8688
}
8789

90+
static getDefaultHttpEndpoint(): string {
91+
return process.env.DAPR_HTTP_ENDPOINT || Settings.defaultHttpEndpoint;
92+
}
93+
94+
static getDefaultGrpcEndpoint(): string {
95+
return process.env.DAPR_GRPC_ENDPOINT || Settings.defaultGrpcEndpoint;
96+
}
97+
8898
/**
8999
* Gets the default port that the application is listening on.
90100
* @param communicationProtocolEnum communication protocol

0 commit comments

Comments
 (0)