Skip to content

Commit 7e1c9f1

Browse files
authored
Merge pull request #2439 from RedisInsight/feature/RI-4806-sso-feature-flag
#RI-4806 sso feature flag
2 parents f9dc63b + 90bd24b commit 7e1c9f1

File tree

35 files changed

+787
-142
lines changed

35 files changed

+787
-142
lines changed

redisinsight/api/config/features-config.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,33 @@
1616
"cond": "eq"
1717
}
1818
]
19+
},
20+
"cloudSso": {
21+
"flag": true,
22+
"perc": [[0,100]],
23+
"filters": [
24+
{
25+
"name": "config.server.buildType",
26+
"value": "ELECTRON",
27+
"cond": "eq"
28+
}
29+
],
30+
"data": {
31+
"selectPlan": {
32+
"components": {
33+
"triggersAndFunctions": [
34+
{
35+
"provider": "AWS",
36+
"regions": ["ap-southeast-1"]
37+
},
38+
{
39+
"provider": "GCP",
40+
"regions": ["asia-northeast1"]
41+
}
42+
]
43+
}
44+
}
45+
}
1946
}
2047
}
2148
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class FeatureSso1691476419592 implements MigrationInterface {
4+
name = 'FeatureSso1691476419592'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`CREATE TABLE "temporary_features" ("name" varchar PRIMARY KEY NOT NULL, "flag" boolean NOT NULL, "strategy" varchar, "data" text)`);
8+
await queryRunner.query(`INSERT INTO "temporary_features"("name", "flag") SELECT "name", "flag" FROM "features"`);
9+
await queryRunner.query(`DROP TABLE "features"`);
10+
await queryRunner.query(`ALTER TABLE "temporary_features" RENAME TO "features"`);
11+
}
12+
13+
public async down(queryRunner: QueryRunner): Promise<void> {
14+
await queryRunner.query(`ALTER TABLE "features" RENAME TO "temporary_features"`);
15+
await queryRunner.query(`CREATE TABLE "features" ("name" varchar PRIMARY KEY NOT NULL, "flag" boolean NOT NULL)`);
16+
await queryRunner.query(`INSERT INTO "features"("name", "flag") SELECT "name", "flag" FROM "temporary_features"`);
17+
await queryRunner.query(`DROP TABLE "temporary_features"`);
18+
}
19+
20+
}

redisinsight/api/migration/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { DatabaseRecommendationUnique1687435940110 } from './1687435940110-datab
3737
import { CloudDatabaseDetails1687166457712 } from './1687166457712-cloud-database-details';
3838
import { FreeCloudDatabase1688989337247 } from './1688989337247-freeCloudDatabase';
3939
import { CloudCapiKeys1691061058385 } from './1691061058385-cloud-capi-keys';
40+
import { FeatureSso1691476419592 } from './1691476419592-feature-sso';
4041

4142
export default [
4243
initialMigration1614164490968,
@@ -78,4 +79,5 @@ export default [
7879
CloudDatabaseDetails1687166457712,
7980
FreeCloudDatabase1688989337247,
8081
CloudCapiKeys1691061058385,
82+
FeatureSso1691476419592,
8183
];

redisinsight/api/src/__mocks__/feature.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { FeatureEntity } from 'src/modules/feature/entities/feature.entity';
1111
import { mockAppSettings } from 'src/__mocks__/app-settings';
1212
import config from 'src/utils/config';
1313
import { KnownFeatures } from 'src/modules/feature/constants';
14+
import { CloudSsoFeatureStrategy } from 'src/modules/cloud/cloud-sso.feature.flag';
1415
import * as defaultConfig from '../../config/features-config.json';
1516

1617
export const mockFeaturesConfigId = '1';
@@ -160,6 +161,28 @@ export const mockFeature = Object.assign(new Feature(), {
160161
flag: true,
161162
});
162163

164+
export const mockFeatureSso = Object.assign(new Feature(), {
165+
name: KnownFeatures.CloudSso,
166+
flag: true,
167+
strategy: CloudSsoFeatureStrategy.DeepLink,
168+
data: {
169+
selectPlan: {
170+
components: {
171+
triggersAndFunctions: [
172+
{
173+
provider: 'AWS',
174+
regions: ['ap-southeast-1'],
175+
},
176+
{
177+
provider: 'GCP',
178+
regions: ['asia-northeast1'],
179+
},
180+
],
181+
},
182+
},
183+
},
184+
});
185+
163186
export const mockUnknownFeature = Object.assign(new Feature(), {
164187
name: 'unknown',
165188
flag: true,
@@ -186,7 +209,7 @@ export const mockFeaturesConfigRepository = jest.fn(() => ({
186209
export const mockFeatureRepository = jest.fn(() => ({
187210
get: jest.fn().mockResolvedValue(mockFeature),
188211
upsert: jest.fn().mockResolvedValue({ updated: 1 }),
189-
list: jest.fn().mockResolvedValue([mockFeature]),
212+
list: jest.fn().mockResolvedValue([mockFeature, mockFeatureSso]),
190213
delete: jest.fn().mockResolvedValue({ deleted: 1 }),
191214
}));
192215

@@ -210,10 +233,10 @@ export const mockFeatureAnalytics = jest.fn(() => ({
210233
}));
211234

212235
export const mockInsightsRecommendationsFlagStrategy = {
213-
calculate: jest.fn().mockResolvedValue(true),
236+
calculate: jest.fn().mockResolvedValue(mockFeature),
214237
};
215238

216239
export const mockFeatureFlagProvider = jest.fn(() => ({
217240
getStrategy: jest.fn().mockResolvedValue(mockInsightsRecommendationsFlagStrategy),
218-
calculate: jest.fn().mockResolvedValue(true),
241+
calculate: jest.fn().mockResolvedValue(mockFeature),
219242
}));
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { FeatureStorage, IFeatureFlag, KnownFeatures } from 'src/modules/feature/constants/index';
22
import { CloudSsoFeatureFlag } from 'src/modules/cloud/cloud-sso.feature.flag';
33

4-
export const knownFeatures: IFeatureFlag[] = [
5-
{
4+
export const knownFeatures: Record<KnownFeatures, IFeatureFlag> = {
5+
[KnownFeatures.InsightsRecommendations]: {
66
name: KnownFeatures.InsightsRecommendations,
77
storage: FeatureStorage.Database,
88
},
9-
{
9+
[KnownFeatures.CloudSso]: {
1010
name: KnownFeatures.CloudSso,
11-
storage: FeatureStorage.Custom,
11+
storage: FeatureStorage.Database,
1212
factory: CloudSsoFeatureFlag.getFeature,
1313
},
14-
];
14+
};

redisinsight/api/src/modules/feature/entities/feature.entity.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
Column, Entity, PrimaryColumn,
33
} from 'typeorm';
44
import { Expose } from 'class-transformer';
5+
import { DataAsJsonString } from 'src/common/decorators';
56

67
@Entity('features')
78
export class FeatureEntity {
@@ -12,4 +13,13 @@ export class FeatureEntity {
1213
@Expose()
1314
@Column()
1415
flag: boolean;
16+
17+
@Expose()
18+
@Column({ nullable: true })
19+
strategy?: string;
20+
21+
@Expose()
22+
@Column({ nullable: true, type: 'text' })
23+
@DataAsJsonString()
24+
data?: string;
1525
}

redisinsight/api/src/modules/feature/feature.service.spec.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {
44
mockFeature, mockFeatureAnalytics, mockFeatureFlagProvider, mockFeatureRepository,
55
mockFeaturesConfig,
66
mockFeaturesConfigJson,
7-
mockFeaturesConfigRepository,
8-
MockType, mockUnknownFeature,
7+
mockFeaturesConfigRepository, mockFeatureSso,
8+
MockType, mockUnknownFeature
99
} from 'src/__mocks__';
1010
import { FeaturesConfigRepository } from 'src/modules/feature/repositories/features-config.repository';
1111
import { EventEmitter2 } from '@nestjs/event-emitter';
@@ -82,24 +82,17 @@ describe('FeatureService', () => {
8282
expect(await service.list())
8383
.toEqual({
8484
features: {
85-
[KnownFeatures.InsightsRecommendations]: {
86-
name: KnownFeatures.InsightsRecommendations,
87-
flag: true,
88-
},
89-
[KnownFeatures.CloudSso]: {
90-
name: KnownFeatures.CloudSso,
91-
flag: true,
92-
strategy: CloudSsoFeatureStrategy.DeepLink,
93-
},
85+
[KnownFeatures.InsightsRecommendations]: mockFeature,
86+
[KnownFeatures.CloudSso]: mockFeatureSso,
9487
},
9588
});
9689
});
9790
});
9891

9992
describe('recalculateFeatureFlags', () => {
10093
it('should recalculate flags (1 update an 1 delete)', async () => {
101-
repository.list.mockResolvedValueOnce([mockFeature, mockUnknownFeature]);
102-
repository.list.mockResolvedValueOnce([mockFeature]);
94+
repository.list.mockResolvedValueOnce([mockFeature, mockFeatureSso, mockUnknownFeature]);
95+
repository.list.mockResolvedValueOnce([mockFeature, mockFeatureSso]);
10396
configsRepository.getOrCreate.mockResolvedValueOnce(mockFeaturesConfig);
10497

10598
await service.recalculateFeatureFlags();
@@ -114,15 +107,8 @@ describe('FeatureService', () => {
114107
expect(analytics.sendFeatureFlagRecalculated).toHaveBeenCalledWith({
115108
configVersion: mockFeaturesConfig.data.version,
116109
features: {
117-
[KnownFeatures.InsightsRecommendations]: {
118-
name: KnownFeatures.InsightsRecommendations,
119-
flag: true,
120-
},
121-
[KnownFeatures.CloudSso]: {
122-
name: KnownFeatures.CloudSso,
123-
flag: true,
124-
strategy: CloudSsoFeatureStrategy.DeepLink,
125-
},
110+
[KnownFeatures.InsightsRecommendations]: mockFeature,
111+
[KnownFeatures.CloudSso]: mockFeatureSso,
126112
},
127113
});
128114
});

redisinsight/api/src/modules/feature/feature.service.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { find } from 'lodash';
1+
import { find, forEach } from 'lodash';
22
import { Injectable, Logger } from '@nestjs/common';
33
import { FeatureRepository } from 'src/modules/feature/repositories/feature.repository';
44
import { FeatureServerEvents, FeatureStorage } from 'src/modules/feature/constants';
@@ -7,6 +7,7 @@ import { FeatureFlagProvider } from 'src/modules/feature/providers/feature-flag/
77
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
88
import { FeatureAnalytics } from 'src/modules/feature/feature.analytics';
99
import { knownFeatures } from 'src/modules/feature/constants/known-features';
10+
import { Feature } from 'src/modules/feature/model/feature';
1011

1112
@Injectable()
1213
export class FeatureService {
@@ -38,14 +39,14 @@ export class FeatureService {
3839
/**
3940
* Returns list of features flags
4041
*/
41-
async list() {
42+
async list(): Promise<{ features: Record<string, Feature> }> {
4243
this.logger.log('Getting features list');
4344

4445
const features = {};
4546

4647
const featuresFromDatabase = await this.repository.list();
4748

48-
knownFeatures.forEach((feature) => {
49+
forEach(knownFeatures, (feature) => {
4950
// todo: implement various storage strategies support with next features
5051
switch (feature?.storage) {
5152
case FeatureStorage.Database: {
@@ -54,6 +55,8 @@ export class FeatureService {
5455
features[feature.name] = {
5556
name: dbFeature.name,
5657
flag: dbFeature.flag,
58+
strategy: dbFeature.strategy || undefined,
59+
data: dbFeature.data || undefined,
5760
};
5861
}
5962
break;
@@ -66,14 +69,6 @@ export class FeatureService {
6669
}
6770
});
6871

69-
try {
70-
this.analytics.sendFeatureFlagRecalculated({
71-
configVersion: (await this.featuresConfigRepository.getOrCreate())?.data?.version,
72-
features,
73-
});
74-
} catch (e) {
75-
// ignore telemetry error
76-
}
7772
return { features };
7873
}
7974

@@ -97,10 +92,11 @@ export class FeatureService {
9792
this.logger.debug('Recalculating features flags for new config', featuresConfig);
9893

9994
await Promise.all(Array.from(featuresConfig?.data?.features || new Map(), async ([name, feature]) => {
100-
actions.toUpsert.push({
101-
name,
102-
flag: await this.featureFlagProvider.calculate(name, feature),
103-
});
95+
if (knownFeatures[name]) {
96+
actions.toUpsert.push({
97+
...(await this.featureFlagProvider.calculate(knownFeatures[name], feature)),
98+
});
99+
}
104100
}));
105101

106102
// calculate to delete features
@@ -115,7 +111,17 @@ export class FeatureService {
115111
`Features flags recalculated. Updated: ${actions.toUpsert.length} deleted: ${actions.toDelete.length}`,
116112
);
117113

118-
this.eventEmitter.emit(FeatureServerEvents.FeaturesRecalculated, await this.list());
114+
const list = await this.list();
115+
this.eventEmitter.emit(FeatureServerEvents.FeaturesRecalculated, list);
116+
117+
try {
118+
this.analytics.sendFeatureFlagRecalculated({
119+
configVersion: (await this.featuresConfigRepository.getOrCreate())?.data?.version,
120+
features: list.features,
121+
});
122+
} catch (e) {
123+
// ignore telemetry error
124+
}
119125
} catch (e) {
120126
this.logger.error('Unable to recalculate features flags', e);
121127
}

redisinsight/api/src/modules/feature/model/feature.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ export class Feature {
99

1010
@Expose()
1111
strategy?: string;
12+
13+
@Expose()
14+
data?: any;
1215
}

redisinsight/api/src/modules/feature/providers/feature-flag/feature-flag.provider.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import {
3+
mockFeature,
34
mockFeaturesConfig,
45
mockFeaturesConfigService, mockInsightsRecommendationsFlagStrategy, mockSettingsService,
56
} from 'src/__mocks__';
@@ -11,6 +12,7 @@ import {
1112
InsightsRecommendationsFlagStrategy,
1213
} from 'src/modules/feature/providers/feature-flag/strategies/insights-recommendations.flag.strategy';
1314
import { DefaultFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/default.flag.strategy';
15+
import { knownFeatures } from 'src/modules/feature/constants/known-features';
1416

1517
describe('FeatureFlagProvider', () => {
1618
let service: FeatureFlagProvider;
@@ -55,10 +57,11 @@ describe('FeatureFlagProvider', () => {
5557
.mockReturnValue(mockInsightsRecommendationsFlagStrategy as unknown as InsightsRecommendationsFlagStrategy);
5658

5759
expect(await service.calculate(
58-
KnownFeatures.InsightsRecommendations,
60+
knownFeatures[KnownFeatures.InsightsRecommendations],
5961
mockFeaturesConfig[KnownFeatures.InsightsRecommendations],
60-
)).toEqual(true);
62+
)).toEqual(mockFeature);
6163
expect(mockInsightsRecommendationsFlagStrategy.calculate).toHaveBeenCalledWith(
64+
knownFeatures[KnownFeatures.InsightsRecommendations],
6265
mockFeaturesConfig[KnownFeatures.InsightsRecommendations],
6366
);
6467
});

0 commit comments

Comments
 (0)