agones.dev/agones@v1.54.0/pkg/gameserverallocations/controller.go (about)

     1  // Copyright 2018 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 gameserverallocations
    16  
    17  import (
    18  	"context"
    19  	"io"
    20  	"mime"
    21  	"net/http"
    22  	"time"
    23  
    24  	gwruntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    25  	"github.com/heptiolabs/healthcheck"
    26  	"github.com/pkg/errors"
    27  	"github.com/sirupsen/logrus"
    28  	"google.golang.org/grpc/status"
    29  	corev1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	k8sruntime "k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/serializer"
    33  	"k8s.io/client-go/informers"
    34  	"k8s.io/client-go/kubernetes"
    35  	"k8s.io/client-go/kubernetes/scheme"
    36  	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    37  	"k8s.io/client-go/tools/record"
    38  
    39  	"agones.dev/agones/pkg/allocation/converters"
    40  	allocationv1 "agones.dev/agones/pkg/apis/allocation/v1"
    41  	"agones.dev/agones/pkg/client/clientset/versioned"
    42  	"agones.dev/agones/pkg/client/informers/externalversions"
    43  	"agones.dev/agones/pkg/gameservers"
    44  	"agones.dev/agones/pkg/processor"
    45  	"agones.dev/agones/pkg/util/apiserver"
    46  	"agones.dev/agones/pkg/util/https"
    47  	"agones.dev/agones/pkg/util/runtime"
    48  )
    49  
    50  func init() {
    51  	registerViews()
    52  }
    53  
    54  // Extensions is a GameServerAllocation controller within the Extensions service
    55  type Extensions struct {
    56  	api             *apiserver.APIServer
    57  	baseLogger      *logrus.Entry
    58  	recorder        record.EventRecorder
    59  	allocator       *Allocator
    60  	processorClient processor.Client
    61  }
    62  
    63  // NewExtensions returns the extensions controller for a GameServerAllocation
    64  func NewExtensions(apiServer *apiserver.APIServer,
    65  	health healthcheck.Handler,
    66  	counter *gameservers.PerNodeCounter,
    67  	kubeClient kubernetes.Interface,
    68  	kubeInformerFactory informers.SharedInformerFactory,
    69  	agonesClient versioned.Interface,
    70  	agonesInformerFactory externalversions.SharedInformerFactory,
    71  	remoteAllocationTimeout time.Duration,
    72  	totalAllocationTimeout time.Duration,
    73  	allocationBatchWaitTime time.Duration,
    74  ) *Extensions {
    75  	c := &Extensions{
    76  		api: apiServer,
    77  	}
    78  
    79  	c.allocator = NewAllocator(
    80  		agonesInformerFactory.Multicluster().V1().GameServerAllocationPolicies(),
    81  		kubeInformerFactory.Core().V1().Secrets(),
    82  		agonesClient.AgonesV1(),
    83  		kubeClient,
    84  		NewAllocationCache(agonesInformerFactory.Agones().V1().GameServers(), counter, health),
    85  		remoteAllocationTimeout,
    86  		totalAllocationTimeout,
    87  		allocationBatchWaitTime)
    88  
    89  	c.baseLogger = runtime.NewLoggerWithType(c)
    90  
    91  	eventBroadcaster := record.NewBroadcaster()
    92  	eventBroadcaster.StartLogging(c.baseLogger.Debugf)
    93  	eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")})
    94  	c.recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "GameServerAllocation-controller"})
    95  
    96  	return c
    97  }
    98  
    99  // NewProcessorExtensions returns the extensions controller for a GameServerAllocation
   100  func NewProcessorExtensions(apiServer *apiserver.APIServer, kubeClient kubernetes.Interface, processorClient processor.Client) *Extensions {
   101  	c := &Extensions{
   102  		api:             apiServer,
   103  		processorClient: processorClient,
   104  	}
   105  
   106  	c.baseLogger = runtime.NewLoggerWithType(c)
   107  
   108  	eventBroadcaster := record.NewBroadcaster()
   109  	eventBroadcaster.StartLogging(c.baseLogger.Debugf)
   110  	eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")})
   111  	c.recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "GameServerAllocation-controller"})
   112  
   113  	return c
   114  }
   115  
   116  // registers the api resource for gameserverallocation
   117  func (c *Extensions) registerAPIResource(ctx context.Context) {
   118  	resource := metav1.APIResource{
   119  		Name:         "gameserverallocations",
   120  		SingularName: "gameserverallocation",
   121  		Namespaced:   true,
   122  		Kind:         "GameServerAllocation",
   123  		Verbs: []string{
   124  			"create",
   125  		},
   126  		ShortNames: []string{"gsa"},
   127  	}
   128  	c.api.AddAPIResource(allocationv1.SchemeGroupVersion.String(), resource, func(w http.ResponseWriter, r *http.Request, n string) error {
   129  		return c.processAllocationRequest(ctx, w, r, n)
   130  	})
   131  }
   132  
   133  // Run runs this extensions controller. Will block until stop is closed.
   134  // Ignores threadiness, as we only needs 1 worker for cache sync
   135  func (c *Extensions) Run(ctx context.Context, _ int) error {
   136  	if !runtime.FeatureEnabled(runtime.FeatureProcessorAllocator) {
   137  		if err := c.allocator.Run(ctx); err != nil {
   138  			return err
   139  		}
   140  	}
   141  
   142  	c.registerAPIResource(ctx)
   143  
   144  	return nil
   145  }
   146  
   147  func (c *Extensions) processAllocationRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, namespace string) (err error) {
   148  	if r.Body != nil {
   149  		defer r.Body.Close() // nolint: errcheck
   150  	}
   151  
   152  	log := https.LogRequest(c.baseLogger, r)
   153  
   154  	if r.Method != http.MethodPost {
   155  		log.Warn("allocation handler only supports POST")
   156  		http.Error(w, "Method not supported", http.StatusMethodNotAllowed)
   157  		return nil
   158  	}
   159  
   160  	gsa, err := c.allocationDeserialization(r, namespace)
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	if runtime.FeatureEnabled(runtime.FeatureProcessorAllocator) {
   166  		var result k8sruntime.Object
   167  		var code int
   168  
   169  		req := converters.ConvertGSAToAllocationRequest(gsa)
   170  		resp, err := c.processorClient.Allocate(ctx, req)
   171  		if err != nil {
   172  			if st, ok := status.FromError(err); ok {
   173  				code = gwruntime.HTTPStatusFromCode(st.Code())
   174  			} else {
   175  				code = http.StatusInternalServerError
   176  			}
   177  
   178  			result = &metav1.Status{
   179  				TypeMeta: metav1.TypeMeta{
   180  					Kind:       "Status",
   181  					APIVersion: "v1",
   182  				},
   183  				Status:  metav1.StatusFailure,
   184  				Message: err.Error(),
   185  				Code:    int32(code),
   186  			}
   187  		} else {
   188  			result = converters.ConvertAllocationResponseToGSA(resp, resp.Source)
   189  			code = http.StatusCreated
   190  		}
   191  
   192  		return c.serialisation(r, w, result, code, scheme.Codecs)
   193  	}
   194  
   195  	result, err := c.allocator.Allocate(ctx, gsa)
   196  	if err != nil {
   197  		return err
   198  	}
   199  	var code int
   200  	switch obj := result.(type) {
   201  	case *metav1.Status:
   202  		code = int(obj.Code)
   203  	case *allocationv1.GameServerAllocation:
   204  		code = http.StatusCreated
   205  	default:
   206  		code = http.StatusOK
   207  	}
   208  
   209  	err = c.serialisation(r, w, result, code, scheme.Codecs)
   210  	return err
   211  }
   212  
   213  // allocationDeserialization processes the request and namespace, and attempts to deserialise its values
   214  // into a GameServerAllocation. Returns an error if it fails for whatever reason.
   215  func (c *Extensions) allocationDeserialization(r *http.Request, namespace string) (*allocationv1.GameServerAllocation, error) {
   216  	gsa := &allocationv1.GameServerAllocation{}
   217  
   218  	gvks, _, err := scheme.Scheme.ObjectKinds(gsa)
   219  	if err != nil {
   220  		return gsa, errors.Wrap(err, "error getting objectkinds for gameserverallocation")
   221  	}
   222  
   223  	gsa.TypeMeta = metav1.TypeMeta{Kind: gvks[0].Kind, APIVersion: gvks[0].Version}
   224  
   225  	mediaTypes := scheme.Codecs.SupportedMediaTypes()
   226  	mt, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
   227  	if err != nil {
   228  		return gsa, errors.Wrap(err, "error parsing mediatype from a request header")
   229  	}
   230  	info, ok := k8sruntime.SerializerInfoForMediaType(mediaTypes, mt)
   231  	if !ok {
   232  		return gsa, errors.New("Could not find deserializer")
   233  	}
   234  
   235  	b, err := io.ReadAll(r.Body)
   236  	if err != nil {
   237  		return gsa, errors.Wrap(err, "could not read body")
   238  	}
   239  
   240  	gvk := allocationv1.SchemeGroupVersion.WithKind("GameServerAllocation")
   241  	_, _, err = info.Serializer.Decode(b, &gvk, gsa)
   242  	if err != nil {
   243  		c.baseLogger.WithField("body", string(b)).Error("error decoding body")
   244  		return gsa, errors.Wrap(err, "error decoding body")
   245  	}
   246  
   247  	gsa.ObjectMeta.Namespace = namespace
   248  	gsa.ObjectMeta.CreationTimestamp = metav1.Now()
   249  	gsa.ApplyDefaults()
   250  
   251  	return gsa, nil
   252  }
   253  
   254  // serialisation takes a runtime.Object, and serialise it to the ResponseWriter in the requested format
   255  func (c *Extensions) serialisation(r *http.Request, w http.ResponseWriter, obj k8sruntime.Object, statusCode int, codecs serializer.CodecFactory) error {
   256  	info, err := apiserver.AcceptedSerializer(r, codecs)
   257  	if err != nil {
   258  		return errors.Wrapf(err, "failed to find serialisation info for %T object", obj)
   259  	}
   260  
   261  	w.Header().Set("Content-Type", info.MediaType)
   262  	// we have to do this here, so that the content type is set before we send a HTTP status header, as the WriteHeader
   263  	// call will send data to the client.
   264  	w.WriteHeader(statusCode)
   265  
   266  	err = info.Serializer.Encode(obj, w)
   267  	return errors.Wrapf(err, "error encoding %T", obj)
   268  }