Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ jobs:
export RI_CLOUD_IDP_GOOGLE_ID=$RI_CLOUD_IDP_GOOGLE_ID_STAGE
export RI_CLOUD_IDP_GH_ID=$RI_CLOUD_IDP_GH_ID_STAGE
export RI_CLOUD_API_URL=$RI_CLOUD_API_URL_STAGE
export RI_CLOUD_CAPI_URL=$RI_CLOUD_CAPI_URL_STAGE

if [ << parameters.env >> == 'stage' ]; then
UPGRADES_LINK=$UPGRADES_LINK_STAGE SEGMENT_WRITE_KEY=$SEGMENT_WRITE_KEY_STAGE yarn package:stage && yarn package:mas
Expand Down Expand Up @@ -724,6 +725,7 @@ jobs:
export RI_CLOUD_IDP_GOOGLE_ID=$RI_CLOUD_IDP_GOOGLE_ID_STAGE
export RI_CLOUD_IDP_GH_ID=$RI_CLOUD_IDP_GH_ID_STAGE
export RI_CLOUD_API_URL=$RI_CLOUD_API_URL_STAGE
export RI_CLOUD_CAPI_URL=$RI_CLOUD_CAPI_URL_STAGE

if [ << parameters.env >> == 'stage' ]; then
UPGRADES_LINK=$UPGRADES_LINK_STAGE SEGMENT_WRITE_KEY=$SEGMENT_WRITE_KEY_STAGE yarn package:stage
Expand Down
2 changes: 1 addition & 1 deletion redisinsight/api/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default {
tlsKey: process.env.SERVER_TLS_KEY,
staticContent: !!process.env.SERVER_STATIC_CONTENT || false,
buildType: process.env.BUILD_TYPE || 'ELECTRON',
appVersion: process.env.APP_VERSION || '2.30.0',
appVersion: process.env.APP_VERSION || '2.32.0',
requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 25000,
excludeRoutes: [],
excludeAuthRoutes: [],
Expand Down
14 changes: 13 additions & 1 deletion redisinsight/api/config/features-config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": 2,
"version": 2.32,
"features": {
"insightsRecommendations": {
"flag": true,
Expand Down Expand Up @@ -43,6 +43,18 @@
}
}
}
},
"redisModuleFilter": {
"flag": true,
"perc": [[0, 100]],
"data": {
"hideByName": [
{
"expression": "RedisGraphStub",
"options": "i"
}
]
}
}
}
}
2 changes: 1 addition & 1 deletion redisinsight/api/config/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const SWAGGER_CONFIG: Omit<OpenAPIObject, 'paths'> = {
info: {
title: 'RedisInsight Backend API',
description: 'RedisInsight Backend API',
version: '2.30.0',
version: '2.32.0',
},
tags: [],
};
Expand Down
2 changes: 1 addition & 1 deletion redisinsight/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redisinsight-api",
"version": "2.30.0",
"version": "2.32.0",
"description": "RedisInsight API",
"private": true,
"author": {
Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/src/__mocks__/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export const mockFeaturesConfigService = jest.fn(() => ({
}));

export const mockFeatureService = jest.fn(() => ({
getByName: jest.fn().mockResolvedValue(undefined),
isFeatureEnabled: jest.fn().mockResolvedValue(true),
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { when } from 'jest-when';
import { IRedisClusterNodeAddress, ReplyError } from 'src/models';
import {
mockFeatureService,
mockIOClusterNode1,
mockIOClusterNode2,
mockIORedisClient,
Expand All @@ -17,14 +18,15 @@ import {
mockSentinelMasterEndpoint,
mockSentinelMasterInDownState,
mockSentinelMasterInOkState,
mockStandaloneRedisInfoReply,
mockStandaloneRedisInfoReply, MockType,
} from 'src/__mocks__';
import { REDIS_MODULES_COMMANDS, AdditionalRedisModuleName } from 'src/constants';
import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider';
import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import { SentinelMasterStatus } from 'src/modules/redis-sentinel/models/sentinel-master';
import ERROR_MESSAGES from 'src/constants/error-messages';
import { FeatureService } from 'src/modules/feature/feature.service';

const mockClusterNodeAddresses: IRedisClusterNodeAddress[] = [
{
Expand Down Expand Up @@ -78,14 +80,22 @@ const mockSentinelConnectionOptions = {

describe('DatabaseInfoProvider', () => {
let service: DatabaseInfoProvider;
let featureService: MockType<FeatureService>;

beforeEach(async () => {
jest.clearAllMocks();
const module: TestingModule = await Test.createTestingModule({
providers: [DatabaseInfoProvider],
providers: [
DatabaseInfoProvider,
{
provide: FeatureService,
useFactory: mockFeatureService,
},
],
}).compile();

service = await module.get(DatabaseInfoProvider);
featureService = await module.get(FeatureService);
});

describe('isCluster', () => {
Expand Down Expand Up @@ -239,7 +249,7 @@ describe('DatabaseInfoProvider', () => {
});

describe('determineDatabaseModules', () => {
it('get modules by using MODULE LIST command', async () => {
it('get modules by using MODULE LIST command (without filters)', async () => {
when(mockIORedisClient.call)
.calledWith('module', ['list'])
.mockResolvedValue(mockRedisModuleList);
Expand All @@ -258,7 +268,37 @@ describe('DatabaseInfoProvider', () => {
{ name: 'customModule', version: 10000, semanticVersion: undefined },
]);
});
it('detect all modules by using COMMAND INFO command', async () => {
it('get modules by using MODULE LIST command (with filters applied)', async () => {
when(mockIORedisClient.call)
.calledWith('module', ['list'])
.mockResolvedValue(mockRedisModuleList);
featureService.getByName.mockResolvedValue({
flag: true,
data: {
hideByName: [
{
expression: 'rejSoN',
options: 'i',
},
],
},
});

const result = await service.determineDatabaseModules(mockIORedisClient);

expect(mockIORedisClient.call).not.toHaveBeenCalledWith('command', expect.anything());
expect(result).toEqual([
{ name: AdditionalRedisModuleName.RedisAI, version: 10000, semanticVersion: '1.0.0' },
{ name: AdditionalRedisModuleName.RedisGraph, version: 10000, semanticVersion: '1.0.0' },
{ name: AdditionalRedisModuleName.RedisGears, version: 10000, semanticVersion: '1.0.0' },
{ name: AdditionalRedisModuleName.RedisBloom, version: 10000, semanticVersion: '1.0.0' },
// { name: AdditionalRedisModuleName.RedisJSON, version: 10000, semanticVersion: '1.0.0' }, should be ignored
{ name: AdditionalRedisModuleName.RediSearch, version: 10000, semanticVersion: '1.0.0' },
{ name: AdditionalRedisModuleName.RedisTimeSeries, version: 10000, semanticVersion: '1.0.0' },
{ name: 'customModule', version: 10000, semanticVersion: undefined },
]);
});
it('detect all modules by using COMMAND INFO command (without filter)', async () => {
when(mockIORedisClient.call)
.calledWith('module', ['list'])
.mockRejectedValue(mockUnknownCommandModule);
Expand All @@ -282,6 +322,41 @@ describe('DatabaseInfoProvider', () => {
{ name: AdditionalRedisModuleName.RedisTimeSeries },
]);
});
it('detect all modules by using COMMAND INFO command (with filter)', async () => {
when(mockIORedisClient.call)
.calledWith('module', ['list'])
.mockRejectedValue(mockUnknownCommandModule);
when(mockIORedisClient.call)
.calledWith('command', expect.anything())
.mockResolvedValue([
null,
['somecommand', -1, ['readonly'], 0, 0, -1, []],
]);
featureService.getByName.mockResolvedValue({
flag: true,
data: {
hideByName: [
{
expression: 'rejSoN',
options: 'i',
},
],
},
});

const result = await service.determineDatabaseModules(mockIORedisClient);

expect(mockIORedisClient.call).toHaveBeenCalledTimes(REDIS_MODULES_COMMANDS.size + 1);
expect(result).toEqual([
{ name: AdditionalRedisModuleName.RedisAI },
{ name: AdditionalRedisModuleName.RedisGraph },
{ name: AdditionalRedisModuleName.RedisGears },
{ name: AdditionalRedisModuleName.RedisBloom },
// { name: AdditionalRedisModuleName.RedisJSON }, should be ignored
{ name: AdditionalRedisModuleName.RediSearch },
{ name: AdditionalRedisModuleName.RedisTimeSeries },
]);
});
it('detect only RediSearch module by using COMMAND INFO command', async () => {
when(mockIORedisClient.call)
.calledWith('module', ['list'])
Expand Down Expand Up @@ -372,7 +447,7 @@ describe('DatabaseInfoProvider', () => {
});
it('should throw an error if no permission to run \'info\' command', async () => {
mockIORedisClient.info.mockRejectedValue({
message: 'NOPERM this user has no permissions to run the \'info\' command'
message: 'NOPERM this user has no permissions to run the \'info\' command',
});

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ import { SentinelMaster, SentinelMasterStatus } from 'src/modules/redis-sentinel
import ERROR_MESSAGES from 'src/constants/error-messages';
import { Endpoint } from 'src/common/models';
import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';
import { FeatureService } from 'src/modules/feature/feature.service';
import { KnownFeatures } from 'src/modules/feature/constants';

@Injectable()
export class DatabaseInfoProvider {
constructor(
private readonly featureService: FeatureService,
) {}

/**
* Check weather current database is a cluster
* @param client
Expand Down Expand Up @@ -67,6 +73,27 @@ export class DatabaseInfoProvider {
}));
}

public async filterRawModules(modules: any[]): Promise<any[]> {
let filteredModules = modules;

try {
const filterModules = await this.featureService.getByName(KnownFeatures.RedisModuleFilter);

if (filterModules?.flag && filterModules.data?.hideByName?.length) {
filteredModules = modules.filter(({ name }) => {
const match = filterModules.data.hideByName.find((filter) => filter.expression
&& (new RegExp(filter.expression, filter.options)).test(name));

return !match;
});
}
} catch (e) {
// ignore
}

return filteredModules;
}

/**
* Determine database modules using "module list" command
* In case when "module" command is not available use "command info" approach
Expand All @@ -75,7 +102,10 @@ export class DatabaseInfoProvider {
public async determineDatabaseModules(client: any): Promise<AdditionalRedisModule[]> {
try {
const reply = await client.call('module', ['list']);
const modules = reply.map((module: any[]) => convertStringsArrayToObject(module));
const modules = await this.filterRawModules(
reply.map((module: any[]) => convertStringsArrayToObject(module)),
);

return modules.map(({ name, ver }) => ({
name: SUPPORTED_REDIS_MODULES[name] ?? name,
version: ver,
Expand Down Expand Up @@ -120,7 +150,8 @@ export class DatabaseInfoProvider {
// continue regardless of error
}
}));
return modules;

return await this.filterRawModules(modules);
}

/**
Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/src/modules/feature/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum FeatureConfigConfigDestination {
export enum KnownFeatures {
InsightsRecommendations = 'insightsRecommendations',
CloudSso = 'cloudSso',
RedisModuleFilter = 'redisModuleFilter',
}

export interface IFeatureFlag {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ export const knownFeatures: Record<KnownFeatures, IFeatureFlag> = {
storage: FeatureStorage.Database,
factory: CloudSsoFeatureFlag.getFeature,
},
[KnownFeatures.RedisModuleFilter]: {
name: KnownFeatures.RedisModuleFilter,
storage: FeatureStorage.Database,
},
};
8 changes: 8 additions & 0 deletions redisinsight/api/src/modules/feature/feature.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ export class FeatureService {
private readonly analytics: FeatureAnalytics,
) {}

async getByName(name: string): Promise<Feature> {
try {
return await this.repository.get(name);
} catch (e) {
return null;
}
}

/**
* Check if feature enabled
* @param name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SettingsService } from 'src/modules/settings/settings.service';
import { IFeatureFlag, KnownFeatures } from 'src/modules/feature/constants';
import { CloudSsoFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/cloud-sso.flag.strategy';
import { Feature } from 'src/modules/feature/model/feature';
import { SimpleFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/simple.flag.strategy';

@Injectable()
export class FeatureFlagProvider {
Expand All @@ -30,6 +31,10 @@ export class FeatureFlagProvider {
this.featuresConfigService,
this.settingsService,
));
this.strategies.set(KnownFeatures.RedisModuleFilter, new SimpleFlagStrategy(
this.featuresConfigService,
this.settingsService,
));
}

getStrategy(name: string): FeatureFlagStrategy {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FeatureFlagStrategy } from 'src/modules/feature/providers/feature-flag/strategies/feature.flag.strategy';
import { Feature } from 'src/modules/feature/model/feature';
import { IFeatureFlag } from 'src/modules/feature/constants';

export class SimpleFlagStrategy extends FeatureFlagStrategy {
async calculate(knownFeature: IFeatureFlag, featureConfig: any): Promise<Feature> {
const isInRange = await this.isInTargetRange(featureConfig?.perc);

return {
name: knownFeature.name,
flag: isInRange && await this.filter(featureConfig?.filters) ? !!featureConfig?.flag : !featureConfig?.flag,
data: featureConfig?.data,
};
}
}
2 changes: 1 addition & 1 deletion redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const ICON_PATH = app.isPackaged

export const AboutPanelOptions = {
applicationName: 'RedisInsight-v2',
applicationVersion: `${app.getVersion() || '2.30.0'}${
applicationVersion: `${app.getVersion() || '2.32.0'}${
!config.isProduction ? `-dev-${process.getCreationTime()}` : ''
}`,
copyright: `Copyright © ${new Date().getFullYear()} Redis Ltd.`,
Expand Down
8 changes: 7 additions & 1 deletion redisinsight/desktop/src/lib/app/app.handlers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { app } from 'electron'
import log from 'electron-log'
import { getBackendGracefulShutdown, WindowType, getWindows, windowFactory, windows } from 'desktopSrc/lib'
import { deepLinkHandler } from 'desktopSrc/lib/app/deep-link.handlers';
import { deepLinkHandler } from 'desktopSrc/lib/app/deep-link.handlers'

export const initAppHandlers = () => {
app.on('activate', () => {
Expand Down Expand Up @@ -49,6 +49,12 @@ export const initAppHandlers = () => {
event.preventDefault()
// todo: implement url handler to map url to a proper function
await deepLinkHandler(url)

if (windows.size) {
const win = windows.values().next().value
if (win.isMinimized()) win.restore()
win.focus()
}
})

// deep link open (win)
Expand Down
Loading