Skip to content

Commit

Permalink
Refactored the the dependency on robot.http away.
Browse files Browse the repository at this point in the history
  • Loading branch information
KeesCBakker committed Apr 30, 2024
1 parent f20c3a7 commit fc68e14
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 145 deletions.
2 changes: 1 addition & 1 deletion src/Bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class Bot {
return null;
}

let client = new GrafanaClient(robot.http, robot.logger, host, apiKey);
let client = new GrafanaClient(robot.logger, host, apiKey);
return new GrafanaService(client);
}

Expand Down
155 changes: 95 additions & 60 deletions src/adapters/implementations/RocketChatUploader.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
'strict';
const { post } = require('../../http');
const { Uploader } = require('../Uploader');

class RocketChatUploader extends Uploader {
Expand All @@ -20,7 +19,11 @@ class RocketChatUploader extends Uploader {
/** @type {string} */
this.rocketchat_url = process.env.ROCKETCHAT_URL;

if (this.rocketchat_url && !this.rocketchat_url.startsWith('http://') && !this.rocketchat_url.startsWith('https://')) {
if (
this.rocketchat_url &&
!this.rocketchat_url.startsWith('http://') &&
!this.rocketchat_url.startsWith('https://')
) {
this.rocketchat_url = `http://${rocketchat_url}`;
}

Expand All @@ -31,6 +34,40 @@ class RocketChatUploader extends Uploader {
this.logger = logger;
}

/**
* Logs in to the RocketChat API using the provided credentials.
* @returns {Promise<{'X-Auth-Token': string, 'X-User-Id': string}>} A promise that resolves to the authentication headers if successful.
* @throws {Error} If authentication fails.
*/
async login() {
const authUrl = `${this.rocketchat_url}/api/v1/login`;
const authForm = {
username: this.rocketchat_user,
password: this.rocketchat_password,
};

let rocketchatResBodyJson = null;

try {
rocketchatResBodyJson = await post(authUrl, authForm);
} catch (err) {
this.logger.error(err);
throw new Error('Could not authenticate.');
}

const { status } = rocketchatResBodyJson;
if (status === 'success') {
return {
'X-Auth-Token': rocketchatResBodyJson.data.authToken,
'X-User-Id': rocketchatResBodyJson.data.userId,
};
}

const errMsg = rocketchatResBodyJson.message;
this.logger.error(errMsg);
throw new Error(errMsg);
}

/**
* Uploads the a screenshot of the dashboards.
*
Expand All @@ -39,69 +76,67 @@ class RocketChatUploader extends Uploader {
* @param {{ body: Buffer, contentType: string}=>void} file the screenshot.
* @param {string} grafanaChartLink link to the Grafana chart.
*/
upload(res, title, file, grafanaChartLink) {
const authData = {
url: `${this.rocketchat_url}/api/v1/login`,
form: {
username: this.rocketchat_user,
password: this.rocketchat_password,
async upload(res, title, file, grafanaChartLink) {
let authHeaders = null;
try {
authHeaders = await this.login();
} catch (ex) {
let msg = ex == 'Could not authenticate.' ? "invalid url, user or password/can't access rocketchat api" : ex;
res.send(`${title} - [Rocketchat auth Error - ${msg}] - ${grafanaChartLink}`);
return;
}

// fill in the POST request. This must be www-form/multipart
// TODO: needs some extra testing!
const uploadUrl = `${this.rocketchat_url}/api/v1/rooms.upload/${res.envelope.user.roomID}`;
const uploadForm = {
msg: `${title}: ${grafanaChartLink}`,
// grafanaDashboardRequest() is the method that downloads the .png
file: {
value: file.body,
options: {
filename: `${title} ${Date()}.png`,
contentType: 'image/png',
},
},
};

// We auth against rocketchat to obtain the auth token
post(robot, authData, async (err, rocketchatResBodyJson) => {
if (err) {
this.logger.error(err);
res.send(`${title} - [Rocketchat auth Error - invalid url, user or password/can't access rocketchat api] - ${grafanaChartLink}`);
return;
}
let errMsg;
const { status } = rocketchatResBodyJson;
if (status !== 'success') {
errMsg = rocketchatResBodyJson.message;
this.logger.error(errMsg);
res.send(`${title} - [Rocketchat auth Error - ${errMsg}] - ${grafanaChartLink}`);
return;
}

const auth = rocketchatResBodyJson.data;

// fill in the POST request. This must be www-form/multipart
// TODO: needs some extra testing!
const uploadData = {
url: `${this.rocketchat_url}/api/v1/rooms.upload/${res.envelope.user.roomID}`,
headers: {
'X-Auth-Token': auth.authToken,
'X-User-Id': auth.userId,
},
formData: {
msg: `${title}: ${grafanaChartLink}`,
// grafanaDashboardRequest() is the method that downloads the .png
file: {
value: file.body,
options: {
filename: `${title} ${Date()}.png`,
contentType: 'image/png',
},
},
},
};
let body = null;

try {
body = await this.post(uploadUrl, uploadForm, authHeaders);
} catch (err) {
this.logger.error(err);
res.send(`${title} - [Upload Error] - ${grafanaChartLink}`);
return;
}

if (!body.success) {
this.logger.error(`rocketchat service error while posting data:${body.error}`);
return res.send(`${title} - [Form Error: can't upload file : ${body.error}] - ${grafanaChartLink}`);
}
}

// Try to upload the image to rocketchat else pass the link over
return post(this.robot, uploadData, (err, body) => {
// Error logging, we must also check the body response.
// It will be something like: { "success": <boolean>, "error": <error message> }
if (err) {
this.logger.error(err);
return res.send(`${title} - [Upload Error] - ${grafanaChartLink}`);
}
if (!body.success) {
errMsg = body.error;
this.logger.error(`rocketchat service error while posting data:${errMsg}`);
return res.send(`${title} - [Form Error: can't upload file : ${errMsg}] - ${grafanaChartLink}`);
}
});
/**
* Posts the data data to the specified url and returns JSON.
* @param {string} url - the URL
* @param {Record<string, unknown>} formData - formatData
* @param {Record<string, string>|null} headers - formatData
* @returns {Promise<unknown>} The deserialized JSON response or an error if something went wrong.
*/
async post(url, formData, headers = null) {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: new FormData(formData),
});

if (!response.ok) {
throw new Error('HTTP request failed');
}

const data = await response.json();
return data;
}
}

Expand Down
122 changes: 61 additions & 61 deletions src/grafana-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,11 @@ const { URL, URLSearchParams } = require('url');
class GrafanaClient {
/**
* Creates a new instance.
* @param {(url: string, options?: HttpOptions)=>ScopedClient} http the HTTP client.
* @param {Hubot.Log} logger the logger.
* @param {string} host the host.
* @param {string} apiKey the api key.
*/
constructor(http, logger, host, apiKey) {
/**
* The HTTP client
* @type {(url: string, options?: HttpOptions)=>ScopedClient}
*/
this.http = http;

constructor(logger, host, apiKey) {
/**
* The logger.
* @type {Hubot.Log}
Expand All @@ -39,66 +32,71 @@ class GrafanaClient {
}

/**
* Creates a scoped HTTP client.
* @param {string} url The URL.
* @param {string | null} contentType Indicates if the HTTP client should post.
* @param {encoding | false} encoding Indicates if an encoding should be set.
* @returns {ScopedClient}
* Performs a GET on the Grafana API.
* Remarks: uses Hubot because of Nock testing.
* @param {string} url the url
* @returns {Promise<unknown>} the response data
*/
createHttpClient(url, contentType = null, encoding = false) {
async get(url) {
if (!url.startsWith('http://') && !url.startsWith('https://') && !this.host) {
throw new Error('No Grafana endpoint configured.');
}

// in case of a download we get a "full" URL
const fullUrl = url.startsWith('http://') || url.startsWith('https://') ? url : `${this.host}/api/${url}`;
const headers = grafanaHeaders(contentType, encoding, this.apiKey);
const client = this.http(fullUrl).headers(headers);

return client;
}

/**
* Performs a GET on the Grafana API.
* Remarks: uses Hubot because of Nock testing.
* @param {string} url the url
* @returns {Promise<any>}
*/
async get(url) {
let client = this.createHttpClient(url);
return new Promise((resolve) => {
client.get()((err, res, body) => {
if (err) {
throw err;
}
const data = JSON.parse(body);
return resolve(data);
});
const response = await fetch(fullUrl, {
method: 'GET',
headers: grafanaHeaders(null, false, this.apiKey),
});

await this.throwIfNotOk(response);

const json = await response.json();
return json;
}

/**
* Performs a POST call to the Grafana API.
*
* @param {string} url The API sub URL
* @param {Record<string, any>} data The data that will be sent.
* @returns {Promise<any>}
* @param {Record<string, unknown>} data The data that will be sent.
* @returns {Promise<unknown>}
*/
post(url, data) {
const http = this.createHttpClient(url, 'application/json');
const jsonPayload = JSON.stringify(data);

return new Promise((resolve, reject) => {
http.post(jsonPayload)((err, res, body) => {
if (err) {
reject(err);
return;
}

data = JSON.parse(body);
resolve(data);
});
async post(url, data) {
const fullUrl = url.startsWith('http://') || url.startsWith('https://') ? url : `${this.host}/api/${url}`;

const response = await fetch(fullUrl, {
method: 'POST',
headers: grafanaHeaders('application/json', false, this.apiKey),
body: JSON.stringify(data),
});

await this.throwIfNotOk(response);

const json = await response.json();
return json;
}

/**
* Ensures that the response is OK. If the response is not OK, an error is thrown.
* @param {fetch.Response} response - The response object.
* @throws {Error} If the response is not OK, an error with the response text is thrown.
*/
async throwIfNotOk(response) {
if (response.ok) {
return;
}

if (response.headers.get('content-type') == 'application/json') {
const json = await response.json();

const error = new Error(json.message || 'Error while fetching data from Grafana.');
error.data = json;
throw error;
}

const text = await response.text();
throw new Error(text);
}

/**
Expand All @@ -107,18 +105,20 @@ class GrafanaClient {
* @returns {Promise<DownloadedFile>}
*/
async download(url) {
return await fetch(url, {
let response = await fetch(url, {
method: 'GET',
headers: grafanaHeaders(null, null, this.apiKey),
}).then(async (res) => {
const contentType = res.headers.get('content-type');
const body = await res.arrayBuffer();

return {
body: Buffer.from(body),
contentType: contentType,
};
});

await this.throwIfNotOk(response);

const contentType = response.headers.get('content-type');
const body = await response.arrayBuffer();

return {
body: Buffer.from(body),
contentType: contentType,
};
}

createGrafanaChartLink(query, uid, panel, timeSpan, variables) {
Expand Down
21 changes: 0 additions & 21 deletions src/http.js

This file was deleted.

8 changes: 6 additions & 2 deletions src/service/GrafanaService.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,12 @@ class GrafanaService {
try {
dashboard = await this.client.get(url);
} catch (err) {
this.logger.error(err, `Error while getting dashboard on URL: ${url}`);
return null;
if (err.message !== 'Dashboard not found') {
this.logger.error(err, `Error while getting dashboard on URL: ${url}`);
return null;
}

dashboard = { message: err.message };
}

this.logger.debug(dashboard);
Expand Down

0 comments on commit fc68e14

Please sign in to comment.