Skip to content

Commit 8163190

Browse files
feat: Add custom certPath support (#1405)
Signed-off-by: Marko Mlakar <marko.mlakar@dynatrace.com> Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
1 parent ba7ebe5 commit 8163190

File tree

9 files changed

+63
-17
lines changed

9 files changed

+63
-17
lines changed

libs/providers/flagd/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Options can be defined in the constructor or as environment variables. Construct
3333
| port | FLAGD_PORT | number | [resolver specific defaults](#resolver-type-specific-defaults) | |
3434
| tls | FLAGD_TLS | boolean | false | |
3535
| socketPath | FLAGD_SOCKET_PATH | string | - | |
36+
| certPath | FLAGD_SERVER_CERT_PATH | string | - | |
3637
| resolverType | FLAGD_RESOLVER | string | rpc | rpc, in-process |
3738
| offlineFlagSourcePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | string | - | |
3839
| selector | FLAGD_SOURCE_SELECTOR | string | - | |

libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { FlagdContainer } from '../tests/flagdContainer';
44
import type { State, Steps } from './state';
55
import { FlagdProvider } from '../../lib/flagd-provider';
66
import type { FlagdProviderOptions } from '../../lib/configuration';
7+
import { getGherkinTestPath } from '@openfeature/flagd-core';
8+
import { resolve } from 'node:path';
9+
import { existsSync } from 'node:fs';
710

811
export const providerSteps: Steps =
912
(state: State) =>
@@ -43,11 +46,20 @@ export const providerSteps: Steps =
4346
case 'unavailable':
4447
flagdOptions['port'] = 9999;
4548
break;
46-
case 'ssl':
47-
// TODO: modify this to support ssl
49+
case 'ssl': {
4850
flagdOptions['port'] = container.getPort(state.resolverType);
51+
flagdOptions['tls'] = true;
52+
const certPath = resolve(getGherkinTestPath('custom-root-cert.crt', 'test-harness/ssl/'));
53+
flagdOptions['certPath'] = certPath;
54+
if (!existsSync(certPath)) {
55+
throw new Error('Certificate file not found at path: ' + certPath);
56+
}
57+
if (state?.config?.selector) {
58+
flagdOptions['selector'] = state.config.selector;
59+
}
4960
type = 'ssl';
5061
break;
62+
}
5163
case 'stable':
5264
flagdOptions['port'] = container.getPort(state.resolverType);
5365
break;

libs/providers/flagd/src/e2e/tests/in-process.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe('in-process', () => {
2222
// remove filters as we add support for features
2323
// see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues
2424
tagFilter:
25-
'@in-process and not @targetURI and not @forbidden and not @customCert and not @events and not @sync and not @grace and not @metadata and not @unixsocket',
25+
'@in-process and not @targetURI and not @forbidden and not @events and not @sync and not @grace and not @metadata and not @unixsocket',
2626
scenarioNameTemplate: (vars) => {
2727
return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`;
2828
},

libs/providers/flagd/src/e2e/tests/rpc.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('rpc', () => {
2323
tagFilter:
2424
// remove filters as we add support for features
2525
// see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues
26-
'@rpc and not @targetURI and not @customCert and not @forbidden and not @events and not @stream and not @grace and not @metadata and not @caching and not @unixsocket',
26+
'@rpc and not @targetURI and not @forbidden and not @events and not @stream and not @grace and not @metadata and not @caching and not @unixsocket',
2727
scenarioNameTemplate: (vars) => {
2828
return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`;
2929
},

libs/providers/flagd/src/lib/configuration.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('Configuration', () => {
3434
const port = 8080;
3535
const tls = true;
3636
const socketPath = '/tmp/flagd.socks';
37+
const certPath = '/etc/cert/ca.crt';
3738
const maxCacheSize = 333;
3839
const cache = 'disabled';
3940
const resolverType = 'in-process';
@@ -45,6 +46,7 @@ describe('Configuration', () => {
4546
process.env['FLAGD_PORT'] = `${port}`;
4647
process.env['FLAGD_TLS'] = `${tls}`;
4748
process.env['FLAGD_SOCKET_PATH'] = socketPath;
49+
process.env['FLAGD_SERVER_CERT_PATH'] = certPath;
4850
process.env['FLAGD_CACHE'] = cache;
4951
process.env['FLAGD_MAX_CACHE_SIZE'] = `${maxCacheSize}`;
5052
process.env['FLAGD_SOURCE_SELECTOR'] = `${selector}`;
@@ -58,6 +60,7 @@ describe('Configuration', () => {
5860
port,
5961
tls,
6062
socketPath,
63+
certPath,
6164
maxCacheSize,
6265
cache,
6366
resolverType,
@@ -103,6 +106,7 @@ describe('Configuration', () => {
103106
host: 'test',
104107
port: 3000,
105108
tls: true,
109+
certPath: '/custom/cert.pem',
106110
maxCacheSize: 1000,
107111
cache: 'lru',
108112
resolverType: 'rpc',
@@ -116,6 +120,7 @@ describe('Configuration', () => {
116120
process.env['FLAGD_PORT'] = '8080';
117121
process.env['FLAGD_SYNC_PORT'] = '9090';
118122
process.env['FLAGD_TLS'] = 'false';
123+
process.env['FLAGD_SERVER_CERT_PATH'] = '/env/cert.pem';
119124
process.env['FLAGD_DEFAULT_AUTHORITY'] = 'test-authority-override';
120125

121126
expect(getConfig(options)).toStrictEqual(options);

libs/providers/flagd/src/lib/configuration.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ export interface Config {
4040
*/
4141
socketPath?: string;
4242

43+
/**
44+
* TLS certificate path to use when TLS connectivity is enabled.
45+
*
46+
* @example "/etc/cert/ca.crt"
47+
*/
48+
certPath?: string;
49+
4350
/**
4451
* Resolver type to use by the provider.
4552
*
@@ -120,6 +127,7 @@ enum ENV_VAR {
120127
FLAGD_DEADLINE_MS = 'FLAGD_DEADLINE_MS',
121128
FLAGD_TLS = 'FLAGD_TLS',
122129
FLAGD_SOCKET_PATH = 'FLAGD_SOCKET_PATH',
130+
FLAGD_SERVER_CERT_PATH = 'FLAGD_SERVER_CERT_PATH',
123131
FLAGD_CACHE = 'FLAGD_CACHE',
124132
FLAGD_MAX_CACHE_SIZE = 'FLAGD_MAX_CACHE_SIZE',
125133
FLAGD_SOURCE_SELECTOR = 'FLAGD_SOURCE_SELECTOR',
@@ -165,6 +173,9 @@ const getEnvVarConfig = (): Partial<Config> => {
165173
...(process.env[ENV_VAR.FLAGD_SOCKET_PATH] && {
166174
socketPath: process.env[ENV_VAR.FLAGD_SOCKET_PATH],
167175
}),
176+
...(process.env[ENV_VAR.FLAGD_SERVER_CERT_PATH] && {
177+
certPath: process.env[ENV_VAR.FLAGD_SERVER_CERT_PATH],
178+
}),
168179
...((process.env[ENV_VAR.FLAGD_CACHE] === 'lru' || process.env[ENV_VAR.FLAGD_CACHE] === 'disabled') && {
169180
cache: process.env[ENV_VAR.FLAGD_CACHE],
170181
}),

libs/providers/flagd/src/lib/service/common/grpc-util.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { ClientReadableStream } from '@grpc/grpc-js';
1+
import { credentials } from '@grpc/grpc-js';
2+
import type { ClientReadableStream, ChannelCredentials } from '@grpc/grpc-js';
3+
import { readFileSync, existsSync } from 'node:fs';
24

35
export const closeStreamIfDefined = (stream: ClientReadableStream<unknown> | undefined) => {
46
/**
@@ -14,3 +16,18 @@ export const closeStreamIfDefined = (stream: ClientReadableStream<unknown> | und
1416
stream.destroy();
1517
}
1618
};
19+
20+
/**
21+
* Creates gRPC channel credentials based on TLS and certificate path configuration.
22+
* @returns Channel credentials for gRPC connection
23+
*/
24+
export const createChannelCredentials = (tls: boolean, certPath?: string): ChannelCredentials => {
25+
if (!tls) {
26+
return credentials.createInsecure();
27+
}
28+
if (certPath && existsSync(certPath)) {
29+
const rootCerts = readFileSync(certPath);
30+
return credentials.createSsl(rootCerts);
31+
}
32+
return credentials.createSsl();
33+
};

libs/providers/flagd/src/lib/service/grpc/grpc-service.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ClientOptions, ClientReadableStream, ClientUnaryCall, ServiceError } from '@grpc/grpc-js';
2-
import { credentials, status } from '@grpc/grpc-js';
2+
import { status } from '@grpc/grpc-js';
33
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
44
import type { EvaluationContext, FlagValue, JsonValue, Logger, ResolutionDetails } from '@openfeature/server-sdk';
55
import {
@@ -11,6 +11,7 @@ import {
1111
} from '@openfeature/server-sdk';
1212
import { LRUCache } from 'lru-cache';
1313
import { promisify } from 'node:util';
14+
1415
import type {
1516
EventStreamResponse,
1617
ResolveBooleanRequest,
@@ -29,7 +30,7 @@ import type { Config } from '../../configuration';
2930
import { DEFAULT_MAX_CACHE_SIZE, EVENT_CONFIGURATION_CHANGE, EVENT_PROVIDER_READY } from '../../constants';
3031
import { FlagdProvider } from '../../flagd-provider';
3132
import type { Service } from '../service';
32-
import { closeStreamIfDefined } from '../common';
33+
import { closeStreamIfDefined, createChannelCredentials } from '../common';
3334

3435
type AnyResponse =
3536
| ResolveBooleanResponse
@@ -79,21 +80,19 @@ export class GRPCService implements Service {
7980
client?: ServiceClient,
8081
private logger?: Logger,
8182
) {
82-
const { host, port, tls, socketPath, defaultAuthority } = config;
83+
const { host, port, tls, socketPath, certPath, defaultAuthority } = config;
8384
let clientOptions: ClientOptions | undefined;
8485
if (defaultAuthority) {
8586
clientOptions = {
8687
'grpc.default_authority': defaultAuthority,
8788
};
8889
}
8990

91+
const channelCredentials = createChannelCredentials(tls, certPath);
92+
9093
this._client = client
9194
? client
92-
: new ServiceClient(
93-
socketPath ? `unix://${socketPath}` : `${host}:${port}`,
94-
tls ? credentials.createSsl() : credentials.createInsecure(),
95-
clientOptions,
96-
);
95+
: new ServiceClient(socketPath ? `unix://${socketPath}` : `${host}:${port}`, channelCredentials, clientOptions);
9796
this._deadline = config.deadlineMs;
9897

9998
if (config.cache === 'lru') {

libs/providers/flagd/src/lib/service/in-process/grpc/grpc-fetch.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import type { ClientReadableStream, ServiceError, ClientOptions } from '@grpc/grpc-js';
2-
import { credentials } from '@grpc/grpc-js';
32
import type { EvaluationContext, Logger } from '@openfeature/server-sdk';
43
import { GeneralError } from '@openfeature/server-sdk';
54
import type { SyncFlagsRequest, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync';
65
import { FlagSyncServiceClient } from '../../../../proto/ts/flagd/sync/v1/sync';
76
import type { Config } from '../../../configuration';
8-
import { closeStreamIfDefined } from '../../common';
7+
import { closeStreamIfDefined, createChannelCredentials } from '../../common';
98
import type { DataFetch } from '../data-fetch';
109

1110
/**
@@ -36,19 +35,21 @@ export class GrpcFetch implements DataFetch {
3635
syncServiceClient?: FlagSyncServiceClient,
3736
logger?: Logger,
3837
) {
39-
const { host, port, tls, socketPath, selector, defaultAuthority } = config;
38+
const { host, port, tls, socketPath, certPath, selector, defaultAuthority } = config;
4039
let clientOptions: ClientOptions | undefined;
4140
if (defaultAuthority) {
4241
clientOptions = {
4342
'grpc.default_authority': defaultAuthority,
4443
};
4544
}
4645

46+
const channelCredentials = createChannelCredentials(tls, certPath);
47+
4748
this._syncClient = syncServiceClient
4849
? syncServiceClient
4950
: new FlagSyncServiceClient(
5051
socketPath ? `unix://${socketPath}` : `${host}:${port}`,
51-
tls ? credentials.createSsl() : credentials.createInsecure(),
52+
channelCredentials,
5253
clientOptions,
5354
);
5455

0 commit comments

Comments
 (0)