Skip to content

Commit

Permalink
Merge pull request #1431 from Unleash/enabled-environments-override
Browse files Browse the repository at this point in the history
Add enabled environments override flag
  • Loading branch information
sighphyre committed Mar 16, 2022
2 parents df86afc + ba07080 commit 8f60dd6
Show file tree
Hide file tree
Showing 14 changed files with 360 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/lib/__snapshots__/create-config.test.ts.snap
Expand Up @@ -41,6 +41,7 @@ Object {
},
"enableOAS": false,
"enterpriseVersion": undefined,
"environmentEnableOverrides": Array [],
"eventBus": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
Expand Down
44 changes: 44 additions & 0 deletions src/lib/create-config.test.ts
Expand Up @@ -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([]);
});
11 changes: 11 additions & 0 deletions src/lib/create-config.ts
Expand Up @@ -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 = {};

Expand Down Expand Up @@ -275,6 +283,8 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
{ initApiTokens: initApiTokens },
]);

const environmentEnableOverrides = loadEnvironmentEnableOverrides();

const importSetting: IImportOption = mergeAll([
defaultImport,
options.import,
Expand Down Expand Up @@ -323,6 +333,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
eventHook: options.eventHook,
enterpriseVersion: options.enterpriseVersion,
eventBus: new EventEmitter(),
environmentEnableOverrides,
};
}

Expand Down
22 changes: 22 additions & 0 deletions src/lib/db/environment-store.ts
Expand Up @@ -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();
}
Expand Down
22 changes: 22 additions & 0 deletions src/lib/db/project-store.ts
Expand Up @@ -24,6 +24,11 @@ const COLUMNS = [
];
const TABLE = 'projects';

export interface IEnvironmentProjectLink {
environmentName: string;
projectId: string;
}

class ProjectStore implements IProjectStore {
private db: Knex;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/lib/server-impl.ts
Expand Up @@ -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(
Expand Down
90 changes: 85 additions & 5 deletions src/lib/services/environment-service.ts
Expand Up @@ -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,
Expand All @@ -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,
);
Expand Down
1 change: 1 addition & 0 deletions src/lib/types/option.ts
Expand Up @@ -158,4 +158,5 @@ export interface IUnleashConfig {
enterpriseVersion?: string;
eventBus: EventEmitter;
disableLegacyFeaturesApi?: boolean;
environmentEnableOverrides?: string[];
}
2 changes: 2 additions & 0 deletions src/lib/types/stores/environment-store.ts
Expand Up @@ -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>;
}
4 changes: 4 additions & 0 deletions src/lib/types/stores/project-store.ts
@@ -1,3 +1,4 @@
import { IEnvironmentProjectLink } from 'lib/db/project-store';
import { IProject, IProjectWithCount } from '../model';
import { Store } from './store';

Expand Down Expand Up @@ -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[]>;
}

0 comments on commit 8f60dd6

Please sign in to comment.