1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-27 00:19:39 +01:00

Merge pull request from Unleash/enabled-environments-override

Add enabled environments override flag
This commit is contained in:
sighphyre 2022-03-16 14:45:55 +02:00 committed by GitHub
commit 8f60dd6958
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 360 additions and 8 deletions

View File

@ -41,6 +41,7 @@ Object {
},
"enableOAS": false,
"enterpriseVersion": undefined,
"environmentEnableOverrides": Array [],
"eventBus": EventEmitter {
"_events": Object {},
"_eventsCount": 0,

View File

@ -224,3 +224,47 @@ test('should handle cases where no env var specified for tokens', async () => {
expect(config.authentication.initApiTokens).toHaveLength(1);
});
test('should load environment overrides from env var', async () => {
process.env.ENABLED_ENVIRONMENTS = 'default,production';
const config = createConfig({
db: {
host: 'localhost',
port: 4242,
user: 'unleash',
password: 'password',
database: 'unleash_db',
},
server: {
port: 4242,
},
authentication: {
initApiTokens: [],
},
});
expect(config.environmentEnableOverrides).toHaveLength(2);
expect(config.environmentEnableOverrides).toContain('production');
delete process.env.ENABLED_ENVIRONMENTS;
});
test('should yield an empty list when no environment overrides are specified', async () => {
const config = createConfig({
db: {
host: 'localhost',
port: 4242,
user: 'unleash',
password: 'password',
database: 'unleash_db',
},
server: {
port: 4242,
},
authentication: {
initApiTokens: [],
},
});
expect(config.environmentEnableOverrides).toStrictEqual([]);
});

View File

@ -217,6 +217,14 @@ const loadInitApiTokens = () => {
];
};
const loadEnvironmentEnableOverrides = () => {
const environmentsString = process.env.ENABLED_ENVIRONMENTS;
if (environmentsString) {
return environmentsString.split(',');
}
return [];
};
export function createConfig(options: IUnleashOptions): IUnleashConfig {
let extraDbOptions = {};
@ -275,6 +283,8 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
{ initApiTokens: initApiTokens },
]);
const environmentEnableOverrides = loadEnvironmentEnableOverrides();
const importSetting: IImportOption = mergeAll([
defaultImport,
options.import,
@ -323,6 +333,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
eventHook: options.eventHook,
enterpriseVersion: options.enterpriseVersion,
eventBus: new EventEmitter(),
environmentEnableOverrides,
};
}

View File

@ -163,6 +163,28 @@ export default class EnvironmentStore implements IEnvironmentStore {
return mapRow(row[0]);
}
async disable(environments: IEnvironment[]): Promise<void> {
await this.db(TABLE)
.update({
enabled: false,
})
.whereIn(
'name',
environments.map((env) => env.name),
);
}
async enable(environments: IEnvironment[]): Promise<void> {
await this.db(TABLE)
.update({
enabled: true,
})
.whereIn(
'name',
environments.map((env) => env.name),
);
}
async delete(name: string): Promise<void> {
await this.db(TABLE).where({ name, protected: false }).del();
}

View File

@ -24,6 +24,11 @@ const COLUMNS = [
];
const TABLE = 'projects';
export interface IEnvironmentProjectLink {
environmentName: string;
projectId: string;
}
class ProjectStore implements IProjectStore {
private db: Knex;
@ -197,6 +202,15 @@ class ProjectStore implements IProjectStore {
}
}
async getProjectLinksForEnvironments(
environments: string[],
): Promise<IEnvironmentProjectLink[]> {
let rows = await this.db('project_environments')
.select(['project_id', 'environment_name'])
.whereIn('environment_name', environments);
return rows.map(this.mapLinkRow);
}
async deleteEnvironmentForProject(
id: string,
environment: string,
@ -251,6 +265,14 @@ class ProjectStore implements IProjectStore {
.then((res) => Number(res[0].count));
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
mapLinkRow(row): IEnvironmentProjectLink {
return {
environmentName: row.environment_name,
projectId: row.project_id,
};
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
mapRow(row): IProject {
if (!row) {

View File

@ -87,6 +87,12 @@ async function createApp(
});
}
if (config.environmentEnableOverrides?.length > 0) {
await services.environmentService.overrideEnabledProjects(
config.environmentEnableOverrides,
);
}
return new Promise((resolve, reject) => {
if (startApp) {
const server = stoppable(

View File

@ -94,6 +94,90 @@ export default class EnvironmentService {
}
}
async overrideEnabledProjects(
environmentNamesToEnable: string[],
): Promise<void> {
if (environmentNamesToEnable.length === 0) {
return Promise.resolve();
}
const allEnvironments = await this.environmentStore.getAll();
const existingEnvironmentsToEnable = allEnvironments.filter((env) =>
environmentNamesToEnable.includes(env.name),
);
if (
existingEnvironmentsToEnable.length !==
environmentNamesToEnable.length
) {
this.logger.warn(
"Found environment enabled overrides but some of the specified environments don't exist, no overrides will be executed",
);
return Promise.resolve();
}
const environmentsNotAlreadyEnabled =
existingEnvironmentsToEnable.filter((env) => env.enabled == false);
const environmentsToDisable = allEnvironments.filter((env) => {
return (
!environmentNamesToEnable.includes(env.name) &&
env.enabled == true
);
});
await this.environmentStore.disable(environmentsToDisable);
await this.environmentStore.enable(environmentsNotAlreadyEnabled);
await this.remapProjectsLinks(
environmentsToDisable,
environmentsNotAlreadyEnabled,
);
}
private async remapProjectsLinks(
toDisable: IEnvironment[],
toEnable: IEnvironment[],
) {
const projectLinks =
await this.projectStore.getProjectLinksForEnvironments(
toDisable.map((env) => env.name),
);
const unlinkTasks = projectLinks.map((link) => {
return this.forceRemoveEnvironmentFromProject(
link.environmentName,
link.projectId,
);
});
await Promise.all(unlinkTasks.flat());
const uniqueProjects = [
...new Set(projectLinks.map((link) => link.projectId)),
];
let linkTasks = uniqueProjects.map((project) => {
return toEnable.map((enabledEnv) => {
return this.addEnvironmentToProject(enabledEnv.name, project);
});
});
await Promise.all(linkTasks.flat());
}
async forceRemoveEnvironmentFromProject(
environment: string,
projectId: string,
): Promise<void> {
await this.featureEnvironmentStore.disconnectFeatures(
environment,
projectId,
);
await this.featureEnvironmentStore.disconnectProject(
environment,
projectId,
);
}
async removeEnvironmentFromProject(
environment: string,
projectId: string,
@ -103,11 +187,7 @@ export default class EnvironmentService {
);
if (projectEnvs.length > 1) {
await this.featureEnvironmentStore.disconnectFeatures(
environment,
projectId,
);
await this.featureEnvironmentStore.disconnectProject(
await this.forceRemoveEnvironmentFromProject(
environment,
projectId,
);

View File

@ -158,4 +158,5 @@ export interface IUnleashConfig {
enterpriseVersion?: string;
eventBus: EventEmitter;
disableLegacyFeaturesApi?: boolean;
environmentEnableOverrides?: string[];
}

View File

@ -16,4 +16,6 @@ export interface IEnvironmentStore extends Store<IEnvironment, string> {
updateSortOrder(id: string, value: number): Promise<void>;
importEnvironments(environments: IEnvironment[]): Promise<IEnvironment[]>;
delete(name: string): Promise<void>;
disable(environments: IEnvironment[]): Promise<void>;
enable(environments: IEnvironment[]): Promise<void>;
}

View File

@ -1,3 +1,4 @@
import { IEnvironmentProjectLink } from 'lib/db/project-store';
import { IProject, IProjectWithCount } from '../model';
import { Store } from './store';
@ -35,4 +36,7 @@ export interface IProjectStore extends Store<IProject, string> {
getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>;
count(): Promise<number>;
getAll(query?: IProjectQuery): Promise<IProject[]>;
getProjectLinksForEnvironments(
environments: string[],
): Promise<IEnvironmentProjectLink[]>;
}

View File

@ -136,3 +136,134 @@ test('Trying to get an environment that does not exist throws NotFoundError', as
new NotFoundError(`Could not find environment with name: ${envName}`),
);
});
test('Setting an override disables all other envs', async () => {
const enabledEnvName = 'should-get-enabled';
const disabledEnvName = 'should-get-disabled';
await db.stores.environmentStore.create({
name: disabledEnvName,
type: 'production',
});
await db.stores.environmentStore.create({
name: enabledEnvName,
type: 'production',
});
//Set these to the wrong state so we can assert that overriding them flips their state
await service.toggleEnvironment(disabledEnvName, true);
await service.toggleEnvironment(enabledEnvName, false);
await service.overrideEnabledProjects([enabledEnvName]);
const environments = await service.getAll();
const targetedEnvironment = environments.find(
(env) => env.name == enabledEnvName,
);
const allOtherEnvironments = environments
.filter((x) => x.name != enabledEnvName)
.map((env) => env.enabled);
expect(targetedEnvironment.enabled).toBe(true);
expect(allOtherEnvironments.every((x) => x === false)).toBe(true);
});
test('Passing an empty override does nothing', async () => {
const enabledEnvName = 'should-be-enabled';
await db.stores.environmentStore.create({
name: enabledEnvName,
type: 'production',
});
await service.toggleEnvironment(enabledEnvName, true);
await service.overrideEnabledProjects([]);
const environments = await service.getAll();
const targetedEnvironment = environments.find(
(env) => env.name == enabledEnvName,
);
expect(targetedEnvironment.enabled).toBe(true);
});
test('When given overrides should remap projects to override environments', async () => {
const enabledEnvName = 'enabled';
const ignoredEnvName = 'ignored';
const disabledEnvName = 'disabled';
const toggleName = 'test-toggle';
await db.stores.environmentStore.create({
name: enabledEnvName,
type: 'production',
});
await db.stores.environmentStore.create({
name: ignoredEnvName,
type: 'production',
});
await db.stores.environmentStore.create({
name: disabledEnvName,
type: 'production',
});
await service.toggleEnvironment(disabledEnvName, true);
await service.toggleEnvironment(ignoredEnvName, true);
await service.toggleEnvironment(enabledEnvName, false);
await stores.featureToggleStore.create('default', {
name: toggleName,
type: 'release',
description: '',
stale: false,
});
await service.addEnvironmentToProject(disabledEnvName, 'default');
await service.overrideEnabledProjects([enabledEnvName]);
const projects = await stores.projectStore.getEnvironmentsForProject(
'default',
);
expect(projects).toContain('enabled');
expect(projects).not.toContain('default');
});
test('Override works correctly when enabling default and disabling prod and dev', async () => {
const defaultEnvironment = 'default';
const prodEnvironment = 'production';
const devEnvironment = 'development';
await db.stores.environmentStore.create({
name: prodEnvironment,
type: 'production',
});
await db.stores.environmentStore.create({
name: devEnvironment,
type: 'development',
});
await service.toggleEnvironment(prodEnvironment, true);
await service.toggleEnvironment(devEnvironment, true);
await service.overrideEnabledProjects([defaultEnvironment]);
const environments = await service.getAll();
const targetedEnvironment = environments.find(
(env) => env.name == defaultEnvironment,
);
const allOtherEnvironments = environments
.filter((x) => x.name != defaultEnvironment)
.map((env) => env.enabled);
const envNames = environments.map((x) => x.name);
expect(envNames).toContain('production');
expect(envNames).toContain('development');
expect(targetedEnvironment.enabled).toBe(true);
expect(allOtherEnvironments.every((x) => x === false)).toBe(true);
});

View File

@ -10,6 +10,22 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
environments: IEnvironment[] = [];
disable(environments: IEnvironment[]): Promise<void> {
for (let env of this.environments) {
if (environments.map((e) => e.name).includes(env.name))
env.enabled = false;
}
return Promise.resolve();
}
enable(environments: IEnvironment[]): Promise<void> {
for (let env of this.environments) {
if (environments.map((e) => e.name).includes(env.name))
env.enabled = true;
}
return Promise.resolve();
}
async getAll(): Promise<IEnvironment[]> {
return this.environments;
}

View File

@ -5,15 +5,23 @@ import {
} from '../../lib/types/stores/project-store';
import { IProject, IProjectWithCount } from '../../lib/types/model';
import NotFoundError from '../../lib/error/notfound-error';
import { IEnvironmentProjectLink } from 'lib/db/project-store';
export default class FakeProjectStore implements IProjectStore {
projects: IProject[] = [];
projectEnvironment: Map<string, Set<string>> = new Map();
getEnvironmentsForProject(): Promise<string[]> {
throw new Error('Method not implemented.');
}
projects: IProject[] = [];
projectEnvironment: Map<string, Set<string>> = new Map();
getProjectLinksForEnvironments(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
environments: string[],
): Promise<IEnvironmentProjectLink[]> {
throw new Error('Method not implemented.');
}
async addEnvironmentToProject(
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -131,6 +131,10 @@ unleash.start(unleashOptions);
- **versionCheck** - the object deciding where to check for latest version
- `url` - The url to check version (Defaults to `https://version.unleash.run`) - Overridable with (`UNLEASH_VERSION_URL`)
- `enable` - Whether version checking is enabled (defaults to true) - Overridable with (`CHECK_VERSION`) (if anything other than `true`, does not check)
- **environmentEnableOverrides** - A list of environment names to force enable at startup. This is feature should be
used with caution. When passed a list, this will enable each environment in that list and disable all other environments. You can't use this to disable all environments, passing an empty list will do nothing. If one of the given environments is not already enabled on startup then it will also enable projects and toggles for that environment. Note that if one of the passed environments doesn't already exist this will do nothing aside from log a warning.
You can also set the environment variable `ENABLED_ENVIRONMENTS` to a comma delimited string of environment names to override environments.
### Disabling Auto-Start {#disabling-auto-start}