blob: c808f51078a7b0e930fbe7de86f0e79da6bff715 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package deployment
import (
"fmt"
"strconv"
)
import (
apps_v1 "k8s.io/api/apps/v1"
core_v1 "k8s.io/api/core/v1"
k8s_labels "k8s.io/apimachinery/pkg/labels"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/config/analysis"
"github.com/apache/dubbo-go-pixiu/pkg/config/analysis/analyzers/util"
"github.com/apache/dubbo-go-pixiu/pkg/config/analysis/msg"
"github.com/apache/dubbo-go-pixiu/pkg/config/resource"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/collection"
"github.com/apache/dubbo-go-pixiu/pkg/config/schema/collections"
)
type ServiceAssociationAnalyzer struct{}
var _ analysis.Analyzer = &ServiceAssociationAnalyzer{}
type (
PortMap map[int32]ProtocolMap
ProtocolMap map[core_v1.Protocol]ServiceNames
ServiceNames []string
ServiceSpecWithName struct {
Name string
Spec *core_v1.ServiceSpec
}
)
// targetPort port serviceName
type targetPortMap map[string]map[int32]string
func (s *ServiceAssociationAnalyzer) Metadata() analysis.Metadata {
return analysis.Metadata{
Name: "deployment.MultiServiceAnalyzer",
Description: "Checks association between services and pods",
Inputs: collection.Names{
collections.K8SCoreV1Services.Name(),
collections.K8SAppsV1Deployments.Name(),
collections.K8SCoreV1Namespaces.Name(),
},
}
}
func (s *ServiceAssociationAnalyzer) Analyze(c analysis.Context) {
c.ForEach(collections.K8SAppsV1Deployments.Name(), func(r *resource.Instance) bool {
if util.DeploymentInMesh(r, c) {
s.analyzeDeploymentPortProtocol(r, c)
s.analyzeDeploymentTargetPorts(r, c)
}
return true
})
}
// analyzeDeploymentPortProtocol analyzes the specific service mesh deployment
func (s *ServiceAssociationAnalyzer) analyzeDeploymentPortProtocol(r *resource.Instance, c analysis.Context) {
// Find matching services with resulting pod from deployment
matchingSvcs := s.findMatchingServices(r, c)
// If there isn't any matching service, generate message: At least one service is needed.
if len(matchingSvcs) == 0 {
c.Report(collections.K8SAppsV1Deployments.Name(), msg.NewDeploymentRequiresServiceAssociated(r))
return
}
// Generate a port map from the matching services.
// It creates a structure that will allow us to detect
// if there are different protocols for the same port.
portMap := servicePortMap(matchingSvcs)
// Determining which ports use more than one protocol.
for port := range portMap {
// In case there are two protocols using same port number, generate a message
protMap := portMap[port]
if len(protMap) > 1 {
// Collect names from both protocols
svcNames := make(ServiceNames, 0)
for protocol := range protMap {
svcNames = append(svcNames, protMap[protocol]...)
}
m := msg.NewDeploymentAssociatedToMultipleServices(r, r.Metadata.FullName.Name.String(), port, svcNames)
if line, ok := util.ErrorLine(r, fmt.Sprintf(util.MetadataName)); ok {
m.Line = line
}
// Reporting the message for the deployment, port and conflicting services.
c.Report(collections.K8SAppsV1Deployments.Name(), m)
}
}
}
// analyzeDeploymentPortProtocol analyzes the targetPorts conflicting
func (s *ServiceAssociationAnalyzer) analyzeDeploymentTargetPorts(r *resource.Instance, c analysis.Context) {
// Find matching services with resulting pod from deployment
matchingSvcs := s.findMatchingServices(r, c)
// If there isn't any matching service, generate message: At least one service is needed.
if len(matchingSvcs) == 0 {
c.Report(collections.K8SAppsV1Deployments.Name(), msg.NewDeploymentRequiresServiceAssociated(r))
return
}
tpm := serviceTargetPortsMap(matchingSvcs)
// Determining which ports use more than one protocol.
for targetPort, portServices := range tpm {
if len(portServices) > 1 {
// Collect names from both protocols
svcNames := make(ServiceNames, 0, len(portServices))
ports := make([]int32, 0, len(portServices))
for p, s := range portServices {
svcNames = append(svcNames, s)
ports = append(ports, p)
}
m := msg.NewDeploymentConflictingPorts(r, r.Metadata.FullName.Name.String(), svcNames, targetPort, ports)
if line, ok := util.ErrorLine(r, fmt.Sprintf(util.MetadataName)); ok {
m.Line = line
}
// Reporting the message for the deployment, port and conflicting services.
c.Report(collections.K8SAppsV1Deployments.Name(), m)
}
}
}
// findMatchingServices returns an slice of Services that matches with deployment's pods.
func (s *ServiceAssociationAnalyzer) findMatchingServices(r *resource.Instance, c analysis.Context) []ServiceSpecWithName {
matchingSvcs := make([]ServiceSpecWithName, 0)
d := r.Message.(*apps_v1.DeploymentSpec)
deploymentNS := r.Metadata.FullName.Namespace.String()
c.ForEach(collections.K8SCoreV1Services.Name(), func(r *resource.Instance) bool {
s := r.Message.(*core_v1.ServiceSpec)
sSelector := k8s_labels.SelectorFromSet(s.Selector)
pLabels := k8s_labels.Set(d.Template.Labels)
if sSelector.Matches(pLabels) && r.Metadata.FullName.Namespace.String() == deploymentNS {
matchingSvcs = append(matchingSvcs, ServiceSpecWithName{r.Metadata.FullName.String(), s})
}
return true
})
return matchingSvcs
}
// servicePortMap build a map of ports and protocols for each Service. e.g. m[80]["TCP"] -> svcA, svcB, svcC
func servicePortMap(svcs []ServiceSpecWithName) PortMap {
portMap := PortMap{}
for _, swn := range svcs {
svc := swn.Spec
for _, sPort := range svc.Ports {
// If it is the first occurrence of this port, create a ProtocolMap
if _, ok := portMap[sPort.Port]; !ok {
portMap[sPort.Port] = ProtocolMap{}
}
// Default protocol is TCP
protocol := sPort.Protocol
if protocol == "" {
protocol = core_v1.ProtocolTCP
}
// Appending the service information for the Port/Protocol combination
portMap[sPort.Port][protocol] = append(portMap[sPort.Port][protocol], swn.Name)
}
}
return portMap
}
// serviceTargetPortsMap build a map of targetPort and ports for each Service. e.g. m["80"][80] -> svc
func serviceTargetPortsMap(svcs []ServiceSpecWithName) targetPortMap {
pm := targetPortMap{}
for _, swn := range svcs {
svc := swn.Spec
for _, sPort := range svc.Ports {
p := sPort.TargetPort.String()
if p == "0" || p == "" {
// By default and for convenience, the targetPort is set to the same value as the port field.
p = strconv.Itoa(int(sPort.Port))
}
if _, ok := pm[p]; !ok {
pm[p] = map[int32]string{}
}
pm[p][sPort.Port] = swn.Name
}
}
return pm
}