From 4d7c0904ef3455a2fc8db0ba813463c222d6c350 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 3 May 2023 18:09:18 +0200 Subject: [PATCH] Elasticsearch: Run version check thorugh backend if enableElasticsearchBackendQuerying enabled (#67679) * Elasticsearch: Run getDBversion trough resource calls * Update * Update * Fix lint * Close response body * Fix lint * Refactor --- pkg/tsdb/elasticsearch/elasticsearch.go | 68 +++++++++++++++++++ .../elasticsearch/datasource.test.ts | 38 ++++++++++- .../datasource/elasticsearch/datasource.ts | 8 ++- 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/elasticsearch/elasticsearch.go b/pkg/tsdb/elasticsearch/elasticsearch.go index ccdf101e555..0f714996d08 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch.go +++ b/pkg/tsdb/elasticsearch/elasticsearch.go @@ -1,10 +1,15 @@ package elasticsearch import ( + "bytes" "context" "encoding/json" "errors" "fmt" + "io" + "net/http" + "net/url" + "path" "strconv" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -172,3 +177,66 @@ func (s *Service) getDSInfo(pluginCtx backend.PluginContext) (*es.DatasourceInfo return &instance, nil } + +func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + logger := eslog.FromContext(ctx) + // allowed paths for resource calls: + // - empty string for fetching db version + if req.Path != "" { + return fmt.Errorf("invalid resource URL: %s", req.Path) + } + + ds, err := s.getDSInfo(req.PluginContext) + if err != nil { + return err + } + + esUrl, err := url.Parse(ds.URL) + if err != nil { + return err + } + + resourcePath, err := url.Parse(req.Path) + if err != nil { + return err + } + + // We take the path and the query-string only + esUrl.RawQuery = resourcePath.RawQuery + esUrl.Path = path.Join(esUrl.Path, resourcePath.Path) + + request, err := http.NewRequestWithContext(ctx, req.Method, esUrl.String(), bytes.NewBuffer(req.Body)) + if err != nil { + return err + } + + response, err := ds.HTTPClient.Do(request) + if err != nil { + return err + } + + defer func() { + if err := response.Body.Close(); err != nil { + logger.Warn("Failed to close response body", "err", err) + } + }() + + body, err := io.ReadAll(response.Body) + if err != nil { + return err + } + + responseHeaders := map[string][]string{ + "content-type": {"application/json"}, + } + + if response.Header.Get("Content-Encoding") != "" { + responseHeaders["content-encoding"] = []string{response.Header.Get("Content-Encoding")} + } + + return sender.Send(&backend.CallResourceResponse{ + Status: response.StatusCode, + Headers: responseHeaders, + Body: body, + }) +} diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index f063ea6fd59..ab9c07af866 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -18,7 +18,7 @@ import { TimeRange, toUtc, } from '@grafana/data'; -import { BackendSrvRequest, FetchResponse, reportInteraction } from '@grafana/runtime'; +import { BackendSrvRequest, FetchResponse, reportInteraction, config } from '@grafana/runtime'; import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; @@ -31,6 +31,8 @@ import { Filters, ElasticsearchOptions, ElasticsearchQuery } from './types'; const ELASTICSEARCH_MOCK_URL = 'http://elasticsearch.local'; +const originalConsoleError = console.error; + jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, @@ -1291,3 +1293,37 @@ const logsResponse = { ], }, }; + +describe('ElasticDatasource using backend', () => { + beforeEach(() => { + console.error = jest.fn(); + config.featureToggles.enableElasticsearchBackendQuerying = true; + }); + + afterEach(() => { + console.error = originalConsoleError; + config.featureToggles.enableElasticsearchBackendQuerying = false; + }); + describe('getDatabaseVersion', () => { + it('should correctly get db version', async () => { + const { ds } = getTestContext(); + ds.getResource = jest.fn().mockResolvedValue({ version: { number: '8.0.0' } }); + const version = await ds.getDatabaseVersion(); + expect(version?.raw).toBe('8.0.0'); + }); + + it('should correctly return null if invalid numeric version', async () => { + const { ds } = getTestContext(); + ds.getResource = jest.fn().mockResolvedValue({ version: { number: 8 } }); + const version = await ds.getDatabaseVersion(); + expect(version).toBe(null); + }); + + it('should correctly return null if rejected request', async () => { + const { ds } = getTestContext(); + ds.getResource = jest.fn().mockRejectedValue({}); + const version = await ds.getDatabaseVersion(); + expect(version).toBe(null); + }); + }); +}); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index b863b60b523..7f2f52da49d 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -1,5 +1,5 @@ import { cloneDeep, find, first as _first, isObject, isString, map as _map } from 'lodash'; -import { generate, lastValueFrom, Observable, of } from 'rxjs'; +import { from, generate, lastValueFrom, Observable, of } from 'rxjs'; import { catchError, first, map, mergeMap, skipWhile, throwIfEmpty, tap } from 'rxjs/operators'; import { SemVer } from 'semver'; @@ -739,7 +739,11 @@ export class ElasticDatasource private getDatabaseVersionUncached(): Promise { // we want this function to never fail - return lastValueFrom(this.legacyQueryRunner.request('GET', '/')).then( + const getDbVersionObservable = config.featureToggles.enableElasticsearchBackendQuerying + ? from(this.getResource('')) + : this.legacyQueryRunner.request('GET', '/'); + + return lastValueFrom(getDbVersionObservable).then( (data) => { const versionNumber = data?.version?.number; if (typeof versionNumber !== 'string') {