mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-15 17:50:48 +02:00
Import of feature still showing env on feature, when environment is disabled on project (#2209)
* Import state test * Update importer Co-authored-by: sjaanus <sellinjaanus@gmail.com>
This commit is contained in:
parent
8916de76be
commit
8618cec832
@ -2,7 +2,7 @@ import { Knex } from 'knex';
|
|||||||
import { Logger, LogProvider } from '../logger';
|
import { Logger, LogProvider } from '../logger';
|
||||||
|
|
||||||
import NotFoundError from '../error/notfound-error';
|
import NotFoundError from '../error/notfound-error';
|
||||||
import { IProject, IProjectWithCount } from '../types/model';
|
import { IEnvironment, IProject, IProjectWithCount } from '../types/model';
|
||||||
import {
|
import {
|
||||||
IProjectHealthUpdate,
|
IProjectHealthUpdate,
|
||||||
IProjectInsert,
|
IProjectInsert,
|
||||||
@ -169,7 +169,10 @@ class ProjectStore implements IProjectStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async importProjects(projects: IProjectInsert[]): Promise<IProject[]> {
|
async importProjects(
|
||||||
|
projects: IProjectInsert[],
|
||||||
|
environments?: IEnvironment[],
|
||||||
|
): Promise<IProject[]> {
|
||||||
const rows = await this.db(TABLE)
|
const rows = await this.db(TABLE)
|
||||||
.insert(projects.map(this.fieldToRow))
|
.insert(projects.map(this.fieldToRow))
|
||||||
.returning(COLUMNS)
|
.returning(COLUMNS)
|
||||||
@ -177,6 +180,13 @@ class ProjectStore implements IProjectStore {
|
|||||||
.ignore();
|
.ignore();
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
await this.addDefaultEnvironment(rows);
|
await this.addDefaultEnvironment(rows);
|
||||||
|
environments
|
||||||
|
?.filter((env) => env.name !== DEFAULT_ENV)
|
||||||
|
.forEach((env) => {
|
||||||
|
projects.forEach((project) => {
|
||||||
|
this.addEnvironmentToProject(project.id, env.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
return rows.map(this.mapRow);
|
return rows.map(this.mapRow);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
@ -1006,7 +1006,6 @@ class FeatureToggleService {
|
|||||||
const defaultEnv = environments.find((e) => e.name === DEFAULT_ENV);
|
const defaultEnv = environments.find((e) => e.name === DEFAULT_ENV);
|
||||||
const strategies = defaultEnv?.strategies || [];
|
const strategies = defaultEnv?.strategies || [];
|
||||||
const enabled = defaultEnv?.enabled || false;
|
const enabled = defaultEnv?.enabled || false;
|
||||||
|
|
||||||
return { ...legacyFeature, enabled, strategies };
|
return { ...legacyFeature, enabled, strategies };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,8 +164,9 @@ export default class StateService {
|
|||||||
}
|
}
|
||||||
const importData = await stateSchema.validateAsync(data);
|
const importData = await stateSchema.validateAsync(data);
|
||||||
|
|
||||||
|
let importedEnvironments: IEnvironment[] = [];
|
||||||
if (importData.environments) {
|
if (importData.environments) {
|
||||||
await this.importEnvironments({
|
importedEnvironments = await this.importEnvironments({
|
||||||
environments: data.environments,
|
environments: data.environments,
|
||||||
userName,
|
userName,
|
||||||
dropBeforeImport,
|
dropBeforeImport,
|
||||||
@ -173,6 +174,16 @@ export default class StateService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (importData.projects) {
|
||||||
|
await this.importProjects({
|
||||||
|
projects: data.projects,
|
||||||
|
importedEnvironments,
|
||||||
|
userName,
|
||||||
|
dropBeforeImport,
|
||||||
|
keepExisting,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (importData.features) {
|
if (importData.features) {
|
||||||
let projectData;
|
let projectData;
|
||||||
if (!importData.version || importData.version === 1) {
|
if (!importData.version || importData.version === 1) {
|
||||||
@ -208,15 +219,6 @@ export default class StateService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (importData.projects) {
|
|
||||||
await this.importProjects({
|
|
||||||
projects: data.projects,
|
|
||||||
userName,
|
|
||||||
dropBeforeImport,
|
|
||||||
keepExisting,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (importData.tagTypes && importData.tags) {
|
if (importData.tagTypes && importData.tags) {
|
||||||
await this.importTagData({
|
await this.importTagData({
|
||||||
tagTypes: data.tagTypes,
|
tagTypes: data.tagTypes,
|
||||||
@ -258,10 +260,13 @@ export default class StateService {
|
|||||||
async importFeatureEnvironments({ featureEnvironments }): Promise<void> {
|
async importFeatureEnvironments({ featureEnvironments }): Promise<void> {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
featureEnvironments.map((env) =>
|
featureEnvironments.map((env) =>
|
||||||
this.featureEnvironmentStore.addEnvironmentToFeature(
|
this.toggleStore
|
||||||
|
.getProjectId(env.featureName)
|
||||||
|
.then((id) =>
|
||||||
|
this.featureEnvironmentStore.connectFeatureToEnvironmentsForProject(
|
||||||
env.featureName,
|
env.featureName,
|
||||||
env.environment,
|
id,
|
||||||
env.enabled,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -410,7 +415,7 @@ export default class StateService {
|
|||||||
userName,
|
userName,
|
||||||
dropBeforeImport,
|
dropBeforeImport,
|
||||||
keepExisting,
|
keepExisting,
|
||||||
}): Promise<void> {
|
}): Promise<IEnvironment[]> {
|
||||||
this.logger.info(`Import ${environments.length} projects`);
|
this.logger.info(`Import ${environments.length} projects`);
|
||||||
const oldEnvs = dropBeforeImport
|
const oldEnvs = dropBeforeImport
|
||||||
? []
|
? []
|
||||||
@ -427,8 +432,9 @@ export default class StateService {
|
|||||||
const envsImport = environments.filter((env) =>
|
const envsImport = environments.filter((env) =>
|
||||||
keepExisting ? !oldEnvs.some((old) => old.name === env.name) : true,
|
keepExisting ? !oldEnvs.some((old) => old.name === env.name) : true,
|
||||||
);
|
);
|
||||||
|
let importedEnvs = [];
|
||||||
if (envsImport.length > 0) {
|
if (envsImport.length > 0) {
|
||||||
const importedEnvs = await this.environmentStore.importEnvironments(
|
importedEnvs = await this.environmentStore.importEnvironments(
|
||||||
envsImport,
|
envsImport,
|
||||||
);
|
);
|
||||||
const importedEnvironmentEvents = importedEnvs.map((env) => ({
|
const importedEnvironmentEvents = importedEnvs.map((env) => ({
|
||||||
@ -447,11 +453,13 @@ export default class StateService {
|
|||||||
this.apiTokenStore.delete(apiToken.secret),
|
this.apiTokenStore.delete(apiToken.secret),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return importedEnvs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async importProjects({
|
async importProjects({
|
||||||
projects,
|
projects,
|
||||||
|
importedEnvironments,
|
||||||
userName,
|
userName,
|
||||||
dropBeforeImport,
|
dropBeforeImport,
|
||||||
keepExisting,
|
keepExisting,
|
||||||
@ -477,6 +485,7 @@ export default class StateService {
|
|||||||
if (projectsToImport.length > 0) {
|
if (projectsToImport.length > 0) {
|
||||||
const importedProjects = await this.projectStore.importProjects(
|
const importedProjects = await this.projectStore.importProjects(
|
||||||
projectsToImport,
|
projectsToImport,
|
||||||
|
importedEnvironments,
|
||||||
);
|
);
|
||||||
const importedProjectEvents = importedProjects.map((project) => ({
|
const importedProjectEvents = importedProjects.map((project) => ({
|
||||||
type: PROJECT_IMPORT,
|
type: PROJECT_IMPORT,
|
||||||
|
@ -2,7 +2,7 @@ import {
|
|||||||
IEnvironmentProjectLink,
|
IEnvironmentProjectLink,
|
||||||
IProjectMembersCount,
|
IProjectMembersCount,
|
||||||
} from '../../db/project-store';
|
} from '../../db/project-store';
|
||||||
import { IProject, IProjectWithCount } from '../model';
|
import { IEnvironment, IProject, IProjectWithCount } from '../model';
|
||||||
import { Store } from './store';
|
import { Store } from './store';
|
||||||
|
|
||||||
export interface IProjectInsert {
|
export interface IProjectInsert {
|
||||||
@ -31,7 +31,10 @@ export interface IProjectStore extends Store<IProject, string> {
|
|||||||
updateHealth(healthUpdate: IProjectHealthUpdate): Promise<void>;
|
updateHealth(healthUpdate: IProjectHealthUpdate): Promise<void>;
|
||||||
create(project: IProjectInsert): Promise<IProject>;
|
create(project: IProjectInsert): Promise<IProject>;
|
||||||
update(update: IProjectInsert): Promise<void>;
|
update(update: IProjectInsert): Promise<void>;
|
||||||
importProjects(projects: IProjectInsert[]): Promise<IProject[]>;
|
importProjects(
|
||||||
|
projects: IProjectInsert[],
|
||||||
|
environments?: IEnvironment[],
|
||||||
|
): Promise<IProject[]>;
|
||||||
addEnvironmentToProject(id: string, environment: string): Promise<void>;
|
addEnvironmentToProject(id: string, environment: string): Promise<void>;
|
||||||
deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
|
deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
|
||||||
getEnvironmentsForProject(id: string): Promise<string[]>;
|
getEnvironmentsForProject(id: string): Promise<string[]>;
|
||||||
|
@ -260,14 +260,7 @@ test('Roundtrip with strategies in multiple environments works', async () => {
|
|||||||
id: projectId,
|
id: projectId,
|
||||||
description: 'Project for export',
|
description: 'Project for export',
|
||||||
});
|
});
|
||||||
await app.services.environmentService.addEnvironmentToProject(
|
|
||||||
environment,
|
|
||||||
projectId,
|
|
||||||
);
|
|
||||||
await app.services.environmentService.addEnvironmentToProject(
|
|
||||||
DEFAULT_ENV,
|
|
||||||
projectId,
|
|
||||||
);
|
|
||||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||||
projectId,
|
projectId,
|
||||||
{
|
{
|
||||||
@ -277,6 +270,15 @@ test('Roundtrip with strategies in multiple environments works', async () => {
|
|||||||
},
|
},
|
||||||
userName,
|
userName,
|
||||||
);
|
);
|
||||||
|
await app.services.environmentService.addEnvironmentToProject(
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.services.environmentService.addEnvironmentToProject(
|
||||||
|
DEFAULT_ENV,
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
await app.services.featureToggleServiceV2.createStrategy(
|
await app.services.featureToggleServiceV2.createStrategy(
|
||||||
{
|
{
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@ -307,7 +309,7 @@ test('Roundtrip with strategies in multiple environments works', async () => {
|
|||||||
userName: 'export-tester',
|
userName: 'export-tester',
|
||||||
});
|
});
|
||||||
const f = await app.services.featureToggleServiceV2.getFeature(featureName);
|
const f = await app.services.featureToggleServiceV2.getFeature(featureName);
|
||||||
expect(f.environments).toHaveLength(2);
|
expect(f.environments).toHaveLength(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`Importing version 2 replaces :global: environment with 'default'`, async () => {
|
test(`Importing version 2 replaces :global: environment with 'default'`, async () => {
|
||||||
@ -320,7 +322,7 @@ test(`Importing version 2 replaces :global: environment with 'default'`, async (
|
|||||||
const feature = await app.services.featureToggleServiceV2.getFeatureToggle(
|
const feature = await app.services.featureToggleServiceV2.getFeatureToggle(
|
||||||
'this-is-fun',
|
'this-is-fun',
|
||||||
);
|
);
|
||||||
expect(feature.environments).toHaveLength(1);
|
expect(feature.environments).toHaveLength(4);
|
||||||
expect(feature.environments[0].name).toBe(DEFAULT_ENV);
|
expect(feature.environments[0].name).toBe(DEFAULT_ENV);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -437,3 +439,22 @@ test(`should clean apitokens for not existing environment after import with drop
|
|||||||
const apiTokens = await app.services.apiTokenService.getAllTokens();
|
const apiTokens = await app.services.apiTokenService.getAllTokens();
|
||||||
expect(apiTokens.length).toEqual(0);
|
expect(apiTokens.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(`should not show environment on feature toggle, when environment is disabled`, async () => {
|
||||||
|
await app.request
|
||||||
|
.post('/api/admin/state/import?drop=true')
|
||||||
|
.attach('file', 'src/test/examples/import-state.json')
|
||||||
|
.expect(202);
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.post('/api/admin/projects/default/environments')
|
||||||
|
.send({ environment: 'state-visible-environment' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const { body } = await app.request
|
||||||
|
.get('/api/admin/projects/default/features/my-feature')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(body.environments).toHaveLength(1);
|
||||||
|
expect(body.environments[0].name).toBe('state-visible-environment');
|
||||||
|
});
|
||||||
|
53
src/test/examples/import-state.json
Normal file
53
src/test/examples/import-state.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"name": "my-feature",
|
||||||
|
"description": "",
|
||||||
|
"type": "release",
|
||||||
|
"project": "default",
|
||||||
|
"stale": false,
|
||||||
|
"variants": [],
|
||||||
|
"createdAt": "2021-09-17T07:06:40.925Z",
|
||||||
|
"lastSeenAt": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"featureStrategies": [
|
||||||
|
{
|
||||||
|
"id": "2ea91298-4565-4db2-8a23-50757001a076",
|
||||||
|
"featureName": "my-feature",
|
||||||
|
"projectId": "default",
|
||||||
|
"environment": "state-visible-environment",
|
||||||
|
"strategyName": "gradualRolloutRandom",
|
||||||
|
"parameters": {
|
||||||
|
"percentage": "100"
|
||||||
|
},
|
||||||
|
"constraints": [],
|
||||||
|
"createdAt": "2021-09-17T07:23:39.374Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"environments": [
|
||||||
|
{
|
||||||
|
"name": "state-visible-environment",
|
||||||
|
"type": "production",
|
||||||
|
"displayName": "Visible"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "state-hidden-environment",
|
||||||
|
"type": "production",
|
||||||
|
"displayName": "Hidden"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"featureEnvironments": [
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"featureName": "my-feature",
|
||||||
|
"environment": "state-visible-environment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"featureName": "my-feature",
|
||||||
|
"environment": "state-hidden-environment"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -150,7 +150,7 @@ export default class FakeFeatureEnvironmentStore
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return Promise.reject(new Error('Not implemented'));
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
disableEnvironmentIfNoStrategies(
|
disableEnvironmentIfNoStrategies(
|
||||||
|
13
src/test/fixtures/fake-project-store.ts
vendored
13
src/test/fixtures/fake-project-store.ts
vendored
@ -3,7 +3,11 @@ import {
|
|||||||
IProjectInsert,
|
IProjectInsert,
|
||||||
IProjectStore,
|
IProjectStore,
|
||||||
} from '../../lib/types/stores/project-store';
|
} from '../../lib/types/stores/project-store';
|
||||||
import { IProject, IProjectWithCount } from '../../lib/types/model';
|
import {
|
||||||
|
IEnvironment,
|
||||||
|
IProject,
|
||||||
|
IProjectWithCount,
|
||||||
|
} from '../../lib/types/model';
|
||||||
import NotFoundError from '../../lib/error/notfound-error';
|
import NotFoundError from '../../lib/error/notfound-error';
|
||||||
import {
|
import {
|
||||||
IEnvironmentProjectLink,
|
IEnvironmentProjectLink,
|
||||||
@ -110,7 +114,12 @@ export default class FakeProjectStore implements IProjectStore {
|
|||||||
return this.exists(id);
|
return this.exists(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async importProjects(projects: IProjectInsert[]): Promise<IProject[]> {
|
async importProjects(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
projects: IProjectInsert[],
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
environments?: IEnvironment[],
|
||||||
|
): Promise<IProject[]> {
|
||||||
return projects.map((p) => this.createInternal(p));
|
return projects.map((p) => this.createInternal(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user