1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

implement proxy all endpoint (#2460)

Signed-off-by: andreas-unleash <andreas@getunleash.ai>

<!-- Thanks for creating a PR! To make it easier for reviewers and
everyone else to understand what your changes relate to, please add some
relevant content to the headings below. Feel free to ignore or delete
sections that you don't think are relevant. Thank you! ❤️ -->

This PR implements the `all` endpoint of unleash-proxy, by adding an
experimental flag that can control the behaviour

## About the changes
<!-- Describe the changes introduced. What are they and why are they
being introduced? Feel free to also add screenshots or steps to view the
changes if they're visual. -->

<!-- Does it close an issue? Multiple? -->
Closes #

<!-- (For internal contributors): Does it relate to an issue on public
roadmap? -->
<!--
Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#
-->

### Important files
<!-- PRs can contain a lot of changes, but not all changes are equally
important. Where should a reviewer start looking to get an overview of
the changes? Are any files particularly important? -->


## Discussion points
<!-- Anything about the PR you'd like to discuss before it gets merged?
Got any questions or doubts? -->

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
andreas-unleash 2022-11-21 12:57:07 +02:00 committed by GitHub
parent efd47b72a8
commit 6e5b214475
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 123 additions and 30 deletions

View File

@ -73,6 +73,7 @@ exports[`should create default config 1`] = `
"cloneEnvironment": false,
"embedProxy": false,
"embedProxyFrontend": false,
"proxyReturnAllToggles": false,
"responseTimeWithAppName": false,
"syncSSOGroups": false,
"toggleTagFiltering": false,
@ -88,6 +89,7 @@ exports[`should create default config 1`] = `
"cloneEnvironment": false,
"embedProxy": false,
"embedProxyFrontend": false,
"proxyReturnAllToggles": false,
"responseTimeWithAppName": false,
"syncSSOGroups": false,
"toggleTagFiltering": false,

View File

@ -10,7 +10,7 @@ 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';
import { conditionalMiddleware } from '../middleware';
import EdgeController from './edge-api';
import { PublicInviteController } from './public-invite';
@ -47,7 +47,8 @@ class IndexRouter extends Controller {
'/api/frontend',
conditionalMiddleware(
() => config.flagResolver.isEnabled('embedProxy'),
new ProxyController(config, services).router,
new ProxyController(config, services, config.flagResolver)
.router,
),
);

View File

@ -1,21 +1,25 @@
import { Response, Request } from 'express';
import { Request, Response } from 'express';
import Controller from '../controller';
import { IUnleashConfig, IUnleashServices } from '../../types';
import {
IFlagResolver,
IUnleashConfig,
IUnleashServices,
NONE,
} from '../../types';
import { Logger } from '../../logger';
import { NONE } from '../../types/permissions';
import ApiUser from '../../types/api-user';
import {
createRequestSchema,
createResponseSchema,
emptyResponse,
ProxyClientSchema,
proxyFeaturesSchema,
ProxyFeaturesSchema,
} from '../../openapi/spec/proxy-features-schema';
ProxyMetricsSchema,
} from '../../openapi';
import { Context } from 'unleash-client';
import { enrichContextWithIp } from '../../proxy/create-context';
import { ProxyMetricsSchema } from '../../openapi/spec/proxy-metrics-schema';
import { ProxyClientSchema } from '../../openapi/spec/proxy-client-schema';
import { createResponseSchema } from '../../openapi/util/create-response-schema';
import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { emptyResponse } from '../../openapi/util/standard-responses';
import { corsOriginMiddleware } from '../../middleware/cors-origin-middleware';
import { enrichContextWithIp } from '../../proxy';
import { corsOriginMiddleware } from '../../middleware';
interface ApiUserRequest<
PARAM = any,
@ -36,10 +40,17 @@ export default class ProxyController extends Controller {
private services: Services;
constructor(config: IUnleashConfig, services: Services) {
private flagResolver: IFlagResolver;
constructor(
config: IUnleashConfig,
services: Services,
flagResolver: IFlagResolver,
) {
super(config);
this.logger = config.getLogger('proxy-api/index.ts');
this.services = services;
this.flagResolver = flagResolver;
// Support CORS requests for the frontend endpoints.
// Preflight requests are handled in `app.ts`.
@ -133,10 +144,18 @@ export default class ProxyController extends Controller {
req: ApiUserRequest,
res: Response<ProxyFeaturesSchema>,
) {
const toggles = await this.services.proxyService.getProxyFeatures(
req.user,
ProxyController.createContext(req),
);
let toggles;
if (this.flagResolver.isEnabled('proxyReturnAllToggles')) {
toggles = await this.services.proxyService.getAllProxyFeatures(
req.user,
ProxyController.createContext(req),
);
} else {
toggles = await this.services.proxyService.getProxyFeatures(
req.user,
ProxyController.createContext(req),
);
}
this.services.openApiService.respondWithValidation(
200,
res,

View File

@ -1,7 +1,6 @@
import { IUnleashConfig } from '../types/option';
import { IUnleashConfig, IUnleashServices, IUnleashStores } from '../types';
import { Logger } from '../logger';
import { IUnleashServices, IUnleashStores } from '../types';
import { ProxyFeatureSchema } from '../openapi/spec/proxy-feature-schema';
import { ProxyFeatureSchema, ProxyMetricsSchema } from '../openapi';
import ApiUser from '../types/api-user';
import {
Context,
@ -10,10 +9,9 @@ import {
Unleash,
UnleashEvents,
} from 'unleash-client';
import { ProxyRepository } from '../proxy/proxy-repository';
import { ProxyRepository } from '../proxy';
import assert from 'assert';
import { ApiTokenType } from '../types/models/api-token';
import { ProxyMetricsSchema } from '../openapi/spec/proxy-metrics-schema';
type Config = Pick<IUnleashConfig, 'getLogger' | 'frontendApi'>;
@ -59,6 +57,21 @@ export class ProxyService {
}));
}
async getAllProxyFeatures(
token: ApiUser,
context: Context,
): Promise<ProxyFeatureSchema[]> {
const client = await this.clientForProxyToken(token);
const definitions = client.getFeatureToggleDefinitions() || [];
return definitions.map((feature) => ({
name: feature.name,
enabled: Boolean(feature.enabled),
variant: client.forceGetVariant(feature.name, context),
impressionData: Boolean(feature.impressionData),
}));
}
async registerProxyMetrics(
token: ApiUser,
metrics: ProxyMetricsSchema,

View File

@ -1,4 +1,4 @@
import { parseEnvVarBoolean } from '../util/parseEnvVar';
import { parseEnvVarBoolean } from '../util';
export type IFlags = Partial<Record<string, boolean>>;
@ -38,6 +38,10 @@ export const defaultExperimentalOptions = {
process.env.UNLEASH_EXPERIMENTAL_TOGGLE_TAG_FILTERING,
false,
),
proxyReturnAllToggles: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_PROXY_RETURN_ALL_TOGGLES,
false,
),
variantsPerEnvironment: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_VARIANTS_PER_ENVIRONMENT,
false,
@ -57,6 +61,7 @@ export interface IExperimentalOptions {
syncSSOGroups?: boolean;
changeRequests?: boolean;
cloneEnvironment?: boolean;
proxyReturnAllToggles?: boolean;
variantsPerEnvironment?: boolean;
};
externalResolver: IExternalFlagResolver;

View File

@ -1,16 +1,19 @@
import { IUnleashTest, setupAppWithAuth } from '../../helpers/test-helper';
import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import { randomId } from '../../../../lib/util/random-id';
import { randomId } from '../../../../lib/util';
import {
ApiTokenType,
IApiToken,
IApiTokenCreate,
} from '../../../../lib/types/models/api-token';
import { startOfHour } from 'date-fns';
import { IConstraint, IStrategyConfig } from '../../../../lib/types/model';
import { ProxyRepository } from '../../../../lib/proxy/proxy-repository';
import { FEATURE_UPDATED } from '../../../../lib/types/events';
import {
FEATURE_UPDATED,
IConstraint,
IStrategyConfig,
} from '../../../../lib/types';
import { ProxyRepository } from '../../../../lib/proxy';
let app: IUnleashTest;
let db: ITestDb;
@ -929,3 +932,52 @@ test('Should not recursively set off timers on events', async () => {
expect(spy.mock.calls.length < 3).toBe(true);
jest.useRealTimers();
});
test('should return all features when specified', async () => {
app.config.experimental.flags.proxyReturnAllToggles = true;
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
await createFeatureToggle({
name: 'enabledFeature1',
enabled: true,
strategies: [{ name: 'default', constraints: [], parameters: {} }],
});
await createFeatureToggle({
name: 'enabledFeature2',
enabled: true,
strategies: [{ name: 'default', constraints: [], parameters: {} }],
});
await createFeatureToggle({
name: 'disabledFeature',
enabled: false,
strategies: [{ name: 'default', constraints: [], parameters: {} }],
});
await app.request
.get('/api/frontend')
.set('Authorization', frontendToken.secret)
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body).toEqual({
toggles: [
{
name: 'enabledFeature1',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
},
{
name: 'enabledFeature2',
enabled: true,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
},
{
name: 'disabledFeature',
enabled: false,
impressionData: false,
variant: { enabled: false, name: 'disabled' },
},
],
});
});
});

View File

@ -4,7 +4,7 @@ import supertest from 'supertest';
import EventEmitter from 'events';
import getApp from '../../../lib/app';
import { createTestConfig } from '../../config/test-config';
import { IAuthType } from '../../../lib/types/option';
import { IAuthType, IUnleashConfig } from '../../../lib/types/option';
import { createServices } from '../../../lib/services';
import sessionDb from '../../../lib/middleware/session-db';
import { IUnleashStores } from '../../../lib/types';
@ -16,6 +16,7 @@ export interface IUnleashTest {
request: supertest.SuperAgentTest;
destroy: () => Promise<void>;
services: IUnleashServices;
config: IUnleashConfig;
}
async function createApp(
@ -49,7 +50,7 @@ async function createApp(
};
// TODO: use create from server-impl instead?
return { request, destroy, services };
return { request, destroy, services, config };
}
export async function setupApp(stores: IUnleashStores): Promise<IUnleashTest> {