Skip to content

Commit

Permalink
Ability to pass GameServer yaml/json to local sdk server
Browse files Browse the repository at this point in the history
To be able to work locally, you need to be able to specify your
local `GameServer` configuration, as it likely will have application specific
configuration in it -- or maybe you want to specify what state it's in.

This commit allow you to specify the local resource as either yaml/json
through a `-f` or `--file` flag.

Closes #296
  • Loading branch information
markmandel committed Aug 26, 2018
1 parent d6d5d10 commit e784314
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 130 deletions.
58 changes: 53 additions & 5 deletions cmd/sdk-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,24 @@ import (
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strings"

"agones.dev/agones/pkg"
"agones.dev/agones/pkg/apis/stable/v1alpha1"
"agones.dev/agones/pkg/client/clientset/versioned"
"agones.dev/agones/pkg/gameservers"
"agones.dev/agones/pkg/sdk"
"agones.dev/agones/pkg/util/runtime"
"agones.dev/agones/pkg/util/signals"
gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"golang.org/x/net/context"
"google.golang.org/grpc"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
Expand All @@ -46,6 +51,7 @@ const (

// Flags (that can also be env vars)
localFlag = "local"
fileFlag = "file"
addressFlag = "address"
)

Expand Down Expand Up @@ -81,7 +87,10 @@ func main() {
defer cancel()

if ctlConf.IsLocal {
sdk.RegisterSDKServer(grpcServer, gameservers.NewLocalSDKServer())
err = registerLocal(grpcServer, ctlConf)
if err != nil {
logger.WithError(err).Fatal("Could not start local sdk server")
}
} else {
var config *rest.Config
config, err = rest.InClusterConfig()
Expand Down Expand Up @@ -124,6 +133,41 @@ func main() {
logger.Info("shutting down sdk server")
}

func registerLocal(grpcServer *grpc.Server, ctlConf config) error {
var local *gameservers.LocalSDKServer
if ctlConf.LocalFile != "" {
path, err := filepath.Abs(ctlConf.LocalFile)
if err != nil {
return err
}

if _, err = os.Stat(path); os.IsNotExist(err) {
return errors.Errorf("Could not find file: %s", path)
}

logger.WithField("path", path).Info("Reading GameServer configuration")
reader, err := os.Open(path) // nolint: gosec
if err != nil {
return err
}

var gs v1alpha1.GameServer
// 4096 is what Kubernetes uses

This comment has been minimized.

Copy link
@victor-prodan

victor-prodan Aug 26, 2018

Contributor

I dont know how go works, but for c++ I would ask to put this into a constant somewhere, or even better import an existing constant from Kubernetes (If it exists)

This comment has been minimized.

Copy link
@markmandel

markmandel Aug 26, 2018

Author Member

No such constant in Kubernetes 😢
https://github.com/kubernetes/kubernetes/blob/master/plugin/pkg/admission/podnodeselector/admission.go#L86

@enocom WDYT?

I normally err on the side of "if it's not repeated, I don't make it a variable. If it is, then I start to look to move it.

For context, the 4096 is how many bytes the Decoder should look into the given file to determine if the file is JSON or not (looking for "{" as far as I understand it).

This comment has been minimized.

Copy link
@enocom

enocom Aug 26, 2018

Contributor

It's too bad there's not a nice constant to reference for this. So I think you've done the right thing here. It's worth adding the link above and some explanation of the number in a comment. Pulling it out into an unexported constant works as well, although since it's used only once a constant seems less fitting IMHO.

decoder := yaml.NewYAMLOrJSONDecoder(reader, 4096)
err = decoder.Decode(&gs)
if err != nil {
return err
}
local = gameservers.NewLocalSDKServer(&gs)
} else {
local = gameservers.NewLocalSDKServer(nil)
}

sdk.RegisterSDKServer(grpcServer, local)

return nil
}

// runGrpc runs the grpc service
func runGrpc(grpcServer *grpc.Server, lis net.Listener) {
logger.Info("Starting SDKServer grpc service...")
Expand Down Expand Up @@ -157,9 +201,11 @@ func runGateway(ctx context.Context, grpcEndpoint string, mux *gwruntime.ServeMu
// a configuration structure
func parseEnvFlags() config {
viper.SetDefault(localFlag, false)
viper.SetDefault(fileFlag, "")
viper.SetDefault(addressFlag, "localhost")
pflag.Bool(localFlag, viper.GetBool(localFlag),
"Set this, or LOCAL env, to 'true' to run this binary in local development mode. Defaults to 'false'")
pflag.StringP(fileFlag, "f", viper.GetString(fileFlag), "Set this, or FILE env var to the path of a local yaml or json file that contains your GameServer resoure configuration")
pflag.String(addressFlag, viper.GetString(addressFlag), "The Address to bind the server grpcPort to. Defaults to 'localhost")
pflag.Parse()

Expand All @@ -170,13 +216,15 @@ func parseEnvFlags() config {
runtime.Must(viper.BindPFlags(pflag.CommandLine))

return config{
IsLocal: viper.GetBool(localFlag),
Address: viper.GetString(addressFlag),
IsLocal: viper.GetBool(localFlag),
Address: viper.GetString(addressFlag),
LocalFile: viper.GetString(fileFlag),
}
}

// config is all the configuration for this program
type config struct {
Address string
IsLocal bool
Address string
IsLocal bool
LocalFile string
}
19 changes: 14 additions & 5 deletions pkg/gameservers/localsdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"io"
"time"

"agones.dev/agones/pkg/apis/stable/v1alpha1"
"agones.dev/agones/pkg/sdk"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
Expand All @@ -27,7 +28,7 @@ import (
var (
_ sdk.SDKServer = &LocalSDKServer{}

fixture = &sdk.GameServer{
defaultGs = &sdk.GameServer{
ObjectMeta: &sdk.GameServer_ObjectMeta{
Name: "local",
Namespace: "default",
Expand All @@ -50,14 +51,22 @@ var (
// is being run for local development, and doesn't connect to the
// Kubernetes cluster
type LocalSDKServer struct {
gs *sdk.GameServer
watchPeriod time.Duration
}

// NewLocalSDKServer returns the default LocalSDKServer
func NewLocalSDKServer() *LocalSDKServer {
return &LocalSDKServer{
func NewLocalSDKServer(gs *v1alpha1.GameServer) *LocalSDKServer {
lss := &LocalSDKServer{
watchPeriod: 5 * time.Second,
gs: defaultGs,
}

if gs != nil {
lss.gs = convert(gs)
}

return lss
}

// Ready logs that the Ready request has been received
Expand Down Expand Up @@ -90,7 +99,7 @@ func (l *LocalSDKServer) Health(stream sdk.SDK_HealthServer) error {
// GetGameServer returns a dummy game server.
func (l *LocalSDKServer) GetGameServer(context.Context, *sdk.Empty) (*sdk.GameServer, error) {
logrus.Info("getting GameServer details")
return fixture, nil
return l.gs, nil
}

// WatchGameServer will return a dummy GameServer (with no changes), 3 times, every 5 seconds
Expand All @@ -100,7 +109,7 @@ func (l *LocalSDKServer) WatchGameServer(_ *sdk.Empty, stream sdk.SDK_WatchGameS

for i := 0; i < times; i++ {
logrus.Info("Sending watched GameServer!")
err := stream.Send(fixture)
err := stream.Send(l.gs)
if err != nil {
logrus.WithError(err).Error("error sending gameserver")
return err
Expand Down
22 changes: 18 additions & 4 deletions pkg/gameservers/localsdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ import (
"testing"
"time"

"agones.dev/agones/pkg/apis/stable/v1alpha1"
"agones.dev/agones/pkg/sdk"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestLocal(t *testing.T) {
ctx := context.Background()
e := &sdk.Empty{}
l := NewLocalSDKServer()
l := NewLocalSDKServer(nil)

_, err := l.Ready(ctx, e)
assert.Nil(t, err, "Ready should not error")
Expand All @@ -53,14 +55,26 @@ func TestLocal(t *testing.T) {
gs, err := l.GetGameServer(ctx, e)
assert.Nil(t, err)

assert.Equal(t, fixture, gs)
assert.Equal(t, defaultGs, gs)
}

func TestLocalSDKWithGameServer(t *testing.T) {
ctx := context.Background()
e := &sdk.Empty{}

fixture := &v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "stuff"}}
l := NewLocalSDKServer(fixture.DeepCopy())
gs, err := l.GetGameServer(ctx, e)
assert.Nil(t, err)

assert.Equal(t, fixture.ObjectMeta.Name, gs.ObjectMeta.Name)
}

func TestLocalSDKServerWatchGameServer(t *testing.T) {
t.Parallel()

e := &sdk.Empty{}
l := NewLocalSDKServer()
l := NewLocalSDKServer(nil)
l.watchPeriod = time.Second

stream := newGameServerMockStream()
Expand All @@ -70,7 +84,7 @@ func TestLocalSDKServerWatchGameServer(t *testing.T) {
for i := 0; i < 3; i++ {
select {
case msg := <-stream.msgs:
assert.Equal(t, fixture, msg)
assert.Equal(t, defaultGs, msg)
case <-time.After(2 * l.watchPeriod):
assert.FailNow(t, "timeout on receiving messagess")
}
Expand Down
65 changes: 65 additions & 0 deletions pkg/gameservers/sdk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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 gameservers

import (
"agones.dev/agones/pkg/apis/stable/v1alpha1"
"agones.dev/agones/pkg/sdk"
)

// convert converts a K8s GameServer object, into a gRPC SDK GameServer object
func convert(gs *v1alpha1.GameServer) *sdk.GameServer {
meta := gs.ObjectMeta
status := gs.Status
health := gs.Spec.Health
result := &sdk.GameServer{
ObjectMeta: &sdk.GameServer_ObjectMeta{
Name: meta.Name,
Namespace: meta.Namespace,
Uid: string(meta.UID),
ResourceVersion: meta.ResourceVersion,
Generation: meta.Generation,
CreationTimestamp: meta.CreationTimestamp.Unix(),
Annotations: meta.Annotations,
Labels: meta.Labels,
},
Spec: &sdk.GameServer_Spec{
Health: &sdk.GameServer_Spec_Health{
Disabled: health.Disabled,
PeriodSeconds: health.PeriodSeconds,
FailureThreshold: health.FailureThreshold,
InitialDelaySeconds: health.InitialDelaySeconds,
},
},
Status: &sdk.GameServer_Status{
State: string(status.State),
Address: status.Address,
},
}
if meta.DeletionTimestamp != nil {
result.ObjectMeta.DeletionTimestamp = meta.DeletionTimestamp.Unix()
}

// loop around and add all the ports
for _, p := range status.Ports {
grpcPort := &sdk.GameServer_Status_Port{
Name: p.Name,
Port: p.Port,
}
result.Status.Ports = append(result.Status.Ports, grpcPort)
}

return result
}
87 changes: 87 additions & 0 deletions pkg/gameservers/sdk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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 gameservers

import (
"testing"

"agones.dev/agones/pkg/apis/stable/v1alpha1"
"agones.dev/agones/pkg/sdk"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestConvert(t *testing.T) {
t.Parallel()

fixture := &v1alpha1.GameServer{
ObjectMeta: v1.ObjectMeta{
CreationTimestamp: v1.Now(),
Namespace: "default",
Name: "test",
Labels: map[string]string{"foo": "bar"},
Annotations: map[string]string{"stuff": "things"},
UID: "1234",
},
Spec: v1alpha1.GameServerSpec{
Health: v1alpha1.Health{
Disabled: false,
InitialDelaySeconds: 10,
FailureThreshold: 15,
PeriodSeconds: 20,
},
},
Status: v1alpha1.GameServerStatus{
NodeName: "george",
Address: "127.0.0.1",
State: "Ready",
Ports: []v1alpha1.GameServerStatusPort{
{Name: "default", Port: 12345},
{Name: "beacon", Port: 123123},
},
},
}

eq := func(t *testing.T, fixture *v1alpha1.GameServer, sdkGs *sdk.GameServer) {
assert.Equal(t, fixture.ObjectMeta.Name, sdkGs.ObjectMeta.Name)
assert.Equal(t, fixture.ObjectMeta.Namespace, sdkGs.ObjectMeta.Namespace)
assert.Equal(t, fixture.ObjectMeta.CreationTimestamp.Unix(), sdkGs.ObjectMeta.CreationTimestamp)
assert.Equal(t, string(fixture.ObjectMeta.UID), sdkGs.ObjectMeta.Uid)
assert.Equal(t, fixture.ObjectMeta.Labels, sdkGs.ObjectMeta.Labels)
assert.Equal(t, fixture.ObjectMeta.Annotations, sdkGs.ObjectMeta.Annotations)
assert.Equal(t, fixture.Spec.Health.Disabled, sdkGs.Spec.Health.Disabled)
assert.Equal(t, fixture.Spec.Health.InitialDelaySeconds, sdkGs.Spec.Health.InitialDelaySeconds)
assert.Equal(t, fixture.Spec.Health.FailureThreshold, sdkGs.Spec.Health.FailureThreshold)
assert.Equal(t, fixture.Spec.Health.PeriodSeconds, sdkGs.Spec.Health.PeriodSeconds)
assert.Equal(t, fixture.Status.Address, sdkGs.Status.Address)
assert.Equal(t, string(fixture.Status.State), sdkGs.Status.State)
assert.Len(t, sdkGs.Status.Ports, len(fixture.Status.Ports))
for i, fp := range fixture.Status.Ports {
p := sdkGs.Status.Ports[i]
assert.Equal(t, fp.Name, p.Name)
assert.Equal(t, fp.Port, p.Port)
}
}

sdkGs := convert(fixture)
eq(t, fixture, sdkGs)
assert.Zero(t, sdkGs.ObjectMeta.DeletionTimestamp)

now := v1.Now()
fixture.DeletionTimestamp = &now
sdkGs = convert(fixture)
eq(t, fixture, sdkGs)
assert.Equal(t, fixture.ObjectMeta.DeletionTimestamp.Unix(), sdkGs.ObjectMeta.DeletionTimestamp)
}
Loading

4 comments on commit e784314

@victor-prodan
Copy link
Contributor

@victor-prodan victor-prodan commented on e784314 Aug 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also not sure about something - are changes to the input file watched and applied? 🤔

Or is there some way to emulate an allocation?

@markmandel
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version doesn't have the watch and apply (yet) I just ran out of time. We still have that tracked in #314

@markmandel
Copy link
Member Author

@markmandel markmandel commented on e784314 Aug 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now you could pass in a gameserver.yaml file that already is set to state Allocated -- not quite ideal, but a step in the right direction.

@markmandel
Copy link
Member Author

@markmandel markmandel commented on e784314 Aug 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly worth noting that #323 has a good chunk of the machinery that will make #314 possible, since that is setup so that changes to labels and annotations will fire WatchGameServer events when in local mode - just like it would when running on a k8s cluster. So need to wait until that gets merged before we can do #314

Please sign in to comment.