Installing Servatrice on Kubernetes - Cockatrice/Cockatrice GitHub Wiki

Prerequisites

  • A working kubernetes cluster with:
    • Loadbalancer support
    • Persistent volume support
    • Cert-Manager
  • kubectl
  • helm
  • source of caffeine
  • modicum of patience

Document Scope

This document will serve as a set of general guidelines and kubernetes resource templates for bringing up your very own servatrice instance for you and your friends

Caveats

While this document does not intend to provide guidance on how to operate and secure a kubernetes cluster, best efforts are made to ensure secure defaults are utilized. For the purposes of this document, I will be using a tool known as KIND to run a kubernetes cluster on my local machine using docker.

The steps are equally applicable to other kubernetes products such as:

Step 1 - MariaDB-Operator

Our servatrice server needs a mysql-compatible database for proper operation. We will utilize MariaDB managed by mariadb-operator.

An operator is a specialized program that takes all the Expert-level knowledge of an SME and packages it into a convenient form-factor thats always running to ensure our database is reconciled to its defined state.

Lets go ahead and install the operator into our cluster

helm upgrade --install mariadb-operator mariadb-operator \
    --repo https://helm.mariadb.com/mariadb-operator \
    --namespace mariadb-system \
    --create-namespace \
    --set webhook.cert.certManager.enabled=true \
    --set certController.enabled=false \
    --set crds.enabled=true 

Explained:

  • webhook.cert.certManager.enabled=true and certController.enabled=false are set so that some neccsary certificates are automatically generated during installation using cert-manager
  • crds.enabled=true is set to ensure the resource definitions (CRDs) for managing our MariaDB instance are loaded

Step 2 - Namespace

Kubernetes groups things together using namespaces. Lets make a namespace to group all the resources for our servatrice server.

00-namespace.yaml:

apiVersion: v1
kind: Namespace
metadata:
  name:  cockatrice

and apply it to our cluster:

kubectl apply -f 00-namespace.yaml

Step 3a - Database

Next, let's define our database instance so the mariadb-operator can create it for us.

01a-mariadb.yaml

apiVersion: k8s.mariadb.com/v1alpha1
kind: MariaDB
metadata:
  name: db-servatrice # name of our MariaDB Server
  namespace: cockatrice # our namespace we defined in step 1
spec:
  rootPasswordSecretKeyRef: # Generate MariaDB root password for operator management
    name: root-servatrice
    key: password
    generate: true
  username: servatrice # MariaDB username 
  passwordSecretKeyRef: # Generate MariaDB password for username
    name: user-servatrice-password
    key: password
    generate: true
  database: servatrice # name of initial database to create on the server
  port: 3306 # Port to listen on - Default: 3306
  storage:
    size: 4Gi # Size of Persistent volume allocated to hold the database files. Adjust accordingly.
  ####
  # NOTE: adjust innodb_buffer_pool_size to 80%-100% of memory limits for better performance
  ####
  myCnf: |
    [mariadb]
    bind-address=*
    default_storage_engine=InnoDB
    binlog_format=row
    innodb_autoinc_lock_mode=2
    innodb_buffer_pool_size=410M
    max_allowed_packet=256M
  resources:
    requests:
      cpu: 100m # starting cpu request for the Database server process
      memory: 128Mi # starting memory request for the Database server process
    limits:
      cpu: 300m # top-end cpu limit for the Database server process
      memory: 512Mi # top-end memory limit for the Database server process
  metrics:
    enabled: false # Disable metrics unless you have prometheus configured in your cluster

and apply it to our cluster:

kubectl apply -f 01a-mariadb.yaml

Step 3b - Database seeding

Now we have a fresh new MariaDB server, assigned user, and database but no tables!

Let's fix that. We can create a sqljob to seed the database and accompanying configmap holding the initial .sql file

01c-sqljob.yaml

apiVersion: k8s.mariadb.com/v1alpha1
kind: SqlJob
metadata:
  name: servatrice-create-table
  namespace: cockatrice
spec:
  database: servatrice
  username: servatrice
  passwordSecretKeyRef:
    name:  user-servatrice-password
    key: password
  mariaDbRef:
    name: db-servatrice
  sqlConfigMapKeyRef:
    name: servatrice-script
    key: 'servatrice.sql'

and
01b-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: servatrice-script
  namespace: cockatrice
data:
  servatrice.sql: |
    -- Schema file for servatrice database.
    -- This schema file is using the default table prefix "cockatrice",
    -- to match the "prefix=cockatrice" default setting in servatrice.ini
    SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";

    /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
    /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
    /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
    /*!40101 SET NAMES utf8 */;

    -- Every time the database schema changes, the schema version number
    -- must be incremented. Also remember to update the corresponding
    -- number in servatrice/src/servatrice_database_interface.h

    CREATE TABLE IF NOT EXISTS `cockatrice_schema_version` (
      `version` int(7) unsigned NOT NULL,
      PRIMARY KEY  (`version`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    INSERT INTO cockatrice_schema_version VALUES(29);

    -- users and user data tables
    CREATE TABLE IF NOT EXISTS `cockatrice_users` (
      `id` int(7) unsigned zerofill NOT NULL auto_increment,
      `admin` tinyint(1) NOT NULL,
      `name` varchar(35) NOT NULL,
      `realname` varchar(255) NOT NULL,
      `password_sha512` char(120) NOT NULL,
      `email` varchar(255) NOT NULL,
      `country` char(2) NOT NULL,
      `avatar_bmp` mediumblob NOT NULL,
      `registrationDate` datetime NOT NULL,
      `active` tinyint(1) NOT NULL,
      `token` binary(16),
      `clientid` varchar(15) NOT NULL,
      `privlevel` enum("NONE","VIP","DONATOR") NOT NULL,
      `privlevelStartDate` datetime NOT NULL,
      `privlevelEndDate` datetime NOT NULL,
      `passwordLastChangedDate` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
      PRIMARY KEY  (`id`),
      UNIQUE KEY `name` (`name`),
      KEY `token` (`token`),
      KEY `email` (`email`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_decklist_files` (
      `id` int(7) unsigned zerofill NOT NULL auto_increment,
      `id_folder` int(7) unsigned zerofill NOT NULL,
      `id_user` int(7) unsigned NULL,
      `name` varchar(50) NOT NULL,
      `upload_time` datetime NOT NULL,
      `content` text NOT NULL,
      PRIMARY KEY  (`id`),
      KEY `FolderPlusUser` (`id_folder`,`id_user`),
      FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`)  ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_decklist_folders` (
      `id` int(7) unsigned zerofill NOT NULL auto_increment,
      `id_parent` int(7) unsigned zerofill NOT NULL,
      `id_user` int(7) unsigned NULL,
      `name` varchar(30) NOT NULL,
      PRIMARY KEY  (`id`),
      KEY `ParentPlusUser` (`id_parent`,`id_user`),
      FOREIGN KEY(`id_user`) REFERENCES `cockatrice_users`(`id`)  ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_ignorelist` (
      `id_user1` int(7) unsigned NOT NULL,
      `id_user2` int(7) unsigned NOT NULL,
      UNIQUE KEY `key` (`id_user1`, `id_user2`),
      FOREIGN KEY(`id_user1`) REFERENCES `cockatrice_users`(`id`)  ON DELETE CASCADE ON UPDATE CASCADE,
      FOREIGN KEY(`id_user2`) REFERENCES `cockatrice_users`(`id`)  ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_buddylist` (
      `id_user1` int(7) unsigned NOT NULL,
      `id_user2` int(7) unsigned NOT NULL,
      UNIQUE KEY `key` (`id_user1`, `id_user2`),
      FOREIGN KEY(`id_user1`) REFERENCES `cockatrice_users`(`id`)  ON DELETE CASCADE ON UPDATE CASCADE,
      FOREIGN KEY(`id_user2`) REFERENCES `cockatrice_users`(`id`)  ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    -- rooms
    CREATE TABLE IF NOT EXISTS `cockatrice_rooms` (
      `id` int(7) unsigned NOT NULL auto_increment,
      `name` varchar(50) NOT NULL,
      `descr` varchar(255) NOT NULL,
      `permissionlevel` enum('NONE','REGISTERED','MODERATOR','ADMINISTRATOR') NOT NULL,
      `privlevel` enum('NONE','PRIVILEGED','VIP','DONATOR') NOT NULL,
      `auto_join` tinyint(1) default 0,
      `join_message` varchar(255) NOT NULL,
      `chat_history_size` int(4) NOT NULL,
      `id_server` tinyint(3) NOT NULL DEFAULT 1,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_rooms_gametypes` (
      `id_room` int(7) unsigned NOT NULL,
      `name` varchar(50) NOT NULL,
      `id_server` tinyint(3) NOT NULL DEFAULT 1,
      FOREIGN KEY(`id_room`) REFERENCES `cockatrice_rooms`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    -- games
    CREATE TABLE IF NOT EXISTS `cockatrice_games` (
      `room_name` varchar(255) NOT NULL,
      `id` int(7) unsigned NOT NULL auto_increment,
      `descr` varchar(50) default NULL,
      `creator_name` varchar(35) NOT NULL,
      `password` tinyint(1) NOT NULL,
      `game_types` varchar(255) NOT NULL,
      `player_count` tinyint(3) NOT NULL,
      `time_started` datetime default NULL,
      `time_finished` datetime default NULL,
      PRIMARY KEY  (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_games_players` (
      `id_game` int(7) unsigned zerofill NOT NULL,
      `player_name` varchar(35) NOT NULL,
      FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    -- Note: an empty row with id_game = NULL is created when the game is created,
    -- and then updated when the game ends with the full replay data.
    CREATE TABLE IF NOT EXISTS `cockatrice_replays` (
      `id` int(7) NOT NULL AUTO_INCREMENT,
      `id_game` int(7) unsigned NULL,
      `duration` int(7) NOT NULL,
      `replay` mediumblob NOT NULL,
      PRIMARY KEY (`id`),
      FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_replays_access` (
      `id_game` int(7) unsigned NOT NULL,
      `id_player` int(7) unsigned NOT NULL,
      `replay_name` varchar(255) NOT NULL,
      `do_not_hide` tinyint(1) NOT NULL,
      KEY `id_player` (`id_player`),
      FOREIGN KEY(`id_game`) REFERENCES `cockatrice_games`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
      FOREIGN KEY(`id_player`) REFERENCES `cockatrice_users`(`id`)  ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    -- server administration
    -- Note: unused table
    CREATE TABLE IF NOT EXISTS `cockatrice_servers` (
      `id` mediumint(8) unsigned NOT NULL,
      `ssl_cert` text NOT NULL,
      `hostname` varchar(255) NOT NULL,
      `address` varchar(255) NOT NULL,
      `game_port` mediumint(8) unsigned NOT NULL,
      `control_port` mediumint(9) NOT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_uptime` (
      `id_server` tinyint(3) NOT NULL,
      `timest` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
      `uptime` int(11) NOT NULL,
      `users_count` int(11) NOT NULL,
      `mods_count` int(11) NOT NULL DEFAULT 0,
      `mods_list` TEXT,
      `games_count` int(11) NOT NULL,
      `rx_bytes` int(11) NOT NULL,
      `tx_bytes` int(11) NOT NULL,
      PRIMARY KEY (`timest`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_servermessages` (
      `id_server` tinyint(3) not null default 1,
      `timest` datetime NOT NULL default '0000-00-00 00:00:00',
      `message` text,
      PRIMARY KEY  (`timest`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_sessions` (
      `id` int(9) NOT NULL AUTO_INCREMENT,
      `user_name` varchar(35) NOT NULL,
      `id_server` tinyint(3) NOT NULL,
      `ip_address` varchar(45) NOT NULL,
      `start_time` datetime NOT NULL,
      `end_time` datetime DEFAULT NULL,
      `clientid` varchar(15) NOT NULL,
      `connection_type` ENUM('tcp', 'websocket'),
      PRIMARY KEY (`id`),
      KEY `username` (`user_name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    -- server moderation
    CREATE TABLE IF NOT EXISTS `cockatrice_bans` (
      `user_name` varchar(35) NOT NULL,
      `ip_address` varchar(45) NOT NULL,
      `id_admin` int(7) unsigned zerofill NOT NULL,
      `time_from` datetime NOT NULL,
      `minutes` int(6) NOT NULL,
      `reason` text NOT NULL,
      `visible_reason` text NOT NULL,
      `clientid` varchar(15) NOT NULL,
      PRIMARY KEY (`user_name`,`time_from`),
      KEY `time_from` (`time_from`,`ip_address`),
      KEY `ip_address` (`ip_address`),
      FOREIGN KEY(`id_admin`) REFERENCES `cockatrice_users`(`id`)  ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_warnings` (
      `user_id` int(7) unsigned NOT NULL,
      `user_name` varchar(35) NOT NULL,
      `mod_name` varchar(35) NOT NULL,
      `reason` text NOT NULL,
      `time_of` datetime NOT NULL,
      `clientid` varchar(15) NOT NULL,
      PRIMARY KEY (`user_id`,`time_of`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_log` (
      `log_time` datetime NOT NULL,
      `sender_id` int(7) unsigned NULL,
      `sender_name` varchar(35) NOT NULL,
      `sender_ip` varchar(45) NOT NULL,
      `log_message` text NOT NULL,
      `target_type` ENUM('room', 'game', 'chat'),
      `target_id` int(7) NULL,
      `target_name` varchar(50) NOT NULL,
      KEY `sender_name` (`sender_name`),
      KEY `sender_ip` (`sender_ip`),
      KEY `target_type` (`target_type`),
      KEY `target_id` (`target_id`),
      KEY `target_name` (`target_name`),
      FOREIGN KEY(`sender_id`) REFERENCES `cockatrice_users`(`id`)  ON DELETE CASCADE ON UPDATE CASCADE
      -- No FK on target_id, it can be zero
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_activation_emails` (
      `name` varchar(35) NOT NULL,
      FOREIGN KEY(`name`) REFERENCES `cockatrice_users`(`name`)  ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_user_analytics` (
      `id` int(7) unsigned zerofill NOT NULL,
      `client_ver` varchar(35) NOT NULL,
      `last_login` datetime NOT NULL,
      `notes` varchar(255) NOT NULL,
      PRIMARY KEY  (`id`),
      FOREIGN KEY(`id`) REFERENCES `cockatrice_users`(`id`)  ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_donations` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
      `username` varchar(35) DEFAULT NULL,
      `email` varchar(255) DEFAULT NULL,
      `payment_pre_fee` double DEFAULT NULL,
      `payment_post_fee` double DEFAULT NULL,
      `term_length` int(11) DEFAULT NULL,
      `date` varchar(255) DEFAULT NULL,
      `pp_type` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_forgot_password` (
      `id` int(7) unsigned zerofill NOT NULL auto_increment,
      `name` varchar(35) NOT NULL,
      `requestDate` datetime NOT NULL default '0000-00-00 00:00:00',
      `emailed` tinyint(1) NOT NULL default 0,
      PRIMARY KEY  (`id`),
      KEY `user_name` (`name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

    CREATE TABLE IF NOT EXISTS `cockatrice_audit` (
      `id` int(7) unsigned zerofill NOT NULL auto_increment,
      `id_server` tinyint(3) NOT NULL,
      `name` varchar(35) NOT NULL,
      `ip_address` varchar(45) NOT NULL,
      `clientid` varchar(15) NOT NULL,
      `incidentDate` datetime NOT NULL default '0000-00-00 00:00:00',
      `action` varchar(35) NOT NULL,
      `results` ENUM('fail', 'success') NOT NULL DEFAULT 'fail',
      `details` varchar(255) NOT NULL,
      PRIMARY KEY  (`id`),
      KEY `user_name` (`name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;

and apply them to our cluster:

kubectl apply -f 01b-configmap.yaml
kubectl apply -f 01c-sqljob.yaml

Step 3d - Databse Connection String

Servatrice can accept a database configuration in two ways:

  • via the configuration file
  • via the environment variable DATABASE_URL

We will be using the second method as to alleviate us from having to programmatically modify the configuration file for EVERY new server we want to setup.

We can use a Connection resourse to get a properly formatted database connection string

01d-connection.yaml

apiVersion: k8s.mariadb.com/v1alpha1
kind: Connection
metadata:
  name: connection
  namespace: cockatrice
spec:
  mariaDbRef:
    name: db-servatrice
  username: servatrice
  passwordSecretKeyRef:
    name: user-servatrice-password
    key: password
  database: servatrice
  secretName: connection-servatrice
  secretTemplate:
    metadata:
      labels:
        k8s.mariadb.com/connection: connection-servatrice
      annotations:
        k8s.mariadb.com/connection: connection-servatrice
    key: dsn
    format: mysql://{{ .Username }}:{{ .Password }}@{{ .Host }}:{{ .Port }}/{{ .Database }}{{ .Params }}
    usernameKey: username
    passwordKey: password
    hostKey: host
    portKey: port
    databaseKey: database
  healthCheck:
    interval: 30s
    retryInterval: 3s
  serviceName: db-servatrice

and apply to our cluster:

kubectl apply -f 01d-connection.yaml

Step 4 - Server Configuration File

Our servatrice server is going to need a configuration file. I will demonstrate a very basic one here that enables the following features:

Since we are connecting using DATABASE_URL, servatrice also assumes the cockatrice table prefix as well as sql mode for authentication and room creation.

Our configuration file will mirror these facts even if those sections are effectively ignored

02-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: servatrice-config
  namespace: cockatrice
data:
  servatrice.ini: |
    [server]
    name="Generic Cockatrice Server"
    web_socket_ip_header="X-Real-IP"
    id=1

    [authentication]
    method=sql
    
    [users]
    minnamelength=4
    maxnamelength=24
    
    [registration]
    enabled=true
    requireemail=false
    requireemailactivation=false
    
    [rooms]
    method=sql
    
    [game]
    store_replays=false

and apply to our cluster:

kubectl apply -f 02-configmap.yaml

Step 5 - Create Initial Room

When servatrice operates in sql mode from rooms, it does NOT create a default room.

Lets pre-populate a General room with game modes for most played formats

03a-configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: default-room-script
  namespace: cockatrice
data:
  initial_room_mtg.sql: |
    insert into cockatrice_rooms (name,descr,permissionlevel,privlevel,auto_join,join_message,chat_history_size,id_server) values ('General Room','Play anything here!','NONE','NONE',1,'Welcome to the General Room.',100,1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Standard',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Pioneer',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Modern',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Legacy',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Vintage',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Limited',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Commander',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'CEDH',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Highlander',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Pre-Modern',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Old-School',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Pauper',1);
    insert into cockatrice_rooms_gametypes (id_room,name,id_server) values (1,'Other',1);

03b-sqljob.yaml

apiVersion: k8s.mariadb.com/v1alpha1
kind: SqlJob
metadata:
  name: servatrice-create-initial-room
  namespace: cockatrice
spec:
  database: servatrice
  username: servatrice
  passwordSecretKeyRef:
    name:  user-servatrice-password
    key: password
  mariaDbRef:
    name: db-servatrice
  sqlConfigMapKeyRef:
    name: default-room-script
    key: 'initial_room_mtg.sql'

and apply them to our cluster:

kubectl apply -f 03a-configmap.yaml
kubectl apply -f 03b-sqljob.yaml

Step 6 - Deploy Servatrice

Its finally time to deploy our server

I have built and published a docker image for Servatrice v2.9.0 using the Docker file from the Github Repo docker.io/troopert/servatrice:v2.9.0-rc2

04-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: servatrice
  namespace: cockatrice
spec:
  selector:
    matchLabels:
      app: servatrice
      role: server
  template:
    metadata:
      labels:
        app: servatrice
        role: server
    spec:
      containers:
      - name: server
        image: docker.io/troopert/servatrice:v2.9.0-rc2
        command: ["servatrice", "--log-to-console", "--config", "/etc/config/servatrice.ini"]
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: connection-servatrice
              key: dsn
        resources:
          limits:
            memory: "512Mi"
            cpu: "1000m"
        ports:
        - containerPort: 4748
          name: websocket
          protocol: TCP
        volumeMounts:
          - name: config-volume
            mountPath: /etc/config
      volumes:
      - name: config-volume
        configMap:
          name: servatrice-config

and apply to our cluster:

kubectl apply -f 04-deployment.yaml

Step 7 - Service

Kubernetes utilizes a Service resource to direct traffic to our running copy of servatrice.

05-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: servatrice
  namespace: cockatrice
spec:
  selector:
    app: servatrice
    role: server
  ports:
  - port: 4748
    name: websocket
    targetPort: websocket

and apply to our cluster:

kubectl apply -f 05-service.yaml

Step 8 - Ingress

The final piece to our puzzle is going to be an Ingress resource. This is what will expose our servatrice instance to the outside world and let us enjoy with friends. This MAY require some amount of tweaking depending on the the ingress controller your kubernetes cluster utilizes as servatrice will be exposed behind a TLS-encrypted websocket.
As-IS this is written with a few pre-conceived notions:

  • The in-use ingress controller is kubernetes official ingress-nginx
  • cert-manager is configured with a ClusterIssuer named letsencrypt-production

06-ingress.yaml

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-servatrice
  namespace: cockatrice
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-production"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/server-snippets: |
      location /servatrice {
        proxy_set_header Upgrade $http_upgrade;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Host $http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header Connection "upgrade";
        proxy_cache_bypass $http_upgrade;
      }
spec:
  tls:
  - hosts:
      - mtg.example.com # Update this to a DNS hostname/domain you control
    secretName: tls-host-mtg
  rules:
  - host: mtg.example.com # Update this to a DNS hostname/domain you control
    http:
      paths:
      - path: /servatrice
        pathType: Prefix
        backend:
          service:
            name: servatrice
            port:  
              name: websocket

and apply to our cluster:

kubectl apply -f 06-ingress.yaml

If all works properly, your server should be issued an SSL certificate via cert-manager within a few minutes

Conclusion

You should now have a running servatrice instance hosted on your kubernetes cluster for you and your friends to enjoy.
Open Cockatrice, register to server with a username and password (no email required) and have tons of fun!!!

⚠️ **GitHub.com Fallback** ⚠️