istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/analysis/analyzers/multicluster/service.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 multicluster 16 17 import ( 18 "reflect" 19 "sort" 20 21 corev1 "k8s.io/api/core/v1" 22 23 "istio.io/istio/pkg/cluster" 24 "istio.io/istio/pkg/config" 25 "istio.io/istio/pkg/config/analysis" 26 "istio.io/istio/pkg/config/analysis/msg" 27 "istio.io/istio/pkg/config/resource" 28 "istio.io/istio/pkg/config/schema/gvk" 29 "istio.io/istio/pkg/slices" 30 "istio.io/istio/pkg/util/sets" 31 ) 32 33 // ServiceAnalyzer validates services in multi-cluster. 34 type ServiceAnalyzer struct{} 35 36 var _ analysis.Analyzer = &ServiceAnalyzer{} 37 38 // Metadata implements Analyzer 39 func (s *ServiceAnalyzer) Metadata() analysis.Metadata { 40 return analysis.Metadata{ 41 Name: "services.MultiClusterServiceAnalyzer", 42 Description: "Check the validity of services in the multi-cluster environment", 43 Inputs: []config.GroupVersionKind{ 44 gvk.Service, 45 }, 46 } 47 } 48 49 // Analyze implements Analyzer 50 func (s *ServiceAnalyzer) Analyze(c analysis.Context) { 51 services := map[resource.FullName]map[cluster.ID]*resource.Instance{} 52 c.ForEach(gvk.Service, func(r *resource.Instance) bool { 53 clusterID := r.Origin.ClusterName() 54 if clusterID == "" { 55 return true 56 } 57 if _, ok := services[r.Metadata.FullName]; !ok { 58 services[r.Metadata.FullName] = map[cluster.ID]*resource.Instance{} 59 } 60 services[r.Metadata.FullName][clusterID] = r 61 return true 62 }) 63 64 for fullname, clusterServices := range services { 65 if len(clusterServices) == 1 { 66 continue 67 } 68 inconsistents, errors := findInconsistencies(clusterServices) 69 if len(inconsistents) > 0 { 70 var serviceInstance *resource.Instance 71 for _, r := range clusterServices { 72 if r != nil { 73 serviceInstance = r 74 break 75 } 76 } 77 message := msg.NewMultiClusterInconsistentService(serviceInstance, fullname.Name.String(), 78 fullname.Namespace.String(), inconsistents, errors) 79 80 c.Report(gvk.Service, message) 81 } 82 } 83 } 84 85 func findInconsistencies(services map[cluster.ID]*resource.Instance) (clusters []string, errors string) { 86 inconsistentClusters := sets.New[string]() 87 inconsistentReasons := sets.New[string]() 88 89 // Convert the first service from resource.Instance to corev1.Service 90 var firstService *corev1.ServiceSpec 91 var firstCluster cluster.ID 92 for id, instance := range services { 93 firstCluster = id 94 firstService = instance.Message.(*corev1.ServiceSpec) 95 if firstService != nil { 96 break 97 } 98 } 99 100 var addedHeadless bool 101 for id, instance := range services { 102 service := instance.Message.(*corev1.ServiceSpec) 103 104 // Compare if service has mixed mode like headless and clusterIP 105 if !addedHeadless && (firstService.ClusterIP == corev1.ClusterIPNone) != (service.ClusterIP == corev1.ClusterIPNone) { 106 addedHeadless = true 107 for c := range services { 108 inconsistentClusters.Insert(c.String()) 109 } 110 inconsistentReasons.Insert("service has mixed mode like headless and clusterIP") 111 } 112 113 // Compare service types 114 if firstService.Type != service.Type { 115 inconsistentClusters.Insert(id.String()) 116 inconsistentReasons.Insert("service type is inconsistent") 117 } 118 119 // Compare ports 120 if !compareServicePorts(firstService.Ports, service.Ports) { 121 inconsistentClusters.Insert(id.String()) 122 inconsistentReasons.Insert("service ports are inconsistent") 123 } 124 } 125 if len(inconsistentClusters) > 0 { 126 inconsistentClusters.Insert(firstCluster.String()) 127 } 128 slices.Sort(inconsistentReasons.UnsortedList()) 129 errStr := "" 130 for i, err := range inconsistentReasons.UnsortedList() { 131 errStr += err 132 if i < len(inconsistentReasons)-1 { 133 errStr += "; " 134 } 135 } 136 return inconsistentClusters.UnsortedList(), errStr 137 } 138 139 func compareServicePorts(a, b []corev1.ServicePort) bool { 140 if len(a) != len(b) { 141 return false 142 } 143 144 sort.SliceStable(a, func(i, j int) bool { 145 return a[i].Name < a[j].Name 146 }) 147 148 sort.SliceStable(b, func(i, j int) bool { 149 return b[i].Name < b[j].Name 150 }) 151 return reflect.DeepEqual(a, b) 152 }