agones.dev/agones@v1.54.0/pkg/util/apiserver/apiserver.go (about)

     1  // Copyright 2019 Google LLC All Rights Reserved.
     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 apiserver manages kubernetes api extension apis
    16  package apiserver
    17  
    18  import (
    19  	"encoding/json"
    20  	"fmt"
    21  	"net/http"
    22  	"reflect"
    23  	"strings"
    24  
    25  	"agones.dev/agones/pkg/util/https"
    26  	"agones.dev/agones/pkg/util/runtime"
    27  	"github.com/go-openapi/spec"
    28  	"github.com/munnerz/goautoneg"
    29  	"github.com/pkg/errors"
    30  	"github.com/sirupsen/logrus"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	k8sruntime "k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	"k8s.io/apimachinery/pkg/runtime/serializer"
    35  	"k8s.io/kube-openapi/pkg/handler3"
    36  )
    37  
    38  var (
    39  	// Reference:
    40  	// https://github.com/googleforgames/agones/blob/main/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go
    41  	// These are public as they may be needed by CRDHandler implementations (usually for returning Status values)
    42  
    43  	// Scheme scheme for unversioned types - such as APIResourceList, and Status
    44  	Scheme = k8sruntime.NewScheme()
    45  	// Codecs for unversioned types - such as APIResourceList, and Status
    46  	Codecs = serializer.NewCodecFactory(Scheme)
    47  
    48  	unversionedVersion = schema.GroupVersion{Version: "v1"}
    49  	unversionedTypes   = []k8sruntime.Object{
    50  		&metav1.Status{},
    51  		&metav1.APIResourceList{},
    52  	}
    53  )
    54  
    55  const (
    56  	// ContentTypeHeader = "Content-Type"
    57  	ContentTypeHeader = "Content-Type"
    58  	// AcceptHeader = "Accept"
    59  	AcceptHeader = "Accept"
    60  )
    61  
    62  const (
    63  	// ListMaxCapacity is the maximum capacity for List in the gamerserver spec and status CRDs.
    64  	ListMaxCapacity = int64(1000)
    65  )
    66  
    67  func init() {
    68  	Scheme.AddUnversionedTypes(unversionedVersion, unversionedTypes...)
    69  }
    70  
    71  // CRDHandler is a http handler, that gets passed the Namespace it's working
    72  // on, and returns an error if a server error occurs
    73  type CRDHandler func(http.ResponseWriter, *http.Request, string) error
    74  
    75  // APIServer is a lightweight library for registering, and providing handlers
    76  // for Kubernetes APIServer extensions.
    77  type APIServer struct {
    78  	logger             *logrus.Entry
    79  	mux                *http.ServeMux
    80  	resourceList       map[string]*metav1.APIResourceList
    81  	openapiv2          *spec.Swagger
    82  	openapiv3Discovery *handler3.OpenAPIV3Discovery
    83  	delegates          map[string]CRDHandler
    84  }
    85  
    86  // NewAPIServer returns a new API Server from the given Mux.
    87  // creates a empty Swagger definition and sets up the endpoint.
    88  func NewAPIServer(mux *http.ServeMux) *APIServer {
    89  	s := &APIServer{
    90  		mux:                mux,
    91  		resourceList:       map[string]*metav1.APIResourceList{},
    92  		openapiv2:          &spec.Swagger{SwaggerProps: spec.SwaggerProps{}},
    93  		openapiv3Discovery: &handler3.OpenAPIV3Discovery{Paths: map[string]handler3.OpenAPIV3DiscoveryGroupVersion{}},
    94  		delegates:          map[string]CRDHandler{},
    95  	}
    96  	s.logger = runtime.NewLoggerWithType(s)
    97  	s.logger.Debug("API Server Started")
    98  
    99  	// We don't *have* to have a v3 openapi api, so just do an empty one for now, and we can expand as needed.
   100  	// If we implement /v3/ we can likely omit the /v2/ handler since only one is needed.
   101  	// This at least stops the K8s api pinging us for the spec all the time.
   102  	mux.HandleFunc("/openapi/v3", https.ErrorHTTPHandler(s.logger, func(w http.ResponseWriter, _ *http.Request) error {
   103  		w.Header().Set(ContentTypeHeader, k8sruntime.ContentTypeJSON)
   104  		err := json.NewEncoder(w).Encode(s.openapiv3Discovery)
   105  		if err != nil {
   106  			return errors.Wrap(err, "error encoding openapi/v3")
   107  		}
   108  		return nil
   109  	}))
   110  
   111  	// We don't *have* to have a v2 openapi api, so just do an empty one for now, and we can expand as needed.
   112  	// kube-openapi could be a potential library to look at for future if we want to be more specific.
   113  	// This at least stops the K8s api pinging us for every iteration of a api descriptor that may exist
   114  	s.openapiv2.SwaggerProps.Info = &spec.Info{InfoProps: spec.InfoProps{Title: "allocation.agones.dev"}}
   115  	mux.HandleFunc("/openapi/v2", https.ErrorHTTPHandler(s.logger, func(w http.ResponseWriter, _ *http.Request) error {
   116  		w.Header().Set(ContentTypeHeader, k8sruntime.ContentTypeJSON)
   117  		err := json.NewEncoder(w).Encode(s.openapiv2)
   118  		if err != nil {
   119  			return errors.Wrap(err, "error encoding openapi/v2")
   120  		}
   121  		return nil
   122  	}))
   123  
   124  	// We don't currently support a root /apis, but since Aggregate Discovery expects
   125  	// a 406, let's give it what it wants, otherwise namespaces don't successfully terminate on <= 1.27.2
   126  	mux.HandleFunc("/apis", func(w http.ResponseWriter, _ *http.Request) {
   127  		w.WriteHeader(http.StatusNotAcceptable)
   128  		w.Header().Set(ContentTypeHeader, k8sruntime.ContentTypeJSON)
   129  	})
   130  
   131  	return s
   132  }
   133  
   134  // AddAPIResource stores the APIResource under the given groupVersion string, and returns it
   135  // in the appropriate place for the K8s discovery service
   136  // e.g. http://localhost:8001/apis/scheduling.k8s.io/v1
   137  // as well as registering a CRDHandler that all http requests for the given APIResource are routed to
   138  func (as *APIServer) AddAPIResource(groupVersion string, resource metav1.APIResource, handler CRDHandler) {
   139  	_, ok := as.resourceList[groupVersion]
   140  	if !ok {
   141  		// discovery handler
   142  		list := &metav1.APIResourceList{GroupVersion: groupVersion, APIResources: []metav1.APIResource{}}
   143  		as.resourceList[groupVersion] = list
   144  		pattern := fmt.Sprintf("/apis/%s", groupVersion)
   145  		as.addSerializedHandler(pattern, list)
   146  		as.logger.WithField("groupversion", groupVersion).WithField("pattern", pattern).Debug("Adding Discovery Handler")
   147  
   148  		// e.g.  /apis/agones.dev/v1/namespaces/default/gameservers
   149  		// CRD handler
   150  		pattern = fmt.Sprintf("/apis/%s/namespaces/", groupVersion)
   151  		as.mux.HandleFunc(pattern, https.ErrorHTTPHandler(as.logger, as.resourceHandler(groupVersion)))
   152  		as.logger.WithField("groupversion", groupVersion).WithField("pattern", pattern).Debug("Adding Resource Handler")
   153  	}
   154  
   155  	// discovery resource
   156  	as.resourceList[groupVersion].APIResources = append(as.resourceList[groupVersion].APIResources, resource)
   157  
   158  	// add specific crd resource handler
   159  	key := fmt.Sprintf("%s/%s", groupVersion, resource.Name)
   160  	as.delegates[key] = handler
   161  
   162  	as.logger.WithField("groupversion", groupVersion).WithField("apiresource", resource).Debug("Adding APIResource")
   163  }
   164  
   165  // resourceHandler handles namespaced resource calls, and sends them to the appropriate CRDHandler delegate
   166  func (as *APIServer) resourceHandler(gv string) https.ErrorHandlerFunc {
   167  	return func(w http.ResponseWriter, r *http.Request) error {
   168  		namespace, resource, err := splitNameSpaceResource(r.URL.Path)
   169  		if err != nil {
   170  			https.FourZeroFour(as.logger.WithError(err), w, r)
   171  			return nil
   172  		}
   173  
   174  		delegate, ok := as.delegates[fmt.Sprintf("%s/%s", gv, resource)]
   175  		if !ok {
   176  			https.FourZeroFour(as.logger, w, r)
   177  			return nil
   178  		}
   179  
   180  		if err := delegate(w, r, namespace); err != nil {
   181  			return err
   182  		}
   183  
   184  		return nil
   185  	}
   186  }
   187  
   188  // addSerializedHandler sets up a handler than will send the serialised content
   189  // to the specified path.
   190  func (as *APIServer) addSerializedHandler(pattern string, m k8sruntime.Object) {
   191  	as.mux.HandleFunc(pattern, https.ErrorHTTPHandler(as.logger, func(w http.ResponseWriter, r *http.Request) error {
   192  		if r.Method == http.MethodGet {
   193  			info, err := AcceptedSerializer(r, Codecs)
   194  			if err != nil {
   195  				return err
   196  			}
   197  
   198  			shallowCopy := shallowCopyObjectForTargetKind(m)
   199  			w.Header().Set(ContentTypeHeader, info.MediaType)
   200  			err = Codecs.EncoderForVersion(info.Serializer, unversionedVersion).Encode(shallowCopy, w)
   201  			if err != nil {
   202  				return errors.New("error marshalling")
   203  			}
   204  		} else {
   205  			https.FourZeroFour(as.logger, w, r)
   206  		}
   207  
   208  		return nil
   209  	}))
   210  }
   211  
   212  // shallowCopyObjectForTargetKind ensures obj is unique by performing a shallow copy
   213  // of the struct Object points to (all Object must be a pointer to a struct in a scheme).
   214  // Copied from https://github.com/kubernetes/kubernetes/pull/101123 until the referenced PR is merged
   215  func shallowCopyObjectForTargetKind(obj k8sruntime.Object) k8sruntime.Object {
   216  	v := reflect.ValueOf(obj).Elem()
   217  	copied := reflect.New(v.Type())
   218  	copied.Elem().Set(v)
   219  	return copied.Interface().(k8sruntime.Object)
   220  }
   221  
   222  // AcceptedSerializer takes the request, and returns a serialiser (if it exists)
   223  // for the given codec factory and
   224  // for the Accepted media types.  If not found, returns error
   225  func AcceptedSerializer(r *http.Request, codecs serializer.CodecFactory) (k8sruntime.SerializerInfo, error) {
   226  	// this is so we know what we can accept
   227  	mediaTypes := codecs.SupportedMediaTypes()
   228  	alternatives := make([]string, len(mediaTypes))
   229  	for i, media := range mediaTypes {
   230  		alternatives[i] = media.MediaType
   231  	}
   232  	header := r.Header.Get(AcceptHeader)
   233  	accept := goautoneg.Negotiate(header, alternatives)
   234  	if accept == "" {
   235  		accept = k8sruntime.ContentTypeJSON
   236  	}
   237  	info, ok := k8sruntime.SerializerInfoForMediaType(mediaTypes, accept)
   238  	if !ok {
   239  		return info, errors.Errorf("Could not find serializer for Accept: %s", header)
   240  	}
   241  
   242  	return info, nil
   243  }
   244  
   245  // splitNameSpaceResource returns the namespace and the type of resource
   246  func splitNameSpaceResource(path string) (namespace, resource string, err error) {
   247  	list := strings.Split(strings.Trim(path, "/"), "/")
   248  	if len(list) < 3 {
   249  		return namespace, resource, errors.Errorf("could not find namespace and resource in path: %s", path)
   250  	}
   251  	last := list[len(list)-3:]
   252  
   253  	if last[0] != "namespaces" {
   254  		return namespace, resource, errors.Errorf("wrong format in path: %s", path)
   255  	}
   256  
   257  	return last[1], last[2], err
   258  }