Skip to content

Commit

Permalink
feat: Support for the DAPR_HTTP_ENDPOINT and DAPR_GRPC_ENDPOINT e…
Browse files Browse the repository at this point in the history
…nvironment 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]>
  • Loading branch information
elena-kolevska and shubham1172 authored Sep 27, 2023
1 parent df7eff2 commit 7464735
Show file tree
Hide file tree
Showing 11 changed files with 769 additions and 28 deletions.
11 changes: 9 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
### vscode ###
.vscode/*
.vscode
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
Expand Down Expand Up @@ -144,4 +144,11 @@ temp/
build/

# version file is auto-generated
src/version.ts
src/version.ts

# OSX
/.DS_Store

# JetBrains
/.idea

16 changes: 16 additions & 0 deletions daprdocs/content/en/js-sdk-docs/js-client/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ dapr run --app-id example-sdk --app-protocol grpc -- npm run start
npm run start:dapr-grpc
```

### Environment Variables

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.

```typescript
import { DaprClient, CommunicationProtocol } from "@dapr/dapr";

// Using HTTP, when DAPR_HTTP_ENDPOINT is set
const client = new DaprClient();

// Using gRPC, when DAPR_GRPC_ENDPOINT is set
const client = new DaprClient({ communicationProtocol: CommunicationProtocol.GRPC });
```

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.

## General

### Increasing Body Size
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
"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'",
"test:e2e": "jest --runInBand --detectOpenHandles",
"test:e2e:all": "npm run test:e2e:http; npm run test:e2e:grpc; npm run test:e2e:common",
"test:e2e:grpc": "npm run test:e2e:grpc:client && npm run test:e2e:grpc:server",
"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' ]",
"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' ]",
"test:e2e:grpc": "npm run test:e2e:grpc:client && npm run test:e2e:grpc:server && npm run test:e2e:grpc:clientWithApiToken",
"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' ]",
"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' ]",
"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' ]",
"test:e2e:http": "npm run test:e2e:http:client && npm run test:e2e:http:server && npm run test:e2e:http:actors",
"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' ]",
"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' ]",
Expand Down
65 changes: 54 additions & 11 deletions src/implementation/Client/GRPCClient/GRPCClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ export default class GRPCClient implements IClient {
private readonly client: GrpcDaprClient;
private readonly clientCredentials: grpc.ChannelCredentials;
private readonly logger: Logger;
private readonly grpcClientOptions: Partial<grpc.ClientOptions>;

constructor(options: DaprClientOptions) {
this.options = options;
this.clientCredentials = this.generateCredentials();
this.grpcClientOptions = this.generateChannelOptions();

this.logger = new Logger("GRPCClient", "GRPCClient", options.logger);
this.isInitialized = false;

this.logger.info(`Opening connection to ${this.options.daprHost}:${this.options.daprPort}`);
this.client = this.generateClient(this.options.daprHost, this.options.daprPort, this.clientCredentials);
this.client = this.generateClient(this.options.daprHost, this.options.daprPort);
}

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

private generateChannelOptions(): Record<string, string | number> {
const options: Record<string, string | number> = {};
getGrpcClientOptions(): grpc.ClientOptions {
return this.grpcClientOptions;
}

private generateCredentials(): grpc.ChannelCredentials {
if (this.options.daprHost.startsWith("https")) {
return grpc.ChannelCredentials.createSsl();
}
return grpc.ChannelCredentials.createInsecure();
}

private generateChannelOptions(): Partial<grpc.ClientOptions> {
// const options: Record<string, string | number> = {};
let options: Partial<grpc.ClientOptions> = {};

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

// Add interceptors if we have an API token
if (this.options.daprApiToken !== "") {
options = {
interceptors: [this.generateInterceptors()],
...options,
};
}

return options;
}

private generateClient(host: string, port: string, credentials: grpc.ChannelCredentials): GrpcDaprClient {
const options = this.generateChannelOptions();
const client = new GrpcDaprClient(`${host}:${port}`, credentials, options);
private generateClient(host: string, port: string): GrpcDaprClient {
return new GrpcDaprClient(
GRPCClient.getEndpoint(host, port),
this.getClientCredentials(),
this.getGrpcClientOptions(),
);
}

return client;
// The grpc client doesn't allow http:// or https:// for grpc connections,
// so we need to remove it, if it exists
static getEndpoint(host: string, port: string): string {
let endpoint = `${host}:${port}`;
const parts = endpoint.split("://");
if (parts.length > 1 && parts[0].startsWith("http")) {
endpoint = parts[1];
}

return endpoint;
}

// @todo: look into making secure credentials
private generateCredentials(): grpc.ChannelCredentials {
const credsChannel = grpc.ChannelCredentials.createInsecure();
return credsChannel;
private generateInterceptors(): (options: any, nextCall: any) => grpc.InterceptingCall {
return (options: any, nextCall: any) => {
return new grpc.InterceptingCall(nextCall(options), {
start: (metadata, listener, next) => {
if (metadata.get("dapr-api-token").length == 0) {
metadata.add("dapr-api-token", this.options.daprApiToken as grpc.MetadataValue);
}
next(metadata, listener);
},
});
};
}

setIsInitialized(isInitialized: boolean): void {
Expand Down
6 changes: 5 additions & 1 deletion src/implementation/Client/GRPCClient/GRPCClientProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ export class GRPCClientProxy<T> {
this.grpcClientOptions.interceptors = [];
}

this.grpcClientOptions.interceptors = [...this.generateInterceptors(), ...this.grpcClientOptions.interceptors];
this.grpcClientOptions.interceptors = [
...this.generateInterceptors(),
...(this.grpcClient.getGrpcClientOptions().interceptors ?? []),
...this.grpcClientOptions.interceptors,
];

const clientCustom = new this.clsProxy(
`${this.grpcClient.options.daprHost}:${this.grpcClient.options.daprPort}`,
Expand Down
89 changes: 78 additions & 11 deletions src/utils/Client.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { Settings } from "./Settings.util";
import { LoggerOptions } from "../types/logger/LoggerOptions";
import { StateConsistencyEnum } from "../enum/StateConsistency.enum";
import { StateConcurrencyEnum } from "../enum/StateConcurrency.enum";
import { URLSearchParams } from "url";
import { URL, URLSearchParams } from "url";
/**
* Adds metadata to a map.
* @param map Input map
Expand Down Expand Up @@ -253,24 +253,91 @@ function getType(o: any) {
/**
* Prepares DaprClientOptions for use by the DaprClient/DaprServer.
* If the user does not provide a value for a mandatory option, the default value is used.
* @param clientoptions DaprClientOptions
* @param clientOptions DaprClientOptions
* @param defaultCommunicationProtocol CommunicationProtocolEnum
* @param defaultLoggerOptions
* @returns DaprClientOptions
*/
export function getClientOptions(
clientoptions: Partial<DaprClientOptions> | undefined,
clientOptions: Partial<DaprClientOptions> | undefined,
defaultCommunicationProtocol: CommunicationProtocolEnum,
defaultLoggerOptions: LoggerOptions | undefined,
): DaprClientOptions {
const clientCommunicationProtocol = clientoptions?.communicationProtocol ?? defaultCommunicationProtocol;
const clientCommunicationProtocol = clientOptions?.communicationProtocol ?? defaultCommunicationProtocol;

// We decide the host/port/endpoint here
let daprEndpoint = "";
if (clientCommunicationProtocol == CommunicationProtocolEnum.HTTP) {
daprEndpoint = Settings.getDefaultHttpEndpoint();
} else if (clientCommunicationProtocol == CommunicationProtocolEnum.GRPC) {
daprEndpoint = Settings.getDefaultGrpcEndpoint();
}

let host = Settings.getDefaultHost();
let port = Settings.getDefaultPort(clientCommunicationProtocol);

if (clientOptions?.daprHost || clientOptions?.daprPort) {
host = clientOptions?.daprHost ?? host;
port = clientOptions?.daprPort ?? port;
} else if (daprEndpoint != "") {
const [scheme, fqdn, p] = parseEndpoint(daprEndpoint);
host = `${scheme}://${fqdn}`;
port = p;
}

return {
daprHost: clientoptions?.daprHost ?? Settings.getDefaultHost(),
daprPort: clientoptions?.daprPort ?? Settings.getDefaultPort(clientCommunicationProtocol),
daprHost: host,
daprPort: port,
communicationProtocol: clientCommunicationProtocol,
isKeepAlive: clientoptions?.isKeepAlive,
logger: clientoptions?.logger ?? defaultLoggerOptions,
actor: clientoptions?.actor,
daprApiToken: clientoptions?.daprApiToken,
maxBodySizeMb: clientoptions?.maxBodySizeMb,
isKeepAlive: clientOptions?.isKeepAlive,
logger: clientOptions?.logger ?? defaultLoggerOptions,
actor: clientOptions?.actor,
daprApiToken: clientOptions?.daprApiToken,
maxBodySizeMb: clientOptions?.maxBodySizeMb,
};
}

/**
* Scheme, fqdn and port
*/
type EndpointTuple = [string, string, string];

/**
* Parses an endpoint to scheme, fqdn and port
* Examples:
* - http://localhost:3500 -> [http, localhost, 3500]
* - localhost:3500 -> [http, localhost, 3500]
* - :3500 -> [http, localhost, 3500]
* - localhost -> [http, localhost, 80]
* - https://localhost:3500 -> [https, localhost, 3500]
* - [::1]:3500 -> [http, ::1, 3500]
* - [::1] -> [http, ::1, 80]
* - http://[2001:db8:1f70:0:999:de8:7648:6e8]:5000 -> [http, 2001:db8:1f70:0:999:de8:7648:6e8, 5000]
* @throws Error if the address is invalid
* @param address Endpoint address
* @returns EndpointTuple (scheme, fqdn, port)
*/
export function parseEndpoint(address: string): EndpointTuple {
// Prefix with a scheme and host when they're not present,
// because the URL library won't parse it otherwise
if (address.startsWith(":")) {
address = "http://localhost" + address;
}
if (!address.includes("://")) {
address = "http://" + address;
}

let scheme, fqdn, port: string;

try {
const myURL = new URL(address);
scheme = myURL.protocol.replace(":", "");
fqdn = myURL.hostname.replace("[", "");
fqdn = fqdn.replace("]", "");
port = myURL.port || (myURL.protocol == "https:" ? "443" : "80");
} catch (error) {
throw new Error(`Invalid address: ${address}`);
}

return [scheme, fqdn, port];
}
10 changes: 10 additions & 0 deletions src/utils/Settings.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export class Settings {
private static readonly defaultHttpPort: string = "3500";
private static readonly defaultGrpcAppPort: string = "50000";
private static readonly defaultGrpcPort: string = "50001";
private static readonly defaultHttpEndpoint: string = "";
private static readonly defaultGrpcEndpoint: string = "";
private static readonly defaultCommunicationProtocol: CommunicationProtocolEnum = CommunicationProtocolEnum.HTTP;
private static readonly defaultKeepAlive: boolean = true;
private static readonly defaultStateGetBulkParallelism: number = 10;
Expand Down Expand Up @@ -85,6 +87,14 @@ export class Settings {
return process.env.APP_PORT ?? Settings.defaultGrpcAppPort;
}

static getDefaultHttpEndpoint(): string {
return process.env.DAPR_HTTP_ENDPOINT || Settings.defaultHttpEndpoint;
}

static getDefaultGrpcEndpoint(): string {
return process.env.DAPR_GRPC_ENDPOINT || Settings.defaultGrpcEndpoint;
}

/**
* Gets the default port that the application is listening on.
* @param communicationProtocolEnum communication protocol
Expand Down
Loading

0 comments on commit 7464735

Please sign in to comment.