istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/ingress/conversion.go (about) 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 }