Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Elasticsearch plugin #337

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
107 changes: 107 additions & 0 deletions plugins/plugin-elasticsearch/cmd/elasticsearch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package main

import (
"net/http"
"strconv"

"github.com/kobsio/kobs/pkg/kube/clusters"
"github.com/kobsio/kobs/pkg/log"
"github.com/kobsio/kobs/pkg/middleware/errresponse"
"github.com/kobsio/kobs/pkg/satellite/plugins/plugin"
"github.com/kobsio/kobs/plugins/plugin-elasticsearch/pkg/instance"

"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"go.uber.org/zap"
)

// Router implements the router for the Elasticsearch plugin, which can be registered in the router for our rest api. It contains
// the api routes for the Elasticsearch plugin and it's configuration.
type Router struct {
*chi.Mux
instances []instance.Instance
}

// getInstance returns a Elasticsearch instance by it's name. If we couldn't found an instance with the provided name and the
// provided name is "default" we return the first instance from the list. The first instance in the list is also the
// first one configured by the user and can be used as default one.
func (router *Router) getInstance(name string) instance.Instance {
for _, i := range router.instances {
if i.GetName() == name {
return i
}
}

if name == "default" && len(router.instances) > 0 {
return router.instances[0]
}

return nil
}

// getLogs returns the raw documents for a given query from Elasticsearch. The result also contains the distribution of
// the documents in the given time range. The name of the Elasticsearch instance must be set via the name path
// parameter, all other values like the query, start and end time are set via query parameters. These
// parameters are then passed to the GetLogs function of the Elasticsearch instance, which returns the documents and
// buckets.
func (router *Router) getLogs(w http.ResponseWriter, r *http.Request) {
name := r.Header.Get("x-kobs-plugin")
query := r.URL.Query().Get("query")
timeStart := r.URL.Query().Get("timeStart")
timeEnd := r.URL.Query().Get("timeEnd")

log.Debug(r.Context(), "Get logs parameters", zap.String("name", name), zap.String("query", query), zap.String("timeStart", timeStart), zap.String("timeEnd", timeEnd))

i := router.getInstance(name)
if i == nil {
log.Error(r.Context(), "Could not find instance name", zap.String("name", name))
errresponse.Render(w, r, nil, http.StatusBadRequest, "Could not find instance name")
return
}

parsedTimeStart, err := strconv.ParseInt(timeStart, 10, 64)
if err != nil {
log.Error(r.Context(), "Could not parse start time", zap.Error(err))
errresponse.Render(w, r, err, http.StatusBadRequest, "Could not parse start time")
return
}

parsedTimeEnd, err := strconv.ParseInt(timeEnd, 10, 64)
if err != nil {
log.Error(r.Context(), "Could not parse end time", zap.Error(err))
errresponse.Render(w, r, err, http.StatusBadRequest, "Could not parse end time")
return
}

data, err := i.GetLogs(r.Context(), query, parsedTimeStart, parsedTimeEnd)
if err != nil {
log.Error(r.Context(), "Could not get logs", zap.Error(err))
errresponse.Render(w, r, err, http.StatusInternalServerError, "Could not get logs")
return
}

render.JSON(w, r, data)
}

// Mount mounts the Elasticsearch plugin routes in the plugins router of a kobs satellite instance.
func Mount(instances []plugin.Instance, clustersClient clusters.Client) (chi.Router, error) {
var elasticsearchInstances []instance.Instance

for _, i := range instances {
elasticsearchInstance, err := instance.New(i.Name, i.Options)
if err != nil {
return nil, err
}

elasticsearchInstances = append(elasticsearchInstances, elasticsearchInstance)
}

router := Router{
chi.NewRouter(),
elasticsearchInstances,
}

router.Get("/logs", router.getLogs)

return router, nil
}
129 changes: 129 additions & 0 deletions plugins/plugin-elasticsearch/cmd/elasticsearch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package main

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/kobsio/kobs/pkg/satellite/plugins/plugin"
"github.com/kobsio/kobs/plugins/plugin-elasticsearch/pkg/instance"

"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestGetInstance(t *testing.T) {
mockInstance := &instance.MockInstance{}
mockInstance.On("GetName").Return("elasticsearch")

router := Router{chi.NewRouter(), []instance.Instance{mockInstance}}
instance1 := router.getInstance("default")
require.NotNil(t, instance1)

instance2 := router.getInstance("elasticsearch")
require.NotNil(t, instance2)

instance3 := router.getInstance("invalidname")
require.Nil(t, instance3)
}

func TestGetLogs(t *testing.T) {
for _, tt := range []struct {
name string
pluginName string
url string
expectedStatusCode int
expectedBody string
prepare func(*instance.MockInstance)
}{
{
name: "invalid instance name",
pluginName: "invalidname",
url: "/logs",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "{\"error\":\"Could not find instance name\"}\n",
prepare: func(mockInstance *instance.MockInstance) {
mockInstance.On("GetName").Return("elasticsearch")
},
},
{
name: "parse time start fails",
pluginName: "elasticsearch",
url: "/logs?timeStart=test",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "{\"error\":\"Could not parse start time: strconv.ParseInt: parsing \\\"test\\\": invalid syntax\"}\n",
prepare: func(mockInstance *instance.MockInstance) {
mockInstance.On("GetName").Return("elasticsearch")
},
},
{
name: "parse time end fails",
pluginName: "elasticsearch",
url: "/logs?timeStart=0&timeEnd=test",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "{\"error\":\"Could not parse end time: strconv.ParseInt: parsing \\\"test\\\": invalid syntax\"}\n",
prepare: func(mockInstance *instance.MockInstance) {
mockInstance.On("GetName").Return("elasticsearch")
},
},
{
name: "get logs error",
pluginName: "elasticsearch",
url: "/logs?timeStart=0&timeEnd=0",
expectedStatusCode: http.StatusInternalServerError,
expectedBody: "{\"error\":\"Could not get logs: bad request\"}\n",
prepare: func(mockInstance *instance.MockInstance) {
mockInstance.On("GetName").Return("elasticsearch")
mockInstance.On("GetLogs", mock.Anything, "", int64(0), int64(0)).Return(nil, fmt.Errorf("bad request"))
},
},
{
name: "get logs success",
pluginName: "elasticsearch",
url: "/logs?timeStart=0&timeEnd=0",
expectedStatusCode: http.StatusOK,
expectedBody: "{\"took\":0,\"hits\":0,\"documents\":null,\"buckets\":null}\n",
prepare: func(mockInstance *instance.MockInstance) {
mockInstance.On("GetName").Return("elasticsearch")
mockInstance.On("GetLogs", mock.Anything, "", int64(0), int64(0)).Return(&instance.Data{}, nil)
},
},
} {
t.Run(tt.name, func(t *testing.T) {
mockInstance := &instance.MockInstance{}
mockInstance.AssertExpectations(t)
tt.prepare(mockInstance)

router := Router{chi.NewRouter(), []instance.Instance{mockInstance}}
router.Route("/{name}", func(r chi.Router) {
r.Get("/logs", router.getLogs)
})

req, _ := http.NewRequest(http.MethodGet, tt.url, nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("name", strings.Split(tt.url, "/")[1])
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
req.Header.Set("x-kobs-plugin", tt.pluginName)

w := httptest.NewRecorder()
router.getLogs(w, req)

require.Equal(t, tt.expectedStatusCode, w.Code)
require.Equal(t, tt.expectedBody, string(w.Body.Bytes()))
})
}
}

func TestMount(t *testing.T) {
router1, err := Mount([]plugin.Instance{{Name: "elasticsearch", Options: map[string]interface{}{}}}, nil)
require.NoError(t, err)
require.NotNil(t, router1)

router2, err := Mount([]plugin.Instance{{Name: "elasticsearch", Options: map[string]interface{}{"token": []string{"token"}}}}, nil)
require.Error(t, err)
require.Nil(t, router2)
}
47 changes: 47 additions & 0 deletions plugins/plugin-elasticsearch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@kobsio/plugin-elasticsearch",
"version": "0.0.0",
"license": "MIT",
"private": true,
"dependencies": {
"@kobsio/react-scripts": "5.0.1-1",
"@patternfly/patternfly": "^4.194.4",
"@patternfly/react-charts": "^6.74.3",
"@patternfly/react-core": "^4.214.1",
"@patternfly/react-icons": "^4.65.1",
"@patternfly/react-styles": "^4.64.1",
"@patternfly/react-table": "^4.83.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/react-router-dom": "^5.3.3",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-query": "^3.39.0",
"react-router-dom": "^6.3.0",
"typescript": "^4.4.2"
},
"scripts": {
"start": "PUBLIC_URL='http://localhost:3001/' PORT=3001 react-scripts start",
"build": "PUBLIC_URL='/plugins/elasticsearch' react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"proxy": "http://localhost:15220",
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Loading