1
0
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:
sellinjaanus 2022-10-19 15:05:07 +03:00 committed by GitHub
parent 8916de76be
commit 8618cec832
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 139 additions and 35 deletions

View File

@ -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 [];

View File

@ -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 };
} }

View File

@ -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,

View File

@ -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[]>;

View File

@ -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');
});

View 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"
}
]
}

View File

@ -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(

View File

@ -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));
} }