diff options
Diffstat (limited to 'model')
-rw-r--r-- | model/bundle_info.go | 20 | ||||
-rw-r--r-- | model/bundle_info_test.go | 30 | ||||
-rw-r--r-- | model/client4.go | 69 | ||||
-rw-r--r-- | model/config.go | 6 | ||||
-rw-r--r-- | model/manifest.go | 118 | ||||
-rw-r--r-- | model/manifest_test.go | 130 |
6 files changed, 373 insertions, 0 deletions
diff --git a/model/bundle_info.go b/model/bundle_info.go new file mode 100644 index 000000000..67b5dd0ed --- /dev/null +++ b/model/bundle_info.go @@ -0,0 +1,20 @@ +package model + +type BundleInfo struct { + Path string + + Manifest *Manifest + ManifestPath string + ManifestError error +} + +// Returns bundle info for the given path. The return value is never nil. +func BundleInfoForPath(path string) *BundleInfo { + m, mpath, err := FindManifest(path) + return &BundleInfo{ + Path: path, + Manifest: m, + ManifestPath: mpath, + ManifestError: err, + } +} diff --git a/model/bundle_info_test.go b/model/bundle_info_test.go new file mode 100644 index 000000000..e94a5cb64 --- /dev/null +++ b/model/bundle_info_test.go @@ -0,0 +1,30 @@ +package model + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBundleInfoForPath(t *testing.T) { + dir, err := ioutil.TempDir("", "mm-plugin-test") + require.NoError(t, err) + defer os.RemoveAll(dir) + + path := filepath.Join(dir, "plugin.json") + f, err := os.Create(path) + require.NoError(t, err) + _, err = f.WriteString(`{"id": "foo"}`) + f.Close() + require.NoError(t, err) + + info := BundleInfoForPath(dir) + assert.Equal(t, info.Path, dir) + assert.NotNil(t, info.Manifest) + assert.Equal(t, info.ManifestPath, path) + assert.Nil(t, info.ManifestError) +} diff --git a/model/client4.go b/model/client4.go index 26ea6ee03..badb60a2a 100644 --- a/model/client4.go +++ b/model/client4.go @@ -178,6 +178,14 @@ func (c *Client4) GetFileRoute(fileId string) string { return fmt.Sprintf(c.GetFilesRoute()+"/%v", fileId) } +func (c *Client4) GetPluginsRoute() string { + return fmt.Sprintf("/plugins") +} + +func (c *Client4) GetPluginRoute(pluginId string) string { + return fmt.Sprintf(c.GetPluginsRoute()+"/%v", pluginId) +} + func (c *Client4) GetSystemRoute() string { return fmt.Sprintf("/system") } @@ -3019,3 +3027,64 @@ func (c *Client4) CancelJob(jobId string) (bool, *Response) { return CheckStatusOK(r), BuildResponse(r) } } + +// Plugin Section + +// UploadPlugin takes an io.Reader stream pointing to the contents of a .tar.gz plugin. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) UploadPlugin(file io.Reader) (*Manifest, *Response) { + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + + if part, err := writer.CreateFormFile("plugin", "plugin.tar.gz"); err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)} + } else if _, err = io.Copy(part, file); err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)} + } + + if err := writer.Close(); err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)} + } + + rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetPluginsRoute(), body) + rq.Header.Set("Content-Type", writer.FormDataContentType()) + rq.Close = true + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil || rp == nil { + return nil, BuildErrorResponse(rp, NewAppError("UploadPlugin", "model.client.connecting.app_error", nil, err.Error(), 0)) + } else { + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } else { + return ManifestFromJson(rp.Body), BuildResponse(rp) + } + } +} + +// GetPlugins will return a list of plugin manifests for currently active plugins. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) GetPlugins() ([]*Manifest, *Response) { + if r, err := c.DoApiGet(c.GetPluginsRoute(), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return ManifestListFromJson(r.Body), BuildResponse(r) + } +} + +// RemovePlugin will deactivate and delete a plugin. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) RemovePlugin(id string) (bool, *Response) { + if r, err := c.DoApiDelete(c.GetPluginRoute(id)); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} diff --git a/model/config.go b/model/config.go index 050110512..65608c9a5 100644 --- a/model/config.go +++ b/model/config.go @@ -477,6 +477,7 @@ type JobSettings struct { } type PluginSettings struct { + Enable *bool Plugins map[string]interface{} } @@ -1522,6 +1523,11 @@ func (o *Config) SetDefaults() { *o.JobSettings.RunScheduler = true } + if o.PluginSettings.Enable == nil { + o.PluginSettings.Enable = new(bool) + *o.PluginSettings.Enable = false + } + if o.PluginSettings.Plugins == nil { o.PluginSettings.Plugins = make(map[string]interface{}) } diff --git a/model/manifest.go b/model/manifest.go new file mode 100644 index 000000000..e61ccc8ad --- /dev/null +++ b/model/manifest.go @@ -0,0 +1,118 @@ +package model + +import ( + "encoding/json" + "io" + "io/ioutil" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +type Manifest struct { + Id string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + Backend *ManifestBackend `json:"backend,omitempty" yaml:"backend,omitempty"` + Webapp *ManifestWebapp `json:"webapp,omitempty" yaml:"webapp,omitempty"` +} + +type ManifestBackend struct { + Executable string `json:"executable" yaml:"executable"` +} + +type ManifestWebapp struct { + BundlePath string `json:"bundle_path" yaml:"bundle_path"` +} + +func (m *Manifest) ToJson() string { + b, err := json.Marshal(m) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ManifestListToJson(m []*Manifest) string { + b, err := json.Marshal(m) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ManifestFromJson(data io.Reader) *Manifest { + decoder := json.NewDecoder(data) + var m Manifest + err := decoder.Decode(&m) + if err == nil { + return &m + } else { + return nil + } +} + +func ManifestListFromJson(data io.Reader) []*Manifest { + decoder := json.NewDecoder(data) + var manifests []*Manifest + err := decoder.Decode(&manifests) + if err == nil { + return manifests + } else { + return nil + } +} + +// FindManifest will find and parse the manifest in a given directory. +// +// In all cases other than a does-not-exist error, path is set to the path of the manifest file that was +// found. +// +// Manifests are JSON or YAML files named plugin.json, plugin.yaml, or plugin.yml. +func FindManifest(dir string) (manifest *Manifest, path string, err error) { + for _, name := range []string{"plugin.yml", "plugin.yaml"} { + path = filepath.Join(dir, name) + f, ferr := os.Open(path) + if ferr != nil { + if !os.IsNotExist(ferr) { + err = ferr + return + } + continue + } + b, ioerr := ioutil.ReadAll(f) + f.Close() + if ioerr != nil { + err = ioerr + return + } + var parsed Manifest + err = yaml.Unmarshal(b, &parsed) + if err != nil { + return + } + manifest = &parsed + return + } + + path = filepath.Join(dir, "plugin.json") + f, ferr := os.Open(path) + if ferr != nil { + if os.IsNotExist(ferr) { + path = "" + } + err = ferr + return + } + defer f.Close() + var parsed Manifest + err = json.NewDecoder(f).Decode(&parsed) + if err != nil { + return + } + manifest = &parsed + return +} diff --git a/model/manifest_test.go b/model/manifest_test.go new file mode 100644 index 000000000..237640564 --- /dev/null +++ b/model/manifest_test.go @@ -0,0 +1,130 @@ +package model + +import ( + "encoding/json" + "gopkg.in/yaml.v2" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindManifest(t *testing.T) { + for _, tc := range []struct { + Filename string + Contents string + ExpectError bool + ExpectNotExist bool + }{ + {"foo", "bar", true, true}, + {"plugin.json", "bar", true, false}, + {"plugin.json", `{"id": "foo"}`, false, false}, + {"plugin.yaml", `id: foo`, false, false}, + {"plugin.yaml", "bar", true, false}, + {"plugin.yml", `id: foo`, false, false}, + {"plugin.yml", "bar", true, false}, + } { + dir, err := ioutil.TempDir("", "mm-plugin-test") + require.NoError(t, err) + defer os.RemoveAll(dir) + + path := filepath.Join(dir, tc.Filename) + f, err := os.Create(path) + require.NoError(t, err) + _, err = f.WriteString(tc.Contents) + f.Close() + require.NoError(t, err) + + m, mpath, err := FindManifest(dir) + assert.True(t, (err != nil) == tc.ExpectError, tc.Filename) + assert.True(t, (err != nil && os.IsNotExist(err)) == tc.ExpectNotExist, tc.Filename) + if !tc.ExpectNotExist { + assert.Equal(t, path, mpath, tc.Filename) + } else { + assert.Empty(t, mpath, tc.Filename) + } + if !tc.ExpectError { + require.NotNil(t, m, tc.Filename) + assert.NotEmpty(t, m.Id, tc.Filename) + } + } +} + +func TestManifestUnmarshal(t *testing.T) { + expected := Manifest{ + Id: "theid", + Backend: &ManifestBackend{ + Executable: "theexecutable", + }, + Webapp: &ManifestWebapp{ + BundlePath: "thebundlepath", + }, + } + + var yamlResult Manifest + require.NoError(t, yaml.Unmarshal([]byte(` +id: theid +backend: + executable: theexecutable +webapp: + bundle_path: thebundlepath +`), &yamlResult)) + assert.Equal(t, expected, yamlResult) + + var jsonResult Manifest + require.NoError(t, json.Unmarshal([]byte(`{ + "id": "theid", + "backend": { + "executable": "theexecutable" + }, + "webapp": { + "bundle_path": "thebundlepath" + } + }`), &jsonResult)) + assert.Equal(t, expected, jsonResult) +} + +func TestFindManifest_FileErrors(t *testing.T) { + for _, tc := range []string{"plugin.yaml", "plugin.json"} { + dir, err := ioutil.TempDir("", "mm-plugin-test") + require.NoError(t, err) + defer os.RemoveAll(dir) + + path := filepath.Join(dir, tc) + require.NoError(t, os.Mkdir(path, 0700)) + + m, mpath, err := FindManifest(dir) + assert.Nil(t, m) + assert.Equal(t, path, mpath) + assert.Error(t, err, tc) + assert.False(t, os.IsNotExist(err), tc) + } +} + +func TestManifestJson(t *testing.T) { + manifest := &Manifest{ + Id: "theid", + Backend: &ManifestBackend{ + Executable: "theexecutable", + }, + Webapp: &ManifestWebapp{ + BundlePath: "thebundlepath", + }, + } + + json := manifest.ToJson() + newManifest := ManifestFromJson(strings.NewReader(json)) + assert.Equal(t, newManifest, manifest) + assert.Equal(t, newManifest.ToJson(), json) + assert.Equal(t, ManifestFromJson(strings.NewReader("junk")), (*Manifest)(nil)) + + manifestList := []*Manifest{manifest} + json = ManifestListToJson(manifestList) + newManifestList := ManifestListFromJson(strings.NewReader(json)) + assert.Equal(t, newManifestList, manifestList) + assert.Equal(t, ManifestListToJson(newManifestList), json) +} |