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 }