istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/echo/kube/workload.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package kube
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/hashicorp/go-multierror"
    24  	corev1 "k8s.io/api/core/v1"
    25  
    26  	istioKube "istio.io/istio/pkg/kube"
    27  	"istio.io/istio/pkg/log"
    28  	"istio.io/istio/pkg/test"
    29  	echoClient "istio.io/istio/pkg/test/echo"
    30  	"istio.io/istio/pkg/test/echo/common"
    31  	"istio.io/istio/pkg/test/echo/proto"
    32  	"istio.io/istio/pkg/test/framework/components/cluster"
    33  	"istio.io/istio/pkg/test/framework/components/echo"
    34  	"istio.io/istio/pkg/test/framework/errors"
    35  	"istio.io/istio/pkg/test/framework/resource"
    36  	"istio.io/istio/pkg/test/scopes"
    37  	"istio.io/istio/pkg/test/util/retry"
    38  )
    39  
    40  const (
    41  	appContainerName = "app"
    42  )
    43  
    44  var _ echo.Workload = &workload{}
    45  
    46  type workloadConfig struct {
    47  	pod        corev1.Pod
    48  	hasSidecar bool
    49  	grpcPort   uint16
    50  	cluster    cluster.Cluster
    51  	tls        *common.TLSSettings
    52  	stop       chan struct{}
    53  }
    54  
    55  type workload struct {
    56  	client *echoClient.Client
    57  
    58  	workloadConfig
    59  	forwarder  istioKube.PortForwarder
    60  	sidecar    *sidecar
    61  	ctx        resource.Context
    62  	mutex      sync.Mutex
    63  	connectErr error
    64  }
    65  
    66  func newWorkload(cfg workloadConfig, ctx resource.Context) (*workload, error) {
    67  	w := &workload{
    68  		workloadConfig: cfg,
    69  		ctx:            ctx,
    70  	}
    71  
    72  	// If the pod is ready, connect.
    73  	if err := w.Update(cfg.pod); err != nil {
    74  		return nil, err
    75  	}
    76  
    77  	go watchPortForward(cfg, w)
    78  
    79  	return w, nil
    80  }
    81  
    82  // watchPortForward wait watch the health of a port-forward connection. If a disconnect is detected, the workload is reconnected.
    83  // TODO: this isn't structured very nicely. We have a port forwarder that can notify us when it fails (ErrChan) and we are competing with
    84  // the pod informer which is sequenced via mutex. This could probably be cleaned up to be more event driven, but would require larger refactoring.
    85  func watchPortForward(cfg workloadConfig, w *workload) {
    86  	t := time.NewTicker(time.Millisecond * 500)
    87  	handler := func() {
    88  		w.mutex.Lock()
    89  		defer w.mutex.Unlock()
    90  		if w.forwarder == nil {
    91  			// We only want to do reconnects here, if we never connected let the main flow handle it.
    92  			return
    93  		}
    94  		// Only reconnect if the pod is ready
    95  		if !isPodReady(w.pod) {
    96  			return
    97  		}
    98  		con := !w.isConnected()
    99  		if con {
   100  			log.Warnf("pod: %s/%s port forward terminated", w.pod.Namespace, w.pod.Name)
   101  			err := w.connect(w.pod)
   102  			if err != nil {
   103  				log.Warnf("pod: %s/%s port forward reconnect failed: %v", w.pod.Namespace, w.pod.Name, err)
   104  			} else {
   105  				log.Warnf("pod: %s/%s port forward reconnect success", w.pod.Namespace, w.pod.Name)
   106  			}
   107  		}
   108  	}
   109  	for {
   110  		select {
   111  		case <-cfg.stop:
   112  			return
   113  		case <-t.C:
   114  			handler()
   115  		}
   116  	}
   117  }
   118  
   119  func (w *workload) IsReady() bool {
   120  	w.mutex.Lock()
   121  	ready := w.isConnected()
   122  	w.mutex.Unlock()
   123  	return ready
   124  }
   125  
   126  func (w *workload) Client() (c *echoClient.Client, err error) {
   127  	w.mutex.Lock()
   128  	c = w.client
   129  	if c == nil {
   130  		err = fmt.Errorf("attempt to use disconnected client for echo pod %s/%s (in cluster %s)",
   131  			w.pod.Namespace, w.pod.Name, w.cluster.Name())
   132  	}
   133  	w.mutex.Unlock()
   134  	return
   135  }
   136  
   137  func (w *workload) Update(pod corev1.Pod) error {
   138  	w.mutex.Lock()
   139  	defer w.mutex.Unlock()
   140  
   141  	if isPodReady(pod) && !w.isConnected() {
   142  		if err := w.connect(pod); err != nil {
   143  			w.connectErr = err
   144  			return err
   145  		}
   146  	} else if !isPodReady(pod) && w.isConnected() {
   147  		scopes.Framework.Infof("echo pod %s/%s (in cluster %s) transitioned to NOT READY. Pod Status=%v",
   148  			pod.Namespace, pod.Name, w.cluster.Name(), pod.Status.Phase)
   149  		w.pod = pod
   150  		return w.disconnect()
   151  	}
   152  
   153  	// Update the pod.
   154  	w.pod = pod
   155  	return nil
   156  }
   157  
   158  func (w *workload) Close() (err error) {
   159  	w.mutex.Lock()
   160  	defer w.mutex.Unlock()
   161  
   162  	if w.isConnected() {
   163  		return w.disconnect()
   164  	}
   165  	return nil
   166  }
   167  
   168  func (w *workload) PodName() string {
   169  	w.mutex.Lock()
   170  	n := w.pod.Name
   171  	w.mutex.Unlock()
   172  	return n
   173  }
   174  
   175  func (w *workload) Address() string {
   176  	w.mutex.Lock()
   177  	ip := w.pod.Status.PodIP
   178  	w.mutex.Unlock()
   179  	return ip
   180  }
   181  
   182  func (w *workload) Addresses() []string {
   183  	w.mutex.Lock()
   184  	var addresses []string
   185  	for _, podIP := range w.pod.Status.PodIPs {
   186  		addresses = append(addresses, podIP.IP)
   187  	}
   188  	w.mutex.Unlock()
   189  	return addresses
   190  }
   191  
   192  func (w *workload) ForwardEcho(ctx context.Context, request *proto.ForwardEchoRequest) (echoClient.Responses, error) {
   193  	w.mutex.Lock()
   194  	c := w.client
   195  	if c == nil {
   196  		w.mutex.Unlock()
   197  		return nil, fmt.Errorf("failed forwarding echo for disconnected pod %s/%s",
   198  			w.pod.Namespace, w.pod.Name)
   199  	}
   200  	w.mutex.Unlock()
   201  
   202  	return c.ForwardEcho(ctx, request)
   203  }
   204  
   205  func (w *workload) Sidecar() echo.Sidecar {
   206  	w.mutex.Lock()
   207  	s := w.sidecar
   208  	w.mutex.Unlock()
   209  	return s
   210  }
   211  
   212  func (w *workload) Cluster() cluster.Cluster {
   213  	return w.cluster
   214  }
   215  
   216  func (w *workload) Logs() (string, error) {
   217  	return w.cluster.PodLogs(context.TODO(), w.pod.Name, w.pod.Namespace, appContainerName, false)
   218  }
   219  
   220  func (w *workload) LogsOrFail(t test.Failer) string {
   221  	t.Helper()
   222  	logs, err := w.Logs()
   223  	if err != nil {
   224  		t.Fatal(err)
   225  	}
   226  	return logs
   227  }
   228  
   229  func isPodReady(pod corev1.Pod) bool {
   230  	return istioKube.CheckPodReady(&pod) == nil
   231  }
   232  
   233  func (w *workload) isConnected() bool {
   234  	if w.forwarder == nil {
   235  		return false
   236  	}
   237  	select {
   238  	case <-w.forwarder.ErrChan():
   239  		// If an error is available, we got disconnected
   240  		return false
   241  	default:
   242  		// Otherwise we are connected
   243  		return true
   244  	}
   245  }
   246  
   247  func (w *workload) connect(pod corev1.Pod) (err error) {
   248  	defer func() {
   249  		if err != nil {
   250  			_ = w.disconnect()
   251  		}
   252  	}()
   253  
   254  	// Create a forwarder to the command port of the app.
   255  	if err = retry.UntilSuccess(func() error {
   256  		w.forwarder, err = w.cluster.NewPortForwarder(pod.Name, pod.Namespace, "", 0, int(w.grpcPort))
   257  		if err != nil {
   258  			return fmt.Errorf("failed creating new port forwarder for pod %s/%s: %v",
   259  				pod.Namespace, pod.Name, err)
   260  		}
   261  		if err = w.forwarder.Start(); err != nil {
   262  			return fmt.Errorf("failed starting port forwarder for pod %s/%s: %v",
   263  				pod.Namespace, pod.Name, err)
   264  		}
   265  		return nil
   266  	}, retry.BackoffDelay(100*time.Millisecond), retry.Timeout(10*time.Second)); err != nil {
   267  		return err
   268  	}
   269  
   270  	// Create a gRPC client to this workload.
   271  	w.client, err = echoClient.New(w.forwarder.Address(), w.tls)
   272  	if err != nil {
   273  		return fmt.Errorf("failed connecting to grpc client to pod %s/%s : %v",
   274  			pod.Namespace, pod.Name, err)
   275  	}
   276  
   277  	if w.hasSidecar {
   278  		w.sidecar = newSidecar(pod, w.cluster)
   279  	}
   280  
   281  	return nil
   282  }
   283  
   284  func (w *workload) disconnect() (err error) {
   285  	if w.client != nil {
   286  		err = multierror.Append(err, w.client.Close()).ErrorOrNil()
   287  		w.client = nil
   288  	}
   289  	if w.forwarder != nil {
   290  		w.forwarder.Close()
   291  		w.forwarder = nil
   292  	}
   293  	if w.ctx.Settings().FailOnDeprecation && w.sidecar != nil {
   294  		err = multierror.Append(err, w.checkDeprecation()).ErrorOrNil()
   295  		w.sidecar = nil
   296  	}
   297  	return err
   298  }
   299  
   300  func (w *workload) checkDeprecation() error {
   301  	logs, err := w.sidecar.Logs()
   302  	if err != nil {
   303  		return fmt.Errorf("could not get sidecar logs to inspect for deprecation messages: %v", err)
   304  	}
   305  
   306  	info := fmt.Sprintf("pod: %s/%s", w.pod.Namespace, w.pod.Name)
   307  	return errors.FindDeprecatedMessagesInEnvoyLog(logs, info)
   308  }