istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/multixds/gather.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 multixds 16 17 // multixds knows how to target either central Istiod or all the Istiod pods on a cluster. 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "net" 26 "net/url" 27 "os" 28 "strings" 29 30 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 31 xdsstatus "github.com/envoyproxy/go-control-plane/envoy/service/status/v3" 32 "google.golang.org/grpc" 33 v1 "k8s.io/api/core/v1" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 36 "istio.io/api/label" 37 "istio.io/istio/istioctl/pkg/clioptions" 38 "istio.io/istio/istioctl/pkg/xds" 39 pilotxds "istio.io/istio/pilot/pkg/xds" 40 "istio.io/istio/pkg/kube" 41 istioversion "istio.io/istio/pkg/version" 42 ) 43 44 const ( 45 // Service account to create tokens in 46 tokenServiceAccount = "default" 47 // Get the pods with limit = 500. 48 kubeClientGetPodLimit = 500 49 ) 50 51 type ControlPlaneNotFoundError struct { 52 Namespace string 53 } 54 55 func (c ControlPlaneNotFoundError) Error() string { 56 return fmt.Sprintf("no running Istio pods in %q", c.Namespace) 57 } 58 59 var _ error = ControlPlaneNotFoundError{} 60 61 type Options struct { 62 // MessageWriter is a writer for displaying messages to users. 63 MessageWriter io.Writer 64 65 // XdsViaAgents accesses Istiod via the tap service of each agent. 66 // This is only used in `proxy-status` command. 67 XdsViaAgents bool 68 69 // XdsViaAgentsLimit is the maximum number of pods being visited by istioctl, 70 // when `XdsViaAgents` is true. This is only used in `proxy-status` command. 71 // 0 means that there is no limit. 72 XdsViaAgentsLimit int 73 } 74 75 var DefaultOptions = Options{ 76 MessageWriter: os.Stdout, 77 XdsViaAgents: false, 78 XdsViaAgentsLimit: 0, 79 } 80 81 // RequestAndProcessXds merges XDS responses from 1 central or 1..N K8s cluster-based XDS servers 82 // Deprecated This method makes multiple responses appear to come from a single control plane; 83 // consider using AllRequestAndProcessXds or FirstRequestAndProcessXds 84 // nolint: lll 85 func RequestAndProcessXds(dr *discovery.DiscoveryRequest, centralOpts clioptions.CentralControlPlaneOptions, istioNamespace string, kubeClient kube.CLIClient) (*discovery.DiscoveryResponse, error) { 86 responses, err := MultiRequestAndProcessXds(true, dr, centralOpts, istioNamespace, 87 istioNamespace, tokenServiceAccount, kubeClient, DefaultOptions) 88 if err != nil { 89 return nil, err 90 } 91 return mergeShards(responses) 92 } 93 94 var GetXdsResponse = xds.GetXdsResponse 95 96 // nolint: lll 97 func queryEachShard(all bool, dr *discovery.DiscoveryRequest, istioNamespace string, kubeClient kube.CLIClient, centralOpts clioptions.CentralControlPlaneOptions) ([]*discovery.DiscoveryResponse, error) { 98 labelSelector := centralOpts.XdsPodLabel 99 if labelSelector == "" { 100 labelSelector = "app=istiod" 101 } 102 pods, err := kubeClient.GetIstioPods(context.TODO(), istioNamespace, metav1.ListOptions{ 103 LabelSelector: labelSelector, 104 FieldSelector: kube.RunningStatus, 105 }) 106 if err != nil { 107 return nil, err 108 } 109 if len(pods) == 0 { 110 return nil, ControlPlaneNotFoundError{istioNamespace} 111 } 112 113 responses := []*discovery.DiscoveryResponse{} 114 xdsOpts := clioptions.CentralControlPlaneOptions{ 115 XDSSAN: makeSan(istioNamespace, kubeClient.Revision()), 116 CertDir: centralOpts.CertDir, 117 Timeout: centralOpts.Timeout, 118 } 119 dialOpts, err := xds.DialOptions(xdsOpts, istioNamespace, tokenServiceAccount, kubeClient) 120 if err != nil { 121 return nil, err 122 } 123 124 for _, pod := range pods { 125 fw, err := kubeClient.NewPortForwarder(pod.Name, pod.Namespace, "localhost", 0, centralOpts.XdsPodPort) 126 if err != nil { 127 return nil, err 128 } 129 err = fw.Start() 130 if err != nil { 131 return nil, err 132 } 133 defer fw.Close() 134 xdsOpts.Xds = fw.Address() 135 response, err := GetXdsResponse(dr, istioNamespace, tokenServiceAccount, xdsOpts, dialOpts) 136 if err != nil { 137 return nil, fmt.Errorf("could not get XDS from discovery pod %q: %v", pod.Name, err) 138 } 139 responses = append(responses, response) 140 if !all && len(responses) > 0 { 141 break 142 } 143 } 144 return responses, nil 145 } 146 147 // queryDebugSynczViaAgents sends a debug/syncz xDS request via Istio Agents. 148 // By this way, even if istioctl cannot access a specific `istiod` instance directly, 149 // `istioctl` can access the debug endpoint. 150 // If `all` is true, `queryDebugSynczViaAgents` iterates all the pod having a proxy 151 // except the pods of which status information is already queried. 152 func queryDebugSynczViaAgents(all bool, dr *discovery.DiscoveryRequest, istioNamespace string, kubeClient kube.CLIClient, 153 centralOpts clioptions.CentralControlPlaneOptions, options Options, 154 ) ([]*discovery.DiscoveryResponse, error) { 155 xdsOpts := clioptions.CentralControlPlaneOptions{ 156 XDSSAN: makeSan(istioNamespace, kubeClient.Revision()), 157 CertDir: centralOpts.CertDir, 158 Timeout: centralOpts.Timeout, 159 } 160 visited := make(map[string]bool) 161 queryToOnePod := func(pod *v1.Pod) (*discovery.DiscoveryResponse, error) { 162 fw, err := kubeClient.NewPortForwarder(pod.Name, pod.Namespace, "localhost", 0, 15004) 163 if err != nil { 164 return nil, err 165 } 166 err = fw.Start() 167 if err != nil { 168 return nil, err 169 } 170 defer fw.Close() 171 xdsOpts.Xds = fw.Address() 172 // Use plaintext. 173 response, err := xds.GetXdsResponse(dr, istioNamespace, tokenServiceAccount, xdsOpts, []grpc.DialOption{}) 174 if err != nil { 175 return nil, fmt.Errorf("could not get XDS from the agent pod %q: %v", pod.Name, err) 176 } 177 for _, resource := range response.GetResources() { 178 switch resource.GetTypeUrl() { 179 case "type.googleapis.com/envoy.service.status.v3.ClientConfig": 180 clientConfig := xdsstatus.ClientConfig{} 181 err := resource.UnmarshalTo(&clientConfig) 182 if err != nil { 183 return nil, err 184 } 185 visited[clientConfig.Node.Id] = true 186 default: 187 // ignore unknown types. 188 } 189 } 190 return response, nil 191 } 192 193 responses := []*discovery.DiscoveryResponse{} 194 if all { 195 token := "" 196 touchedPods := 0 197 198 GetProxyLoop: 199 for { 200 list, err := kubeClient.GetProxyPods(context.TODO(), int64(kubeClientGetPodLimit), token) 201 if err != nil { 202 return nil, err 203 } 204 // Iterate all the pod. 205 for _, pod := range list.Items { 206 touchedPods++ 207 if options.XdsViaAgentsLimit != 0 && touchedPods > options.XdsViaAgentsLimit { 208 fmt.Fprintf(options.MessageWriter, "Some proxies may be missing from the list"+ 209 " because the number of visited pod hits the limit %d,"+ 210 " which can be set by `--xds-via-agents-limit` flag.\n", options.XdsViaAgentsLimit) 211 break GetProxyLoop 212 } 213 namespacedName := pod.Name + "." + pod.Namespace 214 if visited[namespacedName] { 215 // If we already have information about the pod, skip it. 216 continue 217 } 218 resp, err := queryToOnePod(&pod) 219 if err != nil { 220 fmt.Fprintf(os.Stderr, "Skip the agent in Pod %s due to the error: %s\n", namespacedName, err.Error()) 221 continue 222 } 223 responses = append(responses, resp) 224 } 225 token = list.ListMeta.GetContinue() 226 if token == "" { 227 break 228 } 229 } 230 } else { 231 // If there is a specific pod name in ResourceName, use the agent in the pod. 232 if len(dr.ResourceNames) != 1 { 233 return nil, fmt.Errorf("`ResourceNames` must have one element when `all` flag is turned on") 234 } 235 slice := strings.SplitN(dr.ResourceNames[0], ".", 2) 236 if len(slice) != 2 { 237 return nil, fmt.Errorf("invalid resource name format: %v", slice) 238 } 239 podName := slice[0] 240 ns := slice[1] 241 pod, err := kubeClient.Kube().CoreV1().Pods(ns).Get(context.TODO(), podName, metav1.GetOptions{}) 242 if err != nil { 243 return nil, err 244 } 245 resp, err := queryToOnePod(pod) 246 if err != nil { 247 return nil, err 248 } 249 responses = append(responses, resp) 250 return responses, nil 251 } 252 253 return responses, nil 254 } 255 256 func mergeShards(responses map[string]*discovery.DiscoveryResponse) (*discovery.DiscoveryResponse, error) { 257 retval := discovery.DiscoveryResponse{} 258 if len(responses) == 0 { 259 return &retval, nil 260 } 261 262 for _, response := range responses { 263 // Combine all the shards as one, even if that means losing information about 264 // the control plane version from each shard. 265 retval.ControlPlane = response.ControlPlane 266 retval.Resources = append(retval.Resources, response.Resources...) 267 } 268 269 return &retval, nil 270 } 271 272 func makeSan(istioNamespace, revision string) string { 273 if revision == "" { 274 return fmt.Sprintf("istiod.%s.svc", istioNamespace) 275 } 276 return fmt.Sprintf("istiod-%s.%s.svc", revision, istioNamespace) 277 } 278 279 // AllRequestAndProcessXds returns all XDS responses from 1 central or 1..N K8s cluster-based XDS servers 280 // nolint: lll 281 func AllRequestAndProcessXds(dr *discovery.DiscoveryRequest, centralOpts clioptions.CentralControlPlaneOptions, istioNamespace string, 282 ns string, serviceAccount string, kubeClient kube.CLIClient, options Options, 283 ) (map[string]*discovery.DiscoveryResponse, error) { 284 return MultiRequestAndProcessXds(true, dr, centralOpts, istioNamespace, ns, serviceAccount, kubeClient, options) 285 } 286 287 // FirstRequestAndProcessXds returns all XDS responses from 1 central or 1..N K8s cluster-based XDS servers, 288 // stopping after the first response that returns any resources. 289 // nolint: lll 290 func FirstRequestAndProcessXds(dr *discovery.DiscoveryRequest, centralOpts clioptions.CentralControlPlaneOptions, istioNamespace string, 291 ns string, serviceAccount string, kubeClient kube.CLIClient, options Options, 292 ) (map[string]*discovery.DiscoveryResponse, error) { 293 return MultiRequestAndProcessXds(false, dr, centralOpts, istioNamespace, ns, serviceAccount, kubeClient, options) 294 } 295 296 type xdsAddr struct { 297 gcpProject, host, istiod string 298 } 299 300 func getXdsAddressFromWebhooks(client kube.CLIClient) (*xdsAddr, error) { 301 webhooks, err := client.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), metav1.ListOptions{ 302 LabelSelector: fmt.Sprintf("%s=%s,!istio.io/tag", label.IoIstioRev.Name, client.Revision()), 303 }) 304 if err != nil { 305 return nil, err 306 } 307 for _, whc := range webhooks.Items { 308 for _, wh := range whc.Webhooks { 309 if wh.ClientConfig.URL != nil { 310 u, err := url.Parse(*wh.ClientConfig.URL) 311 if err != nil { 312 return nil, fmt.Errorf("parsing webhook URL: %w", err) 313 } 314 if isMCPAddr(u) { 315 return parseMCPAddr(u) 316 } 317 port := u.Port() 318 if port == "" { 319 port = "443" // default from Kubernetes 320 } 321 return &xdsAddr{host: net.JoinHostPort(u.Hostname(), port)}, nil 322 } 323 } 324 } 325 return nil, errors.New("xds address not found") 326 } 327 328 // nolint: lll 329 func MultiRequestAndProcessXds(all bool, dr *discovery.DiscoveryRequest, centralOpts clioptions.CentralControlPlaneOptions, istioNamespace string, 330 ns string, serviceAccount string, kubeClient kube.CLIClient, options Options, 331 ) (map[string]*discovery.DiscoveryResponse, error) { 332 // If Central Istiod case, just call it 333 if ns == "" { 334 ns = istioNamespace 335 } 336 if ns == istioNamespace { 337 serviceAccount = tokenServiceAccount 338 } 339 if centralOpts.Xds != "" { 340 dialOpts, err := xds.DialOptions(centralOpts, ns, serviceAccount, kubeClient) 341 if err != nil { 342 return nil, err 343 } 344 response, err := xds.GetXdsResponse(dr, ns, serviceAccount, centralOpts, dialOpts) 345 if err != nil { 346 return nil, err 347 } 348 return map[string]*discovery.DiscoveryResponse{ 349 CpInfo(response).ID: response, 350 }, nil 351 } 352 353 var ( 354 responses []*discovery.DiscoveryResponse 355 err error 356 ) 357 358 if options.XdsViaAgents { 359 responses, err = queryDebugSynczViaAgents(all, dr, istioNamespace, kubeClient, centralOpts, options) 360 } else { 361 // Self-administered case. Find all Istiods in revision using K8s, port-forward and call each in turn 362 responses, err = queryEachShard(all, dr, istioNamespace, kubeClient, centralOpts) 363 } 364 if err != nil { 365 if _, ok := err.(ControlPlaneNotFoundError); ok { 366 // Attempt to get the XDS address from the webhook and try again 367 addr, err := getXdsAddressFromWebhooks(kubeClient) 368 if err == nil { 369 centralOpts.Xds = addr.host 370 centralOpts.GCPProject = addr.gcpProject 371 centralOpts.IstiodAddr = addr.istiod 372 dialOpts, err := xds.DialOptions(centralOpts, istioNamespace, tokenServiceAccount, kubeClient) 373 if err != nil { 374 return nil, err 375 } 376 response, err := xds.GetXdsResponse(dr, istioNamespace, tokenServiceAccount, centralOpts, dialOpts) 377 if err != nil { 378 return nil, err 379 } 380 return map[string]*discovery.DiscoveryResponse{ 381 CpInfo(response).ID: response, 382 }, nil 383 } 384 } 385 return nil, err 386 } 387 return mapShards(responses) 388 } 389 390 func mapShards(responses []*discovery.DiscoveryResponse) (map[string]*discovery.DiscoveryResponse, error) { 391 retval := map[string]*discovery.DiscoveryResponse{} 392 393 for _, response := range responses { 394 retval[CpInfo(response).ID] = response 395 } 396 397 return retval, nil 398 } 399 400 // CpInfo returns the Istio control plane info from JSON-encoded XDS ControlPlane Identifier 401 func CpInfo(xdsResponse *discovery.DiscoveryResponse) pilotxds.IstioControlPlaneInstance { 402 if xdsResponse.ControlPlane == nil { 403 return pilotxds.IstioControlPlaneInstance{ 404 Component: "MISSING", 405 ID: "MISSING", 406 Info: istioversion.BuildInfo{ 407 Version: "MISSING CP ID", 408 }, 409 } 410 } 411 412 cpID := pilotxds.IstioControlPlaneInstance{} 413 err := json.Unmarshal([]byte(xdsResponse.ControlPlane.Identifier), &cpID) 414 if err != nil { 415 return pilotxds.IstioControlPlaneInstance{ 416 Component: "INVALID", 417 ID: "INVALID", 418 Info: istioversion.BuildInfo{ 419 Version: "INVALID CP ID", 420 }, 421 } 422 } 423 return cpID 424 }