diff --git a/README.md b/README.md index 853819fa0a..a627775309 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Documentation and usage guides on how to develop and host dedicated game servers ### Guides - [Integrating the Game Server SDK](sdks) - [GameServer Health Checking](./docs/health_checking.md) + - [Latency Testing with Multiple Clusters](./docs/ping_service.md) - [Accessing Agones via the Kubernetes API](./docs/access_api.md) - [Troubleshooting](./docs/troubleshooting.md) diff --git a/build/Makefile b/build/Makefile index 116accf34e..3f89f53f30 100644 --- a/build/Makefile +++ b/build/Makefile @@ -72,6 +72,7 @@ build_tag = agones-build:$(build_version) build_remote_tag = $(REGISTRY)/$(build_tag) controller_tag = $(REGISTRY)/agones-controller:$(VERSION) sidecar_tag = $(REGISTRY)/agones-sdk:$(VERSION) +ping_tag = $(REGISTRY)/agones-ping:$(VERSION) go_version_flags = -ldflags "-X agones.dev/agones/pkg.Version=$(VERSION)" DOCKER_RUN ?= docker run --rm $(common_mounts) -e "KUBECONFIG=/root/.kube/$(kubeconfig_file)" $(DOCKER_RUN_ARGS) $(build_tag) @@ -111,7 +112,7 @@ endif build: build-images build-sdks # build the docker images -build-images: build-controller-image build-agones-sdk-image +build-images: build-controller-image build-agones-sdk-image build-ping-image # package the current agones helm chart build-chart: RELEASE_VERSION ?= $(base_version) @@ -147,7 +148,7 @@ test: $(ensure-build-image) test-go test-install-yaml # Run go tests test-go: docker run --rm $(common_mounts) $(build_tag) go test -race $(agones_package)/pkg/... \ - $(agones_package)/sdks/... + $(agones_package)/sdks/... $(agones_package)/cmd/... # Runs end-to-end tests on the current configured cluster # For minikube user the minikube-test-e2e targets @@ -168,21 +169,26 @@ test-install-yaml: diff /tmp/agones-install/install.yaml.sorted /tmp/agones-install/install.current.yaml.sorted # Push all the images up to $(REGISTRY) -push: push-controller-image push-agones-sdk-image +push: push-controller-image push-agones-sdk-image push-ping-image # Installs the current development version of Agones into the Kubernetes cluster install: ALWAYS_PULL_SIDECAR := true install: IMAGE_PULL_POLICY := "Always" +install: PING_SERVICE_TYPE := "LoadBalancer" install: $(ensure-build-image) install-custom-pull-secret $(DOCKER_RUN) \ helm upgrade --install --wait --namespace=agones-system\ - --set agones.image.tag=$(VERSION),agones.image.registry=$(REGISTRY),agones.image.controller.pullPolicy=$(IMAGE_PULL_POLICY),agones.image.sdk.alwaysPull=$(ALWAYS_PULL_SIDECAR),agones.image.controller.pullSecret=$(IMAGE_PULL_SECRET) \ + --set agones.image.tag=$(VERSION),agones.image.registry=$(REGISTRY) \ + --set agones.image.controller.pullPolicy=$(IMAGE_PULL_POLICY),agones.image.sdk.alwaysPull=$(ALWAYS_PULL_SIDECAR) \ + --set agones.image.controller.pullSecret=$(IMAGE_PULL_SECRET) \ + --set agones.ping.http.serviceType=$(PING_SERVICE_TYPE),agones.ping.udp.serviceType=$(PING_SERVICE_TYPE) \ agones $(mount_path)/install/helm/agones/ uninstall: $(ensure-build-image) $(DOCKER_RUN) \ helm delete --purge agones + # Build a static binary for the gameserver controller build-controller-binary: $(ensure-build-image) docker run --rm -e "CGO_ENABLED=0" $(common_mounts) $(build_tag) go build \ @@ -219,6 +225,20 @@ build-agones-sdk-binary: $(ensure-build-image) build-agones-sdk-image: $(ensure-build-image) build-agones-sdk-binary docker build $(agones_path)/cmd/sdk-server/ --tag=$(sidecar_tag) $(DOCKER_BUILD_ARGS) +# Build a static binary for the ping service +build-ping-binary: $(ensure-build-image) + docker run --rm -e "CGO_ENABLED=0" $(common_mounts) $(build_tag) go build \ + -tags $(GO_BUILD_TAGS) -o $(mount_path)/cmd/ping/bin/ping \ + -a $(go_version_flags) -installsuffix cgo $(agones_package)/cmd/ping + +# Pushes up the ping image +push-ping-image: $(ensure-build-image) + docker push $(ping_tag) + +# Build the image for the ping service +build-ping-image: $(ensure-build-image) build-ping-binary + docker build $(agones_path)/cmd/ping/ --tag=$(ping_tag) $(DOCKER_BUILD_ARGS) + # Build the cpp sdk linux archive build-sdk-cpp: $(ensure-build-image) docker run --rm $(common_mounts) -w $(mount_path)/sdks/cpp $(build_tag) make build install archive VERSION=$(VERSION) @@ -471,11 +491,13 @@ minikube-shell: $(ensure-build-image) minikube-agones-profile minikube-push: minikube-agones-profile $(MAKE) minikube-transfer-image TAG=$(sidecar_tag) $(MAKE) minikube-transfer-image TAG=$(controller_tag) + $(MAKE) minikube-transfer-image TAG=$(ping_tag) # Installs the current development version of Agones into the Kubernetes cluster. # Use this instead of `make install`, as it disables PullAlways on the install.yaml minikube-install: minikube-agones-profile - $(MAKE) install DOCKER_RUN_ARGS="--network=host -v $(minikube_cert_mount)" ALWAYS_PULL_SIDECAR=false IMAGE_PULL_POLICY=IfNotPresent + $(MAKE) install DOCKER_RUN_ARGS="--network=host -v $(minikube_cert_mount)" ALWAYS_PULL_SIDECAR=false \ + IMAGE_PULL_POLICY=IfNotPresent PING_SERVICE_TYPE=NodePort minikube-uninstall: $(ensure-build-image) minikube-agones-profile $(MAKE) uninstall DOCKER_RUN_ARGS="--network=host -v $(minikube_cert_mount)" diff --git a/build/README.md b/build/README.md index cb3634f233..b2b2e70c8d 100644 --- a/build/README.md +++ b/build/README.md @@ -385,12 +385,15 @@ Run a bash shell with the developer tools (go tooling, kubectl, etc) and source #### `make godoc` Run a container with godoc (search index enabled) -#### `make build-agones-controller-image` +#### `make build-controller-image` Compile the gameserver controller and then build the docker image #### `make build-agones-sdk-image` Compile the gameserver sidecar and then build the docker image +#### `make build-ping-image` +Compile the ping binary and then build the docker image + #### `make gen-install` Generate the `/install/yaml/install.yaml` from the Helm template diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 755d5e39f6..ca1b9b0cdf 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -59,6 +59,6 @@ steps: dir: "build" args: ["push-build-image"] # push the build image (which won't do anything if it's already there) timeout: "1h" -images: ['gcr.io/$PROJECT_ID/agones-controller', 'gcr.io/$PROJECT_ID/agones-sdk'] +images: ['gcr.io/$PROJECT_ID/agones-controller', 'gcr.io/$PROJECT_ID/agones-sdk', 'gcr.io/$PROJECT_ID/agones-ping'] options: machineType: 'N1_HIGHCPU_8' diff --git a/cmd/controller/pprof.go b/cmd/controller/pprof.go index 3f9a40f492..d1f0d057f7 100644 --- a/cmd/controller/pprof.go +++ b/cmd/controller/pprof.go @@ -17,9 +17,10 @@ package main import ( - "github.com/sirupsen/logrus" "net/http" _ "net/http/pprof" + + "github.com/sirupsen/logrus" ) func init() { @@ -27,4 +28,4 @@ func init() { logrus.WithError(http.ListenAndServe(":6060", nil)).Info("Closed pprof server") }() logrus.Info("*** PPROF PROFILER STARTED on :6060 ***") -} \ No newline at end of file +} diff --git a/cmd/ping/Dockerfile b/cmd/ping/Dockerfile new file mode 100644 index 0000000000..25c3f37c6d --- /dev/null +++ b/cmd/ping/Dockerfile @@ -0,0 +1,26 @@ +# Copyright 2018 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. + +FROM alpine:3.8 + +RUN apk --update add ca-certificates && \ + adduser -D agones + +COPY ./bin/ping /home/agones/ping + +RUN chown -R agones /home/agones && \ + chmod o+x /home/agones/ping + +USER agones +ENTRYPOINT ["/home/agones/ping"] \ No newline at end of file diff --git a/cmd/ping/main.go b/cmd/ping/main.go new file mode 100644 index 0000000000..b00ed90aff --- /dev/null +++ b/cmd/ping/main.go @@ -0,0 +1,137 @@ +// Copyright 2018 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. + +// binary for the pinger service for RTT measurement. +package main + +import ( + "context" + "net/http" + "strings" + "time" + + "agones.dev/agones/pkg" + "agones.dev/agones/pkg/util/runtime" + "agones.dev/agones/pkg/util/signals" + "github.com/heptiolabs/healthcheck" + "github.com/pkg/errors" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "golang.org/x/time/rate" +) + +const ( + httpResponseFlag = "http-response" + udpRateLimitFlag = "udp-rate-limit" +) + +var ( + logger = runtime.NewLoggerWithSource("main") +) + +func main() { + ctlConf := parseEnvFlags() + if err := ctlConf.validate(); err != nil { + logger.WithError(err).Fatal("could not create controller from environment or flags") + } + + logger.WithField("version", pkg.Version). + WithField("ctlConf", ctlConf).Info("starting ping...") + + stop := signals.NewStopChannel() + + udpSrv := serveUDP(ctlConf, stop) + defer udpSrv.close() + + h := healthcheck.NewHandler() + h.AddLivenessCheck("udp-server", udpSrv.Health) + + cancel := serveHTTP(ctlConf, h) + defer cancel() + + <-stop + logger.Info("shutting down...") +} + +func serveUDP(ctlConf config, stop <-chan struct{}) *udpServer { + s := newUDPServer(ctlConf.UDPRateLimit) + s.run(stop) + return s +} + +// serveHTTP starts the HTTP handler, and returns a cancel/shutdown function +func serveHTTP(ctlConf config, h healthcheck.Handler) func() { + // we don't need a health checker, we already have a http endpoint that returns 200 + mux := http.NewServeMux() + srv := &http.Server{ + Addr: ":8080", + Handler: mux, + } + + // add health check as well + mux.HandleFunc("/live", h.LiveEndpoint) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if _, err := w.Write([]byte(ctlConf.HTTPResponse)); err != nil { + w.WriteHeader(http.StatusInternalServerError) + logger.WithError(err).Error("error responding to http request") + } + }) + + go func() { + logger.Info("starting HTTP Server...") + logger.WithError(srv.ListenAndServe()).Fatal("could not start HTTP server") + }() + + return func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + logger.WithError(err).Fatal("could not shut down HTTP server") + } + } +} + +// config retains the configuration information +type config struct { + HTTPResponse string + UDPRateLimit rate.Limit +} + +// validate returns an error if there is a validation problem +func (c *config) validate() error { + if c.UDPRateLimit < 0 { + return errors.New("UDP Rate limit must be greater that or equal to zero") + } + + return nil +} + +func parseEnvFlags() config { + viper.SetDefault(httpResponseFlag, "ok") + viper.SetDefault(udpRateLimitFlag, 20) + + pflag.String(httpResponseFlag, viper.GetString(httpResponseFlag), "Flag to set text value when a 200 response is returned. Can be useful to identify clusters. Defaults to 'ok' Can also use HTTP_RESPONSE env variable") + pflag.Float64(udpRateLimitFlag, viper.GetFloat64(httpResponseFlag), "Flag to set how many UDP requests can be handled by a single source IP per second. Defaults to 20. Can also use UDP_RATE_LIMIT env variable") + + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + runtime.Must(viper.BindEnv(httpResponseFlag)) + runtime.Must(viper.BindEnv(udpRateLimitFlag)) + + return config{ + HTTPResponse: viper.GetString(httpResponseFlag), + UDPRateLimit: rate.Limit(viper.GetFloat64(udpRateLimitFlag)), + } +} diff --git a/cmd/ping/udp.go b/cmd/ping/udp.go new file mode 100644 index 0000000000..9a38c8cc29 --- /dev/null +++ b/cmd/ping/udp.go @@ -0,0 +1,176 @@ +// Copyright 2018 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 main + +import ( + "bytes" + "math" + "net" + "sync" + "time" + + "agones.dev/agones/pkg/util/runtime" + + "github.com/pkg/errors" + + "github.com/sirupsen/logrus" + "golang.org/x/time/rate" + "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/apimachinery/pkg/util/wait" +) + +// udpServer is a rate limited udp server that echos +// udp packets back to senders +type udpServer struct { + logger *logrus.Entry + conn net.PacketConn + rateLimit rate.Limit + rateBurst int + clock clock.Clock + limitsMutex sync.Mutex + limits map[string]*visitor + healthMutex sync.RWMutex + health bool +} + +// visitor tracks when a visitor last sent +// a packet, and it's rate limit +type visitor struct { + stamp time.Time + limit *rate.Limiter +} + +// newUDPServer returns a new udpServer implementation +// withe the rate limit +func newUDPServer(rateLimit rate.Limit) *udpServer { + udpSrv := &udpServer{ + rateLimit: rateLimit, + rateBurst: int(math.Floor(float64(rateLimit))), + clock: clock.RealClock{}, + limitsMutex: sync.Mutex{}, + limits: map[string]*visitor{}, + } + udpSrv.logger = runtime.NewLoggerWithType(udpSrv) + return udpSrv +} + +// run runs the udp server. Non blocking operation +func (u *udpServer) run(stop <-chan struct{}) { + u.healthy() + + logger.Info("starting UDP server") + var err error + u.conn, err = net.ListenPacket("udp", ":8080") + if err != nil { + logger.WithError(err).Fatal("could not start udp server") + } + + go func() { + defer u.unhealthy() + wait.Until(u.cleanUp, time.Minute, stop) + }() + + u.readWriteLoop(stop) +} + +// cleans up visitors, if they are more than a +// minute without being touched +func (u *udpServer) cleanUp() { + u.limitsMutex.Lock() + defer u.limitsMutex.Unlock() + for k, v := range u.limits { + if u.clock.Now().Sub(v.stamp) > time.Minute { + delete(u.limits, k) + } + } +} + +// readWriteLoop reads the UDP packet in, and then echos the data back +// in a rate limited way +func (u *udpServer) readWriteLoop(stop <-chan struct{}) { + go func() { + defer u.unhealthy() + for { + select { + case <-stop: + return + default: + b := make([]byte, 1024) + _, sender, err := u.conn.ReadFrom(b) + if err != nil { + u.logger.WithError(err).Error("error reading udp packet") + continue + } + go u.rateLimitedEchoResponse(b, sender) + } + } + }() +} + +// rateLimitedEchoResponse echos the udp request, but is ignored if +// it is past its rate limit +func (u *udpServer) rateLimitedEchoResponse(b []byte, sender net.Addr) { + var vis *visitor + u.limitsMutex.Lock() + key := sender.String() + vis, ok := u.limits[key] + if !ok { + vis = &visitor{limit: rate.NewLimiter(u.rateLimit, u.rateBurst)} + u.limits[key] = vis + } + vis.stamp = u.clock.Now() + u.limitsMutex.Unlock() + + if vis.limit.Allow() { + b = bytes.TrimRight(b, "\x00") + if _, err := u.conn.WriteTo(b, sender); err != nil { + u.logger.WithError(err).Error("error sending returning udp packet") + } + } else { + logger.WithField("addr", sender.String()).Warn("rate limited. No response sent.") + } +} + +// close closes and shutdown the udp server +func (u *udpServer) close() { + if err := u.conn.Close(); err != nil { + logger.WithError(err).Error("error closing udp connection") + } +} + +// healthy marks this udpServer as healthy +func (u *udpServer) healthy() { + u.healthMutex.Lock() + defer u.healthMutex.Unlock() + u.health = true +} + +// unhealthy marks this udpServer as unhealthy +func (u *udpServer) unhealthy() { + u.healthMutex.Lock() + defer u.healthMutex.Unlock() + u.health = false +} + +// Health returns the health of the UDP Server. +// true is healthy, false is not +func (u *udpServer) Health() error { + u.healthMutex.RLock() + defer u.healthMutex.RUnlock() + if !u.health { + return errors.New("UDP Server is unhealthy") + } + return nil +} diff --git a/cmd/ping/udp_test.go b/cmd/ping/udp_test.go new file mode 100644 index 0000000000..fb63c65832 --- /dev/null +++ b/cmd/ping/udp_test.go @@ -0,0 +1,129 @@ +// Copyright 2018 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 main + +import ( + "net" + "testing" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/clock" +) + +type mockAddr struct { + addr string +} + +func (m *mockAddr) Network() string { + return m.addr +} + +func (m *mockAddr) String() string { + return m.addr +} + +func TestUDPServerVisit(t *testing.T) { + t.Parallel() + + fc := clock.NewFakeClock(time.Now()) + u, err := defaultFixture(fc) + assert.Nil(t, err) + defer u.close() + + // gate + assert.Empty(t, u.limits) + + m := &mockAddr{addr: "[::1]:52998"} + + u.rateLimitedEchoResponse([]byte{}, m) + + // gate + assert.NotEmpty(t, u.limits) + assert.Len(t, u.limits, 1) + assert.Equal(t, fc.Now(), u.limits[m.Network()].stamp) + + fc.Step(30 * time.Second) + + u.rateLimitedEchoResponse([]byte{}, m) + assert.Len(t, u.limits, 1) + assert.Equal(t, fc.Now(), u.limits[m.Network()].stamp) + + m = &mockAddr{addr: "[::1]:52999"} + u.rateLimitedEchoResponse([]byte{}, m) + assert.Len(t, u.limits, 2) + assert.Equal(t, fc.Now(), u.limits[m.Network()].stamp) +} + +func TestUDPServerCleanup(t *testing.T) { + t.Parallel() + + fc := clock.NewFakeClock(time.Now()) + u, err := defaultFixture(fc) + assert.Nil(t, err) + defer u.close() + + // gate + assert.Empty(t, u.limits) + + m := &mockAddr{addr: "[::1]:52998"} + u.rateLimitedEchoResponse([]byte{}, m) + + // gate + assert.NotEmpty(t, u.limits) + + assert.Equal(t, u.clock.Now(), u.limits[m.String()].stamp) + fc.Step(10 * time.Second) + u.cleanUp() + assert.NotEmpty(t, u.limits) + + fc.Step(time.Minute) + u.cleanUp() + assert.Empty(t, u.limits) +} + +func TestUDPServerHealth(t *testing.T) { + t.Parallel() + + fc := clock.NewFakeClock(time.Now()) + u, err := defaultFixture(fc) + assert.Nil(t, err) + defer u.close() + + assert.Error(t, u.Health()) + + stop := make(chan struct{}) + u.run(stop) + + assert.Nil(t, u.Health()) + + close(stop) + + err = wait.PollImmediate(time.Second, 5*time.Second, func() (done bool, err error) { + return u.Health() != nil, nil + }) + + assert.Nil(t, err) +} + +func defaultFixture(clock clock.Clock) (*udpServer, error) { + u := newUDPServer(5) + u.clock = clock + var err error + u.conn, err = net.ListenPacket("udp", ":0") + return u, err +} diff --git a/docs/ping_service.md b/docs/ping_service.md new file mode 100644 index 0000000000..87a350b2fe --- /dev/null +++ b/docs/ping_service.md @@ -0,0 +1,47 @@ +# Latency Testing with Multiple Clusters + +⚠️⚠️⚠️ **This is currently a development feature and has not been released** ⚠️⚠️⚠️ + +When running multiple Agones clusters around the world, you may need to have clients determine which cluster +to connect to based on latency. + +To make that easier, Agones installs with a simple ping service with both HTTP and UDP services that can be called +for the purpose of timing how long the rountrip takes for information to be returned from either of these services. + +## Installing + +By default, Agones installs [Kubernetes Services](https://kubernetes.io/docs/concepts/services-networking/service/) for +both HTTP and the UDP ping endpoints. These can be disabled entirely, +or disabled individually. See the [Helm install guide](../install/helm/README.md) for the parameters to pass through, +as well as configuration options. + +The ping services as all installed under the `agones-system` namespace. + +## HTTP Service + +This exposes an endpoint that returns a simple text HTTP response on request to the root "/" path. By default this is `ok`, but +it can be configured via the `agones.ping.http.response` parameter. + +This could be useful for providing clusters +with unique lookup names, such that clients are able to identify clusters from their responses. + +To lookup the details of this service, run `kubectl describe service agones-ping-http-service --namespace=agones-system` + +## UDP Service + +The UDP ping service is a rate limited UDP echo service that returns the udp packet that it recieves to its designated +sender. + +Since UDP sender details can be spoofed, this service is rate limited to 20 requests per second, +per sender address, per running instance (default is 2). + +This rate limit can be raised or lowered via the Helm install parameter `agones.ping.udp.rateLimit`. + +UDP packets are also limited to 1024 bytes in size. + +To lookup the details of this service, run `kubectl describe service agones-ping-udp-service --namespace=agones-system` + +## Client side tooling + +We deliberately didn't provide any game client libraries, as all major languages and engines have capabilities +to send HTTP requests as well as UDP packets. \ No newline at end of file diff --git a/install/helm/README.md b/install/helm/README.md index 802a5e2c96..e33606fe38 100644 --- a/install/helm/README.md +++ b/install/helm/README.md @@ -95,6 +95,8 @@ The following tables lists the configurable parameters of the Agones chart and t | `agones.image.sdk.cpuRequest` | The [cpu request][constraints] for sdk server container | `30m` | | `agones.image.sdk.cpuLimit` | The [cpu limit][constraints] for the sdk server container | `0` (none) | | `agones.image.sdk.alwaysPull` | Tells if the sdk image should always be pulled | `false` | +| `agones.image.ping.name` | ( ⚠️ development feature ⚠️ ) Image name for the ping service | `agones-ping` | +| `agones.image.ping.pullPolicy` | ( ⚠️ development feature ⚠️ ) Image pull policy for the ping service | `IfNotPresent` | | `agones.controller.healthCheck.http.port` | Port to use for liveness probe service | `8080` | | `agones.controller.healthCheck.initialDelaySeconds` | Initial delay before performing the first probe (in seconds) | `3` | | `agones.controller.healthCheck.periodSeconds` | Seconds between every liveness probe (in seconds) | `3` | @@ -102,11 +104,27 @@ The following tables lists the configurable parameters of the Agones chart and t | `agones.controller.healthCheck.timeoutSeconds` | Number of seconds after which the probe times out (in seconds) | `1` | | `agones.controller.resources` | Controller resource requests/limit | `{}` | | `agones.controller.generateTLS` | Set to true to generate TLS certificates or false to provide your own certificates in `certs/*` | `true` | +| `agones.ping.install` | ( ⚠️ development feature ⚠️ ) Whether to install the [ping service][ping] | `true` | +| `agones.ping.replicas` | ( ⚠️ development feature ⚠️ ) The number of replicas to run in the deployment | `2` | +| `agones.ping.http.expose` | ( ⚠️ development feature ⚠️ ) Expose the http ping service via a Service | `true` | +| `agones.ping.http.response` | ( ⚠️ development feature ⚠️ ) The string response returned from the http service | `ok` | +| `agones.ping.http.port` | ( ⚠️ development feature ⚠️ ) The port to expose on the service | `80` | +| `agones.ping.http.serviceType` | ( ⚠️ development feature ⚠️ ) The [Service Type][service] of the HTTP Service | `LoadBalancer` | +| `agones.ping.udp.expose` | ( ⚠️ development feature ⚠️ ) Expose the udp ping service via a Service | `true` | +| `agones.ping.udp.rateLimit` | ( ⚠️ development feature ⚠️ ) Number of UDP packets the ping service handles per instance, per second, per sender | `20` | +| `agones.ping.udp.port` | ( ⚠️ development feature ⚠️ ) The port to expose on the service | `80` | +| `agones.ping.udp.serviceType` | ( ⚠️ development feature ⚠️ ) The [Service Type][service] of the UDP Service | `LoadBalancer` | +| `agones.ping.healthCheck.initialDelaySeconds` | ( ⚠️ development feature ⚠️ ) Initial delay before performing the first probe (in seconds) | `3` | +| `agones.ping.healthCheck.periodSeconds` | ( ⚠️ development feature ⚠️ ) Seconds between every liveness probe (in seconds) | `3` | +| `agones.ping.healthCheck.failureThreshold` | ( ⚠️ development feature ⚠️ ) Number of times before giving up (in seconds) | `3` | +| `agones.ping.healthCheck.timeoutSeconds` | ( ⚠️ development feature ⚠️ ) Number of seconds after which the probe times out (in seconds) | `1` | | `gameservers.namespaces` | a list of namespaces you are planning to use to deploy game servers | `["default"]` | | `gameservers.minPort` | Minimum port to use for dynamic port allocation | `7000` | | `gameservers.maxPort` | Maximum port to use for dynamic port allocation | `8000` | [constraints]: https://kubernetes.io/docs/tasks/administer-cluster/manage-resources/cpu-constraint-namespace/ +[ping]: ../../docs/ping_service.md +[service]: https://kubernetes.io/docs/concepts/services-networking/service/ Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, diff --git a/install/helm/agones/templates/ping.yaml b/install/helm/agones/templates/ping.yaml new file mode 100644 index 0000000000..311064451e --- /dev/null +++ b/install/helm/agones/templates/ping.yaml @@ -0,0 +1,108 @@ +# Copyright 2018 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. + +{{- if .Values.agones.ping.install }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: agones-ping + namespace: {{ .Release.Namespace }} + labels: + component: ping + app: {{ template "agones.name" . }} + chart: {{ template "agones.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + selector: + matchLabels: + stable.agones.dev/role: ping + app: {{ template "agones.name" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + replicas: {{ .Values.agones.ping.replicas }} + template: + metadata: + labels: + stable.agones.dev/role: ping + app: {{ template "agones.name" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + spec: + containers: + - name: agones-ping + image: "{{ .Values.agones.image.registry }}/{{ .Values.agones.image.ping.name}}:{{ .Values.agones.image.tag }}" + imagePullPolicy: {{ .Values.agones.image.ping.pullPolicy }} + livenessProbe: + httpGet: + port: 8080 + path: /live + initialDelaySeconds: {{ .Values.agones.ping.healthCheck.initialDelaySeconds }} + periodSeconds: {{ .Values.agones.ping.healthCheck.periodSeconds }} + failureThreshold: {{ .Values.agones.ping.healthCheck.failureThreshold }} + timeoutSeconds: {{ .Values.agones.ping.healthCheck.timeoutSeconds }} + env: + - name: HTTP_RESPONSE + value: {{ .Values.agones.ping.http.response | quote }} + - name: UDP_RATE_LIMIT + value: {{ .Values.agones.ping.udp.rateLimit | quote }} + {{- if .Values.agones.ping.http.expose }} +--- +apiVersion: v1 +kind: Service +metadata: + name: agones-ping-http-service + namespace: {{ .Release.Namespace }} + labels: + component: ping + app: {{ template "agones.name" . }} + chart: {{ template "agones.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + selector: + stable.agones.dev/role: ping + ports: + - port: {{ .Values.agones.ping.http.port }} + name: http + targetPort: 8080 + protocol: TCP + type: {{ .Values.agones.ping.http.serviceType }} + {{- end }} + + {{- if .Values.agones.ping.udp.expose }} +--- +apiVersion: v1 +kind: Service +metadata: + name: agones-ping-udp-service + namespace: {{ .Release.Namespace }} + labels: + component: ping + app: {{ template "agones.name" . }} + chart: {{ template "agones.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + selector: + stable.agones.dev/role: ping + ports: + - port: {{ .Values.agones.ping.udp.port }} + name: udp + targetPort: 8080 + protocol: UDP + type: {{ .Values.agones.ping.udp.serviceType }} + {{- end }} + +{{- end }} \ No newline at end of file diff --git a/install/helm/agones/values.yaml b/install/helm/agones/values.yaml index 5373cd2cf1..0a97a563b2 100644 --- a/install/helm/agones/values.yaml +++ b/install/helm/agones/values.yaml @@ -31,6 +31,24 @@ agones: periodSeconds: 3 failureThreshold: 3 timeoutSeconds: 1 + ping: + install: true + replicas: 2 + http: + expose: true + response: ok + port: 80 + serviceType: LoadBalancer + udp: + expose: true + rateLimit: 20 + port: 50000 + serviceType: LoadBalancer + healthCheck: + initialDelaySeconds: 3 + periodSeconds: 3 + failureThreshold: 3 + timeoutSeconds: 1 image: registry: gcr.io/agones-images tag: 0.7.0-rc @@ -42,6 +60,9 @@ agones: cpuRequest: 30m cpuLimit: 0 alwaysPull: false + ping: + name: agones-ping + pullPolicy: IfNotPresent gameservers: namespaces: diff --git a/install/yaml/install.yaml b/install/yaml/install.yaml index 9cd18f6d84..761aaf8a95 100644 --- a/install/yaml/install.yaml +++ b/install/yaml/install.yaml @@ -849,6 +849,107 @@ spec: secretName: agones-manual-cert --- +# Source: agones/templates/ping.yaml +# Copyright 2018 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. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: agones-ping + namespace: agones-system + labels: + component: ping + app: agones + chart: agones-0.7.0-rc + release: agones-manual + heritage: Tiller +spec: + selector: + matchLabels: + stable.agones.dev/role: ping + app: agones + release: agones-manual + heritage: Tiller + replicas: 2 + template: + metadata: + labels: + stable.agones.dev/role: ping + app: agones + release: agones-manual + heritage: Tiller + spec: + containers: + - name: agones-ping + image: "gcr.io/agones-images/agones-ping:0.7.0-rc" + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + port: 8080 + path: /live + initialDelaySeconds: 3 + periodSeconds: 3 + failureThreshold: 3 + timeoutSeconds: 1 + env: + - name: HTTP_RESPONSE + value: "ok" + - name: UDP_RATE_LIMIT + value: "20" +--- +apiVersion: v1 +kind: Service +metadata: + name: agones-ping-http-service + namespace: agones-system + labels: + component: ping + app: agones + chart: agones-0.7.0-rc + release: agones-manual + heritage: Tiller +spec: + selector: + stable.agones.dev/role: ping + ports: + - port: 80 + name: http + targetPort: 8080 + protocol: TCP + type: LoadBalancer +--- +apiVersion: v1 +kind: Service +metadata: + name: agones-ping-udp-service + namespace: agones-system + labels: + component: ping + app: agones + chart: agones-0.7.0-rc + release: agones-manual + heritage: Tiller +spec: + selector: + stable.agones.dev/role: ping + ports: + - port: 50000 + name: udp + targetPort: 8080 + protocol: UDP + type: LoadBalancer +--- # Source: agones/templates/hooks/post_delete_hook.yaml diff --git a/test/e2e/ping_test.go b/test/e2e/ping_test.go new file mode 100644 index 0000000000..e48770363e --- /dev/null +++ b/test/e2e/ping_test.go @@ -0,0 +1,118 @@ +// Copyright 2018 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 e2e + +import ( + "fmt" + "io/ioutil" + "net/http" + "testing" + + e2eframework "agones.dev/agones/test/e2e/framework" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + typedv1 "k8s.io/client-go/kubernetes/typed/core/v1" +) + +func TestPingHTTP(t *testing.T) { + t.Parallel() + + kubeCore := framework.KubeClient.CoreV1() + svc, err := kubeCore.Services("agones-system").Get("agones-ping-http-service", metav1.GetOptions{}) + assert.Nil(t, err) + + ip, err := externalIP(t, kubeCore, svc) + assert.Nil(t, err) + + port := svc.Spec.Ports[0] + // gate + assert.Equal(t, "http", port.Name) + assert.Equal(t, corev1.ProtocolTCP, port.Protocol) + p, err := externalPort(svc, port) + assert.Nil(t, err) + + response, err := http.Get(fmt.Sprintf("http://%s:%d", ip, p)) + assert.Nil(t, err) + defer response.Body.Close() // nolint: errcheck + + assert.Equal(t, http.StatusOK, response.StatusCode) + body, err := ioutil.ReadAll(response.Body) + assert.Nil(t, err) + assert.Equal(t, []byte("ok"), body) +} + +func externalPort(svc *corev1.Service, port corev1.ServicePort) (int32, error) { + switch svc.Spec.Type { + case corev1.ServiceTypeNodePort: + return port.NodePort, nil + + case corev1.ServiceTypeLoadBalancer: + return port.Port, nil + } + + return 0, errors.New("could not find external port") +} + +func TestPingUDP(t *testing.T) { + kubeCore := framework.KubeClient.CoreV1() + svc, err := kubeCore.Services("agones-system").Get("agones-ping-udp-service", metav1.GetOptions{}) + assert.Nil(t, err) + + externalIP, err := externalIP(t, kubeCore, svc) + assert.Nil(t, err) + + port := svc.Spec.Ports[0] + // gate + assert.Equal(t, "udp", port.Name) + assert.Equal(t, corev1.ProtocolUDP, port.Protocol) + p, err := externalPort(svc, port) + assert.Nil(t, err) + + expected := "hello" + reply, err := e2eframework.PingGameServer(expected, fmt.Sprintf("%s:%d", externalIP, p)) + assert.Nil(t, err) + assert.Equal(t, expected, reply) +} + +func externalIP(t *testing.T, kubeCore typedv1.NodesGetter, svc *corev1.Service) (string, error) { + externalIP := "" + + logrus.WithField("svc", svc).Info("load balancer") + + // likely this is minikube, so go get the node ip + if svc.Spec.Type == corev1.ServiceTypeNodePort { + nodes, err := kubeCore.Nodes().List(metav1.ListOptions{}) + assert.Nil(t, err) + assert.Len(t, nodes.Items, 1, "Should only be 1 node on minikube") + + addresses := nodes.Items[0].Status.Addresses + for _, a := range addresses { + if a.Type == corev1.NodeInternalIP { + externalIP = a.Address + } + } + } else { + externalIP = svc.Status.LoadBalancer.Ingress[0].IP + } + + var err error + if externalIP == "" { + err = errors.New("could not find external ip") + } + return externalIP, err +}