istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/wait/wait_test.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 wait
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/spf13/cobra"
    26  	"k8s.io/apimachinery/pkg/api/meta"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/watch"
    31  	dynamicfake "k8s.io/client-go/dynamic/fake"
    32  	clienttesting "k8s.io/client-go/testing"
    33  
    34  	"istio.io/istio/istioctl/pkg/cli"
    35  	"istio.io/istio/pilot/pkg/xds"
    36  	"istio.io/istio/pkg/config/schema/gvk"
    37  	"istio.io/istio/pkg/config/schema/gvr"
    38  )
    39  
    40  func TestWaitCmd(t *testing.T) {
    41  	cannedResponseObj := []xds.SyncedVersions{
    42  		{
    43  			ProxyID:         "foo",
    44  			ClusterVersion:  "1",
    45  			ListenerVersion: "1",
    46  			RouteVersion:    "1",
    47  			EndpointVersion: "1",
    48  		},
    49  	}
    50  	cannedResponse, _ := json.Marshal(cannedResponseObj)
    51  	cannedResponseMap := map[string][]byte{"onlyonepilot": cannedResponse}
    52  
    53  	distributionTrackingDisabledResponse := xds.DistributionTrackingDisabledMessage
    54  	distributionTrackingDisabledResponseMap := map[string][]byte{"onlyonepilot": []byte(distributionTrackingDisabledResponse)}
    55  
    56  	cases := []execTestCase{
    57  		{
    58  			execClientConfig: cannedResponseMap,
    59  			args:             strings.Split("--generation=2 --timeout=20ms virtual-service foo.default", " "),
    60  			wantException:    true,
    61  		},
    62  		{
    63  			execClientConfig: cannedResponseMap,
    64  			args:             strings.Split("--generation=1 virtual-service foo.default", " "),
    65  			wantException:    false,
    66  		},
    67  		{
    68  			execClientConfig: cannedResponseMap,
    69  			args:             strings.Split("--generation=1 VirtualService foo.default", " "),
    70  			wantException:    false,
    71  		},
    72  		{
    73  			execClientConfig: cannedResponseMap,
    74  			args:             strings.Split("--generation=1 VirtualService foo.default --proxy foo", " "),
    75  			wantException:    false,
    76  		},
    77  		{
    78  			execClientConfig: cannedResponseMap,
    79  			args:             strings.Split("--generation=1 --timeout 20ms VirtualService foo.default --proxy not-proxy", " "),
    80  			wantException:    true,
    81  			expectedOutput:   "timeout expired before resource",
    82  		},
    83  		{
    84  			execClientConfig: cannedResponseMap,
    85  			args:             strings.Split("--generation=1 not-service foo.default", " "),
    86  			wantException:    true,
    87  			expectedOutput:   "type not-service is not recognized",
    88  		},
    89  		{
    90  			execClientConfig: cannedResponseMap,
    91  			args:             strings.Split("--timeout 20ms virtual-service bar.default", " "),
    92  			wantException:    true,
    93  			expectedOutput:   "Error: timeout expired before resource networking.istio.io/v1alpha3/VirtualService/default/bar became effective on all sidecars\n",
    94  		},
    95  		{
    96  			execClientConfig: cannedResponseMap,
    97  			args:             strings.Split("--timeout 2s virtualservice foo.default", " "),
    98  			wantException:    false,
    99  		},
   100  		{
   101  			execClientConfig: cannedResponseMap,
   102  			args:             strings.Split("--timeout 2s --revision canary virtualservice foo.default", " "),
   103  			wantException:    false,
   104  		},
   105  		{
   106  			execClientConfig: distributionTrackingDisabledResponseMap,
   107  			args:             strings.Split("--timeout 2s --revision canary virtualservice foo.default", " "),
   108  			wantException:    true,
   109  			expectedOutput:   distributionTrackingDisabledErrorString,
   110  		},
   111  	}
   112  
   113  	for i, c := range cases {
   114  		t.Run(fmt.Sprintf("case %d %s", i, strings.Join(c.args, " ")), func(t *testing.T) {
   115  			cl := cli.NewFakeContext(&cli.NewFakeContextOption{
   116  				Namespace: "default",
   117  				Results:   c.execClientConfig,
   118  			})
   119  			k, err := cl.CLIClient()
   120  			if err != nil {
   121  				t.Fatal(err)
   122  			}
   123  			fc := k.Dynamic().(*dynamicfake.FakeDynamicClient)
   124  			fc.PrependWatchReactor("*", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
   125  				gvr := action.GetResource()
   126  				ns := action.GetNamespace()
   127  				watch, err := fc.Tracker().Watch(gvr, ns)
   128  				if err != nil {
   129  					return false, nil, err
   130  				}
   131  				// Kubernetes Fake watches do not add initial state, but real server does
   132  				// https://kubernetes.io/docs/reference/using-api/api-concepts/#semantics-for-watch
   133  				// Tracking in https://github.com/kubernetes/kubernetes/issues/123109
   134  				gvk := gvk.MustFromGVR(gvr).Kubernetes()
   135  				objs, err := fc.Tracker().List(gvr, gvk, ns)
   136  				// This can happen for PartialMetadata objects unfortunately.. so just continue
   137  				if err == nil {
   138  					l, err := meta.ExtractList(objs)
   139  					if err != nil {
   140  						return false, nil, err
   141  					}
   142  					for _, object := range l {
   143  						watch.(addObject).Add(object)
   144  					}
   145  				}
   146  				return true, watch, nil
   147  			})
   148  			k.Dynamic().Resource(gvr.VirtualService).Namespace("default").Create(context.Background(),
   149  				newUnstructured("networking.istio.io/v1alpha3", "virtualservice", "default", "foo", int64(1)),
   150  				metav1.CreateOptions{})
   151  			k.Dynamic().Resource(gvr.VirtualService).Namespace("default").Create(context.Background(),
   152  				newUnstructured("networking.istio.io/v1alpha3", "virtualservice", "default", "bar", int64(3)),
   153  				metav1.CreateOptions{})
   154  			verifyExecTestOutput(t, Cmd(cl), c)
   155  		})
   156  	}
   157  }
   158  
   159  type addObject interface {
   160  	Add(obj runtime.Object)
   161  }
   162  
   163  type execTestCase struct {
   164  	execClientConfig map[string][]byte
   165  	args             []string
   166  
   167  	// Typically use one of the three
   168  	expectedOutput string // Expected constant output
   169  
   170  	wantException bool
   171  }
   172  
   173  func verifyExecTestOutput(t *testing.T, cmd *cobra.Command, c execTestCase) {
   174  	if c.wantException {
   175  		// Ensure tests do not hang for 30s
   176  		c.args = append(c.args, "--timeout=20ms")
   177  	}
   178  	var out bytes.Buffer
   179  	cmd.SetArgs(c.args)
   180  	cmd.SilenceUsage = true
   181  	cmd.SetOut(&out)
   182  	cmd.SetErr(&out)
   183  
   184  	fErr := cmd.Execute()
   185  	output := out.String()
   186  
   187  	if c.expectedOutput != "" && !strings.Contains(output, c.expectedOutput) {
   188  		t.Fatalf("Unexpected output for 'istioctl %s'\n got: %q\nwant: %q", strings.Join(c.args, " "), output, c.expectedOutput)
   189  	}
   190  
   191  	if c.wantException {
   192  		if fErr == nil {
   193  			t.Fatalf("Wanted an exception for 'istioctl %s', didn't get one, output was %q",
   194  				strings.Join(c.args, " "), output)
   195  		}
   196  	} else {
   197  		if fErr != nil {
   198  			t.Fatalf("Unwanted exception for 'istioctl %s': %v", strings.Join(c.args, " "), fErr)
   199  		}
   200  	}
   201  }
   202  
   203  func newUnstructured(apiVersion, kind, namespace, name string, generation int64) *unstructured.Unstructured {
   204  	return &unstructured.Unstructured{
   205  		Object: map[string]any{
   206  			"apiVersion": apiVersion,
   207  			"kind":       kind,
   208  			"metadata": map[string]any{
   209  				"namespace":  namespace,
   210  				"name":       name,
   211  				"generation": generation,
   212  			},
   213  		},
   214  	}
   215  }