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:
parent
c3b064adfc
commit
8410a8e3ac
@ -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> {
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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[]>;
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
|
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[] = [];
|
||||
|
||||
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();
|
||||
}
|
||||
|
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';
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user