istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/echo/kube/instance.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  	"io"
    21  	"time"
    22  
    23  	"github.com/hashicorp/go-multierror"
    24  	corev1 "k8s.io/api/core/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  
    27  	"istio.io/istio/pkg/config/protocol"
    28  	"istio.io/istio/pkg/test"
    29  	echoClient "istio.io/istio/pkg/test/echo"
    30  	"istio.io/istio/pkg/test/echo/common/scheme"
    31  	"istio.io/istio/pkg/test/framework/components/cluster"
    32  	"istio.io/istio/pkg/test/framework/components/echo"
    33  	"istio.io/istio/pkg/test/framework/components/echo/check"
    34  	"istio.io/istio/pkg/test/framework/components/echo/common"
    35  	"istio.io/istio/pkg/test/framework/resource"
    36  	"istio.io/istio/pkg/test/util/retry"
    37  	"istio.io/istio/pkg/util/istiomultierror"
    38  )
    39  
    40  const (
    41  	tcpHealthPort     = 3333
    42  	httpReadinessPort = 8080
    43  )
    44  
    45  var (
    46  	_ echo.Instance = &instance{}
    47  	_ io.Closer     = &instance{}
    48  
    49  	startDelay = retry.BackoffDelay(time.Millisecond * 100)
    50  )
    51  
    52  type instance struct {
    53  	id             resource.ID
    54  	cfg            echo.Config
    55  	clusterIP      string
    56  	clusterIPs     []string
    57  	ctx            resource.Context
    58  	cluster        cluster.Cluster
    59  	workloadMgr    *workloadManager
    60  	deployment     *deployment
    61  	workloadFilter []echo.Workload
    62  }
    63  
    64  func newInstance(ctx resource.Context, originalCfg echo.Config) (out *instance, err error) {
    65  	cfg := originalCfg.DeepCopy()
    66  
    67  	c := &instance{
    68  		cfg:     cfg,
    69  		ctx:     ctx,
    70  		cluster: cfg.Cluster,
    71  	}
    72  
    73  	// Deploy echo to the cluster
    74  	c.deployment, err = newDeployment(ctx, cfg)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	// Create the manager for echo workloads for this instance.
    80  	c.workloadMgr, err = newWorkloadManager(ctx, cfg, c.deployment)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	// Now that we have the successfully created the workload manager, track this resource so
    86  	// that it will be closed when it goes out of scope.
    87  	c.id = ctx.TrackResource(c)
    88  
    89  	// Now retrieve the service information to find the ClusterIP
    90  	s, err := c.cluster.Kube().CoreV1().Services(cfg.Namespace.Name()).Get(context.TODO(), cfg.Service, metav1.GetOptions{})
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	c.clusterIP = s.Spec.ClusterIP
    96  	c.clusterIPs = s.Spec.ClusterIPs
    97  	switch c.clusterIP {
    98  	case corev1.ClusterIPNone, "":
    99  		if !cfg.Headless {
   100  			return nil, fmt.Errorf("invalid ClusterIP %s for non-headless service %s/%s",
   101  				c.clusterIP,
   102  				c.cfg.Namespace.Name(),
   103  				c.cfg.Service)
   104  		}
   105  		c.clusterIP = ""
   106  	}
   107  
   108  	// Start the workload manager.
   109  	if err := c.workloadMgr.Start(); err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	return c, nil
   114  }
   115  
   116  func (c *instance) ID() resource.ID {
   117  	return c.id
   118  }
   119  
   120  func (c *instance) Address() string {
   121  	return c.clusterIP
   122  }
   123  
   124  func (c *instance) Addresses() []string {
   125  	return c.clusterIPs
   126  }
   127  
   128  func (c *instance) Workloads() (echo.Workloads, error) {
   129  	wls, err := c.workloadMgr.ReadyWorkloads()
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	var final []echo.Workload
   134  	for _, wl := range wls {
   135  		filtered := false
   136  		for _, filter := range c.workloadFilter {
   137  			if wl.Address() != filter.Address() {
   138  				filtered = true
   139  				break
   140  			}
   141  		}
   142  		if !filtered {
   143  			final = append(final, wl)
   144  		}
   145  	}
   146  	return final, nil
   147  }
   148  
   149  func (c *instance) WorkloadsOrFail(t test.Failer) echo.Workloads {
   150  	t.Helper()
   151  	out, err := c.Workloads()
   152  	if err != nil {
   153  		t.Fatal(err)
   154  	}
   155  	return out
   156  }
   157  
   158  func (c *instance) MustWorkloads() echo.Workloads {
   159  	out, err := c.Workloads()
   160  	if err != nil {
   161  		panic(err)
   162  	}
   163  	return out
   164  }
   165  
   166  func (c *instance) Clusters() cluster.Clusters {
   167  	return cluster.Clusters{c.cluster}
   168  }
   169  
   170  func (c *instance) Instances() echo.Instances {
   171  	return echo.Instances{c}
   172  }
   173  
   174  func (c *instance) Close() (err error) {
   175  	return c.workloadMgr.Close()
   176  }
   177  
   178  func (c *instance) NamespacedName() echo.NamespacedName {
   179  	return c.cfg.NamespacedName()
   180  }
   181  
   182  func (c *instance) PortForName(name string) echo.Port {
   183  	return c.cfg.Ports.MustForName(name)
   184  }
   185  
   186  func (c *instance) ServiceName() string {
   187  	return c.cfg.Service
   188  }
   189  
   190  func (c *instance) NamespaceName() string {
   191  	return c.cfg.NamespaceName()
   192  }
   193  
   194  func (c *instance) ServiceAccountName() string {
   195  	return c.cfg.ServiceAccountName()
   196  }
   197  
   198  func (c *instance) ClusterLocalFQDN() string {
   199  	return c.cfg.ClusterLocalFQDN()
   200  }
   201  
   202  func (c *instance) ClusterSetLocalFQDN() string {
   203  	return c.cfg.ClusterSetLocalFQDN()
   204  }
   205  
   206  func (c *instance) Config() echo.Config {
   207  	return c.cfg
   208  }
   209  
   210  func (c *instance) WithWorkloads(wls ...echo.Workload) echo.Instance {
   211  	n := *c
   212  	n.workloadFilter = wls
   213  	return &n
   214  }
   215  
   216  func (c *instance) Cluster() cluster.Cluster {
   217  	return c.cfg.Cluster
   218  }
   219  
   220  func (c *instance) Call(opts echo.CallOptions) (echo.CallResult, error) {
   221  	// Setup default check. This is done here rather than in echo core package to avoid import loops
   222  	if opts.Check == nil {
   223  		opts.Check = check.OK()
   224  	}
   225  	return c.aggregateResponses(opts)
   226  }
   227  
   228  func (c *instance) CallOrFail(t test.Failer, opts echo.CallOptions) echo.CallResult {
   229  	t.Helper()
   230  	r, err := c.Call(opts)
   231  	if err != nil {
   232  		t.Fatal(err)
   233  	}
   234  	return r
   235  }
   236  
   237  func (c *instance) GetWorkloadLabels(labels map[string]string) error {
   238  	for _, wl := range c.workloadMgr.workloads {
   239  		wl.mutex.Lock()
   240  		pod := wl.pod
   241  		wl.mutex.Unlock()
   242  		if pod.Name != "" {
   243  			pod.Labels = labels
   244  			_, err := wl.Cluster().Kube().CoreV1().Pods(c.NamespaceName()).Update(context.TODO(), &pod, metav1.UpdateOptions{})
   245  			return fmt.Errorf("update pod labels failed: %v", err)
   246  		}
   247  	}
   248  	return nil
   249  }
   250  
   251  func (c *instance) UpdateWorkloadLabel(add map[string]string, remove []string) error {
   252  	for _, wl := range c.workloadMgr.workloads {
   253  		wl.mutex.Lock()
   254  		pod := wl.pod
   255  		wl.mutex.Unlock()
   256  		if pod.Name != "" {
   257  			return retry.UntilSuccess(func() (err error) {
   258  				podName := pod.Name
   259  				podNamespace := pod.Namespace
   260  				pod, err := wl.Cluster().Kube().CoreV1().Pods(c.NamespaceName()).Get(context.TODO(), podName, metav1.GetOptions{})
   261  				if err != nil {
   262  					return fmt.Errorf("get pod %s/%s failed: %v", podNamespace, podName, err)
   263  				}
   264  				newLabels := make(map[string]string)
   265  				for k, v := range pod.GetLabels() {
   266  					newLabels[k] = v
   267  				}
   268  				for k, v := range add {
   269  					newLabels[k] = v
   270  				}
   271  				for _, k := range remove {
   272  					delete(newLabels, k)
   273  				}
   274  				pod.Labels = newLabels
   275  				_, err = wl.Cluster().Kube().CoreV1().Pods(c.NamespaceName()).Update(context.TODO(), pod, metav1.UpdateOptions{})
   276  				if err != nil {
   277  					return fmt.Errorf("update pod labels failed: %v", err)
   278  				}
   279  				return nil
   280  			}, retry.Timeout(c.cfg.ReadinessTimeout), startDelay)
   281  		}
   282  	}
   283  	return nil
   284  }
   285  
   286  func (c *instance) Restart() error {
   287  	// Wait for all current workloads to become ready and preserve the original count.
   288  	origWorkloads, err := c.workloadMgr.WaitForReadyWorkloads()
   289  	if err != nil {
   290  		return fmt.Errorf("restart failed to get initial workloads: %v", err)
   291  	}
   292  
   293  	// Restart the deployment.
   294  	if err := c.deployment.Restart(); err != nil {
   295  		return err
   296  	}
   297  
   298  	// Wait until all pods are ready and match the original count.
   299  	return retry.UntilSuccess(func() (err error) {
   300  		// Get the currently ready workloads.
   301  		workloads, err := c.workloadMgr.WaitForReadyWorkloads()
   302  		if err != nil {
   303  			return fmt.Errorf("failed waiting for restarted pods for echo %s/%s: %v",
   304  				c.cfg.Namespace.Name(), c.cfg.Service, err)
   305  		}
   306  
   307  		// Make sure the number of pods matches the original.
   308  		if len(workloads) != len(origWorkloads) {
   309  			return fmt.Errorf("failed restarting echo %s/%s: number of pods %d does not match original %d",
   310  				c.cfg.Namespace.Name(), c.cfg.Service, len(workloads), len(origWorkloads))
   311  		}
   312  
   313  		return nil
   314  	}, retry.Timeout(c.cfg.ReadinessTimeout), startDelay)
   315  }
   316  
   317  // aggregateResponses forwards an echo request from all workloads belonging to this echo instance and aggregates the results.
   318  func (c *instance) aggregateResponses(opts echo.CallOptions) (echo.CallResult, error) {
   319  	// TODO put this somewhere else, or require users explicitly set the protocol - quite hacky
   320  	if c.Config().IsProxylessGRPC() && (opts.Scheme == scheme.GRPC || opts.Port.Name == "grpc" || opts.Port.Protocol == protocol.GRPC) {
   321  		// for gRPC calls, use XDS resolver
   322  		opts.Scheme = scheme.XDS
   323  	}
   324  
   325  	resps := make(echoClient.Responses, 0)
   326  	workloads, err := c.Workloads()
   327  	if err != nil {
   328  		return echo.CallResult{}, err
   329  	}
   330  	aggErr := istiomultierror.New()
   331  	for _, w := range workloads {
   332  		clusterName := w.(*workload).cluster.Name()
   333  		serviceName := fmt.Sprintf("%s (cluster=%s)", c.cfg.Service, clusterName)
   334  
   335  		out, err := common.ForwardEcho(serviceName, c, opts, w.(*workload).Client)
   336  		if err != nil {
   337  			aggErr = multierror.Append(aggErr, err)
   338  			continue
   339  		}
   340  		resps = append(resps, out.Responses...)
   341  	}
   342  	if aggErr.ErrorOrNil() != nil {
   343  		return echo.CallResult{}, aggErr
   344  	}
   345  
   346  	return echo.CallResult{
   347  		From:      c,
   348  		Opts:      opts,
   349  		Responses: resps,
   350  	}, nil
   351  }