2019-01-12 04:58:27 +00:00
/ *
Copyright 2016 The Kubernetes Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package request
import (
"context"
"fmt"
"net/http"
"strings"
"k8s.io/apimachinery/pkg/api/validation/path"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
2019-12-12 01:27:03 +00:00
metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
2019-01-12 04:58:27 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/klog"
)
// LongRunningRequestCheck is a predicate which is true for long-running http requests.
type LongRunningRequestCheck func ( r * http . Request , requestInfo * RequestInfo ) bool
type RequestInfoResolver interface {
NewRequestInfo ( req * http . Request ) ( * RequestInfo , error )
}
// RequestInfo holds information parsed from the http.Request
type RequestInfo struct {
// IsResourceRequest indicates whether or not the request is for an API resource or subresource
IsResourceRequest bool
// Path is the URL path of the request
Path string
// Verb is the kube verb associated with the request for API requests, not the http verb. This includes things like list and watch.
// for non-resource requests, this is the lowercase http verb
Verb string
APIPrefix string
APIGroup string
APIVersion string
Namespace string
// Resource is the name of the resource being requested. This is not the kind. For example: pods
Resource string
// Subresource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind.
// For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod"
// (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding".
Subresource string
// Name is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in.
Name string
// Parts are the path parts for the request, always starting with /{resource}/{name}
Parts [ ] string
}
// specialVerbs contains just strings which are used in REST paths for special actions that don't fall under the normal
// CRUDdy GET/POST/PUT/DELETE actions on REST objects.
// TODO: find a way to keep this up to date automatically. Maybe dynamically populate list as handlers added to
// master's Mux.
var specialVerbs = sets . NewString ( "proxy" , "watch" )
// specialVerbsNoSubresources contains root verbs which do not allow subresources
var specialVerbsNoSubresources = sets . NewString ( "proxy" )
// namespaceSubresources contains subresources of namespace
// this list allows the parser to distinguish between a namespace subresource, and a namespaced resource
var namespaceSubresources = sets . NewString ( "status" , "finalize" )
// NamespaceSubResourcesForTest exports namespaceSubresources for testing in pkg/master/master_test.go, so we never drift
var NamespaceSubResourcesForTest = sets . NewString ( namespaceSubresources . List ( ) ... )
type RequestInfoFactory struct {
APIPrefixes sets . String // without leading and trailing slashes
GrouplessAPIPrefixes sets . String // without leading and trailing slashes
}
// TODO write an integration test against the swagger doc to test the RequestInfo and match up behavior to responses
// NewRequestInfo returns the information from the http request. If error is not nil, RequestInfo holds the information as best it is known before the failure
// It handles both resource and non-resource requests and fills in all the pertinent information for each.
// Valid Inputs:
// Resource paths
// /apis/{api-group}/{version}/namespaces
// /api/{version}/namespaces
// /api/{version}/namespaces/{namespace}
// /api/{version}/namespaces/{namespace}/{resource}
// /api/{version}/namespaces/{namespace}/{resource}/{resourceName}
// /api/{version}/{resource}
// /api/{version}/{resource}/{resourceName}
//
// Special verbs without subresources:
// /api/{version}/proxy/{resource}/{resourceName}
// /api/{version}/proxy/namespaces/{namespace}/{resource}/{resourceName}
//
// Special verbs with subresources:
// /api/{version}/watch/{resource}
// /api/{version}/watch/namespaces/{namespace}/{resource}
//
// NonResource paths
// /apis/{api-group}/{version}
// /apis/{api-group}
// /apis
// /api/{version}
// /api
// /healthz
// /
func ( r * RequestInfoFactory ) NewRequestInfo ( req * http . Request ) ( * RequestInfo , error ) {
// start with a non-resource request until proven otherwise
requestInfo := RequestInfo {
IsResourceRequest : false ,
Path : req . URL . Path ,
Verb : strings . ToLower ( req . Method ) ,
}
currentParts := splitPath ( req . URL . Path )
if len ( currentParts ) < 3 {
// return a non-resource request
return & requestInfo , nil
}
if ! r . APIPrefixes . Has ( currentParts [ 0 ] ) {
// return a non-resource request
return & requestInfo , nil
}
requestInfo . APIPrefix = currentParts [ 0 ]
currentParts = currentParts [ 1 : ]
if ! r . GrouplessAPIPrefixes . Has ( requestInfo . APIPrefix ) {
// one part (APIPrefix) has already been consumed, so this is actually "do we have four parts?"
if len ( currentParts ) < 3 {
// return a non-resource request
return & requestInfo , nil
}
requestInfo . APIGroup = currentParts [ 0 ]
currentParts = currentParts [ 1 : ]
}
requestInfo . IsResourceRequest = true
requestInfo . APIVersion = currentParts [ 0 ]
currentParts = currentParts [ 1 : ]
// handle input of form /{specialVerb}/*
if specialVerbs . Has ( currentParts [ 0 ] ) {
if len ( currentParts ) < 2 {
return & requestInfo , fmt . Errorf ( "unable to determine kind and namespace from url, %v" , req . URL )
}
requestInfo . Verb = currentParts [ 0 ]
currentParts = currentParts [ 1 : ]
} else {
switch req . Method {
case "POST" :
requestInfo . Verb = "create"
case "GET" , "HEAD" :
requestInfo . Verb = "get"
case "PUT" :
requestInfo . Verb = "update"
case "PATCH" :
requestInfo . Verb = "patch"
case "DELETE" :
requestInfo . Verb = "delete"
default :
requestInfo . Verb = ""
}
}
// URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative to kind
if currentParts [ 0 ] == "namespaces" {
if len ( currentParts ) > 1 {
requestInfo . Namespace = currentParts [ 1 ]
// if there is another step after the namespace name and it is not a known namespace subresource
// move currentParts to include it as a resource in its own right
if len ( currentParts ) > 2 && ! namespaceSubresources . Has ( currentParts [ 2 ] ) {
currentParts = currentParts [ 2 : ]
}
}
} else {
requestInfo . Namespace = metav1 . NamespaceNone
}
// parsing successful, so we now know the proper value for .Parts
requestInfo . Parts = currentParts
// parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret
switch {
case len ( requestInfo . Parts ) >= 3 && ! specialVerbsNoSubresources . Has ( requestInfo . Verb ) :
requestInfo . Subresource = requestInfo . Parts [ 2 ]
fallthrough
case len ( requestInfo . Parts ) >= 2 :
requestInfo . Name = requestInfo . Parts [ 1 ]
fallthrough
case len ( requestInfo . Parts ) >= 1 :
requestInfo . Resource = requestInfo . Parts [ 0 ]
}
// if there's no name on the request and we thought it was a get before, then the actual verb is a list or a watch
if len ( requestInfo . Name ) == 0 && requestInfo . Verb == "get" {
opts := metainternalversion . ListOptions { }
2019-12-12 01:27:03 +00:00
if err := metainternalversionscheme . ParameterCodec . DecodeParameters ( req . URL . Query ( ) , metav1 . SchemeGroupVersion , & opts ) ; err != nil {
2019-01-12 04:58:27 +00:00
// An error in parsing request will result in default to "list" and not setting "name" field.
klog . Errorf ( "Couldn't parse request %#v: %v" , req . URL . Query ( ) , err )
// Reset opts to not rely on partial results from parsing.
// However, if watch is set, let's report it.
opts = metainternalversion . ListOptions { }
if values := req . URL . Query ( ) [ "watch" ] ; len ( values ) > 0 {
switch strings . ToLower ( values [ 0 ] ) {
case "false" , "0" :
default :
opts . Watch = true
}
}
}
if opts . Watch {
requestInfo . Verb = "watch"
} else {
requestInfo . Verb = "list"
}
if opts . FieldSelector != nil {
if name , ok := opts . FieldSelector . RequiresExactMatch ( "metadata.name" ) ; ok {
if len ( path . IsValidPathSegmentName ( name ) ) == 0 {
requestInfo . Name = name
}
}
}
}
// if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection
if len ( requestInfo . Name ) == 0 && requestInfo . Verb == "delete" {
requestInfo . Verb = "deletecollection"
}
return & requestInfo , nil
}
type requestInfoKeyType int
// requestInfoKey is the RequestInfo key for the context. It's of private type here. Because
// keys are interfaces and interfaces are equal when the type and the value is equal, this
// does not conflict with the keys defined in pkg/api.
const requestInfoKey requestInfoKeyType = iota
// WithRequestInfo returns a copy of parent in which the request info value is set
func WithRequestInfo ( parent context . Context , info * RequestInfo ) context . Context {
return WithValue ( parent , requestInfoKey , info )
}
// RequestInfoFrom returns the value of the RequestInfo key on the ctx
func RequestInfoFrom ( ctx context . Context ) ( * RequestInfo , bool ) {
info , ok := ctx . Value ( requestInfoKey ) . ( * RequestInfo )
return info , ok
}
// splitPath returns the segments for a URL path.
func splitPath ( path string ) [ ] string {
path = strings . Trim ( path , "/" )
if path == "" {
return [ ] string { }
}
return strings . Split ( path , "/" )
}