github.com/noironetworks/cilium-net@v1.6.12/pkg/k8s/service.go (about) 1 // Copyright 2018-2019 Authors of Cilium 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 k8s 16 17 import ( 18 "fmt" 19 "net" 20 "net/url" 21 "strings" 22 "time" 23 24 "github.com/cilium/cilium/pkg/annotation" 25 "github.com/cilium/cilium/pkg/comparator" 26 "github.com/cilium/cilium/pkg/k8s/types" 27 "github.com/cilium/cilium/pkg/loadbalancer" 28 "github.com/cilium/cilium/pkg/logging/logfields" 29 "github.com/cilium/cilium/pkg/node" 30 "github.com/cilium/cilium/pkg/option" 31 "github.com/cilium/cilium/pkg/service" 32 33 "github.com/sirupsen/logrus" 34 "k8s.io/api/core/v1" 35 ) 36 37 func getAnnotationIncludeExternal(svc *types.Service) bool { 38 if value, ok := svc.ObjectMeta.Annotations[annotation.GlobalService]; ok { 39 return strings.ToLower(value) == "true" 40 } 41 42 return false 43 } 44 45 func getAnnotationShared(svc *types.Service) bool { 46 if value, ok := svc.ObjectMeta.Annotations[annotation.SharedService]; ok { 47 return strings.ToLower(value) == "true" 48 } 49 50 return getAnnotationIncludeExternal(svc) 51 } 52 53 // ParseServiceID parses a Kubernetes service and returns the ServiceID 54 func ParseServiceID(svc *types.Service) ServiceID { 55 return ServiceID{ 56 Name: svc.ObjectMeta.Name, 57 Namespace: svc.ObjectMeta.Namespace, 58 } 59 } 60 61 // ParseService parses a Kubernetes service and returns a Service 62 func ParseService(svc *types.Service) (ServiceID, *Service) { 63 scopedLog := log.WithFields(logrus.Fields{ 64 logfields.K8sSvcName: svc.ObjectMeta.Name, 65 logfields.K8sNamespace: svc.ObjectMeta.Namespace, 66 logfields.K8sAPIVersion: svc.TypeMeta.APIVersion, 67 logfields.K8sSvcType: svc.Spec.Type, 68 }) 69 70 svcID := ParseServiceID(svc) 71 72 switch svc.Spec.Type { 73 case v1.ServiceTypeClusterIP, v1.ServiceTypeNodePort, v1.ServiceTypeLoadBalancer: 74 break 75 76 case v1.ServiceTypeExternalName: 77 // External-name services must be ignored 78 return svcID, nil 79 80 default: 81 scopedLog.Warn("Ignoring k8s service: unsupported type") 82 return svcID, nil 83 } 84 85 if svc.Spec.ClusterIP == "" { 86 return svcID, nil 87 } 88 89 clusterIP := net.ParseIP(svc.Spec.ClusterIP) 90 headless := false 91 if strings.ToLower(svc.Spec.ClusterIP) == "none" { 92 headless = true 93 } 94 svcInfo := NewService(clusterIP, headless, svc.Labels, svc.Spec.Selector) 95 svcInfo.IncludeExternal = getAnnotationIncludeExternal(svc) 96 svcInfo.Shared = getAnnotationShared(svc) 97 98 for _, port := range svc.Spec.Ports { 99 p := loadbalancer.NewFEPort(loadbalancer.L4Type(port.Protocol), uint16(port.Port)) 100 portName := loadbalancer.FEPortName(port.Name) 101 if _, ok := svcInfo.Ports[portName]; !ok { 102 svcInfo.Ports[portName] = p 103 } 104 // This is a hack;-( In the case of NodePort service, we need to create 105 // three surrogate frontends per IP protocol - one with a zero IP addr used 106 // by the host-lb, one with a public iface IP addr and one with cilium_host 107 // IP addr. 108 // For each frontend we will need to store a service ID used for a reverse 109 // NAT translation and for deleting a service. 110 // Unfortunately, doing this in daemon/{loadbalancer,k8s_watcher}.go 111 // would introduce more complexity in already too complex LB codebase, 112 // so for now (until we have refactored the LB code) keep NodePort 113 // frontends in Service.NodePorts. 114 if svc.Spec.Type == v1.ServiceTypeNodePort || svc.Spec.Type == v1.ServiceTypeLoadBalancer { 115 if option.Config.EnableNodePort { 116 if _, ok := svcInfo.NodePorts[portName]; !ok { 117 svcInfo.NodePorts[portName] = 118 make(map[string]*loadbalancer.L3n4AddrID) 119 } 120 proto := loadbalancer.L4Type(port.Protocol) 121 port := uint16(port.NodePort) 122 id := loadbalancer.ID(0) // will be allocated by k8s_watcher 123 124 // TODO(brb) switch to if-clause when dual stack is supported 125 switch { 126 case option.Config.EnableIPv4 && 127 clusterIP != nil && !strings.Contains(svc.Spec.ClusterIP, ":"): 128 129 for _, ip := range []net.IP{net.IPv4(0, 0, 0, 0), node.GetNodePortIPv4(), node.GetInternalIPv4()} { 130 nodePortFE := loadbalancer.NewL3n4AddrID(proto, ip, port, id) 131 svcInfo.NodePorts[portName][nodePortFE.String()] = nodePortFE 132 } 133 case option.Config.EnableIPv6 && 134 clusterIP != nil && strings.Contains(svc.Spec.ClusterIP, ":"): 135 136 for _, ip := range []net.IP{net.IPv6zero, node.GetNodePortIPv6(), node.GetIPv6Router()} { 137 nodePortFE := loadbalancer.NewL3n4AddrID(proto, ip, port, id) 138 svcInfo.NodePorts[portName][nodePortFE.String()] = nodePortFE 139 } 140 } 141 } 142 } 143 } 144 145 return svcID, svcInfo 146 } 147 148 // ServiceID identities the Kubernetes service 149 type ServiceID struct { 150 Name string `json:"serviceName,omitempty"` 151 Namespace string `json:"namespace,omitempty"` 152 } 153 154 // String returns the string representation of a service ID 155 func (s ServiceID) String() string { 156 return fmt.Sprintf("%s/%s", s.Namespace, s.Name) 157 } 158 159 // ParseServiceIDFrom returns a ServiceID derived from the given kubernetes 160 // service FQDN. 161 func ParseServiceIDFrom(dn string) *ServiceID { 162 // typical service name "cilium-etcd-client.kube-system.svc" 163 idx1 := strings.IndexByte(dn, '.') 164 if idx1 >= 0 { 165 svc := ServiceID{ 166 Name: dn[:idx1], 167 } 168 idx2 := strings.IndexByte(dn[idx1+1:], '.') 169 if idx2 >= 0 { 170 // "cilium-etcd-client.kube-system.svc" 171 // ^idx1+1 ^ idx1+1+idx2 172 svc.Namespace = dn[idx1+1 : idx1+1+idx2] 173 } else { 174 // "cilium-etcd-client.kube-system" 175 // ^idx1+1 176 svc.Namespace = dn[idx1+1:] 177 } 178 return &svc 179 } 180 return nil 181 } 182 183 // Service is an abstraction for a k8s service that is composed by the frontend IP 184 // address (FEIP) and the map of the frontend ports (Ports). 185 type Service struct { 186 FrontendIP net.IP 187 IsHeadless bool 188 189 // IncludeExternal is true when external endpoints from other clusters 190 // should be included 191 IncludeExternal bool 192 193 // Shared is true when the service should be exposed/shared to other clusters 194 Shared bool 195 196 Ports map[loadbalancer.FEPortName]*loadbalancer.FEPort 197 // NodePorts stores mapping for port name => NodePort frontend addr string => 198 // NodePort fronted addr. The string addr => addr indirection is to avoid 199 // storing duplicates. 200 NodePorts map[loadbalancer.FEPortName]map[string]*loadbalancer.L3n4AddrID 201 Labels map[string]string 202 Selector map[string]string 203 } 204 205 // String returns the string representation of a service resource 206 func (s *Service) String() string { 207 if s == nil { 208 return "nil" 209 } 210 211 ports := make([]string, len(s.Ports)) 212 i := 0 213 for p := range s.Ports { 214 ports[i] = string(p) 215 i++ 216 } 217 218 return fmt.Sprintf("frontend:%s/ports=%s/selector=%v", s.FrontendIP.String(), ports, s.Selector) 219 } 220 221 // IsExternal returns true if the service is expected to serve out-of-cluster endpoints: 222 func (s Service) IsExternal() bool { 223 return len(s.Selector) == 0 224 } 225 226 // DeepEquals returns true if both services are equal 227 func (s *Service) DeepEquals(o *Service) bool { 228 switch { 229 case (s == nil) != (o == nil): 230 return false 231 case (s == nil) && (o == nil): 232 return true 233 } 234 if s.IsHeadless == o.IsHeadless && 235 s.FrontendIP.Equal(o.FrontendIP) && 236 comparator.MapStringEquals(s.Labels, o.Labels) && 237 comparator.MapStringEquals(s.Selector, o.Selector) { 238 239 if ((s.Ports == nil) != (o.Ports == nil)) || 240 len(s.Ports) != len(o.Ports) { 241 return false 242 } 243 for portName, port := range s.Ports { 244 oPort, ok := o.Ports[portName] 245 if !ok { 246 return false 247 } 248 if !port.EqualsIgnoreID(oPort) { 249 return false 250 } 251 } 252 253 if ((s.NodePorts == nil) != (o.NodePorts == nil)) || 254 len(s.NodePorts) != len(o.NodePorts) { 255 return false 256 } 257 for portName, nodePorts := range s.NodePorts { 258 oNodePorts, ok := o.NodePorts[portName] 259 if !ok { 260 return false 261 } 262 if ((nodePorts == nil) != (oNodePorts == nil)) || 263 len(nodePorts) != len(oNodePorts) { 264 return false 265 } 266 for nodePortName, nodePort := range nodePorts { 267 oNodePort, ok := oNodePorts[nodePortName] 268 if !ok { 269 return false 270 } 271 if !nodePort.Equals(oNodePort) { 272 return false 273 } 274 } 275 } 276 return true 277 } 278 return false 279 } 280 281 // NewService returns a new Service with the Ports map initialized. 282 func NewService(ip net.IP, headless bool, labels map[string]string, selector map[string]string) *Service { 283 return &Service{ 284 FrontendIP: ip, 285 IsHeadless: headless, 286 Ports: map[loadbalancer.FEPortName]*loadbalancer.FEPort{}, 287 NodePorts: map[loadbalancer.FEPortName]map[string]*loadbalancer.L3n4AddrID{}, 288 Labels: labels, 289 Selector: selector, 290 } 291 } 292 293 // UniquePorts returns a map of all unique ports configured in the service 294 func (s *Service) UniquePorts() map[uint16]bool { 295 // We are not discriminating the different L4 protocols on the same L4 296 // port so we create the number of unique sets of service IP + service 297 // port. 298 uniqPorts := map[uint16]bool{} 299 for _, p := range s.Ports { 300 uniqPorts[p.Port] = true 301 } 302 return uniqPorts 303 } 304 305 // NewClusterService returns the service.ClusterService representing a 306 // Kubernetes Service 307 func NewClusterService(id ServiceID, k8sService *Service, k8sEndpoints *Endpoints) service.ClusterService { 308 svc := service.NewClusterService(id.Name, id.Namespace) 309 310 for key, value := range k8sService.Labels { 311 svc.Labels[key] = value 312 } 313 314 for key, value := range k8sService.Selector { 315 svc.Selector[key] = value 316 } 317 318 portConfig := service.PortConfiguration{} 319 for portName, port := range k8sService.Ports { 320 portConfig[string(portName)] = port.L4Addr 321 } 322 323 svc.Frontends = map[string]service.PortConfiguration{} 324 svc.Frontends[k8sService.FrontendIP.String()] = portConfig 325 326 svc.Backends = map[string]service.PortConfiguration{} 327 for ipString, portConfig := range k8sEndpoints.Backends { 328 svc.Backends[ipString] = portConfig 329 } 330 331 return svc 332 } 333 334 type ServiceIPGetter interface { 335 GetServiceIP(svcID ServiceID) *loadbalancer.L3n4Addr 336 } 337 338 // CreateCustomDialer returns a custom dialer that picks the service IP, 339 // from the given ServiceIPGetter, if the address the used to dial is a k8s 340 // service. 341 func CreateCustomDialer(b ServiceIPGetter, log *logrus.Entry) func(s string, duration time.Duration) (conn net.Conn, e error) { 342 return func(s string, duration time.Duration) (conn net.Conn, e error) { 343 // If the service is available, do the service translation to 344 // the service IP. Otherwise dial with the original service 345 // name `s`. 346 u, err := url.Parse(s) 347 if err == nil { 348 svc := ParseServiceIDFrom(u.Host) 349 if svc != nil { 350 svcIP := b.GetServiceIP(*svc) 351 if svcIP != nil { 352 s = svcIP.String() 353 } 354 } else { 355 log.Debug("Service not found") 356 } 357 log.Debugf("custom dialer based on k8s service backend is dialing to %q", s) 358 } else { 359 log.Errorf("Unable to parse etcd service URL %s", err) 360 } 361 return net.Dial("tcp", s) 362 } 363 }