You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
7.9 KiB
201 lines
7.9 KiB
2 years ago
|
import { makeRequest } from './services/network_service';
|
||
|
import {
|
||
|
DataExporterAppPluginMeta,
|
||
|
DataExporterPluginMetaJSONData,
|
||
|
DataExporterPluginMetaSecureJSONData,
|
||
|
} from './types';
|
||
|
|
||
|
import { getBackendSrv } from '@grafana/runtime';
|
||
|
|
||
|
import axios from 'axios';
|
||
|
|
||
|
export type UpdateGrafanaPluginSettingsProps = {
|
||
|
jsonData?: Partial<DataExporterPluginMetaJSONData>;
|
||
|
secureJsonData?: Partial<DataExporterPluginMetaSecureJSONData>;
|
||
|
};
|
||
|
|
||
|
type InstallPluginResponse<DataExporterAPIResponse = any> = Pick<
|
||
|
DataExporterPluginMetaSecureJSONData,
|
||
|
'grafanaToken'
|
||
|
> & {
|
||
|
dataExporterAPIResponse: DataExporterAPIResponse;
|
||
|
};
|
||
|
|
||
|
export type PluginConnectedStatusResponse = {
|
||
|
version: string;
|
||
|
};
|
||
|
|
||
|
class PluginState {
|
||
|
static GRAFANA_PLUGIN_SETTINGS_URL = '/api/plugins/corpglory-dataexporter-app/settings';
|
||
|
static grafanaBackend = getBackendSrv();
|
||
|
|
||
|
static generateInvalidDataExporterApiURLErrorMsg = (dataExporterApiUrl: string): string =>
|
||
|
`Could not communicate with your DataExporter API at ${dataExporterApiUrl}.\nValidate that the URL is correct, your DataExporter API is running, and that it is accessible from your Grafana instance.`;
|
||
|
|
||
|
static generateUnknownErrorMsg = (dataExporterApiUrl: string): string =>
|
||
|
`An unknown error occured when trying to install the plugin. Are you sure that your DataExporter API URL, ${dataExporterApiUrl}, is correct?\nRefresh your page and try again, or try removing your plugin configuration and reconfiguring.`;
|
||
|
|
||
|
static getHumanReadableErrorFromDataExporterError = (e: any, dataExporterApiUrl: string): string => {
|
||
|
let errorMsg: string;
|
||
|
const unknownErrorMsg = this.generateUnknownErrorMsg(dataExporterApiUrl);
|
||
|
const consoleMsg = `occured while trying to install the plugin w/ the DataExporter backend`;
|
||
|
|
||
|
if (axios.isAxiosError(e)) {
|
||
|
const statusCode = e.response?.status;
|
||
|
|
||
|
console.warn(`An HTTP related error ${consoleMsg}`, e.response);
|
||
|
|
||
|
if (statusCode === 502) {
|
||
|
// 502 occurs when the plugin-proxy cannot communicate w/ the DataExporter API using the provided URL
|
||
|
errorMsg = this.generateInvalidDataExporterApiURLErrorMsg(dataExporterApiUrl);
|
||
|
} else if (statusCode === 400) {
|
||
|
/**
|
||
|
* A 400 is 'bubbled-up' from the DataExporter API. It indicates one of three cases:
|
||
|
* 1. there is a communication error when DataExporter API tries to contact Grafana's API
|
||
|
* 2. there is an auth error when DataExporter API tries to contact Grafana's API
|
||
|
* 3. (likely rare) user inputs an DataExporterApiUrl that is not RFC 1034/1035 compliant
|
||
|
*
|
||
|
* Check if the response body has an 'error' JSON attribute, if it does, assume scenario 1 or 2
|
||
|
* Use the error message provided to give the user more context/helpful debugging information
|
||
|
*/
|
||
|
errorMsg = e.response?.data?.error || unknownErrorMsg;
|
||
|
} else {
|
||
|
// this scenario shouldn't occur..
|
||
|
errorMsg = unknownErrorMsg;
|
||
|
}
|
||
|
} else {
|
||
|
// a non-axios related error occured.. this scenario shouldn't occur...
|
||
|
console.warn(`An unknown error ${consoleMsg}`, e);
|
||
|
errorMsg = unknownErrorMsg;
|
||
|
}
|
||
|
return errorMsg;
|
||
|
};
|
||
|
|
||
|
static getHumanReadableErrorFromGrafanaProvisioningError = (e: any, dataExporterApiUrl: string): string => {
|
||
|
let errorMsg: string;
|
||
|
|
||
|
if (axios.isAxiosError(e)) {
|
||
|
// The user likely put in a bogus URL for the DataExporter API URL
|
||
|
console.warn('An HTTP related error occured while trying to provision the plugin w/ Grafana', e.response);
|
||
|
errorMsg = this.generateInvalidDataExporterApiURLErrorMsg(dataExporterApiUrl);
|
||
|
} else {
|
||
|
// a non-axios related error occured.. this scenario shouldn't occur...
|
||
|
console.warn('An unknown error occured while trying to provision the plugin w/ Grafana', e);
|
||
|
errorMsg = this.generateUnknownErrorMsg(dataExporterApiUrl);
|
||
|
}
|
||
|
return errorMsg;
|
||
|
};
|
||
|
|
||
|
static getGrafanaPluginSettings = async (): Promise<DataExporterAppPluginMeta> =>
|
||
|
this.grafanaBackend.get<DataExporterAppPluginMeta>(this.GRAFANA_PLUGIN_SETTINGS_URL);
|
||
|
|
||
|
static updateGrafanaPluginSettings = async (data: UpdateGrafanaPluginSettingsProps, enabled = true) =>
|
||
|
this.grafanaBackend.post(this.GRAFANA_PLUGIN_SETTINGS_URL, { ...data, enabled, pinned: true });
|
||
|
|
||
|
static createGrafanaToken = async () => {
|
||
|
const baseUrl = '/api/auth/keys';
|
||
|
const keys = await this.grafanaBackend.get(baseUrl);
|
||
|
const existingKey = keys.find((key: { id: number; name: string; role: string }) => key.name === 'DataExporter');
|
||
|
|
||
|
if (existingKey) {
|
||
|
await this.grafanaBackend.delete(`${baseUrl}/${existingKey.id}`);
|
||
|
}
|
||
|
|
||
|
return await this.grafanaBackend.post(baseUrl, {
|
||
|
name: 'DataExporter',
|
||
|
role: 'Admin',
|
||
|
secondsToLive: null,
|
||
|
});
|
||
|
};
|
||
|
|
||
|
static timeout = (pollCount: number) => new Promise((resolve) => setTimeout(resolve, 10 * 2 ** pollCount));
|
||
|
|
||
|
static connectBackend = async <RT>(): Promise<InstallPluginResponse<RT>> => {
|
||
|
// TODO: try to disable success alerts from Grafana API
|
||
|
const { key: grafanaToken } = await this.createGrafanaToken();
|
||
|
await this.updateGrafanaPluginSettings({ secureJsonData: { grafanaToken } });
|
||
|
const dataExporterAPIResponse = await makeRequest<RT>(`/connect`, {
|
||
|
method: 'POST',
|
||
|
});
|
||
|
return { grafanaToken, dataExporterAPIResponse };
|
||
|
};
|
||
|
|
||
|
static installPlugin = async (dataExporterApiUrl: string): Promise<string | null> => {
|
||
|
let pluginInstallationDataExporterResponse: InstallPluginResponse<{ version: string }>;
|
||
|
|
||
|
// Step 1. Try provisioning the plugin w/ the Grafana API
|
||
|
try {
|
||
|
await this.updateGrafanaPluginSettings({ jsonData: { dataExporterApiUrl } });
|
||
|
} catch (e) {
|
||
|
return this.getHumanReadableErrorFromGrafanaProvisioningError(e, dataExporterApiUrl);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Step 2:
|
||
|
* - Create a grafana token
|
||
|
* - store that token in the Grafana plugin settings
|
||
|
* - configure the plugin in DataExporter's backend
|
||
|
*/
|
||
|
try {
|
||
|
pluginInstallationDataExporterResponse = await this.connectBackend<{ version: string }>();
|
||
|
} catch (e) {
|
||
|
return this.getHumanReadableErrorFromDataExporterError(e, dataExporterApiUrl);
|
||
|
}
|
||
|
|
||
|
// Step 3. reprovision the Grafana plugin settings, storing information that we get back from DataExporter's backend
|
||
|
try {
|
||
|
const { grafanaToken } = pluginInstallationDataExporterResponse;
|
||
|
|
||
|
await this.updateGrafanaPluginSettings({
|
||
|
jsonData: {
|
||
|
dataExporterApiUrl,
|
||
|
},
|
||
|
secureJsonData: {
|
||
|
grafanaToken,
|
||
|
},
|
||
|
});
|
||
|
} catch (e) {
|
||
|
return this.getHumanReadableErrorFromGrafanaProvisioningError(e, dataExporterApiUrl);
|
||
|
}
|
||
|
|
||
|
return null;
|
||
|
};
|
||
|
|
||
|
static checkIfPluginIsConnected = async (
|
||
|
dataExporterApiUrl: string
|
||
|
): Promise<PluginConnectedStatusResponse | string> => {
|
||
|
try {
|
||
|
const resp = await makeRequest<PluginConnectedStatusResponse>(`/status`, {
|
||
|
method: 'GET',
|
||
|
});
|
||
|
|
||
|
// TODO: check if the server version is compatible with the plugin
|
||
|
if (resp.version) {
|
||
|
return resp;
|
||
|
} else {
|
||
|
throw new Error(`Something is working at ${dataExporterApiUrl} but it's not DataExporter backend`);
|
||
|
}
|
||
|
} catch (e) {
|
||
|
return this.getHumanReadableErrorFromDataExporterError(e, dataExporterApiUrl);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
static resetPlugin = (): Promise<void> => {
|
||
|
/**
|
||
|
* mark both of these objects as Required.. this will ensure that we are resetting every attribute back to null
|
||
|
* and throw a type error in the event that DataExporterPluginMetaJSONData or DataExporterPluginMetaSecureJSONData is updated
|
||
|
* but we forget to add the attribute here
|
||
|
*/
|
||
|
const jsonData: Required<DataExporterPluginMetaJSONData> = {
|
||
|
dataExporterApiUrl: null,
|
||
|
};
|
||
|
const secureJsonData: Required<DataExporterPluginMetaSecureJSONData> = {
|
||
|
grafanaToken: null,
|
||
|
};
|
||
|
|
||
|
return this.updateGrafanaPluginSettings({ jsonData, secureJsonData }, false);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
export default PluginState;
|