Skip to content

Commit a37ca9a

Browse files
author
Artem
committed
#RI-4878 filter redis modules
1 parent 5e3c555 commit a37ca9a

File tree

10 files changed

+167
-9
lines changed

10 files changed

+167
-9
lines changed

redisinsight/api/config/features-config.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": 2,
2+
"version": 2.32,
33
"features": {
44
"insightsRecommendations": {
55
"flag": true,
@@ -43,6 +43,18 @@
4343
}
4444
}
4545
}
46+
},
47+
"redisModuleFilter": {
48+
"flag": true,
49+
"perc": [[0, 100]],
50+
"data": {
51+
"hideByName": [
52+
{
53+
"expression": "RedisGraphStub",
54+
"options": "i"
55+
}
56+
]
57+
}
4658
}
4759
}
4860
}

redisinsight/api/src/__mocks__/feature.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export const mockFeaturesConfigService = jest.fn(() => ({
223223
}));
224224

225225
export const mockFeatureService = jest.fn(() => ({
226+
getByName: jest.fn().mockResolvedValue(undefined),
226227
isFeatureEnabled: jest.fn().mockResolvedValue(true),
227228
}));
228229

redisinsight/api/src/modules/database/providers/database-info.provider.spec.ts

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
22
import { when } from 'jest-when';
33
import { IRedisClusterNodeAddress, ReplyError } from 'src/models';
44
import {
5+
mockFeatureService,
56
mockIOClusterNode1,
67
mockIOClusterNode2,
78
mockIORedisClient,
@@ -17,14 +18,15 @@ import {
1718
mockSentinelMasterEndpoint,
1819
mockSentinelMasterInDownState,
1920
mockSentinelMasterInOkState,
20-
mockStandaloneRedisInfoReply,
21+
mockStandaloneRedisInfoReply, MockType,
2122
} from 'src/__mocks__';
2223
import { REDIS_MODULES_COMMANDS, AdditionalRedisModuleName } from 'src/constants';
2324
import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';
2425
import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';
2526
import { BadRequestException, ForbiddenException } from '@nestjs/common';
2627
import { SentinelMasterStatus } from 'src/modules/redis-sentinel/models/sentinel-master';
2728
import ERROR_MESSAGES from 'src/constants/error-messages';
29+
import { FeatureService } from 'src/modules/feature/feature.service';
2830

2931
const mockClusterNodeAddresses: IRedisClusterNodeAddress[] = [
3032
{
@@ -78,14 +80,22 @@ const mockSentinelConnectionOptions = {
7880

7981
describe('DatabaseInfoProvider', () => {
8082
let service: DatabaseInfoProvider;
83+
let featureService: MockType<FeatureService>;
8184

8285
beforeEach(async () => {
8386
jest.clearAllMocks();
8487
const module: TestingModule = await Test.createTestingModule({
85-
providers: [DatabaseInfoProvider],
88+
providers: [
89+
DatabaseInfoProvider,
90+
{
91+
provide: FeatureService,
92+
useFactory: mockFeatureService,
93+
},
94+
],
8695
}).compile();
8796

8897
service = await module.get(DatabaseInfoProvider);
98+
featureService = await module.get(FeatureService);
8999
});
90100

91101
describe('isCluster', () => {
@@ -239,7 +249,7 @@ describe('DatabaseInfoProvider', () => {
239249
});
240250

241251
describe('determineDatabaseModules', () => {
242-
it('get modules by using MODULE LIST command', async () => {
252+
it('get modules by using MODULE LIST command (without filters)', async () => {
243253
when(mockIORedisClient.call)
244254
.calledWith('module', ['list'])
245255
.mockResolvedValue(mockRedisModuleList);
@@ -258,7 +268,37 @@ describe('DatabaseInfoProvider', () => {
258268
{ name: 'customModule', version: 10000, semanticVersion: undefined },
259269
]);
260270
});
261-
it('detect all modules by using COMMAND INFO command', async () => {
271+
it('get modules by using MODULE LIST command (with filters applied)', async () => {
272+
when(mockIORedisClient.call)
273+
.calledWith('module', ['list'])
274+
.mockResolvedValue(mockRedisModuleList);
275+
featureService.getByName.mockResolvedValue({
276+
flag: true,
277+
data: {
278+
hideByName: [
279+
{
280+
expression: 'rejSoN',
281+
options: 'i',
282+
},
283+
],
284+
},
285+
});
286+
287+
const result = await service.determineDatabaseModules(mockIORedisClient);
288+
289+
expect(mockIORedisClient.call).not.toHaveBeenCalledWith('command', expect.anything());
290+
expect(result).toEqual([
291+
{ name: AdditionalRedisModuleName.RedisAI, version: 10000, semanticVersion: '1.0.0' },
292+
{ name: AdditionalRedisModuleName.RedisGraph, version: 10000, semanticVersion: '1.0.0' },
293+
{ name: AdditionalRedisModuleName.RedisGears, version: 10000, semanticVersion: '1.0.0' },
294+
{ name: AdditionalRedisModuleName.RedisBloom, version: 10000, semanticVersion: '1.0.0' },
295+
// { name: AdditionalRedisModuleName.RedisJSON, version: 10000, semanticVersion: '1.0.0' }, should be ignored
296+
{ name: AdditionalRedisModuleName.RediSearch, version: 10000, semanticVersion: '1.0.0' },
297+
{ name: AdditionalRedisModuleName.RedisTimeSeries, version: 10000, semanticVersion: '1.0.0' },
298+
{ name: 'customModule', version: 10000, semanticVersion: undefined },
299+
]);
300+
});
301+
it('detect all modules by using COMMAND INFO command (without filter)', async () => {
262302
when(mockIORedisClient.call)
263303
.calledWith('module', ['list'])
264304
.mockRejectedValue(mockUnknownCommandModule);
@@ -282,6 +322,41 @@ describe('DatabaseInfoProvider', () => {
282322
{ name: AdditionalRedisModuleName.RedisTimeSeries },
283323
]);
284324
});
325+
it('detect all modules by using COMMAND INFO command (with filter)', async () => {
326+
when(mockIORedisClient.call)
327+
.calledWith('module', ['list'])
328+
.mockRejectedValue(mockUnknownCommandModule);
329+
when(mockIORedisClient.call)
330+
.calledWith('command', expect.anything())
331+
.mockResolvedValue([
332+
null,
333+
['somecommand', -1, ['readonly'], 0, 0, -1, []],
334+
]);
335+
featureService.getByName.mockResolvedValue({
336+
flag: true,
337+
data: {
338+
hideByName: [
339+
{
340+
expression: 'rejSoN',
341+
options: 'i',
342+
},
343+
],
344+
},
345+
});
346+
347+
const result = await service.determineDatabaseModules(mockIORedisClient);
348+
349+
expect(mockIORedisClient.call).toHaveBeenCalledTimes(REDIS_MODULES_COMMANDS.size + 1);
350+
expect(result).toEqual([
351+
{ name: AdditionalRedisModuleName.RedisAI },
352+
{ name: AdditionalRedisModuleName.RedisGraph },
353+
{ name: AdditionalRedisModuleName.RedisGears },
354+
{ name: AdditionalRedisModuleName.RedisBloom },
355+
// { name: AdditionalRedisModuleName.RedisJSON }, should be ignored
356+
{ name: AdditionalRedisModuleName.RediSearch },
357+
{ name: AdditionalRedisModuleName.RedisTimeSeries },
358+
]);
359+
});
285360
it('detect only RediSearch module by using COMMAND INFO command', async () => {
286361
when(mockIORedisClient.call)
287362
.calledWith('module', ['list'])
@@ -372,7 +447,7 @@ describe('DatabaseInfoProvider', () => {
372447
});
373448
it('should throw an error if no permission to run \'info\' command', async () => {
374449
mockIORedisClient.info.mockRejectedValue({
375-
message: 'NOPERM this user has no permissions to run the \'info\' command'
450+
message: 'NOPERM this user has no permissions to run the \'info\' command',
376451
});
377452

378453
try {

redisinsight/api/src/modules/database/providers/database-info.provider.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,15 @@ import { SentinelMaster, SentinelMasterStatus } from 'src/modules/redis-sentinel
2121
import ERROR_MESSAGES from 'src/constants/error-messages';
2222
import { Endpoint } from 'src/common/models';
2323
import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';
24+
import { FeatureService } from 'src/modules/feature/feature.service';
25+
import { KnownFeatures } from 'src/modules/feature/constants';
2426

2527
@Injectable()
2628
export class DatabaseInfoProvider {
29+
constructor(
30+
private readonly featureService: FeatureService,
31+
) {}
32+
2733
/**
2834
* Check weather current database is a cluster
2935
* @param client
@@ -67,6 +73,27 @@ export class DatabaseInfoProvider {
6773
}));
6874
}
6975

76+
public async filterRawModules(modules: any[]): Promise<any[]> {
77+
let filteredModules = modules;
78+
79+
try {
80+
const filterModules = await this.featureService.getByName(KnownFeatures.RedisModuleFilter);
81+
82+
if (filterModules?.flag && filterModules.data?.hideByName?.length) {
83+
filteredModules = modules.filter(({ name }) => {
84+
const match = filterModules.data.hideByName.find((filter) => filter.expression
85+
&& (new RegExp(filter.expression, filter.options)).test(name));
86+
87+
return !match;
88+
});
89+
}
90+
} catch (e) {
91+
// ignore
92+
}
93+
94+
return filteredModules;
95+
}
96+
7097
/**
7198
* Determine database modules using "module list" command
7299
* In case when "module" command is not available use "command info" approach
@@ -75,7 +102,10 @@ export class DatabaseInfoProvider {
75102
public async determineDatabaseModules(client: any): Promise<AdditionalRedisModule[]> {
76103
try {
77104
const reply = await client.call('module', ['list']);
78-
const modules = reply.map((module: any[]) => convertStringsArrayToObject(module));
105+
const modules = await this.filterRawModules(
106+
reply.map((module: any[]) => convertStringsArrayToObject(module)),
107+
);
108+
79109
return modules.map(({ name, ver }) => ({
80110
name: SUPPORTED_REDIS_MODULES[name] ?? name,
81111
version: ver,
@@ -120,7 +150,8 @@ export class DatabaseInfoProvider {
120150
// continue regardless of error
121151
}
122152
}));
123-
return modules;
153+
154+
return await this.filterRawModules(modules);
124155
}
125156

126157
/**

redisinsight/api/src/modules/feature/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export enum FeatureConfigConfigDestination {
2222
export enum KnownFeatures {
2323
InsightsRecommendations = 'insightsRecommendations',
2424
CloudSso = 'cloudSso',
25+
RedisModuleFilter = 'redisModuleFilter',
2526
}
2627

2728
export interface IFeatureFlag {

redisinsight/api/src/modules/feature/constants/known-features.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,8 @@ export const knownFeatures: Record<KnownFeatures, IFeatureFlag> = {
1111
storage: FeatureStorage.Database,
1212
factory: CloudSsoFeatureFlag.getFeature,
1313
},
14+
[KnownFeatures.RedisModuleFilter]: {
15+
name: KnownFeatures.RedisModuleFilter,
16+
storage: FeatureStorage.Database,
17+
},
1418
};

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export class FeatureService {
2121
private readonly analytics: FeatureAnalytics,
2222
) {}
2323

24+
async getByName(name: string): Promise<Feature> {
25+
try {
26+
return await this.repository.get(name);
27+
} catch (e) {
28+
return null;
29+
}
30+
}
31+
2432
/**
2533
* Check if feature enabled
2634
* @param name

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { SettingsService } from 'src/modules/settings/settings.service';
99
import { IFeatureFlag, KnownFeatures } from 'src/modules/feature/constants';
1010
import { CloudSsoFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/cloud-sso.flag.strategy';
1111
import { Feature } from 'src/modules/feature/model/feature';
12+
import { SimpleFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/simple.flag.strategy';
1213

1314
@Injectable()
1415
export class FeatureFlagProvider {
@@ -30,6 +31,10 @@ export class FeatureFlagProvider {
3031
this.featuresConfigService,
3132
this.settingsService,
3233
));
34+
this.strategies.set(KnownFeatures.RedisModuleFilter, new SimpleFlagStrategy(
35+
this.featuresConfigService,
36+
this.settingsService,
37+
));
3338
}
3439

3540
getStrategy(name: string): FeatureFlagStrategy {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { FeatureFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy';
2+
import { Feature } from 'src/modules/feature/model/feature';
3+
import { IFeatureFlag } from 'src/modules/feature/constants';
4+
5+
export class SimpleFlagStrategy extends FeatureFlagStrategy {
6+
async calculate(knownFeature: IFeatureFlag, featureConfig: any): Promise<Feature> {
7+
const isInRange = await this.isInTargetRange(featureConfig?.perc);
8+
9+
return {
10+
name: knownFeature.name,
11+
flag: isInRange && await this.filter(featureConfig?.filters) ? !!featureConfig?.flag : !featureConfig?.flag,
12+
data: featureConfig?.data,
13+
};
14+
}
15+
}

redisinsight/desktop/src/lib/app/app.handlers.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { app } from 'electron'
22
import log from 'electron-log'
33
import { getBackendGracefulShutdown, WindowType, getWindows, windowFactory, windows } from 'desktopSrc/lib'
4-
import { deepLinkHandler } from 'desktopSrc/lib/app/deep-link.handlers';
4+
import { deepLinkHandler } from 'desktopSrc/lib/app/deep-link.handlers'
55

66
export const initAppHandlers = () => {
77
app.on('activate', () => {
@@ -49,6 +49,12 @@ export const initAppHandlers = () => {
4949
event.preventDefault()
5050
// todo: implement url handler to map url to a proper function
5151
await deepLinkHandler(url)
52+
53+
if (windows.size) {
54+
const win = windows.values().next().value
55+
if (win.isMinimized()) win.restore()
56+
win.focus()
57+
}
5258
})
5359

5460
// deep link open (win)

0 commit comments

Comments
 (0)