Skip to content

Commit

Permalink
Add plugin version and user agent fields to PluginContext (#736)
Browse files Browse the repository at this point in the history
* add plugin version

* add user agent

* tidy

* remove unused fields for now

* fix imports

* tidy

* return nil

* add comment
  • Loading branch information
wbrowne authored Sep 20, 2023
1 parent be36c43 commit e5fc54a
Show file tree
Hide file tree
Showing 8 changed files with 416 additions and 221 deletions.
8 changes: 8 additions & 0 deletions backend/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
"github.com/grafana/grafana-plugin-sdk-go/backend/useragent"
"github.com/grafana/grafana-plugin-sdk-go/internal/tenant"
)

Expand Down Expand Up @@ -140,6 +141,9 @@ type PluginContext struct {
// PluginID is the unique identifier of the plugin that the request is for.
PluginID string

// PluginVersion is the version of the plugin that the request is for.
PluginVersion string

// User is the Grafana user making the request.
//
// Will not be provided if Grafana backend initiated the request,
Expand All @@ -165,6 +169,10 @@ type PluginContext struct {

// GrafanaConfig is the configuration settings provided by Grafana.
GrafanaConfig *GrafanaCfg

// UserAgent is the user agent of the Grafana server that initiated the gRPC request.
// Will only be set if request is made from Grafana v10.2.0 or later.
UserAgent *useragent.UserAgent
}

func setCustomOptionsFromHTTPSettings(opts *httpclient.Options, httpSettings *HTTPSettings) {
Expand Down
15 changes: 15 additions & 0 deletions backend/convert_from_protobuf.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"time"

"github.com/grafana/grafana-plugin-sdk-go/backend/useragent"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)
Expand Down Expand Up @@ -70,15 +71,29 @@ func (f ConvertFromProtobuf) DataSourceInstanceSettings(proto *pluginv2.DataSour
}
}

// UserAgent converts protobuf version of a UserAgent to the SDK version.
func (f ConvertFromProtobuf) UserAgent(u string) *useragent.UserAgent {
if len(u) == 0 {
return nil
}
ua, err := useragent.Parse(u)
if err != nil {
return nil
}
return ua
}

// PluginContext converts protobuf version of a PluginContext to the SDK version.
func (f ConvertFromProtobuf) PluginContext(proto *pluginv2.PluginContext) PluginContext {
return PluginContext{
OrgID: proto.OrgId,
PluginID: proto.PluginId,
PluginVersion: proto.PluginVersion,
User: f.User(proto.User),
AppInstanceSettings: f.AppInstanceSettings(proto.AppInstanceSettings),
DataSourceInstanceSettings: f.DataSourceInstanceSettings(proto.DataSourceInstanceSettings, proto.PluginId),
GrafanaConfig: f.GrafanaConfig(proto.GrafanaConfig),
UserAgent: f.UserAgent(proto.UserAgent),
}
}

Expand Down
13 changes: 10 additions & 3 deletions backend/convert_from_protobuf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import (
"testing"
"time"

"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
"github.com/mitchellh/reflectwalk"
"github.com/stretchr/testify/require"

"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)

type walker struct {
Expand Down Expand Up @@ -193,8 +194,9 @@ func TestConvertFromProtobufDataSourceInstanceSettings(t *testing.T) {
}

var protoPluginContext = &pluginv2.PluginContext{
OrgId: 3,
PluginId: "the-best-plugin",
OrgId: 3,
PluginId: "the-best-plugin",
PluginVersion: "1.0.0",
User: &pluginv2.User{
Login: "bestUser",
Name: "Best User",
Expand All @@ -206,6 +208,7 @@ var protoPluginContext = &pluginv2.PluginContext{
GrafanaConfig: map[string]string{
"foo": "bar",
},
UserAgent: "Grafana/10.0.0 (linux; amd64)",
}

func TestConvertFromProtobufPluginContext(t *testing.T) {
Expand Down Expand Up @@ -251,6 +254,7 @@ func TestConvertFromProtobufPluginContext(t *testing.T) {
requireCounter.Equal(t, protoCtx.DataSourceInstanceSettings.Id, sdkCtx.DataSourceInstanceSettings.ID)
requireCounter.Equal(t, protoCtx.DataSourceInstanceSettings.Uid, sdkCtx.DataSourceInstanceSettings.UID)
requireCounter.Equal(t, protoCtx.PluginId, sdkCtx.DataSourceInstanceSettings.Type)
requireCounter.Equal(t, protoCtx.PluginVersion, sdkCtx.PluginVersion)
requireCounter.Equal(t, protoCtx.DataSourceInstanceSettings.Url, sdkCtx.DataSourceInstanceSettings.URL)
requireCounter.Equal(t, protoCtx.DataSourceInstanceSettings.User, sdkCtx.DataSourceInstanceSettings.User)
requireCounter.Equal(t, protoCtx.DataSourceInstanceSettings.Database, sdkCtx.DataSourceInstanceSettings.Database)
Expand All @@ -259,6 +263,7 @@ func TestConvertFromProtobufPluginContext(t *testing.T) {
requireCounter.Equal(t, json.RawMessage(protoCtx.DataSourceInstanceSettings.JsonData), sdkCtx.DataSourceInstanceSettings.JSONData)
requireCounter.Equal(t, map[string]string{"secret": "quiet"}, sdkCtx.DataSourceInstanceSettings.DecryptedSecureJSONData)
requireCounter.Equal(t, time.Unix(0, 86400*2*1e9), sdkCtx.DataSourceInstanceSettings.Updated)
requireCounter.Equal(t, protoCtx.UserAgent, sdkCtx.UserAgent.String())

requireCounter.Equal(t, protoCtx.GrafanaConfig, sdkCtx.GrafanaConfig.config)

Expand Down Expand Up @@ -407,6 +412,7 @@ func TestConvertFromProtobufQueryDataRequest(t *testing.T) {
requireCounter.Equal(t, protoQDR.PluginContext.DataSourceInstanceSettings.Id, sdkQDR.PluginContext.DataSourceInstanceSettings.ID)
requireCounter.Equal(t, protoQDR.PluginContext.DataSourceInstanceSettings.Uid, sdkQDR.PluginContext.DataSourceInstanceSettings.UID)
requireCounter.Equal(t, protoQDR.PluginContext.PluginId, sdkQDR.PluginContext.DataSourceInstanceSettings.Type)
requireCounter.Equal(t, protoQDR.PluginContext.PluginVersion, sdkQDR.PluginContext.PluginVersion)
requireCounter.Equal(t, protoQDR.PluginContext.DataSourceInstanceSettings.Url, sdkQDR.PluginContext.DataSourceInstanceSettings.URL)
requireCounter.Equal(t, protoQDR.PluginContext.DataSourceInstanceSettings.User, sdkQDR.PluginContext.DataSourceInstanceSettings.User)
requireCounter.Equal(t, protoQDR.PluginContext.DataSourceInstanceSettings.Database, sdkQDR.PluginContext.DataSourceInstanceSettings.Database)
Expand All @@ -415,6 +421,7 @@ func TestConvertFromProtobufQueryDataRequest(t *testing.T) {
requireCounter.Equal(t, json.RawMessage(protoQDR.PluginContext.DataSourceInstanceSettings.JsonData), sdkQDR.PluginContext.DataSourceInstanceSettings.JSONData)
requireCounter.Equal(t, map[string]string{"secret": "quiet"}, sdkQDR.PluginContext.DataSourceInstanceSettings.DecryptedSecureJSONData)
requireCounter.Equal(t, time.Unix(0, 86400*2*1e9), sdkQDR.PluginContext.DataSourceInstanceSettings.Updated)
requireCounter.Equal(t, protoQDR.PluginContext.UserAgent, sdkQDR.PluginContext.UserAgent.String())

// Queries
requireCounter.Equal(t, protoQDR.Queries[0].RefId, sdkQDR.Queries[0].RefID)
Expand Down
12 changes: 12 additions & 0 deletions backend/convert_to_protobuf.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"time"

"github.com/grafana/grafana-plugin-sdk-go/backend/useragent"
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)

Expand Down Expand Up @@ -68,15 +69,26 @@ func (t ConvertToProtobuf) DataSourceInstanceSettings(s *DataSourceInstanceSetti
}
}

// UserAgent converts the SDK version of a useragent.UserAgent to the protobuf version.
func (t ConvertToProtobuf) UserAgent(ua *useragent.UserAgent) string {
if ua == nil {
return ""
}

return ua.String()
}

// PluginContext converts the SDK version of a PluginContext to the protobuf version.
func (t ConvertToProtobuf) PluginContext(pluginCtx PluginContext) *pluginv2.PluginContext {
return &pluginv2.PluginContext{
OrgId: pluginCtx.OrgID,
PluginId: pluginCtx.PluginID,
PluginVersion: pluginCtx.PluginVersion,
User: t.User(pluginCtx.User),
AppInstanceSettings: t.AppInstanceSettings(pluginCtx.AppInstanceSettings),
DataSourceInstanceSettings: t.DataSourceInstanceSettings(pluginCtx.DataSourceInstanceSettings),
GrafanaConfig: t.GrafanaConfig(pluginCtx.GrafanaConfig),
UserAgent: t.UserAgent(pluginCtx.UserAgent),
}
}

Expand Down
54 changes: 54 additions & 0 deletions backend/useragent/user_agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package useragent

import (
"errors"
"regexp"
)

var (
userAgentRegex = regexp.MustCompile(`^Grafana/([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9]+)?) \(([a-zA-Z0-9]+); ([a-zA-Z0-9]+)\)$`)
errInvalidFormat = errors.New("invalid user agent format")
)

// UserAgent represents a Grafana user agent.
// Its format is "Grafana/<version> (<os>; <arch>)"
// Example: "Grafana/7.0.0-beta1 (darwin; amd64)", "Grafana/10.0.0 (windows; x86)"
type UserAgent struct {
grafanaVersion string
arch string
os string
}

// New creates a new UserAgent.
// The version must be a valid semver string, and the os and arch must be valid strings.
func New(grafanaVersion, os, arch string) (*UserAgent, error) {
ua := &UserAgent{
grafanaVersion: grafanaVersion,
os: os,
arch: arch,
}

return Parse(ua.String())
}

// Parse creates a new UserAgent from a string.
func Parse(s string) (*UserAgent, error) {
matches := userAgentRegex.FindStringSubmatch(s)
if len(matches) != 4 {
return nil, errInvalidFormat
}

return &UserAgent{
grafanaVersion: matches[1],
os: matches[2],
arch: matches[3],
}, nil
}

func (ua *UserAgent) GrafanaVersion() string {
return ua.grafanaVersion
}

func (ua *UserAgent) String() string {
return "Grafana/" + ua.grafanaVersion + " (" + ua.os + "; " + ua.arch + ")"
}
71 changes: 71 additions & 0 deletions backend/useragent/user_agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package useragent

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestFromString(t *testing.T) {
tcs := []struct {
name string
userAgent string
expected *UserAgent
err error
}{
{
name: "valid",
userAgent: "Grafana/10.2.0 (darwin; amd64)",
expected: &UserAgent{
grafanaVersion: "10.2.0",
os: "darwin",
arch: "amd64",
},
}, {
name: "valid (with semver suffix)",
userAgent: "Grafana/7.0.0-beta1 (darwin; amd64)",
expected: &UserAgent{
grafanaVersion: "7.0.0-beta1",
os: "darwin",
arch: "amd64",
},
},
{
name: "invalid (missing os + arch)",
userAgent: "Grafana/7.0.0-beta1",
err: errInvalidFormat,
},
{
name: "invalid (missing arch)",
userAgent: "Grafana/7.0.0-beta1 (darwin)",
err: errInvalidFormat,
},
{
name: "invalid (missing os)",
userAgent: "Grafana/7.0.0-beta1 (; amd64)",
err: errInvalidFormat,
},
{
name: "invalid (missing semicolon)",
userAgent: "Grafana/7.0.0-beta1 (darwin amd64)",
err: errInvalidFormat,
},
{
name: "invalid (not semver)",
userAgent: "Grafana/10.0 (darwin; amd64)",
err: errInvalidFormat,
},
{
name: "invalid (extra param)",
userAgent: "Grafana/7.0.0-beta1 (darwin; amd64; linux)",
err: errInvalidFormat,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
res, err := Parse(tc.userAgent)
require.ErrorIs(t, err, tc.err)
require.Equalf(t, tc.expected, res, "Parse(%v)", tc.userAgent)
})
}
}
Loading

0 comments on commit e5fc54a

Please sign in to comment.