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