Skip to content

Commit

Permalink
Merge pull request #32 from Enapter/rnovatorov/dev
Browse files Browse the repository at this point in the history
Handle Command Requests
  • Loading branch information
rnovatorov authored Sep 6, 2023
2 parents 793009f + 91abb8d commit 762dfca
Show file tree
Hide file tree
Showing 13 changed files with 556 additions and 167 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## v6.0.0

- Add support for executing commands
- Make labels unique only for a single query

## v5.1.1

- Fix the link to example dashboard in README
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/Enapter/telemetry-grafana-datasource-plugin
go 1.19

require (
github.com/Enapter/http-api-go-client v0.0.3
github.com/bxcodec/faker/v3 v3.6.0
github.com/grafana/grafana-plugin-sdk-go v0.162.0
github.com/hashicorp/go-hclog v1.5.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Enapter/http-api-go-client v0.0.2 h1:ELwet4EKNlp7YqvTUf9LBH8HS+Ii1svn/E5Zac9zELY=
github.com/Enapter/http-api-go-client v0.0.2/go.mod h1:0iLidjPmLzZqqwYR1DE8Q8Ccb++ovLQVZpVPjplCF6s=
github.com/Enapter/http-api-go-client v0.0.3 h1:Q3CPYiqCCiwXaGwYEVMV/yO1x+c0ZGiPxckpYViLsgU=
github.com/Enapter/http-api-go-client v0.0.3/go.mod h1:0iLidjPmLzZqqwYR1DE8Q8Ccb++ovLQVZpVPjplCF6s=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "enapter-telemetry",
"version": "5.1.1",
"version": "6.0.0",
"description": "Enapter Telemetry Grafana Datasource Plugin",
"scripts": {
"build": "grafana-toolkit plugin:build",
Expand Down
114 changes: 114 additions & 0 deletions pkg/commandsapi/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package commandsapi

import (
"context"
"errors"
"net/http"
"time"

enapterhttp "github.com/Enapter/http-api-go-client/pkg/client"

"github.com/Enapter/telemetry-grafana-datasource-plugin/pkg/httperr"
)

type Client interface {
Execute(ctx context.Context, p ExecuteParams) (CommandResponse, error)
}

type client struct {
token string
timeout time.Duration
}

type ClientParams struct {
Token string
Timeout time.Duration
}

const DefaultTimeout = 15 * time.Second

func NewClient(p ClientParams) Client {
if p.Timeout == 0 {
p.Timeout = DefaultTimeout
}

return &client{
token: p.Token,
timeout: p.Timeout,
}
}

type ExecuteParams struct {
User string
Request CommandRequest
}

type CommandRequest struct {
CommandName string
CommandArgs map[string]interface{}
DeviceID string
HardwareID string
}

type CommandResponse struct {
State string
Payload map[string]interface{}
}

func (c *client) Execute(ctx context.Context, p ExecuteParams) (CommandResponse, error) {
resp, err := c.enapterHTTP(p.User).Commands.Execute(ctx, enapterhttp.CommandQuery{
DeviceID: p.Request.DeviceID,
HardwareID: p.Request.HardwareID,
CommandName: p.Request.CommandName,
Arguments: p.Request.CommandArgs,
})
if err != nil {
if respErr := (enapterhttp.ResponseError{}); errors.As(err, &respErr) {
return CommandResponse{}, c.respErrorToMultiError(respErr)
}
return CommandResponse{}, err
}

return CommandResponse{
State: string(resp.State),
Payload: resp.Payload,
}, nil
}

func (c *client) respErrorToMultiError(respErr enapterhttp.ResponseError) error {
if len(respErr.Errors) == 0 {
return respErr
}

multiErr := new(httperr.MultiError)

for _, e := range respErr.Errors {
if len(e.Code) == 0 {
e.Code = "<empty>"
}
multiErr.Errors = append(multiErr.Errors, httperr.Error{
Code: e.Code,
Message: e.Message,
Details: e.Details,
})
}

return multiErr
}

func (c *client) enapterHTTP(user string) *enapterhttp.Client {
transport := http.DefaultTransport

if c.token != "" {
transport = enapterhttp.NewAuthTokenTransport(transport, c.token)
}

if user != "" {
transport = enapterhttp.NewAuthUserTransport(transport, user)
}

return enapterhttp.NewClient(&http.Client{
Timeout: c.timeout,
Transport: transport,
})
}
72 changes: 39 additions & 33 deletions pkg/telemetryapi/multi_error.go → pkg/httperr/multi_error.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
package telemetryapi
package httperr

import (
"encoding/json"
"errors"
"fmt"
"io"
"strings"
)

func parseMultiError(body io.Reader) (*MultiError, error) {
data, err := io.ReadAll(body)
if err != nil {
return nil, fmt.Errorf("read body: %w", err)
}
var (
errEmptyData = errors.New("empty data")
errEmptyErrorList = errors.New("empty error list")
)

if len(data) == 0 {
return nil, errEmptyData
}
type Error struct {
Code string `json:"code"`
Message string `json:"message,omitempty"`
Details map[string]interface{} `json:"details,omitempty"`
}

var multiErr MultiError
if err := json.Unmarshal(data, &multiErr); err != nil {
return nil, fmt.Errorf("parse data: %w", err)
}
func (e *Error) Error() string {
var b strings.Builder

if len(multiErr.Errors) == 0 {
return nil, errEmptyErrorList
b.WriteString(fmt.Sprintf("code=%s", e.Code))

if len(e.Message) > 0 {
b.WriteString(fmt.Sprintf(", message=%q", e.Message))
}

for i, err := range multiErr.Errors {
if len(err.Code) == 0 {
multiErr.Errors[i].Code = "<empty>"
}
if len(e.Details) > 0 {
b.WriteString(fmt.Sprintf(", details=%v", e.Details))
}

return &multiErr, nil
return b.String()
}

type MultiError struct {
Expand All @@ -47,24 +47,30 @@ func (m *MultiError) Error() string {
return fmt.Sprintf("%d errors: %v", len(m.Errors), m.Errors)
}

type Error struct {
Code string `json:"code"`
Message string `json:"message,omitempty"`
Details map[string]interface{} `json:"details,omitempty"`
}
func ParseMultiError(body io.Reader) (*MultiError, error) {
data, err := io.ReadAll(body)
if err != nil {
return nil, fmt.Errorf("read body: %w", err)
}

func (e *Error) Error() string {
var b strings.Builder
if len(data) == 0 {
return nil, errEmptyData
}

b.WriteString(fmt.Sprintf("code=%s", e.Code))
var multiErr MultiError
if err := json.Unmarshal(data, &multiErr); err != nil {
return nil, fmt.Errorf("parse data: %w", err)
}

if len(e.Message) > 0 {
b.WriteString(fmt.Sprintf(", message=%q", e.Message))
if len(multiErr.Errors) == 0 {
return nil, errEmptyErrorList
}

if len(e.Details) > 0 {
b.WriteString(fmt.Sprintf(", details=%v", e.Details))
for i, err := range multiErr.Errors {
if len(err.Code) == 0 {
multiErr.Errors[i].Code = "<empty>"
}
}

return b.String()
return &multiErr, nil
}
12 changes: 10 additions & 2 deletions pkg/plugin/data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/hashicorp/go-hclog"

"github.com/Enapter/telemetry-grafana-datasource-plugin/pkg/commandsapi"
"github.com/Enapter/telemetry-grafana-datasource-plugin/pkg/plugin/internal/handlers"
"github.com/Enapter/telemetry-grafana-datasource-plugin/pkg/telemetryapi"
)
Expand Down Expand Up @@ -38,15 +39,22 @@ func newDataSource(logger hclog.Logger, settings backend.DataSourceInstanceSetti
return nil, fmt.Errorf("JSON data: %w", err)
}

apiToken := settings.DecryptedSecureJSONData["telemetryAPIToken"]

telemetryAPIClient, err := telemetryapi.NewClient(telemetryapi.ClientParams{
BaseURL: jsonData["telemetryAPIBaseURL"],
Token: settings.DecryptedSecureJSONData["telemetryAPIToken"],
Token: apiToken,
})
if err != nil {
return nil, fmt.Errorf("new telemetry API client: %w", err)
}

queryDataHandler := handlers.NewQueryData(logger, telemetryAPIClient)
commandsAPIClient := commandsapi.NewClient(commandsapi.ClientParams{
Token: apiToken,
})

queryDataHandler := handlers.NewQueryData(
logger, telemetryAPIClient, commandsAPIClient)
checkHealthHandler := handlers.NewCheckHealth(logger, telemetryAPIClient)

logger.Info("created new data source")
Expand Down
54 changes: 54 additions & 0 deletions pkg/plugin/internal/handlers/mock_commandsapi_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package handlers_test

import (
"context"

"github.com/stretchr/testify/suite"

"github.com/Enapter/telemetry-grafana-datasource-plugin/pkg/commandsapi"
)

var _ commandsapi.Client = (*MockCommandsAPIClient)(nil)

type MockCommandsAPIClient struct {
suite *suite.Suite
executeHandler func(commandsapi.ExecuteParams) (
commandsapi.CommandResponse, error)
}

func NewMockCommandsAPIClient(s *suite.Suite) *MockCommandsAPIClient {
c := new(MockCommandsAPIClient)
c.suite = s
c.executeHandler = c.unexpectedCall
return c
}

func (c *MockCommandsAPIClient) ExpectExecuteAndReturn(
wantP commandsapi.ExecuteParams,
cmdResp commandsapi.CommandResponse, err error,
) {
c.executeHandler = func(haveP commandsapi.ExecuteParams) (
commandsapi.CommandResponse, error,
) {
defer func() { c.executeHandler = c.unexpectedCall }()
c.suite.Require().Equal(wantP, haveP)
return cmdResp, err
}
}

func (c *MockCommandsAPIClient) Execute(
_ context.Context, p commandsapi.ExecuteParams,
) (commandsapi.CommandResponse, error) {
return c.executeHandler(p)
}

func (c *MockCommandsAPIClient) unexpectedCall(commandsapi.ExecuteParams) (
commandsapi.CommandResponse, error,
) {
c.suite.Require().FailNow("unexpected call")
//nolint: nilnil // unreachable
return commandsapi.CommandResponse{}, nil
}

func (c *MockCommandsAPIClient) Ready(context.Context) error { return nil }
func (c *MockCommandsAPIClient) Close() {}
Loading

0 comments on commit 762dfca

Please sign in to comment.