Skip to content

Commit 081570b

Browse files
committed
feat: Add server DNS record store
Store will keep track of DNS records for all servers running the in the k8s cluster using the shared informer. Create endpoints for gameserver will allow for custom subdomain names for server. The record store will be used to avoid conflicts
1 parent 5284477 commit 081570b

5 files changed

Lines changed: 191 additions & 18 deletions

File tree

‎api/config/config.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const (
2121
REDIS_ADDRESS = "REDIS_ADDRESS"
2222
REDIS_PASSWORD = "REDIS_PASSWORD"
2323
JWT_SECRET = "JWT_SECRET"
24+
DNS_ZONE = "DNS_ZONE"
2425

2526
Production = "production"
2627
Development = "development"
@@ -81,3 +82,7 @@ func GetJWTSecret() string {
8182
func GetRedisCreds() (address string, password string) {
8283
return viper.GetString(REDIS_ADDRESS), viper.GetString(REDIS_PASSWORD)
8384
}
85+
86+
func GetDNSZone() string {
87+
return viper.GetString(DNS_ZONE)
88+
}

‎api/controllers/api/v1/game.go‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/gin-gonic/gin"
88

99
"agones-minecraft/resource/api/v1/errors"
10+
"agones-minecraft/resource/api/v1/game"
1011
"agones-minecraft/services/k8s/agones"
1112
)
1213

@@ -30,20 +31,54 @@ func GetGame(c *gin.Context) {
3031
}
3132

3233
func CreateJava(c *gin.Context) {
33-
gameServer, err := agones.Client().Create(agones.NewJavaServer())
34+
var body game.CreateUserBody
35+
if err := c.ShouldBindJSON(&body); err != nil {
36+
c.Errors = append(c.Errors, errors.NewBadRequestError(err))
37+
return
38+
}
39+
40+
gs := agones.NewJavaServer()
41+
42+
if body.CustomSubdomain != "" {
43+
if ok := agones.Client().HostnameAvailable(agones.GetDNSZone(), body.CustomSubdomain); !ok {
44+
c.Errors = append(c.Errors, errors.NewBadRequestError(fmt.Errorf("custom subdomain %s not available", body.CustomSubdomain)))
45+
return
46+
}
47+
agones.SetHostname(gs, agones.GetDNSZone(), body.CustomSubdomain)
48+
}
49+
50+
gameServer, err := agones.Client().Create(gs)
3451
if err != nil {
3552
c.Errors = append(c.Errors, errors.NewInternalServerError(err))
3653
return
3754
}
55+
3856
c.JSON(http.StatusCreated, gameServer)
3957
}
4058

4159
func CreateBedrock(c *gin.Context) {
42-
gameServer, err := agones.Client().Create(agones.NewBedrockServer())
60+
var body game.CreateUserBody
61+
if err := c.ShouldBindJSON(&body); err != nil {
62+
c.Errors = append(c.Errors, errors.NewBadRequestError(err))
63+
return
64+
}
65+
66+
gs := agones.NewBedrockServer()
67+
68+
if body.CustomSubdomain != "" {
69+
if ok := agones.Client().HostnameAvailable(agones.GetDNSZone(), body.CustomSubdomain); !ok {
70+
c.Errors = append(c.Errors, errors.NewBadRequestError(fmt.Errorf("custom subdomain %s not available", body.CustomSubdomain)))
71+
return
72+
}
73+
agones.SetHostname(gs, agones.GetDNSZone(), body.CustomSubdomain)
74+
}
75+
76+
gameServer, err := agones.Client().Create(gs)
4377
if err != nil {
4478
c.Errors = append(c.Errors, errors.NewInternalServerError(err))
4579
return
4680
}
81+
4782
c.JSON(http.StatusCreated, gameServer)
4883
}
4984

‎api/resource/api/v1/game/game.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package game
2+
3+
type CreateUserBody struct {
4+
CustomSubdomain string `json:"customSubdomain" binding:"hostname_rfc1123"`
5+
}

‎api/services/k8s/agones/agones.go‎

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"k8s.io/client-go/rest"
1616
"k8s.io/client-go/tools/cache"
1717

18+
"agones-minecraft/config"
1819
k8s "agones-minecraft/services/k8s"
1920
)
2021

@@ -58,14 +59,26 @@ const (
5859
// volumes
5960

6061
DefaultDataVolumeName string = "world-vol"
62+
63+
// annotations
64+
65+
HostnameAnnotation string = "external-dns.alpha.kubernetes.io/hostname"
66+
SRVServiceAnnotation string = "external-dns.alpha.kubernetes.io/gameserver-service"
67+
JavaSRVServiceName string = "minecraft"
68+
CustomSubdomainAnnotation string = "external-dns.alpha.kubernetes.io/gameserver-subdomain"
69+
70+
// labels
71+
72+
EditionLabel string = "edition"
6173
)
6274

6375
var agonesClient *AgonesClient
6476

6577
// Agones clientset wrapper
6678
type AgonesClient struct {
67-
clientSet *versioned.Clientset
68-
informer v1Informers.GameServerInformer
79+
clientSet *versioned.Clientset
80+
informer v1Informers.GameServerInformer
81+
recordStore GameServerDNSRecordStore
6982
}
7083

7184
// Initializes Agones client
@@ -88,30 +101,22 @@ func Client() *AgonesClient {
88101
func New(config *rest.Config) (*AgonesClient, error) {
89102
agonesClient, err := versioned.NewForConfig(config)
90103
gameServerInformer := NewGameServerInformer(agonesClient)
104+
recordStore := NewGameServerDNSRecordStore(gameServerInformer)
91105
if err != nil {
92106
return nil, err
93107
}
94108

95-
return &AgonesClient{agonesClient, gameServerInformer}, nil
109+
return &AgonesClient{agonesClient, gameServerInformer, recordStore}, nil
96110
}
97111

98112
// Gets a GameServer by name
99113
func (c *AgonesClient) Get(serverName string) (*agonesv1.GameServer, error) {
100114
return c.informer.Lister().GameServers(metav1.NamespaceDefault).Get(serverName)
101-
102-
// return c.clientSet.
103-
// AgonesV1().
104-
// GameServers(metav1.NamespaceDefault).
105-
// Get(context.Background(), serverName, metav1.GetOptions{})
106115
}
107116

108117
// Gets all GameServers for default namespace
109118
func (c *AgonesClient) List() ([]*agonesv1.GameServer, error) {
110119
return c.informer.Lister().GameServers(metav1.NamespaceDefault).List(labels.Everything())
111-
// return c.clientSet.
112-
// AgonesV1().
113-
// GameServers(metav1.NamespaceDefault).
114-
// List(context.Background(), metav1.ListOptions{})
115120
}
116121

117122
// Creates a new GameServer
@@ -130,15 +135,58 @@ func (c *AgonesClient) Delete(serverName string) error {
130135
Delete(context.Background(), serverName, metav1.DeleteOptions{})
131136
}
132137

138+
func (c *AgonesClient) HostnameAvailable(domain, subdomain string) bool {
139+
hostname := fmt.Sprintf("%s.%s", subdomain, domain)
140+
_, ok := c.recordStore.Get(hostname)
141+
return !ok
142+
}
143+
144+
func (c *AgonesClient) ListRecords() []string {
145+
return c.recordStore.List()
146+
}
147+
148+
func SetHostname(gs *agonesv1.GameServer, domain, subdomain string) {
149+
anno := gs.GetAnnotations()
150+
anno[CustomSubdomainAnnotation] = subdomain
151+
anno[HostnameAnnotation] = domain
152+
gs.SetAnnotations(anno)
153+
}
154+
155+
func GetDomainName(gs *agonesv1.GameServer) string {
156+
return gs.Annotations[HostnameAnnotation]
157+
}
158+
159+
func GetSubdomain(gs *agonesv1.GameServer) (string, bool) {
160+
v, ok := gs.Annotations[CustomSubdomainAnnotation]
161+
return v, ok
162+
}
163+
164+
func GetHostname(gs *agonesv1.GameServer) string {
165+
subdomain := gs.Name
166+
if domain, ok := gs.Annotations[CustomSubdomainAnnotation]; ok {
167+
subdomain = domain
168+
}
169+
domain := gs.Annotations[HostnameAnnotation]
170+
return fmt.Sprintf("%s.%s", subdomain, domain)
171+
}
172+
173+
func GetDNSZone() string {
174+
return config.GetDNSZone()
175+
}
176+
133177
// Initializes a new default Java Minecraft server
134178
// Agones v1 GameServer object
135179
func NewJavaServer() *agonesv1.GameServer {
136180
return &agonesv1.GameServer{
137181
ObjectMeta: metav1.ObjectMeta{
138182
GenerateName: DefaultGenerateName,
139183
Namespace: metav1.NamespaceDefault,
184+
Annotations: map[string]string{
185+
HostnameAnnotation: GetDNSZone(),
186+
SRVServiceAnnotation: JavaSRVServiceName,
187+
},
140188
Labels: map[string]string{
141-
"edition": JavaEdition,
189+
EditionLabel: JavaEdition,
142190
},
143191
},
144192
Spec: agonesv1.GameServerSpec{
@@ -233,8 +281,12 @@ func NewBedrockServer() *agonesv1.GameServer {
233281
ObjectMeta: metav1.ObjectMeta{
234282
GenerateName: DefaultGenerateName,
235283
Namespace: metav1.NamespaceDefault,
284+
Annotations: map[string]string{
285+
HostnameAnnotation: GetDNSZone(),
286+
SRVServiceAnnotation: JavaSRVServiceName,
287+
},
236288
Labels: map[string]string{
237-
"edition": BedrockEdition,
289+
EditionLabel: BedrockEdition,
238290
},
239291
},
240292
Spec: agonesv1.GameServerSpec{
Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,87 @@
11
package agones
22

33
import (
4+
"sync"
5+
6+
agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
47
"agones.dev/agones/pkg/client/clientset/versioned"
58
"agones.dev/agones/pkg/client/informers/externalversions"
6-
v1 "agones.dev/agones/pkg/client/informers/externalversions/agones/v1"
9+
v1Informers "agones.dev/agones/pkg/client/informers/externalversions/agones/v1"
10+
"k8s.io/client-go/tools/cache"
711
)
812

9-
func NewGameServerInformer(clientset *versioned.Clientset) v1.GameServerInformer {
13+
type GameServerDNSRecordStore interface {
14+
Get(domain string) (string, bool)
15+
Set(domain string, serverName string)
16+
Delete(domain string)
17+
List() []string
18+
}
19+
20+
type gameServerDNSRecordStore struct {
21+
records map[string]string
22+
lock sync.RWMutex
23+
}
24+
25+
func NewGameServerInformer(clientset *versioned.Clientset) v1Informers.GameServerInformer {
1026
return externalversions.NewSharedInformerFactory(clientset, 0).Agones().V1().GameServers()
1127
}
28+
29+
func NewGameServerDNSRecordStore(informer v1Informers.GameServerInformer) GameServerDNSRecordStore {
30+
store := gameServerDNSRecordStore{
31+
lock: sync.RWMutex{},
32+
records: make(map[string]string),
33+
}
34+
35+
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
36+
AddFunc: func(obj interface{}) {
37+
gs := obj.(*agonesv1.GameServer)
38+
fullDomain := GetHostname(gs)
39+
store.Set(fullDomain, gs.Name)
40+
},
41+
UpdateFunc: func(oldObj, newObj interface{}) {
42+
newGs := oldObj.(*agonesv1.GameServer)
43+
oldGs := newObj.(*agonesv1.GameServer)
44+
newDomain := GetHostname(newGs)
45+
oldDomain := GetHostname(oldGs)
46+
if oldDomain != newDomain {
47+
store.Delete(oldDomain)
48+
store.Set(newDomain, newGs.Name)
49+
}
50+
},
51+
DeleteFunc: func(obj interface{}) {
52+
gs := obj.(*agonesv1.GameServer)
53+
store.Delete(GetHostname(gs))
54+
},
55+
})
56+
57+
return &store
58+
}
59+
60+
func (s *gameServerDNSRecordStore) Get(domain string) (string, bool) {
61+
s.lock.RLock()
62+
defer s.lock.RUnlock()
63+
v, ok := s.records[domain]
64+
return v, ok
65+
}
66+
67+
func (s *gameServerDNSRecordStore) Set(domain string, serverName string) {
68+
s.lock.Lock()
69+
defer s.lock.Unlock()
70+
s.records[domain] = serverName
71+
}
72+
73+
func (s *gameServerDNSRecordStore) Delete(domain string) {
74+
s.lock.Lock()
75+
defer s.lock.Unlock()
76+
delete(s.records, domain)
77+
}
78+
79+
func (s *gameServerDNSRecordStore) List() []string {
80+
s.lock.RLock()
81+
defer s.lock.RUnlock()
82+
list := make([]string, 0, len(s.records))
83+
for rec := range s.records {
84+
list = append(list, rec)
85+
}
86+
return list
87+
}

0 commit comments

Comments
 (0)