1
// Copyright Istio Authors
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
7
// http://www.apache.org/licenses/LICENSE-2.0
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.
24
"github.com/hashicorp/go-multierror"
25
corev1 "k8s.io/api/core/v1"
26
knetworking "k8s.io/api/networking/v1"
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"
41
IstioIngressController = "istio.io/ingress-controller"
44
var errNotFound = errors.New("item not found")
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)
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, "-")
57
err = fmt.Errorf("could not decode string into ingress rule name: %s", name)
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])
65
if pathErr != nil || ruleErr != nil {
66
err = multierror.Append(
67
fmt.Errorf("could not decode string into ingress rule name: %s", name),
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)
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)
85
// TODO validation when multiple wildcard tls secrets are given
86
if len(tls.Hosts) == 0 {
87
tls.Hosts = []string{"*"}
89
gateway.Servers = append(gateway.Servers, &networking.Server{
90
Port: &networking.Port{
92
Protocol: string(protocol.HTTPS),
93
Name: fmt.Sprintf("https-443-ingress-%s-%s-%d", ingress.Name, ingress.Namespace, i),
96
Tls: &networking.ServerTLSSettings{
98
Mode: networking.ServerTLSSettings_SIMPLE,
99
CredentialName: tls.SecretName,
104
gateway.Servers = append(gateway.Servers, &networking.Server{
105
Port: &networking.Port{
107
Protocol: string(protocol.HTTP),
108
Name: fmt.Sprintf("http-80-ingress-%s-%s", ingress.Name, ingress.Namespace),
110
Hosts: []string{"*"},
113
gatewayConfig := config.Config{
115
GroupVersionKind: gvk.Gateway,
116
Name: ingress.Name + "-" + constants.IstioIngressGatewayName + "-" + ingress.Namespace,
117
Namespace: IngressNamespace,
118
Domain: domainSuffix,
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],
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
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)
141
namePrefix := strings.Replace(host, ".", "-", -1)
145
virtualService := &networking.VirtualService{
146
Hosts: []string{host},
147
Gateways: []string{fmt.Sprintf("%s/%s-%s-%s", IngressNamespace, ingress.Name, constants.IstioIngressGatewayName, ingress.Namespace)},
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},
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},
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)
170
httpMatch.Uri = createFallbackStringMatch(httpPath.Path)
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)
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}
182
httpRoutes = append(httpRoutes, httpRoute)
185
virtualService.Http = httpRoutes
187
virtualServiceConfig := config.Config{
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},
195
Spec: virtualService,
198
old, f := ingressByHost[host]
200
vs := old.Spec.(*networking.VirtualService)
201
vs.Http = append(vs.Http, httpRoutes...)
203
ingressByHost[host] = &virtualServiceConfig
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 {
212
if vs.Http[i].Match != nil || len(vs.Http[i].Match) != 0 {
213
r1Len, r1Ex = getMatchURILength(vs.Http[i].Match[0])
215
if vs.Http[j].Match != nil || len(vs.Http[j].Match) != 0 {
216
r2Len, r2Ex = getMatchURILength(vs.Http[j].Match[0])
218
// TODO: default at the end
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)
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
247
func ingressBackendToHTTPRoute(backend *knetworking.IngressBackend, namespace string,
248
domainSuffix string, services kclient.Client[*corev1.Service],
249
) *networking.HTTPRoute {
254
port := &networking.PortSelector{}
256
if backend.Service == nil {
257
log.Infof("backend service must be specified")
260
if backend.Service.Port.Number > 0 {
261
port.Number = uint32(backend.Service.Port.Number)
263
resolvedPort, err := resolveNamedPort(backend, namespace, services)
265
log.Infof("failed to resolve named port %s, error: %v", backend.Service.Port.Name, err)
268
port.Number = uint32(resolvedPort)
271
return &networking.HTTPRoute{
272
Route: []*networking.HTTPRouteDestination{
274
Destination: &networking.Destination{
275
Host: fmt.Sprintf("%s.%s.svc.%s", backend.Service.Name, namespace, domainSuffix),
284
func resolveNamedPort(backend *knetworking.IngressBackend, namespace string, services kclient.Client[*corev1.Service]) (int32, error) {
285
svc := services.Get(backend.Service.Name, namespace)
287
return 0, errNotFound
289
for _, port := range svc.Spec.Ports {
290
if port.Name == backend.Service.Port.Name {
291
return port.Port, nil
294
return 0, errNotFound
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:
306
case meshconfig.MeshConfig_STRICT:
307
return class == mesh.IngressClass
308
case meshconfig.MeshConfig_DEFAULT:
309
return class == mesh.IngressClass
311
log.Warnf("invalid ingress synchronization mode: %v", mesh.IngressControllerMode)
314
} else if ingressClass != nil {
315
return ingressClass.Spec.Controller == IstioIngressController
317
switch mesh.IngressControllerMode {
318
case meshconfig.MeshConfig_OFF:
320
case meshconfig.MeshConfig_STRICT:
322
case meshconfig.MeshConfig_DEFAULT:
325
log.Warnf("invalid ingress synchronization mode: %v", mesh.IngressControllerMode)
331
func createFallbackStringMatch(s string) *networking.StringMatch {
332
// If the string is empty or a wildcard, return nil
333
if s == "" || s == "*" || s == "/*" || s == ".*" {
337
// Note that this implementation only converts prefix and exact matches, not regexps.
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, ".*")},
345
if strings.HasSuffix(s, "/*") {
346
return &networking.StringMatch{
347
MatchType: &networking.StringMatch_Prefix{Prefix: strings.TrimSuffix(s, "/*")},
351
// Replace e.g. "foo" with a exact match
352
return &networking.StringMatch{
353
MatchType: &networking.StringMatch_Exact{Exact: s},
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}
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}