diff --git a/pkg/sdkserver/localsdk.go b/pkg/sdkserver/localsdk.go index 3b54a9fbd8..37c2948cec 100644 --- a/pkg/sdkserver/localsdk.go +++ b/pkg/sdkserver/localsdk.go @@ -349,6 +349,33 @@ func (l *LocalSDKServer) SetAnnotation(_ context.Context, kv *sdk.KeyValue) (*sd return &sdk.Empty{}, nil } +// SetAnnotations applies multiple Annotations to the backing GameServer metadata +func (l *LocalSDKServer) SetAnnotations(_ context.Context, kvs *sdk.KeyValues) (*sdk.Empty, error) { + l.logger.WithField("values", kvs).Info("Setting annotations") + l.gsMutex.Lock() + defer l.gsMutex.Unlock() + + if l.gs.ObjectMeta == nil { + l.gs.ObjectMeta = &sdk.GameServer_ObjectMeta{} + } + if l.gs.ObjectMeta.Annotations == nil { + l.gs.ObjectMeta.Annotations = map[string]string{} + } + + var val strings.Builder + for _, kv := range kvs.KeyValues { + l.gs.ObjectMeta.Annotations[metadataPrefix+kv.Key] = kv.Value + + val.WriteString(kv.Key) + val.WriteString("=") + val.WriteString(kv.Value) + val.WriteString("\n") + } + l.update <- struct{}{} + l.recordRequestWithValue("setannotations", val.String(), "UID") + return &sdk.Empty{}, nil +} + // GetGameServer returns current GameServer configuration. func (l *LocalSDKServer) GetGameServer(context.Context, *sdk.Empty) (*sdk.GameServer, error) { l.logger.Info("Getting GameServer details") diff --git a/pkg/sdkserver/localsdk_test.go b/pkg/sdkserver/localsdk_test.go index 44d310b2ce..2d6134b298 100644 --- a/pkg/sdkserver/localsdk_test.go +++ b/pkg/sdkserver/localsdk_test.go @@ -281,6 +281,84 @@ func TestLocalSDKServerSetAnnotation(t *testing.T) { } } +// nolint:dupl +func TestLocalSDKServerSetAnnotations(t *testing.T) { + t.Parallel() + + fixtures := map[string]struct { + gs *agonesv1.GameServer + }{ + "default": { + gs: nil, + }, + "no annotation": { + gs: &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "empty"}}, + }, + "empty": { + gs: &agonesv1.GameServer{}, + }, + } + + for k, v := range fixtures { + t.Run(k, func(t *testing.T) { + ctx := context.Background() + e := &sdk.Empty{} + path, err := gsToTmpFile(v.gs) + assert.Nil(t, err) + + l, err := NewLocalSDKServer(path, "") + assert.Nil(t, err) + + kvs := &sdk.KeyValues{ + KeyValues: []*sdk.KeyValue{ + {Key: "bar", Value: "foo"}, + {Key: "baz", Value: "foobar"}, + }, + } + + stream := newGameServerMockStream() + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + err := l.WatchGameServer(e, stream) + assert.Nil(t, err) + }() + assertInitialWatchUpdate(t, stream) + + // make sure length of l.updateObservers is at least 1 + err = wait.PollImmediate(time.Second, 10*time.Second, func() (bool, error) { + ret := false + l.updateObservers.Range(func(_, _ interface{}) bool { + ret = true + return false + }) + + return ret, nil + }) + assert.Nil(t, err) + + _, err = l.SetAnnotations(ctx, kvs) + assert.Nil(t, err) + + gs, err := l.GetGameServer(ctx, e) + assert.Nil(t, err) + assert.Equal(t, gs.ObjectMeta.Annotations[metadataPrefix+"bar"], "foo") + assert.Equal(t, gs.ObjectMeta.Annotations[metadataPrefix+"baz"], "foobar") + + assertWatchUpdate(t, stream, []string{"foo", "foobar"}, func(gs *sdk.GameServer) interface{} { + return []string{ + gs.ObjectMeta.Annotations[metadataPrefix+"bar"], + gs.ObjectMeta.Annotations[metadataPrefix+"baz"], + } + }) + + l.Close() + wg.Wait() + }) + } +} + func TestLocalSDKServerWatchGameServer(t *testing.T) { t.Parallel() diff --git a/pkg/sdkserver/sdkserver.go b/pkg/sdkserver/sdkserver.go index 641bc3e90b..bcff2c94f0 100644 --- a/pkg/sdkserver/sdkserver.go +++ b/pkg/sdkserver/sdkserver.go @@ -598,6 +598,21 @@ func (s *SDKServer) SetAnnotation(_ context.Context, kv *sdk.KeyValue) (*sdk.Emp return &sdk.Empty{}, nil } +// SetAnnotations adds the Key/Values to be used to set the annotations with the metadataPrefix to the `GameServer` +// metdata +func (s *SDKServer) SetAnnotations(_ context.Context, kvs *sdk.KeyValues) (*sdk.Empty, error) { + s.logger.WithField("values", kvs).Debug("Adding SetAnnotations to queue") + + s.gsUpdateMutex.Lock() + for _, kv := range kvs.KeyValues { + s.gsAnnotations[kv.Key] = kv.Value + } + s.gsUpdateMutex.Unlock() + + s.workerqueue.Enqueue(cache.ExplicitKey(string(updateAnnotation))) + return &sdk.Empty{}, nil +} + // GetGameServer returns the current GameServer configuration and state from the backing GameServer CRD func (s *SDKServer) GetGameServer(context.Context, *sdk.Empty) (*sdk.GameServer, error) { s.logger.Debug("Received GetGameServer request") diff --git a/pkg/sdkserver/sdkserver_test.go b/pkg/sdkserver/sdkserver_test.go index 441e80a90c..19008cc877 100644 --- a/pkg/sdkserver/sdkserver_test.go +++ b/pkg/sdkserver/sdkserver_test.go @@ -140,6 +140,25 @@ func TestSidecarRun(t *testing.T) { metadataPrefix + "test-2": "annotation-2"}, }, }, + "annotations": { + f: func(sc *SDKServer, ctx context.Context) { + _, err := sc.SetAnnotations(ctx, &sdk.KeyValues{KeyValues: []*sdk.KeyValue{ + {Key: "test-1", Value: "annotation-1"}, + {Key: "test-2", Value: "annotation-2"}, + }}) + assert.Nil(t, err) + _, err = sc.SetAnnotations(ctx, &sdk.KeyValues{KeyValues: []*sdk.KeyValue{ + {Key: "test-3", Value: "annotation-3"}, + }}) + assert.Nil(t, err) + }, + expected: expected{ + annotations: map[string]string{ + metadataPrefix + "test-1": "annotation-1", + metadataPrefix + "test-2": "annotation-2", + metadataPrefix + "test-3": "annotation-3"}, + }, + }, "allocated": { f: func(sc *SDKServer, ctx context.Context) { _, err := sc.Allocate(ctx, &sdk.Empty{}) diff --git a/proto/sdk/sdk.proto b/proto/sdk/sdk.proto index 6504c64654..a933846ac0 100644 --- a/proto/sdk/sdk.proto +++ b/proto/sdk/sdk.proto @@ -91,6 +91,14 @@ service SDK { }; } + // Apply multiple Annotations to the backing GameServer metadata + rpc SetAnnotations(KeyValues) returns (Empty) { + option (google.api.http) = { + put: "/metadata/annotations" + body: "*" + }; + } + // Marks the GameServer as the Reserved state for Duration rpc Reserve(Duration) returns (Empty) { option (google.api.http) = { @@ -110,6 +118,15 @@ message KeyValue { string value = 2; } +message KeyValues { + repeated KeyValue key_values = 1; +} + +// Key entry +message Key { + string key = 1; +} + // time duration, in seconds message Duration { int64 seconds = 1;