1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +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]);
}
async disableAllExcept(environments: string[]): Promise<void> {
async disable(environments: IEnvironment[]): Promise<void> {
await this.db(TABLE)
.update({
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)
.update({
enabled: true,
})
.whereIn('name', environments);
.whereIn(
'name',
environments.map((env) => env.name),
);
}
async delete(name: string): Promise<void> {

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

@ -95,25 +95,87 @@ export default class EnvironmentService {
}
async overrideEnabledProjects(
environmentsToEnable: string[],
environmentNamesToEnable: string[],
): Promise<void> {
if (environmentsToEnable.length === 0) {
if (environmentNamesToEnable.length === 0) {
return Promise.resolve();
}
const environmentsExist = await Promise.all(
environmentsToEnable.map((env) =>
this.environmentStore.exists(env),
),
const allEnvironments = await this.environmentStore.getAll();
const existingEnvironmentsToEnable = allEnvironments.filter((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",
);
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(
@ -125,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

@ -16,6 +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>;
disableAllExcept(environments: string[]): Promise<void>;
enable(environments: 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

@ -150,7 +150,7 @@ test('Setting an override disables all other envs', async () => {
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(enabledEnvName, false);
@ -165,7 +165,6 @@ test('Setting an override disables all other envs', async () => {
.filter((x) => x.name != enabledEnvName)
.map((env) => env.enabled);
console.log(allOtherEnvironments);
expect(targetedEnvironment.enabled).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);
});
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[] = [];
disableAllExcept(environments: string[]): Promise<void> {
disable(environments: IEnvironment[]): Promise<void> {
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();
}
enable(environments: string[]): Promise<void> {
enable(environments: IEnvironment[]): Promise<void> {
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();
}

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