MM-19606- Rework Prepackaged Plugins (#13449)
* MM-19609 - Add new prepackage configuration settings (#13062) * Add signatures to the prepackaged plugins (#13138) * MM-19612 - Support querying local plugin marketplace when upst… (#13250) * MM-19612 - Support querying local plugin marketplace when upstream unavailable or disabled * Update translations file * Fixed comment * Updated to check EnableRemoteMarketplace setting and LocalOnly to get marketplace plugins * Fixed unit tests * Tests cleanup code * Removed unused error message * Updated tests * MM-19614- Updated Marketplace Service error id (#13388) * [MM-19610] Consume prepackaged plugins (#13005) * consume prepackaged plugins into memory * missing i18n * remove spurious .gitignore changes * return on failure to install prepackged plugins * cleanup * s/plugins/availablePlugins * whitespace * don't return extractDir when not needed * s/plug/plugin * error on icon, cleanup * update armored version of testplugin signature * honour AutomaticPrepackagedPlugins * document getPrepackagedPlugin * MM-19613 - Include prepackaged plugins in marketplace results (#13433) * Added prepackaged plugins to marketplace results * PR Feedback * PR Feedback * Update error where definition * Removing unnecessary var declaration * Updated comments * MM-21263 - Use EnableRemoteMarketplace in marketplace install… (#13438) * MM-21263 - Use EnableRemoteMarketplace in marketplace install endpoint * Call updateConfig before calling NewServer in TestHelper * Added translations * PR feedback * Translations * Feedback * s/helpers.go/download.go * Converging env.PrepackagedPlugins * Initial PR feedback * Ordered imports properly * Updated DownloadURL to return slice of bytes * Fixed method typo * Fixed logging * Added read lock for prepackaged plugins list * PR Feedback * Added condition to only install prepackaged plugin if it was previously enabled * Linting * Updated to check plugin state in config * Closing filereader * Only add local label if remote marketplace is enabled * Updated local tag description * Fixed tests Co-authored-by: Ali Farooq <ali.farooq0@pm.me> Co-authored-by: Shota Gvinepadze <wineson@gmail.com> Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
508fbf2f53
Коммит
87eb7697f9
44
app/download.go
Обычный файл
44
app/download.go
Обычный файл
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// HTTP_REQUEST_TIMEOUT defines a high timeout for downloading large files
|
||||
// from an external URL to avoid slow connections from failing to install.
|
||||
HTTP_REQUEST_TIMEOUT = 1 * time.Hour
|
||||
)
|
||||
|
||||
func (a *App) DownloadFromURL(downloadURL string) ([]byte, error) {
|
||||
if !model.IsValidHttpUrl(downloadURL) {
|
||||
return nil, errors.Errorf("invalid url %s", downloadURL)
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(downloadURL)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("failed to parse url %s", downloadURL)
|
||||
}
|
||||
if !*a.Config().PluginSettings.AllowInsecureDownloadUrl && u.Scheme != "https" {
|
||||
return nil, errors.Errorf("insecure url not allowed %s", downloadURL)
|
||||
}
|
||||
|
||||
client := a.HTTPService.MakeClient(true)
|
||||
client.Timeout = HTTP_REQUEST_TIMEOUT
|
||||
|
||||
resp, err := client.Get(downloadURL)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to fetch from %s", downloadURL)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
@@ -37,11 +37,21 @@ func setupTestHelper(enterprise bool, tb testing.TB) *TestHelper {
|
||||
store := mainHelper.GetStore()
|
||||
store.DropAllTables()
|
||||
|
||||
tempWorkspace, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true})
|
||||
if err != nil {
|
||||
panic("failed to initialize memory store: " + err.Error())
|
||||
}
|
||||
|
||||
config := memoryStore.Get()
|
||||
*config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
|
||||
*config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
memoryStore.Set(config)
|
||||
|
||||
var options []Option
|
||||
options = append(options, ConfigStore(memoryStore))
|
||||
options = append(options, StoreOverride(mainHelper.Store))
|
||||
@@ -88,18 +98,9 @@ func setupTestHelper(enterprise bool, tb testing.TB) *TestHelper {
|
||||
}
|
||||
|
||||
if th.tempWorkspace == "" {
|
||||
dir, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
th.tempWorkspace = dir
|
||||
th.tempWorkspace = tempWorkspace
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(th.tempWorkspace, "plugins")
|
||||
webappDir := filepath.Join(th.tempWorkspace, "webapp")
|
||||
|
||||
th.App.InitPlugins(pluginDir, webappDir)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
|
||||
435
app/plugin.go
435
app/plugin.go
@@ -4,6 +4,10 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -16,9 +20,14 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v5/services/filesstore"
|
||||
"github.com/mattermost/mattermost-server/v5/services/marketplace"
|
||||
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
||||
|
||||
"github.com/blang/semver"
|
||||
svg "github.com/h2non/go-is-svg"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const prepackagedPluginsDir = "prepackaged_plugins"
|
||||
|
||||
type pluginSignaturePath struct {
|
||||
pluginId string
|
||||
path string
|
||||
@@ -163,32 +172,9 @@ func (a *App) InitPlugins(pluginDir, webappPluginDir string) {
|
||||
mlog.Error("Failed to sync plugins from the file store", mlog.Err(err))
|
||||
}
|
||||
|
||||
prepackagedPluginsDir, found := fileutils.FindDir("prepackaged_plugins")
|
||||
if found {
|
||||
if err := filepath.Walk(prepackagedPluginsDir, func(walkPath string, info os.FileInfo, err error) error {
|
||||
if !strings.HasSuffix(walkPath, ".tar.gz") {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileReader, err := os.Open(walkPath)
|
||||
if err != nil {
|
||||
mlog.Error("Failed to open prepackaged plugin", mlog.Err(err), mlog.String("path", walkPath))
|
||||
return nil
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
mlog.Debug("Installing prepackaged plugin", mlog.String("path", walkPath))
|
||||
|
||||
_, appErr := a.installPluginLocally(fileReader, nil, installPluginLocallyOnlyIfNewOrUpgrade)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to unpack prepackaged plugin", mlog.Err(appErr), mlog.String("path", walkPath))
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
mlog.Error("Failed to complete unpacking prepackaged plugins", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
plugins := a.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
pluginsEnvironment = a.GetPluginsEnvironment()
|
||||
pluginsEnvironment.SetPrepackagedPlugins(plugins)
|
||||
|
||||
// Sync plugin active state when config changes. Also notify plugins.
|
||||
a.Srv.PluginsLock.Lock()
|
||||
@@ -319,7 +305,7 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := pluginsEnvironment.Available()
|
||||
availablePlugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -327,7 +313,7 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
id = strings.ToLower(id)
|
||||
|
||||
var manifest *model.Manifest
|
||||
for _, p := range plugins {
|
||||
for _, p := range availablePlugins {
|
||||
if p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
break
|
||||
@@ -361,7 +347,7 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := pluginsEnvironment.Available()
|
||||
availablePlugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -369,7 +355,7 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
id = strings.ToLower(id)
|
||||
|
||||
var manifest *model.Manifest
|
||||
for _, p := range plugins {
|
||||
for _, p := range availablePlugins {
|
||||
if p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
break
|
||||
@@ -423,94 +409,35 @@ func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetMarketplacePlugin returns plugin from marketplace-server
|
||||
func (a *App) GetMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.BaseMarketplacePlugin, *model.AppError) {
|
||||
marketplaceClient, err := marketplace.NewClient(
|
||||
*a.Config().PluginSettings.MarketplaceUrl,
|
||||
a.HTTPService,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
filter := &model.MarketplacePluginFilter{Filter: request.Id, ServerVersion: model.CurrentVersion}
|
||||
plugin, err := marketplaceClient.GetPlugin(filter, request.Version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// GetMarketplacePlugins returns a list of plugins from the marketplace-server,
|
||||
// and plugins that are installed locally.
|
||||
func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) {
|
||||
plugins := map[string]*model.MarketplacePlugin{}
|
||||
|
||||
if *a.Config().PluginSettings.EnableRemoteMarketplace && !filter.LocalOnly {
|
||||
p, appErr := a.getRemotePlugins(filter)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
plugins = p
|
||||
}
|
||||
|
||||
appErr := a.mergePrepackagedPlugins(plugins)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
appErr = a.mergeLocalPlugins(plugins)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
// Filter plugins.
|
||||
var result []*model.MarketplacePlugin
|
||||
pluginSet := map[string]bool{}
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
marketplaceClient, err := marketplace.NewClient(
|
||||
*a.Config().PluginSettings.MarketplaceUrl,
|
||||
a.HTTPService,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Fetch all plugins from marketplace.
|
||||
marketplacePlugins, err := marketplaceClient.GetPlugins(&model.MarketplacePluginFilter{
|
||||
PerPage: -1,
|
||||
ServerVersion: model.CurrentVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.marketplace_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, p := range marketplacePlugins {
|
||||
if p.Manifest == nil || !pluginMatchesFilter(p.Manifest, filter.Filter) {
|
||||
continue
|
||||
for _, p := range plugins {
|
||||
if pluginMatchesFilter(p.Manifest, filter.Filter) {
|
||||
result = append(result, p)
|
||||
}
|
||||
|
||||
marketplacePlugin := &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: p,
|
||||
}
|
||||
|
||||
var manifest *model.Manifest
|
||||
if manifest, err = pluginsEnvironment.GetManifest(p.Manifest.Id); err != nil && err != plugin.ErrNotFound {
|
||||
return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
} else if err == nil {
|
||||
// Plugin is installed.
|
||||
marketplacePlugin.InstalledVersion = manifest.Version
|
||||
}
|
||||
|
||||
pluginSet[p.Manifest.Id] = true
|
||||
result = append(result, marketplacePlugin)
|
||||
}
|
||||
|
||||
// Include all other installed plugins.
|
||||
plugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil || pluginSet[plugin.Manifest.Id] || !pluginMatchesFilter(plugin.Manifest, filter.Filter) {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
// Labels should not (yet) be localized as the labels sent by the Marketplace are not (yet) localizable.
|
||||
Labels: []model.MarketplaceLabel{{
|
||||
Name: "Local",
|
||||
Description: "This plugin is not listed in the marketplace but was installed manually",
|
||||
}},
|
||||
Manifest: plugin.Manifest,
|
||||
},
|
||||
InstalledVersion: plugin.Manifest.Version,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort result alphabetically.
|
||||
@@ -521,6 +448,166 @@ func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*m
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getPrepackagedPlugin returns a pre-packaged plugin.
|
||||
func (a *App) getPrepackagedPlugin(pluginId, version string) (*plugin.PrepackagedPlugin, *model.AppError) {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.config.app_error", nil, "plugin environment is nil", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
prepackagedPlugins := pluginsEnvironment.PrepackagedPlugins()
|
||||
for _, p := range prepackagedPlugins {
|
||||
if p.Manifest.Id == pluginId && p.Manifest.Version == version {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// getRemoteMarketplacePlugin returns plugin from marketplace-server.
|
||||
func (a *App) getRemoteMarketplacePlugin(pluginId, version string) (*model.BaseMarketplacePlugin, *model.AppError) {
|
||||
marketplaceClient, err := marketplace.NewClient(
|
||||
*a.Config().PluginSettings.MarketplaceUrl,
|
||||
a.HTTPService,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
filter := &model.MarketplacePluginFilter{Filter: pluginId, ServerVersion: model.CurrentVersion}
|
||||
plugin, err := marketplaceClient.GetPlugin(filter, version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
func (a *App) getRemotePlugins(filter *model.MarketplacePluginFilter) (map[string]*model.MarketplacePlugin, *model.AppError) {
|
||||
result := map[string]*model.MarketplacePlugin{}
|
||||
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("getRemotePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
marketplaceClient, err := marketplace.NewClient(
|
||||
*a.Config().PluginSettings.MarketplaceUrl,
|
||||
a.HTTPService,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("getRemotePlugins", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Fetch all plugins from marketplace.
|
||||
marketplacePlugins, err := marketplaceClient.GetPlugins(&model.MarketplacePluginFilter{
|
||||
PerPage: -1,
|
||||
ServerVersion: model.CurrentVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("getRemotePlugins", "app.plugin.marketplace_client.failed_to_fetch", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, p := range marketplacePlugins {
|
||||
if p.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result[p.Manifest.Id] = &model.MarketplacePlugin{BaseMarketplacePlugin: p}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// mergePrepackagedPlugins merges pre-packaged plugins to remote marketplace plugins list.
|
||||
func (a *App) mergePrepackagedPlugins(remoteMarketplacePlugins map[string]*model.MarketplacePlugin) *model.AppError {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, prepackaged := range pluginsEnvironment.PrepackagedPlugins() {
|
||||
if prepackaged.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
prepackagedMarketplace := &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
Manifest: prepackaged.Manifest,
|
||||
},
|
||||
}
|
||||
|
||||
// If not available in marketplace, add the prepackaged
|
||||
if remoteMarketplacePlugins[prepackaged.Manifest.Id] == nil {
|
||||
remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace
|
||||
continue
|
||||
}
|
||||
|
||||
// If available in the markteplace, only overwrite if newer.
|
||||
prepackagedVersion, err := semver.Parse(prepackaged.Manifest.Version)
|
||||
if err != nil {
|
||||
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
marketplacePlugin := remoteMarketplacePlugins[prepackaged.Manifest.Id]
|
||||
marketplaceVersion, err := semver.Parse(marketplacePlugin.Manifest.Version)
|
||||
if err != nil {
|
||||
return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if prepackagedVersion.GT(marketplaceVersion) {
|
||||
remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeLocalPlugins merges locally installed plugins to remote marketplace plugins list.
|
||||
func (a *App) mergeLocalPlugins(remoteMarketplacePlugins map[string]*model.MarketplacePlugin) *model.AppError {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
localPlugins, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, plugin := range localPlugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if remoteMarketplacePlugins[plugin.Manifest.Id] != nil {
|
||||
// Remote plugin is installed.
|
||||
remoteMarketplacePlugins[plugin.Manifest.Id].InstalledVersion = plugin.Manifest.Version
|
||||
continue
|
||||
}
|
||||
|
||||
var labels []model.MarketplaceLabel
|
||||
if *a.Config().PluginSettings.EnableRemoteMarketplace {
|
||||
// Labels should not (yet) be localized as the labels sent by the Marketplace are not (yet) localizable.
|
||||
labels = append(labels, model.MarketplaceLabel{
|
||||
Name: "Local",
|
||||
Description: "This plugin is not listed in the marketplace",
|
||||
})
|
||||
}
|
||||
|
||||
remoteMarketplacePlugins[plugin.Manifest.Id] = &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
Labels: labels,
|
||||
Manifest: plugin.Manifest,
|
||||
},
|
||||
InstalledVersion: plugin.Manifest.Version,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pluginMatchesFilter(manifest *model.Manifest, filter string) bool {
|
||||
filter = strings.TrimSpace(strings.ToLower(filter))
|
||||
|
||||
@@ -600,6 +687,10 @@ func (a *App) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.Ap
|
||||
return nil, model.NewAppError("getPluginsFromDir", "app.plugin.sync.list_filestore.app_error", nil, appErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return getPluginsFromFilePaths(fileStorePaths), nil
|
||||
}
|
||||
|
||||
func getPluginsFromFilePaths(fileStorePaths []string) map[string]*pluginSignaturePath {
|
||||
pluginSignaturePathMap := make(map[string]*pluginSignaturePath)
|
||||
for _, path := range fileStorePaths {
|
||||
if strings.HasSuffix(path, ".tar.gz") {
|
||||
@@ -623,5 +714,121 @@ func (a *App) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.Ap
|
||||
}
|
||||
}
|
||||
|
||||
return pluginSignaturePathMap, nil
|
||||
return pluginSignaturePathMap
|
||||
}
|
||||
|
||||
func (a *App) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin {
|
||||
prepackagedPluginsDir, found := fileutils.FindDir(pluginsDir)
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileStorePaths := []string{}
|
||||
err := filepath.Walk(prepackagedPluginsDir, func(walkPath string, info os.FileInfo, err error) error {
|
||||
fileStorePaths = append(fileStorePaths, walkPath)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
mlog.Error("Failed to walk prepackaged plugins", mlog.Err(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
pluginSignaturePathMap := getPluginsFromFilePaths(fileStorePaths)
|
||||
plugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap))
|
||||
for _, pluginPaths := range pluginSignaturePathMap {
|
||||
plugin, err := a.processPrepackagedPlugin(pluginPaths)
|
||||
if err != nil {
|
||||
mlog.Error("Failed to install prepackaged plugin", mlog.String("path", pluginPaths.path), mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
|
||||
plugins = append(plugins, plugin)
|
||||
}
|
||||
|
||||
return plugins
|
||||
}
|
||||
|
||||
// processPrepackagedPlugin will return the prepackaged plugin metadata and will also
|
||||
// install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true.
|
||||
func (a *App) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) {
|
||||
mlog.Debug("Processing prepackaged plugin", mlog.String("path", pluginPath.path))
|
||||
|
||||
fileReader, err := os.Open(pluginPath.path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to open prepackaged plugin %s", pluginPath.path)
|
||||
}
|
||||
tmpDir, err := ioutil.TempDir("", "plugintmp")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to create temp dir plugintmp")
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
plugin, pluginDir, err := getPrepackagedPlugin(pluginPath, fileReader, tmpDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to get prepackaged plugin %s", pluginPath.path)
|
||||
}
|
||||
|
||||
// Skip installing the plugin at all if automatic prepackaged plugins is disabled
|
||||
if !*a.Config().PluginSettings.AutomaticPrepackagedPlugins {
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// Skip installing if the plugin is has not been previously enabled.
|
||||
pluginState := a.Config().PluginSettings.PluginStates[plugin.Manifest.Id]
|
||||
if pluginState == nil || !pluginState.Enable {
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
mlog.Debug("Installing prepackaged plugin", mlog.String("path", pluginPath.path))
|
||||
if _, err := a.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil {
|
||||
return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.path)
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
}
|
||||
|
||||
// getPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted.
|
||||
func getPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) {
|
||||
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
|
||||
if appErr != nil {
|
||||
return nil, "", errors.Wrapf(appErr, "Failed to extract plugin with path %s", pluginPath.path)
|
||||
}
|
||||
|
||||
plugin := new(plugin.PrepackagedPlugin)
|
||||
plugin.Manifest = manifest
|
||||
plugin.Path = pluginPath.path
|
||||
|
||||
if pluginPath.signaturePath != "" {
|
||||
sig := pluginPath.signaturePath
|
||||
sigReader, sigErr := os.Open(sig)
|
||||
if sigErr != nil {
|
||||
return nil, "", errors.Wrapf(sigErr, "Failed to open prepackaged plugin signature %s", sig)
|
||||
}
|
||||
bytes, sigErr := ioutil.ReadAll(sigReader)
|
||||
if sigErr != nil {
|
||||
return nil, "", errors.Wrapf(sigErr, "Failed to read prepackaged plugin signature %s", sig)
|
||||
}
|
||||
plugin.Signature = bytes
|
||||
}
|
||||
|
||||
if manifest.IconPath != "" {
|
||||
iconData, err := getIcon(manifest.IconPath)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrapf(err, "Failed to read icon at %s", manifest.IconPath)
|
||||
}
|
||||
plugin.IconData = iconData
|
||||
}
|
||||
|
||||
return plugin, pluginDir, nil
|
||||
}
|
||||
|
||||
func getIcon(iconPath string) (string, error) {
|
||||
icon, err := ioutil.ReadFile(iconPath)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to open icon at path %s", iconPath)
|
||||
}
|
||||
if !svg.Is(icon) {
|
||||
return "", errors.Wrapf(err, "icon is not svg %s", iconPath)
|
||||
}
|
||||
return fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(icon)), nil
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -45,6 +46,8 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/plugin"
|
||||
@@ -169,6 +172,61 @@ func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStr
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// InstallMarketplacePlugin installs a plugin listed in the marketplace server. It will get the plugin bundle
|
||||
// from the prepackaged folder, if available, or remotely if EnableRemoteMarketplace is true.
|
||||
func (a *App) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) {
|
||||
var pluginFile, signatureFile io.ReadSeeker
|
||||
|
||||
prepackagedPlugin, appErr := a.getPrepackagedPlugin(request.Id, request.Version)
|
||||
if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" {
|
||||
return nil, appErr
|
||||
}
|
||||
if prepackagedPlugin != nil {
|
||||
fileReader, err := os.Open(prepackagedPlugin.Path)
|
||||
if err != nil {
|
||||
err = errors.Wrapf(err, "failed to open prepackaged plugin %s", prepackagedPlugin.Path)
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
pluginFile = fileReader
|
||||
signatureFile = bytes.NewReader(prepackagedPlugin.Signature)
|
||||
}
|
||||
|
||||
if *a.Config().PluginSettings.EnableRemoteMarketplace && pluginFile == nil {
|
||||
var plugin *model.BaseMarketplacePlugin
|
||||
plugin, appErr = a.getRemoteMarketplacePlugin(request.Id, request.Version)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
downloadedPluginBytes, err := a.DownloadFromURL(plugin.DownloadURL)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
signature, err := plugin.DecodeSignature()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, err.Error(), http.StatusNotImplemented)
|
||||
}
|
||||
pluginFile = bytes.NewReader(downloadedPluginBytes)
|
||||
signatureFile = signature
|
||||
}
|
||||
|
||||
if pluginFile == nil {
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
if signatureFile == nil {
|
||||
return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
manifest, appErr := a.InstallPluginWithSignature(pluginFile, signatureFile)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
type pluginInstallationStrategy int
|
||||
|
||||
const (
|
||||
@@ -185,6 +243,7 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
// verify signature
|
||||
if signature != nil {
|
||||
if err := a.VerifyPlugin(pluginFile, signature); err != nil {
|
||||
@@ -198,33 +257,55 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
pluginFile.Seek(0, 0)
|
||||
if err = utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
tmpPluginDir := tmpDir
|
||||
dir, err := ioutil.ReadDir(tmpDir)
|
||||
manifest, appErr = a.installExtractedPlugin(manifest, pluginDir, installationStrategy)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest, string, *model.AppError) {
|
||||
pluginFile.Seek(0, 0)
|
||||
if err := utils.ExtractTarGz(pluginFile, extractDir); err != nil {
|
||||
return nil, "", model.NewAppError("extractPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
dir, err := ioutil.ReadDir(extractDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, "", model.NewAppError("extractPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(dir) == 1 && dir[0].IsDir() {
|
||||
tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name())
|
||||
extractDir = filepath.Join(extractDir, dir[0].Name())
|
||||
}
|
||||
|
||||
manifest, _, err := model.FindManifest(tmpPluginDir)
|
||||
manifest, _, err := model.FindManifest(extractDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
return nil, "", model.NewAppError("extractPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !plugin.IsValidId(manifest.Id) {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidIdRegex}, "", http.StatusBadRequest)
|
||||
return nil, "", model.NewAppError("extractPlugin", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidIdRegex}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return manifest, extractDir, nil
|
||||
}
|
||||
|
||||
func (a *App) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
|
||||
pluginsEnvironment := a.GetPluginsEnvironment()
|
||||
if pluginsEnvironment == nil {
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
bundles, err := pluginsEnvironment.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Check for plugins installed with the same ID.
|
||||
@@ -239,7 +320,7 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa
|
||||
if existingManifest != nil {
|
||||
// Return an error if already installed and strategy disallows installation.
|
||||
if installationStrategy == installPluginLocallyOnlyIfNew {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Skip installation if already installed and newer.
|
||||
@@ -248,12 +329,12 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa
|
||||
|
||||
version, err = semver.Parse(manifest.Version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
existingVersion, err = semver.Parse(existingManifest.Version)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if version.LTE(existingVersion) {
|
||||
@@ -265,37 +346,37 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa
|
||||
// Otherwise remove the existing installation prior to install below.
|
||||
mlog.Debug("Removing existing installation of plugin before local install", mlog.String("plugin_id", existingManifest.Id), mlog.String("version", existingManifest.Version))
|
||||
if err := a.removePluginLocally(existingManifest.Id); err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(*a.Config().PluginSettings.Directory, manifest.Id)
|
||||
err = utils.CopyDir(tmpPluginDir, pluginPath)
|
||||
err = utils.CopyDir(fromPluginDir, pluginPath)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Flag plugin locally as managed by the filestore.
|
||||
f, err := os.Create(filepath.Join(pluginPath, managedPluginFileName))
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.flag_managed.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.flag_managed.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
if manifest.HasWebapp() {
|
||||
updatedManifest, err := pluginsEnvironment.UnpackWebappBundle(manifest.Id)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.webapp_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.webapp_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
manifest = updatedManifest
|
||||
}
|
||||
|
||||
// Activate plugin if it was previously activated.
|
||||
// Activate the plugin if enabled.
|
||||
pluginState := a.Config().PluginSettings.PluginStates[manifest.Id]
|
||||
if pluginState != nil && pluginState.Enable {
|
||||
updatedManifest, _, err := pluginsEnvironment.Activate(manifest.Id)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPluginLocally", "app.plugin.restart.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return nil, model.NewAppError("installExtractedPlugin", "app.plugin.restart.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
manifest = updatedManifest
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/plugin"
|
||||
"github.com/mattermost/mattermost-server/v5/utils"
|
||||
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
||||
)
|
||||
|
||||
@@ -485,108 +486,316 @@ func TestPluginSync(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.Description, func(t *testing.T) {
|
||||
os.MkdirAll("./test-plugins", os.ModePerm)
|
||||
defer os.RemoveAll("./test-plugins")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.Directory = "./test-plugins"
|
||||
*cfg.PluginSettings.ClientDirectory = "./test-client-plugins"
|
||||
*cfg.PluginSettings.RequirePluginSignature = false
|
||||
testCase.ConfigFunc(cfg)
|
||||
})
|
||||
th.App.UpdateConfig(testCase.ConfigFunc)
|
||||
|
||||
env, err := plugin.NewEnvironment(th.App.NewPluginAPI, "./test-plugins", "./test-client-plugins", th.App.Log)
|
||||
require.NoError(t, err)
|
||||
th.App.SetPluginsEnvironment(env)
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
require.NotNil(t, env)
|
||||
|
||||
// New bundle in the file store case
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
fileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
defer fileReader.Close()
|
||||
|
||||
_, appErr := th.App.WriteFile(fileReader, th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
t.Run("new bundle in the file store", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = false
|
||||
})
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
fileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
defer fileReader.Close()
|
||||
|
||||
// Check if installed
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
_, appErr := th.App.WriteFile(fileReader, th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
|
||||
// Bundle removed from the file store case
|
||||
appErr = th.App.RemoveFile(th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
// Check if removed
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, pluginStatus)
|
||||
|
||||
// RequirePluginSignature = true case
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = true
|
||||
// Check if installed
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
})
|
||||
pluginFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
defer pluginFileReader.Close()
|
||||
_, appErr = th.App.WriteFile(pluginFileReader, th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
// no signature
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, pluginStatus)
|
||||
|
||||
// Wrong signature
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, "testpluginv2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
filePath := fmt.Sprintf("%s.sig", th.App.getBundleStorePath("testplugin"))
|
||||
_, appErr = th.App.WriteFile(signatureFileReader, filePath)
|
||||
checkNoError(t, appErr)
|
||||
t.Run("bundle removed from the file store", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = false
|
||||
})
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
appErr := th.App.RemoveFile(th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, pluginStatus)
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
// Correct signature
|
||||
key, err := os.Open(filepath.Join(path, "development-private-key.asc"))
|
||||
require.NoError(t, err)
|
||||
appErr = th.App.AddPublicKey("pub_key", key)
|
||||
checkNoError(t, appErr)
|
||||
// Check if removed
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, pluginStatus)
|
||||
})
|
||||
|
||||
signatureFileReader, err = os.Open(filepath.Join(path, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
filePath = fmt.Sprintf("%s.sig", th.App.getBundleStorePath("testplugin"))
|
||||
_, appErr = th.App.WriteFile(signatureFileReader, filePath)
|
||||
checkNoError(t, appErr)
|
||||
t.Run("plugin signatures required, no signature", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = true
|
||||
})
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
pluginFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
defer pluginFileReader.Close()
|
||||
_, appErr := th.App.WriteFile(pluginFileReader, th.App.getBundleStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
appErr = th.App.DeletePublicKey("pub_key")
|
||||
checkNoError(t, appErr)
|
||||
t.Run("plugin signatures required, wrong signature", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = true
|
||||
})
|
||||
|
||||
appErr = th.App.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, "testplugin2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
_, appErr := th.App.WriteFile(signatureFileReader, th.App.getSignatureStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
t.Run("plugin signatures required, correct signature", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.RequirePluginSignature = true
|
||||
})
|
||||
|
||||
key, err := os.Open(filepath.Join(path, "development-private-key.asc"))
|
||||
require.NoError(t, err)
|
||||
appErr := th.App.AddPublicKey("pub_key", key)
|
||||
checkNoError(t, appErr)
|
||||
|
||||
signatureFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
defer signatureFileReader.Close()
|
||||
_, appErr = th.App.WriteFile(signatureFileReader, th.App.getSignatureStorePath("testplugin"))
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.SyncPlugins()
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.Nil(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
|
||||
appErr = th.App.DeletePublicKey("pub_key")
|
||||
checkNoError(t, appErr)
|
||||
|
||||
appErr = th.App.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPrepackagedPlugins(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
testsPath, _ := fileutils.FindDir("tests")
|
||||
prepackagedPluginsPath := filepath.Join(testsPath, prepackagedPluginsDir)
|
||||
fileErr := os.Mkdir(prepackagedPluginsPath, os.ModePerm)
|
||||
require.NoError(t, fileErr)
|
||||
defer os.RemoveAll(prepackagedPluginsPath)
|
||||
|
||||
prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsPath)
|
||||
require.True(t, found, "failed to find prepackaged plugins directory")
|
||||
|
||||
testPluginPath := filepath.Join(testsPath, "testplugin.tar.gz")
|
||||
fileErr = utils.CopyFile(testPluginPath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz"))
|
||||
require.NoError(t, fileErr)
|
||||
|
||||
t.Run("automatic, enabled plugin, no signature", func(t *testing.T) {
|
||||
// Install the plugin and enable
|
||||
pluginBytes, err := ioutil.ReadFile(testPluginPath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pluginBytes)
|
||||
|
||||
manifest, appErr := th.App.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "testplugin", manifest.Id)
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
activatedManifest, activated, err := env.Activate(manifest.Id)
|
||||
require.NoError(t, err)
|
||||
require.True(t, activated)
|
||||
require.Equal(t, manifest, activatedManifest)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, plugins[0].Manifest.Id, "testplugin")
|
||||
require.Empty(t, plugins[0].Signature, 0)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
|
||||
appErr = th.App.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
t.Run("automatic, not enabled plugin", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, plugins[0].Manifest.Id, "testplugin")
|
||||
require.Empty(t, plugins[0].Signature, 0)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
t.Run("automatic, multiple plugins with signatures, not enabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
// Add signature
|
||||
testPluginSignaturePath := filepath.Join(testsPath, "testplugin.tar.gz.sig")
|
||||
err := utils.CopyFile(testPluginSignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add second plugin
|
||||
testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz")
|
||||
err = utils.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig")
|
||||
err = utils.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 2)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[0].Signature)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[1].Signature)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
t.Run("automatic, multiple plugins with signatures, one enabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = true
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
// Add signature
|
||||
testPluginSignaturePath := filepath.Join(testsPath, "testplugin.tar.gz.sig")
|
||||
err := utils.CopyFile(testPluginSignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Install first plugin and enable
|
||||
pluginBytes, err := ioutil.ReadFile(testPluginPath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pluginBytes)
|
||||
|
||||
manifest, appErr := th.App.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways)
|
||||
require.Nil(t, appErr)
|
||||
require.Equal(t, "testplugin", manifest.Id)
|
||||
|
||||
activatedManifest, activated, err := env.Activate(manifest.Id)
|
||||
require.NoError(t, err)
|
||||
require.True(t, activated)
|
||||
require.Equal(t, manifest, activatedManifest)
|
||||
|
||||
// Add second plugin
|
||||
testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz")
|
||||
err = utils.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig")
|
||||
err = utils.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 2)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[0].Signature)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[1].Signature)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 1)
|
||||
require.Equal(t, pluginStatus[0].PluginId, "testplugin")
|
||||
|
||||
appErr = th.App.RemovePlugin("testplugin")
|
||||
checkNoError(t, appErr)
|
||||
|
||||
pluginStatus, err = env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
|
||||
t.Run("non-automatic, multiple plugins", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
|
||||
testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz")
|
||||
err := utils.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz"))
|
||||
require.NoError(t, err)
|
||||
|
||||
testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig")
|
||||
err = utils.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig"))
|
||||
require.NoError(t, err)
|
||||
|
||||
plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir)
|
||||
require.Len(t, plugins, 2)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[0].Signature)
|
||||
require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id)
|
||||
require.NotEmpty(t, plugins[1].Signature)
|
||||
|
||||
pluginStatus, err := env.Statuses()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, pluginStatus, 0)
|
||||
})
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user