istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/analysis/analyzers/deployment/services.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  package deployment
    15  
    16  import (
    17  	"fmt"
    18  	"sort"
    19  	"strconv"
    20  
    21  	appsv1 "k8s.io/api/apps/v1"
    22  	core_v1 "k8s.io/api/core/v1"
    23  	klabels "k8s.io/apimachinery/pkg/labels"
    24  
    25  	"istio.io/istio/pkg/config"
    26  	"istio.io/istio/pkg/config/analysis"
    27  	"istio.io/istio/pkg/config/analysis/analyzers/util"
    28  	"istio.io/istio/pkg/config/analysis/msg"
    29  	"istio.io/istio/pkg/config/constants"
    30  	"istio.io/istio/pkg/config/resource"
    31  	"istio.io/istio/pkg/config/schema/gvk"
    32  )
    33  
    34  type ServiceAssociationAnalyzer struct{}
    35  
    36  var _ analysis.Analyzer = &ServiceAssociationAnalyzer{}
    37  
    38  type (
    39  	PortMap             map[int32]ProtocolMap
    40  	ProtocolMap         map[core_v1.Protocol]ServiceNames
    41  	ServiceNames        []string
    42  	ServiceSpecWithName struct {
    43  		Name string
    44  		Spec *core_v1.ServiceSpec
    45  	}
    46  )
    47  
    48  // targetPort port serviceName
    49  type targetPortMap map[string]map[int32]string
    50  
    51  func (s *ServiceAssociationAnalyzer) Metadata() analysis.Metadata {
    52  	return analysis.Metadata{
    53  		Name:        "deployment.MultiServiceAnalyzer",
    54  		Description: "Checks association between services and pods",
    55  		Inputs: []config.GroupVersionKind{
    56  			gvk.Service,
    57  			gvk.Deployment,
    58  			gvk.Namespace,
    59  		},
    60  	}
    61  }
    62  
    63  func (s *ServiceAssociationAnalyzer) Analyze(c analysis.Context) {
    64  	c.ForEach(gvk.Deployment, func(r *resource.Instance) bool {
    65  		if !isWaypointDeployment(r) && util.DeploymentInMesh(r, c) {
    66  			s.analyzeDeploymentPortProtocol(r, c)
    67  			s.analyzeDeploymentTargetPorts(r, c)
    68  		}
    69  		return true
    70  	})
    71  }
    72  
    73  func isWaypointDeployment(r *resource.Instance) bool {
    74  	return r.Metadata.Labels[constants.ManagedGatewayLabel] == constants.ManagedGatewayMeshControllerLabel
    75  }
    76  
    77  // analyzeDeploymentPortProtocol analyzes the specific service mesh deployment
    78  func (s *ServiceAssociationAnalyzer) analyzeDeploymentPortProtocol(r *resource.Instance, c analysis.Context) {
    79  	// Find matching services with resulting pod from deployment
    80  	matchingSvcs := s.findMatchingServices(r, c)
    81  
    82  	// Generate a port map from the matching services.
    83  	// It creates a structure that will allow us to detect
    84  	// if there are different protocols for the same port.
    85  	portMap := servicePortMap(matchingSvcs)
    86  
    87  	// Determining which ports use more than one protocol.
    88  	for port := range portMap {
    89  		// In case there are two protocols using same port number, generate a message
    90  		protMap := portMap[port]
    91  		if len(protMap) > 1 {
    92  			// Collect names from both protocols
    93  			svcNames := make(ServiceNames, 0)
    94  			for protocol := range protMap {
    95  				svcNames = append(svcNames, protMap[protocol]...)
    96  			}
    97  			sort.Strings(svcNames)
    98  			m := msg.NewDeploymentAssociatedToMultipleServices(r, r.Metadata.FullName.Name.String(), port, svcNames)
    99  
   100  			if line, ok := util.ErrorLine(r, fmt.Sprintf(util.MetadataName)); ok {
   101  				m.Line = line
   102  			}
   103  
   104  			// Reporting the message for the deployment, port and conflicting services.
   105  			c.Report(gvk.Deployment, m)
   106  		}
   107  	}
   108  }
   109  
   110  // analyzeDeploymentPortProtocol analyzes the targetPorts conflicting
   111  func (s *ServiceAssociationAnalyzer) analyzeDeploymentTargetPorts(r *resource.Instance, c analysis.Context) {
   112  	// Find matching services with resulting pod from deployment
   113  	matchingSvcs := s.findMatchingServices(r, c)
   114  
   115  	tpm := serviceTargetPortsMap(matchingSvcs)
   116  
   117  	// Determining which ports use more than one protocol.
   118  	for targetPort, portServices := range tpm {
   119  		if len(portServices) > 1 {
   120  			// Collect names from both protocols
   121  			svcNames := make(ServiceNames, 0, len(portServices))
   122  			ports := make([]int32, 0, len(portServices))
   123  			for p, s := range portServices {
   124  				svcNames = append(svcNames, s)
   125  				ports = append(ports, p)
   126  			}
   127  
   128  			sort.Strings(svcNames)
   129  			sort.Slice(ports, func(i, j int) bool {
   130  				return ports[i] < ports[j]
   131  			})
   132  			m := msg.NewDeploymentConflictingPorts(r, r.Metadata.FullName.Name.String(), svcNames, targetPort, ports)
   133  
   134  			if line, ok := util.ErrorLine(r, fmt.Sprintf(util.MetadataName)); ok {
   135  				m.Line = line
   136  			}
   137  
   138  			// Reporting the message for the deployment, port and conflicting services.
   139  			c.Report(gvk.Deployment, m)
   140  		}
   141  	}
   142  }
   143  
   144  // findMatchingServices returns an slice of Services that matches with deployment's pods.
   145  func (s *ServiceAssociationAnalyzer) findMatchingServices(r *resource.Instance, c analysis.Context) []ServiceSpecWithName {
   146  	matchingSvcs := make([]ServiceSpecWithName, 0)
   147  	d := r.Message.(*appsv1.DeploymentSpec)
   148  	deploymentNS := r.Metadata.FullName.Namespace.String()
   149  
   150  	c.ForEach(gvk.Service, func(r *resource.Instance) bool {
   151  		s := r.Message.(*core_v1.ServiceSpec)
   152  
   153  		sSelector := klabels.SelectorFromSet(s.Selector)
   154  		pLabels := klabels.Set(d.Template.Labels)
   155  		if !sSelector.Empty() && sSelector.Matches(pLabels) && r.Metadata.FullName.Namespace.String() == deploymentNS {
   156  			matchingSvcs = append(matchingSvcs, ServiceSpecWithName{r.Metadata.FullName.String(), s})
   157  		}
   158  
   159  		return true
   160  	})
   161  
   162  	return matchingSvcs
   163  }
   164  
   165  // servicePortMap build a map of ports and protocols for each Service. e.g. m[80]["TCP"] -> svcA, svcB, svcC
   166  func servicePortMap(svcs []ServiceSpecWithName) PortMap {
   167  	portMap := PortMap{}
   168  
   169  	for _, swn := range svcs {
   170  		svc := swn.Spec
   171  		for _, sPort := range svc.Ports {
   172  			// If it is the first occurrence of this port, create a ProtocolMap
   173  			if _, ok := portMap[sPort.Port]; !ok {
   174  				portMap[sPort.Port] = ProtocolMap{}
   175  			}
   176  
   177  			// Default protocol is TCP
   178  			protocol := sPort.Protocol
   179  			if protocol == "" {
   180  				protocol = core_v1.ProtocolTCP
   181  			}
   182  
   183  			// Appending the service information for the Port/Protocol combination
   184  			portMap[sPort.Port][protocol] = append(portMap[sPort.Port][protocol], swn.Name)
   185  		}
   186  	}
   187  
   188  	return portMap
   189  }
   190  
   191  // serviceTargetPortsMap build a map of targetPort and ports for each Service. e.g. m["80"][80] -> svc
   192  func serviceTargetPortsMap(svcs []ServiceSpecWithName) targetPortMap {
   193  	pm := targetPortMap{}
   194  	for _, swn := range svcs {
   195  		svc := swn.Spec
   196  		for _, sPort := range svc.Ports {
   197  			p := sPort.TargetPort.String()
   198  			if p == "0" || p == "" {
   199  				// By default and for convenience, the targetPort is set to the same value as the port field.
   200  				p = strconv.Itoa(int(sPort.Port))
   201  			}
   202  			if _, ok := pm[p]; !ok {
   203  				pm[p] = map[int32]string{}
   204  			}
   205  			pm[p][sPort.Port] = swn.Name
   206  		}
   207  	}
   208  	return pm
   209  }