Skip to content

Commit

Permalink
Allow plugins to be specified via a URL (#989)
Browse files Browse the repository at this point in the history
We currently allow loading plugins from the local file system
but for sharing of plugins it makes sense to allow users to
specify a URL.

Fixes #986

Signed-off-by: John Schnake <[email protected]>
  • Loading branch information
johnSchnake authored Nov 12, 2019
1 parent 31b5728 commit 73cdb50
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 16 deletions.
2 changes: 1 addition & 1 deletion cmd/sonobuoy/app/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ func AddSSHUserFlag(user *string, flags *pflag.FlagSet) {
// AddPluginSetFlag adds the flag for gen/run which keeps track of which plugins
// to run and loads them from local files if necessary.
func AddPluginSetFlag(p *pluginList, flags *pflag.FlagSet) {
flags.VarP(p, "plugin", "p", "Which plugins to run. Can either point to a local file or be one of the known plugins (e2e or systemd-logs). Can be specified multiple times to run multiple plugins.")
flags.VarP(p, "plugin", "p", "Which plugins to run. Can either point to a URL, local file/directory, or be one of the known plugins (e2e or systemd-logs). Can be specified multiple times to run multiple plugins.")
}

// AddPluginEnvFlag adds the flag for gen/run which keeps track of which plugins
Expand Down
71 changes: 57 additions & 14 deletions cmd/sonobuoy/app/pluginList.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,20 @@ limitations under the License.
package app

import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"

"github.com/vmware-tanzu/sonobuoy/pkg/plugin/manifest"

"github.com/pkg/errors"
"github.com/spf13/pflag"

kuberuntime "k8s.io/apimachinery/pkg/runtime"
)

Expand Down Expand Up @@ -74,20 +79,32 @@ func (p *pluginList) Set(str string) error {
case pluginSystemdLogs:
p.DynamicPlugins = append(p.DynamicPlugins, str)
default:
finfo, err := os.Stat(str)
if err != nil {
return errors.Wrapf(err, "unable to stat %q", str)
}

if finfo.IsDir() {
return p.loadPluginsDir(str)
if isURL(str) {
return p.loadSinglePluginFromURL(str)
}
return p.loadSinglePlugin(str)
return p.loadPluginsFromFilesystem(str)
}

return nil
}

func isURL(s string) bool {
u, err := url.Parse(s)
return err == nil && u.Scheme != "" && u.Host != ""
}

func (p *pluginList) loadPluginsFromFilesystem(str string) error {
finfo, err := os.Stat(str)
if err != nil {
return errors.Wrapf(err, "unable to stat %q", str)
}

if finfo.IsDir() {
return p.loadPluginsDir(str)
}
return p.loadSinglePluginFromFile(str)
}

// loadPluginsDir loads every plugin in the given directory. It does not traverse recursively
// into the directory. A plugin must have the '.yaml' extension to be considered.
// It returns the first error encountered and stops processing.
Expand All @@ -99,7 +116,7 @@ func (p *pluginList) loadPluginsDir(dirpath string) error {

for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), fileExtensionYAML) {
if err := p.loadSinglePlugin(filepath.Join(dirpath, file.Name())); err != nil {
if err := p.loadSinglePluginFromFile(filepath.Join(dirpath, file.Name())); err != nil {
return errors.Wrapf(err, "failed to load plugin in file %q", file.Name())
}
}
Expand All @@ -108,16 +125,42 @@ func (p *pluginList) loadPluginsDir(dirpath string) error {
return nil
}

// loadSinglePlugin loads a single plugin located at the given path.
func (p *pluginList) loadSinglePlugin(filepath string) error {
b, err := ioutil.ReadFile(filepath)
// loadSinglePluginFromURL loads a single plugin located at the given path.
func (p *pluginList) loadSinglePluginFromURL(url string) error {
c := http.Client{
Timeout: 10 * time.Second,
}
resp, err := c.Get(url)
if err != nil {
return errors.Wrapf(err, "unable to GET URL %q", url)
}
if resp.StatusCode > 399 {
return fmt.Errorf("unexpected HTTP response code %v", resp.StatusCode)
}

return errors.Wrapf(p.loadSinglePlugin(resp.Body), "loading plugin from URL %q", url)
}

// loadSinglePluginFromFile loads a single plugin located at the given path.
func (p *pluginList) loadSinglePluginFromFile(filepath string) error {
f, err := os.Open(filepath)
if err != nil {
return errors.Wrapf(err, "unable to read file %q", filepath)
}
return errors.Wrapf(p.loadSinglePlugin(f), "loading plugin from file %q", filepath)
}

// loadSinglePlugin reads the data from the reader and loads the plugin.
func (p *pluginList) loadSinglePlugin(r io.ReadCloser) error {
defer r.Close()
b, err := ioutil.ReadAll(r)
if err != nil {
return errors.Wrap(err, "failed to read data for plugin")
}

newPlugin, err := loadManifest(b)
if err != nil {
return errors.Wrapf(err, "failed to load plugin file %q", filepath)
return errors.Wrap(err, "failed to load plugin")
}

p.StaticPlugins = append(p.StaticPlugins, newPlugin)
Expand Down
25 changes: 24 additions & 1 deletion cmd/sonobuoy/app/pluginList_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,29 @@ package app

import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

"github.com/vmware-tanzu/sonobuoy/pkg/plugin/manifest"

"github.com/kylelemons/godebug/pretty"
)

func TestSetPluginList(t *testing.T) {
serveFile := func(filepath string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
b, err := ioutil.ReadFile(filepath)
if err != nil {
t.Fatal(err)
}
w.Write(b)
})
}
ts := httptest.NewServer(serveFile("testdata/goodmanifest.yaml"))
defer ts.Close()

testCases := []struct {
desc string
list pluginList
Expand All @@ -41,7 +57,7 @@ func TestSetPluginList(t *testing.T) {
}, {
desc: "bad manifest",
input: "testdata/badmanifest.yaml",
expectErr: `failed to load plugin file "testdata/badmanifest.yaml": couldn't decode yaml for plugin definition: couldn't get version/kind; json parse error: json: cannot unmarshal string into Go value of type struct { APIVersion string "json:\"apiVersion,omitempty\""; Kind string "json:\"kind,omitempty\"" }`,
expectErr: `loading plugin from file "testdata/badmanifest.yaml": failed to load plugin: couldn't decode yaml for plugin definition: couldn't get version/kind; json parse error: json: cannot unmarshal string into Go value of type struct { APIVersion string "json:\"apiVersion,omitempty\""; Kind string "json:\"kind,omitempty\"" }`,
}, {
desc: "loading e2e",
input: "e2e",
Expand Down Expand Up @@ -78,6 +94,13 @@ func TestSetPluginList(t *testing.T) {
expect: pluginList{
DynamicPlugins: []string{"e2e", "systemd-logs"},
},
}, {
desc: "loading from url",
input: ts.URL,
list: pluginList{},
expect: pluginList{StaticPlugins: []*manifest.Manifest{
{SonobuoyConfig: manifest.SonobuoyConfig{PluginName: "test"}},
}},
},
}
for _, tc := range testCases {
Expand Down

0 comments on commit 73cdb50

Please sign in to comment.