From 982a583a7f043502edbbf099fd12898a3829657f Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 22 Apr 2020 22:49:18 -0300 Subject: [PATCH 01/11] s/store/staticStore/ --- cmd/lambda/main.go | 4 +- cmd/marketplace/server.go | 4 +- internal/api/plugins_test.go | 2 +- internal/store/plugin_test.go | 216 -------------- internal/store/{plugin.go => static.go} | 49 ++- internal/store/static_test.go | 382 ++++++++++++++++++++++++ internal/store/store.go | 48 --- internal/store/store_test.go | 56 ---- 8 files changed, 434 insertions(+), 327 deletions(-) delete mode 100644 internal/store/plugin_test.go rename internal/store/{plugin.go => static.go} (66%) create mode 100644 internal/store/static_test.go delete mode 100644 internal/store/store.go delete mode 100644 internal/store/store_test.go diff --git a/cmd/lambda/main.go b/cmd/lambda/main.go index 9c650de3..361808a3 100644 --- a/cmd/lambda/main.go +++ b/cmd/lambda/main.go @@ -22,7 +22,7 @@ func main() { } } -func newStatikStore(statikPath string, logger logrus.FieldLogger) (*store.Store, error) { +func newStatikStore(statikPath string, logger logrus.FieldLogger) (*store.StaticStore, error) { statikFS, err := fs.New() if err != nil { return nil, errors.Wrap(err, "failed to open statik fileystem") @@ -34,7 +34,7 @@ func newStatikStore(statikPath string, logger logrus.FieldLogger) (*store.Store, } defer database.Close() - statikStore, err := store.New(database, logger) + statikStore, err := store.NewStaticFromReader(database, logger) if err != nil { return nil, errors.Wrap(err, "failed to initialize store") } diff --git a/cmd/marketplace/server.go b/cmd/marketplace/server.go index fd8d6680..87ae3292 100644 --- a/cmd/marketplace/server.go +++ b/cmd/marketplace/server.go @@ -46,7 +46,7 @@ var serverCmd = &cobra.Command{ } defer databaseFile.Close() - fileStore, err := store.New(databaseFile, logger) + staticStore, err := store.NewStaticFromReader(databaseFile, logger) if err != nil { return errors.Wrap(err, "failed to initialize store") } @@ -57,7 +57,7 @@ var serverCmd = &cobra.Command{ router := mux.NewRouter() api.Register(router, &api.Context{ - Store: fileStore, + Store: staticStore, Logger: logger, }) diff --git a/internal/api/plugins_test.go b/internal/api/plugins_test.go index 0c3b2c48..d3432a4a 100644 --- a/internal/api/plugins_test.go +++ b/internal/api/plugins_test.go @@ -23,7 +23,7 @@ func setupAPI(t *testing.T, plugins []*model.Plugin) (*api.Client, func()) { data, err := json.Marshal(plugins) require.NoError(t, err) - store, err := store.New(bytes.NewReader(data), logger) + store, err := store.NewStaticFromReader(bytes.NewReader(data), logger) require.NoError(t, err) router := mux.NewRouter() diff --git a/internal/store/plugin_test.go b/internal/store/plugin_test.go deleted file mode 100644 index dd9614ea..00000000 --- a/internal/store/plugin_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package store - -import ( - "bytes" - "encoding/json" - "testing" - - mattermostModel "github.com/mattermost/mattermost-server/v5/model" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-marketplace/internal/model" - "github.com/mattermost/mattermost-marketplace/internal/testlib" -) - -func TestPlugins(t *testing.T) { - demoPluginV1Min514 := &model.Plugin{ - HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", - IconData: "icon-data.svg", - DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz", - Manifest: &mattermostModel.Manifest{ - Id: "com.mattermost.demo-plugin", - Name: "Demo Plugin", - Description: "This plugin demonstrates the capabilities of a Mattermost plugin.", - Version: "0.1.0", - MinServerVersion: "5.14.0", - }, - Signature: "signature1", - } - - demoPluginV2Min515 := &model.Plugin{ - HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", - IconData: "icon-data.svg", - DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.2.0/com.mattermost.demo-plugin-0.2.0.tar.gz", - Manifest: &mattermostModel.Manifest{ - Id: "com.mattermost.demo-plugin", - Name: "Demo Plugin", - Description: "This plugin demonstrates the capabilities of a Mattermost plugin.", - Version: "0.2.0", - MinServerVersion: "5.15.0", - }, - Signature: "signature1", - } - - starterPluginV1Min515 := &model.Plugin{ - HomepageURL: "https://github.com/mattermost/mattermost-plugin-starter-template", - IconData: "icon-data2.svg", - DownloadURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz", - Manifest: &mattermostModel.Manifest{ - Id: "com.mattermost.plugin-starter-template", - Name: "Plugin Starter Template", - Description: "This plugin serves as a starting point for writing a Mattermost plugin.", - Version: "0.1.0", - MinServerVersion: "5.15.0", - }, - Signature: "signature2", - } - - data, err := json.Marshal([]*model.Plugin{ - demoPluginV1Min514, - demoPluginV2Min515, - starterPluginV1Min515, - }) - require.NoError(t, err) - - logger := testlib.MakeLogger(t) - sqlStore, err := New(bytes.NewReader(data), logger) - require.NoError(t, err) - - t.Run("page 0, per page 0", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{ - Page: 0, - PerPage: 0, - Filter: "", - }) - require.NoError(t, err) - require.Empty(t, actualPlugins) - }) - - t.Run("page 0, per page 1", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{ - Page: 0, - PerPage: 1, - Filter: "", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515}, actualPlugins) - }) - - t.Run("page 0, per page 10", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{ - Page: 0, - PerPage: 10, - Filter: "", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) - }) - - t.Run("page 0, per page 1", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{ - Page: 0, - PerPage: 1, - Filter: "", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515}, actualPlugins) - }) - - t.Run("page 0, per page 10", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{ - Page: 0, - PerPage: 10, - Filter: "", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) - }) - - t.Run("default paging", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: "", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) - }) - - t.Run("filter spaces", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: " ", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) - }) - - t.Run("id match, exact", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: "com.mattermost.demo-plugin", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515}, actualPlugins) - }) - - t.Run("id match, case-insensitive", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: "com.mattermost.demo-PLUGIN", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515}, actualPlugins) - }) - - t.Run("name match, exact", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: "Plugin Starter Template", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{starterPluginV1Min515}, actualPlugins) - }) - - t.Run("name match, partial", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: "Starter", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{starterPluginV1Min515}, actualPlugins) - }) - - t.Run("name match, case-insensitive", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: "TEMPLATE", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{starterPluginV1Min515}, actualPlugins) - }) - - t.Run("description match, partial", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: "capabilities", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515}, actualPlugins) - }) - - t.Run("description match, case-insensitive, multiple matches", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: "MATTERMOST", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) - }) - - t.Run("plugins that satisfy 5.15", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: "MATTERMOST", - ServerVersion: "5.15.0", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) - }) - - t.Run("plugins that satisfy 5.14", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - Filter: "MATTERMOST", - ServerVersion: "5.14.0", - }) - require.NoError(t, err) - require.Equal(t, []*model.Plugin{demoPluginV1Min514}, actualPlugins) - }) - - t.Run("with a server version that does not satisfy any plugin", func(t *testing.T) { - actualPlugins, err := sqlStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, - ServerVersion: "5.13.0", - }) - require.NoError(t, err) - require.Nil(t, actualPlugins) - }) -} diff --git a/internal/store/plugin.go b/internal/store/static.go similarity index 66% rename from internal/store/plugin.go rename to internal/store/static.go index cfd634b3..99981593 100644 --- a/internal/store/plugin.go +++ b/internal/store/static.go @@ -1,15 +1,60 @@ package store import ( + "io" "sort" "strings" "github.com/blang/semver" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/mattermost/mattermost-marketplace/internal/model" ) +// StaticStore provides access to a store backed by a static set of plugins. +type StaticStore struct { + plugins []*model.Plugin + logger logrus.FieldLogger +} + +// NewStatic constructs a new instance of a static store, parsing the plugins from the given reader. +func NewStaticFromReader(reader io.Reader, logger logrus.FieldLogger) (*StaticStore, error) { + plugins, err := model.PluginsFromReader(reader) + if err != nil { + return nil, errors.Wrap(err, "failed to parse stream") + } + + return NewStatic(plugins, logger) +} + +// NewStatic constructs a new instance of a static store using the given plugins. +func NewStatic(plugins []*model.Plugin, logger logrus.FieldLogger) (*StaticStore, error) { + if err := validatePlugins(plugins); err != nil { + return nil, errors.Wrap(err, "failed to validate plugins") + } + + return &StaticStore{ + plugins, + logger, + }, nil +} + +func validatePlugins(plugins []*model.Plugin) error { + for _, plugin := range plugins { + err := plugin.Manifest.IsValid() + if err != nil { + return errors.Wrapf(err, "invalid manifest for plugin %s", plugin.Manifest.Id) + } + + if plugin.Manifest.Version == "" { + return errors.Errorf("missing version in manifest for plugin%s", plugin.Manifest.Id) + } + } + + return nil +} + func pluginMatchesFilter(plugin *model.Plugin, filter string) bool { filter = strings.ToLower(filter) if strings.ToLower(plugin.Manifest.Id) == filter { @@ -28,7 +73,7 @@ func pluginMatchesFilter(plugin *model.Plugin, filter string) bool { } // GetPlugins fetches the given page of plugins. The first page is 0. -func (store *Store) GetPlugins(pluginFilter *model.PluginFilter) ([]*model.Plugin, error) { +func (store *StaticStore) GetPlugins(pluginFilter *model.PluginFilter) ([]*model.Plugin, error) { if pluginFilter.PerPage == 0 { return nil, nil } @@ -69,7 +114,7 @@ func (store *Store) GetPlugins(pluginFilter *model.PluginFilter) ([]*model.Plugi } // getPlugins returns all plugins compatible with the given server version, sorted by name ascending. -func (store *Store) getPlugins(serverVersion string) ([]*model.Plugin, error) { +func (store *StaticStore) getPlugins(serverVersion string) ([]*model.Plugin, error) { var result []*model.Plugin plugins := map[string]*model.Plugin{} diff --git a/internal/store/static_test.go b/internal/store/static_test.go new file mode 100644 index 00000000..1c67789e --- /dev/null +++ b/internal/store/static_test.go @@ -0,0 +1,382 @@ +package store + +import ( + "bytes" + "encoding/json" + "testing" + + mattermostModel "github.com/mattermost/mattermost-server/v5/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-marketplace/internal/model" + "github.com/mattermost/mattermost-marketplace/internal/testlib" +) + +func TestNewStatic(t *testing.T) { + t.Run("empty stream", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStatic([]*model.Plugin{}, logger) + assert.NoError(t, err) + require.NotNil(t, store) + assert.Empty(t, store.plugins) + }) + + t.Run("missing manifest id", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStatic([]*model.Plugin{ + { + HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", + IconData: "icon-data.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz", + Signature: "c2lnbmF0dXJl", + ReleaseNotesURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0", + Manifest: &mattermostModel.Manifest{}, + }, + { + HomepageURL: "https://github.com/mattermost/mattermost-plugin-starter-template", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz", + Signature: "signature2", + ReleaseNotesURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0", + Manifest: &mattermostModel.Manifest{}, + }, + }, logger) + assert.Error(t, err) + assert.Nil(t, store) + }) + + t.Run("missing manifest version", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStatic([]*model.Plugin{ + { + HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", + IconData: "icon-data.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz", + Signature: "c2lnbmF0dXJl", + ReleaseNotesURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0", + Manifest: &mattermostModel.Manifest{ + Id: "test", + }, + }, + { + HomepageURL: "https://github.com/mattermost/mattermost-plugin-starter-template", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz", + Signature: "signature2", + ReleaseNotesURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0", + Manifest: &mattermostModel.Manifest{ + Id: "test", + }, + }, + }, logger) + assert.Error(t, err) + assert.Nil(t, store) + }) + + t.Run("missing min_server_version version is valid", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStatic([]*model.Plugin{ + { + HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", + IconData: "icon-data.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz", + Signature: "c2lnbmF0dXJl", + ReleaseNotesURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0", + Manifest: &mattermostModel.Manifest{ + Id: "test", + Version: "0.1.0", + }, + }, + { + HomepageURL: "https://github.com/mattermost/mattermost-plugin-starter-template", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz", + Signature: "signature2", + ReleaseNotesURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0", + Manifest: &mattermostModel.Manifest{ + Id: "test", + Version: "0.1.0", + }, + }, + }, logger) + assert.NoError(t, err) + assert.NotNil(t, store) + }) + + t.Run("valid stream", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStatic([]*model.Plugin{ + { + HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", + IconData: "icon-data.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz", + Signature: "c2lnbmF0dXJl", + ReleaseNotesURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0", + Manifest: &mattermostModel.Manifest{ + Id: "test", + Version: "0.1.0", + MinServerVersion: "5.23.0", + }, + }, + { + HomepageURL: "https://github.com/mattermost/mattermost-plugin-starter-template", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz", + Signature: "signature2", + ReleaseNotesURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0", + Manifest: &mattermostModel.Manifest{ + Id: "test", + Version: "0.1.0", + MinServerVersion: "5.23.0", + }, + }, + }, logger) + assert.NoError(t, err) + assert.NotNil(t, store) + }) +} + +func TestNewStaticFromReader(t *testing.T) { + t.Run("empty stream", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStaticFromReader(bytes.NewReader([]byte{}), logger) + assert.NoError(t, err) + require.NotNil(t, store) + assert.Empty(t, store.plugins) + }) + + t.Run("invalid stream", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStaticFromReader(bytes.NewReader([]byte(`{"invalid":`)), logger) + assert.EqualError(t, err, "failed to parse stream: unexpected EOF") + assert.Nil(t, store) + }) + + t.Run("missing manifest id", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStaticFromReader(bytes.NewReader([]byte(`[{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-demo","IconData":"icon-data.svg","DownloadURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","Signature":"c2lnbmF0dXJl","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","Manifest":{}},{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-starter-template","DownloadURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz","Signature":"signature2","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0","Manifest":{}}]`)), logger) + assert.Error(t, err) + assert.Nil(t, store) + }) + + t.Run("missing manifest version", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStaticFromReader(bytes.NewReader([]byte(`[{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-demo","IconData":"icon-data.svg","DownloadURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","Signature":"c2lnbmF0dXJl","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","Manifest":{"id": "test"}},{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-starter-template","DownloadURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz","Signature":"signature2"],"ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0","Manifest":{"id": "test"}}]`)), logger) + assert.Error(t, err) + assert.Nil(t, store) + }) + + t.Run("missing min_server_version version is valid", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStaticFromReader(bytes.NewReader([]byte(`[{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-demo","IconData":"icon-data.svg","DownloadURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","Signature":"c2lnbmF0dXJl","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","Manifest":{"id": "test", "version": "0.1.0"}},{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-starter-template","DownloadURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz","Signature":"signature2","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0","Manifest":{"id": "test", "version": "0.1.0"}}]`)), logger) + assert.NoError(t, err) + assert.NotNil(t, store) + }) + + t.Run("valid stream", func(t *testing.T) { + logger := testlib.MakeLogger(t) + store, err := NewStaticFromReader(bytes.NewReader([]byte(`[{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-demo","IconData":"icon-data.svg","DownloadURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","Signature":"c2lnbmF0dXJl","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","Manifest":{"id": "test", "version": "0.1.0", "min_server_version":"5.23.0"}},{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-starter-template","DownloadURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz","Signature":"signature2","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0","Manifest":{"id": "test", "version": "0.1.0", "min_server_version":"5.23.0"}}]`)), logger) + assert.NoError(t, err) + assert.NotNil(t, store) + }) +} + +func TestStaticGetPlugins(t *testing.T) { + demoPluginV1Min514 := &model.Plugin{ + HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", + IconData: "icon-data.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "com.mattermost.demo-plugin", + Name: "Demo Plugin", + Description: "This plugin demonstrates the capabilities of a Mattermost plugin.", + Version: "0.1.0", + MinServerVersion: "5.14.0", + }, + Signature: "signature1", + } + + demoPluginV2Min515 := &model.Plugin{ + HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", + IconData: "icon-data.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.2.0/com.mattermost.demo-plugin-0.2.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "com.mattermost.demo-plugin", + Name: "Demo Plugin", + Description: "This plugin demonstrates the capabilities of a Mattermost plugin.", + Version: "0.2.0", + MinServerVersion: "5.15.0", + }, + Signature: "signature1", + } + + starterPluginV1Min515 := &model.Plugin{ + HomepageURL: "https://github.com/mattermost/mattermost-plugin-starter-template", + IconData: "icon-data2.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "com.mattermost.plugin-starter-template", + Name: "Plugin Starter Template", + Description: "This plugin serves as a starting point for writing a Mattermost plugin.", + Version: "0.1.0", + MinServerVersion: "5.15.0", + }, + Signature: "signature2", + } + + data, err := json.Marshal([]*model.Plugin{ + demoPluginV1Min514, + demoPluginV2Min515, + starterPluginV1Min515, + }) + require.NoError(t, err) + + logger := testlib.MakeLogger(t) + staticStore, err := NewStaticFromReader(bytes.NewReader(data), logger) + require.NoError(t, err) + + t.Run("page 0, per page 0", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: 0, + Filter: "", + }) + require.NoError(t, err) + require.Empty(t, actualPlugins) + }) + + t.Run("page 0, per page 1", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: 1, + Filter: "", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515}, actualPlugins) + }) + + t.Run("page 0, per page 10", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: 10, + Filter: "", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) + }) + + t.Run("page 0, per page 1", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: 1, + Filter: "", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515}, actualPlugins) + }) + + t.Run("page 0, per page 10", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: 10, + Filter: "", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) + }) + + t.Run("default paging", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: "", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) + }) + + t.Run("filter spaces", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: " ", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) + }) + + t.Run("id match, exact", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: "com.mattermost.demo-plugin", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515}, actualPlugins) + }) + + t.Run("id match, case-insensitive", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: "com.mattermost.demo-PLUGIN", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515}, actualPlugins) + }) + + t.Run("name match, exact", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: "Plugin Starter Template", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{starterPluginV1Min515}, actualPlugins) + }) + + t.Run("name match, partial", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: "Starter", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{starterPluginV1Min515}, actualPlugins) + }) + + t.Run("name match, case-insensitive", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: "TEMPLATE", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{starterPluginV1Min515}, actualPlugins) + }) + + t.Run("description match, partial", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: "capabilities", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515}, actualPlugins) + }) + + t.Run("description match, case-insensitive, multiple matches", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: "MATTERMOST", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) + }) + + t.Run("plugins that satisfy 5.15", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: "MATTERMOST", + ServerVersion: "5.15.0", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV2Min515, starterPluginV1Min515}, actualPlugins) + }) + + t.Run("plugins that satisfy 5.14", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + Filter: "MATTERMOST", + ServerVersion: "5.14.0", + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{demoPluginV1Min514}, actualPlugins) + }) + + t.Run("with a server version that does not satisfy any plugin", func(t *testing.T) { + actualPlugins, err := staticStore.GetPlugins(&model.PluginFilter{PerPage: model.AllPerPage, + ServerVersion: "5.13.0", + }) + require.NoError(t, err) + require.Nil(t, actualPlugins) + }) +} diff --git a/internal/store/store.go b/internal/store/store.go deleted file mode 100644 index 579b63b9..00000000 --- a/internal/store/store.go +++ /dev/null @@ -1,48 +0,0 @@ -package store - -import ( - "io" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - - "github.com/mattermost/mattermost-marketplace/internal/model" -) - -// Store provides access to a store backed by the given reader. -type Store struct { - plugins []*model.Plugin - logger logrus.FieldLogger -} - -// New constructs a new instance of Store. -func New(reader io.Reader, logger logrus.FieldLogger) (*Store, error) { - plugins, err := model.PluginsFromReader(reader) - if err != nil { - return nil, errors.Wrap(err, "failed to parse stream") - } - - if err := validatePlugins(plugins); err != nil { - return nil, errors.Wrap(err, "failed to validate plugins") - } - - return &Store{ - plugins, - logger, - }, nil -} - -func validatePlugins(plugins []*model.Plugin) error { - for _, plugin := range plugins { - err := plugin.Manifest.IsValid() - if err != nil { - return errors.Wrapf(err, "invalid manifest for plugin %s", plugin.Manifest.Id) - } - - if plugin.Manifest.Version == "" { - return errors.Errorf("missing version in manifest for plugin%s", plugin.Manifest.Id) - } - } - - return nil -} diff --git a/internal/store/store_test.go b/internal/store/store_test.go deleted file mode 100644 index 9f159055..00000000 --- a/internal/store/store_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package store - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-marketplace/internal/testlib" -) - -func TestNew(t *testing.T) { - t.Run("empty stream", func(t *testing.T) { - logger := testlib.MakeLogger(t) - store, err := New(bytes.NewReader([]byte{}), logger) - assert.NoError(t, err) - require.NotNil(t, store) - assert.Empty(t, store.plugins) - }) - - t.Run("invalid stream", func(t *testing.T) { - logger := testlib.MakeLogger(t) - store, err := New(bytes.NewReader([]byte(`{"invalid":`)), logger) - assert.EqualError(t, err, "failed to parse stream: unexpected EOF") - assert.Nil(t, store) - }) - - t.Run("missing manifest id", func(t *testing.T) { - logger := testlib.MakeLogger(t) - store, err := New(bytes.NewReader([]byte(`[{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-demo","IconData":"icon-data.svg","DownloadURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","DownloadSignature":"c2lnbmF0dXJl","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","Manifest":{}},{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-starter-template","DownloadURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz","Signatures":[{"signature":"signature2","public_key_hash":"hash2"}],"ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0","Manifest":{}}]`)), logger) - assert.Error(t, err) - assert.Nil(t, store) - }) - - t.Run("missing manifest version", func(t *testing.T) { - logger := testlib.MakeLogger(t) - store, err := New(bytes.NewReader([]byte(`[{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-demo","IconData":"icon-data.svg","DownloadURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","DownloadSignature":"c2lnbmF0dXJl","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","Manifest":{"id": "test"}},{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-starter-template","DownloadURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz","Signatures":[{"signature":"signature2","public_key_hash":"hash2"}],"ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0","Manifest":{"id": "test"}}]`)), logger) - assert.Error(t, err) - assert.Nil(t, store) - }) - - t.Run("missing min_server_version version is valid", func(t *testing.T) { - logger := testlib.MakeLogger(t) - store, err := New(bytes.NewReader([]byte(`[{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-demo","IconData":"icon-data.svg","DownloadURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","DownloadSignature":"c2lnbmF0dXJl","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","Manifest":{"id": "test", "version": "0.1.0"}},{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-starter-template","DownloadURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz","Signatures":[{"signature":"signature2","public_key_hash":"hash2"}],"ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0","Manifest":{"id": "test", "version": "0.1.0"}}]`)), logger) - assert.NoError(t, err) - assert.NotNil(t, store) - }) - - t.Run("valid stream", func(t *testing.T) { - logger := testlib.MakeLogger(t) - store, err := New(bytes.NewReader([]byte(`[{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-demo","IconData":"icon-data.svg","DownloadURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","DownloadSignature":"c2lnbmF0dXJl","ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","Manifest":{"id": "test", "version": "0.1.0"}},{"HomepageURL":"https://github.com/mattermost/mattermost-plugin-starter-template","DownloadURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz","Signatures":[{"signature":"signature2","public_key_hash":"hash2"}],"ReleaseNotesURL":"https://github.com/mattermost/mattermost-plugin-starter-template/releases/v0.1.0","Manifest":{"id": "test", "version": "0.1.0"}}]`)), logger) - assert.NoError(t, err) - assert.NotNil(t, store) - }) -} From 281639d4e9d4680280454c7770f467a0d67eb406 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 22 Apr 2020 22:51:22 -0300 Subject: [PATCH 02/11] prefer later plugins if version exactly matches --- internal/store/static.go | 5 ++++- internal/store/static_test.go | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/internal/store/static.go b/internal/store/static.go index 99981593..e59d24cf 100644 --- a/internal/store/static.go +++ b/internal/store/static.go @@ -141,7 +141,10 @@ func (store *StaticStore) getPlugins(serverVersion string) ([]*model.Plugin, err } storePluginVersion := semver.MustParse(storePlugin.Manifest.Version) - if storePluginVersion.GT(lastSeenPluginVersion) { + + // Replace the existing plugin if this version is newer, or if it's the same but + // appears later in the list. + if storePluginVersion.GTE(lastSeenPluginVersion) { plugins[storePlugin.Manifest.Id] = storePlugin } } diff --git a/internal/store/static_test.go b/internal/store/static_test.go index 1c67789e..6737eb89 100644 --- a/internal/store/static_test.go +++ b/internal/store/static_test.go @@ -207,6 +207,21 @@ func TestStaticGetPlugins(t *testing.T) { Signature: "signature1", } + // earlier will never appear, since later instance with same version overrides + starterPluginV1Min515Earlier := &model.Plugin{ + HomepageURL: "https://github.com/mattermost/mattermost-plugin-starter-template-earlier", + IconData: "icon-data2-earlier.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-starter-template-earlier/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "com.mattermost.plugin-starter-template", + Name: "Plugin Starter Template (Earlier)", + Description: "This plugin serves as a starting point for writing a Mattermost plugin.", + Version: "0.1.0", + MinServerVersion: "5.15.0", + }, + Signature: "signature2-earlier", + } + starterPluginV1Min515 := &model.Plugin{ HomepageURL: "https://github.com/mattermost/mattermost-plugin-starter-template", IconData: "icon-data2.svg", @@ -224,6 +239,7 @@ func TestStaticGetPlugins(t *testing.T) { data, err := json.Marshal([]*model.Plugin{ demoPluginV1Min514, demoPluginV2Min515, + starterPluginV1Min515Earlier, starterPluginV1Min515, }) require.NoError(t, err) From b9ce19a99e5e70345fffdfd64a2a3102503f8996 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 22 Apr 2020 22:48:17 -0300 Subject: [PATCH 03/11] introduce merged and proxy stores --- internal/store/merged.go | 58 ++++++++ internal/store/merged_test.go | 268 ++++++++++++++++++++++++++++++++++ internal/store/proxy.go | 40 +++++ internal/store/proxy_test.go | 76 ++++++++++ 4 files changed, 442 insertions(+) create mode 100644 internal/store/merged.go create mode 100644 internal/store/merged_test.go create mode 100644 internal/store/proxy.go create mode 100644 internal/store/proxy_test.go diff --git a/internal/store/merged.go b/internal/store/merged.go new file mode 100644 index 00000000..514b2d26 --- /dev/null +++ b/internal/store/merged.go @@ -0,0 +1,58 @@ +package store + +import ( + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/mattermost/mattermost-marketplace/internal/model" +) + +// Store describes the interface to the backing store. +type Store interface { + GetPlugins(filter *model.PluginFilter) ([]*model.Plugin, error) +} + +// Merged is a store that merges the results of multiple stores together, always preferring the +// most recently updated plugin record when a conflict occurs. +type Merged struct { + stores []Store + logger logrus.FieldLogger +} + +// NewMerged creates a new instance of the merged store. +func NewMerged(logger logrus.FieldLogger, stores ...Store) *Merged { + return &Merged{ + stores: stores, + logger: logger, + } +} + +// GetPlugins fetches the given page of plugins. The first page is 0. +func (store *Merged) GetPlugins(pluginFilter *model.PluginFilter) ([]*model.Plugin, error) { + // Short-circuit if only one store is configured. + if len(store.stores) == 1 { + return store.stores[0].GetPlugins(pluginFilter) + } + + plugins := []*model.Plugin{} + for i, store := range store.stores { + storePlugins, err := store.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: model.AllPerPage, + Filter: pluginFilter.Filter, + ServerVersion: pluginFilter.ServerVersion, + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to query store %d", i) + } + + plugins = append(plugins, storePlugins...) + } + + staticStore, err := NewStatic(plugins, store.logger) + if err != nil { + return nil, errors.Wrap(err, "failed to initialize static store") + } + + return staticStore.GetPlugins(pluginFilter) +} diff --git a/internal/store/merged_test.go b/internal/store/merged_test.go new file mode 100644 index 00000000..4a798b4a --- /dev/null +++ b/internal/store/merged_test.go @@ -0,0 +1,268 @@ +package store + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-marketplace/internal/model" + "github.com/mattermost/mattermost-marketplace/internal/testlib" + mattermostModel "github.com/mattermost/mattermost-server/v5/model" +) + +func TestMerged(t *testing.T) { + plugin1V1 := &model.Plugin{ + HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", + IconData: "icon-data.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "mattermost-plugin-demo", + Name: "mattermost-plugin-demo", + Version: "0.1.0", + }, + Signature: "signature1", + } + plugin1V2 := &model.Plugin{ + HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", + IconData: "icon-data.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.2.0/com.mattermost.demo-plugin-0.2.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "mattermost-plugin-demo", + Name: "mattermost-plugin-demo", + Version: "0.2.0", + }, + Signature: "signature1", + } + plugin1V3 := &model.Plugin{ + HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", + IconData: "icon-data.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.3.0/com.mattermost.demo-plugin-0.3.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "mattermost-plugin-demo", + Name: "mattermost-plugin-demo", + Version: "0.3.0", + }, + Signature: "signature1", + } + plugin2V1 := &model.Plugin{ + HomepageURL: "https://github.com/mattermost/mattermost-plugin-starter-template", + IconData: "icon-data2.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-starter-template/releases/download/v0.1.0/com.mattermost.plugin-starter-template-0.1.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "mattermost-plugin-starter-template", + Name: "mattermost-plugin-starter-template", + Version: "0.1.0", + }, + Signature: "signature2", + } + plugin3V1 := &model.Plugin{ + HomepageURL: "https://github.com/matterpoll/matterpoll", + IconData: "icon-data3.svg", + DownloadURL: "https://github.com/matterpoll/matterpoll/releases/download/v1.1.0/com.github.matterpoll.matterpoll-1.1.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "matterpoll", + Name: "matterpoll", + Version: "1.1.0", + }, + Signature: "signature3", + } + + plugin3V2 := &model.Plugin{ + HomepageURL: "https://github.com/matterpoll/matterpoll", + IconData: "icon-data3.svg", + DownloadURL: "https://github.com/matterpoll/matterpoll/releases/download/v1.2.0/com.github.matterpoll.matterpoll-1.2.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "matterpoll", + Name: "matterpoll", + Version: "1.2.0", + }, + Signature: "signature3", + } + + plugin3V3 := &model.Plugin{ + HomepageURL: "https://github.com/matterpoll/matterpoll", + IconData: "icon-data3.svg", + DownloadURL: "https://github.com/matterpoll/matterpoll/releases/download/v1.3.0/com.github.matterpoll.matterpoll-1.3.0.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "matterpoll", + Name: "matterpoll", + Version: "1.3.0", + }, + Signature: "signature3", + } + + plugin4V1 := &model.Plugin{ + HomepageURL: "fake_plugin", + IconData: "icon-data3.svg", + DownloadURL: "fake_plugin.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "fake_plugin", + Name: "Zfake_plugin", + Version: "1.2.4", + }, + Signature: "signature3", + } + + plugin4V1Later := &model.Plugin{ + HomepageURL: "fake_plugin", + IconData: "icon-data3-later.svg", + DownloadURL: "fake_plugin.tar.gz", + Manifest: &mattermostModel.Manifest{ + Id: "fake_plugin", + Name: "Zfake_plugin", + Version: "1.2.4", + }, + Signature: "signature3", + } + + t.Run("no stores", func(t *testing.T) { + logger := testlib.MakeLogger(t) + + store := NewMerged(logger) + require.NotNil(t, store) + + plugins, err := store.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: model.AllPerPage, + }) + require.NoError(t, err) + assert.Empty(t, plugins) + }) + + t.Run("single empty store", func(t *testing.T) { + logger := testlib.MakeLogger(t) + + static1, err := NewStatic([]*model.Plugin{}, logger) + require.NoError(t, err) + + store := NewMerged(logger, static1) + assert.NoError(t, err) + require.NotNil(t, store) + + plugins, err := store.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: model.AllPerPage, + }) + require.NoError(t, err) + assert.Empty(t, plugins) + }) + + t.Run("multiple empty stores", func(t *testing.T) { + logger := testlib.MakeLogger(t) + + static1, err := NewStatic([]*model.Plugin{}, logger) + require.NoError(t, err) + static2, err := NewStatic([]*model.Plugin{}, logger) + require.NoError(t, err) + + store := NewMerged(logger, static1, static2) + assert.NoError(t, err) + require.NotNil(t, store) + + plugins, err := store.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: model.AllPerPage, + }) + require.NoError(t, err) + assert.Empty(t, plugins) + }) + + t.Run("single, populated store", func(t *testing.T) { + logger := testlib.MakeLogger(t) + + static1, err := NewStatic([]*model.Plugin{plugin1V3, plugin2V1, plugin3V3, plugin4V1}, logger) + require.NoError(t, err) + + store := NewMerged(logger, static1) + assert.NoError(t, err) + require.NotNil(t, store) + + plugins, err := store.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: model.AllPerPage, + }) + require.NoError(t, err) + assert.Equal(t, []*model.Plugin{ + plugin1V3, + plugin2V1, + plugin3V3, + plugin4V1, + }, plugins) + }) + + t.Run("conflict-free merge", func(t *testing.T) { + logger := testlib.MakeLogger(t) + + static1, err := NewStatic([]*model.Plugin{plugin1V1, plugin1V2, plugin1V3}, logger) + require.NoError(t, err) + static2, err := NewStatic([]*model.Plugin{plugin2V1, plugin3V1, plugin3V2, plugin3V3, plugin4V1}, logger) + require.NoError(t, err) + + store := NewMerged(logger, static1, static2) + assert.NoError(t, err) + require.NotNil(t, store) + + plugins, err := store.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: model.AllPerPage, + }) + require.NoError(t, err) + assert.Equal(t, []*model.Plugin{ + plugin1V3, + plugin2V1, + plugin3V3, + plugin4V1, + }, plugins) + }) + + t.Run("newer versions win across stores", func(t *testing.T) { + logger := testlib.MakeLogger(t) + + static1, err := NewStatic([]*model.Plugin{plugin1V1, plugin2V1, plugin3V1, plugin4V1}, logger) + require.NoError(t, err) + static2, err := NewStatic([]*model.Plugin{plugin1V3, plugin2V1, plugin3V3, plugin4V1}, logger) + require.NoError(t, err) + + store := NewMerged(logger, static1, static2) + assert.NoError(t, err) + require.NotNil(t, store) + + plugins, err := store.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: model.AllPerPage, + }) + require.NoError(t, err) + assert.Equal(t, []*model.Plugin{ + plugin1V3, + plugin2V1, + plugin3V3, + plugin4V1, + }, plugins) + }) + + t.Run("later stores win across versions", func(t *testing.T) { + logger := testlib.MakeLogger(t) + + static1, err := NewStatic([]*model.Plugin{plugin4V1}, logger) + require.NoError(t, err) + static2, err := NewStatic([]*model.Plugin{plugin4V1Later}, logger) + require.NoError(t, err) + static3, err := NewStatic([]*model.Plugin{plugin1V3}, logger) + require.NoError(t, err) + + store := NewMerged(logger, static1, static2, static3) + assert.NoError(t, err) + require.NotNil(t, store) + + plugins, err := store.GetPlugins(&model.PluginFilter{ + Page: 0, + PerPage: model.AllPerPage, + }) + require.NoError(t, err) + assert.Equal(t, []*model.Plugin{ + plugin1V3, + plugin4V1Later, + }, plugins) + }) +} diff --git a/internal/store/proxy.go b/internal/store/proxy.go new file mode 100644 index 00000000..29e48497 --- /dev/null +++ b/internal/store/proxy.go @@ -0,0 +1,40 @@ +package store + +import ( + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/mattermost/mattermost-marketplace/internal/api" + "github.com/mattermost/mattermost-marketplace/internal/model" +) + +// Proxy is a store that fetches its result from some remote marketplace server. +type Proxy struct { + marketplaceURL string + logger logrus.FieldLogger +} + +// NewProxy creates a new instance of a proxy store. +func NewProxy(marketplaceURL string, logger logrus.FieldLogger) (*Proxy, error) { + return &Proxy{ + marketplaceURL: marketplaceURL, + logger: logger.WithField("marketplace_url", marketplaceURL), + }, nil +} + +// GetPlugins fetches the given page of plugins. The first page is 0. +func (store *Proxy) GetPlugins(pluginFilter *model.PluginFilter) ([]*model.Plugin, error) { + client := api.NewClient(store.marketplaceURL) + + plugins, err := client.GetPlugins(&api.GetPluginsRequest{ + Page: pluginFilter.Page, + PerPage: pluginFilter.PerPage, + Filter: pluginFilter.Filter, + ServerVersion: pluginFilter.ServerVersion, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to reach upstream store") + } + + return plugins, nil +} diff --git a/internal/store/proxy_test.go b/internal/store/proxy_test.go new file mode 100644 index 00000000..46cff5e6 --- /dev/null +++ b/internal/store/proxy_test.go @@ -0,0 +1,76 @@ +package store + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-marketplace/internal/model" + "github.com/mattermost/mattermost-marketplace/internal/testlib" + mattermostModel "github.com/mattermost/mattermost-server/v5/model" +) + +func TestProxy(t *testing.T) { + t.Run("empty stream", func(t *testing.T) { + logger := testlib.MakeLogger(t) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + + })) + t.Cleanup(ts.Close) + + proxyStore, err := NewProxy(ts.URL, logger) + require.NoError(t, err) + + plugins, err := proxyStore.GetPlugins(&model.PluginFilter{ + PerPage: model.AllPerPage, + }) + require.NoError(t, err) + require.Empty(t, plugins) + }) + + t.Run("empty stream", func(t *testing.T) { + logger := testlib.MakeLogger(t) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"invalid":`)) + })) + t.Cleanup(ts.Close) + + proxyStore, err := NewProxy(ts.URL, logger) + require.NoError(t, err) + + plugins, err := proxyStore.GetPlugins(&model.PluginFilter{ + PerPage: model.AllPerPage, + }) + require.Error(t, err) + require.Empty(t, plugins) + }) + + t.Run("valid stream", func(t *testing.T) { + logger := testlib.MakeLogger(t) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`[{"homepage_url":"https://github.com/mattermost/mattermost-plugin-demo","icon_data":"icon-data.svg","download_url":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","signature":"signature1", "release_notes_url":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","manifest":{}}]`)) + })) + t.Cleanup(ts.Close) + + proxyStore, err := NewProxy(ts.URL, logger) + require.NoError(t, err) + + plugins, err := proxyStore.GetPlugins(&model.PluginFilter{ + PerPage: model.AllPerPage, + }) + require.NoError(t, err) + require.Equal(t, []*model.Plugin{&model.Plugin{ + HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", + IconData: "icon-data.svg", + DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz", + Signature: "signature1", + ReleaseNotesURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0", + Manifest: &mattermostModel.Manifest{}, + }}, plugins) + }) +} From 7f5e04c50568af9474feb4e6b44ca87ca3517dfd Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 22 Apr 2020 22:48:24 -0300 Subject: [PATCH 04/11] support --upstream --- Makefile | 1 + README.md | 14 ++++++++++++++ cmd/lambda/main.go | 19 ++++++++++++++++++- cmd/marketplace/server.go | 25 +++++++++++++++++++++++-- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 44b07814..67f735e8 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ BUILD_HASH_SHORT = $(shell git rev-parse --short HEAD) LDFLAGS += -X "github.com/mattermost/mattermost-marketplace/internal/api.buildTag=$(BUILD_TAG)" LDFLAGS += -X "github.com/mattermost/mattermost-marketplace/internal/api.buildHash=$(BUILD_HASH)" LDFLAGS += -X "github.com/mattermost/mattermost-marketplace/internal/api.buildHashShort=$(BUILD_HASH_SHORT)" +LDFLAGS += -X "main.upstreamURL=$(BUILD_UPSTREAM_URL)" SLS_STAGE ?= "dev" ## Checks the code style, tests, builds and bundles. diff --git a/README.md b/README.md index bebb47a9..d5830de0 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,20 @@ Running all tests: $ make test ``` +### Proxying upstream + +The marketplace can be configured to proxy to an upstream marketplace, overlaying any locally defined plugins on top of the remote service. Invoke the server with the appropriate flag: + +``` +go run ./cmd/marketplace server --upstream https://api.integrations.mattermost.com +``` + +To compile this flag into the binary such as when building the lambda function, define the appropriate environment variable: +``` +export BUILD_UPSTREAM_URL=https://api.integrations.mattermost.com +make build-lambda +``` + ### Updating plugins.json At the moment, the Marketplace simply points at the latest release of a fixed set of Mattermost plugins. In the future, this database will be fine-tuned to facilitate tracking multiple versions for the appropriate Mattermost server version. To update `plugins.json`, ensure you have [jq](https://stedolan.github.io/jq/) and [sponge](https://linux.die.net/man/1/sponge) installed and run: diff --git a/cmd/lambda/main.go b/cmd/lambda/main.go index 361808a3..bd3d7b3f 100644 --- a/cmd/lambda/main.go +++ b/cmd/lambda/main.go @@ -13,6 +13,11 @@ import ( "github.com/mattermost/mattermost-marketplace/internal/store" ) +var ( + // upstreamURL may be compiled into the binary by defining $BUILD_UPSTREAM_URL + upstreamURL = "" +) + var logger *logrus.Logger func main() { @@ -45,14 +50,26 @@ func newStatikStore(statikPath string, logger logrus.FieldLogger) (*store.Static func listenAndServe() error { logger = logrus.New() + var stores []store.Store + statikStore, err := newStatikStore("/plugins.json", logger) if err != nil { return err } + stores = append(stores, statikStore) + + if upstreamURL != "" { + upstreamStore, err := store.NewProxy(upstreamURL, logger) + if err != nil { + return errors.Wrap(err, "failed to initialize upstream store") + } + + stores = append(stores, upstreamStore) + } router := mux.NewRouter() api.Register(router, &api.Context{ - Store: statikStore, + Store: store.NewMerged(logger, stores...), Logger: logger, }) diff --git a/cmd/marketplace/server.go b/cmd/marketplace/server.go index 87ae3292..738eba88 100644 --- a/cmd/marketplace/server.go +++ b/cmd/marketplace/server.go @@ -18,13 +18,19 @@ import ( "github.com/mattermost/mattermost-marketplace/internal/store" ) -var instanceID string +var ( + instanceID string + + // upstreamURL may be compiled into the binary by defining $BUILD_UPSTREAM_URL + upstreamURL string +) func init() { instanceID = model.NewId() serverCmd.PersistentFlags().String("database", "plugins.json", "The read-only JSON file backing the server.") serverCmd.PersistentFlags().String("listen", ":8085", "The interface and port on which to listen.") + serverCmd.PersistentFlags().String("upstream", upstreamURL, "An upstream marketplace server with which to merge results.") serverCmd.PersistentFlags().Bool("debug", false, "Whether to output debug logs.") } @@ -46,10 +52,25 @@ var serverCmd = &cobra.Command{ } defer databaseFile.Close() + var stores []store.Store + staticStore, err := store.NewStaticFromReader(databaseFile, logger) if err != nil { return errors.Wrap(err, "failed to initialize store") } + stores = append(stores, staticStore) + + upstreamURL, _ := command.Flags().GetString("upstream") + if upstreamURL != "" { + upstreamStore, err := store.NewProxy(upstreamURL, logger) + if err != nil { + return errors.Wrap(err, "failed to initialize upstream store") + } + + logger.WithField("upstream", upstreamURL).Info("Proxying to upstream marketplace") + + stores = append(stores, upstreamStore) + } logger := logger.WithField("instance", instanceID) logger.Info("Starting Plugin Marketplace") @@ -57,7 +78,7 @@ var serverCmd = &cobra.Command{ router := mux.NewRouter() api.Register(router, &api.Context{ - Store: staticStore, + Store: store.NewMerged(logger, stores...), Logger: logger, }) From 2f6018297a9d19bce9f17d6cb3eaa603d308d0d6 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 22 Apr 2020 23:29:31 -0300 Subject: [PATCH 05/11] generate on run-server --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 67f735e8..0a497312 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ run: run-server ## Run the Plugin Marketplace .PHONY: run-server -run-server: +run-server: generate go run -ldflags="$(LDFLAGS)" ./cmd/marketplace server ## Compile the server as a lambda function From a54a1db1411a3967cd97dec1b090939ad5a487b3 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 22 Apr 2020 23:52:31 -0300 Subject: [PATCH 06/11] update to go1.14 --- .circleci/config.yml | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b6710d62..397a732c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 executors: build: docker: - - image: circleci/golang:1.13 + - image: circleci/golang:1.14 deploy: docker: - image: circleci/node:8.10 diff --git a/go.mod b/go.mod index 9dc06fa8..6f759f80 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mattermost/mattermost-marketplace -go 1.12 +go 1.14 require ( github.com/akrylysov/algnhsa v0.0.0-20190319020909-05b3d192e9a7 From 5087090eed4c9ea4d60b24ae7d438f580a108ccf Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Thu, 23 Apr 2020 10:51:27 -0300 Subject: [PATCH 07/11] tidy up interface/comments --- internal/store/merged.go | 12 +++++------- internal/store/store.go | 8 ++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 internal/store/store.go diff --git a/internal/store/merged.go b/internal/store/merged.go index 514b2d26..fc50f658 100644 --- a/internal/store/merged.go +++ b/internal/store/merged.go @@ -7,13 +7,11 @@ import ( "github.com/mattermost/mattermost-marketplace/internal/model" ) -// Store describes the interface to the backing store. -type Store interface { - GetPlugins(filter *model.PluginFilter) ([]*model.Plugin, error) -} - -// Merged is a store that merges the results of multiple stores together, always preferring the -// most recently updated plugin record when a conflict occurs. +// Merged is a store that merges the results of multiple stores together. +// +// If a plugin is present in multiple stores, the later version is preferred. If a plugin with +// the same version is present in multiple stores, the one from the later store (as initialized) +// is preferred. type Merged struct { stores []Store logger logrus.FieldLogger diff --git a/internal/store/store.go b/internal/store/store.go new file mode 100644 index 00000000..491c76f3 --- /dev/null +++ b/internal/store/store.go @@ -0,0 +1,8 @@ +package store + +import "github.com/mattermost/mattermost-marketplace/internal/model" + +// Store describes the interface to the backing store. +type Store interface { + GetPlugins(filter *model.PluginFilter) ([]*model.Plugin, error) +} From 2c932916da715ff4fefca8e74f914a6ba96a1c21 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Thu, 23 Apr 2020 11:37:46 -0300 Subject: [PATCH 08/11] golangci-lint local-only complaints --- cmd/marketplace/server.go | 3 ++- internal/store/merged_test.go | 3 ++- internal/store/proxy_test.go | 12 +++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cmd/marketplace/server.go b/cmd/marketplace/server.go index 738eba88..238e7b60 100644 --- a/cmd/marketplace/server.go +++ b/cmd/marketplace/server.go @@ -62,7 +62,8 @@ var serverCmd = &cobra.Command{ upstreamURL, _ := command.Flags().GetString("upstream") if upstreamURL != "" { - upstreamStore, err := store.NewProxy(upstreamURL, logger) + var upstreamStore *store.Proxy + upstreamStore, err = store.NewProxy(upstreamURL, logger) if err != nil { return errors.Wrap(err, "failed to initialize upstream store") } diff --git a/internal/store/merged_test.go b/internal/store/merged_test.go index 4a798b4a..1906160e 100644 --- a/internal/store/merged_test.go +++ b/internal/store/merged_test.go @@ -6,9 +6,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + mattermostModel "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-marketplace/internal/model" "github.com/mattermost/mattermost-marketplace/internal/testlib" - mattermostModel "github.com/mattermost/mattermost-server/v5/model" ) func TestMerged(t *testing.T) { diff --git a/internal/store/proxy_test.go b/internal/store/proxy_test.go index 46cff5e6..0abd613a 100644 --- a/internal/store/proxy_test.go +++ b/internal/store/proxy_test.go @@ -7,9 +7,10 @@ import ( "github.com/stretchr/testify/require" + mattermostModel "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-marketplace/internal/model" "github.com/mattermost/mattermost-marketplace/internal/testlib" - mattermostModel "github.com/mattermost/mattermost-server/v5/model" ) func TestProxy(t *testing.T) { @@ -17,7 +18,6 @@ func TestProxy(t *testing.T) { logger := testlib.MakeLogger(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - })) t.Cleanup(ts.Close) @@ -35,7 +35,8 @@ func TestProxy(t *testing.T) { logger := testlib.MakeLogger(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"invalid":`)) + _, err := w.Write([]byte(`{"invalid":`)) + require.NoError(t, err) })) t.Cleanup(ts.Close) @@ -53,7 +54,8 @@ func TestProxy(t *testing.T) { logger := testlib.MakeLogger(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte(`[{"homepage_url":"https://github.com/mattermost/mattermost-plugin-demo","icon_data":"icon-data.svg","download_url":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","signature":"signature1", "release_notes_url":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","manifest":{}}]`)) + _, err := w.Write([]byte(`[{"homepage_url":"https://github.com/mattermost/mattermost-plugin-demo","icon_data":"icon-data.svg","download_url":"https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz","signature":"signature1", "release_notes_url":"https://github.com/mattermost/mattermost-plugin-demo/releases/v0.1.0","manifest":{}}]`)) + require.NoError(t, err) })) t.Cleanup(ts.Close) @@ -64,7 +66,7 @@ func TestProxy(t *testing.T) { PerPage: model.AllPerPage, }) require.NoError(t, err) - require.Equal(t, []*model.Plugin{&model.Plugin{ + require.Equal(t, []*model.Plugin{{ HomepageURL: "https://github.com/mattermost/mattermost-plugin-demo", IconData: "icon-data.svg", DownloadURL: "https://github.com/mattermost/mattermost-plugin-demo/releases/download/v0.1.0/com.mattermost.demo-plugin-0.1.0.tar.gz", From bc9c8c7376a4c309c118dd848791046b20ab6c01 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Mon, 27 Apr 2020 10:21:27 -0300 Subject: [PATCH 09/11] Update internal/store/proxy_test.go --- internal/store/proxy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/store/proxy_test.go b/internal/store/proxy_test.go index 0abd613a..cc399e21 100644 --- a/internal/store/proxy_test.go +++ b/internal/store/proxy_test.go @@ -31,7 +31,7 @@ func TestProxy(t *testing.T) { require.Empty(t, plugins) }) - t.Run("empty stream", func(t *testing.T) { + t.Run("empty stream with error", func(t *testing.T) { logger := testlib.MakeLogger(t) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) From bc09c08539615805159f406f1d191548b556c53a Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 29 Apr 2020 14:39:29 -0300 Subject: [PATCH 10/11] revert generate on run-server --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0a497312..67f735e8 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ run: run-server ## Run the Plugin Marketplace .PHONY: run-server -run-server: generate +run-server: go run -ldflags="$(LDFLAGS)" ./cmd/marketplace server ## Compile the server as a lambda function From fd73eea706bed481ab96088ae6165f86e3b63619 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 29 Apr 2020 14:44:42 -0300 Subject: [PATCH 11/11] avoid merged store unless needed --- cmd/lambda/main.go | 11 +++++------ cmd/marketplace/server.go | 9 ++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/cmd/lambda/main.go b/cmd/lambda/main.go index bd3d7b3f..a9b0ec58 100644 --- a/cmd/lambda/main.go +++ b/cmd/lambda/main.go @@ -50,13 +50,12 @@ func newStatikStore(statikPath string, logger logrus.FieldLogger) (*store.Static func listenAndServe() error { logger = logrus.New() - var stores []store.Store - - statikStore, err := newStatikStore("/plugins.json", logger) + var apiStore store.Store + var err error + apiStore, err = newStatikStore("/plugins.json", logger) if err != nil { return err } - stores = append(stores, statikStore) if upstreamURL != "" { upstreamStore, err := store.NewProxy(upstreamURL, logger) @@ -64,12 +63,12 @@ func listenAndServe() error { return errors.Wrap(err, "failed to initialize upstream store") } - stores = append(stores, upstreamStore) + apiStore = store.NewMerged(logger, apiStore, upstreamStore) } router := mux.NewRouter() api.Register(router, &api.Context{ - Store: store.NewMerged(logger, stores...), + Store: apiStore, Logger: logger, }) diff --git a/cmd/marketplace/server.go b/cmd/marketplace/server.go index 238e7b60..3dd43cf1 100644 --- a/cmd/marketplace/server.go +++ b/cmd/marketplace/server.go @@ -52,13 +52,12 @@ var serverCmd = &cobra.Command{ } defer databaseFile.Close() - var stores []store.Store + var apiStore store.Store - staticStore, err := store.NewStaticFromReader(databaseFile, logger) + apiStore, err = store.NewStaticFromReader(databaseFile, logger) if err != nil { return errors.Wrap(err, "failed to initialize store") } - stores = append(stores, staticStore) upstreamURL, _ := command.Flags().GetString("upstream") if upstreamURL != "" { @@ -70,7 +69,7 @@ var serverCmd = &cobra.Command{ logger.WithField("upstream", upstreamURL).Info("Proxying to upstream marketplace") - stores = append(stores, upstreamStore) + apiStore = store.NewMerged(logger, apiStore, upstreamStore) } logger := logger.WithField("instance", instanceID) @@ -79,7 +78,7 @@ var serverCmd = &cobra.Command{ router := mux.NewRouter() api.Register(router, &api.Context{ - Store: store.NewMerged(logger, stores...), + Store: apiStore, Logger: logger, })