1// Copyright 2017 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 15package kube 16 17import ( 18 "fmt" 19 "sort" 20 "strconv" 21 "strings" 22 23 "github.com/hashicorp/go-multierror" 24 coreV1 "k8s.io/api/core/v1" 25 "k8s.io/apimachinery/pkg/util/intstr" 26 27 "istio.io/api/annotation" 28 29 "istio.io/istio/pilot/pkg/model" 30 "istio.io/istio/pilot/pkg/serviceregistry" 31 "istio.io/istio/pkg/config/constants" 32 "istio.io/istio/pkg/config/host" 33 "istio.io/istio/pkg/config/kube" 34 "istio.io/istio/pkg/config/protocol" 35 "istio.io/istio/pkg/config/visibility" 36 "istio.io/istio/pkg/spiffe" 37) 38 39const ( 40 // IngressClassAnnotation is the annotation on ingress resources for the class of controllers 41 // responsible for it 42 IngressClassAnnotation = "kubernetes.io/ingress.class" 43 44 // TODO: move to API 45 // The value for this annotation is a set of key value pairs (node labels) 46 // that can be used to select a subset of nodes from the pool of k8s nodes 47 // It is used for multi-cluster scenario, and with nodePort type gateway service. 48 NodeSelectorAnnotation = "traffic.istio.io/nodeSelector" 49 50 managementPortPrefix = "mgmt-" 51) 52 53func convertPort(port coreV1.ServicePort) *model.Port { 54 return &model.Port{ 55 Name: port.Name, 56 Port: int(port.Port), 57 Protocol: kube.ConvertProtocol(port.Port, port.Name, port.Protocol, port.AppProtocol), 58 } 59} 60 61func ConvertService(svc coreV1.Service, domainSuffix string, clusterID string) *model.Service { 62 addr, external := constants.UnspecifiedIP, "" 63 if svc.Spec.ClusterIP != "" && svc.Spec.ClusterIP != coreV1.ClusterIPNone { 64 addr = svc.Spec.ClusterIP 65 } 66 67 resolution := model.ClientSideLB 68 meshExternal := false 69 70 if svc.Spec.Type == coreV1.ServiceTypeExternalName && svc.Spec.ExternalName != "" { 71 external = svc.Spec.ExternalName 72 resolution = model.DNSLB 73 meshExternal = true 74 } 75 76 if addr == constants.UnspecifiedIP && external == "" { // headless services should not be load balanced 77 resolution = model.Passthrough 78 } 79 80 ports := make([]*model.Port, 0, len(svc.Spec.Ports)) 81 for _, port := range svc.Spec.Ports { 82 ports = append(ports, convertPort(port)) 83 } 84 85 var exportTo map[visibility.Instance]bool 86 serviceaccounts := make([]string, 0) 87 if svc.Annotations[annotation.AlphaCanonicalServiceAccounts.Name] != "" { 88 serviceaccounts = append(serviceaccounts, strings.Split(svc.Annotations[annotation.AlphaCanonicalServiceAccounts.Name], ",")...) 89 } 90 if svc.Annotations[annotation.AlphaKubernetesServiceAccounts.Name] != "" { 91 for _, ksa := range strings.Split(svc.Annotations[annotation.AlphaKubernetesServiceAccounts.Name], ",") { 92 serviceaccounts = append(serviceaccounts, kubeToIstioServiceAccount(ksa, svc.Namespace)) 93 } 94 } 95 if svc.Annotations[annotation.NetworkingExportTo.Name] != "" { 96 exportTo = make(map[visibility.Instance]bool) 97 for _, e := range strings.Split(svc.Annotations[annotation.NetworkingExportTo.Name], ",") { 98 exportTo[visibility.Instance(e)] = true 99 } 100 } 101 sort.Strings(serviceaccounts) 102 103 istioService := &model.Service{ 104 Hostname: ServiceHostname(svc.Name, svc.Namespace, domainSuffix), 105 Ports: ports, 106 Address: addr, 107 ServiceAccounts: serviceaccounts, 108 MeshExternal: meshExternal, 109 Resolution: resolution, 110 CreationTime: svc.CreationTimestamp.Time, 111 Attributes: model.ServiceAttributes{ 112 ServiceRegistry: string(serviceregistry.Kubernetes), 113 Name: svc.Name, 114 Namespace: svc.Namespace, 115 UID: formatUID(svc.Namespace, svc.Name), 116 ExportTo: exportTo, 117 }, 118 } 119 120 switch svc.Spec.Type { 121 case coreV1.ServiceTypeNodePort: 122 if _, ok := svc.Annotations[NodeSelectorAnnotation]; ok { 123 // only do this for istio ingress-gateway services 124 break 125 } 126 // store the service port to node port mappings 127 portMap := make(map[uint32]uint32) 128 for _, p := range svc.Spec.Ports { 129 portMap[uint32(p.Port)] = uint32(p.NodePort) 130 } 131 istioService.Attributes.ClusterExternalPorts = map[string]map[uint32]uint32{clusterID: portMap} 132 // address mappings will be done elsewhere 133 case coreV1.ServiceTypeLoadBalancer: 134 if len(svc.Status.LoadBalancer.Ingress) > 0 { 135 var lbAddrs []string 136 for _, ingress := range svc.Status.LoadBalancer.Ingress { 137 if len(ingress.IP) > 0 { 138 lbAddrs = append(lbAddrs, ingress.IP) 139 } else if len(ingress.Hostname) > 0 { 140 // DO NOT resolve the DNS here. In environments like AWS, the ELB hostname 141 // does not have a repeatable DNS address and IPs resolved at an earlier point 142 // in time may not work. So, when we get just hostnames instead of IPs, we need 143 // to smartly switch from EDS to strict_dns rather than doing the naive thing of 144 // resolving the DNS name and hoping the resolution is one-time task. 145 lbAddrs = append(lbAddrs, ingress.Hostname) 146 } 147 } 148 if len(lbAddrs) > 0 { 149 istioService.Attributes.ClusterExternalAddresses = map[string][]string{clusterID: lbAddrs} 150 } 151 } 152 } 153 154 return istioService 155} 156 157func ExternalNameServiceInstances(k8sSvc coreV1.Service, svc *model.Service) []*model.ServiceInstance { 158 if k8sSvc.Spec.Type != coreV1.ServiceTypeExternalName || k8sSvc.Spec.ExternalName == "" { 159 return nil 160 } 161 out := make([]*model.ServiceInstance, 0, len(svc.Ports)) 162 for _, portEntry := range svc.Ports { 163 out = append(out, &model.ServiceInstance{ 164 Service: svc, 165 ServicePort: portEntry, 166 Endpoint: &model.IstioEndpoint{ 167 Address: k8sSvc.Spec.ExternalName, 168 EndpointPort: uint32(portEntry.Port), 169 ServicePortName: portEntry.Name, 170 Labels: k8sSvc.Labels, 171 }, 172 }) 173 } 174 return out 175} 176 177// ServiceHostname produces FQDN for a k8s service 178func ServiceHostname(name, namespace, domainSuffix string) host.Name { 179 return host.Name(name + "." + namespace + "." + "svc" + "." + domainSuffix) // Format: "%s.%s.svc.%s" 180} 181 182// kubeToIstioServiceAccount converts a K8s service account to an Istio service account 183func kubeToIstioServiceAccount(saname string, ns string) string { 184 return spiffe.MustGenSpiffeURI(ns, saname) 185} 186 187// SecureNamingSAN creates the secure naming used for SAN verification from pod metadata 188func SecureNamingSAN(pod *coreV1.Pod) string { 189 190 //use the identity annotation 191 if identity, exist := pod.Annotations[annotation.AlphaIdentity.Name]; exist { 192 return spiffe.GenCustomSpiffe(identity) 193 } 194 195 return spiffe.MustGenSpiffeURI(pod.Namespace, pod.Spec.ServiceAccountName) 196} 197 198// PodTLSMode returns the tls mode associated with the pod if pod has been injected with sidecar 199func PodTLSMode(pod *coreV1.Pod) string { 200 if pod == nil { 201 return model.DisabledTLSModeLabel 202 } 203 return model.GetTLSModeFromEndpointLabels(pod.Labels) 204} 205 206// KeyFunc is the internal API key function that returns "namespace"/"name" or 207// "name" if "namespace" is empty 208func KeyFunc(name, namespace string) string { 209 if len(namespace) == 0 { 210 return name 211 } 212 return namespace + "/" + name 213} 214 215func ConvertProbePort(c *coreV1.Container, handler *coreV1.Handler) (*model.Port, error) { 216 if handler == nil { 217 return nil, nil 218 } 219 220 var p protocol.Instance 221 var portVal intstr.IntOrString 222 223 // Only two types of handler is allowed by Kubernetes (HTTPGet or TCPSocket) 224 switch { 225 case handler.HTTPGet != nil: 226 portVal = handler.HTTPGet.Port 227 p = protocol.HTTP 228 case handler.TCPSocket != nil: 229 portVal = handler.TCPSocket.Port 230 p = protocol.TCP 231 default: 232 return nil, nil 233 } 234 235 switch portVal.Type { 236 case intstr.Int: 237 port := portVal.IntValue() 238 return &model.Port{ 239 Name: managementPortPrefix + strconv.Itoa(port), 240 Port: port, 241 Protocol: p, 242 }, nil 243 case intstr.String: 244 for _, named := range c.Ports { 245 if named.Name == portVal.String() { 246 port := int(named.ContainerPort) 247 return &model.Port{ 248 Name: managementPortPrefix + strconv.Itoa(port), 249 Port: port, 250 Protocol: p, 251 }, nil 252 } 253 } 254 return nil, fmt.Errorf("missing named port %q", portVal) 255 default: 256 return nil, fmt.Errorf("incorrect port type %q", portVal.Type) 257 } 258} 259 260// ConvertProbesToPorts returns a PortList consisting of the ports where the 261// pod is configured to do Liveness and Readiness probes 262func ConvertProbesToPorts(t *coreV1.PodSpec) (model.PortList, error) { 263 set := make(map[string]*model.Port) 264 var errs error 265 for _, container := range t.Containers { 266 for _, probe := range []*coreV1.Probe{container.LivenessProbe, container.ReadinessProbe} { 267 if probe == nil { 268 continue 269 } 270 271 p, err := ConvertProbePort(&container, &probe.Handler) 272 if err != nil { 273 errs = multierror.Append(errs, err) 274 } else if p != nil && set[p.Name] == nil { 275 // Deduplicate along the way. We don't differentiate between HTTP vs TCP mgmt ports 276 set[p.Name] = p 277 } 278 } 279 } 280 281 mgmtPorts := make(model.PortList, 0, len(set)) 282 for _, p := range set { 283 mgmtPorts = append(mgmtPorts, p) 284 } 285 sort.Slice(mgmtPorts, func(i, j int) bool { return mgmtPorts[i].Port < mgmtPorts[j].Port }) 286 287 return mgmtPorts, errs 288} 289 290func formatUID(namespace, name string) string { 291 return "istio://" + namespace + "/services/" + name // Format : "istio://%s/services/%s" 292} 293