PLT-8018: Bundled jira plugin (#7920)

* bundled jira plugin

* fix generated file formatting, add prepackaged key

* whoops, uploaded wrong file

* whitelist generated files for license check

* make it work for people without go/bin in their path
This commit is contained in:
Chris
2017-11-30 14:55:44 -06:00
committed by GitHub
parent eaca461ee3
commit daebd26a28
17 changed files with 843 additions and 358 deletions

View File

@@ -275,6 +275,17 @@ store-mocks:
go get github.com/vektra/mockery/...
GOPATH=$(shell go env GOPATH) $(shell go env GOPATH)/bin/mockery -dir store -all -output store/storetest/mocks -note 'Regenerate this file using `make store-mocks`.'
update-jira-plugin:
go get github.com/jteeuwen/go-bindata/...
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep darwin-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
$(shell go env GOPATH)/bin/go-bindata -pkg jira -o app/plugin/jira/plugin_darwin_amd64.go plugin.tar.gz
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep linux-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
$(shell go env GOPATH)/bin/go-bindata -pkg jira -o app/plugin/jira/plugin_linux_amd64.go plugin.tar.gz
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep windows-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
$(shell go env GOPATH)/bin/go-bindata -pkg jira -o app/plugin/jira/plugin_windows_amd64.go plugin.tar.gz
rm plugin.tar.gz
gofmt -s -w ./app/plugin/jira
check-licenses:
./scripts/license-check.sh $(TE_PACKAGES) $(EE_PACKAGES)

View File

@@ -88,7 +88,7 @@ func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
response, err := c.App.GetPluginManifests()
response, err := c.App.GetPlugins()
if err != nil {
c.Err = err
return

View File

@@ -510,7 +510,7 @@ func (a *App) trackPlugins() {
backendInactiveCount := 0
settingsCount := 0
plugins, _ := a.GetPluginManifests()
plugins, _ := a.GetPlugins()
if plugins != nil {
totalActiveCount = len(plugins.Active)

View File

@@ -4,6 +4,7 @@
package app
import (
"bytes"
"context"
"io"
"io/ioutil"
@@ -26,9 +27,12 @@ import (
"github.com/mattermost/mattermost-server/plugin/pluginenv"
)
var prepackagedPlugins map[string]func(string) ([]byte, error) = map[string]func(string) ([]byte, error){
"jira": jira.Asset,
}
func (a *App) initBuiltInPlugins() {
plugins := map[string]builtinplugin.Plugin{
"jira": &jira.Plugin{},
"ldapextras": &ldapextras.Plugin{},
}
for id, p := range plugins {
@@ -106,24 +110,28 @@ func (a *App) ActivatePlugins() {
// InstallPlugin unpacks and installs a plugin but does not activate it.
func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
return a.installPlugin(pluginFile, false)
}
func (a *App) installPlugin(pluginFile io.Reader, allowPrepackaged bool) (*model.Manifest, *model.AppError) {
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
return nil, model.NewAppError("InstallPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return nil, model.NewAppError("installPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
tmpDir, err := ioutil.TempDir("", "plugintmp")
if err != nil {
return nil, model.NewAppError("InstallPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
}
defer os.RemoveAll(tmpDir)
if err := utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
return nil, model.NewAppError("InstallPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
return nil, model.NewAppError("installPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
}
tmpPluginDir := tmpDir
dir, err := ioutil.ReadDir(tmpDir)
if err != nil {
return nil, model.NewAppError("InstallPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
}
if len(dir) == 1 && dir[0].IsDir() {
@@ -132,23 +140,27 @@ func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppEr
manifest, _, err := model.FindManifest(tmpPluginDir)
if err != nil {
return nil, model.NewAppError("InstallPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
}
if _, ok := prepackagedPlugins[manifest.Id]; ok && !allowPrepackaged {
return nil, model.NewAppError("installPlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
}
bundles, err := a.PluginEnv.Plugins()
if err != nil {
return nil, model.NewAppError("InstallPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
return nil, model.NewAppError("installPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
}
for _, bundle := range bundles {
if bundle.Manifest.Id == manifest.Id {
return nil, model.NewAppError("InstallPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
return nil, model.NewAppError("installPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
}
}
err = utils.CopyDir(tmpPluginDir, filepath.Join(a.PluginEnv.SearchPath(), manifest.Id))
if err != nil {
return nil, model.NewAppError("InstallPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
return nil, model.NewAppError("installPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
}
// Should add manifest validation and error handling here
@@ -156,22 +168,26 @@ func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppEr
return manifest, nil
}
func (a *App) GetPluginManifests() (*model.PluginsResponse, *model.AppError) {
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
return nil, model.NewAppError("GetPluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins, err := a.PluginEnv.Plugins()
if err != nil {
return nil, model.NewAppError("GetPluginManifests", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
}
resp := &model.PluginsResponse{Active: []*model.Manifest{}, Inactive: []*model.Manifest{}}
resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}}
for _, plugin := range plugins {
info := &model.PluginInfo{
Manifest: *plugin.Manifest,
}
_, info.Prepackaged = prepackagedPlugins[plugin.Manifest.Id]
if a.PluginEnv.IsPluginActive(plugin.Manifest.Id) {
resp.Active = append(resp.Active, plugin.Manifest)
resp.Active = append(resp.Active, info)
} else {
resp.Inactive = append(resp.Inactive, plugin.Manifest)
resp.Inactive = append(resp.Inactive, info)
}
}
@@ -194,13 +210,21 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
}
func (a *App) RemovePlugin(id string) *model.AppError {
return a.removePlugin(id, false)
}
func (a *App) removePlugin(id string, allowPrepackaged bool) *model.AppError {
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if _, ok := prepackagedPlugins[id]; ok && !allowPrepackaged {
return model.NewAppError("removePlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
}
plugins, err := a.PluginEnv.Plugins()
if err != nil {
return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
}
var manifest *model.Manifest
@@ -212,13 +236,13 @@ func (a *App) RemovePlugin(id string) *model.AppError {
}
if manifest == nil {
return model.NewAppError("RemovePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
}
if a.PluginEnv.IsPluginActive(id) {
err := a.PluginEnv.DeactivatePlugin(id)
if err != nil {
return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
}
if manifest.HasClient() {
@@ -230,7 +254,7 @@ func (a *App) RemovePlugin(id string) *model.AppError {
err = os.RemoveAll(filepath.Join(a.PluginEnv.SearchPath(), id))
if err != nil {
return model.NewAppError("RemovePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
@@ -239,7 +263,7 @@ func (a *App) RemovePlugin(id string) *model.AppError {
// EnablePlugin will set the config for an installed plugin to enabled, triggering activation if inactive.
func (a *App) EnablePlugin(id string) *model.AppError {
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins, err := a.PluginEnv.Plugins()
@@ -273,7 +297,7 @@ func (a *App) EnablePlugin(id string) *model.AppError {
// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
func (a *App) DisablePlugin(id string) *model.AppError {
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
}
plugins, err := a.PluginEnv.Plugins()
@@ -315,19 +339,17 @@ func (a *App) InitPlugins(pluginPath, webappPath string) {
l4g.Info("Starting up plugins")
err := os.Mkdir(pluginPath, 0744)
if err != nil && !os.IsExist(err) {
if err := os.Mkdir(pluginPath, 0744); err != nil && !os.IsExist(err) {
l4g.Error("failed to start up plugins: " + err.Error())
return
}
err = os.Mkdir(webappPath, 0744)
if err != nil && !os.IsExist(err) {
if err := os.Mkdir(webappPath, 0744); err != nil && !os.IsExist(err) {
l4g.Error("failed to start up plugins: " + err.Error())
return
}
a.PluginEnv, err = pluginenv.New(
if env, err := pluginenv.New(
pluginenv.SearchPath(pluginPath),
pluginenv.WebappPath(webappPath),
pluginenv.APIProvider(func(m *model.Manifest) (plugin.API, error) {
@@ -340,11 +362,27 @@ func (a *App) InitPlugins(pluginPath, webappPath string) {
},
}, nil
}),
)
if err != nil {
); err != nil {
l4g.Error("failed to start up plugins: " + err.Error())
return
} else {
a.PluginEnv = env
}
for id, asset := range prepackagedPlugins {
if tarball, err := asset("plugin.tar.gz"); err != nil {
l4g.Error("failed to install prepackaged plugin: " + err.Error())
} else if tarball != nil {
a.removePlugin(id, true)
if _, err := a.installPlugin(bytes.NewReader(tarball), true); err != nil {
l4g.Error("failed to install prepackaged plugin: " + err.Error())
}
if _, ok := a.Config().PluginSettings.PluginStates[id]; !ok {
if err := a.EnablePlugin(id); err != nil {
l4g.Error("failed to enable prepackaged plugin: " + err.Error())
}
}
}
}
utils.RemoveConfigListener(a.PluginConfigListenerId)

View File

@@ -1,10 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package jira
type Configuration struct {
Enabled bool
Secret string
UserName string
}

View File

@@ -1,79 +1,10 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
// +build !amd64 !darwin,!linux,!windows
package jira
import (
"crypto/subtle"
"encoding/json"
"net/http"
"sync/atomic"
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/mattermost-server/app/plugin"
"github.com/mattermost/mattermost-server/model"
)
type Plugin struct {
plugin.Base
api plugin.API
configuration atomic.Value
}
func (p *Plugin) Initialize(api plugin.API) {
p.api = api
p.OnConfigurationChange()
api.PluginRouter().HandleFunc("/webhook", p.handleWebhook).Methods("POST")
}
func (p *Plugin) config() *Configuration {
return p.configuration.Load().(*Configuration)
}
func (p *Plugin) OnConfigurationChange() {
var configuration Configuration
if err := p.api.LoadPluginConfiguration(&configuration); err != nil {
l4g.Error(err.Error())
}
p.configuration.Store(&configuration)
}
func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
config := p.config()
if !config.Enabled || config.Secret == "" || config.UserName == "" {
http.Error(w, "This plugin is not configured.", http.StatusForbidden)
return
} else if subtle.ConstantTimeCompare([]byte(r.URL.Query().Get("secret")), []byte(config.Secret)) != 1 {
http.Error(w, "You must provide the configured secret.", http.StatusForbidden)
return
}
var webhook Webhook
if err := json.NewDecoder(r.Body).Decode(&webhook); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
} else if attachment, err := webhook.SlackAttachment(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else if attachment == nil {
return
} else if r.URL.Query().Get("channel") == "" {
http.Error(w, "You must provide a channel.", http.StatusBadRequest)
} else if user, err := p.api.GetUserByName(config.UserName); err != nil {
http.Error(w, p.api.I18n(err.Message, r), err.StatusCode)
} else if team, err := p.api.GetTeamByName(r.URL.Query().Get("team")); err != nil {
http.Error(w, p.api.I18n(err.Message, r), err.StatusCode)
} else if channel, err := p.api.GetChannelByName(team.Id, r.URL.Query().Get("channel")); err != nil {
http.Error(w, p.api.I18n(err.Message, r), err.StatusCode)
} else if _, err := p.api.CreatePost(&model.Post{
ChannelId: channel.Id,
Type: model.POST_SLACK_ATTACHMENT,
UserId: user.Id,
Props: map[string]interface{}{
"from_webhook": "true",
"use_user_icon": "true",
"attachments": []*model.SlackAttachment{attachment},
},
}); err != nil {
http.Error(w, p.api.I18n(err.Message, r), err.StatusCode)
}
func Asset(name string) ([]byte, error) {
return nil, nil
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,174 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package jira
import (
"bytes"
"strings"
"text/template"
"github.com/mattermost/mattermost-server/model"
)
type Webhook struct {
WebhookEvent string
Issue struct {
Self string
Key string
Fields struct {
Assignee *struct {
DisplayName string
Name string
}
Summary string
Description string
Priority *struct {
Id string
Name string
}
IssueType struct {
Name string
IconURL string
}
Resolution *struct {
Id string
}
Status struct {
Id string
}
}
}
User struct {
Name string
AvatarUrls map[string]string
DisplayName string
}
Comment struct {
Body string
}
ChangeLog struct {
Items []struct {
FromString string
ToString string
Field string
}
}
}
// Returns the text to be placed in the resulting post or an empty string if nothing should be
// posted.
func (w *Webhook) SlackAttachment() (*model.SlackAttachment, error) {
switch w.WebhookEvent {
case "jira:issue_created":
case "jira:issue_updated":
isResolutionChange := false
for _, change := range w.ChangeLog.Items {
if change.Field == "resolution" {
isResolutionChange = (change.FromString == "") != (change.ToString == "")
break
}
}
if !isResolutionChange {
return nil, nil
}
case "jira:issue_deleted":
if w.Issue.Fields.Resolution != nil {
return nil, nil
}
default:
return nil, nil
}
pretext, err := w.renderText("" +
"{{.User.DisplayName}} {{.Verb}} {{.Issue.Fields.IssueType.Name}} " +
"[{{.Issue.Key}}]({{.JIRAURL}}/browse/{{.Issue.Key}})" +
"")
if err != nil {
return nil, err
}
text, err := w.renderText("" +
"[{{.Issue.Fields.Summary}}]({{.JIRAURL}}/browse/{{.Issue.Key}})" +
"{{if eq .WebhookEvent \"jira:issue_created\"}}{{if ne .Issue.Fields.Description \"\"}}" +
"{{if len .Issue.Fields.Description | lt 3000}}" +
"\n\n{{printf \"%.3000s\" .Issue.Fields.Description}}..." +
"{{else}}" +
"\n\n{{.Issue.Fields.Description}}" +
"{{end}}" +
"{{end}}{{end}}" +
"")
if err != nil {
return nil, err
}
var fields []*model.SlackAttachmentField
if w.WebhookEvent == "jira:issue_created" {
if w.Issue.Fields.Assignee != nil {
fields = append(fields, &model.SlackAttachmentField{
Title: "Assignee",
Value: w.Issue.Fields.Assignee.DisplayName,
Short: true,
})
}
if w.Issue.Fields.Priority != nil {
fields = append(fields, &model.SlackAttachmentField{
Title: "Priority",
Value: w.Issue.Fields.Priority.Name,
Short: true,
})
}
}
return &model.SlackAttachment{
Fallback: pretext,
Color: "#95b7d0",
Pretext: pretext,
Text: text,
Fields: fields,
}, nil
}
func (w *Webhook) JIRAURL() string {
pos := strings.LastIndex(w.Issue.Self, "/rest/api")
if pos < 0 {
return ""
}
return w.Issue.Self[:pos]
}
func (w *Webhook) renderText(tplBody string) (string, error) {
verb := strings.TrimPrefix(w.WebhookEvent, "jira:issue_")
if w.WebhookEvent == "jira:issue_updated" {
for _, change := range w.ChangeLog.Items {
if change.Field == "resolution" {
if change.ToString == "" && change.FromString != "" {
verb = "reopened"
} else if change.ToString != "" && change.FromString == "" {
verb = "resolved"
}
break
}
}
}
tpl, err := template.New("post").Parse(tplBody)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, struct {
*Webhook
JIRAURL string
Verb string
}{
Webhook: w,
JIRAURL: w.JIRAURL(),
Verb: verb,
}); err != nil {
return "", err
}
return buf.String(), nil
}

View File

@@ -1,16 +0,0 @@
package jira
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWebhookJIRAURL(t *testing.T) {
var w Webhook
w.Issue.Self = "http://localhost:8080/rest/api/2/issue/10006"
assert.Equal(t, "http://localhost:8080", w.JIRAURL())
w.Issue.Self = "http://localhost:8080/foo/bar/rest/api/2/issue/10006"
assert.Equal(t, "http://localhost:8080/foo/bar", w.JIRAURL())
}

View File

@@ -3558,6 +3558,10 @@
"id": "app.notification.subject.notification.full",
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.plugin.prepackaged.app_error",
"translation": "Prepackaged plugins cannot be modified."
},
{
"id": "app.plugin.key_value.set.app_error",
"translation": "Unable to set key value. See detailed error for more information."

View File

@@ -31,9 +31,6 @@ type PluginOption struct {
}
type PluginSetting struct {
// The key that the setting will be assigned to in the configuration file.
Key string `json:"key" yaml:"key"`
// The display name for the setting.
DisplayName string `json:"display_name" yaml:"display_name"`
@@ -79,8 +76,8 @@ type PluginSettingsSchema struct {
// Optional text to display below the settings.
Footer string `json:"footer" yaml:"footer"`
// A list of setting definitions.
Settings []*PluginSetting `json:"settings" yaml:"settings"`
// A mapping of setting keys to schema definitions.
Settings map[string]*PluginSetting `json:"settings" yaml:"settings"`
}
// The plugin manifest defines the metadata required to load and present your plugin. The manifest
@@ -96,7 +93,7 @@ type PluginSettingsSchema struct {
// executable: myplugin
// settings_schema:
// settings:
// - key: enable_extra_thing
// enable_extra_thing:
// type: bool
// display_name: Enable Extra Thing
// help_text: When true, an extra thing will be enabled!
@@ -129,8 +126,6 @@ type Manifest struct {
type ManifestBackend struct {
// The path to your executable binary. This should be relative to the root of your bundle and the
// location of the manifest file.
//
// On Windows, this file must have a ".exe" extension.
Executable string `json:"executable" yaml:"executable"`
}

View File

@@ -70,9 +70,8 @@ func TestManifestUnmarshal(t *testing.T) {
SettingsSchema: &PluginSettingsSchema{
Header: "theheadertext",
Footer: "thefootertext",
Settings: []*PluginSetting{
&PluginSetting{
Key: "thesetting",
Settings: map[string]*PluginSetting{
"thesetting": &PluginSetting{
DisplayName: "thedisplayname",
Type: PLUGIN_CONFIG_TYPE_DROPDOWN,
HelpText: "thehelptext",
@@ -101,7 +100,7 @@ settings_schema:
header: theheadertext
footer: thefootertext
settings:
- key: thesetting
thesetting:
display_name: thedisplayname
type: dropdown
help_text: thehelptext
@@ -126,9 +125,8 @@ settings_schema:
"settings_schema": {
"header": "theheadertext",
"footer": "thefootertext",
"settings": [
{
"key": "thesetting",
"settings": {
"thesetting": {
"display_name": "thedisplayname",
"type": "dropdown",
"help_text": "thehelptext",
@@ -142,7 +140,7 @@ settings_schema:
],
"default": "thedefault"
}
]
}
}
}`), &jsonResult))
assert.Equal(t, expected, jsonResult)
@@ -177,9 +175,8 @@ func TestManifestJson(t *testing.T) {
SettingsSchema: &PluginSettingsSchema{
Header: "theheadertext",
Footer: "thefootertext",
Settings: []*PluginSetting{
&PluginSetting{
Key: "thesetting",
Settings: map[string]*PluginSetting{
"thesetting": &PluginSetting{
DisplayName: "thedisplayname",
Type: PLUGIN_CONFIG_TYPE_DROPDOWN,
HelpText: "thehelptext",
@@ -242,9 +239,8 @@ func TestManifestClientManifest(t *testing.T) {
SettingsSchema: &PluginSettingsSchema{
Header: "theheadertext",
Footer: "thefootertext",
Settings: []*PluginSetting{
&PluginSetting{
Key: "thesetting",
Settings: map[string]*PluginSetting{
"thesetting": &PluginSetting{
DisplayName: "thedisplayname",
Type: PLUGIN_CONFIG_TYPE_DROPDOWN,
HelpText: "thehelptext",

View File

@@ -8,9 +8,14 @@ import (
"io"
)
type PluginInfo struct {
Manifest
Prepackaged bool `json:"prepackaged"`
}
type PluginsResponse struct {
Active []*Manifest `json:"active"`
Inactive []*Manifest `json:"inactive"`
Active []*PluginInfo `json:"active"`
Inactive []*PluginInfo `json:"inactive"`
}
func (m *PluginsResponse) ToJson() string {

View File

@@ -19,8 +19,8 @@ func TestPluginsResponseJson(t *testing.T) {
}
response := &PluginsResponse{
Active: []*Manifest{manifest},
Inactive: []*Manifest{},
Active: []*PluginInfo{{Manifest: *manifest}},
Inactive: []*PluginInfo{},
}
json := response.ToJson()

View File

@@ -5,7 +5,7 @@ count=0
for fileType in GoFiles; do
for file in `go list -f $'{{range .GoFiles}}{{$.Dir}}/{{.}}\n{{end}}' "$@"`; do
case $file in
*/utils/lru.go|*/store/storetest/mocks/*)
*/utils/lru.go|*/store/storetest/mocks/*|*/app/plugin/jira/plugin_*)
# Third-party, doesn't require a header.
;;
*)