-
Notifications
You must be signed in to change notification settings - Fork 12k
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
CLI: Allow installing custom binary plugins #17551
Conversation
@@ -6,35 +6,51 @@ import ( | |||
"path/filepath" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some fixes here because after last change, you could run grafana-cli from repo root, it would install the plugin to wrong directory.
pkg/plugins/plugins.go
Outdated
@@ -164,11 +164,14 @@ func scan(pluginDir string) error { | |||
} | |||
|
|||
func (scanner *PluginScanner) walker(currentPath string, f os.FileInfo, err error) error { | |||
// TODO: Based on how we search for plugin.json in grafan-net fetchPluginJson, it should be enough to scan | |||
// just /src, /dist or / paths inside the plugin dir |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if having plugin.json anywhere in the package is something we want to support, it is not something we support for packages registered in grafana.com API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if having plugin.json anywhere in the package is something we want to support
Based on https://grafana.com/docs/plugins/developing/code-styleguide/#plugin-json-mandatory I would say that we shouldn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is dealing with plugins that embed other plugins, like https://github.com/raintank/worldping-app/tree/master/dist/grafana-worldmap-panel
Within an individual plugin we expect plugin.json to be in those locations, but we walk all the way down through the plugin to find any embedded plugins.
if err != nil { | ||
return err | ||
} | ||
|
||
if f.Name() == "node_modules" { | ||
if f.Name() == "node_modules" || f.Name() == "Chromium.app" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not liking this hardcoding here like this. If we allowed just one level of traversal it could probably just go away.
}() | ||
|
||
resp, err := http.Get(url) // #nosec | ||
if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bit of an issue in this old code. This does not check for response status. So even though for old versions this will return correctly 400 status because we do not know what is the arch of the grafana it will end up with:
zip: not a valid zip file
which is not helpful. Not sure how to make this better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@torkelo what is your opinion on this? Other solution could be not even return the arch specific plugin from the API if we do not sent arch from grafana (so old versions) in that case user would get plugin does not exists or something like that which does not seem much better and at the same time I think the API would be weirder and non standard (ie if you GET /plugins you would get just a subset and there wouldn't be a clear way to get all plugins no matter what arch, would need to add something like /plugins?all=true or something like that)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I understand fully. So new grafana-cli adds arch as parameter, the change to grafana.com api is that backwards compatible for non backend plugins (it will need to be). ?
So this is an error for old grafana versions trying to install a new grafana backend plugin? hard to modify that error message now I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@torkelo
Yes I think you get it but just to reiterate:
GET /plugins
will return all plugins with arch attribute and it is up to the client to filter them correctly- old grafana does not do anything so shows all plugins (so cli plugins list-remote will show everything without knowing which can be installed)
- new grafana will show only available
GET /plugins/:id/downloads
has a new optional paramosAndArch
. If set it will return the zip file for specified arch, if not set it will return zip only if frontend plugin or if there isany
arch zip file- old grafana can download non backend plugins as usual, for backend plugins it will error out with
zip: not a valid zip file
because it does not check for response status and we cannot know what arch the grafana is running on - new grafana sends the
osAndArch
so we pick the correct version (TODO I need to add the status check in the code) and does not allow you to install wrong plugin version with some sensible error message
- old grafana can download non backend plugins as usual, for backend plugins it will error out with
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So adding osAndArch
to GET /plugins
in a way that without it, it will return only plugins which can be safely installed when we do not know the arch would be best backward compatible way, but not sure if that does not break other things that would expect it to return all plugins by default. It is also kind of nonintuitive behaviour.
Other solution would be to start versioning the grafana.com API on some level (either with some header or v2 prefix maybe) if we would consider that a breaking API change. That would make doing changes a bit easier in sense of keeping backward compatibility but probably harder for maintenance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wish we could limit it to new versions but we cant really do that since we offer premium plugins for olders versions of Grafana.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So @DanCech pointed out to me here that we are actually sending OS and ARCH already so it seems it would make sense to reuse those headers. Unfortunately for this issue it just complicates things a bit.
- We only send the headers in the
/plugin/:id
request that gets the metadata but not in the/download
so we could know that plugin supports the grafana os/arch but than we still cannot select the correct version to download. - In the
/plugin/:id
we won't be able to say if it is GF version before the change or after because the headers will be sent also from the old versions.
I thought we could just base it on raw grafana version that we also send in the headers but it seems like it is hardcoded to master
7462577#diff-1146cad1c61c159a07758ade40c6ac2aR12 and does not seem to change since (I assume we forgot to add it to release guidelines that it needs to be updated or something like that)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That variable is set using a compile flag in build.go
Not sure i understand the problem can’t we add these headers to the download call as well. ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That variable is set using a compile flag in build.go
Ah ok then it can be used.
Not sure i understand the problem can’t we add these headers to the download call as well. ?
Yes I will add this. The only issue is old versions of Grafana before the change how will they handle installing plugins and what would happen if they try to install binary plugin. As I mentioned at the moment they will return zip: not a valid zip file
error which is not very helpful. With the grafana version sent, we can return some more helpful error message on the /plugin/:id
call for the old Grafana versions. This would mean minimal changes to the API but still old Grafana will get You need to upgrade your Grafana version to install this plugin
error or something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see. If you can find a way to show error to older Grafana versions that be great.
Maybe you can do that by checking the grafana dependency version plugins specify in plugin.json. This is a big missing thing we have yet to fix, that is return info error message when you try to install a plugin or plugin version that is not compatible with your Grafana version
if downloadURL == "" { | ||
plugin, err := s.GetPlugin(pluginName, c.RepoDirectory()) | ||
if strings.HasPrefix(pluginName, "grafana-") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good enough for now. Any thoughts about how we can sign plugins and verify them in Grafana?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting thought but I am not familiar with these kind of things that much. Only experience I have is iOS app signing and at least the dev experience adds a some unnecessary (I think) complexity. There it is done I think mainly so that you cannot install apps from unsupported places (outside of App store) but if we want to still allow installation of non signed third party plugins not sure if the signing will have that much added value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add signing to our ecosystem for sure. We can use that for whatever we need. Perhaps: automatically trust anything signed by grafana and a big big warnings when installing stuff signed by other people, and even bigger warnings when not signed at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well the question is what is the end goal of that. I mean right now what you can trust is anything that comes from our plugin API I assume. If we have signed plugins I assume you could distribute it by other means and still be able to decide whether to trust it based on whether it is signed, that I assume would allow something like a third party plugin API with trusted plugins but not sure that is important for us.
I am not against that or anything I am just genuinely curious what are the benefits because from my very modest understanding of the topic I do not see that many.
if latestForArch.Version == ver.Version { | ||
return ver, nil | ||
} | ||
return nil, xerrors.Errorf("Version you want is not supported on your architecture. Latest suitable version is %v", latestForArch.Version) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we should create a helper function for this in errutil
?
just to avoid taking deps on the current xerror path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the future this should be changed to errors
and I think it should no be much work to just do s/xerrors/errors
globally. Aliasing everything behind errutils whether transiently or forever does not seem worth it to me, either we will need to rename it later too or we have alias for otherwise standard go package which can be confusing.
}() | ||
|
||
resp, err := http.Get(url) // #nosec | ||
if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wish we could limit it to new versions but we cant really do that since we offer premium plugins for olders versions of Grafana.
func (client *GrafanaComClient) DownloadFile(pluginName, filePath, url string, checksum string) (content []byte, err error) { | ||
client.retryCount = 0 | ||
|
||
defer func() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
) | ||
|
||
func GetGrafanaPluginDir(currentOS string) string { | ||
if isDevEnvironment() { | ||
return "../data/plugins" | ||
rootPath, err := getGrafanaRoot() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Taking a look at this PR is on my things to do - sadly, it's quite a heavy change so it's going to take me some time.
I'll try and find some today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the work @gotjosh is doing goes in better direction. This is more of a quick fix (maybe for separate PR) but it does not seem to directly clash, just something that will be overwritten anyway later on I assume.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! Code wise looking really good and very nice tests 👍
Have added some comments where some of them needs to be fixed and some others is just nit picking. We can you through them together if you want? Haven't done any manual tests yet, only code review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code wise looks great so approving this 👍 But maybe need manual testing before merge?
* grafana/master: Plugins: return a promise for loadPluginCss (grafana#18273) Utils: avoid calling console.warn() too often for deprecation warnings (grafana#18269) CLI: Allow installing custom binary plugins (grafana#17551) Docs: Update link to example app (grafana#18253) GettingStarted: Skip Query for getting started (grafana#18268)
* grafana/master: Chore: noImplicitAny Sub 500 errors (#18287) Plugins: return a promise for loadPluginCss (#18273) Utils: avoid calling console.warn() too often for deprecation warnings (#18269) CLI: Allow installing custom binary plugins (#17551) Docs: Update link to example app (#18253) GettingStarted: Skip Query for getting started (#18268) v6.3.0-beta2 is latest testing (#18283) Release: Changelog update with v6.3.0-beta2 (#18281)
* grafana/master: (82 commits) TablePanel: Remove scroll option on TablePanel (grafana#18318) Keyboard Shortcuts: Sign in to enable them (grafana#18271) GitHub Templates: Pull Request Template update (grafana#18300) Auth Proxy: Include additional headers as part of the cache key (grafana#18298) grafana/toolkit: support windows paths (grafana#18306) Chore: noImplicitAny Sub 500 errors (grafana#18287) Plugins: return a promise for loadPluginCss (grafana#18273) Utils: avoid calling console.warn() too often for deprecation warnings (grafana#18269) CLI: Allow installing custom binary plugins (grafana#17551) Docs: Update link to example app (grafana#18253) GettingStarted: Skip Query for getting started (grafana#18268) v6.3.0-beta2 is latest testing (grafana#18283) Release: Changelog update with v6.3.0-beta2 (grafana#18281) Chore: Upgrades typescript to version 3.5 (grafana#18263) docs: team sync (grafana#18239) SAML: Only show SAML login button on Enterprise version (grafana#18270) Permissions: Show plugins in nav for non admin users but hide plugin configuration (grafana#18234) CI: Change target branch in CI task trigger-docs-update (grafana#18255) Plugins: Include build number and PR in metadata (grafana#18260) Run End-to-End tests for release builds (grafana#18211) ...
* grafana/master: (82 commits) TablePanel: Remove scroll option on TablePanel (grafana#18318) Keyboard Shortcuts: Sign in to enable them (grafana#18271) GitHub Templates: Pull Request Template update (grafana#18300) Auth Proxy: Include additional headers as part of the cache key (grafana#18298) grafana/toolkit: support windows paths (grafana#18306) Chore: noImplicitAny Sub 500 errors (grafana#18287) Plugins: return a promise for loadPluginCss (grafana#18273) Utils: avoid calling console.warn() too often for deprecation warnings (grafana#18269) CLI: Allow installing custom binary plugins (grafana#17551) Docs: Update link to example app (grafana#18253) GettingStarted: Skip Query for getting started (grafana#18268) v6.3.0-beta2 is latest testing (grafana#18283) Release: Changelog update with v6.3.0-beta2 (grafana#18281) Chore: Upgrades typescript to version 3.5 (grafana#18263) docs: team sync (grafana#18239) SAML: Only show SAML login button on Enterprise version (grafana#18270) Permissions: Show plugins in nav for non admin users but hide plugin configuration (grafana#18234) CI: Change target branch in CI task trigger-docs-update (grafana#18255) Plugins: Include build number and PR in metadata (grafana#18260) Run End-to-End tests for release builds (grafana#18211) ...
This is Grafana side of things for https://github.com/raintank/grafana-net/pull/790
Basically it works the same as before, ask grafana.com API for plugin, get a version and then download it via the same API. Main things this add:
There is an issue with how to handle the decompressing properly. Symlinks can create an issue but I think only in case the privileges would be elevated when running. For example install it as user, symlink to some file you do not have access to, the run as root and read the symlink. This is not happening and only issue could be when we scan for plugin.json which we do recursively while following symlinks but I think this should be changed so it goes only 2 level down (see comment in the code). But this could be an issue in the future.
Another issue is probably this https://snyk.io/research/zip-slip-vulnerability. Basically you can unzip files into destination that are outside of the plugin directory. Right now we do not check that in any way. This though is probably at the moment problem only when installing something from custom url. Installing plugin through grafana api installs it only via github source zipball and so I do not think it is possible to generate malicious zip that way. There is popular library that handles archiving and they do not handle this:
Update:
The final version of the code checks for a zip-slip issue directly. Not considering symlinks this seems to be easy. Symlinks are only allowed for internal Grafana plugins (as it is needed only for the renderer and the bundled Chromium right now).
Ref #18083
TODO: