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 }