k8s.io/apiserver@v0.31.1/pkg/util/proxy/proxy.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package proxy 18 19 import ( 20 "context" 21 "fmt" 22 "math/rand" 23 "net" 24 "net/http" 25 "net/url" 26 "strconv" 27 "strings" 28 "time" 29 30 "k8s.io/api/core/v1" 31 "k8s.io/apimachinery/pkg/api/errors" 32 utilnet "k8s.io/apimachinery/pkg/util/net" 33 auditinternal "k8s.io/apiserver/pkg/apis/audit" 34 "k8s.io/apiserver/pkg/audit" 35 genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 36 listersv1 "k8s.io/client-go/listers/core/v1" 37 ) 38 39 const ( 40 // taken from https://github.com/kubernetes/kubernetes/blob/release-1.27/staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go#L47 41 aggregatedDiscoveryTimeout = 5 * time.Second 42 ) 43 44 // findServicePort finds the service port by name or numerically. 45 func findServicePort(svc *v1.Service, port int32) (*v1.ServicePort, error) { 46 for _, svcPort := range svc.Spec.Ports { 47 if svcPort.Port == port { 48 return &svcPort, nil 49 } 50 } 51 return nil, errors.NewServiceUnavailable(fmt.Sprintf("no service port %d found for service %q", port, svc.Name)) 52 } 53 54 // ResolveEndpoint returns a URL to which one can send traffic for the specified service. 55 func ResolveEndpoint(services listersv1.ServiceLister, endpoints listersv1.EndpointsLister, namespace, id string, port int32) (*url.URL, error) { 56 svc, err := services.Services(namespace).Get(id) 57 if err != nil { 58 return nil, err 59 } 60 61 switch { 62 case svc.Spec.Type == v1.ServiceTypeClusterIP, svc.Spec.Type == v1.ServiceTypeLoadBalancer, svc.Spec.Type == v1.ServiceTypeNodePort: 63 // these are fine 64 default: 65 return nil, fmt.Errorf("unsupported service type %q", svc.Spec.Type) 66 } 67 68 svcPort, err := findServicePort(svc, port) 69 if err != nil { 70 return nil, err 71 } 72 73 eps, err := endpoints.Endpoints(namespace).Get(svc.Name) 74 if err != nil { 75 return nil, err 76 } 77 if len(eps.Subsets) == 0 { 78 return nil, errors.NewServiceUnavailable(fmt.Sprintf("no endpoints available for service %q", svc.Name)) 79 } 80 81 // Pick a random Subset to start searching from. 82 ssSeed := rand.Intn(len(eps.Subsets)) 83 84 // Find a Subset that has the port. 85 for ssi := 0; ssi < len(eps.Subsets); ssi++ { 86 ss := &eps.Subsets[(ssSeed+ssi)%len(eps.Subsets)] 87 if len(ss.Addresses) == 0 { 88 continue 89 } 90 for i := range ss.Ports { 91 if ss.Ports[i].Name == svcPort.Name { 92 // Pick a random address. 93 ip := ss.Addresses[rand.Intn(len(ss.Addresses))].IP 94 port := int(ss.Ports[i].Port) 95 return &url.URL{ 96 Scheme: "https", 97 Host: net.JoinHostPort(ip, strconv.Itoa(port)), 98 }, nil 99 } 100 } 101 } 102 return nil, errors.NewServiceUnavailable(fmt.Sprintf("no endpoints available for service %q", id)) 103 } 104 105 func ResolveCluster(services listersv1.ServiceLister, namespace, id string, port int32) (*url.URL, error) { 106 svc, err := services.Services(namespace).Get(id) 107 if err != nil { 108 return nil, err 109 } 110 111 switch { 112 case svc.Spec.Type == v1.ServiceTypeClusterIP && svc.Spec.ClusterIP == v1.ClusterIPNone: 113 return nil, fmt.Errorf(`cannot route to service with ClusterIP "None"`) 114 // use IP from a clusterIP for these service types 115 case svc.Spec.Type == v1.ServiceTypeClusterIP, svc.Spec.Type == v1.ServiceTypeLoadBalancer, svc.Spec.Type == v1.ServiceTypeNodePort: 116 svcPort, err := findServicePort(svc, port) 117 if err != nil { 118 return nil, err 119 } 120 return &url.URL{ 121 Scheme: "https", 122 Host: net.JoinHostPort(svc.Spec.ClusterIP, fmt.Sprintf("%d", svcPort.Port)), 123 }, nil 124 case svc.Spec.Type == v1.ServiceTypeExternalName: 125 return &url.URL{ 126 Scheme: "https", 127 Host: net.JoinHostPort(svc.Spec.ExternalName, fmt.Sprintf("%d", port)), 128 }, nil 129 default: 130 return nil, fmt.Errorf("unsupported service type %q", svc.Spec.Type) 131 } 132 } 133 134 // NewRequestForProxy returns a shallow copy of the original request with a context that may include a timeout for discovery requests 135 func NewRequestForProxy(location *url.URL, req *http.Request) (*http.Request, context.CancelFunc) { 136 newCtx := req.Context() 137 cancelFn := func() {} 138 139 if requestInfo, ok := genericapirequest.RequestInfoFrom(req.Context()); ok { 140 // trim leading and trailing slashes. Then "/apis/group/version" requests are for discovery, so if we have exactly three 141 // segments that we are going to proxy, we have a discovery request. 142 if !requestInfo.IsResourceRequest && len(strings.Split(strings.Trim(requestInfo.Path, "/"), "/")) == 3 { 143 // discovery requests are used by kubectl and others to determine which resources a server has. This is a cheap call that 144 // should be fast for every aggregated apiserver. Latency for aggregation is expected to be low (as for all extensions) 145 // so forcing a short timeout here helps responsiveness of all clients. 146 newCtx, cancelFn = context.WithTimeout(newCtx, aggregatedDiscoveryTimeout) 147 } 148 } 149 150 // WithContext creates a shallow clone of the request with the same context. 151 newReq := req.WithContext(newCtx) 152 newReq.Header = utilnet.CloneHeader(req.Header) 153 newReq.URL = location 154 newReq.Host = location.Host 155 156 // If the original request has an audit ID, let's make sure we propagate this 157 // to the aggregated server. 158 if auditID, found := audit.AuditIDFrom(req.Context()); found { 159 newReq.Header.Set(auditinternal.HeaderAuditID, string(auditID)) 160 } 161 162 return newReq, cancelFn 163 }