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 }