github.com/Equinix-Metal/virtlet@v1.5.2-0.20210807010419-342346535dc5/tests/e2e/framework/pod_interface.go (about)

     1  /*
     2  Copyright 2017 Mirantis
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package framework
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"os"
    27  	"os/signal"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/davecgh/go-spew/spew"
    32  	"k8s.io/api/core/v1"
    33  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/client-go/kubernetes/scheme"
    36  	"k8s.io/client-go/tools/portforward"
    37  	"k8s.io/client-go/tools/remotecommand"
    38  	"k8s.io/client-go/transport/spdy"
    39  	"k8s.io/client-go/util/exec"
    40  
    41  	"github.com/Equinix-Metal/virtlet/pkg/tools"
    42  )
    43  
    44  // PodInterface provides API to work with a pod
    45  type PodInterface struct {
    46  	controller *Controller
    47  	hasService bool
    48  
    49  	Pod *v1.Pod
    50  }
    51  
    52  func newPodInterface(controller *Controller, pod *v1.Pod) *PodInterface {
    53  	return &PodInterface{
    54  		controller: controller,
    55  		Pod:        pod,
    56  	}
    57  }
    58  
    59  // Create creates pod in the k8s
    60  func (pi *PodInterface) Create() error {
    61  	updatedPod, err := pi.controller.client.Pods(pi.controller.Namespace()).Create(pi.Pod)
    62  	if err != nil {
    63  		return err
    64  	}
    65  	pi.Pod = updatedPod
    66  	return nil
    67  }
    68  
    69  // Delete deletes the pod and associated service, which was earlier created by `controller.Run()`
    70  func (pi *PodInterface) Delete() error {
    71  	if pi.hasService {
    72  		pi.controller.client.Services(pi.Pod.Namespace).Delete(pi.Pod.Name, nil)
    73  	}
    74  	return pi.controller.client.Pods(pi.Pod.Namespace).Delete(pi.Pod.Name, nil)
    75  }
    76  
    77  // WaitForPodStatus waits for the pod to reach the specified status. If expectedContainerErrors
    78  // is empty, the pod is expected to become Running and Ready. If it isn't, the pod is expected
    79  // to have one of these errors among its container statuses.
    80  func (pi *PodInterface) WaitForPodStatus(expectedContainerErrors []string, timing ...time.Duration) error {
    81  	timeout := time.Minute * 5
    82  	pollPeriond := time.Second
    83  	consistencyPeriod := time.Second * 5
    84  	if len(timing) > 0 {
    85  		timeout = timing[0]
    86  	}
    87  	if len(timing) > 1 {
    88  		pollPeriond = timing[1]
    89  	}
    90  	if len(timing) > 2 {
    91  		consistencyPeriod = timing[2]
    92  	}
    93  
    94  	return waitForConsistentState(func() error {
    95  		podUpdated, err := pi.controller.client.Pods(pi.Pod.Namespace).Get(pi.Pod.Name, metav1.GetOptions{})
    96  		if err != nil {
    97  			return err
    98  		}
    99  		pi.Pod = podUpdated
   100  
   101  		needErrors := len(expectedContainerErrors) > 0
   102  		phase := v1.PodRunning
   103  		if needErrors {
   104  			phase = v1.PodPending
   105  		}
   106  		if podUpdated.Status.Phase != phase {
   107  			return fmt.Errorf("pod %s is not %s phase: %s", podUpdated.Name, phase, podUpdated.Status.Phase)
   108  		}
   109  
   110  		gotExpectedError := false
   111  		for _, cs := range podUpdated.Status.ContainerStatuses {
   112  			switch {
   113  			case !needErrors && cs.State.Running == nil:
   114  				return fmt.Errorf("container %s in pod %s is not running: %s", cs.Name, podUpdated.Name, spew.Sdump(cs.State))
   115  			case !needErrors && !cs.Ready:
   116  				return fmt.Errorf("container %s in pod %s did not passed its readiness probe", cs.Name, podUpdated.Name)
   117  			case needErrors && cs.State.Waiting == nil:
   118  				return fmt.Errorf("container %s in pod %s not in waiting state", cs.Name, podUpdated.Name)
   119  			case needErrors:
   120  				for _, errStr := range expectedContainerErrors {
   121  					if cs.State.Waiting.Reason == errStr {
   122  						gotExpectedError = true
   123  						break
   124  					}
   125  				}
   126  			}
   127  		}
   128  		if needErrors && !gotExpectedError {
   129  			return fmt.Errorf("didn't get one of expected container errors: %s", strings.Join(expectedContainerErrors, " | "))
   130  		}
   131  		return nil
   132  	}, timeout, pollPeriond, consistencyPeriod)
   133  }
   134  
   135  // Wait waits for pod to start and checks that it doesn't fail immediately after that
   136  func (pi *PodInterface) Wait(timing ...time.Duration) error {
   137  	return pi.WaitForPodStatus(nil, timing...)
   138  }
   139  
   140  // WaitForDestruction waits for the pod to be deleted
   141  func (pi *PodInterface) WaitForDestruction(timing ...time.Duration) error {
   142  	timeout := time.Minute * 5
   143  	pollPeriond := time.Second
   144  	consistencyPeriod := time.Second * 5
   145  	if len(timing) > 0 {
   146  		timeout = timing[0]
   147  	}
   148  	if len(timing) > 1 {
   149  		pollPeriond = timing[1]
   150  	}
   151  	if len(timing) > 2 {
   152  		consistencyPeriod = timing[2]
   153  	}
   154  
   155  	return waitForConsistentState(func() error {
   156  		if _, err := pi.controller.client.Pods(pi.Pod.Namespace).Get(pi.Pod.Name, metav1.GetOptions{}); err != nil {
   157  			if k8serrors.IsNotFound(err) {
   158  				return nil
   159  			}
   160  			return err
   161  		}
   162  		return fmt.Errorf("pod %s was not deleted", pi.Pod.Name)
   163  	}, timeout, pollPeriond, consistencyPeriod)
   164  }
   165  
   166  // Container returns an interface to handle one of the pod's
   167  // containers. If name is empty, it takes the first container
   168  // of the pod.
   169  func (pi *PodInterface) Container(name string) (Executor, error) {
   170  	if name == "" && len(pi.Pod.Spec.Containers) > 0 {
   171  		name = pi.Pod.Spec.Containers[0].Name
   172  	}
   173  	found := false
   174  	for _, c := range pi.Pod.Spec.Containers {
   175  		if c.Name == name {
   176  			found = true
   177  			break
   178  		}
   179  	}
   180  	if !found {
   181  		return nil, fmt.Errorf("container %s doesn't exist in pod %s in namespace %s", name, pi.Pod.Name, pi.Pod.Namespace)
   182  	}
   183  	return &containerInterface{
   184  		podInterface: pi,
   185  		name:         name,
   186  	}, nil
   187  }
   188  
   189  // PortForward starts port forwarding to the specified ports to the specified pod
   190  // in background. If a port entry has LocalPort = 0, it's updated with the real
   191  // port number that was selected by the forwarder.
   192  // Close returned channel to stop the port forwarder.
   193  func (pi *PodInterface) PortForward(ports []*tools.ForwardedPort) (chan struct{}, error) {
   194  	if len(ports) == 0 {
   195  		return nil, errors.New("no ports specified")
   196  	}
   197  
   198  	signals := make(chan os.Signal, 1)
   199  	signal.Notify(signals, os.Interrupt)
   200  	defer signal.Stop(signals)
   201  
   202  	stopCh := make(chan struct{})
   203  	go func() {
   204  		<-signals
   205  		if stopCh != nil {
   206  			close(stopCh)
   207  		}
   208  	}()
   209  
   210  	restClient := pi.controller.client.RESTClient()
   211  	req := restClient.Post().
   212  		Resource("pods").
   213  		Name(pi.Pod.Name).
   214  		Namespace(pi.Pod.Namespace).
   215  		SubResource("portforward")
   216  
   217  	var buf bytes.Buffer
   218  	var portsStr []string
   219  	for _, p := range ports {
   220  		portsStr = append(portsStr, p.String())
   221  	}
   222  	errCh := make(chan error, 1)
   223  	readyCh := make(chan struct{})
   224  	go func() {
   225  		transport, upgrader, err := spdy.RoundTripperFor(pi.controller.restConfig)
   226  		if err != nil {
   227  			errCh <- err
   228  			return
   229  		}
   230  		dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL())
   231  		if err != nil {
   232  			errCh <- err
   233  			return
   234  		}
   235  		fw, err := portforward.New(dialer, portsStr, stopCh, readyCh, &buf, os.Stderr)
   236  		if err != nil {
   237  			errCh <- err
   238  			return
   239  		}
   240  		errCh <- fw.ForwardPorts()
   241  	}()
   242  
   243  	select {
   244  	case err := <-errCh:
   245  		return nil, err
   246  	case <-readyCh:
   247  		// FIXME: there appears to be no better way to get back the local ports as of now
   248  		if err := tools.ParsePortForwardOutput(buf.String(), ports); err != nil {
   249  			return nil, err
   250  		}
   251  	}
   252  	return stopCh, nil
   253  
   254  }
   255  
   256  // DinDNodeExecutor return DinD executor for node, where this pod is located
   257  func (pi *PodInterface) DinDNodeExecutor() (Executor, error) {
   258  	return pi.controller.DinDNodeExecutor(pi.Pod.Spec.NodeName)
   259  }
   260  
   261  // LoadEvents retrieves the evnets for this pod as a list
   262  // of strings of the form Type:Reason:Message
   263  func (pi *PodInterface) LoadEvents() ([]string, error) {
   264  	events, err := pi.controller.client.Events(pi.controller.Namespace()).Search(scheme.Scheme, pi.Pod)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	var r []string
   269  	for _, e := range events.Items {
   270  		r = append(r, fmt.Sprintf("%s:%s:%s", e.Type, e.Reason, e.Message))
   271  	}
   272  	return r, nil
   273  }
   274  
   275  type containerInterface struct {
   276  	podInterface *PodInterface
   277  	name         string
   278  }
   279  
   280  // Run executes commands in one of containers in the pod
   281  func (ci *containerInterface) Run(stdin io.Reader, stdout, stderr io.Writer, command ...string) error {
   282  	restClient := ci.podInterface.controller.client.RESTClient()
   283  	req := restClient.Post().
   284  		Resource("pods").
   285  		Name(ci.podInterface.Pod.Name).
   286  		Namespace(ci.podInterface.Pod.Namespace).
   287  		SubResource("exec")
   288  	req.VersionedParams(&v1.PodExecOptions{
   289  		Container: ci.name,
   290  		Command:   command,
   291  		Stdin:     stdin != nil,
   292  		Stdout:    stdout != nil,
   293  		Stderr:    stderr != nil,
   294  	}, scheme.ParameterCodec)
   295  
   296  	executor, err := remotecommand.NewSPDYExecutor(ci.podInterface.controller.restConfig, "POST", req.URL())
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	options := remotecommand.StreamOptions{
   302  		Stdin:  stdin,
   303  		Stdout: stdout,
   304  		Stderr: stderr,
   305  	}
   306  
   307  	if err := executor.Stream(options); err != nil {
   308  		if c, ok := err.(exec.CodeExitError); ok {
   309  			return CommandError{ExitCode: c.Code}
   310  		}
   311  		return err
   312  	}
   313  
   314  	return nil
   315  }
   316  
   317  // Close closes the executor
   318  func (*containerInterface) Close() error {
   319  	return nil
   320  }
   321  
   322  // Start is a placeholder for fulfilling the Executor interface
   323  func (*containerInterface) Start(stdin io.Reader, stdout, stderr io.Writer, command ...string) (Command, error) {
   324  	return nil, errors.New("Not Implemented")
   325  }
   326  
   327  // Logs returns the logs of the container as a string.
   328  func (ci *containerInterface) Logs() (string, error) {
   329  	restClient := ci.podInterface.controller.client.RESTClient()
   330  	req := restClient.Get().
   331  		Name(ci.podInterface.Pod.Name).
   332  		Namespace(ci.podInterface.Pod.Namespace).
   333  		Resource("pods").
   334  		SubResource("log")
   335  	req.VersionedParams(&v1.PodLogOptions{
   336  		Container: ci.name,
   337  	}, scheme.ParameterCodec)
   338  	stream, err := req.Stream()
   339  	if err != nil {
   340  		return "", err
   341  	}
   342  	defer stream.Close()
   343  
   344  	bs, err := ioutil.ReadAll(stream)
   345  	if err != nil {
   346  		return "", fmt.Errorf("ReadAll(): %v", err)
   347  	}
   348  
   349  	return string(bs), nil
   350  }