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 }