Skip to content

Commit

Permalink
Lightweight library for implementing APIServer extensions
Browse files Browse the repository at this point in the history
The other API extension libraries were too opinionated,
or not flexible enough to provide exactly what we need
-- register a single verb in the API, not have storage, give full control over
the http lifecycle.

This provides just enough functionality for what we need, while also providing
extension points in case we need to expand the API surface area in the future -
such as a more explict OpenAPI spec.

This is the foundational work that is required for converting
GameServerAllocations into an API Extension - next step will
be to use this library to implement that functionality.
  • Loading branch information
markmandel committed Apr 3, 2019
1 parent 8986071 commit 46578ba
Show file tree
Hide file tree
Showing 105 changed files with 15,471 additions and 849 deletions.
5 changes: 5 additions & 0 deletions cmd/controller/main.go
Expand Up @@ -34,6 +34,7 @@ import (
"agones.dev/agones/pkg/gameservers"
"agones.dev/agones/pkg/gameserversets"
"agones.dev/agones/pkg/metrics"
"agones.dev/agones/pkg/util/apiserver"
"agones.dev/agones/pkg/util/https"
"agones.dev/agones/pkg/util/runtime"
"agones.dev/agones/pkg/util/signals"
Expand Down Expand Up @@ -141,6 +142,10 @@ func main() {
// https server and the items that share the Mux for routing
httpsServer := https.NewServer(ctlConf.CertFile, ctlConf.KeyFile)
wh := webhooks.NewWebHook(httpsServer.Mux)
// will register openapi endpoint, which is currently not used
// but gets the code ready for usage in a later PR.
_ = apiserver.NewAPIServer(httpsServer.Mux)

agonesInformerFactory := externalversions.NewSharedInformerFactory(agonesClient, defaultResync)
kubeInformerFactory := informers.NewSharedInformerFactory(kubeClient, defaultResync)

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Expand Up @@ -10,6 +10,7 @@ require (
github.com/aws/aws-sdk-go v1.16.20 // indirect
github.com/evanphx/json-patch v4.1.0+incompatible
github.com/fsnotify/fsnotify v1.4.7
github.com/go-openapi/spec v0.19.0
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 // indirect
github.com/golang/groupcache v0.0.0-20171101203131-84a468cf14b4 // indirect
github.com/golang/protobuf v1.2.0
Expand All @@ -27,6 +28,7 @@ require (
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 // indirect
Expand Down
17 changes: 17 additions & 0 deletions go.sum
Expand Up @@ -9,6 +9,10 @@ fortio.org/fortio v1.3.1/go.mod h1:Go0fRqoPJ1xy5JOWcS23jyF58byVZxFyEePYsGmCR0k=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.15.31/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/aws/aws-sdk-go v1.16.20 h1:fNQRk/PXr8B4rU3olSLeJPu2w6y79U+bTKTF6JRI5C8=
Expand All @@ -28,6 +32,14 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4=
github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM=
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
Expand Down Expand Up @@ -78,6 +90,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGi
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a h1:+J2gw7Bw77w/fbK7wnNJJDKmw1IbWft2Ul5BzrG1Qm8=
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
Expand All @@ -88,6 +102,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
Expand Down Expand Up @@ -144,6 +160,7 @@ golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
219 changes: 219 additions & 0 deletions pkg/util/apiserver/apiserver.go
@@ -0,0 +1,219 @@
// Copyright 2019 Google Inc. All Rights Reserved.
//
// 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 apiserver manages kubernetes api extension apis
package apiserver

import (
"encoding/json"
"fmt"
"net/http"
"strings"

"agones.dev/agones/pkg/util/https"
"agones.dev/agones/pkg/util/runtime"
"github.com/go-openapi/spec"
"github.com/munnerz/goautoneg"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
)

var (
// Reference:
// https://github.com/GoogleCloudPlatform/agones/blob/master/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go
// These are public as they may be needed by CRDHandler implementations (usually for returning Status values)

// Scheme scheme for unversioned types - such as APIResourceList, and Status
Scheme = k8sruntime.NewScheme()
// Codecs for unversioned types - such as APIResourceList, and Status
Codecs = serializer.NewCodecFactory(Scheme)

unversionedVersion = schema.GroupVersion{Version: "v1"}
unversionedTypes = []k8sruntime.Object{
&metav1.Status{},
&metav1.APIResourceList{},
}
)

const (
// ContentTypeHeader = "Content-Type"
ContentTypeHeader = "Content-Type"
// AcceptHeader = "Accept"
AcceptHeader = "Accept"
)

func init() {
Scheme.AddUnversionedTypes(unversionedVersion, unversionedTypes...)
}

// CRDHandler is a http handler, that gets passed the Namespace it's working
// on, and returns an error if a server error occurs
type CRDHandler func(http.ResponseWriter, *http.Request, string) error

// APIServer is a lightweight library for registering, and providing handlers
// for Kubernetes APIServer extensions.
type APIServer struct {
logger *logrus.Entry
mux *http.ServeMux
resourceList map[string]*metav1.APIResourceList
swagger *spec.Swagger
delegates map[string]CRDHandler
}

// NewAPIServer returns a new API Server from the given Mux.
// creates a empty Swagger definition and sets up the endpoint.
func NewAPIServer(mux *http.ServeMux) *APIServer {
s := &APIServer{
mux: mux,
resourceList: map[string]*metav1.APIResourceList{},
swagger: &spec.Swagger{SwaggerProps: spec.SwaggerProps{}},
delegates: map[string]CRDHandler{},
}
s.logger = runtime.NewLoggerWithType(s)

// we don't *have* to have a swagger api, so just do an empty one for now, and we can expand as needed.
// kube-openapi could be a potential library to look at for future if we want to be more specific.
// This at least stops the K8s api pinging us for every iteration of a api descriptor that may exist
s.swagger.SwaggerProps.Info = &spec.Info{InfoProps: spec.InfoProps{Title: "allocation.agones.dev"}}

mux.HandleFunc("/openapi/v2", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(ContentTypeHeader, k8sruntime.ContentTypeJSON)
err := json.NewEncoder(w).Encode(s.swagger)
if err != nil {
s.logger.WithError(errors.WithStack(err)).Error("error return openapi")
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})

return s
}

// AddAPIResource stores the APIResource under the given groupVersion string, and returns it
// in the appropriate place for the K8s discovery service
// e.g. http://localhost:8001/apis/scheduling.k8s.io/v1beta1
// as well as registering a CRDHandler that all http requests for the given APIResource are routed to
func (as *APIServer) AddAPIResource(groupVersion string, resource metav1.APIResource, handler CRDHandler) {
list, ok := as.resourceList[groupVersion]
if !ok {
// discovery handler
list = &metav1.APIResourceList{GroupVersion: groupVersion, APIResources: []metav1.APIResource{}}
as.resourceList[groupVersion] = list
pattern := fmt.Sprintf("/apis/%s", groupVersion)
as.addSerializedHandler(pattern, list)
as.logger.WithField("groupversion", groupVersion).WithField("pattern", pattern).Info("Adding Discovery Handler")

// e.g. /apis/stable.agones.dev/v1alpha1/namespaces/default/gameservers
// CRD handler
pattern = fmt.Sprintf("/apis/%s/namespaces/", groupVersion)
as.mux.HandleFunc(pattern, https.ErrorHTTPHandler(as.logger, as.resourceHandler(groupVersion)))
as.logger.WithField("groupversion", groupVersion).WithField("pattern", pattern).Info("Adding Resource Handler")
}

// discovery resource
list.APIResources = append(as.resourceList[groupVersion].APIResources, resource)

// add specific crd resource handler
key := fmt.Sprintf("%s/%s", groupVersion, resource.Name)
as.delegates[key] = handler

as.logger.WithField("groupversion", groupVersion).WithField("apiresource", resource).Info("Adding APIResource")
}

// resourceHandler handles namespaced resource calls, and sends them to the appropriate CRDHandler delegate
func (as *APIServer) resourceHandler(gv string) https.ErrorHandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
namespace, resource, err := splitNameSpaceResource(r.URL.Path)
if err != nil {
https.FourZeroFour(as.logger.WithError(err), w, r)
return nil
}

delegate, ok := as.delegates[fmt.Sprintf("%s/%s", gv, resource)]
if !ok {
https.FourZeroFour(as.logger, w, r)
return nil
}

if err = delegate(w, r, namespace); err != nil {
return err
}

return nil
}
}

// addSerializedHandler sets up a handler than will send the serialised content
// to the specified path.
func (as *APIServer) addSerializedHandler(pattern string, m k8sruntime.Object) {
as.mux.HandleFunc(pattern, https.ErrorHTTPHandler(as.logger, func(w http.ResponseWriter, r *http.Request) error {
if r.Method == http.MethodGet {
info, err := AcceptedSerializer(r, Codecs)
if err != nil {
return err
}

w.Header().Set(ContentTypeHeader, info.MediaType)
err = Codecs.EncoderForVersion(info.Serializer, unversionedVersion).Encode(m, w)
if err != nil {
return errors.New("error marshalling")
}
} else {
https.FourZeroFour(as.logger, w, r)
}

return nil
}))
}

// AcceptedSerializer takes the request, and returns a serialiser (if it exists)
// for the given codec factory and
// for the Accepted media types. If not found, returns error
func AcceptedSerializer(r *http.Request, codecs serializer.CodecFactory) (k8sruntime.SerializerInfo, error) {
// this is so we know what we can accept
mediaTypes := codecs.SupportedMediaTypes()
alternatives := make([]string, len(mediaTypes))
for i, media := range mediaTypes {
alternatives[i] = media.MediaType
}
header := r.Header.Get(AcceptHeader)
accept := goautoneg.Negotiate(header, alternatives)
if len(accept) == 0 {
accept = k8sruntime.ContentTypeJSON
}
info, ok := k8sruntime.SerializerInfoForMediaType(mediaTypes, accept)
if !ok {
return info, errors.Errorf("Could not find serializer for Accept: %s", header)
}

return info, nil
}

// splitNameSpaceResource returns the namespace and the type of resource
func splitNameSpaceResource(path string) (namespace, resource string, err error) {
list := strings.Split(strings.Trim(path, "/"), "/")
if len(list) < 3 {
return namespace, resource, errors.Errorf("could not find namespace and resource in path: %s", path)
}
last := list[len(list)-3:]

if last[0] != "namespaces" {
return namespace, resource, errors.Errorf("wrong format in path: %s", path)
}

return last[1], last[2], err
}

0 comments on commit 46578ba

Please sign in to comment.