Skip to content

Commit

Permalink
Allow piping input to sonobuoy run (#925)
Browse files Browse the repository at this point in the history
Often a user wants to use `sonbouoy gen` to generate some YAML and
then modify it using tools like jq or yq. We even have this workflow
documented and the user has to, end the end, pipe the data into
`kubectl apply -f`.

`sonobuoy run` is just a shorthand for `sonobuoy gen` + execute with
wait logic; it makes sense to allow loading a file or piping the data
from stdin.

This PR:
 - adds a --file, -f flag to `sonobuoy run`
 - if the value is `-` it will load from stdin, erroring if it is not
connected to a terminal.

It does not add a unit test for this since the `sonobuoy run` logic
is difficult to test in a unit test since it wants to talk to a real cluster.

We may consider adding this to integration tests but it is mostly just input
transformation and leaves little room for error.

Fixes #920

Signed-off-by: John Schnake <jschnake@vmware.com>
  • Loading branch information
johnSchnake committed Oct 1, 2019
1 parent 3a8f83a commit 3290552
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 40 deletions.
1 change: 1 addition & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions cmd/sonobuoy/app/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ import (

"gopkg.in/yaml.v2"

ops "github.com/vmware-tanzu/sonobuoy/pkg/client"
"github.com/vmware-tanzu/sonobuoy/pkg/config"
"github.com/pkg/errors"
"github.com/spf13/pflag"
ops "github.com/vmware-tanzu/sonobuoy/pkg/client"
"github.com/vmware-tanzu/sonobuoy/pkg/config"
v1 "k8s.io/api/core/v1"
)

Expand Down
38 changes: 18 additions & 20 deletions cmd/sonobuoy/app/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ limitations under the License.
package app

import (
"fmt"
"os"
"strings"
"time"

"github.com/pkg/errors"
Expand All @@ -35,6 +33,7 @@ type runFlags struct {
skipPreflight bool
wait int
waitOutput WaitOutputMode
genFile string
}

var runflags runFlags
Expand All @@ -46,20 +45,30 @@ func RunFlagSet(cfg *runFlags) *pflag.FlagSet {
AddSkipPreflightFlag(&cfg.skipPreflight, runset)
AddRunWaitFlag(&cfg.wait, runset)
AddWaitOutputFlag(&cfg.waitOutput, runset, SilentOutputMode)
runset.StringVarP(
&cfg.genFile, "file", "f", "",
"If set, loads the file as if it were the output from sonobuoy gen. Set to `-` to read from stdin.",
)

return runset
}

func (r *runFlags) Config() (*client.RunConfig, error) {
gencfg, err := r.genFlags.Config()
if err != nil {
return nil, err
}
return &client.RunConfig{
GenConfig: *gencfg,
runcfg := &client.RunConfig{
Wait: time.Duration(r.wait) * time.Minute,
WaitOutput: runflags.waitOutput.String(),
}, nil
GenFile: r.genFile,
}

if r.genFile == "" {
gencfg, err := r.genFlags.Config()
if err != nil {
return nil, err
}
runcfg.GenConfig = *gencfg
}

return runcfg, nil
}

func NewCmdRun() *cobra.Command {
Expand All @@ -75,7 +84,6 @@ func NewCmdRun() *cobra.Command {
}

func submitSonobuoyRun(cmd *cobra.Command, args []string) {

sbc, err := getSonobuoyClientFromKubecfg(runflags.kubecfg)
if err != nil {
errlog.LogError(errors.Wrap(err, "could not create sonobuoy client"))
Expand All @@ -88,15 +96,6 @@ func submitSonobuoyRun(cmd *cobra.Command, args []string) {
os.Exit(1)
}

plugins := make([]string, len(runCfg.Config.PluginSelections))
for i, plugin := range runCfg.Config.PluginSelections {
plugins[i] = plugin.Name
}

if len(plugins) > 0 {
fmt.Printf("Running plugins: %v\n", strings.Join(plugins, ", "))
}

if !runflags.skipPreflight {
if errs := sbc.PreflightChecks(&client.PreflightConfig{Namespace: runflags.namespace}); len(errs) > 0 {
errlog.LogError(errors.New("Preflight checks failed"))
Expand All @@ -111,5 +110,4 @@ func submitSonobuoyRun(cmd *cobra.Command, args []string) {
errlog.LogError(errors.Wrap(err, "error attempting to run sonobuoy"))
os.Exit(1)
}

}
12 changes: 9 additions & 3 deletions pkg/client/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import (
"io"
"time"

"github.com/pkg/errors"
"github.com/vmware-tanzu/sonobuoy/pkg/config"
"github.com/vmware-tanzu/sonobuoy/pkg/plugin/aggregation"
"github.com/vmware-tanzu/sonobuoy/pkg/plugin/manifest"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -110,14 +110,20 @@ type E2EConfig struct {
// RunConfig are the input options for running Sonobuoy.
type RunConfig struct {
GenConfig
GenFile string
Wait time.Duration
WaitOutput string
}

// Validate checks the config to determine if it is valid.
func (rc *RunConfig) Validate() error {
err := rc.GenConfig.Validate()
return errors.Wrap(err, "GenConfig validation failed")
// If given a manifest, just load it as-is.
if len(rc.GenFile) == 0 {
err := rc.GenConfig.Validate()
return errors.Wrap(err, "GenConfig validation failed")
}

return nil
}

// DeleteConfig are the input options for cleaning up a Sonobuoy run.
Expand Down
66 changes: 51 additions & 15 deletions pkg/client/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"time"

"github.com/briandowns/spinner"
"github.com/vmware-tanzu/sonobuoy/pkg/plugin/aggregation"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/vmware-tanzu/sonobuoy/pkg/plugin/aggregation"
"golang.org/x/crypto/ssh/terminal"
kubeerror "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -41,22 +44,16 @@ const (
spinnerType int = 14
spinnerDuration time.Duration = 2000 * time.Millisecond
spinnerColor = "red"
)

func (c *SonobuoyClient) Run(cfg *RunConfig) error {
if cfg == nil {
return errors.New("nil RunConfig provided")
}

if err := cfg.Validate(); err != nil {
return errors.Wrap(err, "config validation failed")
}

manifest, err := c.GenerateManifest(&cfg.GenConfig)
if err != nil {
return errors.Wrap(err, "couldn't run invalid manifest")
}
// Special key for when to load manifest from stdin instead of a local file.
stdinFile = "-"
)

// RunManifest is the same as Run(*RunConfig) execpt that the []byte given
// should represent the output from `sonobuoy gen`, a series of YAML resources
// separated by `---`. This method will disregard the RunConfig.GenConfig
// and instead use the given []byte as the manifest.
func (c *SonobuoyClient) RunManifest(cfg *RunConfig, manifest []byte) error {
buf := bytes.NewBuffer(manifest)
d := yaml.NewYAMLOrJSONDecoder(buf, bufferSize)

Expand Down Expand Up @@ -139,6 +136,45 @@ func (c *SonobuoyClient) Run(cfg *RunConfig) error {
return nil
}

// Run will use the given RunConfig to generate YAML for a series of resources and then
// create them in the cluster.
func (c *SonobuoyClient) Run(cfg *RunConfig) error {
if cfg == nil {
return errors.New("nil RunConfig provided")
}

var manifest []byte
var err error
if len(cfg.GenFile) != 0 {
manifest, err = loadManifestFromFile(cfg.GenFile)
if err != nil {
return errors.Wrap(err, "loading manifest")
}
} else {
if err := cfg.Validate(); err != nil {
return errors.Wrap(err, "config validation failed")
}
manifest, err = c.GenerateManifest(&cfg.GenConfig)
if err != nil {
return errors.Wrap(err, "couldn't run invalid manifest")
}
}

return c.RunManifest(cfg, manifest)
}

func loadManifestFromFile(f string) ([]byte, error) {
if f == stdinFile {
if terminal.IsTerminal(int(os.Stdin.Fd())) {
return nil, fmt.Errorf("nothing on stdin to read")
}

return ioutil.ReadAll(os.Stdin)
} else {
return ioutil.ReadFile(f)
}
}

func handleCreateError(name, namespace, resource string, err error) error {
log := logrus.WithFields(logrus.Fields{
"name": name,
Expand Down
10 changes: 10 additions & 0 deletions pkg/client/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ func TestRunInvalidConfig(t *testing.T) {
config: &RunConfig{},
expectedErrorMsg: "config validation failed",
},
{
desc: "Passing a file takes priority over config flags",
config: &RunConfig{
GenFile: "foo.yaml",
},
expectedErrorMsg: "no such file or directory",
},
// NOTE: Running non-failing logic here is not supported at this time due to the fact
// that it tries to actually start executing logic with the dynamic client which
// is nil.
}

c, err := NewSonobuoyClient(nil, nil)
Expand Down

0 comments on commit 3290552

Please sign in to comment.