2016-02-15 21:09:34 +08:00
package commands
import (
2021-05-13 02:05:16 +08:00
"context"
2016-02-15 21:09:34 +08:00
"errors"
2016-03-10 21:43:21 +08:00
"fmt"
2016-02-15 21:09:34 +08:00
"os"
2019-07-29 16:44:58 +08:00
"runtime"
2016-03-13 18:29:43 +08:00
"strings"
2016-03-29 03:42:26 +08:00
2023-07-17 16:22:28 +08:00
"github.com/Masterminds/semver/v3"
2023-02-22 16:24:13 +08:00
"github.com/fatih/color"
2023-07-14 17:49:05 +08:00
2023-02-22 16:24:13 +08:00
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
2021-04-26 22:13:40 +08:00
"github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
2019-05-27 16:47:21 +08:00
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
2023-05-03 20:52:57 +08:00
"github.com/grafana/grafana/pkg/plugins"
2022-08-23 17:50:50 +08:00
"github.com/grafana/grafana/pkg/plugins/repo"
"github.com/grafana/grafana/pkg/plugins/storage"
2016-02-15 21:09:34 +08:00
)
2023-07-12 19:52:12 +08:00
const installArgsSize = 2
func validateInput ( c utils . CommandLine ) error {
2023-07-17 16:22:28 +08:00
args := c . Args ( )
argsLen := args . Len ( )
if argsLen > installArgsSize {
2023-07-12 19:52:12 +08:00
logger . Info ( color . RedString ( "Please specify the correct format. For example ./grafana cli (<command arguments>) plugins install <plugin ID> (<plugin version>)\n\n" ) )
return errors . New ( "install only supports 2 arguments: plugin and version" )
}
2023-07-17 16:22:28 +08:00
arg := args . First ( )
2016-02-15 21:09:34 +08:00
if arg == "" {
return errors . New ( "please specify plugin to install" )
}
2023-07-17 16:22:28 +08:00
if argsLen == installArgsSize {
version := args . Get ( 1 )
_ , err := semver . NewVersion ( version )
if err != nil {
2023-07-19 22:55:31 +08:00
logger . Info ( color . YellowString ( "The provided version doesn't use semantic versioning format\n\n" ) )
2023-07-17 16:22:28 +08:00
}
}
2016-06-25 02:14:58 +08:00
pluginsDir := c . PluginDirectory ( )
2016-03-29 03:42:26 +08:00
if pluginsDir == "" {
return errors . New ( "missing pluginsDir flag" )
2016-02-15 21:09:34 +08:00
}
2016-03-29 03:42:26 +08:00
fileInfo , err := os . Stat ( pluginsDir )
2016-03-11 21:11:25 +08:00
if err != nil {
2024-06-07 01:01:27 +08:00
if err = os . MkdirAll ( pluginsDir , 0 o750 ) ; err != nil {
2018-04-17 02:25:48 +08:00
return fmt . Errorf ( "pluginsDir (%s) is not a writable directory" , pluginsDir )
2016-03-11 21:11:25 +08:00
}
return nil
}
if ! fileInfo . IsDir ( ) {
2016-02-15 21:09:34 +08:00
return errors . New ( "path is not a directory" )
}
return nil
}
2023-02-22 16:24:13 +08:00
func logRestartNotice ( ) {
logger . Info ( color . GreenString ( "Please restart Grafana after installing or removing plugins. Refer to Grafana documentation for instructions if necessary.\n\n" ) )
}
2023-05-04 16:52:09 +08:00
func installCommand ( c utils . CommandLine ) error {
2023-07-12 19:52:12 +08:00
if err := validateInput ( c ) ; err != nil {
2016-02-15 21:09:34 +08:00
return err
}
2021-04-26 22:13:40 +08:00
pluginID := c . Args ( ) . First ( )
2016-02-15 21:09:34 +08:00
version := c . Args ( ) . Get ( 1 )
2024-07-09 22:46:30 +08:00
err := installPlugin ( context . Background ( ) , pluginID , version , newInstallPluginOpts ( c ) )
2023-02-22 16:24:13 +08:00
if err == nil {
logRestartNotice ( )
}
return err
2016-02-15 21:09:34 +08:00
}
2024-07-09 22:46:30 +08:00
type pluginInstallOpts struct {
insecure bool
repoURL string
pluginURL string
pluginDir string
}
func newInstallPluginOpts ( c utils . CommandLine ) pluginInstallOpts {
return pluginInstallOpts {
insecure : c . Bool ( "insecure" ) ,
repoURL : c . PluginRepoURL ( ) ,
pluginURL : c . PluginURL ( ) ,
pluginDir : c . PluginDirectory ( ) ,
}
}
2022-08-23 17:50:50 +08:00
// installPlugin downloads the plugin code as a zip file from the Grafana.com API
// and then extracts the zip into the plugin's directory.
2024-07-09 22:46:30 +08:00
func installPlugin ( ctx context . Context , pluginID , version string , o pluginInstallOpts ) error {
return doInstallPlugin ( ctx , pluginID , version , o , map [ string ] bool { } )
}
// doInstallPlugin is a recursive function that installs a plugin and its dependencies.
// installing is a map that keeps track of which plugins are currently being installed to avoid infinite loops.
func doInstallPlugin ( ctx context . Context , pluginID , version string , o pluginInstallOpts , installing map [ string ] bool ) error {
if installing [ pluginID ] {
return nil
}
installing [ pluginID ] = true
defer func ( ) {
installing [ pluginID ] = false
} ( )
2023-07-14 17:49:05 +08:00
// If a version is specified, check if it is already installed
if version != "" {
2024-07-09 22:46:30 +08:00
if services . PluginVersionInstalled ( pluginID , version , o . pluginDir ) {
2023-07-14 17:49:05 +08:00
services . Logger . Successf ( "Plugin %s v%s already installed." , pluginID , version )
return nil
}
}
2023-05-30 17:48:52 +08:00
repository := repo . NewManager ( repo . ManagerCfg {
2024-07-09 22:46:30 +08:00
SkipTLSVerify : o . insecure ,
BaseURL : o . repoURL ,
2023-05-30 17:48:52 +08:00
Logger : services . Logger ,
} )
2018-02-16 16:49:29 +08:00
2022-08-23 17:50:50 +08:00
compatOpts := repo . NewCompatOpts ( services . GrafanaVersion , runtime . GOOS , runtime . GOARCH )
var archive * repo . PluginArchive
var err error
2024-07-09 22:46:30 +08:00
pluginZipURL := o . pluginURL
2022-08-23 17:50:50 +08:00
if pluginZipURL != "" {
if archive , err = repository . GetPluginArchiveByURL ( ctx , pluginZipURL , compatOpts ) ; err != nil {
return err
}
} else {
if archive , err = repository . GetPluginArchive ( ctx , pluginID , version , compatOpts ) ; err != nil {
return err
}
}
2024-07-09 22:46:30 +08:00
pluginFs := storage . FileSystem ( services . Logger , o . pluginDir )
2023-07-14 17:49:05 +08:00
extractedArchive , err := pluginFs . Extract ( ctx , pluginID , storage . SimpleDirNameGeneratorFunc , archive . File )
2022-08-23 17:50:50 +08:00
if err != nil {
return err
}
for _ , dep := range extractedArchive . Dependencies {
2024-07-09 22:46:30 +08:00
services . Logger . Infof ( "Fetching %s dependency %s..." , pluginID , dep . ID )
return doInstallPlugin ( ctx , dep . ID , dep . Version , pluginInstallOpts {
insecure : o . insecure ,
repoURL : o . repoURL ,
pluginDir : o . pluginDir ,
} , installing )
2022-08-23 17:50:50 +08:00
}
return nil
2016-02-15 21:09:34 +08:00
}
2023-05-03 20:52:57 +08:00
// uninstallPlugin removes the plugin directory
func uninstallPlugin ( _ context . Context , pluginID string , c utils . CommandLine ) error {
2023-07-14 17:49:05 +08:00
for _ , bundle := range services . GetLocalPlugins ( c . PluginDirectory ( ) ) {
if bundle . Primary . JSONData . ID == pluginID {
logger . Infof ( "Removing plugin: %v\n" , pluginID )
if remover , ok := bundle . Primary . FS . ( plugins . FSRemover ) ; ok {
logger . Debugf ( "Removing directory %v\n\n" , bundle . Primary . FS . Base ( ) )
if err := remover . Remove ( ) ; err != nil {
return err
}
return nil
} else {
return fmt . Errorf ( "plugin %v is immutable and therefore cannot be uninstalled" , pluginID )
}
}
2023-05-03 20:52:57 +08:00
}
2023-07-14 17:49:05 +08:00
2023-05-03 20:52:57 +08:00
return nil
}
2019-07-29 16:44:58 +08:00
func osAndArchString ( ) string {
osString := strings . ToLower ( runtime . GOOS )
arch := runtime . GOARCH
return osString + "-" + arch
}
2023-05-08 16:58:47 +08:00
func supportsCurrentArch ( version models . Version ) bool {
2019-07-29 16:44:58 +08:00
if version . Arch == nil {
return true
}
for arch := range version . Arch {
if arch == osAndArchString ( ) || arch == "any" {
return true
}
}
return false
}
2023-05-08 16:58:47 +08:00
func latestSupportedVersion ( plugin models . Plugin ) * models . Version {
2020-06-26 14:46:08 +08:00
for _ , v := range plugin . Versions {
ver := v
2023-05-08 16:58:47 +08:00
if supportsCurrentArch ( ver ) {
2019-07-29 16:44:58 +08:00
return & ver
}
}
return nil
}