istio

Форк
0
/
conversion.go 
371 строка · 12.5 Кб
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

15
package ingress
16

17
import (
18
	"errors"
19
	"fmt"
20
	"sort"
21
	"strconv"
22
	"strings"
23

24
	"github.com/hashicorp/go-multierror"
25
	corev1 "k8s.io/api/core/v1"
26
	knetworking "k8s.io/api/networking/v1"
27

28
	"istio.io/api/annotation"
29
	meshconfig "istio.io/api/mesh/v1alpha1"
30
	networking "istio.io/api/networking/v1alpha3"
31
	"istio.io/istio/pkg/config"
32
	"istio.io/istio/pkg/config/constants"
33
	"istio.io/istio/pkg/config/labels"
34
	"istio.io/istio/pkg/config/protocol"
35
	"istio.io/istio/pkg/config/schema/gvk"
36
	"istio.io/istio/pkg/kube/kclient"
37
	"istio.io/istio/pkg/log"
38
)
39

40
const (
41
	IstioIngressController = "istio.io/ingress-controller"
42
)
43

44
var errNotFound = errors.New("item not found")
45

46
// EncodeIngressRuleName encodes an ingress rule name for a given ingress resource name,
47
// as well as the position of the rule and path specified within it, counting from 1.
48
// ruleNum == pathNum == 0 indicates the default backend specified for an ingress.
49
func EncodeIngressRuleName(ingressName string, ruleNum, pathNum int) string {
50
	return fmt.Sprintf("%s-%d-%d", ingressName, ruleNum, pathNum)
51
}
52

53
// decodeIngressRuleName decodes an ingress rule name previously encoded with EncodeIngressRuleName.
54
func decodeIngressRuleName(name string) (ingressName string, ruleNum, pathNum int, err error) {
55
	parts := strings.Split(name, "-")
56
	if len(parts) < 3 {
57
		err = fmt.Errorf("could not decode string into ingress rule name: %s", name)
58
		return
59
	}
60

61
	ingressName = strings.Join(parts[0:len(parts)-2], "-")
62
	ruleNum, ruleErr := strconv.Atoi(parts[len(parts)-2])
63
	pathNum, pathErr := strconv.Atoi(parts[len(parts)-1])
64

65
	if pathErr != nil || ruleErr != nil {
66
		err = multierror.Append(
67
			fmt.Errorf("could not decode string into ingress rule name: %s", name),
68
			pathErr, ruleErr)
69
		return
70
	}
71

72
	return
73
}
74

75
// ConvertIngressV1alpha3 converts from ingress spec to Istio Gateway
76
func ConvertIngressV1alpha3(ingress knetworking.Ingress, mesh *meshconfig.MeshConfig, domainSuffix string) config.Config {
77
	gateway := &networking.Gateway{}
78
	gateway.Selector = getIngressGatewaySelector(mesh.IngressSelector, mesh.IngressService)
79

80
	for i, tls := range ingress.Spec.TLS {
81
		if tls.SecretName == "" {
82
			log.Infof("invalid ingress rule %s:%s for hosts %q, no secretName defined", ingress.Namespace, ingress.Name, tls.Hosts)
83
			continue
84
		}
85
		// TODO validation when multiple wildcard tls secrets are given
86
		if len(tls.Hosts) == 0 {
87
			tls.Hosts = []string{"*"}
88
		}
89
		gateway.Servers = append(gateway.Servers, &networking.Server{
90
			Port: &networking.Port{
91
				Number:   443,
92
				Protocol: string(protocol.HTTPS),
93
				Name:     fmt.Sprintf("https-443-ingress-%s-%s-%d", ingress.Name, ingress.Namespace, i),
94
			},
95
			Hosts: tls.Hosts,
96
			Tls: &networking.ServerTLSSettings{
97
				HttpsRedirect:  false,
98
				Mode:           networking.ServerTLSSettings_SIMPLE,
99
				CredentialName: tls.SecretName,
100
			},
101
		})
102
	}
103

104
	gateway.Servers = append(gateway.Servers, &networking.Server{
105
		Port: &networking.Port{
106
			Number:   80,
107
			Protocol: string(protocol.HTTP),
108
			Name:     fmt.Sprintf("http-80-ingress-%s-%s", ingress.Name, ingress.Namespace),
109
		},
110
		Hosts: []string{"*"},
111
	})
112

113
	gatewayConfig := config.Config{
114
		Meta: config.Meta{
115
			GroupVersionKind: gvk.Gateway,
116
			Name:             ingress.Name + "-" + constants.IstioIngressGatewayName + "-" + ingress.Namespace,
117
			Namespace:        IngressNamespace,
118
			Domain:           domainSuffix,
119
		},
120
		Spec: gateway,
121
	}
122

123
	return gatewayConfig
124
}
125

126
// ConvertIngressVirtualService converts from ingress spec to Istio VirtualServices
127
func ConvertIngressVirtualService(ingress knetworking.Ingress, domainSuffix string,
128
	ingressByHost map[string]*config.Config, services kclient.Client[*corev1.Service],
129
) {
130
	// Ingress allows a single host - if missing '*' is assumed
131
	// We need to merge all rules with a particular host across
132
	// all ingresses, and return a separate VirtualService for each
133
	// host.
134
	for _, rule := range ingress.Spec.Rules {
135
		if rule.HTTP == nil {
136
			log.Infof("invalid ingress rule %s:%s for host %q, no paths defined", ingress.Namespace, ingress.Name, rule.Host)
137
			continue
138
		}
139

140
		host := rule.Host
141
		namePrefix := strings.Replace(host, ".", "-", -1)
142
		if host == "" {
143
			host = "*"
144
		}
145
		virtualService := &networking.VirtualService{
146
			Hosts:    []string{host},
147
			Gateways: []string{fmt.Sprintf("%s/%s-%s-%s", IngressNamespace, ingress.Name, constants.IstioIngressGatewayName, ingress.Namespace)},
148
		}
149

150
		httpRoutes := make([]*networking.HTTPRoute, 0, len(rule.HTTP.Paths))
151
		for _, httpPath := range rule.HTTP.Paths {
152
			httpMatch := &networking.HTTPMatchRequest{}
153
			if httpPath.PathType != nil {
154
				switch *httpPath.PathType {
155
				case knetworking.PathTypeExact:
156
					httpMatch.Uri = &networking.StringMatch{
157
						MatchType: &networking.StringMatch_Exact{Exact: httpPath.Path},
158
					}
159
				case knetworking.PathTypePrefix:
160
					// Optimize common case of / to not needed regex
161
					httpMatch.Uri = &networking.StringMatch{
162
						MatchType: &networking.StringMatch_Prefix{Prefix: httpPath.Path},
163
					}
164
				default:
165
					// Fallback to the legacy string matching
166
					// If the httpPath.Path is a wildcard path, Uri will be nil
167
					httpMatch.Uri = createFallbackStringMatch(httpPath.Path)
168
				}
169
			} else {
170
				httpMatch.Uri = createFallbackStringMatch(httpPath.Path)
171
			}
172

173
			httpRoute := ingressBackendToHTTPRoute(&httpPath.Backend, ingress.Namespace, domainSuffix, services)
174
			if httpRoute == nil {
175
				log.Infof("invalid ingress rule %s:%s for host %q, no backend defined for path", ingress.Namespace, ingress.Name, rule.Host)
176
				continue
177
			}
178
			// Only create a match if Uri is not nil. HttpMatchRequest cannot be empty
179
			if httpMatch.Uri != nil {
180
				httpRoute.Match = []*networking.HTTPMatchRequest{httpMatch}
181
			}
182
			httpRoutes = append(httpRoutes, httpRoute)
183
		}
184

185
		virtualService.Http = httpRoutes
186

187
		virtualServiceConfig := config.Config{
188
			Meta: config.Meta{
189
				GroupVersionKind: gvk.VirtualService,
190
				Name:             namePrefix + "-" + ingress.Name + "-" + constants.IstioIngressGatewayName,
191
				Namespace:        ingress.Namespace,
192
				Domain:           domainSuffix,
193
				Annotations:      map[string]string{constants.InternalRouteSemantics: constants.RouteSemanticsIngress},
194
			},
195
			Spec: virtualService,
196
		}
197

198
		old, f := ingressByHost[host]
199
		if f {
200
			vs := old.Spec.(*networking.VirtualService)
201
			vs.Http = append(vs.Http, httpRoutes...)
202
		} else {
203
			ingressByHost[host] = &virtualServiceConfig
204
		}
205

206
		// sort routes to meet ingress route precedence requirements
207
		// see https://kubernetes.io/docs/concepts/services-networking/ingress/#multiple-matches
208
		vs := ingressByHost[host].Spec.(*networking.VirtualService)
209
		sort.SliceStable(vs.Http, func(i, j int) bool {
210
			var r1Len, r2Len int
211
			var r1Ex, r2Ex bool
212
			if vs.Http[i].Match != nil || len(vs.Http[i].Match) != 0 {
213
				r1Len, r1Ex = getMatchURILength(vs.Http[i].Match[0])
214
			}
215
			if vs.Http[j].Match != nil || len(vs.Http[j].Match) != 0 {
216
				r2Len, r2Ex = getMatchURILength(vs.Http[j].Match[0])
217
			}
218
			// TODO: default at the end
219
			if r1Len == r2Len {
220
				return r1Ex && !r2Ex
221
			}
222
			return r1Len > r2Len
223
		})
224
	}
225

226
	// Matches * and "/". Currently not supported - would conflict
227
	// with any other explicit VirtualService.
228
	if ingress.Spec.DefaultBackend != nil {
229
		log.Infof("Ignore default wildcard ingress, use VirtualService %s:%s",
230
			ingress.Namespace, ingress.Name)
231
	}
232
}
233

234
// getMatchURILength returns the length of matching path, and whether the match type is EXACT
235
func getMatchURILength(match *networking.HTTPMatchRequest) (length int, exact bool) {
236
	uri := match.GetUri()
237
	switch uri.GetMatchType().(type) {
238
	case *networking.StringMatch_Exact:
239
		return len(uri.GetExact()), true
240
	case *networking.StringMatch_Prefix:
241
		return len(uri.GetPrefix()), false
242
	}
243
	// should not happen
244
	return -1, false
245
}
246

247
func ingressBackendToHTTPRoute(backend *knetworking.IngressBackend, namespace string,
248
	domainSuffix string, services kclient.Client[*corev1.Service],
249
) *networking.HTTPRoute {
250
	if backend == nil {
251
		return nil
252
	}
253

254
	port := &networking.PortSelector{}
255

256
	if backend.Service == nil {
257
		log.Infof("backend service must be specified")
258
		return nil
259
	}
260
	if backend.Service.Port.Number > 0 {
261
		port.Number = uint32(backend.Service.Port.Number)
262
	} else {
263
		resolvedPort, err := resolveNamedPort(backend, namespace, services)
264
		if err != nil {
265
			log.Infof("failed to resolve named port %s, error: %v", backend.Service.Port.Name, err)
266
			return nil
267
		}
268
		port.Number = uint32(resolvedPort)
269
	}
270

271
	return &networking.HTTPRoute{
272
		Route: []*networking.HTTPRouteDestination{
273
			{
274
				Destination: &networking.Destination{
275
					Host: fmt.Sprintf("%s.%s.svc.%s", backend.Service.Name, namespace, domainSuffix),
276
					Port: port,
277
				},
278
				Weight: 100,
279
			},
280
		},
281
	}
282
}
283

284
func resolveNamedPort(backend *knetworking.IngressBackend, namespace string, services kclient.Client[*corev1.Service]) (int32, error) {
285
	svc := services.Get(backend.Service.Name, namespace)
286
	if svc == nil {
287
		return 0, errNotFound
288
	}
289
	for _, port := range svc.Spec.Ports {
290
		if port.Name == backend.Service.Port.Name {
291
			return port.Port, nil
292
		}
293
	}
294
	return 0, errNotFound
295
}
296

297
// shouldProcessIngress determines whether the given knetworking resource should be processed
298
// by the controller, based on its knetworking class annotation or, in more recent versions of
299
// kubernetes (v1.18+), based on the Ingress's specified IngressClass
300
// See https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class
301
func shouldProcessIngressWithClass(mesh *meshconfig.MeshConfig, ingress *knetworking.Ingress, ingressClass *knetworking.IngressClass) bool {
302
	if class, exists := ingress.Annotations[annotation.IoKubernetesIngressClass.Name]; exists {
303
		switch mesh.IngressControllerMode {
304
		case meshconfig.MeshConfig_OFF:
305
			return false
306
		case meshconfig.MeshConfig_STRICT:
307
			return class == mesh.IngressClass
308
		case meshconfig.MeshConfig_DEFAULT:
309
			return class == mesh.IngressClass
310
		default:
311
			log.Warnf("invalid ingress synchronization mode: %v", mesh.IngressControllerMode)
312
			return false
313
		}
314
	} else if ingressClass != nil {
315
		return ingressClass.Spec.Controller == IstioIngressController
316
	} else {
317
		switch mesh.IngressControllerMode {
318
		case meshconfig.MeshConfig_OFF:
319
			return false
320
		case meshconfig.MeshConfig_STRICT:
321
			return false
322
		case meshconfig.MeshConfig_DEFAULT:
323
			return true
324
		default:
325
			log.Warnf("invalid ingress synchronization mode: %v", mesh.IngressControllerMode)
326
			return false
327
		}
328
	}
329
}
330

331
func createFallbackStringMatch(s string) *networking.StringMatch {
332
	// If the string is empty or a wildcard, return nil
333
	if s == "" || s == "*" || s == "/*" || s == ".*" {
334
		return nil
335
	}
336

337
	// Note that this implementation only converts prefix and exact matches, not regexps.
338

339
	// Replace e.g. "foo.*" with prefix match
340
	if strings.HasSuffix(s, ".*") {
341
		return &networking.StringMatch{
342
			MatchType: &networking.StringMatch_Prefix{Prefix: strings.TrimSuffix(s, ".*")},
343
		}
344
	}
345
	if strings.HasSuffix(s, "/*") {
346
		return &networking.StringMatch{
347
			MatchType: &networking.StringMatch_Prefix{Prefix: strings.TrimSuffix(s, "/*")},
348
		}
349
	}
350

351
	// Replace e.g. "foo" with a exact match
352
	return &networking.StringMatch{
353
		MatchType: &networking.StringMatch_Exact{Exact: s},
354
	}
355
}
356

357
func getIngressGatewaySelector(ingressSelector, ingressService string) map[string]string {
358
	// Setup the selector for the gateway
359
	if ingressSelector != "" {
360
		// If explicitly defined, use this one
361
		return labels.Instance{constants.IstioLabel: ingressSelector}
362
	} else if ingressService != "istio-ingressgateway" && ingressService != "" {
363
		// Otherwise, we will use the ingress service as the default. It is common for the selector and service
364
		// to be the same, so this removes the need for two configurations
365
		// However, if its istio-ingressgateway we need to use the old values for backwards compatibility
366
		return labels.Instance{constants.IstioLabel: ingressService}
367
	}
368
	// If we have neither an explicitly defined ingressSelector or ingressService then use a selector
369
	// pointing to the ingressgateway from the default installation
370
	return labels.Instance{constants.IstioLabel: constants.IstioIngressLabelValue}
371
}
372

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.