istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/precheck/precheck.go (about) 1 // Copyright © 2021 NAME HERE <EMAIL ADDRESS> 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 precheck 16 17 import ( 18 "context" 19 "errors" 20 "fmt" 21 "strconv" 22 "strings" 23 24 "github.com/fatih/color" 25 "github.com/spf13/cobra" 26 "gopkg.in/yaml.v2" 27 authorizationapi "k8s.io/api/authorization/v1" 28 corev1 "k8s.io/api/core/v1" 29 crd "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 30 kerrors "k8s.io/apimachinery/pkg/api/errors" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 33 "istio.io/api/label" 34 networking "istio.io/api/networking/v1alpha3" 35 "istio.io/istio/istioctl/pkg/cli" 36 "istio.io/istio/istioctl/pkg/clioptions" 37 "istio.io/istio/istioctl/pkg/install/k8sversion" 38 "istio.io/istio/istioctl/pkg/util/formatting" 39 istiocluster "istio.io/istio/pkg/cluster" 40 "istio.io/istio/pkg/config" 41 "istio.io/istio/pkg/config/analysis" 42 "istio.io/istio/pkg/config/analysis/analyzers/maturity" 43 "istio.io/istio/pkg/config/analysis/diag" 44 legacykube "istio.io/istio/pkg/config/analysis/legacy/source/kube" 45 "istio.io/istio/pkg/config/analysis/local" 46 "istio.io/istio/pkg/config/analysis/msg" 47 "istio.io/istio/pkg/config/host" 48 "istio.io/istio/pkg/config/resource" 49 "istio.io/istio/pkg/config/schema/gvk" 50 "istio.io/istio/pkg/config/schema/kubetypes" 51 "istio.io/istio/pkg/kube" 52 "istio.io/istio/pkg/kube/controllers" 53 "istio.io/istio/pkg/url" 54 "istio.io/istio/pkg/util/sets" 55 ) 56 57 func Cmd(ctx cli.Context) *cobra.Command { 58 var opts clioptions.ControlPlaneOptions 59 var skipControlPlane bool 60 outputThreshold := formatting.MessageThreshold{Level: diag.Warning} 61 var msgOutputFormat string 62 var fromCompatibilityVersion string 63 // cmd represents the upgradeCheck command 64 cmd := &cobra.Command{ 65 Use: "precheck", 66 Short: "Check whether Istio can safely be installed or upgraded", 67 Long: `precheck inspects a Kubernetes cluster for Istio install and upgrade requirements.`, 68 Example: ` # Verify that Istio can be installed or upgraded 69 istioctl x precheck 70 71 # Check only a single namespace 72 istioctl x precheck --namespace default 73 74 # Check for behavioral changes since a specific version 75 istioctl x precheck --from-version 1.10`, 76 RunE: func(cmd *cobra.Command, args []string) (err error) { 77 msgs := diag.Messages{} 78 if !skipControlPlane { 79 msgs, err = checkControlPlane(ctx) 80 if err != nil { 81 return err 82 } 83 } 84 85 if fromCompatibilityVersion != "" { 86 m, err := checkFromVersion(ctx, opts.Revision, fromCompatibilityVersion) 87 if err != nil { 88 return err 89 } 90 msgs = append(msgs, m...) 91 } 92 93 // Print all the messages to stdout in the specified format 94 msgs = msgs.SortedDedupedCopy() 95 outputMsgs := diag.Messages{} 96 for _, m := range msgs { 97 if m.Type.Level().IsWorseThanOrEqualTo(outputThreshold.Level) { 98 outputMsgs = append(outputMsgs, m) 99 } 100 } 101 output, err := formatting.Print(outputMsgs, msgOutputFormat, true) 102 if err != nil { 103 return err 104 } 105 106 if len(outputMsgs) == 0 { 107 fmt.Fprintf(cmd.ErrOrStderr(), color.New(color.FgGreen).Sprint("✔")+" No issues found when checking the cluster. Istio is safe to install or upgrade!\n"+ 108 " To get started, check out https://istio.io/latest/docs/setup/getting-started/\n") 109 } else { 110 fmt.Fprintln(cmd.OutOrStdout(), output) 111 } 112 for _, m := range msgs { 113 if m.Type.Level().IsWorseThanOrEqualTo(diag.Warning) { 114 e := fmt.Sprintf(`Issues found when checking the cluster. Istio may not be safe to install or upgrade. 115 See %s for more information about causes and resolutions.`, url.ConfigAnalysis) 116 return errors.New(e) 117 } 118 } 119 return nil 120 }, 121 } 122 cmd.PersistentFlags().BoolVar(&skipControlPlane, "skip-controlplane", false, "skip checking the control plane") 123 cmd.PersistentFlags().Var(&outputThreshold, "output-threshold", 124 fmt.Sprintf("The severity level of precheck at which to display messages. Valid values: %v", diag.GetAllLevelStrings())) 125 cmd.PersistentFlags().StringVarP(&msgOutputFormat, "output", "o", formatting.LogFormat, 126 fmt.Sprintf("Output format: one of %v", formatting.MsgOutputFormatKeys)) 127 cmd.PersistentFlags().StringVarP(&fromCompatibilityVersion, "from-version", "f", "", 128 "check changes since the provided version") 129 opts.AttachControlPlaneFlags(cmd) 130 return cmd 131 } 132 133 func checkFromVersion(ctx cli.Context, revision, version string) (diag.Messages, error) { 134 cli, err := ctx.CLIClientWithRevision(revision) 135 if err != nil { 136 return nil, err 137 } 138 major, minors, ok := strings.Cut(version, ".") 139 if !ok { 140 return nil, fmt.Errorf("invalid version %v, expected format like '1.0'", version) 141 } 142 if major != "1" { 143 return nil, fmt.Errorf("expected major version 1, got %v", version) 144 } 145 minor, err := strconv.Atoi(minors) 146 if err != nil { 147 return nil, fmt.Errorf("minor version is not a number: %v", minors) 148 } 149 150 var messages diag.Messages = make([]diag.Message, 0) 151 if minor <= 21 { 152 // ENHANCED_RESOURCE_SCOPING 153 if err := checkPilot(cli, ctx.IstioNamespace(), &messages); err != nil { 154 return nil, err 155 } 156 } 157 if minor <= 20 { 158 // VERIFY_CERTIFICATE_AT_CLIENT and ENABLE_AUTO_SNI 159 if err := checkDestinationRuleTLS(cli, &messages); err != nil { 160 return nil, err 161 } 162 // ENABLE_EXTERNAL_NAME_ALIAS 163 if err := checkExternalNameAlias(cli, &messages); err != nil { 164 return nil, err 165 } 166 // PERSIST_OLDEST_FIRST_HEURISTIC_FOR_VIRTUAL_SERVICE_HOST_MATCHING 167 if err := checkVirtualServiceHostMatching(cli, &messages); err != nil { 168 return nil, err 169 } 170 } 171 if minor <= 21 { 172 if err := checkPassthroughTargetPorts(cli, &messages); err != nil { 173 return nil, err 174 } 175 if err := checkTracing(cli, &messages); err != nil { 176 return nil, err 177 } 178 } 179 return messages, nil 180 } 181 182 func checkTracing(cli kube.CLIClient, messages *diag.Messages) error { 183 // In 1.22, we remove the default tracing config which points to zipkin.istio-system 184 // This has no effect for users, unless they have this service. 185 svc, err := cli.Kube().CoreV1().Services("istio-system").Get(context.Background(), "zipkin", metav1.GetOptions{}) 186 if err != nil && !kerrors.IsNotFound(err) { 187 return err 188 } 189 if err != nil { 190 // not found 191 return nil 192 } 193 // found 194 res := ObjectToInstance(svc) 195 messages.Add(msg.NewUpdateIncompatibility(res, 196 "meshConfig.defaultConfig.tracer", "1.21", 197 "tracing is no longer by default enabled to send to 'zipkin.istio-system.svc'; "+ 198 "follow https://istio.io/latest/docs/tasks/observability/distributed-tracing/telemetry-api/", 199 "1.21")) 200 return nil 201 } 202 203 func checkPassthroughTargetPorts(cli kube.CLIClient, messages *diag.Messages) error { 204 ses, err := cli.Istio().NetworkingV1alpha3().ServiceEntries(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{}) 205 if err != nil { 206 return err 207 } 208 for _, se := range ses.Items { 209 if se.Spec.Resolution != networking.ServiceEntry_NONE { 210 continue 211 } 212 changed := false 213 for _, p := range se.Spec.Ports { 214 if p.TargetPort != 0 && p.Number != p.TargetPort { 215 changed = true 216 } 217 } 218 if changed { 219 res := ObjectToInstance(se) 220 messages.Add(msg.NewUpdateIncompatibility(res, 221 "ENABLE_RESOLUTION_NONE_TARGET_PORT", "1.21", 222 "ServiceEntry with resolution NONE and a targetPort set previously did nothing but now is respected", "1.21")) 223 } 224 } 225 return nil 226 } 227 228 func checkExternalNameAlias(cli kube.CLIClient, messages *diag.Messages) error { 229 svcs, err := cli.Kube().CoreV1().Services(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{}) 230 if err != nil { 231 return err 232 } 233 for _, svc := range svcs.Items { 234 if svc.Spec.Type != corev1.ServiceTypeExternalName { 235 continue 236 } 237 res := ObjectToInstance(&svc) 238 messages.Add(msg.NewUpdateIncompatibility(res, 239 "ENABLE_EXTERNAL_NAME_ALIAS", "1.20", 240 "ExternalName services now behavior differently; consult upgrade notes for more information", "1.20")) 241 242 } 243 return nil 244 } 245 246 func checkPilot(cli kube.CLIClient, namespace string, messages *diag.Messages) error { 247 deployments, err := cli.Kube().AppsV1().Deployments(namespace).List(context.TODO(), metav1.ListOptions{ 248 LabelSelector: "app=istiod", 249 }) 250 if err != nil { 251 return err 252 } 253 for _, deployment := range deployments.Items { 254 scopingImpacted := false 255 256 // Obtain configmap to verify if affected features are used 257 configMapName := "istio" 258 if rev := deployment.Labels[label.IoIstioRev.Name]; rev != "default" { 259 configMapName += fmt.Sprintf("-%s", rev) 260 } 261 configMap, err := cli.Kube().CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapName, metav1.GetOptions{}) 262 if err != nil { 263 fmt.Printf("Error getting configmap %s: %v\n", configMapName, err) 264 } 265 meshData := make(map[string]interface{}) 266 if data, exists := configMap.Data["mesh"]; exists { 267 if err := yaml.Unmarshal([]byte(data), &meshData); err != nil { 268 fmt.Printf("Error parsing meshConfig: %v\n", err) 269 return err 270 } 271 } 272 if scopingImpacted = meshData["discoverySelectors"] != nil; !scopingImpacted { 273 continue 274 } 275 // Check if mitigation is already in place 276 for _, container := range deployment.Spec.Template.Spec.Containers { 277 if container.Name == "discovery" { 278 for _, envVar := range container.Env { 279 if envVar.Name == "ENHANCED_RESOURCE_SCOPING" && envVar.Value == "true" { 280 scopingImpacted = false 281 break 282 } 283 } 284 } 285 } 286 if scopingImpacted { 287 res := &resource.Instance{ 288 Origin: &legacykube.Origin{ 289 Type: config.GroupVersionKind(deployment.GroupVersionKind()), 290 FullName: resource.FullName{ 291 Namespace: resource.Namespace(deployment.GetNamespace()), 292 Name: resource.LocalName(deployment.GetName()), 293 }, 294 ResourceVersion: resource.Version(deployment.GetResourceVersion()), 295 Ref: nil, 296 FieldsMap: nil, 297 }, 298 } 299 messages.Add(msg.NewUpdateIncompatibility(res, 300 "ENHANCED_RESOURCE_SCOPING", "1.22", 301 "previously, the enhanced scoping of custom resources was disabled by default; now it will be enabled by default", "1.21")) 302 } 303 } 304 return nil 305 } 306 307 func checkDestinationRuleTLS(cli kube.CLIClient, messages *diag.Messages) error { 308 drs, err := cli.Istio().NetworkingV1alpha3().DestinationRules(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{}) 309 if err != nil { 310 return err 311 } 312 checkVerify := func(tls *networking.ClientTLSSettings) bool { 313 if tls == nil { 314 return false 315 } 316 if tls.Mode == networking.ClientTLSSettings_DISABLE || tls.Mode == networking.ClientTLSSettings_ISTIO_MUTUAL { 317 return false 318 } 319 return tls.CaCertificates == "" && tls.CredentialName == "" && !tls.InsecureSkipVerify.GetValue() 320 } 321 checkSNI := func(tls *networking.ClientTLSSettings) bool { 322 if tls == nil { 323 return false 324 } 325 if tls.Mode == networking.ClientTLSSettings_DISABLE || tls.Mode == networking.ClientTLSSettings_ISTIO_MUTUAL { 326 return false 327 } 328 return tls.Sni == "" 329 } 330 for _, dr := range drs.Items { 331 verificationImpacted := false 332 sniImpacted := false 333 verificationImpacted = verificationImpacted || checkVerify(dr.Spec.GetTrafficPolicy().GetTls()) 334 sniImpacted = sniImpacted || checkSNI(dr.Spec.GetTrafficPolicy().GetTls()) 335 for _, pl := range dr.Spec.GetTrafficPolicy().GetPortLevelSettings() { 336 verificationImpacted = verificationImpacted || checkVerify(pl.GetTls()) 337 sniImpacted = sniImpacted || checkSNI(pl.GetTls()) 338 } 339 for _, ss := range dr.Spec.Subsets { 340 verificationImpacted = verificationImpacted || checkVerify(ss.GetTrafficPolicy().GetTls()) 341 sniImpacted = sniImpacted || checkSNI(ss.GetTrafficPolicy().GetTls()) 342 for _, pl := range ss.GetTrafficPolicy().GetPortLevelSettings() { 343 verificationImpacted = verificationImpacted || checkVerify(pl.GetTls()) 344 sniImpacted = sniImpacted || checkSNI(pl.GetTls()) 345 } 346 } 347 if verificationImpacted { 348 res := ObjectToInstance(dr) 349 messages.Add(msg.NewUpdateIncompatibility(res, 350 "VERIFY_CERTIFICATE_AT_CLIENT", "1.20", 351 "previously, TLS verification was skipped. Set `insecureSkipVerify` if this behavior is desired", "1.20")) 352 } 353 if sniImpacted { 354 res := ObjectToInstance(dr) 355 messages.Add(msg.NewUpdateIncompatibility(res, 356 "ENABLE_AUTO_SNI", "1.20", 357 "previously, no SNI would be set; now it will be automatically set", "1.20")) 358 } 359 } 360 return nil 361 } 362 363 func checkVirtualServiceHostMatching(cli kube.CLIClient, messages *diag.Messages) error { 364 virtualServices, err := cli.Istio().NetworkingV1alpha3().VirtualServices(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{}) 365 if err != nil { 366 return err 367 } 368 for _, vs := range virtualServices.Items { 369 for _, hostname := range vs.Spec.Hosts { 370 if host.Name(hostname).IsWildCarded() { 371 res := ObjectToInstance(vs) 372 messages.Add(msg.NewUpdateIncompatibility(res, 373 "PERSIST_OLDEST_FIRST_HEURISTIC_FOR_VIRTUAL_SERVICE_HOST_MATCHING", "1.20", 374 "previously, VirtualServices with overlapping wildcard hosts would have the oldest "+ 375 "VirtualService take precedence. Now, the most specific VirtualService will win", "1.20"), 376 ) 377 continue 378 } 379 } 380 } 381 return nil 382 } 383 384 func ObjectToInstance(c controllers.Object) *resource.Instance { 385 return &resource.Instance{ 386 Origin: &legacykube.Origin{ 387 Type: kubetypes.GvkFromObject(c), 388 FullName: resource.FullName{ 389 Namespace: resource.Namespace(c.GetNamespace()), 390 Name: resource.LocalName(c.GetName()), 391 }, 392 ResourceVersion: resource.Version(c.GetResourceVersion()), 393 Ref: nil, 394 FieldsMap: nil, 395 }, 396 } 397 } 398 399 func checkControlPlane(ctx cli.Context) (diag.Messages, error) { 400 cli, err := ctx.CLIClient() 401 if err != nil { 402 return nil, err 403 } 404 msgs := diag.Messages{} 405 406 m, err := checkServerVersion(cli) 407 if err != nil { 408 return nil, err 409 } 410 msgs = append(msgs, m...) 411 412 msgs = append(msgs, checkInstallPermissions(cli, ctx.IstioNamespace())...) 413 gwMsg, err := checkGatewayAPIs(cli) 414 if err != nil { 415 return nil, err 416 } 417 msgs = append(msgs, gwMsg...) 418 419 // TODO: add more checks 420 421 sa := local.NewSourceAnalyzer( 422 analysis.Combine("upgrade precheck", &maturity.AlphaAnalyzer{}), 423 resource.Namespace(ctx.Namespace()), 424 resource.Namespace(ctx.IstioNamespace()), 425 nil, 426 ) 427 if err != nil { 428 return nil, err 429 } 430 sa.AddRunningKubeSource(cli) 431 cancel := make(chan struct{}) 432 result, err := sa.Analyze(cancel) 433 if err != nil { 434 return nil, err 435 } 436 if result.Messages != nil { 437 msgs = append(msgs, result.Messages...) 438 } 439 440 return msgs, nil 441 } 442 443 // Checks that if the user has gateway APIs, they are the minimum version. 444 // It is ok to not have them, but they must be at least v1beta1 if they do. 445 func checkGatewayAPIs(cli kube.CLIClient) (diag.Messages, error) { 446 msgs := diag.Messages{} 447 res, err := cli.Ext().ApiextensionsV1().CustomResourceDefinitions().List(context.Background(), metav1.ListOptions{}) 448 if err != nil { 449 return nil, err 450 } 451 452 betaKinds := sets.New(gvk.KubernetesGateway.Kind, gvk.GatewayClass.Kind, gvk.HTTPRoute.Kind, gvk.ReferenceGrant.Kind) 453 for _, r := range res.Items { 454 if r.Spec.Group != gvk.KubernetesGateway.Group { 455 continue 456 } 457 if !betaKinds.Contains(r.Spec.Names.Kind) { 458 continue 459 } 460 461 versions := extractCRDVersions(&r) 462 has := "none" 463 if len(versions) > 0 { 464 has = strings.Join(sets.SortedList(versions), ",") 465 } 466 if !versions.Contains(gvk.KubernetesGateway.Version) { 467 origin := legacykube.Origin{ 468 Type: gvk.CustomResourceDefinition, 469 FullName: resource.FullName{ 470 Namespace: resource.Namespace(r.Namespace), 471 Name: resource.LocalName(r.Name), 472 }, 473 ResourceVersion: resource.Version(r.ResourceVersion), 474 } 475 r := &resource.Instance{ 476 Origin: &origin, 477 } 478 msgs.Add(msg.NewUnsupportedGatewayAPIVersion(r, has, gvk.KubernetesGateway.Version)) 479 } 480 } 481 return msgs, nil 482 } 483 484 func extractCRDVersions(r *crd.CustomResourceDefinition) sets.String { 485 res := sets.New[string]() 486 for _, v := range r.Spec.Versions { 487 if v.Served { 488 res.Insert(v.Name) 489 } 490 } 491 return res 492 } 493 494 func checkInstallPermissions(cli kube.CLIClient, istioNamespace string) diag.Messages { 495 Resources := []struct { 496 namespace string 497 group string 498 version string 499 resource string 500 }{ 501 { 502 version: "v1", 503 resource: "namespaces", 504 }, 505 { 506 group: "rbac.authorization.k8s.io", 507 version: "v1", 508 resource: "clusterroles", 509 }, 510 { 511 group: "rbac.authorization.k8s.io", 512 version: "v1", 513 resource: "clusterrolebindings", 514 }, 515 { 516 group: "apiextensions.k8s.io", 517 version: "v1", 518 resource: "customresourcedefinitions", 519 }, 520 { 521 namespace: istioNamespace, 522 group: "rbac.authorization.k8s.io", 523 version: "v1", 524 resource: "roles", 525 }, 526 { 527 namespace: istioNamespace, 528 version: "v1", 529 resource: "serviceaccounts", 530 }, 531 { 532 namespace: istioNamespace, 533 version: "v1", 534 resource: "services", 535 }, 536 { 537 namespace: istioNamespace, 538 group: "apps", 539 version: "v1", 540 resource: "deployments", 541 }, 542 { 543 namespace: istioNamespace, 544 version: "v1", 545 resource: "configmaps", 546 }, 547 { 548 group: "admissionregistration.k8s.io", 549 version: "v1", 550 resource: "mutatingwebhookconfigurations", 551 }, 552 { 553 group: "admissionregistration.k8s.io", 554 version: "v1", 555 resource: "validatingwebhookconfigurations", 556 }, 557 } 558 msgs := diag.Messages{} 559 for _, r := range Resources { 560 err := checkCanCreateResources(cli, r.namespace, r.group, r.version, r.resource) 561 if err != nil { 562 msgs.Add(msg.NewInsufficientPermissions(&resource.Instance{Origin: clusterOrigin{}}, r.resource, err.Error())) 563 } 564 } 565 return msgs 566 } 567 568 func checkCanCreateResources(c kube.CLIClient, namespace, group, version, resource string) error { 569 s := &authorizationapi.SelfSubjectAccessReview{ 570 Spec: authorizationapi.SelfSubjectAccessReviewSpec{ 571 ResourceAttributes: &authorizationapi.ResourceAttributes{ 572 Namespace: namespace, 573 Verb: "create", 574 Group: group, 575 Version: version, 576 Resource: resource, 577 }, 578 }, 579 } 580 581 response, err := c.Kube().AuthorizationV1().SelfSubjectAccessReviews().Create(context.Background(), s, metav1.CreateOptions{}) 582 if err != nil { 583 return err 584 } 585 586 if !response.Status.Allowed { 587 if len(response.Status.Reason) > 0 { 588 return errors.New(response.Status.Reason) 589 } 590 return errors.New("permission denied") 591 } 592 return nil 593 } 594 595 func checkServerVersion(cli kube.CLIClient) (diag.Messages, error) { 596 v, err := cli.GetKubernetesVersion() 597 if err != nil { 598 return nil, fmt.Errorf("failed to get the Kubernetes version: %v", err) 599 } 600 compatible, err := k8sversion.CheckKubernetesVersion(v) 601 if err != nil { 602 return nil, err 603 } 604 if !compatible { 605 return []diag.Message{ 606 msg.NewUnsupportedKubernetesVersion(&resource.Instance{Origin: clusterOrigin{}}, v.String(), fmt.Sprintf("1.%d", k8sversion.MinK8SVersion)), 607 }, nil 608 } 609 return nil, nil 610 } 611 612 // clusterOrigin defines an Origin that refers to the cluster 613 type clusterOrigin struct{} 614 615 func (o clusterOrigin) ClusterName() istiocluster.ID { 616 return "Cluster" 617 } 618 619 func (o clusterOrigin) String() string { 620 return "" 621 } 622 623 func (o clusterOrigin) FriendlyName() string { 624 return "Cluster" 625 } 626 627 func (o clusterOrigin) Comparator() string { 628 return o.FriendlyName() 629 } 630 631 func (o clusterOrigin) Namespace() resource.Namespace { 632 return "" 633 } 634 635 func (o clusterOrigin) Reference() resource.Reference { 636 return nil 637 } 638 639 func (o clusterOrigin) FieldMap() map[string]int { 640 return make(map[string]int) 641 }