Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d82ea7b
#RI-5661 - add check rdi connection
AmirAllayarovSofteq May 15, 2024
e9f687d
#RI-5661 - add rdi auth
AmirAllayarovSofteq May 22, 2024
df52fd6
#RI-5661 - remove commented code
AmirAllayarovSofteq May 22, 2024
b35d09e
#RI-5619 - update rdi test connections
AmirAllayarovSofteq May 24, 2024
16322a9
#RI-5619 - add action result requests
AmirAllayarovSofteq May 24, 2024
894d81a
#RI-5619 - add trailing comma
AmirAllayarovSofteq May 24, 2024
918bfc4
#RI-5661 - update rdi internal error
AmirAllayarovSofteq May 24, 2024
ad36948
#RI-5619 - update interfaces
AmirAllayarovSofteq May 24, 2024
2760059
#RI-5619 - update transform function
AmirAllayarovSofteq May 24, 2024
8c1b23c
#RI-5619 - fix test
AmirAllayarovSofteq May 24, 2024
d4e05e7
#RI-5619 - resolve comments
AmirAllayarovSofteq May 24, 2024
0c31b53
#RI-5661 - add test
AmirAllayarovSofteq May 24, 2024
5f87925
#RI-5661 - add test
AmirAllayarovSofteq May 24, 2024
9feec72
#RI-5661 - resolve comments
AmirAllayarovSofteq May 27, 2024
3381957
Merge pull request #3409 from RedisInsight/be/feature/RI-5619_update_…
AmirAllayarovSofteq May 27, 2024
064cf8d
Merge pull request #3408 from RedisInsight/fe/feature/RI-5619_update_…
AmirAllayarovSofteq May 27, 2024
db0a4d9
Merge pull request #3406 from RedisInsight/be/feature/RI-5661_rdi_auth
AmirAllayarovSofteq May 27, 2024
70264fd
#RI-5661 - fix lint error
AmirAllayarovSofteq May 27, 2024
e53c80b
Merge pull request #3386 from RedisInsight/fe/feature/RI-5661_rdi_auth
AmirAllayarovSofteq May 27, 2024
0242375
#RI-5752 - send only updated values in edit action
AmirAllayarovSofteq May 28, 2024
f3a24f8
#RI-5752 - resolve comments
AmirAllayarovSofteq May 28, 2024
65a6bfa
#RI-5752 - update edit rdi instance
AmirAllayarovSofteq May 28, 2024
9b84636
Merge pull request #3420 from RedisInsight/be/feature/RI-5752_update_…
AmirAllayarovSofteq May 29, 2024
24e9046
Merge pull request #3419 from RedisInsight/fe/feature/RI-5752_edit_rd…
AmirAllayarovSofteq May 29, 2024
fbb053d
Merge branch 'feature/RI-4616-rdi-support' of https://github.com/Redi…
AmirAllayarovSofteq May 29, 2024
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
14 changes: 14 additions & 0 deletions redisinsight/api/migration/1716370509836-rdi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Rdi1716370509836 implements MigrationInterface {
name = 'Rdi1716370509836'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "rdi" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar, "name" varchar NOT NULL, "username" varchar NOT NULL, "password" varchar NOT NULL, "lastConnection" datetime, "version" varchar NOT NULL, "encryption" varchar)`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "rdi"`);
}

}
2 changes: 2 additions & 0 deletions redisinsight/api/migration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { CloudCapiKeys1691061058385 } from './1691061058385-cloud-capi-keys';
import { FeatureSso1691476419592 } from './1691476419592-feature-sso';
import { AiHistory1713515657364 } from './1713515657364-ai-history';
import { AiHistorySteps1714501203616 } from './1714501203616-ai-history-steps';
import { Rdi1716370509836 } from './1716370509836-rdi';

export default [
initialMigration1614164490968,
Expand Down Expand Up @@ -84,4 +85,5 @@ export default [
FeatureSso1691476419592,
AiHistory1713515657364,
AiHistorySteps1714501203616,
Rdi1716370509836,
];
3 changes: 2 additions & 1 deletion redisinsight/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"test:api:cov": "nyc --reporter=html --reporter=text --reporter=text-summary yarn run test:api",
"test:api:ci:cov": "cross-env nyc -r text -r text-summary -r html yarn run test:api --reporter mocha-multi-reporters --reporter-options configFile=test/api/reporters.json && nyc merge .nyc_output ./coverage/test-run-coverage.json",
"typeorm:migrate": "cross-env NODE_ENV=staging yarn typeorm migration:generate ./migration/migration",
"typeorm:run": "yarn typeorm migration:run"
"typeorm:run": "yarn typeorm migration:run",
"typeorm:run:stage": "cross-env NODE_ENV=staging yarn typeorm migration:run"
},
"resolutions": {
"nanoid": "^3.1.31",
Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/src/__mocks__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ export * from './session';
export * from './cloud-session';
export * from './database-info';
export * from './cloud-job';
export * from './rdi';
64 changes: 64 additions & 0 deletions redisinsight/api/src/__mocks__/rdi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
Rdi,
RdiClientMetadata,
} from 'src/modules/rdi/models';
import { ApiRdiClient } from 'src/modules/rdi/client/api.rdi.client';

export const mockRdiId = 'rdiId';

export class MockRdiClient extends ApiRdiClient {
constructor(metadata: RdiClientMetadata, client: any = jest.fn()) {
super(metadata, client);
}

public getSchema = jest.fn();

public getPipeline = jest.fn();

public getTemplate = jest.fn();

public getStrategies = jest.fn();

public deploy = jest.fn();

public deployJob = jest.fn();

public dryRunJob = jest.fn();

public testConnections = jest.fn();

public getStatistics = jest.fn();

public getPipelineStatus = jest.fn();

public getJobFunctions = jest.fn();

public connect = jest.fn();

public ensureAuth = jest.fn();
}

export const generateMockRdiClient = (
metadata: RdiClientMetadata,
client = jest.fn(),
): MockRdiClient => new MockRdiClient(metadata as RdiClientMetadata, client);

export const mockRdiClientMetadata: RdiClientMetadata = {
sessionMetadata: undefined,
id: mockRdiId,
};

export const mockRdi = Object.assign(new Rdi(), {
name: 'name',
version: '1.2',
url: 'http://localhost:4000',
password: 'pass',
username: 'user',
});

export const mockRdiUnauthorizedError = {
message: 'Request failed with status code 401',
response: {
status: 401,
},
};
2 changes: 2 additions & 0 deletions redisinsight/api/src/constants/custom-error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ export enum CustomErrorCodes {

// RDI errors [11400, 11599]
RdiDeployPipelineFailure = 11_401,
RdiUnauthorized = 11_402,
RdiInternalServerError = 11_403,
}
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 @@ -105,4 +105,5 @@ export default {
COMMON_DEFAULT_IMPORT_ERROR: 'Unable to import default data',

RDI_DEPLOY_PIPELINE_FAILURE: 'Failed to deploy pipeline',
RDI_TIMEOUT_ERROR: 'Encountered a timeout error while attempting to retrieve data',
};
172 changes: 134 additions & 38 deletions redisinsight/api/src/modules/rdi/client/api.rdi.client.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,127 @@
import { AxiosInstance } from 'axios';
import axios, { AxiosInstance } from 'axios';
import { plainToClass } from 'class-transformer';
import { decode } from 'jsonwebtoken';

import { RdiClient } from 'src/modules/rdi/client/rdi.client';
import { RdiUrl } from 'src/modules/rdi/constants';
import { RdiDryRunJobDto, RdiDryRunJobResponseDto, RdiTestConnectionResult } from 'src/modules/rdi/dto';
import { RdiPipelineDeployFailedException } from 'src/modules/rdi/exceptions';
import {
RdiJob,
RdiUrl,
RDI_TIMEOUT,
TOKEN_TRESHOLD,
POLLING_INTERVAL,
MAX_POLLING_TIME,
} from 'src/modules/rdi/constants';
import {
RdiDryRunJobDto,
RdiDryRunJobResponseDto,
RdiTestConnectionsResponseDto,
} from 'src/modules/rdi/dto';
import {
RdiPipelineDeployFailedException,
RdiPipelineInternalServerErrorException,
wrapRdiPipelineError,
} from 'src/modules/rdi/exceptions';
import {
RdiPipeline,
RdiStatisticsResult,
RdiType,
RdiDryRunJobResult,
RdiDyRunJobStatus,
RdiStatisticsStatus,
RdiStatisticsData,
RdiStatisticsData, RdiClientMetadata, Rdi,
} from 'src/modules/rdi/models';
import { convertKeysToCamelCase } from 'src/utils/base.helper';
import { RdiPipelineTimeoutException } from 'src/modules/rdi/exceptions/rdi-pipeline.timeout-error.exception';

const RDI_DEPLOY_FAILED_STATUS = 'failed';

export class ApiRdiClient extends RdiClient {
public type = RdiType.API;

protected readonly client: AxiosInstance;

async isConnected(): Promise<boolean> {
// todo: check if needed and possible
return true;
private auth: { jwt: string, exp: number };

constructor(clientMetadata: RdiClientMetadata, rdi: Rdi) {
super(clientMetadata, rdi);
this.client = axios.create({
baseURL: rdi.url,
timeout: RDI_TIMEOUT,
});
}

async getSchema(): Promise<object> {
const response = await this.client.get(RdiUrl.GetSchema);
return response.data;
try {
const response = await this.client.get(RdiUrl.GetSchema);
return response.data;
} catch (e) {
throw wrapRdiPipelineError(e);
}
}

async getPipeline(): Promise<RdiPipeline> {
const response = await this.client.get(RdiUrl.GetPipeline);
return response.data;
try {
const response = await this.client.get(RdiUrl.GetPipeline);
return response.data;
} catch (e) {
throw wrapRdiPipelineError(e);
}
}

async getStrategies(): Promise<object> {
const response = await this.client.get(RdiUrl.GetStrategies);
return response.data;
try {
const response = await this.client.get(RdiUrl.GetStrategies);
return response.data;
} catch (e) {
throw wrapRdiPipelineError(e);
}
}

async getTemplate(options: object): Promise<object> {
const response = await this.client.get(RdiUrl.GetTemplate, { params: options });
return response.data;
try {
const response = await this.client.get(RdiUrl.GetTemplate, { params: options });
return response.data;
} catch (error) {
throw wrapRdiPipelineError(error);
}
}

async deploy(pipeline: RdiPipeline): Promise<void> {
const response = await this.client.post(RdiUrl.Deploy, { ...pipeline });
let response;
try {
response = await this.client.post(RdiUrl.Deploy, { ...pipeline });
} catch (error) {
throw wrapRdiPipelineError(error, error.response.data.message);
}

if (response.data?.status === RDI_DEPLOY_FAILED_STATUS) {
throw new RdiPipelineDeployFailedException(undefined, { error: response.data?.error });
}
}

async deployJob(job: RdiJob): Promise<RdiJob> {
return null;
}

async dryRunJob(data: RdiDryRunJobDto): Promise<RdiDryRunJobResponseDto> {
const response = await this.client.post(RdiUrl.DryRunJob, data);
return response.data;
try {
const response = await this.client.post(RdiUrl.DryRunJob, data);
return response.data;
} catch (e) {
throw wrapRdiPipelineError(e);
}
}

async testConnections(config: string): Promise<RdiTestConnectionResult> {
const response = await this.client.post(RdiUrl.TestConnections, config);
async testConnections(config: string): Promise<RdiTestConnectionsResponseDto> {
try {
const response = await this.client.post(RdiUrl.TestConnections, config);

const actionId = response.data.action_id;

return response.data;
return this.pollActionStatus(actionId);
} catch (e) {
throw wrapRdiPipelineError(e);
}
}

async getPipelineStatus(): Promise<any> {
const response = await this.client.get(RdiUrl.GetPipelineStatus);
try {
const response = await this.client.get(RdiUrl.GetPipelineStatus);

return response.data;
return response.data;
} catch (e) {
throw wrapRdiPipelineError(e);
}
}

async getStatistics(sections?: string): Promise<RdiStatisticsResult> {
Expand All @@ -91,11 +137,61 @@ export class ApiRdiClient extends RdiClient {
}

async getJobFunctions(): Promise<object> {
const response = await this.client.post(RdiUrl.JobFunctions);
return response.data;
try {
const response = await this.client.post(RdiUrl.JobFunctions);
return response.data;
} catch (e) {
throw wrapRdiPipelineError(e);
}
}

async disconnect(): Promise<void> {
return undefined;
async connect(): Promise<void> {
try {
const response = await this.client.post(
RdiUrl.Login,
{ username: this.rdi.username, password: this.rdi.password },
);
const accessToken = response.data.access_token;
const decodedJwt = decode(accessToken);

this.auth = { jwt: accessToken, exp: decodedJwt.exp };
this.client.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
} catch (e) {
throw wrapRdiPipelineError(e);
}
}

async ensureAuth(): Promise<void> {
const expiresIn = this.auth.exp * 1_000 - Date.now();

if (expiresIn < TOKEN_TRESHOLD) {
await this.connect();
}
}

private async pollActionStatus(actionId: string): Promise<any> {
const startTime = Date.now();
while (true) {
if (Date.now() - startTime > MAX_POLLING_TIME) {
throw new RdiPipelineTimeoutException();
}

try {
const response = await this.client.get(`${RdiUrl.Action}/${actionId}`);
const { status, data, error } = response.data;

if (status === 'failed') {
throw new RdiPipelineInternalServerErrorException(error);
}

if (status === 'completed') {
return data;
}
} catch (e) {
throw wrapRdiPipelineError(e);
}

await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL));
}
}
}
Loading