Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions libs/providers/flagd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Options can be defined in the constructor or as environment variables. Construct
| port | FLAGD_PORT | number | [resolver specific defaults](#resolver-type-specific-defaults) | |
| tls | FLAGD_TLS | boolean | false | |
| socketPath | FLAGD_SOCKET_PATH | string | - | |
| certPath | FLAGD_SERVER_CERT_PATH | string | - | |
| resolverType | FLAGD_RESOLVER | string | rpc | rpc, in-process |
| offlineFlagSourcePath | FLAGD_OFFLINE_FLAG_SOURCE_PATH | string | - | |
| selector | FLAGD_SOURCE_SELECTOR | string | - | |
Expand Down
16 changes: 14 additions & 2 deletions libs/providers/flagd/src/e2e/step-definitions/providerSteps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { FlagdContainer } from '../tests/flagdContainer';
import type { State, Steps } from './state';
import { FlagdProvider } from '../../lib/flagd-provider';
import type { FlagdProviderOptions } from '../../lib/configuration';
import { getGherkinTestPath } from '@openfeature/flagd-core';
import { resolve } from 'node:path';
import { existsSync } from 'node:fs';

export const providerSteps: Steps =
(state: State) =>
Expand Down Expand Up @@ -43,11 +46,20 @@ export const providerSteps: Steps =
case 'unavailable':
flagdOptions['port'] = 9999;
break;
case 'ssl':
// TODO: modify this to support ssl
case 'ssl': {
flagdOptions['port'] = container.getPort(state.resolverType);
flagdOptions['tls'] = true;
const certPath = resolve(getGherkinTestPath('custom-root-cert.crt', 'test-harness/ssl/'));
flagdOptions['certPath'] = certPath;
if (!existsSync(certPath)) {
throw new Error('Certificate file not found at path: ' + certPath);
}
if (state?.config?.selector) {
flagdOptions['selector'] = state.config.selector;
}
type = 'ssl';
break;
}
case 'stable':
flagdOptions['port'] = container.getPort(state.resolverType);
break;
Expand Down
2 changes: 1 addition & 1 deletion libs/providers/flagd/src/e2e/tests/in-process.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('in-process', () => {
// remove filters as we add support for features
// see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues
tagFilter:
'@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',
'@in-process and not @targetURI and not @forbidden and not @events and not @sync and not @grace and not @metadata and not @unixsocket',
scenarioNameTemplate: (vars) => {
return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`;
},
Expand Down
2 changes: 1 addition & 1 deletion libs/providers/flagd/src/e2e/tests/rpc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('rpc', () => {
tagFilter:
// remove filters as we add support for features
// see: https://github.com/open-feature/js-sdk-contrib/issues/1096 and child issues
'@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',
'@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',
scenarioNameTemplate: (vars) => {
return `${vars.scenarioTitle} (${vars.scenarioTags.join(',')} ${vars.featureTags.join(',')})`;
},
Expand Down
5 changes: 5 additions & 0 deletions libs/providers/flagd/src/lib/configuration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('Configuration', () => {
const port = 8080;
const tls = true;
const socketPath = '/tmp/flagd.socks';
const certPath = '/etc/cert/ca.crt';
const maxCacheSize = 333;
const cache = 'disabled';
const resolverType = 'in-process';
Expand All @@ -41,6 +42,7 @@ describe('Configuration', () => {
process.env['FLAGD_PORT'] = `${port}`;
process.env['FLAGD_TLS'] = `${tls}`;
process.env['FLAGD_SOCKET_PATH'] = socketPath;
process.env['FLAGD_SERVER_CERT_PATH'] = certPath;
process.env['FLAGD_CACHE'] = cache;
process.env['FLAGD_MAX_CACHE_SIZE'] = `${maxCacheSize}`;
process.env['FLAGD_SOURCE_SELECTOR'] = `${selector}`;
Expand All @@ -54,6 +56,7 @@ describe('Configuration', () => {
port,
tls,
socketPath,
certPath,
maxCacheSize,
cache,
resolverType,
Expand Down Expand Up @@ -99,6 +102,7 @@ describe('Configuration', () => {
host: 'test',
port: 3000,
tls: true,
certPath: '/custom/cert.pem',
maxCacheSize: 1000,
cache: 'lru',
resolverType: 'rpc',
Expand All @@ -112,6 +116,7 @@ describe('Configuration', () => {
process.env['FLAGD_PORT'] = '8080';
process.env['FLAGD_SYNC_PORT'] = '9090';
process.env['FLAGD_TLS'] = 'false';
process.env['FLAGD_SERVER_CERT_PATH'] = '/env/cert.pem';
process.env['FLAGD_DEFAULT_AUTHORITY'] = 'test-authority-override';

expect(getConfig(options)).toStrictEqual(options);
Expand Down
11 changes: 11 additions & 0 deletions libs/providers/flagd/src/lib/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ export interface Config {
*/
socketPath?: string;

/**
* TLS certificate path to use when TLS connectivity is enabled.
*
* @example "/etc/cert/ca.crt"
*/
certPath?: string;

/**
* Resolver type to use by the provider.
*
Expand Down Expand Up @@ -120,6 +127,7 @@ enum ENV_VAR {
FLAGD_DEADLINE_MS = 'FLAGD_DEADLINE_MS',
FLAGD_TLS = 'FLAGD_TLS',
FLAGD_SOCKET_PATH = 'FLAGD_SOCKET_PATH',
FLAGD_SERVER_CERT_PATH = 'FLAGD_SERVER_CERT_PATH',
FLAGD_CACHE = 'FLAGD_CACHE',
FLAGD_MAX_CACHE_SIZE = 'FLAGD_MAX_CACHE_SIZE',
FLAGD_SOURCE_SELECTOR = 'FLAGD_SOURCE_SELECTOR',
Expand Down Expand Up @@ -165,6 +173,9 @@ const getEnvVarConfig = (): Partial<Config> => {
...(process.env[ENV_VAR.FLAGD_SOCKET_PATH] && {
socketPath: process.env[ENV_VAR.FLAGD_SOCKET_PATH],
}),
...(process.env[ENV_VAR.FLAGD_SERVER_CERT_PATH] && {
certPath: process.env[ENV_VAR.FLAGD_SERVER_CERT_PATH],
}),
...((process.env[ENV_VAR.FLAGD_CACHE] === 'lru' || process.env[ENV_VAR.FLAGD_CACHE] === 'disabled') && {
cache: process.env[ENV_VAR.FLAGD_CACHE],
}),
Expand Down
19 changes: 18 additions & 1 deletion libs/providers/flagd/src/lib/service/common/grpc-util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { ClientReadableStream } from '@grpc/grpc-js';
import { credentials } from '@grpc/grpc-js';
import type { ClientReadableStream, ChannelCredentials } from '@grpc/grpc-js';
import { readFileSync, existsSync } from 'node:fs';

export const closeStreamIfDefined = (stream: ClientReadableStream<unknown> | undefined) => {
/**
Expand All @@ -14,3 +16,18 @@ export const closeStreamIfDefined = (stream: ClientReadableStream<unknown> | und
stream.destroy();
}
};

/**
* Creates gRPC channel credentials based on TLS and certificate path configuration.
* @returns Channel credentials for gRPC connection
*/
export const createChannelCredentials = (tls: boolean, certPath?: string): ChannelCredentials => {
if (!tls) {
return credentials.createInsecure();
}
if (certPath && existsSync(certPath)) {
const rootCerts = readFileSync(certPath);
return credentials.createSsl(rootCerts);
}
return credentials.createSsl();
};
15 changes: 7 additions & 8 deletions libs/providers/flagd/src/lib/service/grpc/grpc-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ClientOptions, ClientReadableStream, ClientUnaryCall, ServiceError } from '@grpc/grpc-js';
import { credentials, status } from '@grpc/grpc-js';
import { status } from '@grpc/grpc-js';
import { ConnectivityState } from '@grpc/grpc-js/build/src/connectivity-state';
import type { EvaluationContext, FlagValue, JsonValue, Logger, ResolutionDetails } from '@openfeature/server-sdk';
import {
Expand All @@ -11,6 +11,7 @@ import {
} from '@openfeature/server-sdk';
import { LRUCache } from 'lru-cache';
import { promisify } from 'node:util';

import type {
EventStreamResponse,
ResolveBooleanRequest,
Expand All @@ -29,7 +30,7 @@ import type { Config } from '../../configuration';
import { DEFAULT_MAX_CACHE_SIZE, EVENT_CONFIGURATION_CHANGE, EVENT_PROVIDER_READY } from '../../constants';
import { FlagdProvider } from '../../flagd-provider';
import type { Service } from '../service';
import { closeStreamIfDefined } from '../common';
import { closeStreamIfDefined, createChannelCredentials } from '../common';

type AnyResponse =
| ResolveBooleanResponse
Expand Down Expand Up @@ -79,21 +80,19 @@ export class GRPCService implements Service {
client?: ServiceClient,
private logger?: Logger,
) {
const { host, port, tls, socketPath, defaultAuthority } = config;
const { host, port, tls, socketPath, certPath, defaultAuthority } = config;
let clientOptions: ClientOptions | undefined;
if (defaultAuthority) {
clientOptions = {
'grpc.default_authority': defaultAuthority,
};
}

const channelCredentials = createChannelCredentials(tls, certPath);

this._client = client
? client
: new ServiceClient(
socketPath ? `unix://${socketPath}` : `${host}:${port}`,
tls ? credentials.createSsl() : credentials.createInsecure(),
clientOptions,
);
: new ServiceClient(socketPath ? `unix://${socketPath}` : `${host}:${port}`, channelCredentials, clientOptions);
this._deadline = config.deadlineMs;

if (config.cache === 'lru') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { ClientReadableStream, ServiceError, ClientOptions } from '@grpc/grpc-js';
import { credentials } from '@grpc/grpc-js';
import type { EvaluationContext, Logger } from '@openfeature/server-sdk';
import { GeneralError } from '@openfeature/server-sdk';
import type { SyncFlagsRequest, SyncFlagsResponse } from '../../../../proto/ts/flagd/sync/v1/sync';
import { FlagSyncServiceClient } from '../../../../proto/ts/flagd/sync/v1/sync';
import type { Config } from '../../../configuration';
import { closeStreamIfDefined } from '../../common';
import { closeStreamIfDefined, createChannelCredentials } from '../../common';
import type { DataFetch } from '../data-fetch';

/**
Expand Down Expand Up @@ -36,19 +35,21 @@ export class GrpcFetch implements DataFetch {
syncServiceClient?: FlagSyncServiceClient,
logger?: Logger,
) {
const { host, port, tls, socketPath, selector, defaultAuthority } = config;
const { host, port, tls, socketPath, certPath, selector, defaultAuthority } = config;
let clientOptions: ClientOptions | undefined;
if (defaultAuthority) {
clientOptions = {
'grpc.default_authority': defaultAuthority,
};
}

const channelCredentials = createChannelCredentials(tls, certPath);

this._syncClient = syncServiceClient
? syncServiceClient
: new FlagSyncServiceClient(
socketPath ? `unix://${socketPath}` : `${host}:${port}`,
tls ? credentials.createSsl() : credentials.createInsecure(),
channelCredentials,
clientOptions,
);

Expand Down