Skip to content

Commit

Permalink
fix: Agones fixes (#1469)
Browse files Browse the repository at this point in the history
* restore old connection parameters

* updated game manager with ingress and services

* fix local aimmo, hope the deployed one still works

* Merge branch 'development' into agones3

* fix game creator tests

* fix aimmo tests

* increase test coverage

* try removing fixes from root codecov
  • Loading branch information
razvan-pro committed Feb 23, 2021
1 parent 681d4b8 commit b9208c0
Show file tree
Hide file tree
Showing 10 changed files with 396 additions and 239 deletions.
3 changes: 0 additions & 3 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ coverage:
round: down
range: "70...100"

fixes:
- "aimmo-game-creator/::"

ignore:
- ".travis.yml"
- "aimmo_setup.py"
Expand Down
2 changes: 1 addition & 1 deletion aimmo-game-creator/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ docker = "*"
google-cloud-logging = "*"

[requires]
python_version = "3.7.7"
python_version = "3.7"

[dev-packages]
black = "==20.8b1"
Expand Down
421 changes: 237 additions & 184 deletions aimmo-game-creator/Pipfile.lock

Large diffs are not rendered by default.

104 changes: 98 additions & 6 deletions aimmo-game-creator/game_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,19 @@ def delete_game(self, game_id):
def recreate_game(self, game_to_add):
"""Deletes and recreates the given game"""
game_id, game_data = game_to_add
LOGGER.info("Deleting game {}".format(game_data["name"]))
LOGGER.info("Deleting game {}".format(game_id))
try:
self.delete_game(game_id)
except Exception as ex:
LOGGER.error("Failed to delete game {}".format(game_data["name"]))
LOGGER.error("Failed to delete game {}".format(game_id))
LOGGER.exception(ex)

LOGGER.info("Recreating game {}".format(game_data["name"]))
LOGGER.info("Recreating game {}".format(game_id))
try:
game_data["GAME_API_URL"] = "{}{}/".format(self.games_url, game_id)
self.create_game(game_id, game_data)
except Exception as ex:
LOGGER.error("Failed to create game {}".format(game_data["name"]))
LOGGER.error("Failed to create game {}".format(game_id))
LOGGER.exception(ex)

def update(self):
Expand Down Expand Up @@ -154,7 +154,7 @@ def run(self):
while True:
self.update()
LOGGER.info("Sleeping")
time.sleep(10)
time.sleep(1)

def _parallel_map(self, func, iterable_args):
with futures.ThreadPoolExecutor() as executor:
Expand All @@ -172,6 +172,12 @@ def __init__(self, *args, **kwargs):
self.custom_objects_api: CustomObjectsApi = CustomObjectsApi(self.api_client)

super(KubernetesGameManager, self).__init__(*args, **kwargs)
self._create_ingress_paths_for_existing_games()

def _create_ingress_paths_for_existing_games(self):
games = self._data.get_games()
for game_id in games:
self._add_path_to_ingress(game_id)

@staticmethod
def _create_game_name(game_id):
Expand All @@ -182,6 +188,88 @@ def _create_game_name(game_id):
"""
return "game-{}".format(game_id)

def _add_path_to_ingress(self, game_id):
game_name = KubernetesGameManager._create_game_name(game_id)
backend = kubernetes.client.NetworkingV1beta1IngressBackend(game_name, 80)
path = kubernetes.client.NetworkingV1beta1HTTPIngressPath(
backend, f"/{game_name}(/|$)(.*)"
)

patch = [{"op": "add", "path": "/spec/rules/0/http/paths/-", "value": path}]

self.networking_api.patch_namespaced_ingress("aimmo-ingress", "default", patch)

def _remove_path_from_ingress(self, game_id):
game_name = KubernetesGameManager._create_game_name(game_id)
backend = kubernetes.client.NetworkingV1beta1IngressBackend(game_name, 80)
path = kubernetes.client.NetworkingV1beta1HTTPIngressPath(
backend, f"/{game_name}(/|$)(.*)"
)
ingress = self.networking_api.list_namespaced_ingress("default").items[0]
paths = ingress.spec.rules[0].http.paths
try:
index_to_delete = paths.index(path)
except ValueError:
return

patch = [
{
"op": "remove",
"path": "/spec/rules/0/http/paths/{}".format(index_to_delete),
}
]

self.networking_api.patch_namespaced_ingress("aimmo-ingress", "default", patch)

def _create_game_service(self, game_id):
result = self.custom_objects_api.list_namespaced_custom_object(
group="agones.dev",
version="v1",
namespace="default",
plural="gameservers",
label_selector=f"game-id={game_id}",
)
game_servers = result["items"]

if len(game_servers) == 0:
raise Exception(f"No game server found for game ID {game_id}.")
elif len(game_servers) > 1:
raise Exception(f"More than one game server found for game ID {game_id}.")

game_server = game_servers[0]
game_server_name = game_server["metadata"]["name"]

service_manifest = kubernetes.client.V1ServiceSpec(
selector={"agones.dev/gameserver": game_server_name},
ports=[
kubernetes.client.V1ServicePort(
name="tcp", protocol="TCP", port=80, target_port=5000
)
],
)

service_metadata = kubernetes.client.V1ObjectMeta(
name=KubernetesGameManager._create_game_name(game_id),
labels={"app": "aimmo-game", "game_id": game_id},
)

service = kubernetes.client.V1Service(
metadata=service_metadata, spec=service_manifest
)
self.api.create_namespaced_service(K8S_NAMESPACE, service)

def _delete_game_service(self, game_id):
app_label = "app=aimmo-game"
game_label = "game_id={}".format(game_id)

resources = self.api.list_namespaced_service(
namespace=K8S_NAMESPACE, label_selector=",".join([app_label, game_label])
)

for resource in resources.items:
LOGGER.info("Removing service: {}".format(resource.metadata.name))
self.api.delete_namespaced_service(resource.metadata.name, K8S_NAMESPACE)

def _create_game_secret(self, game_id):
name = KubernetesGameManager._create_game_name(game_id) + "-token"
try:
Expand All @@ -199,7 +287,7 @@ def _delete_game_secret(self, game_id):
)

for resource in resources.items:
LOGGER.info("Removing: {}".format(resource.metadata.name))
LOGGER.info("Removing game secret: {}".format(resource.metadata.name))
self.api.delete_namespaced_secret(resource.metadata.name, K8S_NAMESPACE)

def _create_game_server_allocation(
Expand Down Expand Up @@ -257,9 +345,13 @@ def _delete_game_server(self, game_id):
def create_game(self, game_id, game_data):
self._create_game_secret(game_id)
self._create_game_server_allocation(game_id, game_data["worksheet_id"])
self._create_game_service(game_id)
self._add_path_to_ingress(game_id)
LOGGER.info("Game started - {}".format(game_id))

def delete_game(self, game_id):
self._remove_path_from_ingress(game_id)
self._delete_game_service(game_id)
self._delete_game_server(game_id)
self._delete_game_secret(game_id)

Expand Down
5 changes: 5 additions & 0 deletions aimmo-game-creator/tests/test_game_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,13 @@ def test_token_generation(self):
def test_adding_a_game_creates_game_allocation(self):
game_manager = KubernetesGameManager("http://test/*")
custom_objects_api = MagicMock()
custom_objects_api.list_namespaced_custom_object = MagicMock(
return_value={"items": [{"metadata": {"name": "test"}}]}
)
game_manager.custom_objects_api = custom_objects_api
game_manager.secret_creator = MagicMock()
game_manager.api = MagicMock()
game_manager.networking_api = MagicMock()
game_manager.create_game(1, {"worksheet_id": 1})

custom_objects_api.create_namespaced_custom_object.assert_called_with(
Expand Down Expand Up @@ -140,6 +144,7 @@ def test_delete_game(self):
game_manager.custom_objects_api = custom_objects_api
game_manager.secret_creator = MagicMock()
game_manager.api = MagicMock()
game_manager.networking_api = MagicMock()

game_manager.delete_game(100)

Expand Down
24 changes: 3 additions & 21 deletions aimmo/game_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
"""
from __future__ import absolute_import

from django.http import Http404
from django.shortcuts import get_object_or_404, render
from kubernetes.client.api.custom_objects_api import CustomObjectsApi
from kubernetes.client.api_client import ApiClient

from aimmo import app_settings, exceptions

Expand Down Expand Up @@ -36,30 +33,15 @@ def get_environment_connection_settings(game_id):
:param game_id: Integer with the ID of the game.
:return: A dict object with all relevant settings.
"""
game_url_base_and_path = app_settings.GAME_SERVER_URL_FUNCTION(game_id)
return {
"game_url_base": get_games_url_base(game_id),
"game_url_base": game_url_base_and_path[0],
"game_url_path": game_url_base_and_path[1],
"game_ssl_flag": app_settings.GAME_SERVER_SSL_FLAG,
"game_id": game_id,
}


def get_games_url_base(game_id: int) -> str:
api_client = ApiClient()
api_instance = CustomObjectsApi(api_client)
result = api_instance.list_namespaced_custom_object(
group="agones.dev",
version="v1",
namespace="default",
plural="gameservers",
label_selector=f"game-id={game_id}",
)
try:
game_server_status = result["items"][0]["status"]
return f"http://{game_server_status['address']}:{game_server_status['ports'][0]['port']}"
except (KeyError, IndexError):
raise Http404


def get_avatar_id_from_user(user, game_id):
"""
A helper function which will return an avatar ID that is assigned to a
Expand Down
6 changes: 1 addition & 5 deletions aimmo/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,15 +254,11 @@ def test_connection_parameters_api_call_returns_404_for_logged_out_user(self):

assert first_response.status_code == 403

@patch("aimmo.game_renderer.CustomObjectsApi")
def test_id_of_connection_parameters_same_as_games_url(self, custom_objects_api):
def test_id_of_connection_parameters_same_as_games_url(self):
"""
Ensures that the id's are consistent throughout the project. Check for ID's received
by the current_avatar URL as well as the games URL api.
"""
custom_objects_api.list_namespaced_custom_object.return_value = {
"items": [{"status": {"address": "base", "ports": [{"port": 4321}]}}]
}
user = self.user
models.Avatar(owner=user, code=self.CODE, game=self.game).save()
client = self.login()
Expand Down
26 changes: 25 additions & 1 deletion example_project/example_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@
# identified as the original program.
"""Django settings for example_project project."""
import os
import subprocess
import mimetypes

from django.http import Http404
from kubernetes.client.api.custom_objects_api import CustomObjectsApi
from kubernetes.client.api_client import ApiClient

ALLOWED_HOSTS = ["*"]

Expand Down Expand Up @@ -100,6 +102,28 @@
"django.contrib.messages.middleware.MessageMiddleware",
]


def get_game_url_base_and_path(game_id: int) -> str:
api_client = ApiClient()
api_instance = CustomObjectsApi(api_client)
result = api_instance.list_namespaced_custom_object(
group="agones.dev",
version="v1",
namespace="default",
plural="gameservers",
label_selector=f"game-id={game_id}",
)
try:
game_server_status = result["items"][0]["status"]
return (
f"http://{game_server_status['address']}:{game_server_status['ports'][0]['port']}",
"/socket.io",
)
except (KeyError, IndexError):
raise Http404


AIMMO_GAME_SERVER_URL_FUNCTION = get_game_url_base_and_path
AIMMO_GAME_SERVER_SSL_FLAG = False

try:
Expand Down
19 changes: 12 additions & 7 deletions game_frontend/src/redux/api/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,29 @@ import { fromEvent, pipe, merge } from 'rxjs'
var socketIO

const connectToGame = () =>
map(action => {
const { game_url_base: gameUrlBase, avatar_id: avatarId } = action.payload.parameters
map((action) => {
const {
game_url_base: gameUrlBase,
game_url_path: gameUrlPath,
avatar_id: avatarId,
} = action.payload.parameters
socketIO = io(gameUrlBase, {
path: gameUrlPath,
query: {
avatar_id: avatarId
}
avatar_id: avatarId,
},
})
return socketIO
})

const listenFor = (eventName, socket, action) =>
fromEvent(socket, eventName).pipe(map(event => action(event)))
fromEvent(socket, eventName).pipe(map((event) => action(event)))

const emitAction = nextAction => socketIO?.emit('action', nextAction)
const emitAction = (nextAction) => socketIO?.emit('action', nextAction)

const startListeners = () =>
pipe(
mergeMap(socket =>
mergeMap((socket) =>
merge(
listenFor('game-state', socket, gameActions.socketGameStateReceived),
listenFor('log', socket, consoleLogActions.socketConsoleLogReceived)
Expand Down
25 changes: 14 additions & 11 deletions rbac/game_creator_role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: game-creator
rules:
- apiGroups: [''] # "" indicates the core API group
resources: ['secrets']
verbs: ['get', 'list', 'create', 'update', 'patch', 'delete']
- apiGroups: ['allocation.agones.dev']
resources: ['gameserverallocations']
verbs: ['create']
- apiGroups: ['agones.dev']
resources: ['gameservers']
verbs: ['get', 'list', 'delete']
- apiGroups: [""] # "" indicates the core API group
resources: ["secrets", "services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["allocation.agones.dev"]
resources: ["gameserverallocations"]
verbs: ["create"]
- apiGroups: ["agones.dev"]
resources: ["gameservers"]
verbs: ["get", "list", "delete"]

---
kind: ClusterRoleBinding
Expand All @@ -28,8 +31,8 @@ subjects:
- kind: ServiceAccount
name: game-creator # Name is case sensitive
namespace: default
apiGroup: ''
apiGroup: ""
roleRef:
kind: ClusterRole #this must be Role or ClusterRole
name: game-creator # this must match the name of the Role or ClusterRole you wish to bind to
apiGroup: ''
apiGroup: ""

0 comments on commit b9208c0

Please sign in to comment.