<|start_filename|>pkg/plugin/aggregation/run.go<|end_filename|>
/*
Copyright 2018 Heptio Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package aggregation

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/http"
	"time"

	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/vmware-tanzu/sonobuoy/pkg/backplane/ca"
	"github.com/vmware-tanzu/sonobuoy/pkg/plugin"
	"github.com/vmware-tanzu/sonobuoy/pkg/plugin/driver/utils"
	sonotime "github.com/vmware-tanzu/sonobuoy/pkg/time"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait"

	"k8s.io/client-go/kubernetes"
)

const (
	annotationUpdateFreq    = 5 * time.Second
	jitterFactor            = 1.2
	timeoutMonitoringOffset = 10 * time.Second

	// pollingInterval is the time between polls when monitoring a plugin.
	pollingInterval = 10 * time.Second
)

// timeoutErr is a wrapper around an error that will satisfy the timeout interface
type timeoutErr struct {
	e error
}

func (t *timeoutErr) Timeout() bool { return true }
func (t *timeoutErr) Error() string { return t.e.Error() }

// Run runs an aggregation server and gathers results, in accordance with the
// given sonobuoy configuration.
//
// Basic workflow:
//
// 1. Create the aggregator object (`aggr`) to keep track of results
// 2. Launch the HTTP server with the aggr's HandleHTTPResult function as the
//    callback
// 3. Run all the aggregation plugins, monitoring each one in a goroutine,
//    configuring them to send failure results through a shared channel
// 4. Hook the shared monitoring channel up to aggr's IngestResults() function
// 5. Block until aggr shows all results accounted for (results come in through
//    the HTTP callback), stopping the HTTP server on completion
func Run(client kubernetes.Interface, plugins []plugin.Interface, cfg plugin.AggregationConfig, progressPort, namespace, outdir string) error {
	// Construct a list of things we'll need to dispatch
	if len(plugins) == 0 {
		logrus.Info("Skipping host data gathering: no plugins defined")
		return nil
	}

	// Get a list of nodes so the plugins can properly estimate what
	// results they'll give.
	// TODO: there are other places that iterate through the CoreV1.Nodes API
	// call, we should only do this in one place and cache it.
	nodes, err := client.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		return errors.WithStack(err)
	}

	// Find out what results we should expect for each of the plugins
	var expectedResults []plugin.ExpectedResult
	for _, p := range plugins {
		expectedResults = append(expectedResults, p.ExpectedResults(nodes.Items)...)
	}

	auth, err := ca.NewAuthority()
	if err != nil {
		return errors.Wrap(err, "couldn't make new certificate authority for plugin aggregator")
	}

	logrus.Infof("Starting server Expected Results: %v", expectedResults)

	// 1. Await results from each plugin
	aggr := NewAggregator(outdir+"/plugins", expectedResults)
	doneAggr := make(chan bool, 1)
	stopWaitCh := make(chan bool, 1)

	go func() {
		aggr.Wait(stopWaitCh)
		doneAggr <- true
	}()

	// AdvertiseAddress often has a port, split this off if so
	advertiseAddress := cfg.AdvertiseAddress
	if host, _, err := net.SplitHostPort(cfg.AdvertiseAddress); err == nil {
		advertiseAddress = host
	}

	tlsCfg, err := auth.MakeServerConfig(advertiseAddress)
	if err != nil {
		return errors.Wrap(err, "couldn't get a server certificate")
	}

	// 2. Launch the aggregation servers
	srv := &http.Server{
		Addr:      fmt.Sprintf("%s:%d", cfg.BindAddress, cfg.BindPort),
		Handler:   NewHandler(aggr.HandleHTTPResult, aggr.HandleHTTPProgressUpdate),
		TLSConfig: tlsCfg,
	}

	doneServ := make(chan error)
	go func() {
		logrus.WithFields(logrus.Fields{
			"address": cfg.BindAddress,
			"port":    cfg.BindPort,
		}).Info("Starting aggregation server")
		doneServ <- srv.ListenAndServeTLS("", "")
	}()
	defer func() {
		logrus.Info("Shutting down aggregation server")
		srv.Close()
	}()

	updater := newUpdater(expectedResults, namespace, client)
	ctxAnnotation, cancelAnnotation := context.WithCancel(context.TODO())
	pluginsdone := false
	defer func() {
		if !pluginsdone {
			logrus.Info("Last update to annotations on exit")
			// This is the async exit cleanup function.
			// 1. Stop the annotation updater
			cancelAnnotation()
			// 2. Try one last time to get an update out on exit
			if err := updater.Annotate(aggr.Results, aggr.LatestProgressUpdates); err != nil {
				logrus.WithError(err).Info("couldn't annotate sonobuoy pod")
			}
		}
	}()

	// 3. Regularly annotate the Aggregator pod with the current run status
	logrus.Info("Starting annotation update routine")
	go func() {
		wait.JitterUntil(func() {
			pluginsdone = aggr.isComplete()
			if err := updater.Annotate(aggr.Results, aggr.LatestProgressUpdates); err != nil {
				logrus.WithError(err).Info("couldn't annotate sonobuoy pod")
			}
			if pluginsdone {
				logrus.Info("All plugins have completed, status has been updated")
				cancelAnnotation()
			}
		}, annotationUpdateFreq, jitterFactor, true, ctxAnnotation.Done())
	}()

	// 4. Launch each plugin, to dispatch workers which submit the results back
	certs := map[string]*tls.Certificate{}
	for _, p := range plugins {
		cert, err := auth.ClientKeyPair(p.GetName())
		if err != nil {
			return errors.Wrapf(err, "couldn't make certificate for plugin %v", p.GetName())
		}
		certs[p.GetName()] = cert
	}

	// Get a reference to the aggregator pod to set up owner references correctly for each started plugin
	aggregatorPod, err := GetAggregatorPod(client, namespace)
	if err != nil {
		return errors.Wrapf(err, "couldn't get aggregator pod")
	}

	for _, p := range plugins {
		logrus.WithField("plugin", p.GetName()).Info("Running plugin")
		go aggr.RunAndMonitorPlugin(context.Background(), time.Duration(cfg.TimeoutSeconds)*time.Second, p, client, nodes.Items, cfg.AdvertiseAddress, certs[p.GetName()], aggregatorPod, progressPort)
	}

	// 6. Wait for aggr to show that all results are accounted for
	for {
		select {
		case err := <-doneServ:
			logrus.Tracef("Server stopped listening and returned error: %v", err)
			stopWaitCh <- true
			return err
		case <-doneAggr:
			logrus.Trace("Aggregator done")
			if aggr.hadTimeout() {
				return &timeoutErr{errors.New("timeout occurred when waiting for plugin results")}
			}
			return nil
		}
	}
}

// Cleanup calls cleanup on all plugins
func Cleanup(client kubernetes.Interface, plugins []plugin.Interface) {
	// Cleanup after each plugin unless cleanup is explicitly skipped
	for _, p := range plugins {
		if !p.SkipCleanup() {
			logrus.
				WithField("plugin", p.GetName()).
				Tracef("Invoking plugin cleanup")
			p.Cleanup(client)
		} else {
			logrus.
				WithField("plugin", p.GetName()).
				Tracef("Skipping plugin cleanup as specified")
		}
	}
}

// RunAndMonitorPlugin will start a plugin then monitor it for errors starting/running.
// Errors detected will be handled by saving an error result in the aggregator.Results.
func (a *Aggregator) RunAndMonitorPlugin(ctx context.Context, timeout time.Duration, p plugin.Interface, client kubernetes.Interface, nodes []corev1.Node, address string, cert *tls.Certificate, aggregatorPod *corev1.Pod, progressPort string) {
	monitorCh := make(chan *plugin.Result, 1)

	// Give the ingestion routine a tad more time to avoid races where the monitor routine, at timeout, tries
	// to return results.
	ctxMonitor, cancelMonitor := context.WithTimeout(ctx, timeout)
	ctxIngest, cancelIngest := context.WithTimeout(ctx, timeout+timeoutMonitoringOffset)

	if err := p.Run(client, address, cert, aggregatorPod, progressPort); err != nil {
		err := errors.Wrapf(err, "error running plugin %v", p.GetName())
		logrus.Error(err)
		monitorCh <- utils.MakeErrorResult(p.GetName(), map[string]interface{}{"error": err.Error()}, "")
	}

	go p.Monitor(ctxMonitor, client, nodes, monitorCh)
	go a.IngestResults(ctxIngest, monitorCh)

	// Control loop; check regularly if we have results or not for this plugin. If results are in,
	// then stop the go routines monitoring the plugin. If the parent context is cancelled, stop monitoring.
	for {
		select {
		case <-ctx.Done():
			cancelMonitor()
			cancelIngest()
			return
		case <-sonotime.After(pollingInterval):
		}

		hasResults := a.pluginHasResults(p)
		if hasResults {
			cancelMonitor()
			cancelIngest()
			return
		}
	}
}

// pluginHasResults returns true if all the expected results for the given plugin
// have already been reported.
func (a *Aggregator) pluginHasResults(p plugin.Interface) bool {
	a.resultsMutex.Lock()
	defer a.resultsMutex.Unlock()

	targetType := p.GetName()
	for expResultID, expResult := range a.ExpectedResults {
		if expResult.ResultType != targetType {
			continue
		}

		if _, ok := a.Results[expResultID]; !ok {
			return false
		}
	}

	return true
}

func (a *Aggregator) hadTimeout() bool {
	a.resultsMutex.Lock()
	defer a.resultsMutex.Unlock()

	for _, r := range a.Results {
		if r.IsTimeout() {
			return true
		}
	}

	return false
}


<|start_filename|>pkg/client/results/item.go<|end_filename|>
/*
Copyright the Sonobuoy contributors 2020

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package results

import (
	"fmt"
	"strings"
)

const (
	// StatusFailed is the key we base junit pass/failure off of and save into
	// our canonical results format.
	StatusFailed = "failed"

	// StatusPassed is the key we base junit pass/failure off of and save into
	// our canonical results format.
	StatusPassed = "passed"

	// StatusSkipped is the key we base junit pass/failure off of and save into
	// our canonical results format.
	StatusSkipped = "skipped"

	// StatusUnknown is the key we fallback to in our canonical results format
	// if another can not be determined.
	StatusUnknown = "unknown"

	// StatusTimeout is the key used when the plugin does not report results within the
	// timeout period. It will be treated as a failure (e.g. its parent will be marked
	// as a failure).
	StatusTimeout = "timeout"

	// PostProcessedResultsFile is the name of the file we create when doing
	// postprocessing on the plugin results.
	PostProcessedResultsFile = "sonobuoy_results.yaml"

	// MetadataFileKey is the key used in an Item's metadata field when the Item is
	// representing the a file summary (and its leaf nodes are individual tests or suites).
	MetadataFileKey = "file"

	// MetadataTypeKey is the key used in an Item's metadata field when describing what type
	// of entry in the tree it is. Currently we just tag summaries, files, and nodes.
	MetadataTypeKey = "type"

	MetadataTypeNode    = "node"
	MetadataTypeFile    = "file"
	MetadataTypeSummary = "summary"

	MetadataDetailsFailure = "failure"
	MetadataDetailsOutput  = "output"
)

// Item is the central format for plugin results. Various plugin
// types can be transformed into this simple format and set at a standard
// location in our results tarball for simplified processing by any consumer.
type Item struct {
	Name     string                 `json:"name" yaml:"name"`
	Status   string                 `json:"status" yaml:"status"`
	Metadata map[string]string      `json:"meta,omitempty" yaml:"meta,omitempty"`
	Details  map[string]interface{} `json:"details,omitempty" yaml:"details,omitempty"`
	Items    []Item                 `json:"items,omitempty" yaml:"items,omitempty"`
}

// Empty returns true if the Item is empty.
func (i Item) Empty() bool {
	if i.Name == "" && i.Status == "" && len(i.Items) == 0 && len(i.Metadata) == 0 {
		return true
	}
	return false
}

// GetSubTreeByName traverses the tree and returns a reference to the
// subtree whose root has the given name.
func (i *Item) GetSubTreeByName(root string) *Item {
	if i == nil {
		return nil
	}

	if root == "" || i.Name == root {
		return i
	}

	if len(i.Items) > 0 {
		for _, v := range i.Items {
			subItem := (&v).GetSubTreeByName(root)
			if subItem != nil {
				return subItem
			}
		}
	}

	return nil
}

// IsTimeoutErr is the snippet of logic that determines whether or not a given Item represents
// a timeout error (i.e. Sonobuoy timed out waiting for results).
func IsTimeoutErr(i Item) bool {
	return strings.Contains(fmt.Sprint(i.Details["error"]), "timeout")
}


<|start_filename|>cmd/sonobuoy/app/gen.go<|end_filename|>
/*
Copyright 2018 Heptio Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package app

import (
	"fmt"
	"io/ioutil"
	"os"
	"time"

	"github.com/vmware-tanzu/sonobuoy/pkg/client"
	"github.com/vmware-tanzu/sonobuoy/pkg/config"
	"github.com/vmware-tanzu/sonobuoy/pkg/errlog"
	imagepkg "github.com/vmware-tanzu/sonobuoy/pkg/image"
	"github.com/vmware-tanzu/sonobuoy/pkg/plugin/manifest"

	"github.com/pkg/errors"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/kubernetes"
)

type genFlags struct {
	sonobuoyConfig     SonobuoyConfig
	rbacMode           RBACMode
	kubecfg            Kubeconfig
	dnsNamespace       string
	dnsPodLabels       []string
	sshKeyPath         string
	k8sVersion         imagepkg.ConformanceImageVersion
	showDefaultPodSpec bool

	// These values are mainly for `run` but we want `gen` to support all the same
	// flags so you can just swap out gen/run.
	skipPreflight bool
	wait          int
	waitOutput    WaitOutputMode
	genFile       string

	// plugins will keep a list of the plugins we want. Custom type for
	// flag support.
	plugins pluginList

	// pluginEnvs is a set of overrides for plugin env vars. Provided out of band
	// from the list of plugins because the e2e/systemd plugins are dynamically
	// generated at the current time and so we can't manipulate those objects.
	pluginEnvs PluginEnvVars

	// nodeSelectors, if set, will be applied to the aggregator allowing it to be
	// schedule on specific nodes.
	nodeSelectors NodeSelectors

	pluginTransforms map[string][]func(*manifest.Manifest) error
}

func GenFlagSet(cfg *genFlags, rbac RBACMode) *pflag.FlagSet {
	genset := pflag.NewFlagSet("generate", pflag.ExitOnError)
	cfg.sonobuoyConfig.Config = *config.New()
	cfg.pluginTransforms = map[string][]func(*manifest.Manifest) error{}
	AddSonobuoyConfigFlag(&cfg.sonobuoyConfig, genset)
	AddKubeconfigFlag(&cfg.kubecfg, genset)
	AddRBACModeFlags(&cfg.rbacMode, genset, rbac)
	AddImagePullPolicyFlag(&cfg.sonobuoyConfig.ImagePullPolicy, genset)
	AddTimeoutFlag(&cfg.sonobuoyConfig.Aggregation.TimeoutSeconds, genset)
	AddShowDefaultPodSpecFlag(&cfg.showDefaultPodSpec, genset)

	AddNamespaceFlag(&cfg.sonobuoyConfig.Namespace, genset)
	AddDNSNamespaceFlag(&cfg.dnsNamespace, genset)
	AddDNSPodLabelsFlag(&cfg.dnsPodLabels, genset)
	AddSonobuoyImage(&cfg.sonobuoyConfig.WorkerImage, genset)
	AddSSHKeyPathFlag(&cfg.sshKeyPath, &cfg.pluginTransforms, genset)

	AddPluginSetFlag(&cfg.plugins, genset)
	AddPluginEnvFlag(&cfg.pluginEnvs, genset)
	AddLegacyE2EFlags(&cfg.pluginEnvs, &cfg.pluginTransforms, genset)

	AddNodeSelectorsFlag(&cfg.nodeSelectors, genset)

	AddKubeConformanceImageVersion(&cfg.k8sVersion, &cfg.pluginTransforms, genset)
	AddKubernetesVersionFlag(&cfg.k8sVersion, &cfg.pluginTransforms, genset)

	AddPluginImage(&cfg.pluginTransforms, genset)
	AddKubeConformanceImage(&cfg.pluginTransforms, genset)
	AddSystemdLogsImage(&cfg.pluginTransforms, genset)

	// This bit of logic enables the plugin management features of Sonobuoy which are currently
	// in development. By placing it here we will impact any command that wants a full set of flags
	// like the `gen` command. Meanwhile, if you just use an empty `pluginList` then caching will
	// be disabled.
	if featureEnabled(FeaturePluginInstallation) {
		cfg.plugins.InstallDir = getPluginCacheLocation()
	}

	AddSecurityContextMode(&cfg.sonobuoyConfig.SecurityContextMode, genset)

	AddSkipPreflightFlag(&cfg.skipPreflight, genset)
	AddRunWaitFlag(&cfg.wait, genset)
	if featureEnabled(FeatureWaitOutputProgressByDefault) {
		AddWaitOutputFlag(&cfg.waitOutput, genset, ProgressOutputMode)
	} else {
		AddWaitOutputFlag(&cfg.waitOutput, genset, SilentOutputMode)
	}

	genset.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 genset
}

func (g *genFlags) Config() (*client.GenConfig, error) {
	if len(g.plugins.DynamicPlugins) == 0 && len(g.plugins.StaticPlugins) == 0 {
		g.plugins.DynamicPlugins = []string{e2ePlugin, systemdLogsPlugin}
	}

	// In some configurations, the kube client isn't actually needed for correct executation
	// Therefore, delay reporting the error until we're sure we need the client
	kubeclient, kubeError := getClient(&g.kubecfg)

	// the Enabled and Disabled modes of rbacmode don't need the client, so kubeclient can be nil.
	// if kubeclient is needed, ErrRBACNoClient will be returned and that error can be reported back up.
	rbacEnabled, err := g.rbacMode.Enabled(kubeclient)
	if err != nil {
		if errors.Cause(err) == ErrRBACNoClient {
			return nil, errors.Wrap(err, kubeError.Error())
		}
		return nil, err
	}

	var k8sVersion string
	switch g.k8sVersion {
	case "", imagepkg.ConformanceImageVersionAuto, imagepkg.ConformanceImageVersionLatest, imagepkg.ConformanceImageVersionIgnore:
		var discoveryClient discovery.ServerVersionInterface
		if kubeclient != nil {
			discoveryClient = kubeclient.DiscoveryClient
		}

		// `auto` k8s version needs resolution as well as any static plugins which use the
		// variable SONOBUOY_K8S_VERSION. Just check for it all by default but allow skipping
		// errors/resolution via flag.
		_, k8sVersion, err = g.k8sVersion.Get(discoveryClient, imagepkg.DevVersionURL)
		if err != nil {
			if errors.Cause(err) == imagepkg.ErrImageVersionNoClient &&
				g.k8sVersion != imagepkg.ConformanceImageVersionIgnore {
				return nil, errors.Wrap(err, kubeError.Error())
			}
			return nil, err
		}
	default:
		k8sVersion = g.k8sVersion.String()
	}

	return &client.GenConfig{
		Config:             &g.sonobuoyConfig.Config,
		EnableRBAC:         rbacEnabled,
		ImagePullPolicy:    g.sonobuoyConfig.ImagePullPolicy,
		SSHKeyPath:         g.sshKeyPath,
		DynamicPlugins:     g.plugins.DynamicPlugins,
		StaticPlugins:      g.plugins.StaticPlugins,
		PluginEnvOverrides: g.pluginEnvs,
		ShowDefaultPodSpec: g.showDefaultPodSpec,
		NodeSelectors:      g.nodeSelectors,
		KubeVersion:        k8sVersion,
		PluginTransforms:   g.pluginTransforms,
	}, nil
}

func (g *genFlags) RunConfig() (*client.RunConfig, error) {
	runcfg := &client.RunConfig{
		Wait:       time.Duration(g.wait) * time.Minute,
		WaitOutput: g.waitOutput.String(),
		GenFile:    g.genFile,
	}

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

	return runcfg, nil
}

func NewCmdGen() *cobra.Command {
	var genflags genFlags
	var GenCommand = &cobra.Command{
		Use:   "gen",
		Short: "Generates a sonobuoy manifest for submission via kubectl",
		Run:   genManifest(&genflags),
		Args:  cobra.ExactArgs(0),
	}
	GenCommand.Flags().AddFlagSet(GenFlagSet(&genflags, EnabledRBACMode))
	return GenCommand
}

func genManifest(genflags *genFlags) func(cmd *cobra.Command, args []string) {
	return func(cmd *cobra.Command, args []string) {
		if len(genflags.genFile) > 0 {
			b, err := ioutil.ReadFile(genflags.genFile)
			if err != nil {
				errlog.LogError(err)
				os.Exit(1)
			}
			fmt.Println(string(b))
			return
		}

		cfg, err := genflags.Config()
		if err != nil {
			errlog.LogError(err)
			os.Exit(1)
		}

		// Generate does not require any client configuration
		sbc := &client.SonobuoyClient{}

		bytes, err := sbc.GenerateManifest(cfg)
		if err == nil {
			fmt.Printf("%s\n", bytes)
			return
		}
		errlog.LogError(errors.Wrap(err, "error attempting to generate sonobuoy manifest"))
		os.Exit(1)
	}
}

// getClient returns a client if one can be found, and the error attempting to retrieve that client if not.
func getClient(kubeconfig *Kubeconfig) (*kubernetes.Clientset, error) {
	// Usually we don't need a client. But in this case, we _might_ if we're using detect.
	// So pass in nil if we get an error, then display the errors from trying to get a client
	// if it turns out we needed it.
	cfg, err := kubeconfig.Get()
	var client *kubernetes.Clientset

	var kubeError error
	if err == nil {
		client, err = kubernetes.NewForConfig(cfg)
		if err != nil {
			kubeError = err
		}
	} else {
		kubeError = err
	}

	return client, kubeError
}


<|start_filename|>pkg/client/template.go<|end_filename|>
/*
Copyright 2018 Heptio Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package client

import (
	_ "embed"
	"strings"
	"text/template"
)

// TemplateFuncs exports (currently singular) functions to be used inside the template
var (
	templateFuncs = map[string]interface{}{
		"indent": func(i int, input string) string {
			split := strings.Split(input, "\n")
			ident := "\n" + strings.Repeat(" ", i)
			// Don't indent the first line, it's already indented in the template
			return strings.Join(split, ident)
		},
	}

	//go:embed gen.tmpl.yaml
	templateDoc string

	// genManifest is the template for the `sonobuoy gen` output
	genManifest = newTemplate("manifest", templateDoc)
)

// secContextFromMode turns a simple string "mode" into the security context it refers to. Users could
// completely customize this by using 'sonobuoy gen' and editing it, but this provides a fast/easy way
// to switch between common values.
func secContextFromMode(mode string) string {
	// TODO(jschnake): Seems like we should be using an actual object and marshalling it
	// but we get into version issues (at time of writing this fsgroup is a new, beta feature).
	// Just explicitly writing it for now and we can evolve this if other use cases come up.
	switch mode {
	case "none":
		return ""
	case "nonroot":
		return "securityContext:\n    runAsUser: 1000\n    runAsGroup: 3000\n    fsGroup: 2000"
	default:
		return "securityContext:\n    runAsUser: 1000\n    runAsGroup: 3000\n    fsGroup: 2000"
	}
}

// newTemplate declares a new template that already has templateFuncs in scope
func newTemplate(name, tmpl string) *template.Template {
	return template.Must(template.New(name).Funcs(templateFuncs).Parse(tmpl))
}


<|start_filename|>pkg/discovery/pods.go<|end_filename|>
/*
Copyright 2018 Heptio Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package discovery

import (
	"context"
	"io/ioutil"
	"os"
	"path"
	"time"

	"github.com/vmware-tanzu/sonobuoy/pkg/config"

	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
)

const (
	// PodLogsLocation is the location within the results tarball where pod
	// information is stored.
	PodLogsLocation = "podlogs"
)

// mapping to k8s.io/api/core/v1/PodLogOptions.
func getPodLogOptions(cfg *config.Config) *v1.PodLogOptions {
	podLogLimits := &cfg.Limits.PodLogs

	options := &v1.PodLogOptions{
		Previous:     podLogLimits.Previous,
		SinceSeconds: podLogLimits.SinceSeconds,
		SinceTime:    podLogLimits.SinceTime,
		Timestamps:   podLogLimits.Timestamps,
		TailLines:    podLogLimits.TailLines,
		LimitBytes:   podLogLimits.LimitBytes,
	}

	// TODO: investigate using LimitBytes (SizeLimitBytes is deprecated)
	//       and SinceSeconds (TimeLimitDuration is deprecated)
	// Only set values if they have values greater than 0 (as in they user specified).
	limitBytes := podLogLimits.SizeLimitBytes(0)                           //nolint:staticcheck
	sinceSeconds := int64(podLogLimits.TimeLimitDuration(0) / time.Second) //nolint:staticcheck
	if limitBytes > 0 {
		options.LimitBytes = &limitBytes
	}
	if sinceSeconds > 0 {
		options.SinceSeconds = &sinceSeconds
	}

	return options
}

// gatherPodLogs will loop through collecting pod logs and placing them into a directory tree
// If ns is not provided,  meaning candidate pods can come from all namespaces
// visitedPods will eliminate duplicate pods when execute overlapping queries,
// e.g. query by namespaces and query by fieldSelectors
func gatherPodLogs(kubeClient kubernetes.Interface, ns string, opts metav1.ListOptions, cfg *config.Config,
	visitedPods map[string]struct{}) error {

	// 1 - Collect the list of pods
	podlist, err := kubeClient.CoreV1().Pods(ns).List(context.TODO(), opts)
	if err != nil {
		return errors.WithStack(err)
	}

	podLogOptions := getPodLogOptions(cfg)

	// 2 - Foreach pod, dump each of its containers' logs in a tree in the following location:
	//   pods/:podname/logs/:containername.txt
	for _, pod := range podlist.Items {
		if _, ok := visitedPods[string(pod.UID)]; ok {
			logrus.
				WithField("pod.UID", pod.UID).WithField("pod.Name", pod.Name).
				Tracef("Skipping pod pod logs since we have already visited it before")
			continue
		}
		visitedPods[string(pod.UID)] = struct{}{}

		if pod.Status.Phase == v1.PodFailed && pod.Status.Reason == "Evicted" {
			logrus.WithField("podName", pod.Name).Trace("Skipping evicted pod.")
			continue
		}
		for _, container := range pod.Spec.Containers {
			podLogOptions.Container = container.Name
			body, err := kubeClient.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, podLogOptions).DoRaw(context.TODO())
			if err != nil {
				return errors.WithStack(err)
			}
			outdir := path.Join(cfg.OutputDir(), PodLogsLocation, pod.Namespace, pod.Name, "logs")
			if err = os.MkdirAll(outdir, 0755); err != nil {
				return errors.WithStack(err)
			}

			outfile := path.Join(outdir, container.Name) + ".txt"
			if err = ioutil.WriteFile(outfile, body, 0644); err != nil {
				return errors.WithStack(err)
			}
		}
	}

	return nil
}


<|start_filename|>cmd/sonobuoy/app/args.go<|end_filename|>
/*
Copyright 2018 Heptio Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package app

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"

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

	corev1 "k8s.io/api/core/v1"

	"github.com/pkg/errors"
	"github.com/spf13/pflag"
	"github.com/vmware-tanzu/sonobuoy/pkg/config"
)

const (
	namespaceFlag           = "namespace"
	sonobuoyImageFlag       = "sonobuoy-image"
	imagePullPolicyFlag     = "image-pull-policy"
	pluginFlag              = "plugin"
	timeoutFlag             = "timeout"
	waitOutputFlag          = "wait-output"
	customRegistryFlag      = "custom-registry"
	kubeconfig              = "kubeconfig"
	kubecontext             = "context"
	e2eFocusFlag            = "e2e-focus"
	e2eSkipFlag             = "e2e-skip"
	e2eParallelFlag         = "e2e-parallel"
	e2eRegistryConfigFlag   = "e2e-repo-config"
	pluginImageFlag         = "plugin-image"
	filenameFlag            = "filename"
	securityContextModeFlag = "security-context-mode"

	// Quick runs a single E2E test and the systemd log tests.
	Quick string = "quick"

	// NonDisruptiveConformance runs all of the `Conformance` E2E tests which are not marked as disuprtive and the systemd log tests.
	NonDisruptiveConformance string = "non-disruptive-conformance"

	// CertifiedConformance runs all of the `Conformance` E2E tests and the systemd log tests.
	CertifiedConformance string = "certified-conformance"

	// nonDisruptiveSkipList should generally just need to skip disruptive tests since upstream
	// will disallow the other types of tests from being tagged as Conformance. However, in v1.16
	// two disruptive tests were  not marked as such, meaning we needed to specify them here to ensure
	// user workload safety. See https://github.com/kubernetes/kubernetes/issues/82663
	// and https://github.com/kubernetes/kubernetes/issues/82787
	nonDisruptiveSkipList = `\[Disruptive\]|NoExecuteTaintManager`
	conformanceFocus      = `\[Conformance\]`
	quickFocus            = "Pods should be submitted and removed"
)

// AddNamespaceFlag initialises a namespace flag.
func AddNamespaceFlag(str *string, flags *pflag.FlagSet) {
	flags.StringVarP(
		str, namespaceFlag, "n", config.DefaultNamespace,
		"The namespace to run Sonobuoy in. Only one Sonobuoy run can exist per namespace simultaneously.",
	)
}

// AddDNSNamespaceFlag initialises the dns-namespace flag.
// The value of this flag is used during preflight checks to determine which namespace to use
// when looking for the DNS pods.
func AddDNSNamespaceFlag(str *string, flags *pflag.FlagSet) {
	flags.StringVar(
		str, "dns-namespace", config.DefaultDNSNamespace,
		"The namespace to check for DNS pods during preflight checks.",
	)
}

// AddDNSPodLabelsFlag initialises the dns-pod-labels flag.
// The value of this flag is used during preflight checks to determine which labels to use
// when looking for the DNS pods.
func AddDNSPodLabelsFlag(str *[]string, flags *pflag.FlagSet) {
	flags.StringSliceVar(
		str, "dns-pod-labels", config.DefaultDNSPodLabels,
		"The label selectors to use for locating DNS pods during preflight checks. Can be specified multiple times or as a comma-separated list.",
	)
}

// AddSonobuoyImage initialises an image url flag.
func AddSonobuoyImage(image *string, flags *pflag.FlagSet) {
	flags.StringVar(
		image, sonobuoyImageFlag, *image,
		"Container image override for the sonobuoy worker and aggregator.",
	)
}

func AddPluginImage(pluginTransforms *map[string][]func(*manifest.Manifest) error, fs *pflag.FlagSet) {
	fs.Var(
		&pluginImageFlagType{
			transforms: *pluginTransforms,
		},
		pluginImageFlag,
		"Override a plugins image from what is in its definition (e.g. myPlugin:testimage)",
	)
}

// AddKubeConformanceImage initialises the kube-conformance-image flag. Really just a legacy wrapper for --plugin-image=e2e:imageName
func AddKubeConformanceImage(pluginTransforms *map[string][]func(*manifest.Manifest) error, fs *pflag.FlagSet) {
	fs.Var(
		&hardcodedPluginImageFlagType{
			plugin: e2ePlugin,
			underlyingFlag: &pluginImageFlagType{
				transforms: *pluginTransforms,
			},
		},
		"kube-conformance-image",
		"Container image override for the e2e plugin. Shorthand for --plugin-image=e2e:<string>",
	)
}

// AddSystemdLogsImage initialises the systemd-logs-image flag. Really just a legacy wrapper for --plugin-image=systemd-logs:imageName
func AddSystemdLogsImage(pluginTransforms *map[string][]func(*manifest.Manifest) error, fs *pflag.FlagSet) {
	fs.Var(
		&hardcodedPluginImageFlagType{
			plugin: systemdLogsPlugin,
			underlyingFlag: &pluginImageFlagType{
				transforms: *pluginTransforms,
			},
		},
		"systemd-logs-image",
		"Container image override for the systemd-logs plugin. Shorthand for --plugin-image=systemd-logs:<string>",
	)
}

// AddKubeConformanceImageVersion initialises an image version flag.
func AddKubeConformanceImageVersion(imageVersion *image.ConformanceImageVersion, pluginTransforms *map[string][]func(*manifest.Manifest) error, flags *pflag.FlagSet) {
	help := "Use default e2e plugin image, but override the version. "
	help += "Default is 'auto', which will be set to your cluster's version if detected, erroring otherwise."
	help += "You can also choose 'latest' which will find the latest dev image upstream."

	flags.Var(
		&kubernetesVersionLogicFlag{
			raw: imageVersion,
			underlyingFlag: &pluginImageFlagType{
				transforms: *pluginTransforms,
			},
		}, "kube-conformance-image-version", help)
	if err := flags.MarkDeprecated("kube-conformance-image-version", "Use --kubernetes-version instead."); err != nil {
		panic("Failed to setup flags properly")
	}
}

// AddKubeconfigFlag adds a kubeconfig and context flags to the provided command.
func AddKubeconfigFlag(cfg *Kubeconfig, flags *pflag.FlagSet) {
	// The default is the empty string (look in the environment)
	flags.Var(cfg, "kubeconfig", "Path to explicit kubeconfig file.")
	flags.StringVar(&cfg.Context, kubecontext, "", "Context in the kubeconfig to use.")
}

func AddSecurityContextMode(mode *string, flags *pflag.FlagSet) {
	flags.StringVar(
		mode, securityContextModeFlag, "nonroot",
		"Type of security context to use for the aggregator pod. Allowable values are [none, nonroot]",
	)
}

// AddE2ERegistryConfigFlag adds a e2eRegistryConfigFlag flag to the provided command.
func AddE2ERegistryConfigFlag(cfg *string, flags *pflag.FlagSet) {
	flags.StringVar(
		cfg, e2eRegistryConfigFlag, "",
		"Specify a yaml file acting as KUBE_TEST_REPO_LIST, overriding registries for test images. Required when pushing images for the e2e plugin.",
	)
}

// AddCustomRepoFlag adds a custom registry flag to the provided command.
func AddCustomRegistryFlag(cfg *string, flags *pflag.FlagSet) {
	flags.StringVar(
		cfg, customRegistryFlag, "",
		"Specify a registry to override the Sonobuoy and Plugin image registries.",
	)
}

// AddSonobuoyConfigFlag adds a SonobuoyConfig flag to the provided command.
func AddSonobuoyConfigFlag(cfg *SonobuoyConfig, flags *pflag.FlagSet) {
	flags.Var(
		cfg, "config",
		"Path to a sonobuoy configuration JSON file.",
	)
}

// AddLegacyE2EFlags is a way to add flags which target the e2e plugin specifically
// by leveraging the existing flags. They typically wrap other fields (like the env var
// overrides) and modify those.
func AddLegacyE2EFlags(env *PluginEnvVars, pluginTransforms *map[string][]func(*manifest.Manifest) error, fs *pflag.FlagSet) {
	m := &Mode{
		env:  env,
		name: "",
	}
	if err := m.Set(NonDisruptiveConformance); err != nil {
		panic("Failed to initial mode flag")
	}
	fs.VarP(
		m, "mode", "m",
		fmt.Sprintf("What mode to run the e2e plugin in. Valid modes are %s.", []string{NonDisruptiveConformance, CertifiedConformance, Quick}),
	)
	fs.Var(
		&envVarModierFlag{
			plugin: e2ePlugin, field: "E2E_FOCUS",
			PluginEnvVars:  *env,
			validationFunc: regexpValidation},
		e2eFocusFlag,
		"Specify the E2E_FOCUS value for the e2e plugin, specifying which tests to run. Shorthand for --plugin-env=e2e.E2E_FOCUS=<string>",
	)
	fs.Var(
		&envVarModierFlag{
			plugin: e2ePlugin, field: "E2E_SKIP",
			PluginEnvVars:  *env,
			validationFunc: regexpValidation},
		e2eSkipFlag,
		"Specify the E2E_SKIP value for the e2e plugin, specifying which tests to skip. Shorthand for --plugin-env=e2e.E2E_SKIP=<string>",
	)
	pf := fs.VarPF(
		&envVarModierFlag{
			plugin:        e2ePlugin,
			field:         "E2E_PARALLEL",
			PluginEnvVars: *env,
		}, e2eParallelFlag, "",
		"Specify the E2E_PARALLEL value for the e2e plugin. Shorthand for --plugin-env=e2e.E2E_PARALLEL=<string>",
	)
	if err := pf.Value.Set("false"); err != nil {
		panic("Failed to initial parallel flag")
	}
	pf.Hidden = true

	// Used to replace the --rerun-failed functionality from the old e2e command.
	fs.Var(
		&rerunFailedFlag{env: env}, "rerun-failed",
		"Read the given tarball and set the E2E_FOCUS to target all the failed tests",
	)

	// Used by the container when enabling E2E tests which require SSH.
	fs.Var(
		&envVarModierFlag{plugin: e2ePlugin, field: "KUBE_SSH_USER", PluginEnvVars: *env}, "ssh-user",
		"SSH user for ssh-key. Required if running e2e plugin with certain tests that require SSH access to nodes.",
	)

	fs.Var(
		&e2eRepoFlag{
			plugin:     e2ePlugin,
			transforms: *pluginTransforms,
		}, e2eRegistryConfigFlag,
		"Specify a yaml file acting as KUBE_TEST_REPO_LIST, overriding registries for test images.",
	)
}

// AddRBACModeFlags adds an E2E Argument with the provided default.
func AddRBACModeFlags(mode *RBACMode, flags *pflag.FlagSet, defaultMode RBACMode) {
	*mode = defaultMode // default
	flags.Var(
		mode, "rbac",
		// Doesn't use the map in app.rbacModeMap to preserve order so we can add an explanation for detect.
		"Whether to enable RBAC on Sonobuoy. Valid modes are Enable, Disable, and Detect (query the server to see whether to enable RBAC).",
	)
}

// AddSkipPreflightFlag adds a boolean flag to skip preflight checks.
func AddSkipPreflightFlag(flag *bool, flags *pflag.FlagSet) {
	flags.BoolVar(
		flag, "skip-preflight", false,
		"If true, skip all checks before starting the sonobuoy run.",
	)
}

// AddDeleteAllFlag adds a boolean flag for deleting everything (including E2E tests).
func AddDeleteAllFlag(flag *bool, flags *pflag.FlagSet) {
	flags.BoolVar(
		flag, "all", false,
		"In addition to deleting Sonobuoy namespaces, also clean up dangling e2e namespaces (those with 'e2e-framework' and 'e2e-run' labels).",
	)
}

// AddDeleteWaitFlag adds a boolean flag for waiting for the delete process to complete.
func AddDeleteWaitFlag(flag *int, flags *pflag.FlagSet) {
	flags.IntVar(
		flag, "wait", 0,
		"Wait for resources to be deleted before completing. 0 indicates do not wait. By providing --wait the default is to wait up to 1 hour.",
	)
	flags.Lookup("wait").NoOptDefVal = "60"
}

// AddRunWaitFlag adds an int flag for waiting for the entire run to finish.
func AddRunWaitFlag(flag *int, flags *pflag.FlagSet) {
	flags.IntVar(
		flag, "wait", 0,
		"How long (in minutes) for the CLI to wait for sonobuoy run to be completed or fail, where 0 indicates do not wait. If specified, the default wait time is 1 day.",
	)
	flags.Lookup("wait").NoOptDefVal = "1440"
}

// AddTimeoutFlag adds an int flag for waiting for the entire run to finish.
func AddTimeoutFlag(flag *int, flags *pflag.FlagSet) {
	flags.IntVar(
		flag, timeoutFlag, config.DefaultAggregationServerTimeoutSeconds,
		"How long (in seconds) Sonobuoy aggregator will wait for plugins to complete before exiting. 0 indicates no timeout.",
	)
}

// AddShowDefaultPodSpecFlag adds an bool flag for determining whether or not to include the default pod spec
// used by Sonobuoy in the output
func AddShowDefaultPodSpecFlag(flag *bool, flags *pflag.FlagSet) {
	flags.BoolVar(
		flag, "show-default-podspec", false,
		"If true, include the default pod spec used for plugins in the output.",
	)
}

// AddWaitOutputFlag adds a flag for spinner when wait flag is set for Sonobuoy operations.
func AddWaitOutputFlag(mode *WaitOutputMode, flags *pflag.FlagSet, defaultMode WaitOutputMode) {
	*mode = defaultMode
	flags.Var(
		mode, waitOutputFlag,
		"Specify the type of output Sonobuoy should produce when --wait is used. Valid modes are silent, spinner, or progress")
}

// AddImagePullPolicyFlag adds a boolean flag for deleting everything (including E2E tests).
func AddImagePullPolicyFlag(policy *string, flags *pflag.FlagSet) {
	flags.StringVar(
		policy, imagePullPolicyFlag, config.DefaultSonobuoyPullPolicy,
		fmt.Sprintf("Set the ImagePullPolicy for the Sonobuoy image and all plugins. Valid options are %s.", strings.Join(ValidPullPolicies(), ", ")),
	)
}

// AddSSHKeyPathFlag initialises an SSH key path flag. The SSH key is uploaded
// as a secret and used in the containers to enable running of E2E tests which
// require SSH keys to be present.
func AddSSHKeyPathFlag(path *string, pluginTransforms *map[string][]func(*manifest.Manifest) error, flags *pflag.FlagSet) {
	flags.Var(
		&sshPathFlag{
			filename:   path,
			transforms: *pluginTransforms,
		}, "ssh-key",
		"Path to the private key enabling SSH to cluster nodes. May be required by some tests from the e2e plugin.",
	)
}

// 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 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
// to run and loads them from local files if necessary.
func AddPluginEnvFlag(p *PluginEnvVars, flags *pflag.FlagSet) {
	flags.Var(p, "plugin-env", "Set env vars on plugins. Values can be given multiple times and are in the form plugin.env=value")
}

// AddPluginListFlag adds the flag to keep track of which built-in plugins to use.
func AddPluginListFlag(p *[]string, flags *pflag.FlagSet) {
	flags.StringSliceVarP(p, "plugin", "p", []string{"e2e", "systemd-logs"}, "Describe which plugin's images to interact with (Valid plugins are 'e2e', 'systemd-logs').")
}

// AddKubernetesVersionFlag initialises an image version flag.
func AddKubernetesVersionFlag(imageVersion *image.ConformanceImageVersion, pluginTransforms *map[string][]func(*manifest.Manifest) error, flags *pflag.FlagSet) {
	help := "Use default E2E image, but override the version. "
	help += "Default is 'auto', which will be set to your cluster's version if detected, erroring otherwise. "
	help += "'ignore' will try version resolution but ignore errors. "
	help += "'latest' will find the latest dev image/version upstream."

	flags.Var(
		&kubernetesVersionLogicFlag{
			raw: imageVersion,
			underlyingFlag: &pluginImageFlagType{
				transforms: *pluginTransforms,
			},
		}, "kubernetes-version", help)
}

// AddShortFlag adds a boolean flag to just print the Sonobuoy version and
// nothing else. Useful in scripts.
func AddShortFlag(flag *bool, flags *pflag.FlagSet) {
	flags.BoolVar(
		flag, "short", false,
		"If true, prints just the Sonobuoy version.",
	)
}

// AddDryRunFlag adds a boolean flag to perform a dry-run of image operations.
func AddDryRunFlag(flag *bool, flags *pflag.FlagSet) {
	flags.BoolVar(
		flag, "dry-run", false,
		"If true, only print the image operations that would be performed.",
	)
}

// AddNodeSelectorFlag adds the flag for gen/run which keeps track of node selectors
// to add to the aggregator. Allows running of the aggregator on Windows nodes.
func AddNodeSelectorsFlag(p *NodeSelectors, flags *pflag.FlagSet) {
	flags.Var(p, "aggregator-node-selector", "Node selectors to add to the aggregator. Values can be given multiple times and are in the form key:value")
}

// AddExtractFlag adds a boolean flag to extract results instead of just downloading the tarball.
func AddExtractFlag(flag *bool, flags *pflag.FlagSet) {
	flags.BoolVarP(
		flag, "extract", "x", false,
		"If true, extracts the results instead of just downloading the results tarball.",
	)
}

// AddFilename initialises a namespace flag.
func AddFilenameFlag(str *string, flags *pflag.FlagSet) {
	flags.StringVarP(
		str, filenameFlag, "f", "",
		"Specify the name of the downloaded file. If empty, defaults to the name of the tarball in the pod.",
	)
}

// Used if we're just setting the given string as the value; focus and skip need
// regexp validation first.
type envVarModierFlag struct {
	plugin, field string
	PluginEnvVars
	validationFunc func(string) error
}

func (i *envVarModierFlag) String() string {
	if i != nil && (i.PluginEnvVars)[i.plugin] != nil {
		return (i.PluginEnvVars)[i.plugin][i.field]
	}
	return ""
}
func (i *envVarModierFlag) Type() string { return "envModifier" }
func (i *envVarModierFlag) Set(str string) error {
	if i.validationFunc != nil {
		if err := i.validationFunc(str); err != nil {
			return err
		}
	}
	return (i.PluginEnvVars).Set(fmt.Sprintf("%v.%v=%v", i.plugin, i.field, str))
}

func regexpValidation(s string) error {
	if _, err := regexp.Compile(s); err != nil {
		return errors.Wrapf(err, "flag value %q fails regexp validation", s)
	}
	return nil
}

// Mode represents the sonobuoy configuration for a given mode.
type Mode struct {
	name string
	env  *PluginEnvVars
}

func (m *Mode) String() string { return m.name }
func (m *Mode) Type() string   { return "Mode" }
func (m *Mode) Set(str string) error {
	lcase := strings.ToLower(str)
	switch lcase {
	case NonDisruptiveConformance:
		if err := m.env.Set(fmt.Sprintf("e2e.E2E_FOCUS=%v", conformanceFocus)); err != nil {
			return fmt.Errorf("failed to set flag with value %v", str)
		}
		if err := m.env.Set(fmt.Sprintf("e2e.E2E_SKIP=%v", nonDisruptiveSkipList)); err != nil {
			return fmt.Errorf("failed to set flag with value %v", str)
		}
	case Quick:
		if err := m.env.Set(fmt.Sprintf("e2e.E2E_FOCUS=%v", quickFocus)); err != nil {
			return fmt.Errorf("failed to set flag with value %v", str)
		}
		if err := m.env.Set("e2e.E2E_SKIP"); err != nil {
			return fmt.Errorf("failed to set flag with value %v", str)
		}
	case CertifiedConformance:
		if err := m.env.Set(fmt.Sprintf("e2e.E2E_FOCUS=%v", conformanceFocus)); err != nil {
			return fmt.Errorf("failed to set flag with value %v", str)
		}
		if err := m.env.Set("e2e.E2E_SKIP"); err != nil {
			return fmt.Errorf("failed to set flag with value %v", str)
		}
	default:
		return fmt.Errorf("unknown mode %v", lcase)
	}
	m.name = lcase
	return nil
}

type e2eRepoFlag struct {
	plugin   string
	filename string

	transforms map[string][]func(*manifest.Manifest) error

	// Value to put in the configmap as the filename. Defaults to filename.
	filenameOverride string
}

func (f *e2eRepoFlag) String() string { return f.filename }
func (f *e2eRepoFlag) Type() string   { return "yaml-filepath" }
func (f *e2eRepoFlag) Set(str string) error {
	f.filename = str
	name := filepath.Base(str)
	if len(f.filenameOverride) > 0 {
		name = f.filenameOverride
	}
	fData, err := os.ReadFile(str)
	if err != nil {
		return errors.Wrapf(err, "failed to read file %q", str)
	}

	f.transforms[f.plugin] = append(f.transforms[f.plugin], func(m *manifest.Manifest) error {
		m.ConfigMap = map[string]string{
			name: string(fData),
		}
		m.Spec.Env = append(m.Spec.Env, corev1.EnvVar{
			Name:  "KUBE_TEST_REPO_LIST",
			Value: fmt.Sprintf("/tmp/sonobuoy/config/%v", name),
		})
		return nil
	})
	return nil
}

// The ssh-key flag needs to store the path to the ssh key but also
// wire up the e2e plugin for using it.
type sshPathFlag struct {
	filename   *string
	transforms map[string][]func(*manifest.Manifest) error
}

func (f *sshPathFlag) String() string { return *f.filename }
func (f *sshPathFlag) Type() string   { return "yamlFile" }
func (f *sshPathFlag) Set(str string) error {
	*f.filename = str

	f.transforms[e2ePlugin] = append(f.transforms[e2ePlugin], func(m *manifest.Manifest) error {
		// Add volume mount, volume, and 3 env vars (for different possible platforms) for SSH capabilities.
		defMode := int32(256)
		m.ExtraVolumes = append(m.ExtraVolumes, manifest.Volume{
			Volume: corev1.Volume{
				Name: "sshkey-vol",
				VolumeSource: corev1.VolumeSource{
					Secret: &corev1.SecretVolumeSource{
						SecretName:  "ssh-key",
						DefaultMode: &defMode,
					},
				},
			},
		})
		m.Spec.Env = append(m.Spec.Env,
			corev1.EnvVar{Name: "LOCAL_SSH_KEY", Value: "id_rsa"},
			corev1.EnvVar{Name: "AWS_SSH_KEY", Value: "/root/.ssh/id_rsa"},
			corev1.EnvVar{Name: "KUBE_SSH_KEY", Value: "id_rsa"},
		)
		m.Spec.VolumeMounts = append(m.Spec.VolumeMounts,
			corev1.VolumeMount{
				ReadOnly:  false,
				Name:      "sshkey-vol",
				MountPath: "/root/.ssh",
			},
		)
		return nil
	})
	return nil
}

type pluginImageFlagType struct {
	overrides  map[string]string
	transforms map[string][]func(*manifest.Manifest) error
}

func (f *pluginImageFlagType) String() string { return fmt.Sprint(f.overrides) }
func (f *pluginImageFlagType) Type() string   { return "plugin:image" }
func (f *pluginImageFlagType) Set(str string) error {
	parts := strings.SplitN(str, ":", 2)
	if len(parts) != 2 {
		return fmt.Errorf("failed to parse plugin image flag, expected format plugin:image and got %v", str)
	}
	if f.overrides == nil {
		f.overrides = map[string]string{}
	}
	f.overrides[parts[0]] = parts[1]

	f.transforms[parts[0]] = append(f.transforms[parts[0]], func(m *manifest.Manifest) error {
		m.Spec.Image = parts[1]
		return nil
	})
	return nil
}

type hardcodedPluginImageFlagType struct {
	underlyingFlag *pluginImageFlagType
	plugin         string
}

func (f *hardcodedPluginImageFlagType) String() string {
	return f.underlyingFlag.String()
}
func (f *hardcodedPluginImageFlagType) Type() string { return "image" }
func (f *hardcodedPluginImageFlagType) Set(str string) error {
	return f.underlyingFlag.Set(fmt.Sprintf("%v:%v", f.plugin, str))
}

type kubernetesVersionLogicFlag struct {
	underlyingFlag *pluginImageFlagType
	raw            *image.ConformanceImageVersion
}

func (f *kubernetesVersionLogicFlag) String() string {
	return f.raw.String()
}
func (f *kubernetesVersionLogicFlag) Type() string { return "string" }
func (f *kubernetesVersionLogicFlag) Set(str string) error {
	if err := f.raw.Set(str); err != nil {
		return err
	}

	var img, ver string
	switch *f.raw {
	case "", image.ConformanceImageVersionIgnore, image.ConformanceImageVersionAuto:
		img = config.UpstreamKubeConformanceImageURL
		ver = "$SONOBUOY_K8S_VERSION"
	case image.ConformanceImageVersionLatest:
		version, err := image.GetLatestDevVersion(image.DevVersionURL)
		if err != nil {
			return errors.Wrap(err, "couldn't identify latest dev image")
		}

		img = image.DevVersionImageURL
		ver = version
	default:
		img = config.UpstreamKubeConformanceImageURL
		ver = f.raw.String()
	}
	return f.underlyingFlag.Set(fmt.Sprintf("%v:%v:%v", e2ePlugin, img, ver))
}

type rerunFailedFlag struct {
	s   string
	env *PluginEnvVars
}

func (f *rerunFailedFlag) String() string { return f.s }
func (f *rerunFailedFlag) Type() string   { return "tar.gz file" }
func (f *rerunFailedFlag) Set(str string) error {
	failedList, err := failedTestsFromTar(str, "e2e")
	if err != nil {
		return err
	}

	if len(failedList) == 0 {
		return fmt.Errorf("no tests failed for plugin %q in tarball %q", "e2e", str)
	}

	val := stringsToRegexp(failedList)
	if err := f.env.Set(fmt.Sprintf("e2e.E2E_FOCUS=%v", val)); err != nil {
		return fmt.Errorf("failed to set flag with value %v", str)
	}

	// Removes E2E_SKIP value.
	if err := f.env.Set("e2e.E2E_SKIP"); err != nil {
		return fmt.Errorf("failed to set flag with value %v", str)
	}
	f.s = val
	return nil
}
