mirror of https://github.com/grafana/grafana
Plugins: Enable plugin runtime install/uninstall capabilities (#33836)
* add uninstall flow * add install flow * small cleanup * smaller-footprint solution * cleanup + make bp start auto * fix interface contract * improve naming * accept version arg * ensure use of shared logger * make installer a field * add plugin decommissioning * add basic error checking * fix api docs * making initialization idempotent * add mutex * fix comment * fix test * add test for decommission * improve existing test * add more test coverage * more tests * change test func to use read lock * refactoring + adding test asserts * improve purging old install flow * improve dupe checking * change log name * skip over dupe scanned * make test assertion more flexible * remove trailing line * fix pointer receiver name * update comment * add context to API * add config flag * add base http api test + fix update functionality * simplify existing check * clean up test * refactor tests based on feedback * add single quotes to errs * use gcmp in tests + fix logo issue * make plugin list testing more flexible * address feedback * fix API test * fix linter * undo preallocate * Update docs/sources/administration/configuration.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/administration/configuration.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/administration/configuration.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * fix linting issue in test * add docs placeholder * update install notes * Update docs/sources/plugins/marketplace.md Co-authored-by: Marcus Olsson <marcus.olsson@hey.com> * update access wording * add more placeholder docs * add link to more info * PR feedback - improved errors, refactor, lock fix * improve err details * propagate plugin version errors * don't autostart renderer * add H1 * fix imports Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Marcus Olsson <marcus.olsson@hey.com>pull/34021/head
parent
1fbadab600
commit
c39d6ad97d
@ -0,0 +1,22 @@ |
||||
+++ |
||||
title = "Plugin marketplace" |
||||
aliases = ["/docs/grafana/latest/plugins/marketplace/"] |
||||
weight = 1 |
||||
+++ |
||||
|
||||
# Plugin marketplace |
||||
|
||||
Marketplace for Grafana is a plugin bundled with Grafana versions 8.0+. It allows users to browse and manage plugins from within Grafana. Only Grafana Admins can access and use the Marketplace. |
||||
|
||||
[screenshot placeholder] |
||||
|
||||
To use the Marketplace for Grafana, you first need to enable it in the Grafana [configuration]({{< relref "../administration/configuration.md#marketplace_app_enabled" >}}). |
||||
|
||||
## Install a plugin from the Marketplace |
||||
To install a plugin ... |
||||
|
||||
### Updating a plugin |
||||
To update a plugin ... |
||||
|
||||
## Uninstall a plugin from the Marketplace |
||||
To uninstall a plugin ... |
@ -0,0 +1,27 @@ |
||||
|
||||
-----BEGIN PGP SIGNED MESSAGE----- |
||||
Hash: SHA512 |
||||
|
||||
{ |
||||
"manifestVersion": "2.0.0", |
||||
"signatureType": "grafana", |
||||
"signedByOrg": "grafana", |
||||
"signedByOrgName": "Grafana Labs", |
||||
"plugin": "test", |
||||
"version": "1.0.0", |
||||
"time": 1605807330546, |
||||
"keyId": "7e4d0c6a708866e7", |
||||
"files": { |
||||
"plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f" |
||||
} |
||||
} |
||||
-----BEGIN PGP SIGNATURE----- |
||||
Version: OpenPGP.js v4.10.1 |
||||
Comment: https://openpgpjs.org |
||||
|
||||
wqEEARMKAAYFAl+2rOIACgkQfk0ManCIZudNOwIJAT8FTzwnRFCSLTOaR3F3 |
||||
2Fh96eRbghokXcQG9WqpQAg8ZiVfGXeWWRNtV+nuQ9VOZOTO0BovWLuMkym2 |
||||
ci8ABpWOAgd46LkGn3Dd8XVnGmLI6UPqHAXflItOrCMRiGcYJn5PxP1aCz8h |
||||
D0JoNI9TIKrhMtM4voU3Qhf3mIOTHueuDNS48w== |
||||
=mu2j |
||||
-----END PGP SIGNATURE----- |
@ -0,0 +1,16 @@ |
||||
{ |
||||
"type": "datasource", |
||||
"name": "Test", |
||||
"id": "test", |
||||
"backend": true, |
||||
"executable": "test", |
||||
"state": "alpha", |
||||
"info": { |
||||
"version": "1.0.0", |
||||
"description": "Test", |
||||
"author": { |
||||
"name": "Will Browne", |
||||
"url": "https://willbrowne.com" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,89 @@ |
||||
package plugins |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/bus" |
||||
"github.com/grafana/grafana/pkg/infra/log" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/tests/testinfra" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
const ( |
||||
usernameAdmin = "admin" |
||||
usernameNonAdmin = "nonAdmin" |
||||
defaultPassword = "password" |
||||
) |
||||
|
||||
func TestPluginInstallAccess(t *testing.T) { |
||||
dir, cfgPath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ |
||||
MarketplaceAppEnabled: true, |
||||
}) |
||||
store := testinfra.SetUpDatabase(t, dir) |
||||
store.Bus = bus.GetBus() // in order to allow successful user auth
|
||||
grafanaListedAddr := testinfra.StartGrafana(t, dir, cfgPath, store) |
||||
|
||||
createUser(t, store, usernameNonAdmin, defaultPassword, false) |
||||
createUser(t, store, usernameAdmin, defaultPassword, true) |
||||
|
||||
t.Run("Request is forbidden if not from an admin", func(t *testing.T) { |
||||
statusCode, body := makePostRequest(t, grafanaAPIURL(usernameNonAdmin, grafanaListedAddr, "plugins/grafana-plugin/install")) |
||||
assert.Equal(t, 403, statusCode) |
||||
assert.JSONEq(t, "{\"message\": \"Permission denied\"}", body) |
||||
|
||||
statusCode, body = makePostRequest(t, grafanaAPIURL(usernameNonAdmin, grafanaListedAddr, "plugins/grafana-plugin/uninstall")) |
||||
assert.Equal(t, 403, statusCode) |
||||
assert.JSONEq(t, "{\"message\": \"Permission denied\"}", body) |
||||
}) |
||||
|
||||
t.Run("Request is not forbidden if from an admin", func(t *testing.T) { |
||||
statusCode, body := makePostRequest(t, grafanaAPIURL(usernameAdmin, grafanaListedAddr, "plugins/test/install")) |
||||
assert.Equal(t, 404, statusCode) |
||||
assert.JSONEq(t, "{\"error\":\"plugin not found\", \"message\":\"Plugin not found\"}", body) |
||||
|
||||
statusCode, body = makePostRequest(t, grafanaAPIURL(usernameAdmin, grafanaListedAddr, "plugins/test/uninstall")) |
||||
assert.Equal(t, 404, statusCode) |
||||
assert.JSONEq(t, "{\"error\":\"plugin is not installed\", \"message\":\"Plugin not installed\"}", body) |
||||
}) |
||||
} |
||||
|
||||
func createUser(t *testing.T, store *sqlstore.SQLStore, username, password string, isAdmin bool) { |
||||
t.Helper() |
||||
|
||||
cmd := models.CreateUserCommand{ |
||||
Login: username, |
||||
Password: password, |
||||
IsAdmin: isAdmin, |
||||
} |
||||
_, err := store.CreateUser(context.Background(), cmd) |
||||
require.NoError(t, err) |
||||
} |
||||
|
||||
func makePostRequest(t *testing.T, URL string) (int, string) { |
||||
t.Helper() |
||||
|
||||
// nolint:gosec
|
||||
resp, err := http.Post(URL, "application/json", bytes.NewBufferString("")) |
||||
require.NoError(t, err) |
||||
t.Cleanup(func() { |
||||
_ = resp.Body.Close() |
||||
log.Warn("Failed to close response body", "err", err) |
||||
}) |
||||
b, err := ioutil.ReadAll(resp.Body) |
||||
require.NoError(t, err) |
||||
|
||||
return resp.StatusCode, string(b) |
||||
} |
||||
|
||||
func grafanaAPIURL(username string, grafanaListedAddr string, path string) string { |
||||
return fmt.Sprintf("http://%s:%s@%s/api/%s", username, defaultPassword, grafanaListedAddr, path) |
||||
} |
Loading…
Reference in new issue