mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 01:32:17 +08:00
Chore: use errutil for pluginRepo errors (#78647)
* Chore: use errutil for pluginRepo errors * Update pkg/util/errutil/status.go * Use errutil helper functions Co-Authored-By: Marcus Efraimsson <marcus.efraimsson@gmail.com> * Forgot the log level * Use entity --------- Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
@ -449,14 +449,6 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
|
|||||||
if errors.As(err, &dupeErr) {
|
if errors.As(err, &dupeErr) {
|
||||||
return response.Error(http.StatusConflict, "Plugin already installed", err)
|
return response.Error(http.StatusConflict, "Plugin already installed", err)
|
||||||
}
|
}
|
||||||
var versionUnsupportedErr repo.ErrVersionUnsupported
|
|
||||||
if errors.As(err, &versionUnsupportedErr) {
|
|
||||||
return response.Error(http.StatusConflict, "Plugin version not supported", err)
|
|
||||||
}
|
|
||||||
var versionNotFoundErr repo.ErrVersionNotFound
|
|
||||||
if errors.As(err, &versionNotFoundErr) {
|
|
||||||
return response.Error(http.StatusNotFound, "Plugin version not found", err)
|
|
||||||
}
|
|
||||||
var clientError repo.ErrResponse4xx
|
var clientError repo.ErrResponse4xx
|
||||||
if errors.As(err, &clientError) {
|
if errors.As(err, &clientError) {
|
||||||
return response.Error(clientError.StatusCode(), clientError.Message(), err)
|
return response.Error(clientError.StatusCode(), clientError.Message(), err)
|
||||||
@ -464,12 +456,8 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
|
|||||||
if errors.Is(err, plugins.ErrInstallCorePlugin) {
|
if errors.Is(err, plugins.ErrInstallCorePlugin) {
|
||||||
return response.Error(http.StatusForbidden, "Cannot install or change a Core plugin", err)
|
return response.Error(http.StatusForbidden, "Cannot install or change a Core plugin", err)
|
||||||
}
|
}
|
||||||
var archError repo.ErrArcNotFound
|
|
||||||
if errors.As(err, &archError) {
|
|
||||||
return response.Error(http.StatusNotFound, archError.Error(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Error(http.StatusInternalServerError, "Failed to install plugin", err)
|
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to install plugin", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) {
|
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) {
|
||||||
|
@ -164,7 +164,7 @@ func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, comp
|
|||||||
return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err)
|
return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err)
|
||||||
}
|
}
|
||||||
if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) {
|
if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) {
|
||||||
return ErrChecksumMismatch{archiveURL: pluginURL}
|
return ErrChecksumMismatch(pluginURL)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
)
|
||||||
|
|
||||||
type ErrResponse4xx struct {
|
type ErrResponse4xx struct {
|
||||||
message string
|
message string
|
||||||
@ -43,47 +47,44 @@ func (e ErrResponse4xx) Error() string {
|
|||||||
return fmt.Sprintf("%d", e.statusCode)
|
return fmt.Sprintf("%d", e.statusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrVersionUnsupported struct {
|
var (
|
||||||
pluginID string
|
ErrVersionUnsupportedMsg = "{{.Public.PluginID}} v{{.Public.Version}} is not supported on your system {{.Public.SysInfo}}"
|
||||||
requestedVersion string
|
ErrVersionUnsupportedBase = errutil.Conflict("plugin.unsupportedVersion").
|
||||||
systemInfo string
|
MustTemplate(ErrVersionUnsupportedMsg, errutil.WithPublic(ErrVersionUnsupportedMsg))
|
||||||
|
|
||||||
|
ErrVersionNotFoundMsg = "{{.Public.PluginID}} v{{.Public.Version}} either does not exist or is not supported on your system {{.Public.SysInfo}}"
|
||||||
|
ErrVersionNotFoundBase = errutil.NotFound("plugin.versionNotFound").
|
||||||
|
MustTemplate(ErrVersionNotFoundMsg, errutil.WithPublic(ErrVersionNotFoundMsg))
|
||||||
|
|
||||||
|
ErrArcNotFoundMsg = "{{.Public.PluginID}} is not compatible with your system architecture: {{.Public.SysInfo}}"
|
||||||
|
ErrArcNotFoundBase = errutil.NotFound("plugin.archNotFound").
|
||||||
|
MustTemplate(ErrArcNotFoundMsg, errutil.WithPublic(ErrArcNotFoundMsg))
|
||||||
|
|
||||||
|
ErrChecksumMismatchMsg = "expected SHA256 checksum does not match the downloaded archive ({{.Public.ArchiveURL}}) - please contact security@grafana.com"
|
||||||
|
ErrChecksumMismatchBase = errutil.UnprocessableEntity("plugin.checksumMismatch").
|
||||||
|
MustTemplate(ErrChecksumMismatchMsg, errutil.WithPublic(ErrChecksumMismatchMsg))
|
||||||
|
|
||||||
|
ErrCorePluginMsg = "plugin {{.Public.PluginID}} is a core plugin and cannot be installed separately"
|
||||||
|
ErrCorePluginBase = errutil.Forbidden("plugin.forbiddenCorePluginInstall").
|
||||||
|
MustTemplate(ErrCorePluginMsg, errutil.WithPublic(ErrCorePluginMsg))
|
||||||
|
)
|
||||||
|
|
||||||
|
func ErrVersionUnsupported(pluginID, requestedVersion, systemInfo string) error {
|
||||||
|
return ErrVersionUnsupportedBase.Build(errutil.TemplateData{Public: map[string]any{"PluginID": pluginID, "Version": requestedVersion, "SysInfo": systemInfo}})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ErrVersionUnsupported) Error() string {
|
func ErrVersionNotFound(pluginID, requestedVersion, systemInfo string) error {
|
||||||
return fmt.Sprintf("%s v%s is not supported on your system (%s)", e.pluginID, e.requestedVersion, e.systemInfo)
|
return ErrVersionNotFoundBase.Build(errutil.TemplateData{Public: map[string]any{"PluginID": pluginID, "Version": requestedVersion, "SysInfo": systemInfo}})
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrVersionNotFound struct {
|
func ErrArcNotFound(pluginID, systemInfo string) error {
|
||||||
pluginID string
|
return ErrArcNotFoundBase.Build(errutil.TemplateData{Public: map[string]any{"PluginID": pluginID, "SysInfo": systemInfo}})
|
||||||
requestedVersion string
|
|
||||||
systemInfo string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ErrVersionNotFound) Error() string {
|
func ErrChecksumMismatch(archiveURL string) error {
|
||||||
return fmt.Sprintf("%s v%s either does not exist or is not supported on your system (%s)", e.pluginID, e.requestedVersion, e.systemInfo)
|
return ErrChecksumMismatchBase.Build(errutil.TemplateData{Public: map[string]any{"ArchiveURL": archiveURL}})
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrArcNotFound struct {
|
func ErrCorePlugin(pluginID string) error {
|
||||||
pluginID string
|
return ErrCorePluginBase.Build(errutil.TemplateData{Public: map[string]any{"PluginID": pluginID}})
|
||||||
systemInfo string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrArcNotFound) Error() string {
|
|
||||||
return fmt.Sprintf("%s is not compatible with your system architecture: %s", e.pluginID, e.systemInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrChecksumMismatch struct {
|
|
||||||
archiveURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrChecksumMismatch) Error() string {
|
|
||||||
return fmt.Sprintf("expected SHA256 checksum does not match the downloaded archive (%s) - please contact security@grafana.com", e.archiveURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrCorePlugin struct {
|
|
||||||
id string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrCorePlugin) Error() string {
|
|
||||||
return fmt.Sprintf("plugin %s is a core plugin and cannot be installed separately", e.id)
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,3 +26,37 @@ func TestErrResponse4xx(t *testing.T) {
|
|||||||
require.Equal(t, compatInfo, err.compatibilityInfo)
|
require.Equal(t, compatInfo, err.compatibilityInfo)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestErrorTemplates(t *testing.T) {
|
||||||
|
base := &errutil.Error{}
|
||||||
|
|
||||||
|
err := ErrVersionUnsupported("grafana-test-app", "1.0.0", "darwin-amd64")
|
||||||
|
require.True(t, errors.As(err, base))
|
||||||
|
require.Equal(t, http.StatusConflict, base.Public().StatusCode)
|
||||||
|
require.Equal(t, "plugin.unsupportedVersion", base.Public().MessageID)
|
||||||
|
require.Equal(t, "grafana-test-app v1.0.0 is not supported on your system darwin-amd64", base.Public().Message)
|
||||||
|
|
||||||
|
err = ErrVersionNotFound("grafana-test-app", "1.0.0", "darwin-amd64")
|
||||||
|
require.True(t, errors.As(err, base))
|
||||||
|
require.Equal(t, http.StatusNotFound, base.Public().StatusCode)
|
||||||
|
require.Equal(t, "plugin.versionNotFound", base.Public().MessageID)
|
||||||
|
require.Equal(t, "grafana-test-app v1.0.0 either does not exist or is not supported on your system darwin-amd64", base.Public().Message)
|
||||||
|
|
||||||
|
err = ErrArcNotFound("grafana-test-app", "darwin-amd64")
|
||||||
|
require.True(t, errors.As(err, base))
|
||||||
|
require.Equal(t, http.StatusNotFound, base.Public().StatusCode)
|
||||||
|
require.Equal(t, "plugin.archNotFound", base.Public().MessageID)
|
||||||
|
require.Equal(t, "grafana-test-app is not compatible with your system architecture: darwin-amd64", base.Public().Message)
|
||||||
|
|
||||||
|
err = ErrChecksumMismatch("http://localhost:6481/grafana-test-app/versions/1.0.0/download")
|
||||||
|
require.True(t, errors.As(err, base))
|
||||||
|
require.Equal(t, http.StatusUnprocessableEntity, base.Public().StatusCode)
|
||||||
|
require.Equal(t, "plugin.checksumMismatch", base.Public().MessageID)
|
||||||
|
require.Equal(t, "expected SHA256 checksum does not match the downloaded archive (http://localhost:6481/grafana-test-app/versions/1.0.0/download) - please contact security@grafana.com", base.Public().Message)
|
||||||
|
|
||||||
|
err = ErrCorePlugin("grafana-test-app")
|
||||||
|
require.True(t, errors.As(err, base))
|
||||||
|
require.Equal(t, http.StatusForbidden, base.Public().StatusCode)
|
||||||
|
require.Equal(t, "plugin.forbiddenCorePluginInstall", base.Public().MessageID)
|
||||||
|
require.Equal(t, "plugin grafana-test-app is a core plugin and cannot be installed separately", base.Public().Message)
|
||||||
|
}
|
||||||
|
@ -98,7 +98,7 @@ func (m *Manager) PluginVersion(pluginID, version string, compatOpts CompatOpts)
|
|||||||
_, hasAnyArch := compatibleVer.Arch["any"]
|
_, hasAnyArch := compatibleVer.Arch["any"]
|
||||||
if isGrafanaCorePlugin && hasAnyArch {
|
if isGrafanaCorePlugin && hasAnyArch {
|
||||||
// Trying to install a coupled core plugin
|
// Trying to install a coupled core plugin
|
||||||
return VersionData{}, ErrCorePlugin{id: pluginID}
|
return VersionData{}, ErrCorePlugin(pluginID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return compatibleVer, nil
|
return compatibleVer, nil
|
||||||
|
@ -35,14 +35,14 @@ func TestGetPluginArchive(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Incorrect SHA returns error",
|
name: "Incorrect SHA returns error",
|
||||||
sha: "1a2b3c",
|
sha: "1a2b3c",
|
||||||
err: &ErrChecksumMismatch{},
|
err: ErrChecksumMismatchBase,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Core plugin",
|
name: "Core plugin",
|
||||||
sha: "69f698961b6ea651211a187874434821c4727cc22de022e3a7059116d21c75b1",
|
sha: "69f698961b6ea651211a187874434821c4727cc22de022e3a7059116d21c75b1",
|
||||||
apiOpSys: "any",
|
apiOpSys: "any",
|
||||||
apiUrl: "https://github.com/grafana/grafana/tree/main/public/app/plugins/test",
|
apiUrl: "https://github.com/grafana/grafana/tree/main/public/app/plugins/test",
|
||||||
err: &ErrCorePlugin{},
|
err: ErrCorePluginBase,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Decoupled core plugin",
|
name: "Decoupled core plugin",
|
||||||
@ -99,7 +99,7 @@ func TestGetPluginArchive(t *testing.T) {
|
|||||||
co := NewCompatOpts(grafanaVersion, opSys, arch)
|
co := NewCompatOpts(grafanaVersion, opSys, arch)
|
||||||
archive, err := m.GetPluginArchive(context.Background(), pluginID, version, co)
|
archive, err := m.GetPluginArchive(context.Background(), pluginID, version, co)
|
||||||
if tc.err != nil {
|
if tc.err != nil {
|
||||||
require.ErrorAs(t, err, tc.err)
|
require.ErrorIs(t, err, tc.err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -25,10 +25,7 @@ func SelectSystemCompatibleVersion(log log.PrettyLogger, versions []Version, plu
|
|||||||
var ver Version
|
var ver Version
|
||||||
latestForArch, exists := latestSupportedVersion(versions, compatOpts)
|
latestForArch, exists := latestSupportedVersion(versions, compatOpts)
|
||||||
if !exists {
|
if !exists {
|
||||||
return VersionData{}, ErrArcNotFound{
|
return VersionData{}, ErrArcNotFound(pluginID, compatOpts.OSAndArch())
|
||||||
pluginID: pluginID,
|
|
||||||
systemInfo: compatOpts.OSAndArch(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if version == "" {
|
if version == "" {
|
||||||
@ -49,21 +46,13 @@ func SelectSystemCompatibleVersion(log log.PrettyLogger, versions []Version, plu
|
|||||||
if len(ver.Version) == 0 {
|
if len(ver.Version) == 0 {
|
||||||
log.Debugf("Requested plugin version %s v%s not found but potential fallback version '%s' was found",
|
log.Debugf("Requested plugin version %s v%s not found but potential fallback version '%s' was found",
|
||||||
pluginID, version, latestForArch.Version)
|
pluginID, version, latestForArch.Version)
|
||||||
return VersionData{}, ErrVersionNotFound{
|
return VersionData{}, ErrVersionNotFound(pluginID, version, compatOpts.OSAndArch())
|
||||||
pluginID: pluginID,
|
|
||||||
requestedVersion: version,
|
|
||||||
systemInfo: compatOpts.OSAndArch(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !supportsCurrentArch(ver, compatOpts) {
|
if !supportsCurrentArch(ver, compatOpts) {
|
||||||
log.Debugf("Requested plugin version %s v%s is not supported on your system but potential fallback version '%s' was found",
|
log.Debugf("Requested plugin version %s v%s is not supported on your system but potential fallback version '%s' was found",
|
||||||
pluginID, version, latestForArch.Version)
|
pluginID, version, latestForArch.Version)
|
||||||
return VersionData{}, ErrVersionUnsupported{
|
return VersionData{}, ErrVersionUnsupported(pluginID, version, compatOpts.OSAndArch())
|
||||||
pluginID: pluginID,
|
|
||||||
requestedVersion: version,
|
|
||||||
systemInfo: compatOpts.OSAndArch(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return VersionData{
|
return VersionData{
|
||||||
|
@ -55,6 +55,28 @@ func NotFound(msgID string, opts ...BaseOpt) Base {
|
|||||||
return NewBase(StatusNotFound, msgID, opts...)
|
return NewBase(StatusNotFound, msgID, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnprocessableContent initializes a new [Base] error with reason StatusUnprocessableEntity
|
||||||
|
// that is used to construct [Error]. The msgID is passed to the caller
|
||||||
|
// to serve as the base for user facing error messages.
|
||||||
|
//
|
||||||
|
// msgID should be structured as component.errorBrief, for example
|
||||||
|
//
|
||||||
|
// plugin.checksumMismatch
|
||||||
|
func UnprocessableEntity(msgID string, opts ...BaseOpt) Base {
|
||||||
|
return NewBase(StatusUnprocessableEntity, msgID, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conflict initializes a new [Base] error with reason StatusConflict
|
||||||
|
// that is used to construct [Error]. The msgID is passed to the caller
|
||||||
|
// to serve as the base for user facing error messages.
|
||||||
|
//
|
||||||
|
// msgID should be structured as component.errorBrief, for example
|
||||||
|
//
|
||||||
|
// folder.alreadyExists
|
||||||
|
func Conflict(msgID string, opts ...BaseOpt) Base {
|
||||||
|
return NewBase(StatusConflict, msgID, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
// BadRequest initializes a new [Base] error with reason StatusBadRequest
|
// BadRequest initializes a new [Base] error with reason StatusBadRequest
|
||||||
// that is used to construct [Error]. The msgID is passed to the caller
|
// that is used to construct [Error]. The msgID is passed to the caller
|
||||||
// to serve as the base for user facing error messages.
|
// to serve as the base for user facing error messages.
|
||||||
|
@ -20,6 +20,15 @@ const (
|
|||||||
// corresponding document to return to the request.
|
// corresponding document to return to the request.
|
||||||
// HTTP status code 404.
|
// HTTP status code 404.
|
||||||
StatusNotFound CoreStatus = "Not found"
|
StatusNotFound CoreStatus = "Not found"
|
||||||
|
// StatusUnprocessableEntity means that the server understands the request,
|
||||||
|
// the content type and the syntax but it was unable to process the
|
||||||
|
// contained instructions.
|
||||||
|
// HTTP status code 422.
|
||||||
|
StatusUnprocessableEntity CoreStatus = "Unprocessable Entity"
|
||||||
|
// StatusConflict means that the server cannot fulfill the request
|
||||||
|
// there is a conflict in the current state of a resource
|
||||||
|
// HTTP status code 409.
|
||||||
|
StatusConflict CoreStatus = "Conflict"
|
||||||
// StatusTooManyRequests means that the client is rate limited
|
// StatusTooManyRequests means that the client is rate limited
|
||||||
// by the server and should back-off before trying again.
|
// by the server and should back-off before trying again.
|
||||||
// HTTP status code 429.
|
// HTTP status code 429.
|
||||||
@ -92,6 +101,10 @@ func (s CoreStatus) HTTPStatus() int {
|
|||||||
return http.StatusNotFound
|
return http.StatusNotFound
|
||||||
case StatusTimeout, StatusGatewayTimeout:
|
case StatusTimeout, StatusGatewayTimeout:
|
||||||
return http.StatusGatewayTimeout
|
return http.StatusGatewayTimeout
|
||||||
|
case StatusUnprocessableEntity:
|
||||||
|
return http.StatusUnprocessableEntity
|
||||||
|
case StatusConflict:
|
||||||
|
return http.StatusConflict
|
||||||
case StatusTooManyRequests:
|
case StatusTooManyRequests:
|
||||||
return http.StatusTooManyRequests
|
return http.StatusTooManyRequests
|
||||||
case StatusBadRequest, StatusValidationFailed:
|
case StatusBadRequest, StatusValidationFailed:
|
||||||
@ -120,6 +133,10 @@ func (s CoreStatus) LogLevel() LogLevel {
|
|||||||
return LevelInfo
|
return LevelInfo
|
||||||
case StatusTimeout:
|
case StatusTimeout:
|
||||||
return LevelInfo
|
return LevelInfo
|
||||||
|
case StatusUnprocessableEntity:
|
||||||
|
return LevelInfo
|
||||||
|
case StatusConflict:
|
||||||
|
return LevelInfo
|
||||||
case StatusTooManyRequests:
|
case StatusTooManyRequests:
|
||||||
return LevelInfo
|
return LevelInfo
|
||||||
case StatusBadRequest:
|
case StatusBadRequest:
|
||||||
|
Reference in New Issue
Block a user