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

Merge branch 'master' into docs/more-feature-toggle-type-info

This commit is contained in:
Thomas Heartman 2021-11-24 08:47:16 +01:00 committed by GitHub
commit ff4114e7d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 594 additions and 62 deletions

17
.github/workflows/build_doc_prs.yaml vendored Normal file
View File

@ -0,0 +1,17 @@
name: PR -> Build Docs
on:
pull_request:
paths:
- website/**
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: setup git config
run: |
# Build the site
cd website && yarn && yarn build

View File

@ -137,7 +137,7 @@
"del-cli": "4.0.1", "del-cli": "4.0.1",
"eslint": "8.3.0", "eslint": "8.3.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "15.0.0", "eslint-config-airbnb-typescript": "16.0.0",
"eslint-config-prettier": "8.3.0", "eslint-config-prettier": "8.3.0",
"eslint-plugin-import": "2.25.3", "eslint-plugin-import": "2.25.3",
"eslint-plugin-prettier": "4.0.0", "eslint-plugin-prettier": "4.0.0",

View File

@ -29,6 +29,7 @@ import EnvironmentStore from './environment-store';
import FeatureTagStore from './feature-tag-store'; import FeatureTagStore from './feature-tag-store';
import { FeatureEnvironmentStore } from './feature-environment-store'; import { FeatureEnvironmentStore } from './feature-environment-store';
import { ClientMetricsStoreV2 } from './client-metrics-store-v2'; import { ClientMetricsStoreV2 } from './client-metrics-store-v2';
import UserSplashStore from './user-splash-store';
export const createStores = ( export const createStores = (
config: IUnleashConfig, config: IUnleashConfig,
@ -85,6 +86,7 @@ export const createStores = (
eventBus, eventBus,
getLogger, getLogger,
), ),
userSplashStore: new UserSplashStore(db, eventBus, getLogger),
}; };
}; };

View File

@ -0,0 +1,105 @@
import { Knex } from 'knex';
import { EventEmitter } from 'events';
import { LogProvider, Logger } from '../logger';
import {
IUserSplash,
IUserSplashKey,
IUserSplashStore,
} from '../types/stores/user-splash-store';
const COLUMNS = ['user_id', 'splash_id', 'seen'];
const TABLE = 'user_splash';
interface IUserSplashTable {
seen?: boolean;
splash_id: string;
user_id: number;
}
const fieldToRow = (fields: IUserSplash): IUserSplashTable => ({
seen: fields.seen,
splash_id: fields.splashId,
user_id: fields.userId,
});
const rowToField = (row: IUserSplashTable): IUserSplash => ({
seen: row.seen,
splashId: row.splash_id,
userId: row.user_id,
});
export default class UserSplashStore implements IUserSplashStore {
private db: Knex;
private logger: Logger;
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
this.db = db;
this.logger = getLogger('user-splash-store.ts');
}
async getAllUserSplashs(userId: number): Promise<IUserSplash[]> {
const userSplash = await this.db
.table<IUserSplashTable>(TABLE)
.select()
.where({ user_id: userId });
return userSplash.map(rowToField);
}
async getSplash(userId: number, splashId: string): Promise<IUserSplash> {
const userSplash = await this.db
.table<IUserSplashTable>(TABLE)
.select()
.where({ user_id: userId, splash_id: splashId })
.first();
return rowToField(userSplash);
}
async updateSplash(splash: IUserSplash): Promise<IUserSplash> {
const insertedSplash = await this.db
.table<IUserSplashTable>(TABLE)
.insert(fieldToRow(splash))
.onConflict(['user_id', 'splash_id'])
.merge()
.returning(COLUMNS);
return rowToField(insertedSplash[0]);
}
async delete({ userId, splashId }: IUserSplashKey): Promise<void> {
await this.db(TABLE)
.where({ user_id: userId, splash_id: splashId })
.del();
}
async deleteAll(): Promise<void> {
await this.db(TABLE).del();
}
destroy(): void {}
async exists({ userId, splashId }: IUserSplashKey): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE user_id = ? AND splash_id = ?) AS present`,
[userId, splashId],
);
const { present } = result.rows[0];
return present;
}
async get({ userId, splashId }: IUserSplashKey): Promise<IUserSplash> {
return this.getSplash(userId, splashId);
}
async getAll(): Promise<IUserSplash[]> {
const userSplashs = await this.db
.table<IUserSplashTable>(TABLE)
.select();
return userSplashs.map(rowToField);
}
}
module.exports = UserSplashStore;

View File

@ -21,6 +21,7 @@ import ApiTokenController from './api-token-controller';
import UserAdminController from './user-admin'; import UserAdminController from './user-admin';
import EmailController from './email'; import EmailController from './email';
import UserFeedbackController from './user-feedback-controller'; import UserFeedbackController from './user-feedback-controller';
import UserSplashController from './user-splash-controller';
import ProjectApi from './project'; import ProjectApi from './project';
import { EnvironmentsController } from './environments-controller'; import { EnvironmentsController } from './environments-controller';
@ -92,6 +93,10 @@ class AdminApi extends Controller {
'/environments', '/environments',
new EnvironmentsController(config, services).router, new EnvironmentsController(config, services).router,
); );
this.app.use(
'/splash',
new UserSplashController(config, services).router,
);
} }
index(req, res) { index(req, res) {

View File

@ -0,0 +1,49 @@
import { Response } from 'express';
import Controller from '../controller';
import { Logger } from '../../logger';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import UserSplashService from '../../services/user-splash-service';
import { IAuthRequest } from '../unleash-types';
interface ISplashBody {
seen: boolean;
splashId: string;
}
class UserSplashController extends Controller {
private logger: Logger;
private userSplashService: UserSplashService;
constructor(
config: IUnleashConfig,
{ userSplashService }: Pick<IUnleashServices, 'userSplashService'>,
) {
super(config);
this.logger = config.getLogger('splash-controller.ts');
this.userSplashService = userSplashService;
this.post('/:id', this.updateSplashSettings);
}
private async updateSplashSettings(
req: IAuthRequest<any, any, ISplashBody, any>,
res: Response,
): Promise<void> {
const { user } = req;
const { id } = req.params;
const splash = {
splashId: id,
userId: user.id,
seen: true,
};
const updated = await this.userSplashService.updateSplash(splash);
res.json(updated);
}
}
module.exports = UserSplashController;
export default UserSplashController;

View File

@ -7,6 +7,7 @@ import { IUnleashServices } from '../../types/services';
import UserService from '../../services/user-service'; import UserService from '../../services/user-service';
import SessionService from '../../services/session-service'; import SessionService from '../../services/session-service';
import UserFeedbackService from '../../services/user-feedback-service'; import UserFeedbackService from '../../services/user-feedback-service';
import UserSplashService from '../../services/user-splash-service';
interface IChangeUserRequest { interface IChangeUserRequest {
password: string; password: string;
@ -22,6 +23,8 @@ class UserController extends Controller {
private sessionService: SessionService; private sessionService: SessionService;
private userSplashService: UserSplashService;
constructor( constructor(
config: IUnleashConfig, config: IUnleashConfig,
{ {
@ -29,12 +32,14 @@ class UserController extends Controller {
userService, userService,
sessionService, sessionService,
userFeedbackService, userFeedbackService,
userSplashService,
}: Pick< }: Pick<
IUnleashServices, IUnleashServices,
| 'accessService' | 'accessService'
| 'userService' | 'userService'
| 'sessionService' | 'sessionService'
| 'userFeedbackService' | 'userFeedbackService'
| 'userSplashService'
>, >,
) { ) {
super(config); super(config);
@ -42,6 +47,7 @@ class UserController extends Controller {
this.userService = userService; this.userService = userService;
this.sessionService = sessionService; this.sessionService = sessionService;
this.userFeedbackService = userFeedbackService; this.userFeedbackService = userFeedbackService;
this.userSplashService = userSplashService;
this.get('/', this.getUser); this.get('/', this.getUser);
this.post('/change-password', this.updateUserPass); this.post('/change-password', this.updateUserPass);
@ -57,11 +63,15 @@ class UserController extends Controller {
const feedback = await this.userFeedbackService.getAllUserFeedback( const feedback = await this.userFeedbackService.getAllUserFeedback(
user, user,
); );
const splash = await this.userSplashService.getAllUserSplashs(user);
// TODO: remove this line after we remove it from db. // TODO: remove this line after we remove it from db.
delete user.permissions; delete user.permissions;
return res.status(200).json({ user, permissions, feedback }).end(); return res
.status(200)
.json({ user, permissions, feedback, splash })
.end();
} }
async updateUserPass( async updateUserPass(

View File

@ -27,6 +27,7 @@ import FeatureToggleService from './feature-toggle-service';
import EnvironmentService from './environment-service'; import EnvironmentService from './environment-service';
import FeatureTagService from './feature-tag-service'; import FeatureTagService from './feature-tag-service';
import ProjectHealthService from './project-health-service'; import ProjectHealthService from './project-health-service';
import UserSplashService from './user-splash-service';
export const createServices = ( export const createServices = (
stores: IUnleashStores, stores: IUnleashStores,
@ -72,6 +73,7 @@ export const createServices = (
accessService, accessService,
featureToggleServiceV2, featureToggleServiceV2,
); );
const userSplashService = new UserSplashService(stores, config);
return { return {
accessService, accessService,
@ -100,6 +102,7 @@ export const createServices = (
userFeedbackService, userFeedbackService,
featureTagService, featureTagService,
projectHealthService, projectHealthService,
userSplashService,
}; };
}; };

View File

@ -0,0 +1,54 @@
import { Logger } from '../logger';
import { IUnleashStores } from '../types/stores';
import { IUnleashConfig } from '../types/option';
import User from '../types/user';
import {
IUserSplash,
IUserSplashStore,
} from '../types/stores/user-splash-store';
export default class UserSplashService {
private userSplashStore: IUserSplashStore;
private logger: Logger;
constructor(
{ userSplashStore }: Pick<IUnleashStores, 'userSplashStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
) {
this.userSplashStore = userSplashStore;
this.logger = getLogger('services/user-splash-service.js');
}
async getAllUserSplashs(user: User): Promise<Object> {
if (user.isAPI) {
return [];
}
try {
const splashs = (
await this.userSplashStore.getAllUserSplashs(user.id)
).reduce(
(splashObject, splash) => ({
...splashObject,
[splash.splashId]: splash.seen,
}),
{},
);
return splashs;
} catch (err) {
this.logger.error(err);
return {};
}
}
async getSplash(user_id: number, splash_id: string): Promise<IUserSplash> {
return this.userSplashStore.getSplash(user_id, splash_id);
}
async updateSplash(splash: IUserSplash): Promise<IUserSplash> {
return this.userSplashStore.updateSplash(splash);
}
}
module.exports = UserSplashService;

View File

@ -23,6 +23,7 @@ import EnvironmentService from '../services/environment-service';
import FeatureTagService from '../services/feature-tag-service'; import FeatureTagService from '../services/feature-tag-service';
import ProjectHealthService from '../services/project-health-service'; import ProjectHealthService from '../services/project-health-service';
import ClientMetricsServiceV2 from '../services/client-metrics/client-metrics-service-v2'; import ClientMetricsServiceV2 from '../services/client-metrics/client-metrics-service-v2';
import UserSplashService from '../services/user-splash-service';
export interface IUnleashServices { export interface IUnleashServices {
accessService: AccessService; accessService: AccessService;
@ -51,4 +52,5 @@ export interface IUnleashServices {
userFeedbackService: UserFeedbackService; userFeedbackService: UserFeedbackService;
userService: UserService; userService: UserService;
versionService: VersionService; versionService: VersionService;
userSplashService: UserSplashService;
} }

View File

@ -23,6 +23,7 @@ import { IFeatureStrategiesStore } from './stores/feature-strategies-store';
import { IEnvironmentStore } from './stores/environment-store'; import { IEnvironmentStore } from './stores/environment-store';
import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store'; import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store';
import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2'; import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2';
import { IUserSplashStore } from './stores/user-splash-store';
export interface IUnleashStores { export interface IUnleashStores {
accessStore: IAccessStore; accessStore: IAccessStore;
@ -50,4 +51,5 @@ export interface IUnleashStores {
tagTypeStore: ITagTypeStore; tagTypeStore: ITagTypeStore;
userFeedbackStore: IUserFeedbackStore; userFeedbackStore: IUserFeedbackStore;
userStore: IUserStore; userStore: IUserStore;
userSplashStore: IUserSplashStore;
} }

View File

@ -0,0 +1,18 @@
import { Store } from './store';
export interface IUserSplash {
seen: boolean;
splashId: string;
userId: number;
}
export interface IUserSplashKey {
userId: number;
splashId: string;
}
export interface IUserSplashStore extends Store<IUserSplash, IUserSplashKey> {
getAllUserSplashs(userId: number): Promise<IUserSplash[]>;
getSplash(userId: number, splashId: string): Promise<IUserSplash>;
updateSplash(splash: IUserSplash): Promise<IUserSplash>;
}

View File

@ -0,0 +1,25 @@
'use strict';
exports.up = function (db, cb) {
db.runSql(
`
CREATE TABLE IF NOT EXISTS user_splash
(user_id INTEGER NOT NULL references users (id) ON DELETE CASCADE,
splash_id TEXT,
seen BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (user_id, splash_id));
CREATE INDEX user_splash_user_id_idx ON user_splash (user_id);
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
DROP INDEX user_splash_user_id_idx;
DROP TABLE user_splash;
`,
cb,
);
};

View File

@ -0,0 +1,10 @@
exports.up = function (db, cb) {
db.runSql(
`INSERT INTO user_splash(splash_id, user_id, seen) SELECT 'environment', u.id, false FROM users u`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql('DELETE FROM user_splash', cb);
};

View File

@ -0,0 +1,68 @@
import { Application, NextFunction, Request, Response } from 'express';
import { setupAppWithCustomAuth } from '../../helpers/test-helper';
import dbInit from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import { IUnleashConfig } from '../../../../lib/types/option';
import { IUnleashServices } from '../../../../lib/types/services';
let stores;
let db;
let app;
beforeAll(async () => {
db = await dbInit('splash_api_serial', getLogger);
stores = db.stores;
const email = 'custom-user@mail.com';
const preHook = (
application: Application,
config: IUnleashConfig,
{ userService }: IUnleashServices,
) => {
application.use(
'/api/admin/',
async (req: Request, res: Response, next: NextFunction) => {
// @ts-ignore
req.user = await userService.loginUserWithoutPassword(
email,
true,
);
next();
},
);
};
app = await setupAppWithCustomAuth(stores, preHook);
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
test('it updates splash for user', async () => {
expect.assertions(1);
return app.request
.post('/api/admin/splash/environment')
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.seen).toBe(true);
});
});
test('it retrieves splash for user', async () => {
expect.assertions(1);
return app.request
.get('/api/admin/user')
.set('Content-Type', 'application/json')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.splash).toStrictEqual({ environment: true });
});
});

View File

@ -0,0 +1,82 @@
import { IUserSplashStore } from 'lib/types/stores/user-splash-store';
import { IUserStore } from 'lib/types/stores/user-store';
import dbInit from '../helpers/database-init';
import getLogger from '../../fixtures/no-logger';
let stores;
let db;
let userSplashStore: IUserSplashStore;
let userStore: IUserStore;
let currentUser;
beforeAll(async () => {
db = await dbInit('user_splash_store', getLogger);
stores = db.stores;
userSplashStore = stores.userSplashStore;
userStore = stores.userStore;
currentUser = await userStore.upsert({ email: 'me.feedback@mail.com' });
});
afterAll(async () => {
await db.destroy();
});
afterEach(async () => {
await userSplashStore.deleteAll();
});
test('should create userSplash', async () => {
await userSplashStore.updateSplash({
splashId: 'some-id',
userId: currentUser.id,
seen: false,
});
const userSplashs = await userSplashStore.getAllUserSplashs(currentUser.id);
expect(userSplashs).toHaveLength(1);
expect(userSplashs[0].splashId).toBe('some-id');
});
test('should get userSplash', async () => {
await userSplashStore.updateSplash({
splashId: 'some-id',
userId: currentUser.id,
seen: false,
});
const userSplash = await userSplashStore.getSplash(
currentUser.id,
'some-id',
);
expect(userSplash.splashId).toBe('some-id');
});
test('should exists', async () => {
await userSplashStore.updateSplash({
splashId: 'some-id-3',
userId: currentUser.id,
seen: false,
});
const exists = await userSplashStore.exists({
userId: currentUser.id,
splashId: 'some-id-3',
});
expect(exists).toBe(true);
});
test('should not exists', async () => {
const exists = await userSplashStore.exists({
userId: currentUser.id,
splashId: 'some-id-not-here',
});
expect(exists).toBe(false);
});
test('should get all userSplashs', async () => {
await userSplashStore.updateSplash({
splashId: 'some-id-2',
userId: currentUser.id,
seen: false,
});
const userSplashs = await userSplashStore.getAll();
expect(userSplashs).toHaveLength(1);
expect(userSplashs[0].splashId).toBe('some-id-2');
});

View File

@ -0,0 +1,50 @@
import {
IUserSplashKey,
IUserSplash,
IUserSplashStore,
} from '../../lib/types/stores/user-splash-store';
export default class FakeUserSplashStore implements IUserSplashStore {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getAllUserSplashs(userId: number): Promise<IUserSplash[]> {
return Promise.resolve([]);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getSplash(userId: number, splashId: string): Promise<IUserSplash> {
return Promise.resolve(undefined);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
updateSplash(splash: IUserSplash): Promise<IUserSplash> {
return Promise.resolve(undefined);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
exists(key: IUserSplashKey): Promise<boolean> {
return Promise.resolve(false);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
get(key: IUserSplashKey): Promise<IUserSplash> {
return Promise.resolve(undefined);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getAll(): Promise<IUserSplash[]> {
return Promise.resolve([]);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
delete(key: IUserSplashKey): Promise<void> {
return Promise.resolve(undefined);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
deleteAll(): Promise<void> {
return Promise.resolve(undefined);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
destroy(): void {}
}

View File

@ -24,6 +24,7 @@ import FakeFeatureTypeStore from './fake-feature-type-store';
import FakeResetTokenStore from './fake-reset-token-store'; import FakeResetTokenStore from './fake-reset-token-store';
import FakeFeatureToggleClientStore from './fake-feature-toggle-client-store'; import FakeFeatureToggleClientStore from './fake-feature-toggle-client-store';
import FakeClientMetricsStoreV2 from './fake-client-metrics-store-v2'; import FakeClientMetricsStoreV2 from './fake-client-metrics-store-v2';
import FakeUserSplashStore from './fake-user-splash-store';
const createStores: () => IUnleashStores = () => { const createStores: () => IUnleashStores = () => {
const db = { const db = {
@ -59,6 +60,7 @@ const createStores: () => IUnleashStores = () => {
featureTypeStore: new FakeFeatureTypeStore(), featureTypeStore: new FakeFeatureTypeStore(),
resetTokenStore: new FakeResetTokenStore(), resetTokenStore: new FakeResetTokenStore(),
sessionStore: new FakeSessionStore(), sessionStore: new FakeSessionStore(),
userSplashStore: new FakeUserSplashStore(),
}; };
}; };

View File

@ -57,11 +57,13 @@ docker run -p 4242:4242 \
-e DATABASE_HOST=postgres -e DATABASE_NAME=unleash \ -e DATABASE_HOST=postgres -e DATABASE_NAME=unleash \
-e DATABASE_USERNAME=unleash_user -e DATABASE_PASSWORD=some_password \ -e DATABASE_USERNAME=unleash_user -e DATABASE_PASSWORD=some_password \
-e DATABASE_SSL=false \ -e DATABASE_SSL=false \
--network unleash unleashorg/unleash-server --network unleash --pull=always unleashorg/unleash-server
``` ```
### Option 2 - use Docker-compose {#option-two---use-docker-compose} ### Option 2 - use Docker-compose {#option-two---use-docker-compose}
**Steps:** **Steps:**
1. Clone the [unleash-docker](https://github.com/Unleash/unleash-docker) repository. 1. Clone the [unleash-docker](https://github.com/Unleash/unleash-docker) repository.
2. Run `docker-compose build` in repository root folder. 2. Run `docker-compose build` in repository root folder.
3. Run `docker-compose up` in repository root folder. 3. Run `docker-compose up` in repository root folder.
@ -95,7 +97,7 @@ docker run -p 4242:4242 \
port: 4242, port: 4242,
}, },
}) })
.then(unleash => { .then((unleash) => {
console.log( console.log(
`Unleash started on http://localhost:${unleash.app.get('port')}`, `Unleash started on http://localhost:${unleash.app.get('port')}`,
); );

View File

@ -198,27 +198,42 @@ unleash.on('synchronized', () => {
## I want to run Unleash locally ## I want to run Unleash locally
### Run Unleash with Docker ### Run Unleash with Docker {#run-unleash-with-docker}
The easiest way to run unleash locally is using [docker](https://www.docker.com/). The easiest way to run unleash locally is using [docker](https://www.docker.com/).
:::tip
Each container that runs in your local Docker instance must have a unique name. If you've run these commands before, you can either start the containers again (`docker start ${CONTAINER_NAME}`) or remove them (`docker rm ${CONTAINER_NAME}`) and run the commands again.
:::
1. Create a network by running `docker network create unleash` 1. Create a network by running `docker network create unleash`
2. Start a postgres database: 2. Start a postgres database:
```sh ```sh
docker run -e POSTGRES_PASSWORD=some_password \ docker run \
-e POSTGRES_USER=unleash_user -e POSTGRES_DB=unleash \ -e POSTGRES_USER=unleash_user \
--network unleash --name postgres postgres -e POSTGRES_PASSWORD=some_password \
-e POSTGRES_DB=unleash \
--network unleash \
--name postgres \
postgres
``` ```
3. Start Unleash via docker: 3. Start Unleash via docker:
```sh ```sh
docker run -p 4242:4242 \ docker run \
-e DATABASE_HOST=postgres -e DATABASE_NAME=unleash \ -p 4242:4242 \
-e DATABASE_USERNAME=unleash_user -e DATABASE_PASSWORD=some_password \ -e DATABASE_HOST=postgres \
-e DATABASE_NAME=unleash \
-e DATABASE_USERNAME=unleash_user \
-e DATABASE_PASSWORD=some_password \
-e DATABASE_SSL=false \ -e DATABASE_SSL=false \
--network unleash unleashorg/unleash-server --network unleash \
--name unleash \
--pull=always unleashorg/unleash-server
``` ```
[Click here to see all options to get started locally.](deploy/getting-started.md) [Click here to see all options to get started locally.](deploy/getting-started.md)
@ -232,6 +247,58 @@ username: admin
password: unleash4all password: unleash4all
``` ```
### Run Unleash and the Unleash proxy with Docker
Follow steps outlined in the [Run Unleash with Docker](#run-unleash-with-docker) section to get the Unleash instance up and running. Once you have done that you need to first get an API key from your Unleash instance and then use that API key when starting the Unleash proxy.
1. Get an API key.
To get an API key, access your Unleash instance in a web browser. First, navigate to the API access screen.
[![The Unleash UI showing a dropdown menu under the Configure menu
entry. The dropdown menu's API Access option is highlighted and
you're told to navigate there.]](/img/api_access_navigation.png 'Navigate to the API access page.')
Next, create an API key with these details
- **name:** proxy-key (this can be whatever you want)
- **token type:** client
- **project:** all
- **environment:** select your preferred environment (this option is only available in Unleash 4.3 and later)
Copy the API key to your clipboard. You'll need it in the next step.
:::note
Depending on whether you have the environments feature enabled or not, the API key will look a little different. If you don't have environments enabled, it'll just be a 64 character long hexadecimal string (for instance `be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178`). If you do have environments enabled, the key will be prefixed with the project and the environment that the key is valid for. It'll use the format `<project>:<environment>.<key>`, e.g. `demo-app:production.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178`.
Regardless of which format your string uses, do not modify it.
:::
2. Start the Unleash proxy
Start a container with the Unleash proxy by running the following command. Replace `${API_KEY}` with the key you created in the following step.
```sh
docker run \
-e UNLEASH_PROXY_SECRETS=some-secret \
-e UNLEASH_URL='http://unleash:4242/api/' \
-e UNLEASH_API_TOKEN='${API_KEY}' \
-p 3000:3000 \
--network unleash \
--name unleash-proxy \
--pull=always unleashorg/unleash-proxy
```
3. Test the proxy
To make sure the proxy is running successfully, you can test it by running the following command:
```curl
curl http://localhost:3000/proxy -H "Authorization: some-secret"
```
### Create your first toggle ### Create your first toggle
In order to create a toggle through the UI, [you can follow this guide](create-feature-toggle.md). Once you have created your feature toggle, you are ready to connect your application using an SDK. In order to create a toggle through the UI, [you can follow this guide](create-feature-toggle.md). Once you have created your feature toggle, you are ready to connect your application using an SDK.

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -2335,29 +2335,6 @@ errorhandler@^1.5.1:
accepts "~1.3.7" accepts "~1.3.7"
escape-html "~1.0.3" escape-html "~1.0.3"
es-abstract@^1.18.2:
version "1.18.5"
resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz"
integrity sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==
dependencies:
call-bind "^1.0.2"
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
get-intrinsic "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.2"
internal-slot "^1.0.3"
is-callable "^1.2.3"
is-negative-zero "^2.0.1"
is-regex "^1.1.3"
is-string "^1.0.6"
object-inspect "^1.11.0"
object-keys "^1.1.1"
object.assign "^4.1.2"
string.prototype.trimend "^1.0.4"
string.prototype.trimstart "^1.0.4"
unbox-primitive "^1.0.1"
es-abstract@^1.19.0, es-abstract@^1.19.1: es-abstract@^1.19.0, es-abstract@^1.19.1:
version "1.19.1" version "1.19.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3"
@ -2466,7 +2443,7 @@ escodegen@^2.0.0:
optionalDependencies: optionalDependencies:
source-map "~0.6.1" source-map "~0.6.1"
eslint-config-airbnb-base@15.0.0: eslint-config-airbnb-base@15.0.0, eslint-config-airbnb-base@^15.0.0:
version "15.0.0" version "15.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz#6b09add90ac79c2f8d723a2580e07f3925afd236" resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz#6b09add90ac79c2f8d723a2580e07f3925afd236"
integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig== integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==
@ -2476,21 +2453,12 @@ eslint-config-airbnb-base@15.0.0:
object.entries "^1.1.5" object.entries "^1.1.5"
semver "^6.3.0" semver "^6.3.0"
eslint-config-airbnb-base@^14.2.1: eslint-config-airbnb-typescript@16.0.0:
version "14.2.1" version "16.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e" resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-16.0.0.tgz#75007e27d5a7fb75530721f48197817c1d2ad4d1"
integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA== integrity sha512-qDOyD0YYZo5Us1YvOnWig2Ly/+IlQKmMZpnqKnJgVtHdK8SkjaSyVBHKbD41dEaQxk8vRVGBC94PuR2ceSwbLQ==
dependencies: dependencies:
confusing-browser-globals "^1.0.10" eslint-config-airbnb-base "^15.0.0"
object.assign "^4.1.2"
object.entries "^1.1.2"
eslint-config-airbnb-typescript@15.0.0:
version "15.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-15.0.0.tgz#c88007b3cca5dd0f47125420ca5e8f6efac418fd"
integrity sha512-DTWGwqytbTnB8kSKtmkrGkRf3xwTs2l15shSH0w/3Img47AQwCCrIA/ON/Uj0XXBxP31LHyEItPXeuH3mqCNLA==
dependencies:
eslint-config-airbnb-base "^14.2.1"
eslint-config-prettier@8.3.0: eslint-config-prettier@8.3.0:
version "8.3.0" version "8.3.0"
@ -3635,7 +3603,7 @@ is-buffer@^1.1.5:
resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-callable@^1.1.4, is-callable@^1.2.3, is-callable@^1.2.4: is-callable@^1.1.4, is-callable@^1.2.4:
version "1.2.4" version "1.2.4"
resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz"
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
@ -3824,7 +3792,7 @@ is-promise@^2.2.2:
resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz" resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz"
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
is-regex@^1.1.3, is-regex@^1.1.4: is-regex@^1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz"
integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
@ -3854,7 +3822,7 @@ is-stream@^2.0.0:
resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
is-string@^1.0.5, is-string@^1.0.6, is-string@^1.0.7: is-string@^1.0.5, is-string@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz"
integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
@ -5274,15 +5242,6 @@ object.defaults@^1.1.0:
for-own "^1.0.0" for-own "^1.0.0"
isobject "^3.0.0" isobject "^3.0.0"
object.entries@^1.1.2:
version "1.1.4"
resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz"
integrity sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
es-abstract "^1.18.2"
object.entries@^1.1.5: object.entries@^1.1.5:
version "1.1.5" version "1.1.5"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861"