Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for the DAPR_HTTP_ENDPOINT and DAPR_GRPC_ENDPOINT environment variables. Adds support for DAPR_API_TOKEN to gRPC client #519

Merged
merged 23 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
aaf074c
feat: Adds endpoint parsing
elena-kolevska Sep 6, 2023
49ab722
Adds support for the DAPR_HTTP_ENDPOINT environment variable
elena-kolevska Sep 12, 2023
0aaa913
Adds tests for endpoint environment variables (HTTP only)
elena-kolevska Sep 12, 2023
6fd90c5
test: Adds tests for endpoint environment variables (HTTP only)
elena-kolevska Sep 12, 2023
8f26003
Merge branch 'tls-support' of github.com:elena-kolevska/js-sdk into t…
elena-kolevska Sep 12, 2023
a29bc66
fix(style) Linter fixes
elena-kolevska Sep 13, 2023
6d175a2
Adds support for dapr-api-token metadata
elena-kolevska Sep 18, 2023
d009132
Adds support for the DAPR_GRPC_ENDPOINT environment variable
elena-kolevska Sep 18, 2023
e63f270
Fixes linter issues
elena-kolevska Sep 18, 2023
0e5672d
Fixes linter issues
elena-kolevska Sep 18, 2023
dc482b9
Only add api token interceptor if it’s specified
elena-kolevska Sep 18, 2023
e0fd4a3
Reorganises the code a bit
elena-kolevska Sep 18, 2023
08407db
Runs pretty-fix
elena-kolevska Sep 18, 2023
6e16458
Adds test for scheme prefix removal for grpc
elena-kolevska Sep 19, 2023
2f1a277
Apply suggestions from code review
elena-kolevska Sep 20, 2023
959ea7b
Apply suggestions from code review
elena-kolevska Sep 20, 2023
afb2eaf
Adds examples for the parseEndpoint function
elena-kolevska Sep 20, 2023
1d731d1
Adds tests for the dapr-api-token metadata in gRPC calls
elena-kolevska Sep 24, 2023
342a5a8
Updates after review
elena-kolevska Sep 25, 2023
9226112
docs: Adds info and examples about the new environment variables to t…
elena-kolevska Sep 25, 2023
605c587
Addresses review comments
elena-kolevska Sep 26, 2023
94f271e
Small formatting fix
elena-kolevska Sep 26, 2023
3b27c23
Fixes docs
elena-kolevska Sep 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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({ daprHost, daprPort });

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we showing here that this is the equivalent of setting the environment variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is how we can instantiate the client when we have DAPR_GRPC_ENDPOINT set. I noticed I had a copy/paste error for the HTTP example above though, so I fixed that. Basically, we don't need to pass constructor arguments for host and port when we have the env variables set.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if DAPR_GRPC_ENDPOINT is set, should we also by-default use communication protocol GRPC. Also while instantiating the DaprClient, should we clearly log if we used env var to figure out host/port/protocol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I can add a log.
Regarding the automatic setting of the protocol, I think we can't do it, because, as per the proposal we can have both DAPR_GRPC_ENDPOINT and DAPR_HTTP_ENDPOINT set at the same time.

DAPR_GRPC_ENDPOINT and DAPR_HTTP_ENDPOINT can be set at the same time since some SDKs (Java, as of now) supports both protocols at the same time and app can pick which one to use.

Copy link
Member

@shubham1172 shubham1172 Sep 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right (and maybe this can be added to the proposal to unify SDK implementations), so if

  1. DAPR_GRPC_ENDPOINT is set, set protocol to gRPC
  2. DAPR_HTTP_ENDPOINT is set, set protocol to HTTP
  3. Both are set, use HTTP
  4. Finally, if function param is specified, that overrides everything
  5. No function param or env var also means HTTP

PS, we can track this separately as well.

```

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
elena-kolevska marked this conversation as resolved.
Show resolved Hide resolved
* 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 {
elena-kolevska marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading