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 }