github.com/ezbuy/gauge@v0.9.4-0.20171013092048-7ac5bd3931cd/plugin/install/install.go (about)

     1  // Copyright 2015 ThoughtWorks, Inc.
     2  
     3  // This file is part of Gauge.
     4  
     5  // Gauge is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  
    10  // Gauge is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU General Public License for more details.
    14  
    15  // You should have received a copy of the GNU General Public License
    16  // along with Gauge.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package install
    19  
    20  import (
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"os"
    25  	"path"
    26  	"path/filepath"
    27  	"regexp"
    28  	"runtime"
    29  	"sort"
    30  	"strings"
    31  
    32  	"github.com/getgauge/common"
    33  	"github.com/getgauge/gauge/config"
    34  	"github.com/getgauge/gauge/logger"
    35  	"github.com/getgauge/gauge/manifest"
    36  	"github.com/getgauge/gauge/plugin"
    37  	"github.com/getgauge/gauge/runner"
    38  	"github.com/getgauge/gauge/util"
    39  	"github.com/getgauge/gauge/version"
    40  )
    41  
    42  const (
    43  	pluginJSON = "plugin.json"
    44  	jsonExt    = ".json"
    45  	x86        = "386"
    46  )
    47  
    48  type installDescription struct {
    49  	Name        string
    50  	Description string
    51  	Versions    []versionInstallDescription
    52  }
    53  
    54  type versionInstallDescription struct {
    55  	Version             string
    56  	GaugeVersionSupport version.VersionSupport
    57  	Install             platformSpecificCommand
    58  	DownloadUrls        downloadUrls
    59  }
    60  
    61  type downloadUrls struct {
    62  	X86 platformSpecificURL
    63  	X64 platformSpecificURL
    64  }
    65  
    66  type platformSpecificCommand struct {
    67  	Windows []string
    68  	Linux   []string
    69  	Darwin  []string
    70  }
    71  
    72  type platformSpecificURL struct {
    73  	Windows string
    74  	Linux   string
    75  	Darwin  string
    76  }
    77  
    78  // InstallResult represents the result of plugin installation
    79  type InstallResult struct {
    80  	Error   error
    81  	Warning string
    82  	Info    string
    83  	Success bool
    84  	Skipped bool
    85  }
    86  
    87  func (installResult *InstallResult) getMessage() string {
    88  	return installResult.Error.Error()
    89  }
    90  
    91  func installError(err error) InstallResult {
    92  	return InstallResult{Error: err, Success: false}
    93  }
    94  
    95  func installSuccess(info string) InstallResult {
    96  	return InstallResult{Info: info, Success: true}
    97  }
    98  func installSkipped(warning, info string) InstallResult {
    99  	return InstallResult{Warning: warning, Info: info, Skipped: true}
   100  }
   101  
   102  // GaugePlugin represents any plugin to Gauge. It can be an language runner or any other plugin.
   103  type GaugePlugin struct {
   104  	ID          string
   105  	Version     string
   106  	Description string
   107  	PreInstall  struct {
   108  		Windows []string
   109  		Linux   []string
   110  		Darwin  []string
   111  	}
   112  	PostInstall struct {
   113  		Windows []string
   114  		Linux   []string
   115  		Darwin  []string
   116  	}
   117  	PreUnInstall struct {
   118  		Windows []string
   119  		Linux   []string
   120  		Darwin  []string
   121  	}
   122  	PostUnInstall struct {
   123  		Windows []string
   124  		Linux   []string
   125  		Darwin  []string
   126  	}
   127  	GaugeVersionSupport version.VersionSupport
   128  }
   129  
   130  func getGoArch() string {
   131  	arch := runtime.GOARCH
   132  	if arch == x86 {
   133  		return "x86"
   134  	}
   135  	return "x86_64"
   136  }
   137  
   138  func isPlatformIndependent(zipfile string) bool {
   139  	re, _ := regexp.Compile("-([a-z]*)\\.")
   140  	return !re.MatchString(zipfile)
   141  }
   142  
   143  func isOsOSCompatible(zipfile string) bool {
   144  	os := runtime.GOOS
   145  	arch := getGoArch()
   146  	return strings.Contains(zipfile, fmt.Sprintf("%s.%s", os, arch))
   147  }
   148  
   149  // InstallPluginFromZipFile installs plugin from given zip file
   150  func InstallPluginFromZipFile(zipFile string, pluginName string) InstallResult {
   151  	tempDir := common.GetTempDir()
   152  	defer common.Remove(tempDir)
   153  	unzippedPluginDir, err := common.UnzipArchive(zipFile, tempDir)
   154  	if err != nil {
   155  		return installError(err)
   156  	}
   157  	if !isPlatformIndependent(zipFile) && !isOsOSCompatible(zipFile) {
   158  		err := fmt.Errorf("Provided plugin is not compatible with OS %s_%s.", runtime.GOOS, runtime.GOARCH)
   159  		return installError(err)
   160  	}
   161  	gp, err := parsePluginJSON(unzippedPluginDir, pluginName)
   162  	if err != nil || gp.ID != pluginName {
   163  		err := fmt.Errorf("Provided zip file is not a valid plugin of %s.", pluginName)
   164  		return installError(err)
   165  	}
   166  	if err = runPlatformCommands(gp.PreInstall, unzippedPluginDir); err != nil {
   167  		return installError(err)
   168  	}
   169  
   170  	pluginInstallDir, err := getPluginInstallDir(gp.ID, getVersionedPluginDirName(zipFile))
   171  	if err != nil {
   172  		return installError(err)
   173  	}
   174  
   175  	// copy files to gauge plugin install location
   176  	logger.Debugf("Installing plugin %s %s", gp.ID, filepath.Base(pluginInstallDir))
   177  	if _, err = common.MirrorDir(unzippedPluginDir, pluginInstallDir); err != nil {
   178  		return installError(err)
   179  	}
   180  
   181  	if err = runPlatformCommands(gp.PostInstall, pluginInstallDir); err != nil {
   182  		return installError(err)
   183  	}
   184  	return installSuccess("")
   185  }
   186  
   187  func getPluginInstallDir(pluginID, pluginDirName string) (string, error) {
   188  	pluginsDir, err := common.GetPrimaryPluginsInstallDir()
   189  	if err != nil {
   190  		return "", err
   191  	}
   192  	pluginDirPath := filepath.Join(pluginsDir, pluginID, pluginDirName)
   193  	if common.DirExists(pluginDirPath) {
   194  		return "", fmt.Errorf("Plugin %s %s already installed at %s", pluginID, pluginDirName, pluginDirPath)
   195  	}
   196  	return pluginDirPath, nil
   197  }
   198  
   199  func parsePluginJSON(pluginDir, pluginName string) (*GaugePlugin, error) {
   200  	var file string
   201  	if common.FileExists(filepath.Join(pluginDir, pluginName+jsonExt)) {
   202  		file = filepath.Join(pluginDir, pluginName+jsonExt)
   203  	} else {
   204  		file = filepath.Join(pluginDir, pluginJSON)
   205  	}
   206  
   207  	var gp GaugePlugin
   208  	contents, err := common.ReadFileContents(file)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	if err = json.Unmarshal([]byte(contents), &gp); err != nil {
   213  		return nil, err
   214  	}
   215  	return &gp, nil
   216  }
   217  
   218  // Plugin download and install the latest plugin(if version not specified) of given plugin name
   219  func Plugin(pluginName, version string) InstallResult {
   220  	logger.Debugf("Gathering metadata for %s", pluginName)
   221  	installDescription, result := getInstallDescription(pluginName, false)
   222  	defer util.RemoveTempDir()
   223  	if !result.Success {
   224  		return result
   225  	}
   226  	return installPluginWithDescription(installDescription, version)
   227  }
   228  
   229  func installPluginWithDescription(installDescription *installDescription, currentVersion string) InstallResult {
   230  	var versionInstallDescription *versionInstallDescription
   231  	var err error
   232  	if currentVersion != "" {
   233  		versionInstallDescription, err = installDescription.getVersion(currentVersion)
   234  		if err != nil {
   235  			return installError(err)
   236  		}
   237  		if compatibilityError := version.CheckCompatibility(version.CurrentGaugeVersion, &versionInstallDescription.GaugeVersionSupport); compatibilityError != nil {
   238  			return installError(fmt.Errorf("Plugin Version %s-%s is not supported for gauge %s : %s", installDescription.Name, versionInstallDescription.Version, version.CurrentGaugeVersion.String(), compatibilityError.Error()))
   239  		}
   240  	} else {
   241  		versionInstallDescription, err = installDescription.getLatestCompatibleVersionTo(version.CurrentGaugeVersion)
   242  		if err != nil {
   243  			return installError(fmt.Errorf("Could not find compatible version for plugin %s. : %s", installDescription.Name, err))
   244  		}
   245  	}
   246  	return installPluginVersion(installDescription, versionInstallDescription)
   247  }
   248  
   249  func installPluginVersion(installDesc *installDescription, versionInstallDescription *versionInstallDescription) InstallResult {
   250  	if common.IsPluginInstalled(installDesc.Name, versionInstallDescription.Version) {
   251  		return installSkipped("", fmt.Sprintf("Plugin %s %s is already installed.", installDesc.Name, versionInstallDescription.Version))
   252  	}
   253  
   254  	downloadLink, err := getDownloadLink(versionInstallDescription.DownloadUrls)
   255  	if err != nil {
   256  		return installError(fmt.Errorf("Could not get download link: %s", err.Error()))
   257  	}
   258  
   259  	tempDir := common.GetTempDir()
   260  	defer common.Remove(tempDir)
   261  	logger.Debugf("Downloading %s", filepath.Base(downloadLink))
   262  	pluginZip, err := util.Download(downloadLink, tempDir, "", false)
   263  	if err != nil {
   264  		return installError(fmt.Errorf("Failed to download the plugin. %s", err.Error()))
   265  	}
   266  	return InstallPluginFromZipFile(pluginZip, installDesc.Name)
   267  }
   268  
   269  func runPlatformCommands(commands platformSpecificCommand, workingDir string) error {
   270  	command := []string{}
   271  	switch runtime.GOOS {
   272  	case "windows":
   273  		command = commands.Windows
   274  		break
   275  	case "darwin":
   276  		command = commands.Darwin
   277  		break
   278  	default:
   279  		command = commands.Linux
   280  		break
   281  	}
   282  
   283  	if len(command) == 0 {
   284  		return nil
   285  	}
   286  
   287  	logger.Debugf("Running plugin hook command => %s", command)
   288  	cmd, err := common.ExecuteSystemCommand(command, workingDir, os.Stdout, os.Stderr)
   289  
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	return cmd.Wait()
   295  }
   296  
   297  // UninstallPlugin uninstall the given plugin of the given uninstallVersion
   298  // If uninstallVersion is not specified, it uninstalls all the versions of given plugin
   299  func UninstallPlugin(pluginName string, uninstallVersion string) {
   300  	pluginsHome, err := common.GetPrimaryPluginsInstallDir()
   301  	if err != nil {
   302  		logger.Fatalf("Failed to uninstall plugin %s. %s", pluginName, err.Error())
   303  	}
   304  	if !common.DirExists(filepath.Join(pluginsHome, pluginName, uninstallVersion)) {
   305  		logger.Errorf("Plugin %s not found.", strings.TrimSpace(pluginName+" "+uninstallVersion))
   306  		os.Exit(0)
   307  	}
   308  	var failed bool
   309  	pluginsDir := filepath.Join(pluginsHome, pluginName)
   310  	filepath.Walk(pluginsDir, func(dir string, info os.FileInfo, err error) error {
   311  		if err == nil && info.IsDir() && dir != pluginsDir {
   312  			if matchesUninstallVersion(filepath.Base(dir), uninstallVersion) {
   313  				if err := uninstallVersionOfPlugin(dir, pluginName, filepath.Base(dir)); err != nil {
   314  					logger.Errorf("Failed to uninstall plugin %s %s. %s", pluginName, uninstallVersion, err.Error())
   315  					failed = true
   316  				}
   317  			}
   318  		}
   319  		return nil
   320  	})
   321  	if failed {
   322  		os.Exit(1)
   323  	}
   324  	if uninstallVersion == "" {
   325  		if err := os.RemoveAll(pluginsDir); err != nil {
   326  			logger.Fatalf("Failed to remove directory %s. %s", pluginsDir, err.Error())
   327  		}
   328  	}
   329  }
   330  
   331  func matchesUninstallVersion(pluginDirPath, uninstallVersion string) bool {
   332  	if uninstallVersion == "" {
   333  		return true
   334  	}
   335  	return pluginDirPath == uninstallVersion
   336  }
   337  
   338  func uninstallVersionOfPlugin(pluginDir, pluginName, uninstallVersion string) error {
   339  	gp, err := parsePluginJSON(pluginDir, pluginName)
   340  	if err != nil {
   341  		return err
   342  	}
   343  	if err := runPlatformCommands(gp.PreUnInstall, pluginDir); err != nil {
   344  		return err
   345  	}
   346  
   347  	if err := os.RemoveAll(pluginDir); err != nil {
   348  		return err
   349  	}
   350  	if err := runPlatformCommands(gp.PostUnInstall, path.Dir(pluginDir)); err != nil {
   351  		return err
   352  	}
   353  	logger.Infof("Successfully uninstalled plugin %s %s.", pluginName, uninstallVersion)
   354  	return nil
   355  }
   356  
   357  func getDownloadLink(downloadUrls downloadUrls) (string, error) {
   358  	var platformLinks *platformSpecificURL
   359  	if strings.Contains(runtime.GOARCH, "64") {
   360  		platformLinks = &downloadUrls.X64
   361  	} else {
   362  		platformLinks = &downloadUrls.X86
   363  	}
   364  
   365  	var downloadLink string
   366  	switch runtime.GOOS {
   367  	case "windows":
   368  		downloadLink = platformLinks.Windows
   369  		break
   370  	case "darwin":
   371  		downloadLink = platformLinks.Darwin
   372  		break
   373  	default:
   374  		downloadLink = platformLinks.Linux
   375  		break
   376  	}
   377  	if downloadLink == "" {
   378  		return "", fmt.Errorf("Platform not supported for %s. Download URL not specified.", runtime.GOOS)
   379  	}
   380  	return downloadLink, nil
   381  }
   382  
   383  func getInstallDescription(plugin string, silent bool) (*installDescription, InstallResult) {
   384  	versionInstallDescriptionJSONFile := plugin + "-install.json"
   385  	versionInstallDescriptionJSONUrl, result := constructPluginInstallJSONURL(plugin)
   386  	if !result.Success {
   387  		return nil, installError(fmt.Errorf("Could not construct plugin install json file URL. %s", result.Error))
   388  	}
   389  	tempDir := common.GetTempDir()
   390  	defer common.Remove(tempDir)
   391  
   392  	downloadedFile, downloadErr := util.Download(versionInstallDescriptionJSONUrl, tempDir, versionInstallDescriptionJSONFile, silent)
   393  	if downloadErr != nil {
   394  		logger.Debugf("Failed to download %s file: %s", versionInstallDescriptionJSONFile, downloadErr)
   395  		return nil, installError(fmt.Errorf("Invalid plugin. Could not download %s file.", versionInstallDescriptionJSONFile))
   396  	}
   397  
   398  	return getInstallDescriptionFromJSON(downloadedFile)
   399  }
   400  
   401  func getInstallDescriptionFromJSON(installJSON string) (*installDescription, InstallResult) {
   402  	InstallJSONContents, readErr := common.ReadFileContents(installJSON)
   403  	if readErr != nil {
   404  		return nil, installError(readErr)
   405  	}
   406  	installDescription := &installDescription{}
   407  	if err := json.Unmarshal([]byte(InstallJSONContents), installDescription); err != nil {
   408  		return nil, installError(err)
   409  	}
   410  	return installDescription, installSuccess("")
   411  }
   412  
   413  func constructPluginInstallJSONURL(p string) (string, InstallResult) {
   414  	repoURL := config.GaugeRepositoryUrl()
   415  	if repoURL == "" {
   416  		return "", installError(fmt.Errorf("Could not find gauge repository url from configuration."))
   417  	}
   418  	JSONURL := fmt.Sprintf("%s/%s", repoURL, p)
   419  	if qp := plugin.QueryParams(); qp != "" {
   420  		JSONURL += qp
   421  	}
   422  	return JSONURL, installSuccess("")
   423  }
   424  
   425  func (installDesc *installDescription) getVersion(version string) (*versionInstallDescription, error) {
   426  	for _, versionInstallDescription := range installDesc.Versions {
   427  		if versionInstallDescription.Version == version {
   428  			return &versionInstallDescription, nil
   429  		}
   430  	}
   431  	return nil, errors.New("Could not find install description for Version " + version)
   432  }
   433  
   434  func (installDesc *installDescription) getLatestCompatibleVersionTo(currentVersion *version.Version) (*versionInstallDescription, error) {
   435  	installDesc.sortVersionInstallDescriptions()
   436  	for _, versionInstallDesc := range installDesc.Versions {
   437  		if err := version.CheckCompatibility(currentVersion, &versionInstallDesc.GaugeVersionSupport); err == nil {
   438  			return &versionInstallDesc, nil
   439  		}
   440  	}
   441  	return nil, fmt.Errorf("Compatible version to %s not found", currentVersion)
   442  }
   443  
   444  func (installDesc *installDescription) sortVersionInstallDescriptions() {
   445  	sort.Sort(byDecreasingVersion(installDesc.Versions))
   446  }
   447  
   448  func getVersionedPluginDirName(pluginZip string) string {
   449  	zipFileName := filepath.Base(pluginZip)
   450  	if !strings.Contains(zipFileName, "nightly") {
   451  		re, _ := regexp.Compile("[0-9]+\\.[0-9]+\\.[0-9]+")
   452  		return re.FindString(zipFileName)
   453  	}
   454  	re, _ := regexp.Compile("[0-9]+\\.[0-9]+\\.[0-9]+\\.nightly-[0-9]+-[0-9]+-[0-9]+")
   455  	return re.FindString(zipFileName)
   456  }
   457  
   458  func getRunnerJSONContents(file string) (*runner.RunnerInfo, error) {
   459  	var r runner.RunnerInfo
   460  	contents, err := common.ReadFileContents(file)
   461  	if err != nil {
   462  		return nil, err
   463  	}
   464  	err = json.Unmarshal([]byte(contents), &r)
   465  	if err != nil {
   466  		return nil, err
   467  	}
   468  	return &r, nil
   469  }
   470  
   471  // AllPlugins install the latest version of all plugins specified in Gauge project manifest file
   472  func AllPlugins() {
   473  	manifest, err := manifest.ProjectManifest()
   474  	if err != nil {
   475  		logger.Fatalf(err.Error())
   476  	}
   477  	installPluginsFromManifest(manifest)
   478  }
   479  
   480  // UpdatePlugins updates all the currently installed plugins to its latest version
   481  func UpdatePlugins() {
   482  	var failedPlugin []string
   483  	for _, pluginInfo := range plugin.GetPluginsInfo() {
   484  		logger.Debugf("Updating plugin '%s'", pluginInfo.Name)
   485  		passed := HandleUpdateResult(Plugin(pluginInfo.Name, ""), pluginInfo.Name, false)
   486  		if !passed {
   487  			failedPlugin = append(failedPlugin, pluginInfo.Name)
   488  		}
   489  	}
   490  	if len(failedPlugin) > 0 {
   491  		logger.Fatalf("Failed to update '%s' plugins.", strings.Join(failedPlugin, ", "))
   492  	}
   493  	logger.Infof("Successfully updated all the plugins.")
   494  }
   495  
   496  // HandleInstallResult handles the result of plugin Installation
   497  // TODO: Merge both HandleInstallResult and HandleUpdateResult, eliminate boolean exitIfFailure
   498  func HandleInstallResult(result InstallResult, pluginName string, exitIfFailure bool) bool {
   499  	if result.Info != "" {
   500  		logger.Debugf(result.Info)
   501  	}
   502  	if result.Warning != "" {
   503  		logger.Warningf(result.Warning)
   504  	}
   505  	if result.Skipped {
   506  		return true
   507  	}
   508  	if !result.Success {
   509  		logger.Errorf("Failed to install plugin '%s'.\nReason: %s", pluginName, result.getMessage())
   510  		if exitIfFailure {
   511  			os.Exit(1)
   512  		}
   513  		return false
   514  	}
   515  
   516  	logger.Infof("Successfully installed plugin '%s'.", pluginName)
   517  	return true
   518  }
   519  
   520  // HandleUpdateResult handles the result of plugin Installation
   521  func HandleUpdateResult(result InstallResult, pluginName string, exitIfFailure bool) bool {
   522  	if result.Info != "" {
   523  		logger.Debugf(result.Info)
   524  	}
   525  	if result.Warning != "" {
   526  		logger.Warningf(result.Warning)
   527  	}
   528  	if result.Skipped {
   529  		return true
   530  	}
   531  	if !result.Success {
   532  		logger.Errorf("Failed to update plugin '%s'.\nReason: %s", pluginName, result.getMessage())
   533  		if exitIfFailure {
   534  			os.Exit(1)
   535  		}
   536  		return false
   537  	}
   538  	logger.Infof("Successfully updated plugin '%s'.", pluginName)
   539  	return true
   540  }
   541  
   542  func installPluginsFromManifest(manifest *manifest.Manifest) {
   543  	pluginsMap := make(map[string]bool, 0)
   544  	pluginsMap[manifest.Language] = true
   545  	for _, plugin := range manifest.Plugins {
   546  		pluginsMap[plugin] = false
   547  	}
   548  
   549  	for pluginName, isRunner := range pluginsMap {
   550  		if !IsCompatiblePluginInstalled(pluginName, isRunner) {
   551  			logger.Infof("Compatible version of plugin %s not found. Installing plugin %s...", pluginName, pluginName)
   552  			HandleInstallResult(Plugin(pluginName, ""), pluginName, false)
   553  		} else {
   554  			logger.Debugf("Plugin %s is already installed.", pluginName)
   555  		}
   556  	}
   557  }
   558  
   559  // IsCompatiblePluginInstalled checks if a plugin compatible to gauge is installed
   560  // TODO: This always checks if latest installed version of a given plugin is compatible. This should also check for older versions.
   561  func IsCompatiblePluginInstalled(pluginName string, isRunner bool) bool {
   562  	if isRunner {
   563  		return isCompatibleLanguagePluginInstalled(pluginName)
   564  	}
   565  	pd, err := plugin.GetPluginDescriptor(pluginName, "")
   566  	if err != nil {
   567  		return false
   568  	}
   569  	return version.CheckCompatibility(version.CurrentGaugeVersion, &pd.GaugeVersionSupport) == nil
   570  }
   571  
   572  func isCompatibleLanguagePluginInstalled(name string) bool {
   573  	jsonFilePath, err := plugin.GetLanguageJSONFilePath(name)
   574  	if err != nil {
   575  		return false
   576  	}
   577  
   578  	r, err := getRunnerJSONContents(jsonFilePath)
   579  	if err != nil {
   580  		return false
   581  	}
   582  	return version.CheckCompatibility(version.CurrentGaugeVersion, &r.GaugeVersionSupport) == nil
   583  }
   584  
   585  type byDecreasingVersion []versionInstallDescription
   586  
   587  func (a byDecreasingVersion) Len() int      { return len(a) }
   588  func (a byDecreasingVersion) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   589  func (a byDecreasingVersion) Less(i, j int) bool {
   590  	version1, _ := version.ParseVersion(a[i].Version)
   591  	version2, _ := version.ParseVersion(a[j].Version)
   592  	return version1.IsGreaterThan(version2)
   593  }
   594  
   595  // AddPluginToProject adds the given plugin to current Gauge project.
   596  func AddPluginToProject(pluginName string) error {
   597  	m, err := manifest.ProjectManifest()
   598  	if err != nil {
   599  		return nil
   600  	}
   601  	if plugin.IsLanguagePlugin(pluginName) {
   602  		return nil
   603  	}
   604  	pd, err := plugin.GetPluginDescriptor(pluginName, "")
   605  	if err != nil {
   606  		return err
   607  	}
   608  	if plugin.IsPluginAdded(m, pd) {
   609  		logger.Debugf("Plugin %s is already added.", pd.Name)
   610  		return nil
   611  	}
   612  	m.Plugins = append(m.Plugins, pd.ID)
   613  	if err = m.Save(); err != nil {
   614  		return err
   615  	}
   616  	logger.Infof("Plugin %s was successfully added to the project\n", pluginName)
   617  	return nil
   618  }