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