mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-23 01:16:27 +02:00
Feat/unleash flags embedded proxy (#1974)
* feat: use unleash flags for embedded proxy * feat: add a separate flag for the proxy frontend * fix: setup unleash in dev * fix: check flagResolver on each request * fix: remove unleash client setup * refactor: update frontend routes snapshot * refactor: make batchMetrics flag dynamic * fix: always check dynamic CORS origins config * fix: make conditionalMiddleware work with the OpenAPI schema generation Co-authored-by: olav <mail@olav.io>
This commit is contained in:
parent
2d8dfafea9
commit
85b45b9965
@ -70,7 +70,7 @@ const ApiTokenForm: React.FC<IApiTokenFormProps> = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (uiConfig.embedProxy) {
|
if (uiConfig.flags.embedProxyFrontend) {
|
||||||
selectableTypes.splice(1, 0, {
|
selectableTypes.splice(1, 0, {
|
||||||
key: TokenType.FRONTEND,
|
key: TokenType.FRONTEND,
|
||||||
label: `Client-side SDK (${TokenType.FRONTEND})`,
|
label: `Client-side SDK (${TokenType.FRONTEND})`,
|
||||||
|
@ -85,7 +85,7 @@ function AdminMenu() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{uiConfig.embedProxy && (
|
{uiConfig.flags.embedProxyFrontend && (
|
||||||
<Tab
|
<Tab
|
||||||
value="/admin/cors"
|
value="/admin/cors"
|
||||||
label={
|
label={
|
||||||
|
@ -449,7 +449,7 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"configFlag": "embedProxy",
|
"flag": "embedProxyFrontend",
|
||||||
"menu": {
|
"menu": {
|
||||||
"adminSettings": true,
|
"adminSettings": true,
|
||||||
},
|
},
|
||||||
|
@ -496,7 +496,7 @@ export const routes: IRoute[] = [
|
|||||||
title: 'CORS origins',
|
title: 'CORS origins',
|
||||||
component: CorsAdmin,
|
component: CorsAdmin,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
configFlag: 'embedProxy',
|
flag: 'embedProxyFrontend',
|
||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { VoidFunctionComponent } from 'react';
|
import { VoidFunctionComponent } from 'react';
|
||||||
import { IUiConfig } from 'interfaces/uiConfig';
|
import { IFlags, IUiConfig } from 'interfaces/uiConfig';
|
||||||
|
|
||||||
export interface IRoute {
|
export interface IRoute {
|
||||||
path: string;
|
path: string;
|
||||||
@ -7,7 +7,7 @@ export interface IRoute {
|
|||||||
type: 'protected' | 'unprotected';
|
type: 'protected' | 'unprotected';
|
||||||
layout?: string;
|
layout?: string;
|
||||||
parent?: string;
|
parent?: string;
|
||||||
flag?: string;
|
flag?: keyof IFlags;
|
||||||
configFlag?: keyof IUiConfig;
|
configFlag?: keyof IUiConfig;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
enterprise?: boolean;
|
enterprise?: boolean;
|
||||||
|
@ -17,7 +17,6 @@ export interface IUiConfig {
|
|||||||
segmentValuesLimit?: number;
|
segmentValuesLimit?: number;
|
||||||
strategySegmentsLimit?: number;
|
strategySegmentsLimit?: number;
|
||||||
frontendApiOrigins?: string[];
|
frontendApiOrigins?: string[];
|
||||||
embedProxy?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProclamationToast {
|
export interface IProclamationToast {
|
||||||
@ -40,6 +39,7 @@ export interface IFlags {
|
|||||||
UNLEASH_CLOUD?: boolean;
|
UNLEASH_CLOUD?: boolean;
|
||||||
UG?: boolean;
|
UG?: boolean;
|
||||||
ENABLE_DARK_MODE_SUPPORT?: boolean;
|
ENABLE_DARK_MODE_SUPPORT?: boolean;
|
||||||
|
embedProxyFrontend?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -70,6 +70,7 @@ Object {
|
|||||||
"anonymiseEventLog": false,
|
"anonymiseEventLog": false,
|
||||||
"batchMetrics": false,
|
"batchMetrics": false,
|
||||||
"embedProxy": false,
|
"embedProxy": false,
|
||||||
|
"embedProxyFrontend": false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"flagResolver": FlagResolver {
|
"flagResolver": FlagResolver {
|
||||||
@ -78,6 +79,7 @@ Object {
|
|||||||
"anonymiseEventLog": false,
|
"anonymiseEventLog": false,
|
||||||
"batchMetrics": false,
|
"batchMetrics": false,
|
||||||
"embedProxy": false,
|
"embedProxy": false,
|
||||||
|
"embedProxyFrontend": false,
|
||||||
},
|
},
|
||||||
"externalResolver": Object {
|
"externalResolver": Object {
|
||||||
"isEnabled": [Function],
|
"isEnabled": [Function],
|
||||||
|
@ -22,6 +22,7 @@ import secureHeaders from './middleware/secure-headers';
|
|||||||
|
|
||||||
import { loadIndexHTML } from './util/load-index-html';
|
import { loadIndexHTML } from './util/load-index-html';
|
||||||
import { findPublicFolder } from './util/findPublicFolder';
|
import { findPublicFolder } from './util/findPublicFolder';
|
||||||
|
import { conditionalMiddleware } from './middleware/conditional-middleware';
|
||||||
|
|
||||||
export default async function getApp(
|
export default async function getApp(
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -69,15 +70,16 @@ export default async function getApp(
|
|||||||
services.openApiService.useDocs(app);
|
services.openApiService.useDocs(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
config.experimental.flags.embedProxy &&
|
|
||||||
config.frontendApiOrigins.length > 0
|
|
||||||
) {
|
|
||||||
// Support CORS preflight requests for the frontend endpoints.
|
// Support CORS preflight requests for the frontend endpoints.
|
||||||
// Preflight requests should not have Authorization headers,
|
// Preflight requests should not have Authorization headers,
|
||||||
// so this must be handled before the API token middleware.
|
// so this must be handled before the API token middleware.
|
||||||
app.options('/api/frontend*', corsOriginMiddleware(services));
|
app.options(
|
||||||
}
|
'/api/frontend*',
|
||||||
|
conditionalMiddleware(
|
||||||
|
() => config.flagResolver.isEnabled('embedProxy'),
|
||||||
|
corsOriginMiddleware(services),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
switch (config.authentication.type) {
|
switch (config.authentication.type) {
|
||||||
case IAuthType.OPEN_SOURCE: {
|
case IAuthType.OPEN_SOURCE: {
|
||||||
@ -152,7 +154,7 @@ export default async function getApp(
|
|||||||
|
|
||||||
app.get(`${baseUriPath}/*`, (req, res) => {
|
app.get(`${baseUriPath}/*`, (req, res) => {
|
||||||
if (req.path.startsWith(`${baseUriPath}/api`)) {
|
if (req.path.startsWith(`${baseUriPath}/api`)) {
|
||||||
res.status(404).send({ message: '404 - Not found' });
|
res.status(404).send({ message: 'Not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ const apiAccessMiddleware = (
|
|||||||
{
|
{
|
||||||
getLogger,
|
getLogger,
|
||||||
authentication,
|
authentication,
|
||||||
experimental,
|
flagResolver,
|
||||||
}: Pick<IUnleashConfig, 'getLogger' | 'authentication' | 'experimental'>,
|
}: Pick<IUnleashConfig, 'getLogger' | 'authentication' | 'flagResolver'>,
|
||||||
{ apiTokenService }: any,
|
{ apiTokenService }: any,
|
||||||
): any => {
|
): any => {
|
||||||
const logger = getLogger('/middleware/api-token.ts');
|
const logger = getLogger('/middleware/api-token.ts');
|
||||||
@ -54,7 +54,7 @@ const apiAccessMiddleware = (
|
|||||||
(apiUser.type === CLIENT && !isClientApi(req)) ||
|
(apiUser.type === CLIENT && !isClientApi(req)) ||
|
||||||
(apiUser.type === FRONTEND && !isProxyApi(req)) ||
|
(apiUser.type === FRONTEND && !isProxyApi(req)) ||
|
||||||
(apiUser.type === FRONTEND &&
|
(apiUser.type === FRONTEND &&
|
||||||
!experimental.flags.embedProxy)
|
!flagResolver.isEnabled('embedProxy'))
|
||||||
) {
|
) {
|
||||||
res.status(403).send({ message: TOKEN_TYPE_ERROR_MESSAGE });
|
res.status(403).send({ message: TOKEN_TYPE_ERROR_MESSAGE });
|
||||||
return;
|
return;
|
||||||
|
19
src/lib/middleware/conditional-middleware.ts
Normal file
19
src/lib/middleware/conditional-middleware.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { RequestHandler, Router } from 'express';
|
||||||
|
|
||||||
|
export const conditionalMiddleware = (
|
||||||
|
condition: () => boolean,
|
||||||
|
middleware: RequestHandler,
|
||||||
|
): RequestHandler => {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
if (condition()) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.status(404).send({ message: 'Not found' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use(middleware);
|
||||||
|
return router;
|
||||||
|
};
|
@ -69,9 +69,6 @@ export const uiConfigSchema = {
|
|||||||
versionInfo: {
|
versionInfo: {
|
||||||
$ref: '#/components/schemas/versionSchema',
|
$ref: '#/components/schemas/versionSchema',
|
||||||
},
|
},
|
||||||
embedProxy: {
|
|
||||||
type: 'boolean',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
@ -118,7 +118,6 @@ class ConfigController extends Controller {
|
|||||||
frontendApiOrigins: frontendSettings.frontendApiOrigins,
|
frontendApiOrigins: frontendSettings.frontendApiOrigins,
|
||||||
versionInfo: this.versionService.getVersionInfo(),
|
versionInfo: this.versionService.getVersionInfo(),
|
||||||
disablePasswordAuth,
|
disablePasswordAuth,
|
||||||
embedProxy: this.config.experimental.flags.embedProxy,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
|
@ -10,10 +10,12 @@ const ClientApi = require('./client-api');
|
|||||||
const Controller = require('./controller');
|
const Controller = require('./controller');
|
||||||
import { HealthCheckController } from './health-check';
|
import { HealthCheckController } from './health-check';
|
||||||
import ProxyController from './proxy-api';
|
import ProxyController from './proxy-api';
|
||||||
|
import { conditionalMiddleware } from '../middleware/conditional-middleware';
|
||||||
|
|
||||||
class IndexRouter extends Controller {
|
class IndexRouter extends Controller {
|
||||||
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
this.use('/health', new HealthCheckController(config, services).router);
|
this.use('/health', new HealthCheckController(config, services).router);
|
||||||
this.use('/internal-backstage', new BackstageController(config).router);
|
this.use('/internal-backstage', new BackstageController(config).router);
|
||||||
this.use('/logout', new LogoutController(config).router);
|
this.use('/logout', new LogoutController(config).router);
|
||||||
@ -28,14 +30,15 @@ class IndexRouter extends Controller {
|
|||||||
this.use('/api/admin', new AdminApi(config, services).router);
|
this.use('/api/admin', new AdminApi(config, services).router);
|
||||||
this.use('/api/client', new ClientApi(config, services).router);
|
this.use('/api/client', new ClientApi(config, services).router);
|
||||||
|
|
||||||
if (config.experimental.flags.embedProxy) {
|
|
||||||
this.use(
|
this.use(
|
||||||
'/api/frontend',
|
'/api/frontend',
|
||||||
|
conditionalMiddleware(
|
||||||
|
() => config.flagResolver.isEnabled('embedProxy'),
|
||||||
new ProxyController(config, services).router,
|
new ProxyController(config, services).router,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default IndexRouter;
|
export default IndexRouter;
|
||||||
|
|
||||||
|
@ -41,11 +41,9 @@ export default class ProxyController extends Controller {
|
|||||||
this.logger = config.getLogger('client-api/feature.js');
|
this.logger = config.getLogger('client-api/feature.js');
|
||||||
this.services = services;
|
this.services = services;
|
||||||
|
|
||||||
if (config.frontendApiOrigins.length > 0) {
|
|
||||||
// Support CORS requests for the frontend endpoints.
|
// Support CORS requests for the frontend endpoints.
|
||||||
// Preflight requests are handled in `app.ts`.
|
// Preflight requests are handled in `app.ts`.
|
||||||
this.app.use(corsOriginMiddleware(services));
|
this.app.use(corsOriginMiddleware(services));
|
||||||
}
|
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
import { clientMetricsSchema } from './schema';
|
import { clientMetricsSchema } from './schema';
|
||||||
import { hoursToMilliseconds, secondsToMilliseconds } from 'date-fns';
|
import { hoursToMilliseconds, secondsToMilliseconds } from 'date-fns';
|
||||||
import { IFeatureToggleStore } from '../../types/stores/feature-toggle-store';
|
import { IFeatureToggleStore } from '../../types/stores/feature-toggle-store';
|
||||||
import EventEmitter from 'events';
|
|
||||||
import { CLIENT_METRICS } from '../../types/events';
|
import { CLIENT_METRICS } from '../../types/events';
|
||||||
import ApiUser from '../../types/api-user';
|
import ApiUser from '../../types/api-user';
|
||||||
import { ALL } from '../../types/models/api-token';
|
import { ALL } from '../../types/models/api-token';
|
||||||
@ -18,6 +17,8 @@ import User from '../../types/user';
|
|||||||
import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics';
|
import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics';
|
||||||
|
|
||||||
export default class ClientMetricsServiceV2 {
|
export default class ClientMetricsServiceV2 {
|
||||||
|
private config: IUnleashConfig;
|
||||||
|
|
||||||
private timers: NodeJS.Timeout[] = [];
|
private timers: NodeJS.Timeout[] = [];
|
||||||
|
|
||||||
private unsavedMetrics: IClientMetricsEnv[] = [];
|
private unsavedMetrics: IClientMetricsEnv[] = [];
|
||||||
@ -26,10 +27,6 @@ export default class ClientMetricsServiceV2 {
|
|||||||
|
|
||||||
private featureToggleStore: IFeatureToggleStore;
|
private featureToggleStore: IFeatureToggleStore;
|
||||||
|
|
||||||
private batchMetricsEnabled: boolean;
|
|
||||||
|
|
||||||
private eventBus: EventEmitter;
|
|
||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -37,28 +34,21 @@ export default class ClientMetricsServiceV2 {
|
|||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
clientMetricsStoreV2,
|
clientMetricsStoreV2,
|
||||||
}: Pick<IUnleashStores, 'featureToggleStore' | 'clientMetricsStoreV2'>,
|
}: Pick<IUnleashStores, 'featureToggleStore' | 'clientMetricsStoreV2'>,
|
||||||
{
|
config: IUnleashConfig,
|
||||||
experimental,
|
|
||||||
eventBus,
|
|
||||||
getLogger,
|
|
||||||
}: Pick<IUnleashConfig, 'eventBus' | 'getLogger' | 'experimental'>,
|
|
||||||
bulkInterval = secondsToMilliseconds(5),
|
bulkInterval = secondsToMilliseconds(5),
|
||||||
) {
|
) {
|
||||||
this.featureToggleStore = featureToggleStore;
|
this.featureToggleStore = featureToggleStore;
|
||||||
this.clientMetricsStoreV2 = clientMetricsStoreV2;
|
this.clientMetricsStoreV2 = clientMetricsStoreV2;
|
||||||
this.batchMetricsEnabled = experimental.flags.batchMetrics;
|
this.config = config;
|
||||||
this.eventBus = eventBus;
|
this.logger = config.getLogger(
|
||||||
this.logger = getLogger(
|
|
||||||
'/services/client-metrics/client-metrics-service-v2.ts',
|
'/services/client-metrics/client-metrics-service-v2.ts',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.batchMetricsEnabled) {
|
|
||||||
this.timers.push(
|
this.timers.push(
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this.bulkAdd().catch(console.error);
|
this.bulkAdd().catch(console.error);
|
||||||
}, bulkInterval).unref(),
|
}, bulkInterval).unref(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
this.timers.push(
|
this.timers.push(
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@ -90,7 +80,7 @@ export default class ClientMetricsServiceV2 {
|
|||||||
}))
|
}))
|
||||||
.filter((item) => !(item.yes === 0 && item.no === 0));
|
.filter((item) => !(item.yes === 0 && item.no === 0));
|
||||||
|
|
||||||
if (this.batchMetricsEnabled) {
|
if (this.config.flagResolver.isEnabled('batchMetrics')) {
|
||||||
this.unsavedMetrics = collapseHourlyMetrics([
|
this.unsavedMetrics = collapseHourlyMetrics([
|
||||||
...this.unsavedMetrics,
|
...this.unsavedMetrics,
|
||||||
...clientMetrics,
|
...clientMetrics,
|
||||||
@ -99,11 +89,11 @@ export default class ClientMetricsServiceV2 {
|
|||||||
await this.clientMetricsStoreV2.batchInsertMetrics(clientMetrics);
|
await this.clientMetricsStoreV2.batchInsertMetrics(clientMetrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.eventBus.emit(CLIENT_METRICS, value);
|
this.config.eventBus.emit(CLIENT_METRICS, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkAdd(): Promise<void> {
|
async bulkAdd(): Promise<void> {
|
||||||
if (this.batchMetricsEnabled && this.unsavedMetrics.length > 0) {
|
if (this.unsavedMetrics.length > 0) {
|
||||||
// Make a copy of `unsavedMetrics` in case new metrics
|
// Make a copy of `unsavedMetrics` in case new metrics
|
||||||
// arrive while awaiting `batchInsertMetrics`.
|
// arrive while awaiting `batchInsertMetrics`.
|
||||||
const copy = [...this.unsavedMetrics];
|
const copy = [...this.unsavedMetrics];
|
||||||
|
@ -10,6 +10,10 @@ export const defaultExperimentalOptions = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY,
|
process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
embedProxyFrontend: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_EMBED_PROXY_FRONTEND,
|
||||||
|
false,
|
||||||
|
),
|
||||||
batchMetrics: parseEnvVarBoolean(
|
batchMetrics: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_BATCH_METRICS,
|
process.env.UNLEASH_EXPERIMENTAL_BATCH_METRICS,
|
||||||
false,
|
false,
|
||||||
@ -23,6 +27,7 @@ export interface IExperimentalOptions {
|
|||||||
[key: string]: boolean;
|
[key: string]: boolean;
|
||||||
ENABLE_DARK_MODE_SUPPORT?: boolean;
|
ENABLE_DARK_MODE_SUPPORT?: boolean;
|
||||||
embedProxy?: boolean;
|
embedProxy?: boolean;
|
||||||
|
embedProxyFrontend?: boolean;
|
||||||
batchMetrics?: boolean;
|
batchMetrics?: boolean;
|
||||||
anonymiseEventLog?: boolean;
|
anonymiseEventLog?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -34,6 +34,7 @@ process.nextTick(async () => {
|
|||||||
// externalResolver: unleash,
|
// externalResolver: unleash,
|
||||||
flags: {
|
flags: {
|
||||||
embedProxy: true,
|
embedProxy: true,
|
||||||
|
embedProxyFrontend: true,
|
||||||
batchMetrics: true,
|
batchMetrics: true,
|
||||||
anonymiseEventLog: false,
|
anonymiseEventLog: false,
|
||||||
},
|
},
|
||||||
|
@ -25,6 +25,7 @@ export function createTestConfig(config?: IUnleashOptions): IUnleashConfig {
|
|||||||
experimental: {
|
experimental: {
|
||||||
flags: {
|
flags: {
|
||||||
embedProxy: true,
|
embedProxy: true,
|
||||||
|
embedProxyFrontend: true,
|
||||||
batchMetrics: true,
|
batchMetrics: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -2839,9 +2839,6 @@ Object {
|
|||||||
"emailEnabled": Object {
|
"emailEnabled": Object {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
},
|
},
|
||||||
"embedProxy": Object {
|
|
||||||
"type": "boolean",
|
|
||||||
},
|
|
||||||
"environment": Object {
|
"environment": Object {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user