Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions redisinsight/api/src/constants/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default {
'Key with this name does not exist or does not have an associated timeout.',
SERVER_NOT_AVAILABLE: 'Server is not available. Please try again later.',
REDIS_CLOUD_FORBIDDEN: 'Error fetching account details.',
NO_INFO_COMMAND_PERMISSION: 'has no permissions to run the \'info\' command',

DATABASE_IS_INACTIVE: 'The database is inactive.',
DATABASE_ALREADY_EXISTS: 'The database already exists.',
Expand Down
45 changes: 45 additions & 0 deletions redisinsight/api/src/modules/database/dto/redis-info.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,48 @@ export class RedisDatabaseModuleDto {
})
ver?: number;
}

export class RedisDatabaseHelloResponse {
@ApiProperty({
description: 'Redis database id',
type: Number,
})
id: number;

@ApiProperty({
description: 'Redis database server name',
type: String,
})
server: string;

@ApiProperty({
description: 'Redis database version',
type: String,
})
version: string;

@ApiProperty({
description: 'Redis database proto',
type: Number,
})
proto: number;

@ApiProperty({
description: 'Redis database mode',
type: String,
})
mode: "standalone" | "sentinel" | "cluster";

@ApiProperty({
description: 'Redis database role',
type: String,
})
role: 'master' | 'slave';

@ApiProperty({
description: 'Redis database modules',
type: RedisDatabaseModuleDto,
isArray: true,
})
modules: RedisDatabaseModuleDto[]
}
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,8 @@ describe('DatabaseInfoProvider', () => {

describe('determineDatabaseServer', () => {
it('get modules by using MODULE LIST command', async () => {
when(standaloneClient.call)
.calledWith(['info', 'server'], expect.anything())
when(standaloneClient.sendCommand)
.calledWith(['info'], expect.anything())
.mockResolvedValue(mockRedisServerInfoResponse);

const result = await service.determineDatabaseServer(standaloneClient);
Expand Down Expand Up @@ -379,13 +379,52 @@ describe('DatabaseInfoProvider', () => {
nodes: [mockRedisGeneralInfo, mockRedisGeneralInfo],
});
});
it('should throw an error if no permission to run \'info\' command', async () => {
it('should get info from hello command when info command is not available', async () => {
when(standaloneClient.sendCommand)
.calledWith(['info'], { replyEncoding: 'utf8' })
.mockRejectedValue({
message: 'NOPERM this user has no permissions to run the \'info\' command',
});

when(standaloneClient.sendCommand)
.calledWith(['hello'], { replyEncoding: 'utf8' })
.mockResolvedValue([
'version', mockRedisGeneralInfo.version,
'mode', mockRedisServerInfoDto.redis_mode,
'role', mockRedisGeneralInfo.role,
'server', 'redis',
]);

const result = await service.getRedisGeneralInfo(standaloneClient);

expect(result).toEqual({
...mockRedisGeneralInfo,
server: {
redis_mode: mockRedisServerInfoDto.redis_mode,
redis_version: mockRedisGeneralInfo.version,
server_name: 'redis',
},
uptimeInSeconds: undefined,
totalKeys: undefined,
usedMemory: undefined,
hitRatio: undefined,
connectedClients: undefined,
cashedScripts: undefined,
});
});
it('should throw an error if no permission to run \'info\' and \'hello\' commands', async () => {
when(standaloneClient.sendCommand)
.calledWith(['info'], { replyEncoding: 'utf8' })
.mockRejectedValue({
message: 'NOPERM this user has no permissions to run the \'info\' command',
});

when(standaloneClient.sendCommand)
.calledWith(['hello'], { replyEncoding: 'utf8' })
.mockRejectedValue({
message: 'NOPERM this user has no permissions to run the \'hello\' command',
});

try {
await service.getRedisGeneralInfo(standaloneClient);
fail('Should throw an error');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import { Injectable } from '@nestjs/common';
import {
calculateRedisHitRatio,
catchAclError,
convertArrayOfKeyValuePairsToObject,
convertIntToSemanticVersion,
convertRedisInfoReplyToObject,
} from 'src/utils';
import { AdditionalRedisModule } from 'src/modules/database/models/additional.redis.module';
import { REDIS_MODULES_COMMANDS, SUPPORTED_REDIS_MODULES } from 'src/constants';
import { get, isNil } from 'lodash';
import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';
import { RedisDatabaseHelloResponse, RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';
import { FeatureService } from 'src/modules/feature/feature.service';
import { KnownFeatures } from 'src/modules/feature/constants';
import { convertArrayReplyToObject, convertMultilineReplyToObject } from 'src/modules/redis/utils';
import { RedisClient, RedisClientConnectionType } from 'src/modules/redis/client';
import ERROR_MESSAGES from 'src/constants/error-messages';
import { SessionMetadata } from 'src/common/models';
import { plainToClass } from 'class-transformer';

@Injectable()
export class DatabaseInfoProvider {
Expand Down Expand Up @@ -76,10 +79,7 @@ export class DatabaseInfoProvider {
*/
public async determineDatabaseServer(client: RedisClient): Promise<string> {
try {
const reply = convertRedisInfoReplyToObject(await client.call(
['info', 'server'],
{ replyEncoding: 'utf8' },
) as string);
const reply = await this.getRedisInfo(client);
return reply['server']?.redis_version;
} catch (e) {
// continue regardless of error
Expand Down Expand Up @@ -135,10 +135,7 @@ export class DatabaseInfoProvider {
client: RedisClient,
): Promise<RedisDatabaseInfoResponse> {
try {
const info = convertRedisInfoReplyToObject(await client.sendCommand(
['info'],
{ replyEncoding: 'utf8' },
) as string);
const info = await this.getRedisInfo(client);
const serverInfo = info['server'];
const memoryInfo = info['memory'];
const keyspaceInfo = info['keyspace'];
Expand Down Expand Up @@ -261,4 +258,54 @@ export class DatabaseInfoProvider {
throw catchAclError(e);
}
}

public async getRedisInfo(client: RedisClient) {
try {
return convertRedisInfoReplyToObject(
(await client.sendCommand(['info'], {
replyEncoding: 'utf8',
})) as string,
);
} catch (error) {
if (error.message.includes(ERROR_MESSAGES.NO_INFO_COMMAND_PERMISSION)) {
// Fallback to getting basic information from `hello` command
return this.getRedisHelloInfo(client);
}

throw error;
}
}

private async getRedisHelloInfo(client: RedisClient) {
const helloResponse = await this.getRedisHelloResponse(client);

return {
replication: {
role: helloResponse.role,
},
server: {
server_name: helloResponse.server,
redis_version: helloResponse.version,
redis_mode: helloResponse.mode,
},
};
}

private async getRedisHelloResponse(client: RedisClient): Promise<RedisDatabaseHelloResponse> {
try {
const helloResponse = (await client.sendCommand(['hello'], {
replyEncoding: 'utf8',
})) as any[];

const helloInfoResponse = convertArrayOfKeyValuePairsToObject(helloResponse);

if (helloInfoResponse.modules?.length) {
helloInfoResponse.modules = helloInfoResponse.modules.map(convertArrayOfKeyValuePairsToObject);
}

return plainToClass(RedisDatabaseHelloResponse, helloInfoResponse)
} catch (e) {
throw catchAclError(e);
}
}
}
30 changes: 30 additions & 0 deletions redisinsight/api/src/utils/converter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
convertArrayOfKeyValuePairsToObject,
convertIntToSemanticVersion,
convertStringToNumber,
} from './converter';
Expand Down Expand Up @@ -49,3 +50,32 @@ describe('convertStringToNumber', () => {
});
});
});

describe('convertArrayOfKeyValuePairsToObject', () => {
it('should convert array of key value pairs to object', () => {
const input = ['key1', 'value1', 'key2', 'value2'];
const output = { key1: 'value1', key2: 'value2' };

const result = convertArrayOfKeyValuePairsToObject(input);

expect(result).toEqual(output);
});

it('should convert array of key value pairs to object with odd number of elements', () => {
const input = ['key1', 'value1', 'key2'];
const output = { key1: 'value1' };

const result = convertArrayOfKeyValuePairsToObject(input);

expect(result).toEqual(output);
});

it('should convert empty array to empty object', () => {
const input: any[] = [];
const output = {};

const result = convertArrayOfKeyValuePairsToObject(input);

expect(result).toEqual(output);
});
});
12 changes: 12 additions & 0 deletions redisinsight/api/src/utils/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,15 @@ export const convertStringToNumber = (value: any, defaultValue?: number): number

return num;
};

export const convertArrayOfKeyValuePairsToObject = (array: any[]) => {
const result: Record<string, any> = {};

for (let i = 0; i + 1 < array.length; i += 2) {
const key = array[i];
const value = array[i + 1];
result[key] = value;
}

return result;
};
Loading