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  }