1
0
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:
Fredrik Strand Oseberg 2022-08-26 15:16:29 +02:00 committed by GitHub
parent 2d8dfafea9
commit 85b45b9965
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 74 additions and 60 deletions

View File

@ -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})`,

View File

@ -85,7 +85,7 @@ function AdminMenu() {
</NavLink>
}
/>
{uiConfig.embedProxy && (
{uiConfig.flags.embedProxyFrontend && (
<Tab
value="/admin/cors"
label={

View File

@ -449,7 +449,7 @@ exports[`returns all baseRoutes 1`] = `
},
{
"component": [Function],
"configFlag": "embedProxy",
"flag": "embedProxyFrontend",
"menu": {
"adminSettings": true,
},

View File

@ -496,7 +496,7 @@ export const routes: IRoute[] = [
title: 'CORS origins',
component: CorsAdmin,
type: 'protected',
configFlag: 'embedProxy',
flag: 'embedProxyFrontend',
menu: { adminSettings: true },
},
{

View File

@ -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;

View File

@ -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 {

View File

@ -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],

View File

@ -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;
}

View File

@ -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;

View 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;
};

View File

@ -69,9 +69,6 @@ export const uiConfigSchema = {
versionInfo: {
$ref: '#/components/schemas/versionSchema',
},
embedProxy: {
type: 'boolean',
},
},
components: {
schemas: {

View File

@ -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(

View File

@ -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;

View File

@ -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',

View File

@ -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];

View File

@ -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;
};

View File

@ -34,6 +34,7 @@ process.nextTick(async () => {
// externalResolver: unleash,
flags: {
embedProxy: true,
embedProxyFrontend: true,
batchMetrics: true,
anonymiseEventLog: false,
},

View File

@ -25,6 +25,7 @@ export function createTestConfig(config?: IUnleashOptions): IUnleashConfig {
experimental: {
flags: {
embedProxy: true,
embedProxyFrontend: true,
batchMetrics: true,
},
},

View File

@ -2839,9 +2839,6 @@ Object {
"emailEnabled": Object {
"type": "boolean",
},
"embedProxy": Object {
"type": "boolean",
},
"environment": Object {
"type": "string",
},