Skip to content

Commit c960ea4

Browse files
Merge pull request #3414 from RedisInsight/feature/RI-5661_rdi_auth
Feature/ri 5661 rdi auth
2 parents 4fc73f8 + fbb053d commit c960ea4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1259
-342
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class Rdi1716370509836 implements MigrationInterface {
4+
name = 'Rdi1716370509836'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
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)`);
8+
}
9+
10+
public async down(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(`DROP TABLE "rdi"`);
12+
}
13+
14+
}

redisinsight/api/migration/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { CloudCapiKeys1691061058385 } from './1691061058385-cloud-capi-keys';
4040
import { FeatureSso1691476419592 } from './1691476419592-feature-sso';
4141
import { AiHistory1713515657364 } from './1713515657364-ai-history';
4242
import { AiHistorySteps1714501203616 } from './1714501203616-ai-history-steps';
43+
import { Rdi1716370509836 } from './1716370509836-rdi';
4344

4445
export default [
4546
initialMigration1614164490968,
@@ -84,4 +85,5 @@ export default [
8485
FeatureSso1691476419592,
8586
AiHistory1713515657364,
8687
AiHistorySteps1714501203616,
88+
Rdi1716370509836,
8789
];

redisinsight/api/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"test:api:cov": "nyc --reporter=html --reporter=text --reporter=text-summary yarn run test:api",
3535
"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",
3636
"typeorm:migrate": "cross-env NODE_ENV=staging yarn typeorm migration:generate ./migration/migration",
37-
"typeorm:run": "yarn typeorm migration:run"
37+
"typeorm:run": "yarn typeorm migration:run",
38+
"typeorm:run:stage": "cross-env NODE_ENV=staging yarn typeorm migration:run"
3839
},
3940
"resolutions": {
4041
"nanoid": "^3.1.31",

redisinsight/api/src/__mocks__/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ export * from './session';
3939
export * from './cloud-session';
4040
export * from './database-info';
4141
export * from './cloud-job';
42+
export * from './rdi';
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {
2+
Rdi,
3+
RdiClientMetadata,
4+
} from 'src/modules/rdi/models';
5+
import { ApiRdiClient } from 'src/modules/rdi/client/api.rdi.client';
6+
7+
export const mockRdiId = 'rdiId';
8+
9+
export class MockRdiClient extends ApiRdiClient {
10+
constructor(metadata: RdiClientMetadata, client: any = jest.fn()) {
11+
super(metadata, client);
12+
}
13+
14+
public getSchema = jest.fn();
15+
16+
public getPipeline = jest.fn();
17+
18+
public getTemplate = jest.fn();
19+
20+
public getStrategies = jest.fn();
21+
22+
public deploy = jest.fn();
23+
24+
public deployJob = jest.fn();
25+
26+
public dryRunJob = jest.fn();
27+
28+
public testConnections = jest.fn();
29+
30+
public getStatistics = jest.fn();
31+
32+
public getPipelineStatus = jest.fn();
33+
34+
public getJobFunctions = jest.fn();
35+
36+
public connect = jest.fn();
37+
38+
public ensureAuth = jest.fn();
39+
}
40+
41+
export const generateMockRdiClient = (
42+
metadata: RdiClientMetadata,
43+
client = jest.fn(),
44+
): MockRdiClient => new MockRdiClient(metadata as RdiClientMetadata, client);
45+
46+
export const mockRdiClientMetadata: RdiClientMetadata = {
47+
sessionMetadata: undefined,
48+
id: mockRdiId,
49+
};
50+
51+
export const mockRdi = Object.assign(new Rdi(), {
52+
name: 'name',
53+
version: '1.2',
54+
url: 'http://localhost:4000',
55+
password: 'pass',
56+
username: 'user',
57+
});
58+
59+
export const mockRdiUnauthorizedError = {
60+
message: 'Request failed with status code 401',
61+
response: {
62+
status: 401,
63+
},
64+
};

redisinsight/api/src/constants/custom-error-codes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,6 @@ export enum CustomErrorCodes {
5252

5353
// RDI errors [11400, 11599]
5454
RdiDeployPipelineFailure = 11_401,
55+
RdiUnauthorized = 11_402,
56+
RdiInternalServerError = 11_403,
5557
}

redisinsight/api/src/constants/error-messages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,5 @@ export default {
105105
COMMON_DEFAULT_IMPORT_ERROR: 'Unable to import default data',
106106

107107
RDI_DEPLOY_PIPELINE_FAILURE: 'Failed to deploy pipeline',
108+
RDI_TIMEOUT_ERROR: 'Encountered a timeout error while attempting to retrieve data',
108109
};
Lines changed: 134 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,127 @@
1-
import { AxiosInstance } from 'axios';
1+
import axios, { AxiosInstance } from 'axios';
22
import { plainToClass } from 'class-transformer';
3+
import { decode } from 'jsonwebtoken';
34

45
import { RdiClient } from 'src/modules/rdi/client/rdi.client';
5-
import { RdiUrl } from 'src/modules/rdi/constants';
6-
import { RdiDryRunJobDto, RdiDryRunJobResponseDto, RdiTestConnectionResult } from 'src/modules/rdi/dto';
7-
import { RdiPipelineDeployFailedException } from 'src/modules/rdi/exceptions';
86
import {
9-
RdiJob,
7+
RdiUrl,
8+
RDI_TIMEOUT,
9+
TOKEN_TRESHOLD,
10+
POLLING_INTERVAL,
11+
MAX_POLLING_TIME,
12+
} from 'src/modules/rdi/constants';
13+
import {
14+
RdiDryRunJobDto,
15+
RdiDryRunJobResponseDto,
16+
RdiTestConnectionsResponseDto,
17+
} from 'src/modules/rdi/dto';
18+
import {
19+
RdiPipelineDeployFailedException,
20+
RdiPipelineInternalServerErrorException,
21+
wrapRdiPipelineError,
22+
} from 'src/modules/rdi/exceptions';
23+
import {
1024
RdiPipeline,
1125
RdiStatisticsResult,
12-
RdiType,
13-
RdiDryRunJobResult,
14-
RdiDyRunJobStatus,
1526
RdiStatisticsStatus,
16-
RdiStatisticsData,
27+
RdiStatisticsData, RdiClientMetadata, Rdi,
1728
} from 'src/modules/rdi/models';
1829
import { convertKeysToCamelCase } from 'src/utils/base.helper';
30+
import { RdiPipelineTimeoutException } from 'src/modules/rdi/exceptions/rdi-pipeline.timeout-error.exception';
1931

2032
const RDI_DEPLOY_FAILED_STATUS = 'failed';
2133

2234
export class ApiRdiClient extends RdiClient {
23-
public type = RdiType.API;
24-
2535
protected readonly client: AxiosInstance;
2636

27-
async isConnected(): Promise<boolean> {
28-
// todo: check if needed and possible
29-
return true;
37+
private auth: { jwt: string, exp: number };
38+
39+
constructor(clientMetadata: RdiClientMetadata, rdi: Rdi) {
40+
super(clientMetadata, rdi);
41+
this.client = axios.create({
42+
baseURL: rdi.url,
43+
timeout: RDI_TIMEOUT,
44+
});
3045
}
3146

3247
async getSchema(): Promise<object> {
33-
const response = await this.client.get(RdiUrl.GetSchema);
34-
return response.data;
48+
try {
49+
const response = await this.client.get(RdiUrl.GetSchema);
50+
return response.data;
51+
} catch (e) {
52+
throw wrapRdiPipelineError(e);
53+
}
3554
}
3655

3756
async getPipeline(): Promise<RdiPipeline> {
38-
const response = await this.client.get(RdiUrl.GetPipeline);
39-
return response.data;
57+
try {
58+
const response = await this.client.get(RdiUrl.GetPipeline);
59+
return response.data;
60+
} catch (e) {
61+
throw wrapRdiPipelineError(e);
62+
}
4063
}
4164

4265
async getStrategies(): Promise<object> {
43-
const response = await this.client.get(RdiUrl.GetStrategies);
44-
return response.data;
66+
try {
67+
const response = await this.client.get(RdiUrl.GetStrategies);
68+
return response.data;
69+
} catch (e) {
70+
throw wrapRdiPipelineError(e);
71+
}
4572
}
4673

4774
async getTemplate(options: object): Promise<object> {
48-
const response = await this.client.get(RdiUrl.GetTemplate, { params: options });
49-
return response.data;
75+
try {
76+
const response = await this.client.get(RdiUrl.GetTemplate, { params: options });
77+
return response.data;
78+
} catch (error) {
79+
throw wrapRdiPipelineError(error);
80+
}
5081
}
5182

5283
async deploy(pipeline: RdiPipeline): Promise<void> {
53-
const response = await this.client.post(RdiUrl.Deploy, { ...pipeline });
84+
let response;
85+
try {
86+
response = await this.client.post(RdiUrl.Deploy, { ...pipeline });
87+
} catch (error) {
88+
throw wrapRdiPipelineError(error, error.response.data.message);
89+
}
5490

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

60-
async deployJob(job: RdiJob): Promise<RdiJob> {
61-
return null;
62-
}
63-
6496
async dryRunJob(data: RdiDryRunJobDto): Promise<RdiDryRunJobResponseDto> {
65-
const response = await this.client.post(RdiUrl.DryRunJob, data);
66-
return response.data;
97+
try {
98+
const response = await this.client.post(RdiUrl.DryRunJob, data);
99+
return response.data;
100+
} catch (e) {
101+
throw wrapRdiPipelineError(e);
102+
}
67103
}
68104

69-
async testConnections(config: string): Promise<RdiTestConnectionResult> {
70-
const response = await this.client.post(RdiUrl.TestConnections, config);
105+
async testConnections(config: string): Promise<RdiTestConnectionsResponseDto> {
106+
try {
107+
const response = await this.client.post(RdiUrl.TestConnections, config);
108+
109+
const actionId = response.data.action_id;
71110

72-
return response.data;
111+
return this.pollActionStatus(actionId);
112+
} catch (e) {
113+
throw wrapRdiPipelineError(e);
114+
}
73115
}
74116

75117
async getPipelineStatus(): Promise<any> {
76-
const response = await this.client.get(RdiUrl.GetPipelineStatus);
118+
try {
119+
const response = await this.client.get(RdiUrl.GetPipelineStatus);
77120

78-
return response.data;
121+
return response.data;
122+
} catch (e) {
123+
throw wrapRdiPipelineError(e);
124+
}
79125
}
80126

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

93139
async getJobFunctions(): Promise<object> {
94-
const response = await this.client.post(RdiUrl.JobFunctions);
95-
return response.data;
140+
try {
141+
const response = await this.client.post(RdiUrl.JobFunctions);
142+
return response.data;
143+
} catch (e) {
144+
throw wrapRdiPipelineError(e);
145+
}
96146
}
97147

98-
async disconnect(): Promise<void> {
99-
return undefined;
148+
async connect(): Promise<void> {
149+
try {
150+
const response = await this.client.post(
151+
RdiUrl.Login,
152+
{ username: this.rdi.username, password: this.rdi.password },
153+
);
154+
const accessToken = response.data.access_token;
155+
const decodedJwt = decode(accessToken);
156+
157+
this.auth = { jwt: accessToken, exp: decodedJwt.exp };
158+
this.client.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
159+
} catch (e) {
160+
throw wrapRdiPipelineError(e);
161+
}
162+
}
163+
164+
async ensureAuth(): Promise<void> {
165+
const expiresIn = this.auth.exp * 1_000 - Date.now();
166+
167+
if (expiresIn < TOKEN_TRESHOLD) {
168+
await this.connect();
169+
}
170+
}
171+
172+
private async pollActionStatus(actionId: string): Promise<any> {
173+
const startTime = Date.now();
174+
while (true) {
175+
if (Date.now() - startTime > MAX_POLLING_TIME) {
176+
throw new RdiPipelineTimeoutException();
177+
}
178+
179+
try {
180+
const response = await this.client.get(`${RdiUrl.Action}/${actionId}`);
181+
const { status, data, error } = response.data;
182+
183+
if (status === 'failed') {
184+
throw new RdiPipelineInternalServerErrorException(error);
185+
}
186+
187+
if (status === 'completed') {
188+
return data;
189+
}
190+
} catch (e) {
191+
throw wrapRdiPipelineError(e);
192+
}
193+
194+
await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL));
195+
}
100196
}
101197
}

0 commit comments

Comments
 (0)