1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01:00

feat: enabled environments override now also moves projects and toggles to new environments

This commit is contained in:
sighphyre 2022-03-11 15:52:13 +02:00
parent c3b064adfc
commit 8410a8e3ac
8 changed files with 174 additions and 31 deletions

View File

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

View File

@ -24,6 +24,11 @@ const COLUMNS = [
]; ];
const TABLE = 'projects'; const TABLE = 'projects';
export interface IEnvironmentProjectLink {
environmentName: string;
projectId: string;
}
class ProjectStore implements IProjectStore { class ProjectStore implements IProjectStore {
private db: Knex; 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( async deleteEnvironmentForProject(
id: string, id: string,
environment: string, environment: string,
@ -251,6 +265,14 @@ class ProjectStore implements IProjectStore {
.then((res) => Number(res[0].count)); .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 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
mapRow(row): IProject { mapRow(row): IProject {
if (!row) { if (!row) {

View File

@ -95,25 +95,87 @@ export default class EnvironmentService {
} }
async overrideEnabledProjects( async overrideEnabledProjects(
environmentsToEnable: string[], environmentNamesToEnable: string[],
): Promise<void> { ): Promise<void> {
if (environmentsToEnable.length === 0) { if (environmentNamesToEnable.length === 0) {
return Promise.resolve(); return Promise.resolve();
} }
const environmentsExist = await Promise.all( const allEnvironments = await this.environmentStore.getAll();
environmentsToEnable.map((env) => const existingEnvironmentsToEnable = allEnvironments.filter((env) =>
this.environmentStore.exists(env), environmentNamesToEnable.includes(env.name),
),
); );
if (!environmentsExist.every((exists) => exists)) {
this.logger.error( 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", "Found environment enabled overrides but some of the specified environments don't exist, no overrides will be executed",
); );
return; return Promise.resolve();
} }
await this.environmentStore.disableAllExcept(environmentsToEnable);
await this.environmentStore.enable(environmentsToEnable); 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( async removeEnvironmentFromProject(
@ -125,11 +187,7 @@ export default class EnvironmentService {
); );
if (projectEnvs.length > 1) { if (projectEnvs.length > 1) {
await this.featureEnvironmentStore.disconnectFeatures( await this.forceRemoveEnvironmentFromProject(
environment,
projectId,
);
await this.featureEnvironmentStore.disconnectProject(
environment, environment,
projectId, projectId,
); );

View File

@ -16,6 +16,6 @@ export interface IEnvironmentStore extends Store<IEnvironment, string> {
updateSortOrder(id: string, value: number): Promise<void>; updateSortOrder(id: string, value: number): Promise<void>;
importEnvironments(environments: IEnvironment[]): Promise<IEnvironment[]>; importEnvironments(environments: IEnvironment[]): Promise<IEnvironment[]>;
delete(name: string): Promise<void>; delete(name: string): Promise<void>;
disableAllExcept(environments: string[]): Promise<void>; disable(environments: IEnvironment[]): Promise<void>;
enable(environments: string[]): 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 { IProject, IProjectWithCount } from '../model';
import { Store } from './store'; import { Store } from './store';
@ -35,4 +36,7 @@ export interface IProjectStore extends Store<IProject, string> {
getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>; getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>;
count(): Promise<number>; count(): Promise<number>;
getAll(query?: IProjectQuery): Promise<IProject[]>; getAll(query?: IProjectQuery): Promise<IProject[]>;
getProjectLinksForEnvironments(
environments: string[],
): Promise<IEnvironmentProjectLink[]>;
} }

View File

@ -150,7 +150,7 @@ test('Setting an override disables all other envs', async () => {
type: 'production', type: 'production',
}); });
//Set these to the wrong state so we can assert that overriding them flips //Set these to the wrong state so we can assert that overriding them flips their state
await service.toggleEnvironment(disabledEnvName, true); await service.toggleEnvironment(disabledEnvName, true);
await service.toggleEnvironment(enabledEnvName, false); await service.toggleEnvironment(enabledEnvName, false);
@ -165,7 +165,6 @@ test('Setting an override disables all other envs', async () => {
.filter((x) => x.name != enabledEnvName) .filter((x) => x.name != enabledEnvName)
.map((env) => env.enabled); .map((env) => env.enabled);
console.log(allOtherEnvironments);
expect(targetedEnvironment.enabled).toBe(true); expect(targetedEnvironment.enabled).toBe(true);
expect(allOtherEnvironments.every((x) => x === false)).toBe(true); expect(allOtherEnvironments.every((x) => x === false)).toBe(true);
}); });
@ -189,3 +188,47 @@ test('Passing an empty override does nothing', async () => {
expect(targetedEnvironment.enabled).toBe(true); 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');
});

View File

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

View File

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