github.com/smartcontractkit/chainlink-testing-framework/libs@v0.0.0-20240227141906-ec710b4eb1a3/k8s/client/client.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"regexp"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/google/uuid"
    15  	"github.com/rs/zerolog/log"
    16  	"k8s.io/apimachinery/pkg/runtime/schema"
    17  	"k8s.io/apimachinery/pkg/runtime/serializer"
    18  	"k8s.io/apimachinery/pkg/types"
    19  	"k8s.io/apimachinery/pkg/util/wait"
    20  	"k8s.io/cli-runtime/pkg/genericclioptions"
    21  	"k8s.io/client-go/kubernetes/scheme"
    22  	"k8s.io/client-go/tools/remotecommand"
    23  	"k8s.io/kubectl/pkg/cmd/cp"
    24  
    25  	v1 "k8s.io/api/core/v1"
    26  	metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/client-go/kubernetes"
    28  	"k8s.io/client-go/rest"
    29  	"k8s.io/client-go/tools/clientcmd"
    30  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    31  )
    32  
    33  const (
    34  	TempDebugManifest    = "tmp-manifest-%s.yaml"
    35  	K8sStatePollInterval = 10 * time.Second
    36  	JobFinalizedTimeout  = 2 * time.Minute
    37  	AppLabel             = "app"
    38  )
    39  
    40  // K8sClient high level k8s client
    41  type K8sClient struct {
    42  	ClientSet  *kubernetes.Clientset
    43  	RESTConfig *rest.Config
    44  }
    45  
    46  // GetLocalK8sDeps get local k8s context config
    47  func GetLocalK8sDeps() (*kubernetes.Clientset, *rest.Config, error) {
    48  	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
    49  	kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
    50  	k8sConfig, err := kubeConfig.ClientConfig()
    51  	if err != nil {
    52  		return nil, nil, err
    53  	}
    54  	k8sClient, err := kubernetes.NewForConfig(k8sConfig)
    55  	if err != nil {
    56  		return nil, nil, err
    57  	}
    58  	return k8sClient, k8sConfig, nil
    59  }
    60  
    61  // NewK8sClient creates a new k8s client with a REST config
    62  func NewK8sClient() (*K8sClient, error) {
    63  	cs, cfg, err := GetLocalK8sDeps()
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	return &K8sClient{
    68  		ClientSet:  cs,
    69  		RESTConfig: cfg,
    70  	}, nil
    71  }
    72  
    73  // ListPods lists pods for a namespace and selector
    74  func (m *K8sClient) ListPods(namespace, selector string) (*v1.PodList, error) {
    75  	pods, err := m.ClientSet.CoreV1().Pods(namespace).List(context.Background(), metaV1.ListOptions{LabelSelector: selector})
    76  	sort.Slice(pods.Items, func(i, j int) bool {
    77  		return pods.Items[i].CreationTimestamp.Before(pods.Items[j].CreationTimestamp.DeepCopy())
    78  	})
    79  	return pods.DeepCopy(), err
    80  }
    81  
    82  // ListPods lists services for a namespace and selector
    83  func (m *K8sClient) ListServices(namespace, selector string) (*v1.ServiceList, error) {
    84  	services, err := m.ClientSet.CoreV1().Services(namespace).List(context.Background(), metaV1.ListOptions{LabelSelector: selector})
    85  	return services.DeepCopy(), err
    86  }
    87  
    88  // ListNamespaces lists k8s namespaces
    89  func (m *K8sClient) ListNamespaces(selector string) (*v1.NamespaceList, error) {
    90  	return m.ClientSet.CoreV1().Namespaces().List(context.Background(), metaV1.ListOptions{LabelSelector: selector})
    91  }
    92  
    93  // AddLabel adds a new label to a group of pods defined by selector
    94  func (m *K8sClient) AddLabel(namespace string, selector string, label string) error {
    95  	podList, err := m.ListPods(namespace, selector)
    96  	if err != nil {
    97  		return err
    98  	}
    99  	l := strings.Split(label, "=")
   100  	if len(l) != 2 {
   101  		return fmt.Errorf("labels must be in format key=value")
   102  	}
   103  	for _, pod := range podList.Items {
   104  		labelPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/labels/%s","value":"%s" }]`, l[0], l[1])
   105  		_, err := m.ClientSet.CoreV1().Pods(namespace).Patch(
   106  			context.Background(),
   107  			pod.GetName(),
   108  			types.JSONPatchType,
   109  			[]byte(labelPatch),
   110  			metaV1.PatchOptions{},
   111  		)
   112  		if err != nil {
   113  			return fmt.Errorf("failed to update labels %s for pod %s err: %w", labelPatch, pod.Name, err)
   114  		}
   115  	}
   116  	log.Debug().Str("Selector", selector).Str("Label", label).Msg("Updated label")
   117  	return nil
   118  }
   119  
   120  func (m *K8sClient) LabelChaosGroup(namespace string, labelPrefix string, startInstance int, endInstance int, group string) error {
   121  	for i := startInstance; i <= endInstance; i++ {
   122  		err := m.AddLabel(namespace, fmt.Sprintf("%s%d", labelPrefix, i), fmt.Sprintf("%s=1", group))
   123  		if err != nil {
   124  			return err
   125  		}
   126  	}
   127  	return nil
   128  }
   129  
   130  func (m *K8sClient) LabelChaosGroupByLabels(namespace string, labels map[string]string, group string) error {
   131  	labelSelector := ""
   132  	for key, value := range labels {
   133  		if labelSelector == "" {
   134  			labelSelector = fmt.Sprintf("%s=%s", key, value)
   135  		} else {
   136  			labelSelector = fmt.Sprintf("%s, %s=%s", labelSelector, key, value)
   137  		}
   138  	}
   139  	podList, err := m.ListPods(namespace, labelSelector)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	for _, pod := range podList.Items {
   144  		err = m.AddPodLabel(namespace, pod, group, "1")
   145  		if err != nil {
   146  			return err
   147  		}
   148  	}
   149  	return nil
   150  }
   151  
   152  // AddPodsLabels adds map of labels to all pods in list
   153  func (m *K8sClient) AddPodsLabels(namespace string, podList *v1.PodList, labels map[string]string) error {
   154  	for _, pod := range podList.Items {
   155  		for k, v := range labels {
   156  			err := m.AddPodLabel(namespace, pod, k, v)
   157  			if err != nil {
   158  				return err
   159  			}
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  // AddPodsAnnotations adds map of annotations to all pods in list
   166  func (m *K8sClient) AddPodsAnnotations(namespace string, podList *v1.PodList, annotations map[string]string) error {
   167  	// when applying annotations the key doesn't like `/` characters here but everywhere else it does
   168  	// replacing it here with ~1
   169  	fixedAnnotations := make(map[string]string)
   170  	for k, v := range annotations {
   171  		fixedAnnotations[strings.ReplaceAll(k, "/", "~1")] = v
   172  	}
   173  	for _, pod := range podList.Items {
   174  		for k, v := range fixedAnnotations {
   175  			err := m.AddPodAnnotation(namespace, pod, k, v)
   176  			if err != nil {
   177  				return err
   178  			}
   179  		}
   180  	}
   181  	return nil
   182  }
   183  
   184  // UniqueLabels gets all unique application labels
   185  func (m *K8sClient) UniqueLabels(namespace string, selector string) ([]string, error) {
   186  	uniqueLabels := make([]string, 0)
   187  	isUnique := make(map[string]bool)
   188  	podList, err := m.ListPods(namespace, selector)
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	for _, p := range podList.Items {
   193  		appLabel := p.Labels[AppLabel]
   194  		if _, ok := isUnique[appLabel]; !ok {
   195  			uniqueLabels = append(uniqueLabels, appLabel)
   196  		}
   197  	}
   198  	log.Info().
   199  		Interface("Apps", uniqueLabels).
   200  		Int("Count", len(uniqueLabels)).
   201  		Msg("Apps found")
   202  	return uniqueLabels, nil
   203  }
   204  
   205  // AddPodLabel adds a label to a pod
   206  func (m *K8sClient) AddPodLabel(namespace string, pod v1.Pod, key, value string) error {
   207  	labelPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/labels/%s","value":"%s" }]`, key, value)
   208  	_, err := m.ClientSet.CoreV1().Pods(namespace).Patch(
   209  		context.Background(), pod.GetName(), types.JSONPatchType, []byte(labelPatch), metaV1.PatchOptions{})
   210  	if err != nil {
   211  		return err
   212  	}
   213  	return nil
   214  }
   215  
   216  // AddPodAnnotation adds an annotation to a pod
   217  func (m *K8sClient) AddPodAnnotation(namespace string, pod v1.Pod, key, value string) error {
   218  	labelPatch := fmt.Sprintf(`[{"op":"add","path":"/metadata/annotations/%s","value":"%s" }]`, key, value)
   219  	_, err := m.ClientSet.CoreV1().Pods(namespace).Patch(
   220  		context.Background(), pod.GetName(), types.JSONPatchType, []byte(labelPatch), metaV1.PatchOptions{})
   221  	if err != nil {
   222  		return err
   223  	}
   224  	return nil
   225  }
   226  
   227  // EnumerateInstances enumerate pods with instance label
   228  func (m *K8sClient) EnumerateInstances(namespace string, selector string) error {
   229  	podList, err := m.ListPods(namespace, selector)
   230  	if err != nil {
   231  		return err
   232  	}
   233  
   234  	for id, pod := range podList.Items {
   235  		// skip if already labeled with instance
   236  		existingLabels := pod.Labels
   237  		_, exists := existingLabels["instance"]
   238  		if exists {
   239  			continue
   240  		}
   241  		if err := m.AddPodLabel(namespace, pod, "instance", strconv.Itoa(id)); err != nil {
   242  			return err
   243  		}
   244  	}
   245  	return nil
   246  }
   247  
   248  // waitForPodsExist waits for all the expected number of pods to exist
   249  func (m *K8sClient) waitForPodsExist(ns string, expectedPodCount int) error {
   250  	log.Debug().Int("ExpectedCount", expectedPodCount).Msg("Waiting for pods to exist")
   251  	var exitErr error
   252  	timeout := 15 * time.Minute
   253  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
   254  	defer cancel()
   255  	if err := wait.PollUntilContextTimeout(ctx, 2*time.Second, timeout, true, func(ctx context.Context) (bool, error) {
   256  		// nolint:contextcheck
   257  		apps, err2 := m.UniqueLabels(ns, AppLabel)
   258  		if err2 != nil {
   259  			exitErr = err2
   260  			return false, nil
   261  		}
   262  		if len(apps) >= expectedPodCount {
   263  			exitErr = nil
   264  			return true, nil
   265  		}
   266  		return false, nil
   267  	}); err != nil {
   268  		return err
   269  	}
   270  
   271  	return exitErr
   272  }
   273  
   274  // WaitPodsReady waits until all pods are ready
   275  func (m *K8sClient) WaitPodsReady(ns string, rcd *ReadyCheckData, expectedPodCount int) error {
   276  	// Wait for pods to exist
   277  	err := m.waitForPodsExist(ns, expectedPodCount)
   278  	if err != nil {
   279  		return err
   280  	}
   281  
   282  	log.Info().Msg("Waiting for pods to be ready")
   283  	ticker := time.NewTicker(K8sStatePollInterval)
   284  	defer ticker.Stop()
   285  	timeout := time.NewTimer(rcd.Timeout)
   286  	readyCount := 0
   287  	defer timeout.Stop()
   288  	for {
   289  		select {
   290  		case <-timeout.C:
   291  			return fmt.Errorf("waitcontainersready, no pods in '%s' with selector '%s' after timeout '%s'",
   292  				ns, rcd.ReadinessProbeCheckSelector, rcd.Timeout)
   293  		case <-ticker.C:
   294  			podList, err := m.ListPods(ns, rcd.ReadinessProbeCheckSelector)
   295  			if err != nil {
   296  				return err
   297  			}
   298  			if len(podList.Items) == 0 && expectedPodCount > 0 {
   299  				log.Debug().
   300  					Str("Namespace", ns).
   301  					Str("Selector", rcd.ReadinessProbeCheckSelector).
   302  					Msg("No pods found with selector")
   303  				continue
   304  			}
   305  			log.Debug().Interface("Pods", podNames(podList)).Msg("Waiting for pods readiness probes")
   306  			allReady := true
   307  			for _, pod := range podList.Items {
   308  				if pod.Status.Phase == "Succeeded" {
   309  					log.Debug().Str("Pod", pod.Name).Msg("Pod is in Succeeded state")
   310  					continue
   311  				} else if pod.Status.Phase != v1.PodRunning {
   312  					log.Debug().Str("Pod", pod.Name).Str("Phase", string(pod.Status.Phase)).Msg("Pod is not running")
   313  					allReady = false
   314  					break
   315  				}
   316  				for _, c := range pod.Status.Conditions {
   317  					if c.Type == v1.ContainersReady && c.Status != "True" {
   318  						log.Debug().Str("Text", c.Message).Msg("Pod condition message")
   319  						allReady = false
   320  					}
   321  				}
   322  			}
   323  
   324  			if allReady {
   325  				readyCount++
   326  				// wait for it to be ready 3 times since there is no good way to know if an old pod
   327  				// was present but not yet decommisiond during a rollout
   328  				// usually there is just a very small blip that we can run into this and this will
   329  				// prevent that from happening
   330  				if readyCount == 3 {
   331  					return nil
   332  				}
   333  			}
   334  		}
   335  	}
   336  }
   337  
   338  // NamespaceExists check if namespace exists
   339  func (m *K8sClient) NamespaceExists(namespace string) bool {
   340  	if _, err := m.ClientSet.CoreV1().Namespaces().Get(context.Background(), namespace, metaV1.GetOptions{}); err != nil {
   341  		return false
   342  	}
   343  	return true
   344  }
   345  
   346  // RemoveNamespace removes namespace
   347  func (m *K8sClient) RemoveNamespace(namespace string) error {
   348  	log.Info().Str("Namespace", namespace).Msg("Removing namespace")
   349  	return m.ClientSet.CoreV1().Namespaces().Delete(context.Background(), namespace, metaV1.DeleteOptions{})
   350  }
   351  
   352  // RolloutStatefulSets applies "rollout statefulset" to all existing statefulsets in that namespace
   353  func (m *K8sClient) RolloutStatefulSets(ctx context.Context, namespace string) error {
   354  	stsClient := m.ClientSet.AppsV1().StatefulSets(namespace)
   355  	sts, err := stsClient.List(ctx, metaV1.ListOptions{})
   356  	if err != nil {
   357  		return err
   358  	}
   359  	for _, s := range sts.Items {
   360  		cmd := fmt.Sprintf("kubectl rollout restart statefulset %s --namespace %s", s.Name, namespace)
   361  		log.Info().Str("Command", cmd).Msg("Applying StatefulSet rollout")
   362  		if err := ExecCmdWithContext(ctx, cmd); err != nil {
   363  			return err
   364  		}
   365  	}
   366  	// wait for the statefulsets to be ready in a separate loop otherwise this can take a long time
   367  	for _, s := range sts.Items {
   368  		// wait for the rollout to be complete
   369  		scmd := fmt.Sprintf("kubectl rollout status statefulset %s --namespace %s", s.Name, namespace)
   370  		log.Info().Str("Command", scmd).Msg("Waiting for StatefulSet rollout to finish")
   371  		if err := ExecCmdWithContext(ctx, scmd); err != nil {
   372  			return err
   373  		}
   374  	}
   375  	return nil
   376  }
   377  
   378  // RolloutRestartBySelector rollouts and restarts object by selector
   379  func (m *K8sClient) RolloutRestartBySelector(ctx context.Context, namespace, resource, selector string) error {
   380  	cmd := fmt.Sprintf("kubectl --namespace %s rollout restart -l %s %s", namespace, selector, resource)
   381  	log.Info().Str("Command", cmd).Msg("rollout restart by selector")
   382  	if err := ExecCmdWithContext(ctx, cmd); err != nil {
   383  		return err
   384  	}
   385  	// wait for the rollout to be complete
   386  	waitCmd := fmt.Sprintf("kubectl --namespace %s rollout status -l %s %s", namespace, selector, resource)
   387  	log.Info().Str("Command", waitCmd).Msg("Waiting for StatefulSet rollout to finish")
   388  	return ExecCmdWithContext(ctx, waitCmd)
   389  }
   390  
   391  // ReadyCheckData data to check if selected pods are running and all containers are ready ( readiness check ) are ready
   392  type ReadyCheckData struct {
   393  	ReadinessProbeCheckSelector string
   394  	Timeout                     time.Duration
   395  }
   396  
   397  // WaitForJob wait for job execution, follow logs and returns an error if job failed
   398  func (m *K8sClient) WaitForJob(namespaceName string, jobName string, fundReturnStatus func(string)) error {
   399  	cmd := fmt.Sprintf("kubectl --namespace %s logs --follow job/%s", namespaceName, jobName)
   400  	log.Info().Str("Job", jobName).Str("cmd", cmd).Msg("Waiting for job to complete")
   401  	ctx := context.Background()
   402  	if err := ExecCmdWithOptions(ctx, cmd, fundReturnStatus); err != nil {
   403  		return err
   404  	}
   405  	var exitErr error
   406  	ctx, cancel := context.WithTimeout(ctx, JobFinalizedTimeout)
   407  	defer cancel()
   408  	if err := wait.PollUntilContextTimeout(ctx, K8sStatePollInterval, JobFinalizedTimeout, true, func(ctx context.Context) (bool, error) {
   409  		job, err := m.ClientSet.BatchV1().Jobs(namespaceName).Get(ctx, jobName, metaV1.GetOptions{})
   410  		if err != nil {
   411  			exitErr = err
   412  		}
   413  		if int(job.Status.Failed) > 0 {
   414  			exitErr = fmt.Errorf("job failed")
   415  			return true, nil
   416  		}
   417  		if int(job.Status.Succeeded) > 0 {
   418  			exitErr = nil
   419  			return true, nil
   420  		}
   421  		return false, nil
   422  	}); err != nil {
   423  		return err
   424  	}
   425  	return exitErr
   426  }
   427  
   428  func (m *K8sClient) WaitForDeploymentsAvailable(ctx context.Context, namespace string) error {
   429  	deployments, err := m.ClientSet.AppsV1().Deployments(namespace).List(ctx, metaV1.ListOptions{})
   430  	if err != nil {
   431  		return err
   432  	}
   433  	log.Debug().Int("Number", len(deployments.Items)).Msg("Deployments found")
   434  	for _, d := range deployments.Items {
   435  		log.Debug().Str("status", d.Status.String()).Msg("Deployment info")
   436  		waitCmd := fmt.Sprintf("kubectl rollout status -n %s deployment/%s", namespace, d.Name)
   437  		log.Debug().Str("cmd", waitCmd).Msg("wait for deployment to be available")
   438  		if err := ExecCmdWithContext(ctx, waitCmd); err != nil {
   439  			return err
   440  		}
   441  	}
   442  	return nil
   443  }
   444  
   445  // Apply applying a manifest to a currently connected k8s context
   446  func (m *K8sClient) Apply(ctx context.Context, manifest, namespace string, waitForDeployment bool) error {
   447  	manifestFile := fmt.Sprintf(TempDebugManifest, uuid.NewString())
   448  	log.Info().Str("File", manifestFile).Msg("Applying manifest")
   449  	if err := os.WriteFile(manifestFile, []byte(manifest), os.ModePerm); err != nil {
   450  		return err
   451  	}
   452  	cmd := fmt.Sprintf("kubectl apply -f %s", manifestFile)
   453  	log.Debug().Str("cmd", cmd).Msg("Apply command")
   454  	if err := ExecCmdWithContext(ctx, cmd); err != nil {
   455  		return err
   456  	}
   457  	if waitForDeployment {
   458  		return m.WaitForDeploymentsAvailable(ctx, namespace)
   459  	}
   460  	return nil
   461  }
   462  
   463  // DeleteResource deletes resource
   464  func (m *K8sClient) DeleteResource(namespace string, resource string, instance string) error {
   465  	return ExecCmd(fmt.Sprintf("kubectl delete %s %s --namespace %s", resource, instance, namespace))
   466  }
   467  
   468  // Create creating a manifest to a currently connected k8s context
   469  func (m *K8sClient) Create(manifest string) error {
   470  	manifestFile := fmt.Sprintf(TempDebugManifest, uuid.NewString())
   471  	log.Info().Str("File", manifestFile).Msg("Creating manifest")
   472  	if err := os.WriteFile(manifestFile, []byte(manifest), os.ModePerm); err != nil {
   473  		return err
   474  	}
   475  	cmd := fmt.Sprintf("kubectl create -f %s", manifestFile)
   476  	return ExecCmd(cmd)
   477  }
   478  
   479  // DryRun generates manifest and writes it in a file
   480  func (m *K8sClient) DryRun(manifest string) error {
   481  	manifestFile := fmt.Sprintf(TempDebugManifest, uuid.NewString())
   482  	log.Info().Str("File", manifestFile).Msg("Creating manifest")
   483  	return os.WriteFile(manifestFile, []byte(manifest), os.ModePerm)
   484  }
   485  
   486  // CopyToPod copies src to a particular container. Destination should be in the form of a proper K8s destination path
   487  // NAMESPACE/POD_NAME:folder/FILE_NAME
   488  func (m *K8sClient) CopyToPod(namespace, src, destination, containername string) (*bytes.Buffer, *bytes.Buffer, *bytes.Buffer, error) {
   489  	m.RESTConfig.APIPath = "/api"
   490  	m.RESTConfig.GroupVersion = &schema.GroupVersion{Version: "v1"} // this targets the core api groups so the url path will be /api/v1
   491  	m.RESTConfig.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: scheme.Codecs}
   492  	ioStreams, in, out, errOut := genericclioptions.NewTestIOStreams()
   493  
   494  	copyOptions := cp.NewCopyOptions(ioStreams)
   495  	configFlags := genericclioptions.NewConfigFlags(false)
   496  	f := cmdutil.NewFactory(configFlags)
   497  	cmd := cp.NewCmdCp(f, ioStreams)
   498  	err := copyOptions.Complete(f, cmd, []string{src, destination})
   499  	if err != nil {
   500  		return nil, nil, nil, err
   501  	}
   502  	copyOptions.Clientset = m.ClientSet
   503  	copyOptions.ClientConfig = m.RESTConfig
   504  	copyOptions.Container = containername
   505  	copyOptions.Namespace = namespace
   506  
   507  	formatted, err := regexp.MatchString(".*?\\/.*?\\:.*", destination)
   508  	if err != nil {
   509  		return nil, nil, nil, fmt.Errorf("could not parse the pod destination: %w", err)
   510  	}
   511  	if !formatted {
   512  		return nil, nil, nil, fmt.Errorf("pod destination string improperly formatted, see reference 'NAMESPACE/POD_NAME:folder/FILE_NAME'")
   513  	}
   514  
   515  	log.Info().
   516  		Str("Namespace", namespace).
   517  		Str("Source", src).
   518  		Str("Destination", destination).
   519  		Str("Container", containername).
   520  		Msg("Uploading file to pod")
   521  	err = copyOptions.Run()
   522  	if err != nil {
   523  		return nil, nil, nil, fmt.Errorf("could not run copy operation: %w", err)
   524  	}
   525  	return in, out, errOut, nil
   526  }
   527  
   528  // ExecuteInPod is similar to kubectl exec
   529  func (m *K8sClient) ExecuteInPod(namespace, podName, containerName string, command []string) ([]byte, []byte, error) {
   530  	log.Info().Interface("Command", command).Msg("Executing command in pod")
   531  	req := m.ClientSet.CoreV1().RESTClient().Post().
   532  		Resource("pods").
   533  		Name(podName).
   534  		Namespace(namespace).
   535  		SubResource("exec")
   536  	req.VersionedParams(&v1.PodExecOptions{
   537  		Container: containerName,
   538  		Command:   command,
   539  		Stdin:     false,
   540  		Stdout:    true,
   541  		Stderr:    true,
   542  		TTY:       false,
   543  	}, scheme.ParameterCodec)
   544  
   545  	exec, err := remotecommand.NewSPDYExecutor(m.RESTConfig, "POST", req.URL())
   546  	if err != nil {
   547  		return []byte{}, []byte{}, err
   548  	}
   549  
   550  	var stdout, stderr bytes.Buffer
   551  	err = exec.Stream(remotecommand.StreamOptions{
   552  		Stdin:  nil,
   553  		Stdout: &stdout,
   554  		Stderr: &stderr,
   555  	})
   556  	return stdout.Bytes(), stderr.Bytes(), err
   557  }
   558  
   559  func podNames(podItems *v1.PodList) []string {
   560  	pn := make([]string, 0)
   561  	for _, p := range podItems.Items {
   562  		pn = append(pn, p.Name)
   563  	}
   564  	return pn
   565  }