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:
parent
c3b064adfc
commit
8410a8e3ac
@ -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> {
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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[]>;
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
10
src/test/fixtures/fake-environment-store.ts
vendored
10
src/test/fixtures/fake-environment-store.ts
vendored
@ -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();
|
||||||
}
|
}
|
||||||
|
14
src/test/fixtures/fake-project-store.ts
vendored
14
src/test/fixtures/fake-project-store.ts
vendored
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user